Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/frontend/src/features/admin/scans/AdminScansPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ import {
import { useAdminScans } from "@/features/admin/scans/api/useAdminScans";
import RelativeTime from "@/components/RelativeTime";
import { cn } from "@/lib/utils";
import type { ScanKind } from "@/lib/projectsApi";
import { SCAN_KIND_VALUES, type ScanKind } from "@/lib/projectsApi";

const PAGE_SIZE_OPTIONS = [25, 50, 100] as const;

const KIND_OPTIONS: ScanKind[] = ["source", "container"];
const KIND_OPTIONS: ScanKind[] = [...SCAN_KIND_VALUES];

type ScansTab = "running" | "queued" | "failed" | "all";

Expand Down
13 changes: 12 additions & 1 deletion apps/frontend/src/lib/projectsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,18 @@ import { api } from "@/lib/api";
// ---------------------------------------------------------------------------

export type ProjectVisibility = "team" | "organization";
export type ScanKind = "source" | "container";
/**
* Closed scan-kind set — runtime mirror of the backend's scan `kind` values
* (`source` cdxgen→SBOM, `container` image scan, `sbom` external CycloneDX
* ingest, PR #406). Same pattern as `SCAN_STATUS_VALUES`: the array is walked
* by `tests/unit/contracts/catalogMirrors.test.ts` to assert every kind owns
* its own `page.kind.*` / `overview.recent_scans.kind.*` /
* `scans.filter.kind.*` label in both locales, so a kind added on the backend
* fails a PR-time vitest instead of silently rendering a raw i18n key.
*/
export const SCAN_KIND_VALUES = ["source", "container", "sbom"] as const;

export type ScanKind = (typeof SCAN_KIND_VALUES)[number];
/**
* Closed scan status set — runtime mirror of the backend's
* `models/scan.py::SCAN_STATUS_VALUES`, same order. PR-6 FE regression
Expand Down
3 changes: 2 additions & 1 deletion apps/frontend/src/locales/en/admin.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@
"kind_all": "All kinds",
"kind": {
"source": "Source",
"container": "Container"
"container": "Container",
"sbom": "SBOM upload"
},
"project_label": "Project",
"project_placeholder": "Filter by project name…"
Expand Down
3 changes: 2 additions & 1 deletion apps/frontend/src/locales/en/project_detail.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,8 @@
},
"kind": {
"source": "Source",
"container": "Container"
"container": "Container",
"sbom": "SBOM upload"
}
},
"gate_card": {
Expand Down
3 changes: 2 additions & 1 deletion apps/frontend/src/locales/en/scans.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
},
"kind": {
"source": "Source",
"container": "Container"
"container": "Container",
"sbom": "SBOM upload"
},
"status": {
"queued": "Queued",
Expand Down
3 changes: 2 additions & 1 deletion apps/frontend/src/locales/ko/admin.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@
"kind_all": "전체 종류",
"kind": {
"source": "소스",
"container": "컨테이너"
"container": "컨테이너",
"sbom": "SBOM 업로드"
},
"project_label": "프로젝트",
"project_placeholder": "프로젝트 이름으로 필터…"
Expand Down
3 changes: 2 additions & 1 deletion apps/frontend/src/locales/ko/project_detail.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,8 @@
},
"kind": {
"source": "소스",
"container": "컨테이너"
"container": "컨테이너",
"sbom": "SBOM 업로드"
}
},
"gate_card": {
Expand Down
3 changes: 2 additions & 1 deletion apps/frontend/src/locales/ko/scans.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
},
"kind": {
"source": "소스",
"container": "컨테이너"
"container": "컨테이너",
"sbom": "SBOM 업로드"
},
"status": {
"queued": "대기 중",
Expand Down
3 changes: 3 additions & 0 deletions apps/frontend/tests/unit/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ vi.mock("@/lib/api", () => ({
// provide those too. All return empty lists so the dashboard renders its
// empty-state CTA path on the / index.
vi.mock("@/lib/projectsApi", () => ({
// Module-level constant consumed by AdminScansPage's KIND_OPTIONS; the
// wholesale mock must re-export it or the route tree fails to import.
SCAN_KIND_VALUES: ["source", "container", "sbom"] as const,
listProjects: vi
.fn()
.mockResolvedValue({ items: [], total: 0, page: 1, size: 100 }),
Expand Down
3 changes: 3 additions & 0 deletions apps/frontend/tests/unit/components/AppShellSidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ vi.mock("@/lib/api", () => ({
}));

vi.mock("@/lib/projectsApi", () => ({
// Module-level constant consumed by AdminScansPage's KIND_OPTIONS; the
// wholesale mock must re-export it or the route tree fails to import.
SCAN_KIND_VALUES: ["source", "container", "sbom"] as const,
listProjects: vi
.fn()
.mockResolvedValue({ items: [], total: 0, page: 1, size: 100 }),
Expand Down
59 changes: 58 additions & 1 deletion apps/frontend/tests/unit/contracts/catalogMirrors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,18 @@ import { NOTIFICATION_KINDS } from "@/features/notifications/api/notificationsAp
import { KNOWN_OBLIGATION_KINDS } from "@/features/projects/api/obligationsApi";
import { ALL_VULNERABILITY_STATUSES } from "@/features/projects/lib/vulnerabilityTransitions";
import { visualFor } from "@/features/projects/components/ProjectStatusBadge";
import { SCAN_STATUS_VALUES } from "@/lib/projectsApi";
import { SCAN_KIND_VALUES, SCAN_STATUS_VALUES } from "@/lib/projectsApi";

import enAdmin from "@/locales/en/admin.json";
import koAdmin from "@/locales/ko/admin.json";
import enNotifications from "@/locales/en/notifications.json";
import koNotifications from "@/locales/ko/notifications.json";
import enProjectDetail from "@/locales/en/project_detail.json";
import koProjectDetail from "@/locales/ko/project_detail.json";
import enProjects from "@/locales/en/projects.json";
import koProjects from "@/locales/ko/projects.json";
import enScans from "@/locales/en/scans.json";
import koScans from "@/locales/ko/scans.json";

// Shared cross-app fixture (repo root). The backend enum and this FE mirror
// must both equal this list; asserting the BE side against the same file is
Expand Down Expand Up @@ -141,6 +145,59 @@ describe("scan statuses — FE mirror of SCAN_STATUS_VALUES", () => {
});
});

describe("scan kinds — FE mirror of the scan `kind` set", () => {
// Same latent-drift class as scan statuses: `kind` was a bare string union
// until the external SBOM ingest (PR #406) added a third emitted value. Each
// emitted/selectable kind renders through a dynamic `…kind.${scan.kind}` key
// and the admin filter offers `KIND_OPTIONS`, so a missing label silently
// shows a raw i18n key (table badge) or an un-selectable raw value (filter).
it("matches the backend's closed kind set, in canonical order", () => {
expect([...SCAN_KIND_VALUES]).toEqual(["source", "container", "sbom"]);
});

it.each([
["en", enScans],
["ko", koScans],
])("every kind owns a %s ScansPage `page.kind.*` label", (_locale, ns) => {
const kinds = labelMap(ns, "page", "kind");
for (const kind of SCAN_KIND_VALUES) {
expect(kinds[kind], `page.kind.${kind} missing`).toBeTruthy();
}
});

it.each([
["en", enProjectDetail],
["ko", koProjectDetail],
])(
"every kind owns a %s `overview.recent_scans.kind.*` label",
(_locale, ns) => {
const kinds = labelMap(ns, "overview", "recent_scans", "kind");
for (const kind of SCAN_KIND_VALUES) {
expect(
kinds[kind],
`overview.recent_scans.kind.${kind} missing`,
).toBeTruthy();
}
},
);

it.each([
["en", enAdmin],
["ko", koAdmin],
])(
"every kind owns a %s admin `scans.filter.kind.*` label",
(_locale, ns) => {
const kinds = labelMap(ns, "admin", "scans", "filter", "kind");
for (const kind of SCAN_KIND_VALUES) {
expect(
kinds[kind],
`admin scans.filter.kind.${kind} missing`,
).toBeTruthy();
}
},
);
});

describe("vulnerability statuses — label-map half of the 7-state mirror", () => {
it.each([
["en", enProjectDetail],
Expand Down
Loading