# 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: ```html [disabled]="submitting() || form.invalid" ``` to: ```html [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.