import { INestApplication } from '@nestjs/common'; import { markdownToDoc } from '@dorfteich/shared'; import { PrismaClient } from '@prisma/client'; import request from 'supertest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { PondPermissionCache } from '../permissions/pond-permission-cache'; import { createTestApp, sessionCookieOf } from '../testing/test-app'; import { createTestPrisma, hasTestDb, uniqueSuffix } from '../testing/test-db'; import { UsersService } from '../users/users.service'; import { docToState } from './yjs-content'; /** * The task overview collection (issue #154): tasks of the page and its live * subtree, permission-filtered per source page; the public rendering expands * the placeholder into a static table. */ describe.skipIf(!hasTestDb)('task overview endpoint (e2e, issue #154)', () => { let app: INestApplication; let prisma: PrismaClient; const suffix = uniqueSuffix(); const password = 'uebersicht zeigt alles 1'; let ownerId: string; let ownerCookie: string; let pondSlug: string; let pondId: string; const api = () => request(app.getHttpServer()); async function makePage( slug: string, title: string, markdown: string, parentId: string | null = null, ): Promise { const doc = markdownToDoc(markdown); const page = await prisma.page.create({ data: { pondId, slug, title, parentId, createdBy: ownerId, sortKey: 'a0', ydocState: docToState(doc), contentCache: { create: { plainText: markdown, markdown, html: '', outline: [] }, }, }, }); return page.id; } beforeAll(async () => { prisma = createTestPrisma(); app = await createTestApp(); const users = app.get(UsersService); const username = `overview-owner-${suffix}`; const owner = await users.createUser({ username, email: `${username}@example.test`, displayName: 'Overview Owner', password, locale: 'en', }); ownerId = owner.id; await users.markEmailVerified(ownerId); ownerCookie = sessionCookieOf( await api() .post('/api/v1/auth/login') .send({ usernameOrEmail: username, password }) .expect(200), ); pondSlug = `overview-pond-${suffix}`; const pond = await prisma.pond.create({ data: { slug: pondSlug, name: 'Overview Pond', type: 'SHARED', ownerId }, }); pondId = pond.id; await prisma.roleGrant.create({ data: { pondId, subjectType: 'USER', subjectId: ownerId, role: 'EDITOR', scopeType: 'POND', scopeId: null, effect: 'ALLOW', createdBy: ownerId, }, }); }); afterAll(async () => { await prisma.roleGrant.deleteMany({ where: { pond: { ownerId } } }); await prisma.pageContentCache.deleteMany({ where: { page: { pond: { ownerId } } } }); await prisma.page.deleteMany({ where: { pond: { ownerId } } }); await prisma.pond.deleteMany({ where: { ownerId } }); await prisma.session.deleteMany({ where: { userId: ownerId } }); await prisma.user.deleteMany({ where: { id: ownerId } }); await prisma.$disconnect(); await app.close(); }); it('collects tasks of the page and its subtree with mentions and dates', async () => { const rootId = await makePage( `plan-${suffix}`, 'Plan', '- [ ] Bühne buchen >>2026-08-01\n\n```dorfteich-tasks\n```', ); await makePage(`kabel-${suffix}`, 'Kabel', '- [x] Kabel prüfen <<2026-07-01', rootId); // A sibling outside the subtree contributes nothing. await makePage(`anders-${suffix}`, 'Anders', '- [ ] Fremde Aufgabe'); const res = await api() .get(`/api/v1/read/${pondSlug}/plan-${suffix}/tasks`) .set('Cookie', ownerCookie) .expect(200); const pages = res.body as { title: string; tasks: { text: string; checked: boolean; dueDate: string | null }[]; }[]; expect(pages.map((p) => p.title)).toEqual(['Plan', 'Kabel']); expect(pages[0]!.tasks[0]).toMatchObject({ text: 'Bühne buchen', checked: false, dueDate: '2026-08-01', }); expect(pages[1]!.tasks[0]).toMatchObject({ text: 'Kabel prüfen', checked: true }); const allTexts = pages.flatMap((p) => p.tasks.map((t) => t.text)); expect(allTexts).not.toContain('Fremde Aufgabe'); }); it('requires a session and hides unreadable pages', async () => { await api().get(`/api/v1/read/${pondSlug}/plan-${suffix}/tasks`).expect(401); }); it('expands the placeholder into a static table in the public rendering', async () => { await prisma.roleGrant.create({ data: { pondId, subjectType: 'PUBLIC', subjectId: null, role: 'READER', scopeType: 'POND', scopeId: null, effect: 'ALLOW', createdBy: ownerId, }, }); // Raw grant rows bypass the permission cache — drop the pond's entry. app.get(PondPermissionCache).invalidate(pondId); // The public body comes from the content cache — regenerate it with the // real renderer so the placeholder div is present. const { docToHtml } = await import('@dorfteich/shared'); const doc = markdownToDoc('- [ ] Bühne buchen >>2026-08-01\n\n```dorfteich-tasks\n```'); await prisma.pageContentCache.updateMany({ where: { page: { pondId, slug: `plan-${suffix}` } }, data: { html: docToHtml(doc) }, }); const res = await api().get(`/api/v1/public/${pondSlug}/plan-${suffix}/content`).expect(200); const html = (res.body as { html: string }).html; expect(html).toContain('dt-task-overview-table'); expect(html).toContain('Bühne buchen'); expect(html).toContain('Kabel prüfen'); expect(html).not.toContain('data-task-overview'); }); });