diff --git a/helpers/mock-agent/endpoint-posts.js b/helpers/mock-agent/endpoint-posts.js index ebbf1d76d..040f65a72 100644 --- a/helpers/mock-agent/endpoint-posts.js +++ b/helpers/mock-agent/endpoint-posts.js @@ -35,37 +35,6 @@ export function mockClient() { }) .persist(); - // Get source information for all items from external micropub endpoint - agent - .get(micropubEndpointOrigin) - .intercept({ - path: "/?q=source", - }) - .reply(200, { - items: [ - { - type: ["h-entry"], - properties: { - uid: ["123"], - name: ["Foobar"], - "post-type": ["note"], - published: ["2024-12-21"], - url: [postOrigin], - }, - }, - { - type: ["h-entry"], - properties: { - uid: ["401"], - name: ["401"], - "post-type": ["note"], - url: [postBadOrigin], - }, - }, - ], - }) - .persist(); - // Upload file to external micropub endpoint agent .get(micropubEndpointOrigin) diff --git a/packages/endpoint-posts/lib/middleware/post-data.js b/packages/endpoint-posts/lib/middleware/post-data.js index 9ce94d2a5..cce69abb1 100644 --- a/packages/endpoint-posts/lib/middleware/post-data.js +++ b/packages/endpoint-posts/lib/middleware/post-data.js @@ -54,11 +54,7 @@ export const postData = { const { action, uid } = request.params; const { access_token, scope } = request.session; - const properties = await getPostProperties( - uid, - application.micropubEndpoint, - access_token, - ); + const properties = await getPostProperties(uid, application); if (!properties) { throw IndiekitError.notFound(response.locals.__("NotFoundError.page")); diff --git a/packages/endpoint-posts/lib/utils.js b/packages/endpoint-posts/lib/utils.js index 4c8a1eead..79e0cc452 100644 --- a/packages/endpoint-posts/lib/utils.js +++ b/packages/endpoint-posts/lib/utils.js @@ -1,10 +1,8 @@ import { Buffer } from "node:buffer"; -import { sanitise, ISO_6709_RE } from "@indiekit/util"; -import { mf2tojf2 } from "@paulrobertlloyd/mf2tojf2"; +import { getObjectId, sanitise, ISO_6709_RE } from "@indiekit/util"; import formatcoords from "formatcoords"; -import { endpoint } from "./endpoint.js"; import { statusTypes } from "./status-types.js"; /** @@ -151,25 +149,30 @@ export const getPostName = (publication, properties) => { }; /** - * Query Micropub endpoint for post data - * @param {string} uid - Item UID - * @param {string} micropubEndpoint - Micropub endpoint - * @param {string} accessToken - Access token - * @returns {Promise} JF2 properties + * Get post properties + * @param {string} uid - Post ID + * @param {object} application - Application configuration + * @returns {Promise} JF2 properties, else false if not found */ -export const getPostProperties = async (uid, micropubEndpoint, accessToken) => { - const micropubUrl = new URL(micropubEndpoint); - micropubUrl.searchParams.append("q", "source"); +export const getPostProperties = async (uid, application) => { + const postsCollection = application?.collections?.get("posts"); + if (!postsCollection) { + return false; + } - const micropubResponse = await endpoint.get(micropubUrl.href, accessToken); + let postData; + try { + postData = await postsCollection.findOne({ _id: getObjectId(uid) }); + } catch { + // Not a valid ObjectId + return false; + } - if (micropubResponse?.items?.length > 0) { - const jf2 = mf2tojf2(micropubResponse); - const items = jf2.children || [jf2]; - return items.find((item) => item.uid === uid); + if (!postData?.properties) { + return false; } - return false; + return { ...postData.properties, uid: postData._id.toString() }; }; /** diff --git a/packages/endpoint-posts/test/integration/200-get-delete.js b/packages/endpoint-posts/test/integration/200-get-delete.js index 65fdf1304..623cf80a6 100644 --- a/packages/endpoint-posts/test/integration/200-get-delete.js +++ b/packages/endpoint-posts/test/integration/200-get-delete.js @@ -1,22 +1,41 @@ import { strict as assert } from "node:assert"; import { after, describe, it } from "node:test"; +import { testDatabase } from "@indiekit-test/database"; import { mockAgent } from "@indiekit-test/mock-agent"; +import { postData } from "@indiekit-test/post-data"; import { testServer } from "@indiekit-test/server"; import { testCookie } from "@indiekit-test/session"; import { JSDOM } from "jsdom"; import supertest from "supertest"; await mockAgent("endpoint-posts"); +const { client, mongoServer, mongoUri } = await testDatabase(); const server = await testServer({ - application: { micropubEndpoint: "https://micropub-endpoint.example" }, + application: { + micropubEndpoint: "https://micropub-endpoint.example", + mongodbUrl: mongoUri, + }, }); const request = supertest.agent(server); +const { insertedId } = await client + .db("indiekit") + .collection("posts") + .insertOne({ + ...postData, + properties: { + ...postData.properties, + name: "Foobar", + url: "https://website.example/foobar", + }, + }); +const uid = insertedId.toString(); + describe("endpoint-posts GET /posts/:uid/delete", () => { it("Gets delete confirmation page", async () => { const response = await request - .get(`/posts/123/delete`) + .get(`/posts/${uid}/delete`) .set("cookie", testCookie()); const dom = new JSDOM(response.text); const result = dom.window.document.querySelector("title").textContent; @@ -27,5 +46,9 @@ describe("endpoint-posts GET /posts/:uid/delete", () => { ); }); - after(() => server.close()); + after(async () => { + await client.close(); + await mongoServer.stop(); + server.close((error) => process.exit(error ? 1 : 0)); + }); }); diff --git a/packages/endpoint-posts/test/integration/200-post-not-in-latest-40.js b/packages/endpoint-posts/test/integration/200-post-not-in-latest-40.js new file mode 100644 index 000000000..237aa4fec --- /dev/null +++ b/packages/endpoint-posts/test/integration/200-post-not-in-latest-40.js @@ -0,0 +1,52 @@ +import { strict as assert } from "node:assert"; +import { after, describe, it } from "node:test"; + +import { testDatabase } from "@indiekit-test/database"; +import { testServer } from "@indiekit-test/server"; +import { testCookie } from "@indiekit-test/session"; +import { JSDOM } from "jsdom"; +import supertest from "supertest"; + +const { client, mongoServer, mongoUri } = await testDatabase(); +const server = await testServer({ + application: { mongodbUrl: mongoUri }, +}); +const request = supertest.agent(server); + +// Cursor pagination returns 40 posts by default; the oldest post here is +// the 41st most recent, so it never appears in a `?q=source` listing +const { insertedIds } = await client + .db("indiekit") + .collection("posts") + .insertMany( + Array.from({ length: 41 }, (_, index) => ({ + path: `post-${index}.md`, + properties: { + name: `Post ${index}`, + "post-status": "published", + "post-type": "note", + published: new Date(2020, 0, 1 + index).toISOString(), + url: `https://website.example/post-${index}`, + }, + })), + ); +const oldestUid = insertedIds[0].toString(); + +describe("endpoint-posts GET /posts/:uid", () => { + it("Returns post that is not among the 40 most recent", async () => { + const response = await request + .get(`/posts/${oldestUid}`) + .set("cookie", testCookie()); + const dom = new JSDOM(response.text); + const result = dom.window.document.querySelector("title").textContent; + + assert.equal(response.status, 200); + assert.equal(result, "Post 0 - Test configuration"); + }); + + after(async () => { + await client.close(); + await mongoServer.stop(); + server.close((error) => process.exit(error ? 1 : 0)); + }); +}); diff --git a/packages/endpoint-posts/test/integration/200-post.js b/packages/endpoint-posts/test/integration/200-post.js index f2cf0b26d..b353d97aa 100644 --- a/packages/endpoint-posts/test/integration/200-post.js +++ b/packages/endpoint-posts/test/integration/200-post.js @@ -1,28 +1,51 @@ import { strict as assert } from "node:assert"; import { after, describe, it } from "node:test"; +import { testDatabase } from "@indiekit-test/database"; import { mockAgent } from "@indiekit-test/mock-agent"; +import { postData } from "@indiekit-test/post-data"; import { testServer } from "@indiekit-test/server"; import { testCookie } from "@indiekit-test/session"; import { JSDOM } from "jsdom"; import supertest from "supertest"; await mockAgent("endpoint-posts"); +const { client, mongoServer, mongoUri } = await testDatabase(); const server = await testServer({ - application: { micropubEndpoint: "https://micropub-endpoint.example" }, + application: { + micropubEndpoint: "https://micropub-endpoint.example", + mongodbUrl: mongoUri, + }, }); const request = supertest.agent(server); +const { insertedId } = await client + .db("indiekit") + .collection("posts") + .insertOne({ + ...postData, + properties: { + ...postData.properties, + name: "Foobar", + url: "https://website.example/foobar", + }, + }); +const uid = insertedId.toString(); + describe("endpoint-posts GET /posts/:uid", () => { it("Returns published post", async () => { const response = await request - .get(`/posts/123`) + .get(`/posts/${uid}`) .set("cookie", testCookie()); const dom = new JSDOM(response.text); const result = dom.window.document.querySelector("title").textContent; - assert.equal(result, `Foobar - Test configuration`); + assert.equal(result, "Foobar - Test configuration"); }); - after(() => server.close()); + after(async () => { + await client.close(); + await mongoServer.stop(); + server.close((error) => process.exit(error ? 1 : 0)); + }); }); diff --git a/packages/endpoint-posts/test/integration/302-get-delete.js b/packages/endpoint-posts/test/integration/302-get-delete.js index 157a13317..7f3f122ad 100644 --- a/packages/endpoint-posts/test/integration/302-get-delete.js +++ b/packages/endpoint-posts/test/integration/302-get-delete.js @@ -1,26 +1,49 @@ import { strict as assert } from "node:assert"; import { after, describe, it } from "node:test"; +import { testDatabase } from "@indiekit-test/database"; import { mockAgent } from "@indiekit-test/mock-agent"; +import { postData } from "@indiekit-test/post-data"; import { testServer } from "@indiekit-test/server"; import { testCookie } from "@indiekit-test/session"; import supertest from "supertest"; await mockAgent("endpoint-posts"); +const { client, mongoServer, mongoUri } = await testDatabase(); const server = await testServer({ - application: { micropubEndpoint: "https://micropub-endpoint.example" }, + application: { + micropubEndpoint: "https://micropub-endpoint.example", + mongodbUrl: mongoUri, + }, }); const request = supertest.agent(server); +const { insertedId } = await client + .db("indiekit") + .collection("posts") + .insertOne({ + ...postData, + properties: { + ...postData.properties, + name: "Foobar", + url: "https://website.example/foobar", + }, + }); +const uid = insertedId.toString(); + describe("endpoint-posts GET /posts/:uid/delete", () => { it("Redirects to post page if no delete permissions", async () => { const result = await request - .get(`/posts/123/delete`) + .get(`/posts/${uid}/delete`) .set("cookie", testCookie({ scope: "create" })); assert.equal(result.status, 302); assert.match(result.text, /Found. Redirecting to \/posts\/(.*)/); }); - after(() => server.close()); + after(async () => { + await client.close(); + await mongoServer.stop(); + server.close((error) => process.exit(error ? 1 : 0)); + }); }); diff --git a/packages/endpoint-posts/test/integration/302-post-delete.js b/packages/endpoint-posts/test/integration/302-post-delete.js index 5ba54e2b8..ef00dd750 100644 --- a/packages/endpoint-posts/test/integration/302-post-delete.js +++ b/packages/endpoint-posts/test/integration/302-post-delete.js @@ -1,21 +1,40 @@ import { strict as assert } from "node:assert"; import { after, describe, it } from "node:test"; +import { testDatabase } from "@indiekit-test/database"; import { mockAgent } from "@indiekit-test/mock-agent"; +import { postData } from "@indiekit-test/post-data"; import { testServer } from "@indiekit-test/server"; import { testCookie } from "@indiekit-test/session"; import supertest from "supertest"; await mockAgent("endpoint-posts"); +const { client, mongoServer, mongoUri } = await testDatabase(); const server = await testServer({ - application: { micropubEndpoint: "https://micropub-endpoint.example" }, + application: { + micropubEndpoint: "https://micropub-endpoint.example", + mongodbUrl: mongoUri, + }, }); const request = supertest.agent(server); +const { insertedId } = await client + .db("indiekit") + .collection("posts") + .insertOne({ + ...postData, + properties: { + ...postData.properties, + name: "Foobar", + url: "https://website.example/foobar", + }, + }); +const uid = insertedId.toString(); + describe("endpoint-posts POST /posts/:uid/delete", () => { it("Deletes file and redirects to posts page", async () => { const result = await request - .post(`/posts/123/delete`) + .post(`/posts/${uid}/delete`) .set("cookie", testCookie()) .send({ url: "https://website.example/foobar" }); @@ -23,5 +42,9 @@ describe("endpoint-posts POST /posts/:uid/delete", () => { assert.match(result.text, /Found. Redirecting to \/posts\?success/); }); - after(() => server.close()); + after(async () => { + await client.close(); + await mongoServer.stop(); + server.close((error) => process.exit(error ? 1 : 0)); + }); }); diff --git a/packages/endpoint-posts/test/integration/401-post-delete-unauthorized.js b/packages/endpoint-posts/test/integration/401-post-delete-unauthorized.js index d30ef5f44..576581a7c 100644 --- a/packages/endpoint-posts/test/integration/401-post-delete-unauthorized.js +++ b/packages/endpoint-posts/test/integration/401-post-delete-unauthorized.js @@ -1,22 +1,37 @@ import { strict as assert } from "node:assert"; import { after, describe, it } from "node:test"; +import { testDatabase } from "@indiekit-test/database"; import { mockAgent } from "@indiekit-test/mock-agent"; +import { postData } from "@indiekit-test/post-data"; import { testServer } from "@indiekit-test/server"; import { testCookie } from "@indiekit-test/session"; import { JSDOM } from "jsdom"; import supertest from "supertest"; await mockAgent("endpoint-posts"); +const { client, mongoServer, mongoUri } = await testDatabase(); const server = await testServer({ - application: { micropubEndpoint: "https://micropub-endpoint.example" }, + application: { + micropubEndpoint: "https://micropub-endpoint.example", + mongodbUrl: mongoUri, + }, }); const request = supertest.agent(server); -describe("endpoint-files POST /posts/:uid/delete", () => { +const { insertedId } = await client + .db("indiekit") + .collection("posts") + .insertOne({ + ...postData, + properties: { ...postData.properties, url: "https://website.example/401" }, + }); +const uid = insertedId.toString(); + +describe("endpoint-posts POST /posts/:uid/delete", () => { it("Returns 401 error deleting post", async () => { const response = await request - .post(`/posts/401/delete`) + .post(`/posts/${uid}/delete`) .set("cookie", testCookie()) .send({ url: "https://website.example/401" }); const dom = new JSDOM(response.text); @@ -28,5 +43,9 @@ describe("endpoint-files POST /posts/:uid/delete", () => { assert.match(result, /Unauthorized/); }); - after(() => server.close()); + after(async () => { + await client.close(); + await mongoServer.stop(); + server.close((error) => process.exit(error ? 1 : 0)); + }); }); diff --git a/packages/endpoint-posts/test/unit/utils.js b/packages/endpoint-posts/test/unit/utils.js index 478d5b812..c037f721a 100644 --- a/packages/endpoint-posts/test/unit/utils.js +++ b/packages/endpoint-posts/test/unit/utils.js @@ -1,6 +1,8 @@ import { strict as assert } from "node:assert"; -import { describe, it } from "node:test"; +import { after, before, describe, it } from "node:test"; +import { testDatabase } from "@indiekit-test/database"; +import { postData } from "@indiekit-test/post-data"; import { mockResponse } from "mock-req-res"; import { @@ -10,6 +12,7 @@ import { getLocationProperty, getPhotoUrl, getPostName, + getPostProperties, getPostStatusBadges, getPostUrl, getSyndicateToItems, @@ -259,6 +262,53 @@ describe("endpoint-posts/lib/utils", () => { ]); }); + describe("getPostProperties", () => { + let client; + let mongoServer; + let application; + let uid; + + before(async () => { + ({ client, mongoServer } = await testDatabase()); + const posts = client.db().collection("posts"); + application = { collections: new Map([["posts", posts]]) }; + ({ insertedId: uid } = await posts.insertOne({ ...postData })); + }); + + it("Gets post properties by ID", async () => { + const result = await getPostProperties(uid.toString(), application); + + assert.equal(result.name, "note"); + assert.equal(result.uid, uid.toString()); + }); + + it("Returns false if no post with ID", async () => { + const result = await getPostProperties( + "000000000000000000000000", + application, + ); + + assert.equal(result, false); + }); + + it("Returns false if ID is not an ObjectId", async () => { + const result = await getPostProperties("123", application); + + assert.equal(result, false); + }); + + it("Returns false if no database", async () => { + const result = await getPostProperties(uid.toString(), {}); + + assert.equal(result, false); + }); + + after(async () => { + await client.close(); + await mongoServer.stop(); + }); + }); + it("Gets post URL", () => { assert.equal( getPostUrl("aHR0cHM6Ly93ZWJzaXRlLmV4YW1wbGUvZm9vYmFy"),