1
10import { afterAll, beforeAll, describe, expect, it } from "vitest";
11import { createTestApp, type TestAppHandle } from "./helpers/api";
12import { addMemberViaSeam, authHeaders, createWorkspace, createUser } from "./helpers/seed";
13
14let app: TestAppHandle;
15
16beforeAll(async () => {
17 app = await createTestApp();
18}, 60_000);
19afterAll(async () => {
20 await app?.close();
21});
22
23async function mkPage(wsId: string, headers: Record<string, string>, title: string): Promise<string> {
24 const res = await app.request("POST", `/api/v1/workspaces/${wsId}/pages`, { headers, body: { title } });
25 expect(res.status).toBe(201);
26 return ((await res.json()) as { id: string }).id;
27}
28
29interface ActivityRow {
30 id: string;
31 action: string;
32 actorId: string;
33 actorName: string | null;
34 workspaceId: string | null;
35 pageId: string | null;
36 createdAt: string;
37}
38
39describe("T-158: activity feeds", () => {
40 it("my activity includes my actions + my spaces' rows; other spaces don't leak", async () => {
41 const owner = await createUser(app.request, { name: "Act Owner" });
42 const other = await createUser(app.request, { name: "Act Other" });
43 const h = authHeaders(owner.accessToken!);
44 const ho = authHeaders(other.accessToken!);
45 const ws1 = await createWorkspace(app.request, h, "Act WS1");
46 const ws2 = await createWorkspace(app.request, ho, "Act WS2");
47
48 await mkPage(ws1.workspaceId, h, "Visible to owner");
49 await mkPage(ws2.workspaceId, ho, "Other's page");
50
51 const res = await app.request("GET", "/api/v1/users/me/activity", { headers: h });
52 expect(res.status).toBe(200);
53 const body = (await res.json()) as { data: ActivityRow[] };
54 const actions = body.data.map((r) => r.action);
55
56 expect(actions).toContain("page.created");
57 const created = body.data.find((r) => r.action === "page.created")!;
58 expect(created.actorId).toBe(owner.userId);
59 expect(created.actorName).toBe("Act Owner");
60 expect(created.workspaceId).toBe(ws1.workspaceId);
61
62 expect(body.data.every((r) => r.workspaceId !== ws2.workspaceId)).toBe(true);
63
64
65 const page1 = (await (await app.request("GET", "/api/v1/users/me/activity?limit=1", { headers: h })).json()) as { data: ActivityRow[]; nextCursor: string };
66 expect(page1.data).toHaveLength(1);
67 expect(page1.nextCursor).toBeTruthy();
68 const page2 = (await (await app.request("GET", `/api/v1/users/me/activity?limit=1&cursor=${encodeURIComponent(page1.nextCursor!)}`, { headers: h })).json()) as { data: ActivityRow[] };
69 expect(page2.data).toHaveLength(1);
70 expect(page2.data[0]?.id).not.toBe(page1.data[0]?.id);
71 });
72
73 it("workspace activity is membership-gated + lists the space's rows", async () => {
74 const owner = await createUser(app.request, { name: "WS Act Owner" });
75 const member = await createUser(app.request, { name: "WS Act Member" });
76 const outsider = await createUser(app.request, { name: "WS Act Outsider" });
77 const h = authHeaders(owner.accessToken!);
78 const hm = authHeaders(member.accessToken!);
79 const ws = await createWorkspace(app.request, h, "WS Act");
80
81 await addMemberViaSeam(app.request, app.internalServiceToken, ws.workspaceId, member.email, "MEMBER");
82 const pageId = await mkPage(ws.workspaceId, h, "Act Page");
83
84
85 const res = await app.request("GET", `/api/v1/workspaces/${ws.workspaceId}/activity`, { headers: hm });
86 expect(res.status).toBe(200);
87 const body = (await res.json()) as { data: ActivityRow[] };
88 const created = body.data.find((r) => r.action === "page.created" && r.pageId === pageId);
89 expect(created).toBeTruthy();
90 expect(created!.actorName).toBe("WS Act Owner");
91
92
93 const denied = await app.request("GET", `/api/v1/workspaces/${ws.workspaceId}/activity`, { headers: authHeaders(outsider.accessToken!) });
94 expect(denied.status).toBe(403);
95 });
96});
97