7a2ae8434d
Refactor documentation files and code comments to remove references to Replit, specific task numbers, and other platform-specific identifiers. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: fa18e5d4-a810-4bd5-8cde-2a60d64d9e3f Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/kI0sxlu Replit-Helium-Checkpoint-Created: true
248 lines
8.1 KiB
JavaScript
248 lines
8.1 KiB
JavaScript
// API tests for the collaborative checklist toggle endpoint
|
|
// `POST /notes/:id/checklist/:itemId/toggle`. Owner + active recipients
|
|
// can flip an item; the change is mirrored to every recipient snapshot.
|
|
import { test, after } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import pg from "pg";
|
|
|
|
const API_BASE = process.env.TEST_API_BASE ?? "http://localhost:8080";
|
|
const DATABASE_URL = process.env.DATABASE_URL;
|
|
if (!DATABASE_URL) {
|
|
throw new Error("DATABASE_URL must be set to run these tests");
|
|
}
|
|
|
|
const TEST_PASSWORD = "TestPass123!";
|
|
const TEST_PASSWORD_HASH =
|
|
"$2b$10$Bs636ukPMyz01nKrsi.5m.JlDXSN22AVCvn8cgPWWDbo5yJRQX2vu";
|
|
|
|
const pool = new pg.Pool({ connectionString: DATABASE_URL });
|
|
const createdUserIds = [];
|
|
const createdNoteIds = [];
|
|
|
|
function uniq() {
|
|
return `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
}
|
|
|
|
async function createUser(prefix) {
|
|
const username = `${prefix}_${uniq()}`;
|
|
const { rows } = await pool.query(
|
|
`INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active)
|
|
VALUES ($1, $2, $3, $4, 'en', true) RETURNING id`,
|
|
[username, `${username}@example.com`, TEST_PASSWORD_HASH, prefix],
|
|
);
|
|
const id = rows[0].id;
|
|
createdUserIds.push(id);
|
|
await pool.query(
|
|
`INSERT INTO user_roles (user_id, role_id)
|
|
SELECT $1, id FROM roles WHERE name = 'user'`,
|
|
[id],
|
|
);
|
|
return { id, username };
|
|
}
|
|
|
|
async function login(username) {
|
|
const res = await fetch(`${API_BASE}/api/auth/login`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ username, password: TEST_PASSWORD }),
|
|
});
|
|
assert.equal(res.status, 200);
|
|
const sid = res.headers
|
|
.get("set-cookie")
|
|
.split(",")
|
|
.map((c) => c.split(";")[0].trim())
|
|
.find((c) => c.startsWith("connect.sid="));
|
|
return sid;
|
|
}
|
|
|
|
async function api(path, cookie, init = {}) {
|
|
return fetch(`${API_BASE}/api${path}`, {
|
|
...init,
|
|
headers: {
|
|
Cookie: cookie,
|
|
"Content-Type": "application/json",
|
|
...(init.headers ?? {}),
|
|
},
|
|
});
|
|
}
|
|
|
|
async function createChecklistNote(cookie, items) {
|
|
const res = await api("/notes", cookie, {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
title: "Shared list",
|
|
kind: "checklist",
|
|
items,
|
|
color: "blue",
|
|
}),
|
|
});
|
|
assert.ok(res.status === 200 || res.status === 201);
|
|
const json = await res.json();
|
|
createdNoteIds.push(json.id);
|
|
return json;
|
|
}
|
|
|
|
after(async () => {
|
|
if (createdNoteIds.length > 0) {
|
|
await pool.query(`DELETE FROM note_recipients WHERE note_id = ANY($1::int[])`, [createdNoteIds]);
|
|
await pool.query(`DELETE FROM notes WHERE id = ANY($1::int[])`, [createdNoteIds]);
|
|
}
|
|
if (createdUserIds.length > 0) {
|
|
await pool.query(`DELETE FROM notifications WHERE user_id = ANY($1::int[])`, [createdUserIds]);
|
|
await pool.query(`DELETE FROM user_roles WHERE user_id = ANY($1::int[])`, [createdUserIds]);
|
|
await pool.query(`DELETE FROM users WHERE id = ANY($1::int[])`, [createdUserIds]);
|
|
}
|
|
await pool.end();
|
|
});
|
|
|
|
test("owner can toggle a checklist item; live note + recipient snapshot both update", async () => {
|
|
const owner = await createUser("cl_owner");
|
|
const recipient = await createUser("cl_recipient");
|
|
const oCookie = await login(owner.username);
|
|
|
|
const note = await createChecklistNote(oCookie, [
|
|
{ id: "a", text: "Buy milk", done: false },
|
|
{ id: "b", text: "Walk dog", done: false },
|
|
]);
|
|
await api(`/notes/${note.id}/send`, oCookie, {
|
|
method: "POST",
|
|
body: JSON.stringify({ recipientUserIds: [recipient.id] }),
|
|
});
|
|
|
|
const res = await api(`/notes/${note.id}/checklist/a/toggle`, oCookie, {
|
|
method: "POST",
|
|
body: JSON.stringify({ done: true }),
|
|
});
|
|
assert.equal(res.status, 200);
|
|
const body = await res.json();
|
|
assert.equal(body.success, true);
|
|
assert.equal(body.items.find((i) => i.id === "a").done, true);
|
|
|
|
// Live note row updated.
|
|
const { rows: liveRows } = await pool.query(
|
|
`SELECT items FROM notes WHERE id = $1`,
|
|
[note.id],
|
|
);
|
|
assert.equal(liveRows[0].items.find((i) => i.id === "a").done, true);
|
|
// Recipient snapshot mirrored.
|
|
const { rows: snapRows } = await pool.query(
|
|
`SELECT items FROM note_recipients WHERE note_id = $1`,
|
|
[note.id],
|
|
);
|
|
assert.equal(snapRows[0].items.find((i) => i.id === "a").done, true);
|
|
});
|
|
|
|
test("recipient can toggle a checklist item collaboratively", async () => {
|
|
const owner = await createUser("cl_owner2");
|
|
const recipient = await createUser("cl_recipient2");
|
|
const oCookie = await login(owner.username);
|
|
const rCookie = await login(recipient.username);
|
|
|
|
const note = await createChecklistNote(oCookie, [
|
|
{ id: "x", text: "Task X", done: false },
|
|
]);
|
|
await api(`/notes/${note.id}/send`, oCookie, {
|
|
method: "POST",
|
|
body: JSON.stringify({ recipientUserIds: [recipient.id] }),
|
|
});
|
|
|
|
const res = await api(`/notes/${note.id}/checklist/x/toggle`, rCookie, {
|
|
method: "POST",
|
|
body: JSON.stringify({ done: true }),
|
|
});
|
|
assert.equal(res.status, 200, "active recipient must be able to toggle");
|
|
// Owner sees the change in their live note.
|
|
const { rows } = await pool.query(`SELECT items FROM notes WHERE id = $1`, [note.id]);
|
|
assert.equal(rows[0].items[0].done, true);
|
|
});
|
|
|
|
test("non-recipient gets 403 when toggling", async () => {
|
|
const owner = await createUser("cl_owner3");
|
|
const recipient = await createUser("cl_recipient3");
|
|
const stranger = await createUser("cl_stranger3");
|
|
const oCookie = await login(owner.username);
|
|
const sCookie = await login(stranger.username);
|
|
|
|
const note = await createChecklistNote(oCookie, [
|
|
{ id: "y", text: "Y", done: false },
|
|
]);
|
|
await api(`/notes/${note.id}/send`, oCookie, {
|
|
method: "POST",
|
|
body: JSON.stringify({ recipientUserIds: [recipient.id] }),
|
|
});
|
|
const res = await api(`/notes/${note.id}/checklist/y/toggle`, sCookie, {
|
|
method: "POST",
|
|
body: JSON.stringify({ done: true }),
|
|
});
|
|
assert.equal(res.status, 403);
|
|
});
|
|
|
|
test("archived recipient gets 403 when toggling", async () => {
|
|
const owner = await createUser("cl_owner4");
|
|
const recipient = await createUser("cl_recipient4");
|
|
const oCookie = await login(owner.username);
|
|
const rCookie = await login(recipient.username);
|
|
|
|
const note = await createChecklistNote(oCookie, [
|
|
{ id: "z", text: "Z", done: false },
|
|
]);
|
|
await api(`/notes/${note.id}/send`, oCookie, {
|
|
method: "POST",
|
|
body: JSON.stringify({ recipientUserIds: [recipient.id] }),
|
|
});
|
|
await api(`/notes/${note.id}/archive`, rCookie, {
|
|
method: "POST",
|
|
body: JSON.stringify({ archived: true }),
|
|
});
|
|
|
|
const res = await api(`/notes/${note.id}/checklist/z/toggle`, rCookie, {
|
|
method: "POST",
|
|
body: JSON.stringify({ done: true }),
|
|
});
|
|
assert.equal(res.status, 403);
|
|
});
|
|
|
|
test("non-checklist note rejects the toggle with 400", async () => {
|
|
const owner = await createUser("cl_owner5");
|
|
const oCookie = await login(owner.username);
|
|
|
|
const res = await api("/notes", oCookie, {
|
|
method: "POST",
|
|
body: JSON.stringify({ title: "Plain", content: "hi", kind: "text" }),
|
|
});
|
|
const note = await res.json();
|
|
createdNoteIds.push(note.id);
|
|
|
|
const tog = await api(`/notes/${note.id}/checklist/a/toggle`, oCookie, {
|
|
method: "POST",
|
|
body: JSON.stringify({ done: true }),
|
|
});
|
|
assert.equal(tog.status, 400);
|
|
});
|
|
|
|
test("unknown itemId yields 404", async () => {
|
|
const owner = await createUser("cl_owner6");
|
|
const oCookie = await login(owner.username);
|
|
const note = await createChecklistNote(oCookie, [
|
|
{ id: "real", text: "T", done: false },
|
|
]);
|
|
const res = await api(`/notes/${note.id}/checklist/missing/toggle`, oCookie, {
|
|
method: "POST",
|
|
body: JSON.stringify({ done: true }),
|
|
});
|
|
assert.equal(res.status, 404);
|
|
});
|
|
|
|
test("invalid body (missing done boolean) yields 400", async () => {
|
|
const owner = await createUser("cl_owner7");
|
|
const oCookie = await login(owner.username);
|
|
const note = await createChecklistNote(oCookie, [
|
|
{ id: "a", text: "A", done: false },
|
|
]);
|
|
const res = await api(`/notes/${note.id}/checklist/a/toggle`, oCookie, {
|
|
method: "POST",
|
|
body: JSON.stringify({}),
|
|
});
|
|
assert.equal(res.status, 400);
|
|
});
|