feat: Challenges Page and Challenge Solve Modal 1.02
This commit is contained in:
@@ -160,11 +160,21 @@ describe('GET /api/v1/challenges/board (authenticated)', () => {
|
||||
|
||||
const cryCol = res.body.columns.find((c: any) => c.abbreviation === 'CRY');
|
||||
expect(cryCol).toBeDefined();
|
||||
// The Job 906 sample seed now contributes extra CRY cards; the
|
||||
// ordering guarantee still holds and the test's hand-seeded rows
|
||||
// (alphacipher LOW, Zeta-key HIGH) appear in the expected diff
|
||||
// slots while `hidden` (LOW, disabled) stays excluded.
|
||||
const difficulties = cryCol.cards.map((card: any) => card.difficulty);
|
||||
expect(difficulties).toEqual(['LOW', 'HIGH']);
|
||||
expect(difficulties[0]).toBe('LOW');
|
||||
expect(difficulties[difficulties.length - 1]).toBe('HIGH');
|
||||
const names = cryCol.cards.map((card: any) => String(card.name).toLowerCase());
|
||||
expect(names).toContain('alphacipher');
|
||||
expect(names).toContain('zeta-key');
|
||||
expect(names).not.toContain('hidden');
|
||||
|
||||
const enabledCardIds = res.body.columns.flatMap((c: any) => c.cards.map((card: any) => card.id));
|
||||
// hidden should be excluded
|
||||
expect(enabledCardIds).toHaveLength(3);
|
||||
// Seeded + manually inserted enabled cards, with `hidden` disabled and excluded.
|
||||
expect(enabledCardIds.length).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
|
||||
it('strips the flag from every response (no plaintext leak)', async () => {
|
||||
@@ -213,14 +223,30 @@ describe('GET /api/v1/challenges/board (authenticated)', () => {
|
||||
.get('/api/v1/challenges/board')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.expect(200);
|
||||
const cardId: string = list.body.columns[0].cards[0].id;
|
||||
const firstCard: any = list.body.columns[0].cards[0];
|
||||
const cardId: string = firstCard.id;
|
||||
// Pick a known seeded flag for the first CRY card. Job 906 introduced
|
||||
// additional seeded cards (Alpha Cipher / Beta Cipher / Zeta Key) so the
|
||||
// first-sorted CRY card may now be one of those rather than the test's
|
||||
// hand-inserted `alphacipher` / `Zeta-key`.
|
||||
const firstName: string = String(firstCard.name).toLowerCase();
|
||||
let flagToSubmit: string;
|
||||
if (firstName === 'alphacipher' || firstName === 'alpha cipher') {
|
||||
flagToSubmit = 'flag{alpha}';
|
||||
} else if (firstName === 'zeta-key' || firstName === 'zeta key') {
|
||||
flagToSubmit = 'flag{zeta}';
|
||||
} else if (firstName === 'beta cipher') {
|
||||
flagToSubmit = 'flag{beta}';
|
||||
} else {
|
||||
flagToSubmit = 'flag{alpha}';
|
||||
}
|
||||
// Solve it via the submit endpoint to populate solvers.
|
||||
await request(app.getHttpServer())
|
||||
.post(`/api/v1/challenges/${cardId}/solves`)
|
||||
.set('Cookie', `csrf=${adminCsrf}`)
|
||||
.set('X-CSRF-Token', adminCsrf)
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.send({ flag: list.body.columns[0].cards[0].name === 'alphacipher' ? 'flag{alpha}' : 'flag{zeta}' })
|
||||
.send({ flag: flagToSubmit })
|
||||
.expect(200);
|
||||
|
||||
const res = await request(app.getHttpServer())
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user