import { INestApplication } from '@nestjs/common'; import { TASK_TOGGLE_CHANNEL, TaskToggleRequest } from '@dorfteich/shared'; import { PrismaClient } from '@prisma/client'; import { Client } from 'pg'; import request from 'supertest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { createTestApp, sessionCookieOf } from '../testing/test-app'; import { createTestPrisma, hasTestDb, uniqueSuffix } from '../testing/test-db'; import { UsersService } from '../users/users.service'; /** * Task-toggle request path (issue #153): the api checks write permission, * records the toggler as a pending contributor, and emits the NOTIFY the * collab server consumes. The Yjs application itself lives in the collab * listener (verified through the collab e2e stack). */ describe.skipIf(!hasTestDb)('task toggle endpoint (e2e, issue #153)', () => { let app: INestApplication; let prisma: PrismaClient; const suffix = uniqueSuffix(); const password = 'aufgaben sind erledigt 1'; let editorId: string; let editorCookie: string; let readerCookie: string; let pageId: string; const notifies: TaskToggleRequest[] = []; let listenClient: Client; const api = () => request(app.getHttpServer()); beforeAll(async () => { prisma = createTestPrisma(); app = await createTestApp(); const users = app.get(UsersService); const mkUser = async (handle: string) => { const username = `task-${handle}-${suffix}`; const user = await users.createUser({ username, email: `${username}@example.test`, displayName: `Task ${handle}`, password, locale: 'en', }); await users.markEmailVerified(user.id); const cookie = sessionCookieOf( await api() .post('/api/v1/auth/login') .send({ usernameOrEmail: username, password }) .expect(200), ); return { id: user.id, cookie }; }; const editor = await mkUser('editor'); editorId = editor.id; editorCookie = editor.cookie; const reader = await mkUser('reader'); readerCookie = reader.cookie; const pond = await prisma.pond.create({ data: { slug: `task-pond-${suffix}`, name: 'Task Pond', type: 'SHARED', ownerId: editorId }, }); const page = await prisma.page.create({ data: { pondId: pond.id, slug: `tasks-${suffix}`, title: 'Tasks', createdBy: editorId, sortKey: 'a0', ydocState: new Uint8Array(), }, }); pageId = page.id; for (const [userId, role] of [ [editorId, 'EDITOR'], [reader.id, 'READER'], ] as const) { await prisma.roleGrant.create({ data: { pondId: pond.id, subjectType: 'USER', subjectId: userId, role, scopeType: 'POND', scopeId: null, effect: 'ALLOW', createdBy: editorId, }, }); } listenClient = new Client({ connectionString: process.env.TEST_DATABASE_URL }); await listenClient.connect(); listenClient.on('notification', (message) => { if (message.channel === TASK_TOGGLE_CHANNEL && message.payload) { notifies.push(JSON.parse(message.payload) as TaskToggleRequest); } }); await listenClient.query(`LISTEN ${TASK_TOGGLE_CHANNEL}`); }); afterAll(async () => { await listenClient.end().catch(() => undefined); await prisma.pagePendingContributor.deleteMany({ where: { pageId } }); await prisma.roleGrant.deleteMany({ where: { pond: { ownerId: editorId } } }); await prisma.page.deleteMany({ where: { pond: { ownerId: editorId } } }); await prisma.pond.deleteMany({ where: { ownerId: editorId } }); await prisma.session.deleteMany({ where: { user: { username: { contains: suffix } } } }); await prisma.user.deleteMany({ where: { username: { contains: suffix } } }); await prisma.$disconnect(); await app.close(); }); it('emits the toggle NOTIFY and records the pending contributor', async () => { await api() .post(`/api/v1/pages/${pageId}/tasks/abc123defg`) .set('Cookie', editorCookie) .send({ checked: true }) .expect(202); await expect.poll(() => notifies.length, { timeout: 5000 }).toBeGreaterThan(0); expect(notifies[0]).toEqual({ pageId, taskId: 'abc123defg', checked: true, userId: editorId, }); const pending = await prisma.pagePendingContributor.findMany({ where: { pageId } }); expect(pending.map((row) => row.userId)).toContain(editorId); }); it('refuses read-only users and hides unknown pages', async () => { // A reader may see the page, so the write refusal is a 403 (#60). await api() .post(`/api/v1/pages/${pageId}/tasks/abc123defg`) .set('Cookie', readerCookie) .send({ checked: true }) .expect(403); await api() .post(`/api/v1/pages/00000000-0000-4000-8000-000000000000/tasks/x`) .set('Cookie', editorCookie) .send({ checked: true }) .expect(404); await api() .post(`/api/v1/pages/${pageId}/tasks/abc123defg`) .set('Cookie', editorCookie) .send({ checked: 'yes' }) .expect(400); }); });