AI Implementation feature(906): Challenges Page and Challenge Solve Modal 1.02 (#48)

This commit was merged in pull request #48.
This commit is contained in:
2026-07-23 02:24:44 +00:00
parent cbfa9c35ca
commit f194641e8c
11 changed files with 506 additions and 133 deletions
+118 -2
View File
@@ -6,6 +6,7 @@ import { AddCategoryTimestampsAndUniqueAbbrev1700000000200 } from '../../backend
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 { SeedSampleChallenges1700000000600 } from '../../backend/src/database/migrations/1700000000600-SeedSampleChallenges';
import { DataSource } from 'typeorm';
import { UserEntity } from '../../backend/src/database/entities/user.entity';
import { SettingEntity } from '../../backend/src/database/entities/setting.entity';
@@ -31,6 +32,7 @@ describe('Migrations', () => {
UpdateSystemCategoryKeys1700000000300,
RepairCategorySchemaAndSystemCategories1700000000400,
UpgradeChallengeAdminSchema1700000000500,
SeedSampleChallenges1700000000600,
],
migrationsRun: true,
synchronize: false,
@@ -146,10 +148,124 @@ describe('Migrations', () => {
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));
const names = new Set(cols.map((c: any) => 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);
});
});
it('seeds sample challenges on a fresh database (Job 906)', async () => {
const repo = dataSource.getRepository(ChallengeEntity);
const rows = await repo.find();
expect(rows.length).toBeGreaterThan(0);
const names = new Set(rows.map((r) => String((r as any).name).toLowerCase()));
for (const expected of [
'alpha cipher',
'beta cipher',
'zeta key',
'web welcome',
'mobile mayhem',
'hardware hello',
'markdown mystery',
'reverse ranger',
]) {
expect(names.has(expected)).toBe(true);
}
});
it('seeded sample challenges are enabled, schema-compatible, and point at the right categories (Job 906)', async () => {
const cats: any[] = await dataSource.getRepository(CategoryEntity).find();
const idByKey = new Map(cats.filter((c) => c.systemKey).map((c) => [String(c.systemKey), String(c.id)]));
const rows = await dataSource.getRepository(ChallengeEntity).find();
expect(rows.length).toBeGreaterThan(0);
for (const row of rows) {
expect(Boolean((row as any).enabled)).toBe(true);
expect(['LOW', 'MEDIUM', 'HIGH']).toContain((row as any).difficulty);
expect(Number((row as any).initialPoints)).toBeGreaterThanOrEqual(Number((row as any).minimumPoints));
expect(typeof (row as any).flag).toBe('string');
expect(((row as any).flag as string).length).toBeGreaterThan(0);
expect(idByKey.values()).toContain((row as any).categoryId);
}
});
});
describe('SeedSampleChallenges down() scope (Job 906)', () => {
let dataSource: DataSource;
beforeAll(async () => {
dataSource = new DataSource({
type: 'better-sqlite3',
database: ':memory:',
entities: [UserEntity, SettingEntity, CategoryEntity, ChallengeEntity, ChallengeFileEntity, SolveEntity, RefreshTokenEntity, BlogPostEntity],
migrations: [
InitSchema1700000000000,
SeedSystemData1700000000100,
AddCategoryTimestampsAndUniqueAbbrev1700000000200,
UpdateSystemCategoryKeys1700000000300,
RepairCategorySchemaAndSystemCategories1700000000400,
UpgradeChallengeAdminSchema1700000000500,
],
migrationsRun: true,
synchronize: false,
logging: false,
});
await dataSource.initialize();
});
afterAll(async () => {
await dataSource.destroy();
});
it('removes only the seeded sample names and leaves unrelated rows untouched', async () => {
const cat: any = await dataSource.getRepository(CategoryEntity).findOne({ where: { systemKey: 'CRY' } });
expect(cat).toBeDefined();
// The seed migration was deliberately excluded above so we can insert
// both a sample-name row and an unrelated manual row by hand and verify
// down() targets only the sample names.
await dataSource.query(
`INSERT INTO "challenge" (
"id","name","description_md","category_id","difficulty",
"initial_points","minimum_points","decay_solves","flag","protocol",
"port","ip_address","enabled","created_at","updated_at"
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,strftime('%Y-%m-%dT%H:%M:%fZ','now'),strftime('%Y-%m-%dT%H:%M:%fZ','now'))`,
[
'00000000-0000-0000-0000-000000000a01',
'Alpha Cipher',
'sample',
cat.id,
'LOW',
200, 100, 5, 'flag{alpha}', 'WEB', null, '', 1,
],
);
await dataSource.query(
`INSERT INTO "challenge" (
"id","name","description_md","category_id","difficulty",
"initial_points","minimum_points","decay_solves","flag","protocol",
"port","ip_address","enabled","created_at","updated_at"
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,strftime('%Y-%m-%dT%H:%M:%fZ','now'),strftime('%Y-%m-%dT%H:%M:%fZ','now'))`,
[
'00000000-0000-0000-0000-000000000a02',
'Other',
'manual',
cat.id,
'MEDIUM',
100, 50, 10, 'flag{other}', 'WEB', null, '', 1,
],
);
const qr = dataSource.createQueryRunner();
try {
const seed = new SeedSampleChallenges1700000000600();
await seed.down(qr);
} finally {
await qr.release();
}
const remaining: any[] = await dataSource.query(`SELECT name FROM "challenge" ORDER BY name`);
const names = remaining.map((r) => String(r.name));
expect(names).toContain('Other');
expect(names).not.toContain('Alpha Cipher');
});
});