Files
HIPCTF2/.kilo/plans/843.md
T
2026-07-21 16:11:04 +00:00

9.4 KiB

Implementation Plan: First-Start Create Admin Modal — Empty-Field Validation Exercise

Status: NOT ALREADY IMPLEMENTED

The feature is partially in place. The SetupCreateAdminComponent (the modal loaded by the /bootstrap route) already contains reactive-form validators (required / minlength / maxlength / pattern on username, required on password + passwordConfirm, plus the group-level passwordMatchValidator), and submit() already calls form.markAllAsTouched() before bailing out on invalid forms. However, the submit button is hard-disabled while the form is invalid (setup-create-admin.component.html:57): [disabled]="submitting() || form.invalid", so the user can never actually click submit while fields are empty/missing — the empty-field validation path is unreachable. The job requires that submitting an empty / partially filled form keeps the overlay open and surfaces field validation without creating an administrator; today that interaction cannot be exercised.

The "Already Implemented Check" therefore does not pass and a code change is required.


1. Architectural Reconnaissance

  • Codebase style & conventions:
    • Monorepo: NestJS REST API in backend/ + Angular 20 standalone SPA in frontend/. Workspace orchestration via root package.json (workspaces: ["backend", "frontend"]).
    • TypeScript strict mode (tsconfig.base.json), OnPush change detection on every component, inject()-based DI, Signals + computed() + effect().
    • Reactive forms via FormBuilder.nonNullable.group, custom validators kept in standalone files (setup-create-admin.validators.ts).
    • @if / @for new control-flow syntax (no *ngIf/*ngFor).
  • Data Layer: SQLite via TypeORM (backend/src/database/entities/user.entity.ts, setting.entity.ts). No schema migration is required for this job — the bug is purely client-side UX gating.
  • Test Framework & Structure:
    • Jest 29 with two projects defined in tests/jest.config.js: backend (node) and frontend (jsdom).
    • Tests live under tests/backend/ and tests/frontend/, never inside source folders.
    • Single root command: npm test (mapped to jest --config tests/jest.config.js). Sub-commands npm run test:backend / npm run test:frontend exist.
    • Setup script: bash setup.sh (no new system tools needed).
  • Required Tools & Dependencies: None new. Existing stack (Angular 20, @angular/forms, @angular/common/http, jest, ts-jest, jest-environment-jsdom) is sufficient.

2. Impacted Files

  • To Modify:
    • frontend/src/app/features/setup/setup-create-admin.component.html — relax the submit-button disabled binding so empty-field submission is exercisable; submit still must not double-fire.
    • frontend/src/app/features/setup/setup-create-admin.component.ts — make submit() an idempotent no-op when already in flight, and ensure that when form.invalid the call only marks touched (it already does) so the validation messages render under data-testid="setup-username-error" / data-testid="setup-confirm-error".
    • frontend/src/app/features/setup/setup-create-admin.component.ts — guard the "already initialized → bounce" effect() so that the modal can still be opened when an integration test seeds the BootstrapService.initialized signal as false (the existing alreadyInitialized computed is already correct; no change required, but the plan documents the expected harness setup).
  • To Create:
    • tests/frontend/setup-create-admin-submit.spec.ts — focused spec asserting that (a) clicking submit with empty fields keeps the modal open, (b) the username required / minlength / pattern errors render, (c) the password-confirm mismatch error renders, and (d) the SetupCreateAdminService.create() call is not made.

3. Proposed Changes

  1. HTML — relax submit-button gating

    • In setup-create-admin.component.html change:
      [disabled]="submitting() || form.invalid"
      
      to:
      [disabled]="submitting()"
      
    • Keep data-testid="setup-submit" and the [type="submit"] so existing test selectors and the (ngSubmit)="submit()" binding continue to work. Add attr.aria-disabled only if needed for a11y — not required by the job.
    • Why: lets the user press the button with empty/missing fields. The component's submit() already short-circuits via markAllAsTouched() + early return when form.invalid, so no server call fires.
  2. Component TS — harden submit() re-entry

    • In setup-create-admin.component.ts:99-129, the existing submit() already guards with if (this.submitting()) return; and markAllAsTouched() then if (this.form.invalid) return;. No structural change is required; the only edit is to document in a one-line code comment that empty/invalid submissions are intentionally allowed through the UI so the validation branch is reachable. (Per project policy, no gratuitous comments — this is only added if the team prefers; the implementer can skip it.)
    • Concrete visible behavior unchanged: when form is invalid the usernameErrors() and confirmErrors() computed()s already surface data-testid="setup-username-error" / data-testid="setup-confirm-error" strings because markAllAsTouched() flips c.touched to true.
  3. Component TS — already-initialized effect stays as-is

    • The effect() at lines 39-43 still bounces to /login (or / when authenticated) when the installation is already initialized. This is the documented product behavior per docs/guides/bootstrap.md ("the setup route becomes a 404 and the modal can never appear again"). The job is explicitly scoped to the uninitialized-installation path, so no change here.
    • The test plan therefore seeds BootstrapService.initialized as false before instantiating the component (the standard pattern used by guard-bootstrap-race.spec.ts and the existing setup-create-admin.spec.ts validator-only spec).
  4. No backend / no DB change. POST /api/v1/setup/create-admin already returns 400 VALIDATION_FAILED for empty bodies via the zod schema (backend/src/modules/setup/dto/create-admin.dto.ts), but the frontend never needs to hit that endpoint because the form is rejected client-side first.

4. Test Strategy

  • Target Unit Test File: tests/frontend/setup-create-admin-submit.spec.ts (new), kept under the existing tests/frontend/ Jest project (jsdom, @angular/forms reactive form runtime, TestBed).

  • Mocking Strategy:

    • Use TestBed.configureTestingModule({ imports: [SetupCreateAdminComponent] }) per the project's standalone-component pattern (see existing tests/frontend/setup-create-admin.spec.ts and the angular-testing skill rules).
    • Stub BootstrapService with a tiny TestBed.runInInjectionContext provider that exposes initialized = signal(false) and passwordPolicy = signal({ description: '...', minLength: 12, requireMixed: true }). No HttpTestingController needed because the success path is never reached; for the mismatch-error assertion we can populate two valid-password controls directly via form.patchValue to bypass minLength/required.
    • Stub AuthService and Router with jest.fn()-style test doubles so navigation / session mutations can be asserted (they should NOT fire in the empty-field cases).
    • Spy on SetupCreateAdminService.create via provideHttpClientTesting() (preferred per skill angular-testing §2) and assert .expectNone(...) / .verify() to prove no network call was made.
    • Drive the DOM via Angular's ComponentFixture.debugElement.query using the documented data-testid attributes (setup-username, setup-password, setup-password-confirm, setup-submit, setup-username-error, setup-confirm-error) — no visual confirmation required.
  • Assertions:

    1. With empty form, click [data-testid="setup-submit"] → modal remains mounted (component is not destroyed) and [data-testid="setup-username-error"] text equals "Username is required".
    2. With username = "ab" and empty passwords → submit click does not invoke SetupCreateAdminService.create (no HTTP request flushed) and the username error reads "Username must be at least 3 characters".
    3. With password = "Sup3r!" and passwordConfirm = "Different!" → after markAllAsTouched(), [data-testid="setup-confirm-error"] renders "Passwords do not match".
    4. The button's disabled attribute reflects submitting() only (assert disabled === false when form is invalid and not submitting).
  • Runnability: Picked up automatically by npm run test:frontend and npm test. No new deps, no visual checks, no extra setup.


5. Out of Scope

  • Backend setup endpoint behavior (already correct: returns 400 VALIDATION_FAILED for empty payloads).
  • /api/v1/bootstrap returning initialized:false — already implemented in backend/src/modules/system/system.service.ts:36-49.
  • Changing the /bootstrap route guard or the decideAuthRedirect function (already correct per tests/frontend/guard-bootstrap-race.spec.ts).
  • Any schema migration.