191 lines
9.4 KiB
Markdown
191 lines
9.4 KiB
Markdown
# 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. |