feat: Admin Area Challenges: List, Import/Export and Add/Edit Modal

This commit is contained in:
OpenVelo Agent
2026-07-22 20:26:27 +00:00
parent 6c18603a7d
commit 216795b5db
33 changed files with 4256 additions and 105 deletions
+55 -3
View File
@@ -5,6 +5,7 @@ import { SeedSystemData1700000000100 } from '../../backend/src/database/migratio
import { AddCategoryTimestampsAndUniqueAbbrev1700000000200 } from '../../backend/src/database/migrations/1700000000200-AddCategoryTimestampsAndUniqueAbbrev';
import { UpdateSystemCategoryKeys1700000000300 } from '../../backend/src/database/migrations/1700000000300-UpdateSystemCategoryKeys';
import { RepairCategorySchemaAndSystemCategories1700000000400 } from '../../backend/src/database/migrations/1700000000400-RepairCategorySchemaAndSystemCategories';
import { UpgradeChallengeAdminSchema1700000000500 } from '../../backend/src/database/migrations/1700000000500-UpgradeChallengeAdminSchema';
import { DataSource } from 'typeorm';
import { UserEntity } from '../../backend/src/database/entities/user.entity';
import { SettingEntity } from '../../backend/src/database/entities/setting.entity';
@@ -29,6 +30,7 @@ describe('Migrations', () => {
AddCategoryTimestampsAndUniqueAbbrev1700000000200,
UpdateSystemCategoryKeys1700000000300,
RepairCategorySchemaAndSystemCategories1700000000400,
UpgradeChallengeAdminSchema1700000000500,
],
migrationsRun: true,
synchronize: false,
@@ -77,10 +79,11 @@ describe('Migrations', () => {
expect(uq).toBeDefined();
});
it('enforces ON DELETE RESTRICT FK on solve', async () => {
it('keeps ON DELETE RESTRICT on solve.user_id (Job 861)', async () => {
const fks = await dataSource.query("PRAGMA foreign_key_list('solve')");
const restrict = fks.filter((f: any) => f.on_delete === 'RESTRICT');
expect(restrict.length).toBeGreaterThanOrEqual(2);
const userFk = fks.find((f: any) => f.table === 'user');
expect(userFk).toBeDefined();
expect(userFk.on_delete).toBe('RESTRICT');
});
it('creates the category table with created_at and updated_at columns (Job 889)', async () => {
@@ -100,4 +103,53 @@ describe('Migrations', () => {
expect(Array.isArray(rows)).toBe(true);
expect(rows.length).toBeGreaterThanOrEqual(6);
});
it('upgrades challenge to canonical enums and adds enabled/updated_at columns (Job 861)', async () => {
const cols: any[] = await dataSource.query(`PRAGMA table_info("challenge")`);
const names = new Set(cols.map((c) => c.name));
expect(names.has('enabled')).toBe(true);
expect(names.has('updated_at')).toBe(true);
expect(names.has('description_md')).toBe(true);
});
it('enforces unique lower-cased challenge names (Job 861)', async () => {
const indexes = await dataSource.query(`SELECT name, sql FROM sqlite_master WHERE type='index' AND tbl_name='challenge'`);
const uq = indexes.find((i: any) => /UNIQUE.*LOWER\(\s*(?:"name")\s*\)/i.test(i.sql || ''));
expect(uq).toBeDefined();
});
it('cascade-deletes challenge_file rows when challenge is deleted (Job 861)', async () => {
const fks = await dataSource.query(`PRAGMA foreign_key_list('challenge_file')`);
const cascade = fks.find((f: any) => f.on_delete === 'CASCADE');
expect(cascade).toBeDefined();
});
it('cascade-deletes solve rows when challenge is deleted (Job 861)', async () => {
const fks = await dataSource.query(`PRAGMA foreign_key_list('solve')`);
const cascade = fks.find((f: any) => f.on_delete === 'CASCADE');
expect(cascade).toBeDefined();
});
it('still enforces UNIQUE(challenge_id,user_id) on solve (Job 861)', async () => {
const indexes = await dataSource.query("SELECT name, sql FROM sqlite_master WHERE type='index' AND tbl_name='solve'");
const uq = indexes.find((i: any) => /UNIQUE.*challenge_id.*user_id/i.test(i.sql || ''));
expect(uq).toBeDefined();
});
it('adds is_first/is_second/is_third columns to solve (Job 861)', async () => {
const cols: any[] = await dataSource.query(`PRAGMA table_info("solve")`);
const names = new Set(cols.map((c) => c.name));
expect(names.has('is_first')).toBe(true);
expect(names.has('is_second')).toBe(true);
expect(names.has('is_third')).toBe(true);
});
it('adds stored_filename/mime_type/size_bytes/created_at to challenge_file (Job 861)', async () => {
const cols: any[] = await dataSource.query(`PRAGMA table_info("challenge_file")`);
const names = new Set(cols.map((c) => c.name));
expect(names.has('stored_filename')).toBe(true);
expect(names.has('mime_type')).toBe(true);
expect(names.has('size_bytes')).toBe(true);
expect(names.has('created_at')).toBe(true);
});
});