AI Implementation feature(901): Admin Area Challenges: List, Import/Export and Add/Edit Modal 1.01 (#42)

This commit was merged in pull request #42.
This commit is contained in:
2026-07-22 21:26:25 +00:00
parent 3d93693fc6
commit d3307c3225
7 changed files with 394 additions and 98 deletions
@@ -12,8 +12,10 @@ import {
buildInitialFiles,
defaultMinimumPoints as pureDefault,
deriveMinimumPoints,
firstErrorTab,
isValidIpOrHostname,
mapServerFieldErrors,
partitionFilesBySize,
prefillChallengeFormDefaults,
toCreateBody,
validateChallengeForm,
@@ -216,4 +218,100 @@ describe('syncChallengeForm', () => {
expect(form.controls.minimumPoints.value).toBe(50);
expect(out.protocol).toBe('WEB');
});
});
class StubAdminService {
stageChallengeFile = jest.fn(async (file: File) => ({
token: `tok-${file.name}`,
publicUrl: `/uploads/challenges/${file.name}`,
originalFilename: file.name,
mimeType: file.type || 'application/octet-stream',
sizeBytes: file.size,
}));
discardChallengeFile = jest.fn(async () => undefined);
createChallenge = jest.fn(async () => undefined);
updateChallenge = jest.fn(async () => undefined);
getChallengeDetail = jest.fn(async () => null);
}
describe('challenge form modal: regression Job 901 (pure)', () => {
it('firstErrorTab maps port errors to the connection tab', () => {
expect(firstErrorTab({ port: 'Port is required for NC' })).toBe('connection');
expect(firstErrorTab({ ipAddress: 'bad' })).toBe('connection');
expect(firstErrorTab({ protocol: 'bad' })).toBe('connection');
expect(firstErrorTab({ name: 'required' })).toBe('general');
expect(firstErrorTab({ flag: 'required' })).toBe('general');
expect(firstErrorTab({})).toBeNull();
});
it('Case 3: blank NC port triggers validateChallengeForm port error', () => {
const errs = validateChallengeForm(
makeDefaults({
protocol: 'NC',
port: null,
}),
);
expect(errs.port).toBe('Port is required for NC');
});
it('Case 3: port FormControl has a pattern validator that rejects non-digits', () => {
const form = buildChallengeFormGroup(new FormBuilder());
form.controls.port.setValue('12a3' as unknown as number);
form.controls.port.markAsTouched();
expect(form.controls.port.errors?.['pattern']).toBeDefined();
});
it('Case 4: validateChallengeForm rejects blank NC port regardless of blank IP', () => {
const errs = validateChallengeForm(
makeDefaults({
protocol: 'NC',
port: null,
ipAddress: '',
}),
);
expect(errs.port).toBe('Port is required for NC');
expect(errs.ipAddress).toBeUndefined();
});
it('Case 5: oversize file is rejected by partitionFilesBySize', () => {
const small = new File([new Uint8Array(100)], 'ok.txt', { type: 'text/plain' });
const big = new File([new Uint8Array(2048)], 'big.bin', { type: 'application/octet-stream' });
const out = partitionFilesBySize([small, big], 1024);
expect(out.accepted.length).toBe(1);
expect(out.accepted[0].file).toBe(small);
expect(out.rejected.length).toBe(1);
expect(out.rejected[0].file).toBe(big);
expect(out.rejected[0].reason).toContain('big.bin');
expect(out.rejected[0].reason).toContain('global size limit');
});
it('Case 6: drop-zone partitioning routes valid files to be staged', async () => {
const stub = new StubAdminService();
const small = new File([new Uint8Array(10)], 'dropped.txt', { type: 'text/plain' });
const dt = { files: [small], dropEffect: 'copy' } as unknown as DataTransfer;
const ev = { preventDefault: jest.fn(), dataTransfer: dt } as unknown as DragEvent;
expect(ev.dataTransfer?.files.length).toBe(1);
const outcome = partitionFilesBySize(
Array.from(ev.dataTransfer!.files),
50 * 1024 * 1024,
);
expect(outcome.accepted.length).toBe(1);
expect(outcome.rejected.length).toBe(0);
expect(outcome.accepted[0].file.name).toBe('dropped.txt');
const staged = await stub.stageChallengeFile(outcome.accepted[0].file);
expect(staged.token).toBe('tok-dropped.txt');
});
it('Case 6: drop-zone partitioning rejects oversize files without network call', () => {
const stub = new StubAdminService();
const big = new File([new Uint8Array(2048)], 'big.bin', { type: 'application/octet-stream' });
const dt = { files: [big], dropEffect: 'copy' } as unknown as DataTransfer;
const outcome = partitionFilesBySize(
Array.from(dt.files),
1024,
);
expect(outcome.accepted.length).toBe(0);
expect(outcome.rejected.length).toBe(1);
expect(stub.stageChallengeFile).not.toHaveBeenCalled();
});
});