Files
HIPCTF2/tests/frontend/admin-challenges-form.spec.ts
T

200 lines
6.9 KiB
TypeScript

import '@angular/compiler';
import { FormBuilder } from '@angular/forms';
import {
buildChallengeFormGroup,
defaultMinimumPoints,
syncChallengeForm,
} from '../../frontend/src/app/features/admin/challenges/challenge-form-modal.component';
import {
ChallengeFormDefaults,
ChallengeFormFileItem,
ChallengeStagedFileUi,
buildInitialFiles,
defaultMinimumPoints as pureDefault,
deriveMinimumPoints,
isValidIpOrHostname,
mapServerFieldErrors,
prefillChallengeFormDefaults,
toCreateBody,
validateChallengeForm,
} from '../../frontend/src/app/features/admin/challenges/challenge-form.pure';
import { AdminChallengeDetail } from '../../frontend/src/app/core/services/admin.service';
function makeDetail(overrides: Partial<AdminChallengeDetail> = {}): AdminChallengeDetail {
return {
id: 'd1',
name: 'Sample',
descriptionMd: '# hi',
categoryId: 'cat-1',
categoryAbbreviation: 'CRY',
categoryIconPath: '/uploads/icons/CRY.png',
difficulty: 'MEDIUM',
initialPoints: 200,
minimumPoints: 100,
decaySolves: 5,
flag: 'flag{X}',
protocol: 'NC',
port: 1337,
ipAddress: '127.0.0.1',
enabled: true,
createdAt: '2026-07-22T00:00:00.000Z',
updatedAt: '2026-07-22T00:00:00.000Z',
files: [
{
id: 'f1',
originalFilename: 'manual.txt',
mimeType: 'text/plain',
sizeBytes: 42,
createdAt: '2026-07-22T00:00:00.000Z',
},
],
...overrides,
};
}
function makeDefaults(overrides: Partial<ChallengeFormDefaults> = {}): ChallengeFormDefaults {
return {
name: 'X',
descriptionMd: 'd',
categoryId: 'cat-1',
difficulty: 'MEDIUM',
initialPoints: 100,
minimumPoints: 50,
decaySolves: 10,
flag: 'f',
protocol: 'WEB',
port: null,
ipAddress: '',
enabled: true,
...overrides,
};
}
describe('pure: challenge form helpers', () => {
it('defaultMinimumPoints returns half of the initial points, floored, with a minimum of 1', () => {
expect(defaultMinimumPoints(100)).toBe(50);
expect(defaultMinimumPoints(1)).toBe(1);
expect(defaultMinimumPoints(3)).toBe(1);
expect(pureDefault(99)).toBe(49);
});
it('deriveMinimumPoints follows the auto-derived rule until the minimum is touched, then preserves the value', () => {
expect(deriveMinimumPoints(200, 100, 100, false)).toBe(100); // touched: kept
expect(deriveMinimumPoints(200, 50, 100, false)).toBe(100); // not touched, previous matched default
expect(deriveMinimumPoints(50, 50, 100, false)).toBe(25); // auto-derives because new initial
});
it('isValidIpOrHostname accepts IPv4, IPv6, hostnames and rejects garbage', () => {
expect(isValidIpOrHostname('127.0.0.1')).toBe(true);
expect(isValidIpOrHostname('::1')).toBe(true);
expect(isValidIpOrHostname('2001:db8::1')).toBe(true);
expect(isValidIpOrHostname('example.com')).toBe(true);
expect(isValidIpOrHostname('')).toBe(true);
expect(isValidIpOrHostname('not a host')).toBe(false);
expect(isValidIpOrHostname('-bad.com')).toBe(false);
});
it('validateChallengeForm flags missing name, min > initial, NC without port, invalid IP', () => {
const errs = validateChallengeForm(
makeDefaults({
name: '',
descriptionMd: '',
categoryId: '',
flag: '',
protocol: 'NC',
port: null,
initialPoints: 100,
minimumPoints: 200,
decaySolves: 0,
ipAddress: 'bad value',
}),
);
expect(errs.name).toBeDefined();
expect(errs.descriptionMd).toBeDefined();
expect(errs.categoryId).toBeDefined();
expect(errs.minimumPoints).toBeDefined();
expect(errs.flag).toBeDefined();
expect(errs.port).toBeDefined();
expect(errs.decaySolves).toBeDefined();
expect(errs.ipAddress).toBeDefined();
});
it('validateChallengeForm accepts a well-formed WEB challenge with optional port', () => {
const errs = validateChallengeForm(makeDefaults());
expect(errs).toEqual({});
});
it('mapServerFieldErrors flattens a Zod details array into per-field messages', () => {
const errs = mapServerFieldErrors([
{ path: 'name', message: 'taken' },
{ path: 'port', message: 'required' },
]);
expect(errs.name).toBe('taken');
expect(errs.port).toBe('required');
});
it('prefillChallengeFormDefaults hydrates every field from the detail', () => {
const d = makeDetail({ minimumPoints: 75, protocol: 'WEB', port: 443 });
const out = prefillChallengeFormDefaults(d);
expect(out.name).toBe('Sample');
expect(out.minimumPoints).toBe(75);
expect(out.protocol).toBe('WEB');
expect(out.port).toBe(443);
});
it('toCreateBody omits WEB port when null and keeps NC port', () => {
const web = toCreateBody(makeDefaults({ protocol: 'WEB', port: null }), []);
expect(web.port).toBeNull();
const nc = toCreateBody(makeDefaults({ protocol: 'NC', port: 1337 }), []);
expect(nc.port).toBe(1337);
});
it('toCreateBody maps staged files into the manifest, including removal markers', () => {
const files: ChallengeFormFileItem[] = [
{ id: 'f1', originalFilename: '', mimeType: '', sizeBytes: 0, remove: true },
{
stagedToken: 'tok',
originalFilename: 'a.txt',
mimeType: 'text/plain',
sizeBytes: 5,
},
];
const out = toCreateBody(makeDefaults(), files);
expect(out.files?.length).toBe(2);
expect(out.files?.[0].remove).toBe(true);
expect(out.files?.[1].stagedToken).toBe('tok');
});
it('buildInitialFiles hydrates a UI list from detail.files', () => {
const list: ChallengeStagedFileUi[] = buildInitialFiles(makeDetail());
expect(list.length).toBe(1);
expect(list[0].kind).toBe('existing');
expect(list[0].id).toBe('f1');
});
});
describe('syncChallengeForm', () => {
it('populates every control from the detail and marks them pristine', () => {
const form = buildChallengeFormGroup(new FormBuilder());
const out = syncChallengeForm(form, 'edit', makeDetail());
expect(form.controls.name.value).toBe('Sample');
expect(form.controls.initialPoints.value).toBe(200);
expect(form.controls.minimumPoints.value).toBe(100);
expect(form.controls.flag.value).toBe('flag{X}');
expect(form.controls.protocol.value).toBe('NC');
expect(form.controls.port.value).toBe(1337);
expect(form.controls.ipAddress.value).toBe('127.0.0.1');
expect(form.controls.name.pristine).toBe(true);
expect(out.minimumPoints).toBe(100);
});
it('resets every control when switching to create mode with null detail', () => {
const form = buildChallengeFormGroup(new FormBuilder());
syncChallengeForm(form, 'edit', makeDetail());
const out = syncChallengeForm(form, 'create', null);
expect(form.controls.name.value).toBe('');
expect(form.controls.initialPoints.value).toBe(100);
expect(form.controls.minimumPoints.value).toBe(50);
expect(out.protocol).toBe('WEB');
});
});