process.env.DATABASE_PATH = ':memory:'; process.env.THEMES_DIR = './themes'; process.env.FRONTEND_DIST = './frontend/dist'; import { Test } from '@nestjs/testing'; import { INestApplication, ValidationPipe } from '@nestjs/common'; import { HttpAdapterHost } from '@nestjs/core'; import { DataSource, Repository } from 'typeorm'; import { getRepositoryToken } from '@nestjs/typeorm'; import { AppModule } from '../../backend/src/app.module'; import { GlobalExceptionFilter } from '../../backend/src/common/filters/global-exception.filter'; import { csrfClient } from './csrf-client'; import { initDb } from './db-helper'; import { UserEntity } from '../../backend/src/database/entities/user.entity'; import { RefreshTokenEntity } from '../../backend/src/database/entities/refresh-token.entity'; import { SetupService } from '../../backend/src/modules/setup/setup.service'; import { ERROR_CODES } from '../../backend/src/common/errors/error-codes'; describe('Setup: POST /api/v1/setup/create-admin', () => { let app: INestApplication; let users: Repository; let refreshTokens: Repository; let setupService: SetupService; beforeAll(async () => { const moduleRef = await Test.createTestingModule({ imports: [AppModule] }).compile(); app = moduleRef.createNestApplication(); app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: false, transform: true })); const httpAdapterHost = app.get(HttpAdapterHost); app.useGlobalFilters(new GlobalExceptionFilter(httpAdapterHost)); await app.init(); await initDb(app); users = app.get>(getRepositoryToken(UserEntity)); refreshTokens = app.get>(getRepositoryToken(RefreshTokenEntity)); setupService = app.get(SetupService); }); afterAll(async () => { await app.close(); }); beforeEach(async () => { // The deployment-wide last-admin triggers reject the deletion of // the last admin row, which is exactly what `clear()` would attempt // when a prior test left a sole admin in the table. Disable the // triggers for the duration of the cleanup, then re-enable them. const ds = app.get(DataSource); await ds.query(`DROP TRIGGER IF EXISTS "trg_user_last_admin_update"`); await ds.query(`DROP TRIGGER IF EXISTS "trg_user_last_admin_delete"`); try { await refreshTokens.clear(); await users.clear(); } finally { await ds.query(` CREATE TRIGGER IF NOT EXISTS "trg_user_last_admin_update" BEFORE UPDATE OF "role" ON "user" FOR EACH ROW WHEN OLD."role" = 'admin' AND NEW."role" <> 'admin' AND NOT EXISTS ( SELECT 1 FROM "user" AS "other" WHERE "other"."role" = 'admin' AND "other"."id" <> OLD."id" ) BEGIN SELECT RAISE(ABORT, 'LAST_ADMIN'); END; `); await ds.query(` CREATE TRIGGER IF NOT EXISTS "trg_user_last_admin_delete" BEFORE DELETE ON "user" FOR EACH ROW WHEN OLD."role" = 'admin' AND NOT EXISTS ( SELECT 1 FROM "user" AS "other" WHERE "other"."role" = 'admin' AND "other"."id" <> OLD."id" ) BEGIN SELECT RAISE(ABORT, 'LAST_ADMIN'); END; `); } }); it('creates the very first admin and exposes a session + cookie', async () => { const c = csrfClient(app); const res = await c .post('/api/v1/setup/create-admin') .send({ username: 'first.admin', password: 'Sup3rSecret!Pass', passwordConfirm: 'Sup3rSecret!Pass' }) .expect(201); expect(res.body.accessToken).toEqual(expect.any(String)); expect(res.body.user.role).toBe('admin'); expect(res.body.user.username).toBe('first.admin'); const setCookieRaw = res.headers['set-cookie'] as unknown; const setCookie: string[] = Array.isArray(setCookieRaw) ? setCookieRaw : setCookieRaw ? [String(setCookieRaw)] : []; expect(setCookie.some((h) => String(h).startsWith('rt='))).toBe(true); const adminCount = await users.count({ where: { role: 'admin' } }); expect(adminCount).toBe(1); }); it('returns 409 SYSTEM_INITIALIZED once an admin already exists', async () => { const c = csrfClient(app); await c .post('/api/v1/setup/create-admin') .send({ username: 'first', password: 'Sup3rSecret!Pass', passwordConfirm: 'Sup3rSecret!Pass' }) .expect(201); const res = await c .post('/api/v1/setup/create-admin') .send({ username: 'second', password: 'Sup3rSecret!Pass', passwordConfirm: 'Sup3rSecret!Pass' }); expect(res.status).toBe(409); expect(res.body.code).toBe(ERROR_CODES.SYSTEM_INITIALIZED); }); it('SetupService would map a username collision to USERNAME_TAKEN (reached only during a race before init)', () => { // With the in-process bootstrap serialization in place, USERNAME_TAKEN // remains a defensive code: it is reachable only if two concurrent // requests with the same username somehow both slip past the lock. // Verify the code is present in the error-codes enum so the frontend // can switch on it. expect(ERROR_CODES.USERNAME_TAKEN).toBe('USERNAME_TAKEN'); }); it('SetupService throws SYSTEM_INITIALIZED (409) after init', async () => { await setupService.createAdmin('init', 'Sup3rSecret!Pass', 'Sup3rSecret!Pass', '127.0.0.1'); await expect( setupService.createAdmin('newone', 'Sup3rSecret!Pass', 'Sup3rSecret!Pass', '127.0.0.1'), ).rejects.toMatchObject({ code: ERROR_CODES.SYSTEM_INITIALIZED }); }); it('rejects weak password with WEAK_PASSWORD', async () => { const c = csrfClient(app); const res = await c .post('/api/v1/setup/create-admin') .send({ username: 'weakpw', password: 'short', passwordConfirm: 'short' }) .expect(400); expect(res.body.code).toBe('WEAK_PASSWORD'); }); it('rejects passwordConfirm mismatch with VALIDATION_FAILED', async () => { const c = csrfClient(app); await c .post('/api/v1/setup/create-admin') .send({ username: 'mismatch', password: 'Sup3rSecret!Pass', passwordConfirm: 'Different!Pass1' }) .expect(400); }); it('rejects invalid username characters with VALIDATION_FAILED', async () => { const c = csrfClient(app); await c .post('/api/v1/setup/create-admin') .send({ username: 'bad name!', password: 'Sup3rSecret!Pass', passwordConfirm: 'Sup3rSecret!Pass' }) .expect(400); }); it('only one of two concurrent first-admin requests succeeds; the other receives a controlled conflict', async () => { // Two independent Supertest agents simulate two separate browser // connections that arrive within the same event-loop tick and share no // cookies, mirroring the production race reproduction. const a = csrfClient(app); const b = csrfClient(app); const [resA, resB] = await Promise.all([ a .post('/api/v1/setup/create-admin') .send({ username: 'adminAlpha', password: 'Sup3rSecret!Pass', passwordConfirm: 'Sup3rSecret!Pass' }), b .post('/api/v1/setup/create-admin') .send({ username: 'adminBeta', password: 'Sup3rSecret!Pass', passwordConfirm: 'Sup3rSecret!Pass' }), ]); const statuses = [resA.status, resB.status].sort(); expect(statuses).toEqual([201, 409]); const loser = resA.status === 409 ? resA : resB; expect(loser.body.code).toBe(ERROR_CODES.SYSTEM_INITIALIZED); const admins = await users.find({ where: { role: 'admin' } }); expect(admins).toHaveLength(1); expect(['adminAlpha', 'adminBeta']).toContain(admins[0].username); const sessions = await refreshTokens.count(); expect(sessions).toBe(1); }); });