import { INestApplication } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; import { PinoLogger } from 'nestjs-pino'; import request from 'supertest'; import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; import { InstanceSettingsService } from '../settings/instance-settings.service'; import { createTestApp, sessionCookieOf } from '../testing/test-app'; import { createTestPrisma, grantOwnerAdmin, hasTestDb, uniqueSuffix } from '../testing/test-db'; import { UsersService } from '../users/users.service'; import { ReadTrailService } from './read-trail.service'; /** * The read-trail master switch (issue #225, ADR 0023): OFF is the default * and means no event is written ANYWHERE — no row, no stdout line; ON * restores the full #222 semantics. The switch position is announced so a * silent trail is never ambiguous. */ describe.skipIf(!hasTestDb)('read-trail switch (e2e, issue #225)', () => { let app: INestApplication; let prisma: PrismaClient; const suffix = uniqueSuffix(); const password = 'schalter zeugen 123'; let ownerId: string; let ownerCookie: string; let pondId: string; let classifiedId: string; const api = () => request(app.getHttpServer()); beforeAll(async () => { prisma = createTestPrisma(); await prisma.rateLimit.deleteMany({}); // No leftover switch row: this suite tests the DEFAULT (off). await prisma.instanceSetting.deleteMany({ where: { key: 'readTrail.enabled' } }); app = await createTestApp(); const users = app.get(UsersService); const owner = await users.createUser({ username: `switch-owner-${suffix}`, email: `switch-owner-${suffix}@example.test`, displayName: 'Switch Owner', password, locale: 'en', }); await users.markEmailVerified(owner.id); ownerId = owner.id; ownerCookie = sessionCookieOf( await api() .post('/api/v1/auth/login') .send({ usernameOrEmail: `switch-owner-${suffix}`, password }) .expect(200), ); const pond = await prisma.pond.create({ data: { slug: `switch-pond-${suffix}`, name: 'Switch Pond', type: 'SHARED', ownerId }, }); pondId = pond.id; await grantOwnerAdmin(prisma, pondId, ownerId); const page = await prisma.page.create({ data: { pondId, slug: `classified-${suffix}`, title: 'Classified', classification: 'VS_NFD', createdBy: ownerId, sortKey: 'a0', ydocState: new Uint8Array(), contentCache: { create: { plainText: 'x', markdown: 'x', html: '

x

', outline: [] }, }, }, }); classifiedId = page.id; }); afterAll(async () => { await prisma.instanceSetting.deleteMany({ where: { key: 'readTrail.enabled' } }); await prisma.readEvent.deleteMany({ where: { pondId } }); await prisma.roleGrant.deleteMany({ where: { pondId } }); await prisma.pageContentCache.deleteMany({ where: { page: { pondId } } }); await prisma.page.deleteMany({ where: { pondId } }); await prisma.pond.deleteMany({ where: { id: pondId } }); await prisma.user.deleteMany({ where: { id: ownerId } }); await prisma.$disconnect(); await app.close(); }); it('is OFF by default: a classified read writes nothing — no row, no stdout line', async () => { const trail = app.get(ReadTrailService); const logger = (trail as unknown as { logger: PinoLogger }).logger; const infoSpy = vi.spyOn(logger, 'info'); try { await api().get(`/api/v1/pages/${classifiedId}`).set('Cookie', ownerCookie).expect(200); } finally { infoSpy.mockRestore(); } expect(await prisma.readEvent.count({ where: { pondId } })).toBe(0); expect(infoSpy).not.toHaveBeenCalled(); }); it('records again the moment the switch turns on', async () => { await app.get(InstanceSettingsService).set('readTrail.enabled', true, ownerId); try { await api().get(`/api/v1/pages/${classifiedId}`).set('Cookie', ownerCookie).expect(200); expect(await prisma.readEvent.count({ where: { pondId } })).toBe(1); } finally { await app.get(InstanceSettingsService).set('readTrail.enabled', false, ownerId); } }); it('announces the switch position so silence is never ambiguous', async () => { const trail = app.get(ReadTrailService); const logger = (trail as unknown as { logger: PinoLogger }).logger; const warnSpy = vi.spyOn(logger, 'warn'); const infoSpy = vi.spyOn(logger, 'info'); try { await trail.announceState(); // switch is off after the previous test expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('NOT evidenced')); await app.get(InstanceSettingsService).set('readTrail.enabled', true, ownerId); await trail.announceState(); expect(infoSpy).toHaveBeenCalledWith(expect.stringContaining('enabled')); } finally { warnSpy.mockRestore(); infoSpy.mockRestore(); await app.get(InstanceSettingsService).set('readTrail.enabled', false, ownerId); } }); });