process.env.DATABASE_PATH = ':memory:'; process.env.THEMES_DIR = './themes'; process.env.FRONTEND_DIST = './frontend/dist'; process.env.NODE_ENV = 'test'; import { Test } from '@nestjs/testing'; import { ConfigModule } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; import { validateEnv } from '../../backend/src/config/env.schema'; import { AdminCategoriesService } from '../../backend/src/modules/admin/categories.service'; import { CategoryEntity } from '../../backend/src/database/entities/category.entity'; import { ChallengeEntity } from '../../backend/src/database/entities/challenge.entity'; import { v4 as uuid } from 'uuid'; import { Repository } from 'typeorm'; import { getRepositoryToken } from '@nestjs/typeorm'; describe('AdminCategoriesService - CRUD + business rules', () => { let categories: AdminCategoriesService; let catRepo: Repository; let chRepo: Repository; beforeAll(async () => { const moduleRef = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ isGlobal: true, validate: validateEnv }), TypeOrmModule.forRoot({ type: 'better-sqlite3', database: ':memory:', entities: [CategoryEntity, ChallengeEntity], synchronize: true, }), TypeOrmModule.forFeature([CategoryEntity, ChallengeEntity]), ], providers: [AdminCategoriesService], }).compile(); moduleRef.init(); categories = moduleRef.get(AdminCategoriesService); catRepo = moduleRef.get>(getRepositoryToken(CategoryEntity)); chRepo = moduleRef.get>(getRepositoryToken(ChallengeEntity)); }); it('uppercases the abbreviation on create', async () => { const c = await categories.create({ name: 'X', abbreviation: 'xy', description: '' }); expect(c.abbreviation).toBe('XY'); await categories.remove(c.id); }); it('rejects duplicate abbreviations', async () => { const c = await categories.create({ name: 'A', abbreviation: 'TT', description: '' }); await expect( categories.create({ name: 'B', abbreviation: 'tt', description: '' }), ).rejects.toMatchObject({ status: 409 }); await categories.remove(c.id); }); it('allows editing a user category name and description', async () => { const c = await categories.create({ name: 'Original', abbreviation: 'oo', description: 'd' }); const updated = await categories.update(c.id, { name: 'New', description: 'd2' }); expect(updated.name).toBe('New'); expect(updated.description).toBe('d2'); expect(updated.abbreviation).toBe('OO'); await categories.remove(c.id); }); it('protects system category abbreviation from change', async () => { const sys = await categories.create({ name: 'Sys', abbreviation: 'SY', description: '' }); await catRepo.update({ id: sys.id }, { systemKey: 'SY' }); await expect( categories.update(sys.id, { abbreviation: 'ZZ' }), ).rejects.toMatchObject({ status: 409 }); await catRepo.update({ id: sys.id }, { systemKey: null }); await categories.remove(sys.id); }); it('allows system category name/description updates', async () => { const sys = await categories.create({ name: 'Sys', abbreviation: 'SY2', description: 'd' }); await catRepo.update({ id: sys.id }, { systemKey: 'SY2' }); const updated = await categories.update(sys.id, { name: 'Renamed', description: 'x' }); expect(updated.name).toBe('Renamed'); expect(updated.description).toBe('x'); expect(updated.abbreviation).toBe('SY2'); await catRepo.update({ id: sys.id }, { systemKey: null }); await categories.remove(sys.id); }); it('blocks deletion of a system category', async () => { const sys = await categories.create({ name: 'Sys', abbreviation: 'SY3', description: '' }); await catRepo.update({ id: sys.id }, { systemKey: 'SY3' }); await expect(categories.remove(sys.id)).rejects.toMatchObject({ status: 403 }); await catRepo.update({ id: sys.id }, { systemKey: null }); await categories.remove(sys.id); }); it('blocks deletion of user category with challenges attached', async () => { const c = await categories.create({ name: 'WithChal', abbreviation: 'WC', description: '' }); await chRepo.save(chRepo.create({ id: uuid(), name: 'challenge', descriptionMd: '', categoryId: c.id, difficulty: 'low', initialPoints: 100, minimumPoints: 50, decaySolves: 5, flag: 'flag{...}', protocol: 'nc', ipAddress: '', })); await expect(categories.remove(c.id)).rejects.toMatchObject({ status: 409 }); await chRepo.delete({ categoryId: c.id }); await categories.remove(c.id); }); it('allows deletion of user category with no challenges', async () => { const c = await categories.create({ name: 'NoChal', abbreviation: 'NC', description: '' }); await categories.remove(c.id); const list = await categories.list(); expect(list.find((x) => x.id === c.id)).toBeUndefined(); }); it('lists categories sorted by lowercase abbreviation', async () => { const c1 = await categories.create({ name: 'Bravo', abbreviation: 'br', description: '' }); const c2 = await categories.create({ name: 'Alpha', abbreviation: 'al', description: '' }); const list = await categories.list(); const ours = list.filter((c) => c.id === c1.id || c.id === c2.id).map((c) => c.abbreviation); expect(ours).toEqual(['AL', 'BR']); await categories.remove(c1.id); await categories.remove(c2.id); }); });