7.6 KiB
Implementation Plan: Fix Admin Challenge Create Validation (All Fields "Required")
1. Architectural Reconnaissance
- Codebase style & conventions: TypeScript end-to-end, NestJS 10 REST API with
@Controller/@UseGuards/@Bodyparameter pipes, Angular 17+ standalone components using signals +ReactiveFormsModule. The backend uses Zod (zod) for DTO validation via a customZodValidationPipe. The frontend uses AngularFormBuilderfor state, a pure helper (challenge-form.pure.ts) for payload shaping, and an AngularHttpClient-basedAdminService. - Data Layer: TypeORM +
better-sqlite3(./data/SQLite file). Entities underbackend/src/database/entities/. Schema is migration-driven. - Test Framework & Structure: Jest multi-project (
tests/jest.config.js) with two projects —backend(Node env,tests/backend/**/*.spec.ts) andfrontend(jsdom env,tests/frontend/**/*.spec.ts). Run withnpm testfrom the repo root. Frontend tests already cover the pure helpers inchallenge-form.pure.ts(tests/frontend/admin-challenges-form.spec.ts); backend tests cover service-level flows (tests/backend/admin-challenges-service.spec.ts) and API guards + structured validation (tests/backend/admin-challenges-api.spec.ts). - Required Tools & Dependencies: No new dependencies. The fix is contained in the frontend. Backend, schemas, and entity are unchanged.
2. Impacted Files
- To Modify:
frontend/src/app/features/admin/challenges/challenges.component.ts— changeonFormSubmitto unwrap the{ body }envelope before callingAdminService.createChallenge/updateChallenge. The modal already emits{ body }(seechallenge-form-modal.component.ts:528), so the parent must extractbodyfirst. This is the root cause of the bug.tests/frontend/admin-challenges-form.spec.ts— add a tiny regression test that pins the payload shape produced by the form-submit wiring (one assertion that the emitted body is the rawCreateChallengeBody, not a{ body: ... }wrapper).
- To Create:
- None. The backend DTOs (
backend/src/modules/admin/dto/challenges.dto.ts), service (backend/src/modules/admin/challenges.service.ts), and controller (backend/src/modules/admin/admin-challenges.controller.ts) are already correct and need no changes.
- None. The backend DTOs (
3. Proposed Changes
-
Root cause (no schema change required): The frontend form modal already builds the correct
CreateChallengeBodyviatoCreateBody(...)(challenge-form.pure.ts:158) and emits it wrapped:this.submit.emit({ body })(challenge-form-modal.component.ts:528). The smart container atchallenges.component.ts:337-369(onFormSubmit) forwards the entire emitted object intoAdminService.createChallenge(payload)/updateChallenge(id, payload)(admin.service.ts:268and:276). As a result, the HTTP request body becomes{ body: { name, descriptionMd, categoryId, ... } }— every field is one level too deep. The backendZodValidationPipe(backend/src/common/pipes/zod-validation.pipe.ts) runsCreateChallengeSchema.safeParse(value)wherevalueis that wrapped object. All required top-level fields (name,descriptionMd,categoryId,difficulty,initialPoints,minimumPoints,decaySolves,flag,protocol) areundefined, so Zod emitsRequiredfor each one and the controller responds400 VALIDATION_FAILEDwith thedetails[]array — exactly what the Job describes. -
Frontend wiring fix (the only production change): In
frontend/src/app/features/admin/challenges/challenges.component.ts, change the body ofonFormSubmit(payload)so the wrapped envelope is unwrapped before hitting the HTTP layer:async onFormSubmit(payload: { body: CreateChallengeBody | UpdateChallengeBody }): Promise<void> { this.saving.set(true); this.formError.set(null); this.fieldErrors.set(null); const body = payload.body; try { if (this.formMode() === 'create') { await this.admin.createChallenge(body as CreateChallengeBody); } else { const detail = this.formDetail(); if (!detail) throw new Error('Missing challenge detail'); await this.admin.updateChallenge(detail.id, body as UpdateChallengeBody); } // ...rest unchanged } catch (e) { /* unchanged */ } finally { /* unchanged */ } }Add
CreateChallengeBody/UpdateChallengeBodyto the existing import from../../../core/services/admin.serviceat the top of the file (they are already exported there). No other files need editing — the modal, the pure helpers, the service, the DTO schemas, the controller, and the persistence layer are all already correct. -
Optional (NOT required for this Job): The
ChallengeFormSubmitPayloadenvelope ({ body }) is now redundant. Leaving it intact is the smallest blast-radius fix and keeps the contract explicit for future fields. No change here. -
Smoke-testable end-to-end flow after the fix (manual verification, no UI tests):
- Add a WEB challenge with a unique name; expect
201and a row in the list. - Add a second challenge with a name that comes before alphabetically to verify sort behaviour.
- Type in the search box to verify
q=debounce filters case-insensitively. - Toggle the Name sort header to verify asc/desc.
- Add a WEB challenge with a unique name; expect
4. Test Strategy
- Target Test File:
tests/frontend/admin-challenges-form.spec.ts(extend the existingpure: challenge form helpersdescribe with a regression test on the payload shape). - Mocking Strategy: No new mocking needed. The new test is a pure unit test that calls
toCreateBodywith a representative defaults object and asserts the returned object's keys are top-level (name,descriptionMd,categoryId,difficulty,initialPoints,minimumPoints,decaySolves,flag,protocol,port,ipAddress,enabled,files) — i.e. there is no nestedbodyproperty. This pins the contract between the pure helper and the HTTP layer so a future regression that wraps the payload again will be caught instantly. - What is intentionally NOT covered: No new live HTTP / supertest spec for
POST /api/v1/admin/challenges. The existingtests/backend/admin-challenges-api.spec.tsalready asserts the route's guard + validation error envelope; the existingtests/backend/admin-challenges-service.spec.tsalready exercisesAdminChallengesService.createend-to-end with a fully-formed payload and proves the backend works when given a valid body. Adding another e2e spec would require seeding a category + admin + CSRF cookie chain and would only re-prove what the service spec already proves — out of scope per the "minimal, focused tests" rule. - Run:
npm test(ornpm run test:frontendfor the targeted project).
Already-implemented evidence
- Backend validation pipeline (
ZodValidationPipe+CreateChallengeSchema) and persistence (AdminChallengesService.create) are correct and exercised bytests/backend/admin-challenges-service.spec.ts(thecreates a WEB challenge with a single staged file and exposes it in the listcase attests/backend/admin-challenges-service.spec.ts:88). - Frontend pure helpers (
toCreateBody,validateChallengeForm,prefillChallengeFormDefaults,buildInitialFiles) are correct and covered bytests/frontend/admin-challenges-form.spec.ts. - The defect is exclusively the missing unwrap in
challenges.component.ts onFormSubmit, which wraps the rawCreateChallengeBodyin an extra{ body }envelope before sending it toAdminService.createChallenge. The fix is one site, one new regression assertion, no schema or API change.