2026-05-23 04:26:56 +02:00
|
|
|
import { describe, it, expect, afterEach, vi } from 'vitest';
|
|
|
|
|
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
|
|
|
|
import { EngagementCreateDialog } from './EngagementCreateDialog';
|
|
|
|
|
import { installFetchMock, renderWithProviders } from '@/test/testUtils';
|
|
|
|
|
|
|
|
|
|
describe('EngagementCreateDialog', () => {
|
|
|
|
|
let fetchMock: ReturnType<typeof installFetchMock>;
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
fetchMock?.restore();
|
|
|
|
|
});
|
|
|
|
|
|
fix(frontend): align types + UI to backend contract (docs/api.md @ dd5c508)
Backend pushed the authoritative contract in docs/api.md and tightened
the error envelope via a global HTTPException handler (dd5c508). This
commit folds the frontend onto that contract — every drift flagged by
the code-reviewer MAJOR is closed.
Types (src/types/api.ts)
- User: `id` → `user_id`; `display_name` is `string | null`; add
`permissions: string[]` and `groups: string[]`; drop `engagement_id`
and `engagement_name` (not part of CurrentUser).
- Engagement: drop `name`, `client_name` is non-null `string`; status
enum aligned to `draft | active | closed | archived`; `c2_type` is
non-null `C2Type`; drop `created_at` (not in EngagementRead v1).
- EngagementCreate body: `client_name` required, plus optional
`description`, `c2_type`, `start_date`, `end_date`. No `name`.
- Replace ApiError + ApiValidationError with a single uniform envelope:
`{ error: string, message: string, details?: PydanticErrorItem[] }`,
matching the new HTTPException handler. PydanticErrorItem is the
per-field shape on 422 (`{ loc, msg, type }`).
Fetch client (src/lib/api.ts)
- `bodyAsApiError` now recognizes the uniform envelope by shape
(error+message strings). Anything else returns null so callers fall
back to a generic message — keeps us robust if the backend ever
emits a non-JSON response.
Engagements API (src/screens/engagements/engagementsApi.ts)
- Drop the `{ items: [] }` envelope tolerance — backend serves a bare
`Engagement[]`.
- Hit `/engagements/` with trailing slash explicitly; backend now sets
`strict_slashes=False` but staying consistent with docs/api.md.
EngagementsPage
- Status tone map switched to the new enum (`draft → pending`,
`closed → soc`).
- Drop "Name" column. `client_name` is the primary identifier; the
description column replaces the now-meaningless name field.
- `c2_type` is non-null, so no nullable rendering path.
EngagementCreateDialog
- Drop `name` field. New required field is `client_name`; add a
`c2_type` select (default `mythic`); brief textarea stays optional.
- `mapValidationErrors` now reads `body.details[*].loc` (last segment
matches the form field) — direct alignment with the backend's new
shape after dd5c508.
- 401 still surfaces "Session expirée"; 403 gains a dedicated message;
other errors fall back to a capitalized backend `message` when
available, then to a generic French string.
Sidebar
- Display fallback: `user.display_name ?? user.username` (now nullable).
- Drop the `ENG · {engagement_name}` line; show `user.username` (the
email) as the secondary identity instead.
LoginPage
- Field label "Username" → "Email or username" so RT users with email
accounts find the field semantically obvious (per docs/api.md note
on the username/email mapping).
Tests (Vitest, 14 cases, all green)
- Refreshed fixtures to the new shapes (no more `name`, no
`created_at`, status `draft`, envelopes carry `error`+`message`).
- New 422 test exercises the `details[*].loc` mapping shape.
- New 401 test on the dialog covers the top-of-form alert path.
2026-05-23 11:14:32 +02:00
|
|
|
it('rejects empty client name client-side without calling the backend', () => {
|
2026-05-23 04:26:56 +02:00
|
|
|
fetchMock = installFetchMock([]);
|
|
|
|
|
renderWithProviders(<EngagementCreateDialog onClose={vi.fn()} />);
|
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /arm engagement/i }));
|
fix(frontend): align types + UI to backend contract (docs/api.md @ dd5c508)
Backend pushed the authoritative contract in docs/api.md and tightened
the error envelope via a global HTTPException handler (dd5c508). This
commit folds the frontend onto that contract — every drift flagged by
the code-reviewer MAJOR is closed.
Types (src/types/api.ts)
- User: `id` → `user_id`; `display_name` is `string | null`; add
`permissions: string[]` and `groups: string[]`; drop `engagement_id`
and `engagement_name` (not part of CurrentUser).
- Engagement: drop `name`, `client_name` is non-null `string`; status
enum aligned to `draft | active | closed | archived`; `c2_type` is
non-null `C2Type`; drop `created_at` (not in EngagementRead v1).
- EngagementCreate body: `client_name` required, plus optional
`description`, `c2_type`, `start_date`, `end_date`. No `name`.
- Replace ApiError + ApiValidationError with a single uniform envelope:
`{ error: string, message: string, details?: PydanticErrorItem[] }`,
matching the new HTTPException handler. PydanticErrorItem is the
per-field shape on 422 (`{ loc, msg, type }`).
Fetch client (src/lib/api.ts)
- `bodyAsApiError` now recognizes the uniform envelope by shape
(error+message strings). Anything else returns null so callers fall
back to a generic message — keeps us robust if the backend ever
emits a non-JSON response.
Engagements API (src/screens/engagements/engagementsApi.ts)
- Drop the `{ items: [] }` envelope tolerance — backend serves a bare
`Engagement[]`.
- Hit `/engagements/` with trailing slash explicitly; backend now sets
`strict_slashes=False` but staying consistent with docs/api.md.
EngagementsPage
- Status tone map switched to the new enum (`draft → pending`,
`closed → soc`).
- Drop "Name" column. `client_name` is the primary identifier; the
description column replaces the now-meaningless name field.
- `c2_type` is non-null, so no nullable rendering path.
EngagementCreateDialog
- Drop `name` field. New required field is `client_name`; add a
`c2_type` select (default `mythic`); brief textarea stays optional.
- `mapValidationErrors` now reads `body.details[*].loc` (last segment
matches the form field) — direct alignment with the backend's new
shape after dd5c508.
- 401 still surfaces "Session expirée"; 403 gains a dedicated message;
other errors fall back to a capitalized backend `message` when
available, then to a generic French string.
Sidebar
- Display fallback: `user.display_name ?? user.username` (now nullable).
- Drop the `ENG · {engagement_name}` line; show `user.username` (the
email) as the secondary identity instead.
LoginPage
- Field label "Username" → "Email or username" so RT users with email
accounts find the field semantically obvious (per docs/api.md note
on the username/email mapping).
Tests (Vitest, 14 cases, all green)
- Refreshed fixtures to the new shapes (no more `name`, no
`created_at`, status `draft`, envelopes carry `error`+`message`).
- New 422 test exercises the `details[*].loc` mapping shape.
- New 401 test on the dialog covers the top-of-form alert path.
2026-05-23 11:14:32 +02:00
|
|
|
expect(screen.getByText(/client requis/i)).toBeInTheDocument();
|
2026-05-23 04:26:56 +02:00
|
|
|
expect(fetchMock.calls).toHaveLength(0);
|
|
|
|
|
});
|
|
|
|
|
|
fix(frontend): align types + UI to backend contract (docs/api.md @ dd5c508)
Backend pushed the authoritative contract in docs/api.md and tightened
the error envelope via a global HTTPException handler (dd5c508). This
commit folds the frontend onto that contract — every drift flagged by
the code-reviewer MAJOR is closed.
Types (src/types/api.ts)
- User: `id` → `user_id`; `display_name` is `string | null`; add
`permissions: string[]` and `groups: string[]`; drop `engagement_id`
and `engagement_name` (not part of CurrentUser).
- Engagement: drop `name`, `client_name` is non-null `string`; status
enum aligned to `draft | active | closed | archived`; `c2_type` is
non-null `C2Type`; drop `created_at` (not in EngagementRead v1).
- EngagementCreate body: `client_name` required, plus optional
`description`, `c2_type`, `start_date`, `end_date`. No `name`.
- Replace ApiError + ApiValidationError with a single uniform envelope:
`{ error: string, message: string, details?: PydanticErrorItem[] }`,
matching the new HTTPException handler. PydanticErrorItem is the
per-field shape on 422 (`{ loc, msg, type }`).
Fetch client (src/lib/api.ts)
- `bodyAsApiError` now recognizes the uniform envelope by shape
(error+message strings). Anything else returns null so callers fall
back to a generic message — keeps us robust if the backend ever
emits a non-JSON response.
Engagements API (src/screens/engagements/engagementsApi.ts)
- Drop the `{ items: [] }` envelope tolerance — backend serves a bare
`Engagement[]`.
- Hit `/engagements/` with trailing slash explicitly; backend now sets
`strict_slashes=False` but staying consistent with docs/api.md.
EngagementsPage
- Status tone map switched to the new enum (`draft → pending`,
`closed → soc`).
- Drop "Name" column. `client_name` is the primary identifier; the
description column replaces the now-meaningless name field.
- `c2_type` is non-null, so no nullable rendering path.
EngagementCreateDialog
- Drop `name` field. New required field is `client_name`; add a
`c2_type` select (default `mythic`); brief textarea stays optional.
- `mapValidationErrors` now reads `body.details[*].loc` (last segment
matches the form field) — direct alignment with the backend's new
shape after dd5c508.
- 401 still surfaces "Session expirée"; 403 gains a dedicated message;
other errors fall back to a capitalized backend `message` when
available, then to a generic French string.
Sidebar
- Display fallback: `user.display_name ?? user.username` (now nullable).
- Drop the `ENG · {engagement_name}` line; show `user.username` (the
email) as the secondary identity instead.
LoginPage
- Field label "Username" → "Email or username" so RT users with email
accounts find the field semantically obvious (per docs/api.md note
on the username/email mapping).
Tests (Vitest, 14 cases, all green)
- Refreshed fixtures to the new shapes (no more `name`, no
`created_at`, status `draft`, envelopes carry `error`+`message`).
- New 422 test exercises the `details[*].loc` mapping shape.
- New 401 test on the dialog covers the top-of-form alert path.
2026-05-23 11:14:32 +02:00
|
|
|
it('maps 422 backend errors (details[].loc) to per-field messages', async () => {
|
2026-05-23 04:26:56 +02:00
|
|
|
fetchMock = installFetchMock([
|
|
|
|
|
{
|
|
|
|
|
status: 422,
|
|
|
|
|
body: {
|
fix(frontend): align types + UI to backend contract (docs/api.md @ dd5c508)
Backend pushed the authoritative contract in docs/api.md and tightened
the error envelope via a global HTTPException handler (dd5c508). This
commit folds the frontend onto that contract — every drift flagged by
the code-reviewer MAJOR is closed.
Types (src/types/api.ts)
- User: `id` → `user_id`; `display_name` is `string | null`; add
`permissions: string[]` and `groups: string[]`; drop `engagement_id`
and `engagement_name` (not part of CurrentUser).
- Engagement: drop `name`, `client_name` is non-null `string`; status
enum aligned to `draft | active | closed | archived`; `c2_type` is
non-null `C2Type`; drop `created_at` (not in EngagementRead v1).
- EngagementCreate body: `client_name` required, plus optional
`description`, `c2_type`, `start_date`, `end_date`. No `name`.
- Replace ApiError + ApiValidationError with a single uniform envelope:
`{ error: string, message: string, details?: PydanticErrorItem[] }`,
matching the new HTTPException handler. PydanticErrorItem is the
per-field shape on 422 (`{ loc, msg, type }`).
Fetch client (src/lib/api.ts)
- `bodyAsApiError` now recognizes the uniform envelope by shape
(error+message strings). Anything else returns null so callers fall
back to a generic message — keeps us robust if the backend ever
emits a non-JSON response.
Engagements API (src/screens/engagements/engagementsApi.ts)
- Drop the `{ items: [] }` envelope tolerance — backend serves a bare
`Engagement[]`.
- Hit `/engagements/` with trailing slash explicitly; backend now sets
`strict_slashes=False` but staying consistent with docs/api.md.
EngagementsPage
- Status tone map switched to the new enum (`draft → pending`,
`closed → soc`).
- Drop "Name" column. `client_name` is the primary identifier; the
description column replaces the now-meaningless name field.
- `c2_type` is non-null, so no nullable rendering path.
EngagementCreateDialog
- Drop `name` field. New required field is `client_name`; add a
`c2_type` select (default `mythic`); brief textarea stays optional.
- `mapValidationErrors` now reads `body.details[*].loc` (last segment
matches the form field) — direct alignment with the backend's new
shape after dd5c508.
- 401 still surfaces "Session expirée"; 403 gains a dedicated message;
other errors fall back to a capitalized backend `message` when
available, then to a generic French string.
Sidebar
- Display fallback: `user.display_name ?? user.username` (now nullable).
- Drop the `ENG · {engagement_name}` line; show `user.username` (the
email) as the secondary identity instead.
LoginPage
- Field label "Username" → "Email or username" so RT users with email
accounts find the field semantically obvious (per docs/api.md note
on the username/email mapping).
Tests (Vitest, 14 cases, all green)
- Refreshed fixtures to the new shapes (no more `name`, no
`created_at`, status `draft`, envelopes carry `error`+`message`).
- New 422 test exercises the `details[*].loc` mapping shape.
- New 401 test on the dialog covers the top-of-form alert path.
2026-05-23 11:14:32 +02:00
|
|
|
error: 'validation_error',
|
|
|
|
|
message: 'request failed',
|
|
|
|
|
details: [
|
2026-05-23 04:26:56 +02:00
|
|
|
{
|
fix(frontend): align types + UI to backend contract (docs/api.md @ dd5c508)
Backend pushed the authoritative contract in docs/api.md and tightened
the error envelope via a global HTTPException handler (dd5c508). This
commit folds the frontend onto that contract — every drift flagged by
the code-reviewer MAJOR is closed.
Types (src/types/api.ts)
- User: `id` → `user_id`; `display_name` is `string | null`; add
`permissions: string[]` and `groups: string[]`; drop `engagement_id`
and `engagement_name` (not part of CurrentUser).
- Engagement: drop `name`, `client_name` is non-null `string`; status
enum aligned to `draft | active | closed | archived`; `c2_type` is
non-null `C2Type`; drop `created_at` (not in EngagementRead v1).
- EngagementCreate body: `client_name` required, plus optional
`description`, `c2_type`, `start_date`, `end_date`. No `name`.
- Replace ApiError + ApiValidationError with a single uniform envelope:
`{ error: string, message: string, details?: PydanticErrorItem[] }`,
matching the new HTTPException handler. PydanticErrorItem is the
per-field shape on 422 (`{ loc, msg, type }`).
Fetch client (src/lib/api.ts)
- `bodyAsApiError` now recognizes the uniform envelope by shape
(error+message strings). Anything else returns null so callers fall
back to a generic message — keeps us robust if the backend ever
emits a non-JSON response.
Engagements API (src/screens/engagements/engagementsApi.ts)
- Drop the `{ items: [] }` envelope tolerance — backend serves a bare
`Engagement[]`.
- Hit `/engagements/` with trailing slash explicitly; backend now sets
`strict_slashes=False` but staying consistent with docs/api.md.
EngagementsPage
- Status tone map switched to the new enum (`draft → pending`,
`closed → soc`).
- Drop "Name" column. `client_name` is the primary identifier; the
description column replaces the now-meaningless name field.
- `c2_type` is non-null, so no nullable rendering path.
EngagementCreateDialog
- Drop `name` field. New required field is `client_name`; add a
`c2_type` select (default `mythic`); brief textarea stays optional.
- `mapValidationErrors` now reads `body.details[*].loc` (last segment
matches the form field) — direct alignment with the backend's new
shape after dd5c508.
- 401 still surfaces "Session expirée"; 403 gains a dedicated message;
other errors fall back to a capitalized backend `message` when
available, then to a generic French string.
Sidebar
- Display fallback: `user.display_name ?? user.username` (now nullable).
- Drop the `ENG · {engagement_name}` line; show `user.username` (the
email) as the secondary identity instead.
LoginPage
- Field label "Username" → "Email or username" so RT users with email
accounts find the field semantically obvious (per docs/api.md note
on the username/email mapping).
Tests (Vitest, 14 cases, all green)
- Refreshed fixtures to the new shapes (no more `name`, no
`created_at`, status `draft`, envelopes carry `error`+`message`).
- New 422 test exercises the `details[*].loc` mapping shape.
- New 401 test on the dialog covers the top-of-form alert path.
2026-05-23 11:14:32 +02:00
|
|
|
loc: ['client_name'],
|
|
|
|
|
msg: 'String should have at least 1 character',
|
2026-05-23 04:26:56 +02:00
|
|
|
type: 'string_too_short',
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
renderWithProviders(<EngagementCreateDialog onClose={vi.fn()} />);
|
|
|
|
|
|
fix(frontend): align types + UI to backend contract (docs/api.md @ dd5c508)
Backend pushed the authoritative contract in docs/api.md and tightened
the error envelope via a global HTTPException handler (dd5c508). This
commit folds the frontend onto that contract — every drift flagged by
the code-reviewer MAJOR is closed.
Types (src/types/api.ts)
- User: `id` → `user_id`; `display_name` is `string | null`; add
`permissions: string[]` and `groups: string[]`; drop `engagement_id`
and `engagement_name` (not part of CurrentUser).
- Engagement: drop `name`, `client_name` is non-null `string`; status
enum aligned to `draft | active | closed | archived`; `c2_type` is
non-null `C2Type`; drop `created_at` (not in EngagementRead v1).
- EngagementCreate body: `client_name` required, plus optional
`description`, `c2_type`, `start_date`, `end_date`. No `name`.
- Replace ApiError + ApiValidationError with a single uniform envelope:
`{ error: string, message: string, details?: PydanticErrorItem[] }`,
matching the new HTTPException handler. PydanticErrorItem is the
per-field shape on 422 (`{ loc, msg, type }`).
Fetch client (src/lib/api.ts)
- `bodyAsApiError` now recognizes the uniform envelope by shape
(error+message strings). Anything else returns null so callers fall
back to a generic message — keeps us robust if the backend ever
emits a non-JSON response.
Engagements API (src/screens/engagements/engagementsApi.ts)
- Drop the `{ items: [] }` envelope tolerance — backend serves a bare
`Engagement[]`.
- Hit `/engagements/` with trailing slash explicitly; backend now sets
`strict_slashes=False` but staying consistent with docs/api.md.
EngagementsPage
- Status tone map switched to the new enum (`draft → pending`,
`closed → soc`).
- Drop "Name" column. `client_name` is the primary identifier; the
description column replaces the now-meaningless name field.
- `c2_type` is non-null, so no nullable rendering path.
EngagementCreateDialog
- Drop `name` field. New required field is `client_name`; add a
`c2_type` select (default `mythic`); brief textarea stays optional.
- `mapValidationErrors` now reads `body.details[*].loc` (last segment
matches the form field) — direct alignment with the backend's new
shape after dd5c508.
- 401 still surfaces "Session expirée"; 403 gains a dedicated message;
other errors fall back to a capitalized backend `message` when
available, then to a generic French string.
Sidebar
- Display fallback: `user.display_name ?? user.username` (now nullable).
- Drop the `ENG · {engagement_name}` line; show `user.username` (the
email) as the secondary identity instead.
LoginPage
- Field label "Username" → "Email or username" so RT users with email
accounts find the field semantically obvious (per docs/api.md note
on the username/email mapping).
Tests (Vitest, 14 cases, all green)
- Refreshed fixtures to the new shapes (no more `name`, no
`created_at`, status `draft`, envelopes carry `error`+`message`).
- New 422 test exercises the `details[*].loc` mapping shape.
- New 401 test on the dialog covers the top-of-form alert path.
2026-05-23 11:14:32 +02:00
|
|
|
fireEvent.change(screen.getByLabelText(/client name/i), { target: { value: 'x' } });
|
2026-05-23 04:26:56 +02:00
|
|
|
fireEvent.click(screen.getByRole('button', { name: /arm engagement/i }));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
fix(frontend): align types + UI to backend contract (docs/api.md @ dd5c508)
Backend pushed the authoritative contract in docs/api.md and tightened
the error envelope via a global HTTPException handler (dd5c508). This
commit folds the frontend onto that contract — every drift flagged by
the code-reviewer MAJOR is closed.
Types (src/types/api.ts)
- User: `id` → `user_id`; `display_name` is `string | null`; add
`permissions: string[]` and `groups: string[]`; drop `engagement_id`
and `engagement_name` (not part of CurrentUser).
- Engagement: drop `name`, `client_name` is non-null `string`; status
enum aligned to `draft | active | closed | archived`; `c2_type` is
non-null `C2Type`; drop `created_at` (not in EngagementRead v1).
- EngagementCreate body: `client_name` required, plus optional
`description`, `c2_type`, `start_date`, `end_date`. No `name`.
- Replace ApiError + ApiValidationError with a single uniform envelope:
`{ error: string, message: string, details?: PydanticErrorItem[] }`,
matching the new HTTPException handler. PydanticErrorItem is the
per-field shape on 422 (`{ loc, msg, type }`).
Fetch client (src/lib/api.ts)
- `bodyAsApiError` now recognizes the uniform envelope by shape
(error+message strings). Anything else returns null so callers fall
back to a generic message — keeps us robust if the backend ever
emits a non-JSON response.
Engagements API (src/screens/engagements/engagementsApi.ts)
- Drop the `{ items: [] }` envelope tolerance — backend serves a bare
`Engagement[]`.
- Hit `/engagements/` with trailing slash explicitly; backend now sets
`strict_slashes=False` but staying consistent with docs/api.md.
EngagementsPage
- Status tone map switched to the new enum (`draft → pending`,
`closed → soc`).
- Drop "Name" column. `client_name` is the primary identifier; the
description column replaces the now-meaningless name field.
- `c2_type` is non-null, so no nullable rendering path.
EngagementCreateDialog
- Drop `name` field. New required field is `client_name`; add a
`c2_type` select (default `mythic`); brief textarea stays optional.
- `mapValidationErrors` now reads `body.details[*].loc` (last segment
matches the form field) — direct alignment with the backend's new
shape after dd5c508.
- 401 still surfaces "Session expirée"; 403 gains a dedicated message;
other errors fall back to a capitalized backend `message` when
available, then to a generic French string.
Sidebar
- Display fallback: `user.display_name ?? user.username` (now nullable).
- Drop the `ENG · {engagement_name}` line; show `user.username` (the
email) as the secondary identity instead.
LoginPage
- Field label "Username" → "Email or username" so RT users with email
accounts find the field semantically obvious (per docs/api.md note
on the username/email mapping).
Tests (Vitest, 14 cases, all green)
- Refreshed fixtures to the new shapes (no more `name`, no
`created_at`, status `draft`, envelopes carry `error`+`message`).
- New 422 test exercises the `details[*].loc` mapping shape.
- New 401 test on the dialog covers the top-of-form alert path.
2026-05-23 11:14:32 +02:00
|
|
|
expect(screen.getByText(/at least 1 character/i)).toBeInTheDocument();
|
2026-05-23 04:26:56 +02:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('invalidates the engagements query and closes on success', async () => {
|
|
|
|
|
const onClose = vi.fn();
|
|
|
|
|
fetchMock = installFetchMock([
|
|
|
|
|
{
|
|
|
|
|
status: 201,
|
|
|
|
|
body: {
|
|
|
|
|
id: 'eng_new',
|
fix(frontend): align types + UI to backend contract (docs/api.md @ dd5c508)
Backend pushed the authoritative contract in docs/api.md and tightened
the error envelope via a global HTTPException handler (dd5c508). This
commit folds the frontend onto that contract — every drift flagged by
the code-reviewer MAJOR is closed.
Types (src/types/api.ts)
- User: `id` → `user_id`; `display_name` is `string | null`; add
`permissions: string[]` and `groups: string[]`; drop `engagement_id`
and `engagement_name` (not part of CurrentUser).
- Engagement: drop `name`, `client_name` is non-null `string`; status
enum aligned to `draft | active | closed | archived`; `c2_type` is
non-null `C2Type`; drop `created_at` (not in EngagementRead v1).
- EngagementCreate body: `client_name` required, plus optional
`description`, `c2_type`, `start_date`, `end_date`. No `name`.
- Replace ApiError + ApiValidationError with a single uniform envelope:
`{ error: string, message: string, details?: PydanticErrorItem[] }`,
matching the new HTTPException handler. PydanticErrorItem is the
per-field shape on 422 (`{ loc, msg, type }`).
Fetch client (src/lib/api.ts)
- `bodyAsApiError` now recognizes the uniform envelope by shape
(error+message strings). Anything else returns null so callers fall
back to a generic message — keeps us robust if the backend ever
emits a non-JSON response.
Engagements API (src/screens/engagements/engagementsApi.ts)
- Drop the `{ items: [] }` envelope tolerance — backend serves a bare
`Engagement[]`.
- Hit `/engagements/` with trailing slash explicitly; backend now sets
`strict_slashes=False` but staying consistent with docs/api.md.
EngagementsPage
- Status tone map switched to the new enum (`draft → pending`,
`closed → soc`).
- Drop "Name" column. `client_name` is the primary identifier; the
description column replaces the now-meaningless name field.
- `c2_type` is non-null, so no nullable rendering path.
EngagementCreateDialog
- Drop `name` field. New required field is `client_name`; add a
`c2_type` select (default `mythic`); brief textarea stays optional.
- `mapValidationErrors` now reads `body.details[*].loc` (last segment
matches the form field) — direct alignment with the backend's new
shape after dd5c508.
- 401 still surfaces "Session expirée"; 403 gains a dedicated message;
other errors fall back to a capitalized backend `message` when
available, then to a generic French string.
Sidebar
- Display fallback: `user.display_name ?? user.username` (now nullable).
- Drop the `ENG · {engagement_name}` line; show `user.username` (the
email) as the secondary identity instead.
LoginPage
- Field label "Username" → "Email or username" so RT users with email
accounts find the field semantically obvious (per docs/api.md note
on the username/email mapping).
Tests (Vitest, 14 cases, all green)
- Refreshed fixtures to the new shapes (no more `name`, no
`created_at`, status `draft`, envelopes carry `error`+`message`).
- New 422 test exercises the `details[*].loc` mapping shape.
- New 401 test on the dialog covers the top-of-form alert path.
2026-05-23 11:14:32 +02:00
|
|
|
client_name: 'OPERATION ZETA',
|
2026-05-23 04:26:56 +02:00
|
|
|
description: null,
|
fix(frontend): align types + UI to backend contract (docs/api.md @ dd5c508)
Backend pushed the authoritative contract in docs/api.md and tightened
the error envelope via a global HTTPException handler (dd5c508). This
commit folds the frontend onto that contract — every drift flagged by
the code-reviewer MAJOR is closed.
Types (src/types/api.ts)
- User: `id` → `user_id`; `display_name` is `string | null`; add
`permissions: string[]` and `groups: string[]`; drop `engagement_id`
and `engagement_name` (not part of CurrentUser).
- Engagement: drop `name`, `client_name` is non-null `string`; status
enum aligned to `draft | active | closed | archived`; `c2_type` is
non-null `C2Type`; drop `created_at` (not in EngagementRead v1).
- EngagementCreate body: `client_name` required, plus optional
`description`, `c2_type`, `start_date`, `end_date`. No `name`.
- Replace ApiError + ApiValidationError with a single uniform envelope:
`{ error: string, message: string, details?: PydanticErrorItem[] }`,
matching the new HTTPException handler. PydanticErrorItem is the
per-field shape on 422 (`{ loc, msg, type }`).
Fetch client (src/lib/api.ts)
- `bodyAsApiError` now recognizes the uniform envelope by shape
(error+message strings). Anything else returns null so callers fall
back to a generic message — keeps us robust if the backend ever
emits a non-JSON response.
Engagements API (src/screens/engagements/engagementsApi.ts)
- Drop the `{ items: [] }` envelope tolerance — backend serves a bare
`Engagement[]`.
- Hit `/engagements/` with trailing slash explicitly; backend now sets
`strict_slashes=False` but staying consistent with docs/api.md.
EngagementsPage
- Status tone map switched to the new enum (`draft → pending`,
`closed → soc`).
- Drop "Name" column. `client_name` is the primary identifier; the
description column replaces the now-meaningless name field.
- `c2_type` is non-null, so no nullable rendering path.
EngagementCreateDialog
- Drop `name` field. New required field is `client_name`; add a
`c2_type` select (default `mythic`); brief textarea stays optional.
- `mapValidationErrors` now reads `body.details[*].loc` (last segment
matches the form field) — direct alignment with the backend's new
shape after dd5c508.
- 401 still surfaces "Session expirée"; 403 gains a dedicated message;
other errors fall back to a capitalized backend `message` when
available, then to a generic French string.
Sidebar
- Display fallback: `user.display_name ?? user.username` (now nullable).
- Drop the `ENG · {engagement_name}` line; show `user.username` (the
email) as the secondary identity instead.
LoginPage
- Field label "Username" → "Email or username" so RT users with email
accounts find the field semantically obvious (per docs/api.md note
on the username/email mapping).
Tests (Vitest, 14 cases, all green)
- Refreshed fixtures to the new shapes (no more `name`, no
`created_at`, status `draft`, envelopes carry `error`+`message`).
- New 422 test exercises the `details[*].loc` mapping shape.
- New 401 test on the dialog covers the top-of-form alert path.
2026-05-23 11:14:32 +02:00
|
|
|
status: 'draft',
|
|
|
|
|
c2_type: 'mythic',
|
2026-05-23 04:26:56 +02:00
|
|
|
start_date: null,
|
|
|
|
|
end_date: null,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
renderWithProviders(<EngagementCreateDialog onClose={onClose} />);
|
|
|
|
|
|
fix(frontend): align types + UI to backend contract (docs/api.md @ dd5c508)
Backend pushed the authoritative contract in docs/api.md and tightened
the error envelope via a global HTTPException handler (dd5c508). This
commit folds the frontend onto that contract — every drift flagged by
the code-reviewer MAJOR is closed.
Types (src/types/api.ts)
- User: `id` → `user_id`; `display_name` is `string | null`; add
`permissions: string[]` and `groups: string[]`; drop `engagement_id`
and `engagement_name` (not part of CurrentUser).
- Engagement: drop `name`, `client_name` is non-null `string`; status
enum aligned to `draft | active | closed | archived`; `c2_type` is
non-null `C2Type`; drop `created_at` (not in EngagementRead v1).
- EngagementCreate body: `client_name` required, plus optional
`description`, `c2_type`, `start_date`, `end_date`. No `name`.
- Replace ApiError + ApiValidationError with a single uniform envelope:
`{ error: string, message: string, details?: PydanticErrorItem[] }`,
matching the new HTTPException handler. PydanticErrorItem is the
per-field shape on 422 (`{ loc, msg, type }`).
Fetch client (src/lib/api.ts)
- `bodyAsApiError` now recognizes the uniform envelope by shape
(error+message strings). Anything else returns null so callers fall
back to a generic message — keeps us robust if the backend ever
emits a non-JSON response.
Engagements API (src/screens/engagements/engagementsApi.ts)
- Drop the `{ items: [] }` envelope tolerance — backend serves a bare
`Engagement[]`.
- Hit `/engagements/` with trailing slash explicitly; backend now sets
`strict_slashes=False` but staying consistent with docs/api.md.
EngagementsPage
- Status tone map switched to the new enum (`draft → pending`,
`closed → soc`).
- Drop "Name" column. `client_name` is the primary identifier; the
description column replaces the now-meaningless name field.
- `c2_type` is non-null, so no nullable rendering path.
EngagementCreateDialog
- Drop `name` field. New required field is `client_name`; add a
`c2_type` select (default `mythic`); brief textarea stays optional.
- `mapValidationErrors` now reads `body.details[*].loc` (last segment
matches the form field) — direct alignment with the backend's new
shape after dd5c508.
- 401 still surfaces "Session expirée"; 403 gains a dedicated message;
other errors fall back to a capitalized backend `message` when
available, then to a generic French string.
Sidebar
- Display fallback: `user.display_name ?? user.username` (now nullable).
- Drop the `ENG · {engagement_name}` line; show `user.username` (the
email) as the secondary identity instead.
LoginPage
- Field label "Username" → "Email or username" so RT users with email
accounts find the field semantically obvious (per docs/api.md note
on the username/email mapping).
Tests (Vitest, 14 cases, all green)
- Refreshed fixtures to the new shapes (no more `name`, no
`created_at`, status `draft`, envelopes carry `error`+`message`).
- New 422 test exercises the `details[*].loc` mapping shape.
- New 401 test on the dialog covers the top-of-form alert path.
2026-05-23 11:14:32 +02:00
|
|
|
fireEvent.change(screen.getByLabelText(/client name/i), {
|
2026-05-23 04:26:56 +02:00
|
|
|
target: { value: 'OPERATION ZETA' },
|
|
|
|
|
});
|
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /arm engagement/i }));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(onClose).toHaveBeenCalledTimes(1);
|
|
|
|
|
});
|
fix(frontend): align types + UI to backend contract (docs/api.md @ dd5c508)
Backend pushed the authoritative contract in docs/api.md and tightened
the error envelope via a global HTTPException handler (dd5c508). This
commit folds the frontend onto that contract — every drift flagged by
the code-reviewer MAJOR is closed.
Types (src/types/api.ts)
- User: `id` → `user_id`; `display_name` is `string | null`; add
`permissions: string[]` and `groups: string[]`; drop `engagement_id`
and `engagement_name` (not part of CurrentUser).
- Engagement: drop `name`, `client_name` is non-null `string`; status
enum aligned to `draft | active | closed | archived`; `c2_type` is
non-null `C2Type`; drop `created_at` (not in EngagementRead v1).
- EngagementCreate body: `client_name` required, plus optional
`description`, `c2_type`, `start_date`, `end_date`. No `name`.
- Replace ApiError + ApiValidationError with a single uniform envelope:
`{ error: string, message: string, details?: PydanticErrorItem[] }`,
matching the new HTTPException handler. PydanticErrorItem is the
per-field shape on 422 (`{ loc, msg, type }`).
Fetch client (src/lib/api.ts)
- `bodyAsApiError` now recognizes the uniform envelope by shape
(error+message strings). Anything else returns null so callers fall
back to a generic message — keeps us robust if the backend ever
emits a non-JSON response.
Engagements API (src/screens/engagements/engagementsApi.ts)
- Drop the `{ items: [] }` envelope tolerance — backend serves a bare
`Engagement[]`.
- Hit `/engagements/` with trailing slash explicitly; backend now sets
`strict_slashes=False` but staying consistent with docs/api.md.
EngagementsPage
- Status tone map switched to the new enum (`draft → pending`,
`closed → soc`).
- Drop "Name" column. `client_name` is the primary identifier; the
description column replaces the now-meaningless name field.
- `c2_type` is non-null, so no nullable rendering path.
EngagementCreateDialog
- Drop `name` field. New required field is `client_name`; add a
`c2_type` select (default `mythic`); brief textarea stays optional.
- `mapValidationErrors` now reads `body.details[*].loc` (last segment
matches the form field) — direct alignment with the backend's new
shape after dd5c508.
- 401 still surfaces "Session expirée"; 403 gains a dedicated message;
other errors fall back to a capitalized backend `message` when
available, then to a generic French string.
Sidebar
- Display fallback: `user.display_name ?? user.username` (now nullable).
- Drop the `ENG · {engagement_name}` line; show `user.username` (the
email) as the secondary identity instead.
LoginPage
- Field label "Username" → "Email or username" so RT users with email
accounts find the field semantically obvious (per docs/api.md note
on the username/email mapping).
Tests (Vitest, 14 cases, all green)
- Refreshed fixtures to the new shapes (no more `name`, no
`created_at`, status `draft`, envelopes carry `error`+`message`).
- New 422 test exercises the `details[*].loc` mapping shape.
- New 401 test on the dialog covers the top-of-form alert path.
2026-05-23 11:14:32 +02:00
|
|
|
expect(fetchMock.calls[0]?.url).toBe('/api/v1/engagements/');
|
2026-05-23 04:26:56 +02:00
|
|
|
expect(fetchMock.calls[0]?.init?.method).toBe('POST');
|
|
|
|
|
});
|
fix(frontend): align types + UI to backend contract (docs/api.md @ dd5c508)
Backend pushed the authoritative contract in docs/api.md and tightened
the error envelope via a global HTTPException handler (dd5c508). This
commit folds the frontend onto that contract — every drift flagged by
the code-reviewer MAJOR is closed.
Types (src/types/api.ts)
- User: `id` → `user_id`; `display_name` is `string | null`; add
`permissions: string[]` and `groups: string[]`; drop `engagement_id`
and `engagement_name` (not part of CurrentUser).
- Engagement: drop `name`, `client_name` is non-null `string`; status
enum aligned to `draft | active | closed | archived`; `c2_type` is
non-null `C2Type`; drop `created_at` (not in EngagementRead v1).
- EngagementCreate body: `client_name` required, plus optional
`description`, `c2_type`, `start_date`, `end_date`. No `name`.
- Replace ApiError + ApiValidationError with a single uniform envelope:
`{ error: string, message: string, details?: PydanticErrorItem[] }`,
matching the new HTTPException handler. PydanticErrorItem is the
per-field shape on 422 (`{ loc, msg, type }`).
Fetch client (src/lib/api.ts)
- `bodyAsApiError` now recognizes the uniform envelope by shape
(error+message strings). Anything else returns null so callers fall
back to a generic message — keeps us robust if the backend ever
emits a non-JSON response.
Engagements API (src/screens/engagements/engagementsApi.ts)
- Drop the `{ items: [] }` envelope tolerance — backend serves a bare
`Engagement[]`.
- Hit `/engagements/` with trailing slash explicitly; backend now sets
`strict_slashes=False` but staying consistent with docs/api.md.
EngagementsPage
- Status tone map switched to the new enum (`draft → pending`,
`closed → soc`).
- Drop "Name" column. `client_name` is the primary identifier; the
description column replaces the now-meaningless name field.
- `c2_type` is non-null, so no nullable rendering path.
EngagementCreateDialog
- Drop `name` field. New required field is `client_name`; add a
`c2_type` select (default `mythic`); brief textarea stays optional.
- `mapValidationErrors` now reads `body.details[*].loc` (last segment
matches the form field) — direct alignment with the backend's new
shape after dd5c508.
- 401 still surfaces "Session expirée"; 403 gains a dedicated message;
other errors fall back to a capitalized backend `message` when
available, then to a generic French string.
Sidebar
- Display fallback: `user.display_name ?? user.username` (now nullable).
- Drop the `ENG · {engagement_name}` line; show `user.username` (the
email) as the secondary identity instead.
LoginPage
- Field label "Username" → "Email or username" so RT users with email
accounts find the field semantically obvious (per docs/api.md note
on the username/email mapping).
Tests (Vitest, 14 cases, all green)
- Refreshed fixtures to the new shapes (no more `name`, no
`created_at`, status `draft`, envelopes carry `error`+`message`).
- New 422 test exercises the `details[*].loc` mapping shape.
- New 401 test on the dialog covers the top-of-form alert path.
2026-05-23 11:14:32 +02:00
|
|
|
|
|
|
|
|
it('surfaces a generic top-of-form error on 401', async () => {
|
|
|
|
|
fetchMock = installFetchMock([
|
|
|
|
|
{
|
|
|
|
|
status: 401,
|
|
|
|
|
body: { error: 'not_authenticated', message: 'no active session' },
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
renderWithProviders(<EngagementCreateDialog onClose={vi.fn()} />);
|
|
|
|
|
fireEvent.change(screen.getByLabelText(/client name/i), {
|
|
|
|
|
target: { value: 'Anyone' },
|
|
|
|
|
});
|
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /arm engagement/i }));
|
|
|
|
|
const alert = await screen.findByRole('alert');
|
|
|
|
|
expect(alert.textContent).toMatch(/session expirée/i);
|
|
|
|
|
});
|
2026-05-23 04:26:56 +02:00
|
|
|
});
|