131 lines
5.5 KiB
TypeScript
131 lines
5.5 KiB
TypeScript
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<CategoryEntity>;
|
|
let chRepo: Repository<ChallengeEntity>;
|
|
|
|
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<Repository<CategoryEntity>>(getRepositoryToken(CategoryEntity));
|
|
chRepo = moduleRef.get<Repository<ChallengeEntity>>(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);
|
|
});
|
|
});
|