import { UsersService } from '../../backend/src/modules/users/users.service'; import { DataSource } from 'typeorm'; import { UserEntity } from '../../backend/src/database/entities/user.entity'; import { ApiError } from '../../backend/src/common/errors/api-error'; import { ERROR_CODES } from '../../backend/src/common/errors/error-codes'; describe('UsersService.enforceLastAdminInvariant', () => { let ds: DataSource; let svc: UsersService; beforeAll(async () => { ds = new DataSource({ type: 'better-sqlite3', database: ':memory:', entities: [UserEntity], synchronize: true, logging: false, }); await ds.initialize(); svc = new UsersService(ds.getRepository(UserEntity)); }); afterAll(async () => { await ds.destroy(); }); beforeEach(async () => { await ds.getRepository(UserEntity).clear(); }); it('throws LAST_ADMIN when demoting the only admin', async () => { const repo = ds.getRepository(UserEntity); const admin = repo.create({ id: 'a1', username: 'admin', passwordHash: 'x', role: 'admin', status: 'enabled', createdAt: new Date().toISOString(), }); await repo.save(admin); await expect( ds.transaction(async (manager) => svc.enforceLastAdminInvariant(manager, 'a1', 'player')), ).rejects.toMatchObject({ response: { code: ERROR_CODES.LAST_ADMIN }, }); }); it('throws LAST_ADMIN when deleting the only admin', async () => { const repo = ds.getRepository(UserEntity); const admin = repo.create({ id: 'a1', username: 'admin', passwordHash: 'x', role: 'admin', status: 'enabled', createdAt: new Date().toISOString(), }); await repo.save(admin); await expect( ds.transaction(async (manager) => svc.enforceLastAdminInvariant(manager, 'a1')), ).rejects.toMatchObject({ response: { code: ERROR_CODES.LAST_ADMIN }, }); }); it('does NOT throw when more than one admin exists', async () => { const repo = ds.getRepository(UserEntity); await repo.save([ repo.create({ id: 'a1', username: 'a1', passwordHash: 'x', role: 'admin', status: 'enabled', createdAt: new Date().toISOString() }), repo.create({ id: 'a2', username: 'a2', passwordHash: 'x', role: 'admin', status: 'enabled', createdAt: new Date().toISOString() }), ]); await expect( ds.transaction(async (manager) => svc.enforceLastAdminInvariant(manager, 'a1', 'player')), ).resolves.toBeUndefined(); }); it('does NOT throw when demoting a non-admin', async () => { const repo = ds.getRepository(UserEntity); await repo.save([ repo.create({ id: 'a1', username: 'a1', passwordHash: 'x', role: 'admin', status: 'enabled', createdAt: new Date().toISOString() }), repo.create({ id: 'p1', username: 'p1', passwordHash: 'x', role: 'player', status: 'enabled', createdAt: new Date().toISOString() }), ]); await expect( ds.transaction(async (manager) => svc.enforceLastAdminInvariant(manager, 'p1', 'admin')), ).resolves.toBeUndefined(); }); });