From b51d818db3740068bf289fded02f80d350f6c8ad Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Mon, 13 Jul 2020 18:23:15 -0700 Subject: [PATCH] feat: Adds documents.export endpoint to return cleaned up Markdown (#1343) --- server/api/documents.js | 27 +++++- server/api/documents.test.js | 182 +++++++++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+), 5 deletions(-) diff --git a/server/api/documents.js b/server/api/documents.js index 9458ca41..1c52b487 100644 --- a/server/api/documents.js +++ b/server/api/documents.js @@ -366,11 +366,7 @@ router.post("documents.drafts", auth(), pagination(), async ctx => { }; }); -router.post("documents.info", auth({ required: false }), async ctx => { - const { id, shareId } = ctx.body; - ctx.assertPresent(id || shareId, "id or shareId is required"); - - const user = ctx.state.user; +async function loadDocument({ id, shareId, user }) { let document; if (shareId) { @@ -404,6 +400,15 @@ router.post("documents.info", auth({ required: false }), async ctx => { authorize(user, "read", document); } + return document; +} + +router.post("documents.info", auth({ required: false }), async ctx => { + const { id, shareId } = ctx.body; + ctx.assertPresent(id || shareId, "id or shareId is required"); + + const user = ctx.state.user; + const document = await loadDocument({ id, shareId, user }); const isPublic = cannot(user, "read", document); ctx.body = { @@ -412,6 +417,18 @@ router.post("documents.info", auth({ required: false }), async ctx => { }; }); +router.post("documents.export", auth({ required: false }), async ctx => { + const { id, shareId } = ctx.body; + ctx.assertPresent(id || shareId, "id or shareId is required"); + + const user = ctx.state.user; + const document = await loadDocument({ id, shareId, user }); + + ctx.body = { + data: document.toMarkdown(), + }; +}); + router.post("documents.restore", auth(), async ctx => { const { id, revisionId } = ctx.body; ctx.assertPresent(id, "id is required"); diff --git a/server/api/documents.test.js b/server/api/documents.test.js index f71c99a6..07dc61bc 100644 --- a/server/api/documents.test.js +++ b/server/api/documents.test.js @@ -210,6 +210,188 @@ describe("#documents.info", async () => { }); }); +describe("#documents.export", async () => { + it("should return published document", async () => { + const { user, document } = await seed(); + const res = await server.post("/api/documents.export", { + body: { token: user.getJwtToken(), id: document.id }, + }); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body.data).toEqual(document.toMarkdown()); + }); + + it("should return archived document", async () => { + const { user, document } = await seed(); + await document.archive(user.id); + const res = await server.post("/api/documents.export", { + body: { token: user.getJwtToken(), id: document.id }, + }); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body.data).toEqual(document.toMarkdown()); + }); + + it("should not return published document in collection not a member of", async () => { + const user = await buildUser(); + const collection = await buildCollection({ + private: true, + teamId: user.teamId, + }); + const document = await buildDocument({ collectionId: collection.id }); + + const res = await server.post("/api/documents.export", { + body: { token: user.getJwtToken(), id: document.id }, + }); + + expect(res.status).toEqual(403); + }); + + it("should return drafts", async () => { + const { user, document } = await seed(); + document.publishedAt = null; + await document.save(); + + const res = await server.post("/api/documents.export", { + body: { token: user.getJwtToken(), id: document.id }, + }); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body.data).toEqual(document.toMarkdown()); + }); + + it("should return document from shareId without token", async () => { + const { document, user } = await seed(); + const share = await buildShare({ + documentId: document.id, + teamId: document.teamId, + userId: user.id, + }); + + const res = await server.post("/api/documents.export", { + body: { shareId: share.id }, + }); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body.data).toEqual(document.toMarkdown()); + }); + + it("should not return document from revoked shareId", async () => { + const { document, user } = await seed(); + const share = await buildShare({ + documentId: document.id, + teamId: document.teamId, + userId: user.id, + }); + await share.revoke(user.id); + + const res = await server.post("/api/documents.export", { + body: { shareId: share.id }, + }); + expect(res.status).toEqual(400); + }); + + it("should not return document from archived shareId", async () => { + const { document, user } = await seed(); + const share = await buildShare({ + documentId: document.id, + teamId: document.teamId, + userId: user.id, + }); + await document.archive(user.id); + + const res = await server.post("/api/documents.export", { + body: { shareId: share.id }, + }); + expect(res.status).toEqual(400); + }); + + it("should return document from shareId with token", async () => { + const { user, document } = await seed(); + const share = await buildShare({ + documentId: document.id, + teamId: document.teamId, + userId: user.id, + }); + + const res = await server.post("/api/documents.export", { + body: { token: user.getJwtToken(), shareId: share.id }, + }); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body.data).toEqual(document.toMarkdown()); + }); + + it("should return draft document from shareId with token", async () => { + const { user, document } = await seed(); + document.publishedAt = null; + await document.save(); + + const share = await buildShare({ + documentId: document.id, + teamId: document.teamId, + userId: user.id, + }); + + const res = await server.post("/api/documents.export", { + body: { token: user.getJwtToken(), shareId: share.id }, + }); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body.data).toEqual(document.toMarkdown()); + }); + + it("should return document from shareId in collection not a member of", async () => { + const { user, document, collection } = await seed(); + const share = await buildShare({ + documentId: document.id, + teamId: document.teamId, + userId: user.id, + }); + + collection.private = true; + await collection.save(); + + const res = await server.post("/api/documents.export", { + body: { token: user.getJwtToken(), shareId: share.id }, + }); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body.data).toEqual(document.toMarkdown()); + }); + + it("should require authorization without token", async () => { + const { document } = await seed(); + const res = await server.post("/api/documents.export", { + body: { id: document.id }, + }); + expect(res.status).toEqual(403); + }); + + it("should require authorization with incorrect token", async () => { + const { document } = await seed(); + const user = await buildUser(); + const res = await server.post("/api/documents.export", { + body: { token: user.getJwtToken(), id: document.id }, + }); + expect(res.status).toEqual(403); + }); + + it("should require a valid shareId", async () => { + const res = await server.post("/api/documents.export", { + body: { shareId: 123 }, + }); + expect(res.status).toEqual(400); + }); +}); + describe("#documents.list", async () => { it("should return documents", async () => { const { user, document } = await seed();