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 infrontend/. Workspace orchestration via rootpackage.json(workspaces: ["backend", "frontend"]). - TypeScript strict mode (
tsconfig.base.json),OnPushchange 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/@fornew control-flow syntax (no*ngIf/*ngFor).
- Monorepo: NestJS REST API in
- 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) andfrontend(jsdom). - Tests live under
tests/backend/andtests/frontend/, never inside source folders. - Single root command:
npm test(mapped tojest --config tests/jest.config.js). Sub-commandsnpm run test:backend/npm run test:frontendexist. - Setup script:
bash setup.sh(no new system tools needed).
- Jest 29 with two projects defined in
- 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-buttondisabledbinding so empty-field submission is exercisable; submit still must not double-fire.frontend/src/app/features/setup/setup-create-admin.component.ts— makesubmit()an idempotent no-op when already in flight, and ensure that whenform.invalidthe call only marks touched (it already does) so the validation messages render underdata-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 theBootstrapService.initializedsignal asfalse(the existingalreadyInitializedcomputed 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) theSetupCreateAdminService.create()call is not made.
3. Proposed Changes
-
HTML — relax submit-button gating
- In
setup-create-admin.component.htmlchange:to:[disabled]="submitting() || form.invalid"[disabled]="submitting()" - Keep
data-testid="setup-submit"and the[type="submit"]so existing test selectors and the(ngSubmit)="submit()"binding continue to work. Addattr.aria-disabledonly 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 viamarkAllAsTouched()+ early return whenform.invalid, so no server call fires.
- In
-
Component TS — harden submit() re-entry
- In
setup-create-admin.component.ts:99-129, the existingsubmit()already guards withif (this.submitting()) return;andmarkAllAsTouched()thenif (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()andconfirmErrors()computed()s already surfacedata-testid="setup-username-error"/data-testid="setup-confirm-error"strings becausemarkAllAsTouched()flipsc.touchedto true.
- In
-
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 perdocs/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.initializedasfalsebefore instantiating the component (the standard pattern used byguard-bootstrap-race.spec.tsand the existingsetup-create-admin.spec.tsvalidator-only spec).
- The
-
No backend / no DB change.
POST /api/v1/setup/create-adminalready returns400 VALIDATION_FAILEDfor 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 existingtests/frontend/Jest project (jsdom,@angular/formsreactive form runtime,TestBed). -
Mocking Strategy:
- Use
TestBed.configureTestingModule({ imports: [SetupCreateAdminComponent] })per the project's standalone-component pattern (see existingtests/frontend/setup-create-admin.spec.tsand theangular-testingskill rules). - Stub
BootstrapServicewith a tinyTestBed.runInInjectionContextprovider that exposesinitialized = signal(false)andpasswordPolicy = signal({ description: '...', minLength: 12, requireMixed: true }). NoHttpTestingControllerneeded because the success path is never reached; for the mismatch-error assertion we can populate two valid-password controls directly viaform.patchValueto bypassminLength/required. - Stub
AuthServiceandRouterwithjest.fn()-style test doubles so navigation / session mutations can be asserted (they should NOT fire in the empty-field cases). - Spy on
SetupCreateAdminService.createviaprovideHttpClientTesting()(preferred per skillangular-testing§2) and assert.expectNone(...)/.verify()to prove no network call was made. - Drive the DOM via Angular's
ComponentFixture.debugElement.queryusing the documenteddata-testidattributes (setup-username,setup-password,setup-password-confirm,setup-submit,setup-username-error,setup-confirm-error) — no visual confirmation required.
- Use
-
Assertions:
- 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". - With
username = "ab"and empty passwords → submit click does not invokeSetupCreateAdminService.create(no HTTP request flushed) and the username error reads"Username must be at least 3 characters". - With
password = "Sup3r!"andpasswordConfirm = "Different!"→ aftermarkAllAsTouched(),[data-testid="setup-confirm-error"]renders"Passwords do not match". - The button's
disabledattribute reflectssubmitting()only (assertdisabled === falsewhen form is invalid and not submitting).
- With empty form, click
-
Runnability: Picked up automatically by
npm run test:frontendandnpm test. No new deps, no visual checks, no extra setup.
5. Out of Scope
- Backend setup endpoint behavior (already correct: returns 400
VALIDATION_FAILEDfor empty payloads). /api/v1/bootstrapreturninginitialized:false— already implemented inbackend/src/modules/system/system.service.ts:36-49.- Changing the
/bootstraproute guard or thedecideAuthRedirectfunction (already correct pertests/frontend/guard-bootstrap-race.spec.ts). - Any schema migration.