import { assertEquals } from "@std/assert"; import { find, insert } from "utils/db.ts"; import { getCryptoString } from "utils/server.ts"; import { getSetCookies } from "@std/http"; let testCounter = 0; function getTestDbPath() { testCounter++; return `data/test_api_share_${testCounter}_${Date.now()}.db`; } function cleanup(dbPath: string) { try { Deno.removeSync(dbPath); } catch { // File may not exist } } function makeCtx(method: string, body?: object, cookie?: string) { const headers: Record = { "Content-Type": "application/json", }; if (cookie) headers["Cookie"] = cookie; const req = new Request("http://localhost", { method, headers, body: method === "GET" ? undefined : JSON.stringify(body || {}), }); return { req }; } async function seedUserAndToken(email: string, password: string) { const hashedPw = await getCryptoString(password, "MD5"); const user = insert("User", { name: email.split("@")[0], email, password: hashedPw, }); const userId = user[0]["id"] as string | number; const token = await getCryptoString( "share-token-" + userId + Date.now(), "MD5", ); insert("Token", { token, user_id: userId }); return { userId, token }; } Deno.test("API share - share a post", async () => { const dbPath = getTestDbPath(); Deno.env.set("POSTDOWN_DB_PATH", dbPath); try { const { userId, token } = await seedUserAndToken( "shareshare@example.com", "password", ); insert("Post", { id: "share-post-1", title: "Share Me", content: "Content", user_id: userId, shared: 0, }); const { handler } = await import("../../routes/api/share/index.tsx"); const ctx = makeCtx("POST", { id: "share-post-1", shared: true, }, `pd-user-token=${token}`); const res = await handler.POST!(ctx as any); const body = await res.json(); assertEquals(body.success, true); const post = find("Post", { id: "share-post-1" }, ["shared"]); assertEquals(post[0]["shared"], 1); } finally { Deno.env.delete("POSTDOWN_DB_PATH"); cleanup(dbPath); } }); Deno.test("API share - unshare a post", async () => { const dbPath = getTestDbPath(); Deno.env.set("POSTDOWN_DB_PATH", dbPath); try { const { userId, token } = await seedUserAndToken( "shareunshare@example.com", "password", ); insert("Post", { id: "unshare-post-1", title: "Unshare Me", content: "Content", user_id: userId, shared: 1, }); const { handler } = await import("../../routes/api/share/index.tsx"); const ctx = makeCtx("POST", { id: "unshare-post-1", shared: false, }, `pd-user-token=${token}`); const res = await handler.POST!(ctx as any); const body = await res.json(); assertEquals(body.success, true); const post = find("Post", { id: "unshare-post-1" }, ["shared"]); assertEquals(post[0]["shared"], 0); } finally { Deno.env.delete("POSTDOWN_DB_PATH"); cleanup(dbPath); } }); Deno.test("API share - without token returns error", async () => { const dbPath = getTestDbPath(); Deno.env.set("POSTDOWN_DB_PATH", dbPath); try { const { handler } = await import("../../routes/api/share/index.tsx"); const ctx = makeCtx("POST", { id: "some-post", shared: true, }); const res = await handler.POST!(ctx as any); const body = await res.json(); assertEquals(body.success, false); } finally { Deno.env.delete("POSTDOWN_DB_PATH"); cleanup(dbPath); } }); Deno.test("API share - share another user's post returns error", async () => { const dbPath = getTestDbPath(); Deno.env.set("POSTDOWN_DB_PATH", dbPath); try { const { userId: otherUserId } = await seedUserAndToken( "shareother@example.com", "password", ); insert("Post", { id: "other-post-1", title: "Other's Post", content: "Content", user_id: otherUserId, shared: 0, }); const { token: myToken } = await seedUserAndToken( "shareme@example.com", "password2", ); const { handler } = await import("../../routes/api/share/index.tsx"); const ctx = makeCtx("POST", { id: "other-post-1", shared: true, }, `pd-user-token=${myToken}`); const res = await handler.POST!(ctx as any); const body = await res.json(); assertEquals(body.success, false); } finally { Deno.env.delete("POSTDOWN_DB_PATH"); cleanup(dbPath); } }); Deno.test("API share - share with password stores hashed password", async () => { const dbPath = getTestDbPath(); Deno.env.set("POSTDOWN_DB_PATH", dbPath); try { const { userId, token } = await seedUserAndToken( "sharepw@example.com", "password", ); insert("Post", { id: "pw-post-1", title: "Password Post", content: "Secret content", user_id: userId, shared: 0, }); const { handler } = await import("../../routes/api/share/index.tsx"); const ctx = makeCtx("POST", { id: "pw-post-1", shared: true, password: "mypassword", }, `pd-user-token=${token}`); const res = await handler.POST!(ctx as any); const body = await res.json(); assertEquals(body.success, true); const post = find("Post", { id: "pw-post-1" }, [ "shared", "share_password", ]); assertEquals(post[0]["shared"], 1); const expectedHash = await getCryptoString("mypassword", "MD5"); assertEquals(post[0]["share_password"], expectedHash); } finally { Deno.env.delete("POSTDOWN_DB_PATH"); cleanup(dbPath); } }); Deno.test("API share - unshare clears password", async () => { const dbPath = getTestDbPath(); Deno.env.set("POSTDOWN_DB_PATH", dbPath); try { const { userId, token } = await seedUserAndToken( "unshpw@example.com", "password", ); const hashedPw = await getCryptoString("oldpw", "MD5"); insert("Post", { id: "pw-post-2", title: "PW Post", content: "Content", user_id: userId, shared: 1, share_password: hashedPw, }); const { handler } = await import("../../routes/api/share/index.tsx"); const ctx = makeCtx("POST", { id: "pw-post-2", shared: false, }, `pd-user-token=${token}`); const res = await handler.POST!(ctx as any); const body = await res.json(); assertEquals(body.success, true); const post = find("Post", { id: "pw-post-2" }, [ "shared", "share_password", ]); assertEquals(post[0]["shared"], 0); assertEquals(post[0]["share_password"], ""); } finally { Deno.env.delete("POSTDOWN_DB_PATH"); cleanup(dbPath); } }); Deno.test("API share-verify - correct password returns success and sets cookie", async () => { const dbPath = getTestDbPath(); Deno.env.set("POSTDOWN_DB_PATH", dbPath); try { const hashedPw = await getCryptoString("secret123", "MD5"); const { userId } = await seedUserAndToken( "verifypw@example.com", "password", ); insert("Post", { id: "verify-post-1", title: "Protected", content: "Secret", user_id: userId, shared: 1, share_password: hashedPw, }); const { handler } = await import("../../routes/api/share/verify.tsx"); const ctx = makeCtx("POST", { id: "verify-post-1", password: "secret123", }); const res = await handler.POST!(ctx as any); const body = await res.json(); assertEquals(body.success, true); const setCookies = getSetCookies(res.headers); const shareCookie = setCookies.find((c) => c.name === "pd-share-verify-post-1" ); const expectedToken = await getCryptoString( "verify-post-1" + hashedPw, "MD5", ); assertEquals(shareCookie?.value, expectedToken); } finally { Deno.env.delete("POSTDOWN_DB_PATH"); cleanup(dbPath); } }); Deno.test("API share-verify - wrong password returns error", async () => { const dbPath = getTestDbPath(); Deno.env.set("POSTDOWN_DB_PATH", dbPath); try { const hashedPw = await getCryptoString("secret123", "MD5"); const { userId } = await seedUserAndToken( "verifywrong@example.com", "password", ); insert("Post", { id: "verify-post-2", title: "Protected", content: "Secret", user_id: userId, shared: 1, share_password: hashedPw, }); const { handler } = await import("../../routes/api/share/verify.tsx"); const ctx = makeCtx("POST", { id: "verify-post-2", password: "wrongpassword", }); const res = await handler.POST!(ctx as any); const body = await res.json(); assertEquals(body.success, false); } finally { Deno.env.delete("POSTDOWN_DB_PATH"); cleanup(dbPath); } });