Files
mimic-big/frontend/src/screens/engagements/EngagementsPage.test.tsx

61 lines
2.0 KiB
TypeScript
Raw Normal View History

import { describe, it, expect, afterEach } from 'vitest';
import { screen, waitFor } from '@testing-library/react';
import { EngagementsPage } from './EngagementsPage';
import { installFetchMock, renderWithProviders } from '@/test/testUtils';
describe('EngagementsPage', () => {
let fetchMock: ReturnType<typeof installFetchMock>;
afterEach(() => {
fetchMock?.restore();
});
it('shows the loading row while the engagements query is pending', () => {
fetchMock = installFetchMock([]); // never resolve in this test
const pending: typeof fetch = () => new Promise(() => null);
globalThis.fetch = pending;
renderWithProviders(<EngagementsPage />);
expect(screen.getByText(/fetching engagements/i)).toBeInTheDocument();
});
it('renders the empty state when the list is empty', async () => {
fetchMock = installFetchMock([{ status: 200, body: [] }]);
renderWithProviders(<EngagementsPage />);
await screen.findByText(/no engagements yet/i);
});
it('renders the error state on 500', async () => {
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
fetchMock = installFetchMock([
{ status: 500, body: { error: 'internal_error', message: 'boom' } },
]);
renderWithProviders(<EngagementsPage />);
await waitFor(() => {
expect(screen.getByText(/fetch failed/i)).toBeInTheDocument();
});
expect(screen.getByRole('button', { name: /retry/i })).toBeInTheDocument();
});
it('renders rows when the backend returns engagements', async () => {
fetchMock = installFetchMock([
{
status: 200,
body: [
{
id: 'eng_1',
client_name: 'Acme',
description: null,
status: 'active',
c2_type: 'mythic',
start_date: '2026-05-20',
end_date: '2026-05-30',
},
],
},
]);
renderWithProviders(<EngagementsPage />);
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
await screen.findByText('Acme');
expect(screen.getByText('MYTHIC')).toBeInTheDocument();
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(/active/)).toBeInTheDocument();
});
});