import { INestApplication } from '@nestjs/common'; import { AdminUserListView } from '@dorfteich/shared'; import { PrismaClient } from '@prisma/client'; import request from 'supertest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { PondsService } from '../ponds/ponds.service'; import { createTestApp, sessionCookieOf } from '../testing/test-app'; import { createTestPrisma, deletePondsWhere, hasTestDb, uniqueSuffix } from '../testing/test-db'; import { UsersService } from '../users/users.service'; /** * Site-Admin user management end to end (issue #59): disable logs a user out * and blocks login; delete pseudonymizes authorship and trashes the personal * pond; the last Site Admin and one's own account are protected. */ describe.skipIf(!hasTestDb)('user admin (e2e, issue #59)', () => { let app: INestApplication; let prisma: PrismaClient; const suffix = uniqueSuffix(); const password = 'nutzerverwaltung ist ernst 1'; const ids: Record = {}; const cookies: Record = {}; const api = () => request(app.getHttpServer()); async function makeUser(handle: string, siteAdmin: boolean): Promise { const users = app.get(UsersService); const username = `ua-${handle}-${suffix}`; const user = await users.createUser({ username, email: `${username}@example.org`, displayName: `UA ${handle}`, password, locale: 'en', }); await users.markEmailVerified(user.id); if (siteAdmin) await prisma.user.update({ where: { id: user.id }, data: { isSiteAdmin: true } }); ids[handle] = user.id; cookies[handle] = sessionCookieOf( await api() .post('/api/v1/auth/login') .send({ usernameOrEmail: username, password }) .expect(200), ); } beforeAll(async () => { prisma = createTestPrisma(); await prisma.rateLimit.deleteMany({}); app = await createTestApp(); await makeUser('admin1', true); await makeUser('admin2', true); await makeUser('bob', false); // Bob gets a personal pond (trashed on delete). await app .get(PondsService) .ensurePersonalPond(await prisma.user.findUniqueOrThrow({ where: { id: ids.bob! } })); }); afterAll(async () => { const all = Object.values(ids); await prisma.session.deleteMany({ where: { userId: { in: all } } }); await deletePondsWhere(prisma, { ownerId: { in: all } }); await prisma.userIdentity.deleteMany({ where: { userId: { in: all } } }); await prisma.user.deleteMany({ where: { id: { in: all } } }); await prisma.$disconnect(); await app.close(); }); it('lists and searches users (Site-Admin only)', async () => { const res = await api() .get(`/api/v1/admin/users?q=ua-bob-${suffix}`) .set('Cookie', cookies.admin1!) .expect(200); const body = res.body as AdminUserListView; expect(body.users.map((u) => u.id)).toContain(ids.bob); await api().get('/api/v1/admin/users').set('Cookie', cookies.bob!).expect(403); }); it('disabling logs the user out everywhere and blocks login', async () => { await api() .patch(`/api/v1/admin/users/${ids.bob}/disabled`) .set('Cookie', cookies.admin1!) .send({ disabled: true }) .expect(200); // Existing session is gone… await api().get('/api/v1/auth/me').set('Cookie', cookies.bob!).expect(401); // …and login is refused with a distinct code. await api() .post('/api/v1/auth/login') .send({ usernameOrEmail: `ua-bob-${suffix}`, password }) .expect(403) .expect((r) => expect((r.body as { code: string }).code).toBe('account_disabled')); await api() .patch(`/api/v1/admin/users/${ids.bob}/disabled`) .set('Cookie', cookies.admin1!) .send({ disabled: false }) .expect(200); }); it('deleting pseudonymizes authorship and trashes the personal pond', async () => { await api().delete(`/api/v1/admin/users/${ids.bob}`).set('Cookie', cookies.admin1!).expect(204); const user = await prisma.user.findUniqueOrThrow({ where: { id: ids.bob! } }); expect(user.displayName).toBe('Deleted user'); expect(user.username).toBe(`deleted-${ids.bob}`); expect(user.status).toBe('DISABLED'); const personal = await prisma.pond.findFirstOrThrow({ where: { ownerId: ids.bob!, type: 'PERSONAL' }, }); expect(personal.deletedAt).not.toBeNull(); // Credentials are gone → login impossible even with the old password. expect(await prisma.userIdentity.count({ where: { userId: ids.bob! } })).toBe(0); }); it("protects the last Site Admin and one's own account", async () => { // Revoke admin2 → fine (admin1 remains). await api() .patch(`/api/v1/admin/users/${ids.admin2}/site-admin`) .set('Cookie', cookies.admin1!) .send({ isSiteAdmin: false }) .expect(200); // Now admin1 is the last → cannot revoke self, and self-action is blocked anyway. await api() .patch(`/api/v1/admin/users/${ids.admin1}/site-admin`) .set('Cookie', cookies.admin1!) .send({ isSiteAdmin: false }) .expect(400) .expect((r) => expect((r.body as { code: string }).code).toBe('cannot_modify_self')); await api() .delete(`/api/v1/admin/users/${ids.admin1}`) .set('Cookie', cookies.admin1!) .expect(400) .expect((r) => expect((r.body as { code: string }).code).toBe('cannot_modify_self')); }); });