Files
prowler/ui/actions/registry/registry.adapter.test.ts
T

697 lines
19 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
REGISTRY_ENDPOINT,
REGISTRY_FAILURE,
REGISTRY_SUBMISSION,
} from "@/types/registry";
import {
adaptRegistryCredentialStatus,
adaptRegistryTenantArtifacts,
classifyRegistryFailure,
collectCompleteRegistryCatalog,
parseRegistryArtifactSubmission,
} from "./registry.adapter";
const credentialPayload = {
data: {
attributes: {
configured: true,
is_valid: true,
scopes: ["catalog:read"],
last_validated_at: "2026-03-20T12:00:00Z",
validation_status: "valid",
validation_pending: false,
key: "registry-secret-value",
masked_key: "reg_***",
pending_key: "queued-secret",
arbitrary_backend_detail: "do not expose",
},
},
};
const activeCredential = adaptRegistryCredentialStatus(credentialPayload);
const jsonError = (status: number, code: string) =>
new Response(
JSON.stringify({ errors: [{ code, detail: "private detail" }] }),
{
status,
},
);
describe("Registry adapter", () => {
it("reads the resolved installed version separately from the requested spec", () => {
// Given / When
const artifacts = adaptRegistryTenantArtifacts({
data: [
{
type: "registry-artifacts",
id: "template",
attributes: {
version_spec: "latest",
resolved_version: " 1.0.0 ",
},
},
],
});
// Then
expect(artifacts).toEqual([
expect.objectContaining({
normalizedName: "template",
versionSpec: "latest",
resolvedVersion: "1.0.0",
}),
]);
});
it.each([undefined, null, "", " "])(
"accepts an unknown resolved version %j",
(resolvedVersion) => {
// Given / When
const artifacts = adaptRegistryTenantArtifacts({
data: [
{
type: "registry-artifacts",
id: "template",
attributes: {
version_spec: "latest",
resolved_version: resolvedVersion,
},
},
],
});
// Then
expect(artifacts).toMatchObject([{ resolvedVersion: undefined }]);
},
);
it("maps only documented non-secret credential status fields", () => {
// Given
const malformedPayload = { data: { attributes: { configured: true } } };
// When
const status = adaptRegistryCredentialStatus(credentialPayload);
// Then
expect(status).toEqual({
configured: true,
isValid: true,
scopes: ["catalog:read"],
lastValidatedAt: "2026-03-20T12:00:00Z",
validationStatus: "valid",
validationPending: false,
});
expect(adaptRegistryCredentialStatus(malformedPayload)).toBeNull();
});
it("normalizes an absent credential status with nullable validation fields", () => {
// Given
const absentCredentialPayload = {
data: {
attributes: {
configured: false,
is_valid: false,
scopes: [],
last_validated_at: null,
validation_status: null,
validation_pending: false,
},
},
};
// When
const status = adaptRegistryCredentialStatus(absentCredentialPayload);
// Then
expect(status).toEqual({
configured: false,
isValid: false,
scopes: [],
lastValidatedAt: undefined,
validationStatus: undefined,
validationPending: false,
});
});
it("accepts only a matching artifact 202 task and fixed Content-Location path", async () => {
// Given
const response = new Response(
JSON.stringify({ data: { type: "tasks", id: "task-123" } }),
{
status: 202,
headers: { "Content-Location": "/api/v1/tasks/task-123" },
},
);
// When
const result = await parseRegistryArtifactSubmission(response);
// Then
expect(result).toEqual({
status: REGISTRY_SUBMISSION.PENDING,
taskId: "task-123",
});
});
it("rejects a non-202 response or a mismatched task location", async () => {
// Given
const task = JSON.stringify({ data: { type: "tasks", id: "task-123" } });
const wrongStatus = new Response(task, { status: 201 });
const wrongLocation = new Response(task, {
status: 202,
headers: { "Content-Location": "/api/v1/tasks/other" },
});
// When
const results = await Promise.all([
parseRegistryArtifactSubmission(wrongStatus),
parseRegistryArtifactSubmission(wrongLocation),
]);
// Then
expect(results).toEqual([
{ status: REGISTRY_SUBMISSION.ERROR },
{ status: REGISTRY_SUBMISSION.ERROR },
]);
});
it("classifies every Registry 401 or 403 as access denied first", async () => {
// Given
const responses = [
[401, REGISTRY_ENDPOINT.CREDENTIAL],
[403, REGISTRY_ENDPOINT.MUTATION],
[403, REGISTRY_ENDPOINT.PROVIDERS],
] as const;
// When
const results = await Promise.all(
responses.map(([status, endpoint]) =>
classifyRegistryFailure(
jsonError(status, "registry_key_rejected"),
endpoint,
activeCredential,
),
),
);
// Then
expect(results).toEqual([
{ status: REGISTRY_FAILURE.ACCESS_DENIED },
{ status: REGISTRY_FAILURE.ACCESS_DENIED },
{ status: REGISTRY_FAILURE.ACCESS_DENIED },
]);
});
it("maps only a 409 with an authoritative no-active credential to onboarding", async () => {
// Given
const noCredential = adaptRegistryCredentialStatus({
data: {
attributes: {
configured: false,
is_valid: false,
scopes: [],
validation_pending: false,
},
},
});
// When
const results = await Promise.all(
[noCredential, null].map((credential) =>
classifyRegistryFailure(
new Response(null, { status: 409 }),
REGISTRY_ENDPOINT.AVAILABLE_ARTIFACTS,
credential,
),
),
);
// Then
expect(results).toEqual([
{ status: REGISTRY_FAILURE.ONBOARDING },
{ status: REGISTRY_FAILURE.ERROR },
]);
});
it("maps only exact documented 502 and 503 status-code pairs", async () => {
// Given
const rejected = jsonError(502, "registry_key_rejected");
const unavailable = jsonError(503, "registry_unavailable");
// When
const results = await Promise.all([
classifyRegistryFailure(
rejected,
REGISTRY_ENDPOINT.PROVIDERS,
activeCredential,
),
classifyRegistryFailure(
unavailable,
REGISTRY_ENDPOINT.AVAILABLE_ARTIFACTS,
activeCredential,
),
]);
// Then
expect(results).toEqual([
{ status: REGISTRY_FAILURE.RECONNECT },
{ status: REGISTRY_FAILURE.UNAVAILABLE },
]);
});
it("keeps wrong, malformed, and unrelated failures generic", async () => {
// Given
const malformed = new Response("<html>key=private</html>", { status: 503 });
// When
const results = await Promise.all([
classifyRegistryFailure(
jsonError(502, "other_error"),
REGISTRY_ENDPOINT.PROVIDERS,
activeCredential,
),
classifyRegistryFailure(
jsonError(502, "registry_unavailable"),
REGISTRY_ENDPOINT.PROVIDERS,
activeCredential,
),
classifyRegistryFailure(
malformed,
REGISTRY_ENDPOINT.PROVIDERS,
activeCredential,
),
]);
// Then
expect(results).toEqual([
{ status: REGISTRY_FAILURE.ERROR },
{ status: REGISTRY_FAILURE.ERROR },
{ status: REGISTRY_FAILURE.ERROR },
]);
});
it("degrades a non-terminal empty first catalog page", async () => {
// Given
const document = (page: number) => ({
data: [],
meta: { pagination: { page, pages: 2, count: 0 } },
});
// When
const result = await collectCompleteRegistryCatalog(async (page) =>
document(page),
);
// Then
expect(result).toEqual({
status: "incomplete",
reason: "invalid_page",
collectedCount: 0,
});
});
it("accepts a terminal empty first catalog page", async () => {
// Given
const document = {
data: [],
meta: { pagination: { page: 1, pages: 1, count: 0 } },
};
// When
const result = await collectCompleteRegistryCatalog(async () => document);
// Then
expect(result).toEqual({ status: "complete", artifacts: [] });
});
it("maps the flat owner attributes tolerantly", async () => {
// Given
const document = {
data: [
{
type: "registry-artifacts",
id: "core",
attributes: {
owner_name: "Prowler",
owner_slug: "prowler",
owner_type: "organization",
owner_logo_url: "https://cdn.example/prowler.png",
},
},
{
type: "registry-artifacts",
id: "plain-owner",
attributes: {
owner_name: "Ada",
owner_slug: "ada",
owner_type: "user",
owner_logo_url: null,
},
},
{
type: "registry-artifacts",
id: "ownerless",
attributes: { owner_name: " ", owner_logo_url: " " },
},
],
meta: { pagination: { page: 1, pages: 1, count: 3 } },
};
// When
const result = await collectCompleteRegistryCatalog(async () => document);
// Then
expect(result).toMatchObject({
status: "complete",
artifacts: [
{
normalizedName: "core",
owners: [
{
type: "organization",
name: "Prowler",
logoUrl: "https://cdn.example/prowler.png",
},
],
},
{
normalizedName: "ownerless",
owners: [],
},
{
normalizedName: "plain-owner",
owners: [{ type: "user", name: "Ada", logoUrl: undefined }],
},
],
});
});
it("defaults omitted built-in status and maps explicit built-ins", async () => {
// Given
const document = {
data: [
{ type: "registry-artifacts", id: "installable", attributes: {} },
{
type: "registry-artifacts",
id: "built-in",
attributes: { is_builtin: true },
},
],
meta: { pagination: { page: 1, pages: 1, count: 2 } },
};
// When
const result = await collectCompleteRegistryCatalog(async () => document);
// Then
expect(result).toMatchObject({
status: "complete",
artifacts: [
{ normalizedName: "built-in", isBuiltin: true },
{ normalizedName: "installable", isBuiltin: false },
],
});
});
it("rejects malformed built-in values and preserves built-in duplicates", async () => {
// Given
const document = (data: unknown[]) => ({
data,
meta: { pagination: { page: 1, pages: 1, count: data.length } },
});
const resource = (id: string, isBuiltin: unknown) => ({
type: "registry-artifacts",
id,
attributes: { is_builtin: isBuiltin },
});
// When
const explicitFalse = await collectCompleteRegistryCatalog(async () =>
document([resource("installable", false)]),
);
const malformed = await Promise.all(
[null, "true", 1].map((isBuiltin) =>
collectCompleteRegistryCatalog(async () =>
document([resource("malformed", isBuiltin)]),
),
),
);
const duplicate = await collectCompleteRegistryCatalog(async (page) => ({
data: [resource("built-in", page === 2)],
meta: { pagination: { page, pages: 2, count: 2 } },
}));
// Then
expect(explicitFalse).toMatchObject({
status: "complete",
artifacts: [{ normalizedName: "installable", isBuiltin: false }],
});
expect(malformed).toEqual([
{ status: "incomplete", reason: "invalid_resource", collectedCount: 1 },
{ status: "incomplete", reason: "invalid_resource", collectedCount: 1 },
{ status: "incomplete", reason: "invalid_resource", collectedCount: 1 },
]);
expect(duplicate).toMatchObject({
status: "complete",
artifacts: [{ normalizedName: "built-in", isBuiltin: true }],
});
});
it("preserves artifact counts, including zero, without inventing missing counts", async () => {
// Given
const resources = [
{ id: "aws", attributes: { check_count: 645, compliance_count: 45 } },
{ id: "openai", attributes: { check_count: 2, compliance_count: 0 } },
{ id: "missing", attributes: {} },
{
id: "unknown",
attributes: { check_count: null, compliance_count: null },
},
{ id: "aws", attributes: { check_count: 645 } },
].map((resource) => ({
type: "registry-available-artifacts",
...resource,
}));
// When
const result = await collectCompleteRegistryCatalog(async () => ({
data: resources,
meta: { pagination: { page: 1, pages: 1, count: resources.length } },
}));
// Then
expect(result).toMatchObject({
status: "complete",
artifacts: [
{ normalizedName: "aws", checkCount: 645, complianceCount: 45 },
{
normalizedName: "missing",
checkCount: undefined,
complianceCount: undefined,
},
{ normalizedName: "openai", checkCount: 2, complianceCount: 0 },
{
normalizedName: "unknown",
checkCount: undefined,
complianceCount: undefined,
},
],
});
});
it("preserves the declared provider when merging complementary catalog entries", async () => {
// Given
const fetchPage = async (page: number) => ({
data: [
{
type: "registry-artifacts",
id: "external-package",
attributes:
page === 1
? { providers: ["aaa"], has_checks: true }
: { providers: ["zzz"], has_provider: true },
},
],
meta: { pagination: { page, pages: 2, count: 2 } },
});
// When
const result = await collectCompleteRegistryCatalog(fetchPage);
// Then
expect(result).toMatchObject({
status: "complete",
artifacts: [
{ hasProvider: true, providerSlug: "zzz", providers: ["aaa", "zzz"] },
],
});
});
it("rejects duplicate catalog entries with conflicting declared providers", async () => {
// Given
const fetchPage = async (page: number) => ({
data: [
{
type: "registry-artifacts",
id: "external-package",
attributes: {
has_provider: true,
providers: [page === 1 ? "aaa" : "zzz"],
},
},
],
meta: { pagination: { page, pages: 2, count: 2 } },
});
// When / Then
await expect(
collectCompleteRegistryCatalog(fetchPage),
).resolves.toMatchObject({
status: "incomplete",
reason: "conflicting_duplicate",
});
});
it("traverses, merges, and degrades unsafe catalog data", async () => {
// Given
const resource = (
id: string,
attributes: Record<string, unknown> = {},
) => ({ type: "registry-artifacts", id, attributes });
const document = (
page: number,
pages: number,
count: number,
data: unknown[],
) => ({ data, meta: { pagination: { page, pages, count } } });
const requests: Array<[number, string | null, string | null]> = [];
// When
const complete = await collectCompleteRegistryCatalog(
async (page, query) => {
requests.push([
page,
query.get("page[number]"),
query.get("page[size]"),
]);
return page === 1
? document(1, 2, 3, [
resource("core", {
name: "Core",
providers: ["AWS"],
is_verified: true,
version_count: 1,
total_downloads: 2,
owner_name: "Prowler",
owner_type: "organization",
}),
resource("zeta"),
])
: document(2, 2, 3, [
resource("core", {
description: "Registry core",
latest_version: "2.0.0",
providers: ["gcp"],
is_official: true,
has_checks: true,
version_count: 3,
total_downloads: 8,
}),
]);
},
);
const limits = await Promise.all(
[999, 1000, 1001].map(async (pages) => {
let requests = 0;
const result = await collectCompleteRegistryCatalog(async (page) => {
requests += 1;
return document(page, pages, pages, [resource(`item-${page}`)]);
});
return [pages, requests, result] as const;
}),
);
const failures = await Promise.all([
collectCompleteRegistryCatalog(async () => ({ data: {}, meta: {} })),
collectCompleteRegistryCatalog(async () =>
document(1, 1, 2, [resource("one")]),
),
collectCompleteRegistryCatalog(async (page) =>
document(page === 1 ? 1 : 1, 2, 2, [resource(`item-${page}`)]),
),
collectCompleteRegistryCatalog(async (page) =>
document(page, page === 1 ? 2 : 3, 2, [resource(`item-${page}`)]),
),
collectCompleteRegistryCatalog(async () =>
document(1, 1, 1, [resource("")]),
),
collectCompleteRegistryCatalog(async (page) =>
document(page, 2, 2, [
resource("duplicate", { name: page === 1 ? "One" : "Two" }),
]),
),
collectCompleteRegistryCatalog(async (page) => {
if (page === 2) throw new Error("offline");
return document(1, 2, 2, [resource("first")]);
}),
]);
// Then
expect(requests).toEqual([
[1, "1", "100"],
[2, "2", "100"],
]);
expect(complete).toMatchObject({
status: "complete",
artifacts: [
{
normalizedName: "core",
name: "Core",
description: "Registry core",
latestVersion: "2.0.0",
providers: ["aws", "gcp"],
isVerified: true,
isOfficial: true,
hasChecks: true,
versionCount: 3,
totalDownloads: 8,
owners: [{ type: "organization", name: "Prowler" }],
},
{ normalizedName: "zeta" },
],
});
expect(limits.map(([pages, requests]) => [pages, requests])).toEqual([
[999, 999],
[1000, 1000],
[1001, 1],
]);
expect(limits[2]?.[2]).toEqual({
status: "incomplete",
reason: "guard_exhausted",
collectedCount: 1,
});
expect(
failures.map((result) =>
result.status === "incomplete" ? result.reason : undefined,
),
).toEqual([
"invalid_page",
"count_mismatch",
"invalid_page",
"invalid_page",
"invalid_resource",
"conflicting_duplicate",
"page_failed",
]);
failures.forEach((result) =>
expect(result).not.toHaveProperty("artifacts"),
);
});
});