From ef9926dfc149ef9fc872a6e482236b5fd5e51de9 Mon Sep 17 00:00:00 2001 From: m0rph3us1987 Date: Wed, 22 Jul 2026 18:38:15 +0000 Subject: [PATCH] AI Implementation feature(896): Admin Area General Settings and Categories 1.14 (#36) --- .kilo/plans/895.md | 276 ------------------ .kilo/plans/896.md | 274 +++++++++++++++++ docs/architecture/key-files.md | 4 +- docs/guides/admin-categories.md | 25 +- docs/index.md | 2 +- .../category-form-modal.component.ts | 70 +++-- .../admin-categories-form-modal.spec.ts | 125 ++++++++ tests/jest.config.js | 8 +- 8 files changed, 483 insertions(+), 301 deletions(-) delete mode 100644 .kilo/plans/895.md create mode 100644 .kilo/plans/896.md create mode 100644 tests/frontend/admin-categories-form-modal.spec.ts diff --git a/.kilo/plans/895.md b/.kilo/plans/895.md deleted file mode 100644 index 3caae65..0000000 --- a/.kilo/plans/895.md +++ /dev/null @@ -1,276 +0,0 @@ -#[ALREADY_IMPLEMENTED] - -# Implementation Plan: Admin Area General Settings and Categories 1.13 (Job 895) - -## Status - -**The job's required behavior is already fully implemented in this repository.** -No additional source changes, schema migrations, or new tests are required to -satisfy the acceptance criteria. The components, validators, endpoints, and -documented behavior already exist; the failure modes described in the bug -report (HTTP 401, malformed `datetime-local`, End ≤ Start, invalid challenge -IP, missing event-state derivation) are all addressed by code that already -lives in the repo. - -This plan therefore documents **where** each requirement is already fulfilled -and explains the evidence under `/repo`. The implementer should NOT add new -features — only verify the existing tests pass and the existing code path -matches the acceptance criteria. - ---- - -## 1. Architectural Reconnaissance - -- **Codebase style & conventions:** TypeScript end-to-end. NestJS 10 + Express - on the backend (modular structure under `backend/src/modules/**`), Angular - 17 standalone components with signals + `ChangeDetectionStrategy.OnPush` on - the frontend under `frontend/src/app/features/**`. Pure helpers live - alongside their consumers (`*.pure.ts`). -- **Data Layer:** SQLite via `better-sqlite3`, accessed through `DatabaseService` - and a `SettingsService` that persists key/value rows in a `setting` table. - Migrations live under `backend/src/database/migrations/`. Persistent user - data and uploads live under `/data/hipctf` (created by `setup.sh`). -- **Test Framework & Structure:** Jest 29 + `ts-jest`. A single root Jest - config at `tests/jest.config.js` defines two projects (`backend` = - `ts-jest` + `node`, `frontend` = `ts-jest` + `jsdom`). All tests live - under the dedicated `tests/` folder (never co-located with source) and - are runnable from the root with `npm test` (and `npm run test:backend`, - `npm run test:frontend`). -- **Required Tools & Dependencies:** No new dependencies are needed. The - existing `package.json` already declares `jest`, `ts-jest`, `jsdom`, - `jest-environment-jsdom`, `@types/jest`, `@types/jsdom`, `typescript`, - `sharp`, plus the workspace dependencies for NestJS and Angular. The - `setup.sh` script already installs root + workspace deps, rebuilds - `better-sqlite3`, and builds the frontend and backend. - -### Frontend / backend topology - -- The Angular SPA is built into `frontend/dist/` by `npm --workspace frontend run build`. -- `main.ts` (backend) statically serves `frontend/dist` (or `…/browser`) when - present, and `SpaFallbackMiddleware` (`backend/src/frontend/spa.controller.ts`) - serves `index.html` for any non-`/api` non-asset `GET`. **No Angular dev - proxy is required** — the SPA and the API share the same origin (default - `http://localhost:3000`), which is exactly what the bug report calls - out as the working configuration. -- All in-app HTTP calls use **relative** `/api/v1/...` paths - (`frontend/src/app/core/services/admin.service.ts:76-141` and throughout - `auth.service.ts`, `bootstrap.service.ts`, …). The `withCredentials: true` - flag is set on every call so the refresh-token cookie is sent. - ---- - -## 2. Impacted Files - -### Existing files that already implement the job (no edits required) - -No files will be modified. The job is fully covered by the following -already-present files: - -- **Frontend pure helpers** — `frontend/src/app/features/admin/general.pure.ts` -- **Frontend component** — `frontend/src/app/features/admin/general.component.ts` -- **Frontend service** — `frontend/src/app/core/services/admin.service.ts` -- **Frontend SPA wiring** — `backend/src/frontend/spa.controller.ts` + `backend/src/main.ts` -- **Frontend tests** — `tests/frontend/admin-general-pure.spec.ts` -- **Backend DTO** — `backend/src/modules/admin/dto/general.dto.ts` -- **Backend controller** — `backend/src/modules/admin/admin-general.controller.ts` -- **Backend service** — `backend/src/modules/admin/general.service.ts` -- **Backend tests** — `tests/backend/admin-general-event-window.spec.ts`, `tests/backend/admin-general-event.spec.ts`, `tests/backend/admin-general-list-themes.spec.ts`, `tests/backend/admin-general-service.spec.ts`, `tests/backend/admin-validation.spec.ts` -- **Categories (already embedded in the same page)** — `frontend/src/app/features/admin/categories/**`, `backend/src/modules/admin/admin-categories.controller.ts`, `backend/src/modules/admin/categories.service.ts`, `backend/src/modules/admin/dto/categories.dto.ts`, `tests/backend/admin-categories-service.spec.ts` - -### To Create - -None. - ---- - -## 3. Proposed Changes - -**None required.** Each acceptance criterion from the job is already -satisfied by the existing implementation. Evidence per requirement: - -### 1. "Accept valid UTC schedules" - -- `frontend/src/app/features/admin/general.pure.ts:126-140` defines - `toIsoUtc(local)`, which parses `YYYY-MM-DDTHH:mm[:ss[.fff]]` via a - regex and constructs `new Date(Date.UTC(...))` so the wall-clock - components are interpreted as **UTC** regardless of the browser's - timezone. The same file defines `toDatetimeLocal(iso)` which uses - `getUTC*` getters to render the picker. -- `backend/src/modules/admin/dto/general.dto.ts:57-58` requires both - `eventStartUtc` and `eventEndUtc` to be `z.string().datetime(...)` - (the zod ISO-8601 helper), and `superRefine` enforces - `eventEndUtc > eventStartUtc` (lines 62-72). -- Lifecycle: `AdminGeneralComponent.applySettings` (`general.component.ts:407-433`) - converts the stored UTC ISO strings to `datetime-local` strings on - load; `onSubmit` (lines 435-463) converts them back via `toIsoUtc` - before issuing `PUT /api/v1/admin/general/settings`. -- Locked in by `tests/frontend/admin-general-pure.spec.ts:79-112` - (datetime helpers) and `tests/backend/admin-general-event-window.spec.ts:19-22`. - -### 2. "Reject malformed values while preserving the prior schedule/value" - -- `isoDatetimeValidator` (`general.pure.ts:142-146`) returns - `{ invalidDatetime: true }` for empty/unparseable strings, including - `''` (this is the strict policy). -- `endAfterStartValidator` (`general.pure.ts:106-116`) returns - `{ endBeforeStart: true }` when both fields are valid and end is not - strictly after start. -- The reactive form uses `Validators.required` + the custom validators - (lines 231-243) and the Save button is bound to - `[disabled]="submitting() || form.invalid"` (line 184). When the - server returns `400 VALIDATION_FAILED`, the assignable values are - unchanged and the response is rendered into `general-save-error` - (lines 185-187). The component also calls `applySettings(updated)` - on success so the form is reset to its validated state. -- The backend `GeneralSettingsSchema` (`general.dto.ts:51-72`) applies - the same trim + ISO-8601 + ordering rules on the server. The - `SettingsService.set` calls in `backend/src/modules/admin/general.service.ts:62-71` - are only invoked after `ZodValidationPipe` has validated the body. -- Locked in by `tests/frontend/admin-general-pure.spec.ts:187-244` - (validator + message helpers) and `tests/backend/admin-general-event-window.spec.ts:26-67`. - -### 3. "Expose timestamp-derived event states and working controls" - -- `deriveEventState(start, end)` (`general.pure.ts:95-104`) returns - `'unconfigured' | 'countdown' | 'running' | 'stopped'` based on the - current `Date.now()`. -- `eventStateLabel` is a `computed` signal bound to the disabled - `general-event-toggle` button (`general.component.ts:247-252` and - template lines 174-176), so the label is always derived from the - current timestamps in the form. -- All other form controls are enabled; only the event-toggle button is - intentionally disabled (it's a status display, not an action). -- Locked in by `tests/frontend/admin-general-pure.spec.ts:18-49` (the - `deriveEventState` suite). - -### 4. "HTTP 401 UNAUTHORIZED on GET /api/v1/admin/general/settings" - -- `AdminGeneralController` (`backend/src/modules/admin/admin-general.controller.ts:11-13`) - is mounted under `UseGuards(AdminGuard)` and `@Roles('admin')`. -- The global `JwtAuthGuard` rejects unauthenticated requests with 401 - *before* the route handler runs. Verified by `tests/backend/admin-guard.spec.ts` - and `tests/backend/csrf-protected-routes.spec.ts`. The component - treats any load failure as `loadError` and renders - `general-error` (`general.component.ts:52-53, 379-382`). - -### 5. "Frontend requests to localhost:4200 returning 404 because no proxy was configured; API on localhost:3000" - -- This configuration is already prevented by the deployment topology: - the Angular SPA is built and served by the **same** NestJS process on - port 3000 (`backend/src/main.ts:96-105` + `backend/src/frontend/spa.controller.ts`). - All HTTP calls use **relative** `/api/v1/...` paths (see `admin.service.ts:76-141`), - so they resolve to `http://localhost:3000/api/v1/...` regardless of - where the SPA was opened from. No `proxy.conf.json` is required and - none is present in `frontend/angular.json`. -- `CORS_ORIGINS` defaults to `http://localhost:4200,http://localhost:3000` - (`main.ts:26`), so the SPA can also be opened from a dev server on - 4200 in development; in production both the SPA and the API live on - 3000. - -### 6. "datetime-local rejected malformed text at the browser control layer" - -- The native browser `datetime-local` rejects malformed text by - reporting the value as an empty string to JavaScript. The component - then sets the form value to `''`, which triggers - `isoDatetimeValidator` → `{ invalidDatetime: true }` → form invalid - → Save disabled. An inline error region - (`general-eventStart-error` / `general-eventEnd-error`) renders the - per-field message via `datetimeMessage` / `eventEndFieldMessage` - helpers (`general.pure.ts:148-176`, template lines 109-128). - -### 7. "End <= Start left Save disabled" - -- `endAfterStartValidator` (`general.pure.ts:106-116`) emits - `{ endBeforeStart: true }` form-level error. -- `eventEndFieldMessage` (`general.pure.ts:165-176`) renders - `"Event end must be after event start."` into - `general-eventEnd-error` when the per-field ISO check passes but the - cross-field `endBeforeStart` error is set. -- `template` lines 121-128 bind the inline error and apply - `aria-invalid="true"` / `aria-describedby` to the end input. -- Locked in by `tests/frontend/admin-general-pure.spec.ts:51-77, 227-244`. - -### 8. "Default challenge IP — valid IPv4 / hostname" - -- `isValidDefaultChallengeAddress` (`general.pure.ts:28-31`) accepts - full IPv4 (no leading zeros on multi-digit octets, four 0–255 parts) - or a valid RFC-1123 hostname with at least two labels and no - all-numeric labels. -- `defaultChallengeAddressError` / `defaultChallengeAddressMessage` - (`general.pure.ts:33-64`) produce the required and format messages. -- `defaultChallengeAddressValidator` (component line 394-405) attaches - `defaultChallengeAddressFormat` to the control when malformed. -- The backend `defaultChallengeIpSchema` (`general.dto.ts:22-49`) trims - and re-validates with `isIP` + the same hostname regex, emitting - `defaultChallengeIp must be a valid IPv4 address or hostname` on - failure. -- Locked in by `tests/frontend/admin-general-pure.spec.ts:246-328` and - `tests/backend/admin-general-event-window.spec.ts` (the schema - requires a valid `defaultChallengeIp` for any successful parse). - -### 9. "Admin Categories page alongside General settings" - -- Embedded via the `` element in the General - Settings template (`general.component.ts:195`). -- `backend/src/modules/admin/admin-categories.controller.ts` exposes - `GET/POST /api/v1/admin/categories` and `PUT/DELETE /:id`. -- `backend/src/modules/admin/categories.service.ts` enforces the - system-row protection, challenge-attached protection, and abbreviation - uppercase + uniqueness rules. Verified by - `tests/backend/admin-categories-service.spec.ts`. - -### 10. "Emit `general` SSE so other tabs see the new theme" - -- `AdminGeneralService.updateSettings` calls `SseHubService.emitEvent` - with `{ topic: 'general', themeKey, registrationsEnabled }` - (`backend/src/modules/admin/general.service.ts:72-76`). Documented in - `docs/guides/admin-general-settings.md` and `docs/api/admin.md`. - ---- - -## 4. Test Strategy - -**No new tests are required.** The repository already contains a single -focused Jest project per technology that locks in the relevant behavior -and is runnable from the root via `npm test`: - -- **Frontend logic (`tests/frontend/admin-general-pure.spec.ts`)** — - covers `deriveEventState`, `endAfterStartValidator`, `toIsoUtc`, - `toDatetimeLocal`, `isoDatetimeValidator`, `datetimeMessage`, - `eventEndFieldMessage`, `pageTitleError` / `pageTitleMessage`, - `isValidDefaultChallengeAddress` / `defaultChallengeAddressError` / - `defaultChallengeAddressMessage`, and `normalizeDefaultChallengeAddress`. - These exercise the pure helpers that the component uses to decide - when Save is enabled and which inline error message to render. -- **Frontend shell + state** — `tests/frontend/admin-shell.spec.ts`, - `tests/frontend/admin-navigation.spec.ts`, - `tests/frontend/shell-led.spec.ts`, `tests/frontend/shell-active-section.spec.ts`. -- **Backend validation** — - `tests/backend/admin-general-event-window.spec.ts` (likely-end sort - and end-before-start), `tests/backend/admin-general-event.spec.ts`, - `tests/backend/admin-general-service.spec.ts`, - `tests/backend/admin-general-list-themes.spec.ts`, - `tests/backend/admin-validation.spec.ts` (the zod schema). -- **Backend auth/guard** — `tests/backend/admin-guard.spec.ts`, - `tests/backend/csrf-protected-routes.spec.ts`. -- **Backend categories** — `tests/backend/admin-categories-service.spec.ts`. - -### Mocking strategy (already in place) - -- Frontend Jest project uses `jsdom` + `tests/frontend/jest.setup.ts` - and consumes only the pure helper module (`*.pure.ts`), so no - `TestBed`, HTTP mocks, or component fixtures are needed for the - validation regressions. -- Backend tests use `tests/backend/db-helper.ts` to provision an - isolated SQLite database under a temp directory and exercise the - `ZodValidationPipe` directly, so no HTTP-level mocking is needed for - the validation cases. - -### Verification command - -```bash -npm test -``` - -This single command runs all Jest projects (`backend` and `frontend`) -from the repo root and confirms every behavior the job requires. diff --git a/.kilo/plans/896.md b/.kilo/plans/896.md new file mode 100644 index 0000000..41da5e4 --- /dev/null +++ b/.kilo/plans/896.md @@ -0,0 +1,274 @@ +# Implementation Plan: Admin Area General Settings and Categories 1.14 (Job 896) + +## 0. Status — NOT yet implemented + +The required behaviour for this Job is **NOT** fully implemented in this +repository. The category edit modal opens but the `Name`, `Abbreviation`, +and `Description` reactive-form controls are not populated with the +selected category's existing values when the user clicks the edit button +(e.g. `cat-edit-CRY`), and the abbreviation input is therefore not marked +`readonly` for system rows. The bug is reproduced every time an existing +category is opened for edit, regardless of whether the user previously +opened it in create mode or not. + +Evidence (read-only inspection): +* `frontend/src/app/features/admin/categories/category-form-modal.component.ts:91-110` + declares a constructor `effect()` that *intends* to call + `this.form.patchValue({ name, abbreviation, description })` and + `this.abbreviationReadonly.set(c.isSystem)` when `mode() === 'edit' && + category()` is truthy. The rest of the source tree (parent component, + service, backend controller/service/DTO, schemas, migrations, and the + existing backend Jest suite) already implements everything the Guide + describes; only the frontend modal's edit-prefill step is broken. + +Because the fix lives entirely in one frontend component (and a new +Jest test), no DB migration, backend code change, dependency addition, +or `setup.sh` change is required. + +--- + +## 1. Architectural Reconnaissance + +- **Codebase style & conventions:** + - Frontend: Angular 17.3 (`/repo/frontend/package.json`), standalone + components, `ChangeDetectionStrategy.OnPush`, signal inputs / + outputs, reactive forms via `fb.nonNullable.group`. + - Backend: NestJS 10 + Express, modular layout under + `backend/src/modules/admin/**`, TypeORM + `better-sqlite3`. + - Tests: Jest 29 with two projects (`backend` = `ts-jest`+`node`, + `frontend` = `ts-jest`+`jsdom`) configured in + `/repo/tests/jest.config.js`. A single `npm test` from the repo + root runs every spec. Frontend specs live under `tests/frontend/` + alongside existing admin specs. +- **Data Layer:** + - SQLite via TypeORM; `category` table has + `id`, `system_key`, `name`, `abbreviation`, `description`, + `icon_path`, `created_at`, `updated_at` columns + (`backend/src/database/entities/category.entity.ts`). + - Categories are CRUD-ed via `AdminCategoriesService` which already + uppercases the abbreviation, protects system rows from + abbreviation edits, and rejects duplicate abbreviations + (`backend/src/modules/admin/categories.service.ts:39-78`). + - User-supplied persistent state is stored under `/data/hipctf` + (created by `setup.sh`); this job reads no project-shared data. +- **Test Framework & Structure:** + - Jest 29 (`ts-jest`). Two projects configured in + `tests/jest.config.js`. Frontend specs in `tests/frontend/`, + backend specs in `tests/backend/`. Single `npm test` (also wired + into `package.json` at the repo root) executes everything. + - No DOM harness library is configured; existing frontend specs + query the rendered DOM through `fixture.nativeElement.querySelector` + on the rendered template (see e.g. + `tests/frontend/admin-general-pure.spec.ts`). +- **Required Tools & Dependencies:** No new dependencies are needed. + All tooling required to fix and verify this job is already + installed by `setup.sh` and the root `package.json` / + `tests/package.json`. The implementer does **not** need to touch + `setup.sh`. + +### Component wiring already present in the repo + +| Layer | File | Status | +|-------|------|--------| +| Edit modal | `frontend/src/app/features/admin/categories/category-form-modal.component.ts` | **buggy** — edit pre-fill does not reach the DOM | +| Categories list + edit dispatcher | `frontend/src/app/features/admin/categories/categories.component.ts:109-114` | correct (`openEdit(c)` sets `editing` and `modalMode`) | +| General page (embeds categories) | `frontend/src/app/features/admin/general.component.ts:195` | already imports `AdminCategoriesComponent` | +| Admin service | `frontend/src/app/core/services/admin.service.ts:96-122` | correct (`listCategories`, `updateCategory`) | +| Backend controller | `backend/src/modules/admin/admin-categories.controller.ts` | correct | +| Backend service + DTO | `backend/src/modules/admin/categories.service.ts`, `backend/src/modules/admin/dto/categories.dto.ts` | correct | +| Backend tests | `tests/backend/admin-categories-service.spec.ts` | correct (system-row, dup, etc.) | +| Guide | `docs/guides/admin-categories.md` | correct (states the modal must pre-fill) | + +--- + +## 2. Impacted Files + +### To Modify (1 file) + +- `frontend/src/app/features/admin/categories/category-form-modal.component.ts` + — replace the broken constructor `effect()` with a robust Angular 17 + reactive-form prefill that always populates `name`, + `abbreviation`, `description`, sets `abbreviationReadonly` from + `c.isSystem`, and resets `iconPreview`/`iconFile` whenever the + parent changes the `category` or `mode` input. Must keep the + existing `[data-testid]` selectors intact. + +### To Create (1 file) + +- `tests/frontend/admin-categories-form-modal.spec.ts` + — Jest spec that mounts `CategoryFormModalComponent` with + `TestBed`, simulates the parent passing `open=true`, `mode='edit'`, + and a fully-populated `AdminCategory` (including `isSystem: true`), + then asserts that: + 1. `input[data-testid="cf-name"]` value equals the row's `name`, + 2. `input[data-testid="cf-abbr"]` value equals the row's + `abbreviation` *and* has the `readonly` attribute when + `isSystem === true`, + 3. `textarea[data-testid="cf-desc"]` value equals the row's + `description`, + 4. switching the inputs to a different category (a second + `setInput('category', ...)` / `setInput('mode', 'edit')` call) + re-populates the form to the new row's values, + 5. switching back to `mode='create'` clears the form. + +--- + +## 3. Proposed Changes + +### 1. Fix the edit-prefill in the modal (`category-form-modal.component.ts`) + +The bug: the existing constructor `effect()` reads +`this.category()` and `this.mode()` and calls +`this.form.patchValue(...)` inside an OnPush component. In Angular 17 +the value-update flow works, but the *display* of the patched value +inside the `` element is not reaching the +DOM because the change originates from a constructor +`effect()` while change-detection is sitting idle, and the +`formControlName` directive's write-back into the native `` +relies on the OnPush view being marked dirty. The same code path also +fails to mark `abbreviationReadonly`, which is exactly what the bug +report observes (abbreviation is not `readonly` and the three fields +read `''`). + +Replace the constructor body so that: + +1. We keep reading both inputs inside the effect: + `const c = this.category(); const m = this.mode();`. +2. We synchronise the form using **individual + `FormControl.setValue` calls** (not `patchValue` on the group), so + the directive chain inside each `FormControlName` runs the + `DefaultValueAccessor.writeValue` path that reaches the native + input. +3. After every prefill (edit or create-reset), we explicitly call + `inject(ChangeDetectorRef).markForCheck()` so the OnPush view + repaints. +4. We still set `abbreviationReadonly.set(c.isSystem)` and + `iconPreview.set(c.iconPath || null)` inside the `edit` branch, and + `abbreviationReadonly.set(false)` / + `iconPreview.set(null)` in the `else` branch. +5. We also clear `iconFile` (already done in current code) and reset + the `description` control in the create branch. +6. We add `allowSignalWrites: true` to the `effect()` options so that + the signal writes inside it are tolerated. + +Pseudocode for the replacement constructor: + +```ts +private readonly cdr = inject(ChangeDetectorRef); + +constructor() { + effect( + () => { + const c = this.category(); + const m = this.mode(); + if (m === 'edit' && c) { + this.form.controls.name.setValue(c.name, { emitEvent: false }); + this.form.controls.abbreviation.setValue(c.abbreviation, { emitEvent: false }); + this.form.controls.description.setValue(c.description ?? '', { emitEvent: false }); + this.abbreviationReadonly.set(!!c.isSystem); + this.iconPreview.set(c.iconPath || null); + } else { + this.form.controls.name.setValue('', { emitEvent: false }); + this.form.controls.abbreviation.setValue('', { emitEvent: false }); + this.form.controls.description.setValue('', { emitEvent: false }); + this.abbreviationReadonly.set(false); + this.iconPreview.set(null); + } + this.iconFile.set(null); + this.form.controls.name.markAsUntouched(); + this.form.controls.name.markAsPristine(); + this.form.controls.abbreviation.markAsUntouched(); + this.form.controls.abbreviation.markAsPristine(); + this.form.controls.description.markAsUntouched(); + this.form.controls.description.markAsPristine(); + this.cdr.markForCheck(); + }, + { allowSignalWrites: true }, + ); +} +``` + +All other parts of the modal (template selectors `cf-name`, +`cf-abbr`, `cf-desc`, `cf-icon`, `cf-cancel`, `cf-ok`, +`cat-form-modal`, `cat-form-backdrop`; the `CategoryFormSubmit` +output; the icon FileReader; the readonly binding on the abbreviation +input) stay exactly the same, so the parent +`AdminCategoriesComponent` and its `submit` handler do not need to +change. + +### 2. Add a frontend Jest spec (`admin-categories-form-modal.spec.ts`) + +The spec file lives under `tests/frontend/`. It runs with the existing +`tests/jest.config.js` `frontend` project (jest + jsdom), is picked up +by `npm test` from the repo root, requires no UI, and asserts the +behaviour the bug report says is broken. + +Mocking strategy: +* No HTTP mocks are required — the modal is rendered in isolation with + its three inputs (`open`, `mode`, `category`) bound directly via + `fixture.componentRef.setInput(...)`. `AdminService` is unused by + the modal. +* Construct `CategoryFormModalComponent` through `TestBed` and target + by querying the existing `data-testid` attributes + (`cf-name`, `cf-abbr`, `cf-desc`). No CSS selectors are introduced. +* Provide `FormBuilder` and `DestroyRef` via DI (already provided + via `inject(FormBuilder)` and `inject(DestroyRef)` because + `TestBed.createComponent` resolves Angular's DI tree). + +Spec assertions (single `describe('edit prefill')`, four `it`s): + +1. `populates name, abbreviation, description for a system row` — + set `open=true`, `mode='edit'`, + `category = { id:'x', name:'Cryptography', abbreviation:'CRY', + description:'Cryptographic challenges', iconPath:'', + isSystem:true }`, call `fixture.detectChanges()`, + read each input's `.value`, expect the row's values, and expect + `cf-abbr` to carry the `readonly` attribute. +2. `populates name, abbreviation, description for a user row` — same + shape but `isSystem: false`; expect `cf-abbr` to NOT be `readonly`. +3. `re-populates when the parent switches to another row` — + `setInput('mode','edit')` with row A, `detectChanges()`, assert + values, then `setInput('category', rowB)`, `detectChanges()`, + assert new values. +4. `clears the form when the parent switches back to create` — + after the edit-prefill assertions, `setInput('mode','create')`, + `setInput('category', null)`, `detectChanges()`, assert empty + values. + +The spec is intentionally minimal (5 assertions per case, no async +timers, no DOM harnesses) so it compiles and runs instantly. + +--- + +## 4. Test Strategy + +- **Target spec file:** + `tests/frontend/admin-categories-form-modal.spec.ts` (new). +- **Mocking strategy:** no HTTP mocks, no service mocks. The modal is + component-only; `fixture.componentRef.setInput(...)` simulates the + parent setting the `open`, `mode`, `category` signals, and + `fixture.detectChanges()` flushes the patched values to the DOM + via the OnPush view marker we add. +- **Coverage scope:** the four bullets above plus a smoke check that + `submit` and `cancel` outputs are not emitted by the modal alone + (the parent owns the network call). +- **Run command:** `npm test` from the repository root + (`tests/jest.config.js` is the active config). The new spec runs + inside the `frontend` project and finishes in well under a second + with no network or DB activity. +- **Existing tests:** re-run `tests/backend/admin-categories-service.spec.ts` + to confirm the system-row / dup / CRUD rules still pass; the + frontend modal change has no backend impact. + +--- + +## 5. Files Touched — Summary + +| Path | Action | +|------|--------| +| `frontend/src/app/features/admin/categories/category-form-modal.component.ts` | edit — replace constructor `effect()` body with the explicit-`setValue` + `markForCheck` version above; add `inject(ChangeDetectorRef)` and `{ allowSignalWrites: true }` option | +| `tests/frontend/admin-categories-form-modal.spec.ts` | create — the four `it()`s described in §3.2 | + +No backend, schema, DTO, controller, service, route, migration, or +dependency change is required, and `setup.sh` does not need to be +modified. diff --git a/docs/architecture/key-files.md b/docs/architecture/key-files.md index e52dc6e..0a2c4d3 100644 --- a/docs/architecture/key-files.md +++ b/docs/architecture/key-files.md @@ -3,7 +3,7 @@ type: architecture title: Key Files Index description: One-line responsibility for important source and contract-test files, including strict event-window validation and the public bootstrap SSE listener. tags: [architecture, key-files, event-window, validation, default-challenge-ip, sse, bootstrap, migrations] -timestamp: 2026-07-22T16:44:54Z +timestamp: 2026-07-22T18:37:00Z --- # Backend @@ -52,8 +52,10 @@ timestamp: 2026-07-22T16:44:54Z | `frontend/src/app/features/shell/tabs/quick-tabs.component.ts` | Main shell navigation tabs. | | `frontend/src/app/features/shell/change-password/change-password-modal.component.ts` | Change-password form modal. | | `frontend/src/app/features/admin/general.component.ts` | `AdminGeneralComponent` reactive form for `/admin/general` — per-field inline error rendering (page-title + event-start + event-end), logo upload wiring, welcome Markdown preview, event-state derivation, and SSE `general` event handling. | +| `frontend/src/app/features/admin/categories/category-form-modal.component.ts` | Standalone OnPush modal for create + edit; owns the `CategoryFormGroup`, exposes the pure `syncCategoryForm` helper, and reacts to `open` / `mode` / `category` signal inputs via a `markForCheck` effect so edit prefill reaches the DOM. | | `frontend/src/app/features/admin/general.pure.ts` | Pure General Settings helpers, including required datetime validation, field messages, UTC conversion, end-after-start validation, and the default-challenge-address IPv4/hostname validator/normalizer/message trio. | | `tests/frontend/admin-general-pure.spec.ts` | Pure client-contract tests for required event timestamps, datetime messaging, UTC conversion, event-window ordering, and default-challenge-address validation, error mapping, and normalization. | +| `tests/frontend/admin-categories-form-modal.spec.ts` | Tests the pure `syncCategoryForm` helper that drives the edit/create prefill in `CategoryFormModalComponent`: system-row abbreviation lock, user-row unlock, re-population on second invocation, clearing on create, and iconPreview passthrough. | | `tests/frontend/authenticated-event-source.spec.ts` | Tests SSE authorization, frame transport behavior, and the `401`/`403` unauthorized path. | | `tests/frontend/auth-session-events.spec.ts` | Pure tests for cross-tab invalidation message encoding, payload validation, and `storage`-event filtering. | diff --git a/docs/guides/admin-categories.md b/docs/guides/admin-categories.md index 4f0bc45..e9780f2 100644 --- a/docs/guides/admin-categories.md +++ b/docs/guides/admin-categories.md @@ -3,7 +3,7 @@ type: guide title: Admin — Categories description: How an admin lists, creates, edits, and deletes challenge categories from the /admin/categories page, including system-row protection and challenge-attached protection. tags: [guide, admin, categories, tester] -timestamp: 2026-07-22T12:00:00Z +timestamp: 2026-07-22T18:37:00Z --- # When this view is available @@ -89,6 +89,29 @@ side-nav (categories are also embedded inside the General settings page). file is selected, the new icon is uploaded first and the returned `publicUrl` replaces `iconPath` in the same update. +### Edit prefill mechanics + +When the parent sets `open=true` + `mode='edit'` + a `category`, a +constructor `effect()` runs `syncCategoryForm(form, mode, category)` +which: + +* writes `name`, `abbreviation`, `description` via per-control + `setValue(..., { emitEvent: false })`, +* marks every control pristine + untouched so validators do not flash + errors on a freshly opened modal, +* returns `{ abbreviationReadonly: category.isSystem === true, + iconPreview: category.iconPath || null }` which the effect pushes + into `abbreviationReadonly` and `iconPreview` signals and clears + `iconFile`, +* and triggers `ChangeDetectorRef.markForCheck()` so the OnPush view + repaints the freshly-patched `` / `