Files
prowler/ui/lib/provider-credentials/provider-credential-schema.test.ts

267 lines
7.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import openaiSchema from "./fixtures/openai-credential-schema.json";
import templateSchema from "./fixtures/template-credential-schema.json";
import {
parseRegistryCredentialSchema,
REGISTRY_CREDENTIAL_SCHEMA_LIMITS,
} from "./provider-credential-schema";
const schema = {
type: "object",
properties: {
api_key: {
title: "API Key",
description: "The key.",
type: "string",
format: "password",
writeOnly: true,
},
scheme: {
title: "Scheme",
type: "string",
enum: ["bearer", "basic"],
default: "bearer",
},
notes: {
title: "Notes",
type: "string",
"x-prowler-widget": "textarea",
default: "",
},
},
required: ["api_key"],
};
describe("parseRegistryCredentialSchema", () => {
it("accepts the installed Template 0.2.5 schema with typed fields and examples", () => {
// Given / When
const result = parseRegistryCredentialSchema(templateSchema);
// Then
expect(result?.fields.map(({ name, kind }) => [name, kind])).toEqual([
["api_url", "text"],
["api_key", "password"],
["ca_bundle", "textarea"],
["verify_tls", "checkbox"],
["timeout_seconds", "integer"],
["auth_scheme", "select"],
]);
expect(result?.fields[0]).toMatchObject({
placeholder: "https://api.acme.com",
required: true,
});
expect(result?.fields[3].defaultValue).toBe(true);
expect(result?.fields[4]).toMatchObject({
defaultValue: 30,
minimum: 1,
maximum: 300,
});
});
it.each([
"api_key",
"platform_api_key",
"platform-api-key",
"apiKey",
"platformApiKey",
"platformAPIKey",
"API_KEY",
"apikey",
])("masks the plain API key field %s without schema annotations", (name) => {
// Given / When
const result = parseRegistryCredentialSchema({
type: "object",
properties: { [name]: { type: "string" } },
});
// Then
expect(result?.fields[0].kind).toBe("password");
});
it("keeps identifiers and explicitly configured widgets unchanged", () => {
// Given / When
const result = parseRegistryCredentialSchema({
type: "object",
properties: {
api_key_id: { type: "string" },
api_key_url: { type: "string" },
selected_api_key: { type: "string", enum: ["primary", "secondary"] },
multiline_api_key: { type: "string", "x-prowler-widget": "textarea" },
},
});
// Then
expect(result?.fields.map(({ kind }) => kind)).toEqual([
"text",
"text",
"select",
"textarea",
]);
});
it("accepts the installed OpenAI schema with its full description", () => {
// Given / When: the materialized OpenAI 0.1.5 schema contains a long docstring.
const result = parseRegistryCredentialSchema(openaiSchema);
// Then
expect(result?.fields.map(({ name }) => name)).toEqual([
"organization_id",
"platform_api_key",
"base_url",
]);
expect(result?.fields[2].defaultValue).toBe("https://api.openai.com/v1");
expect(result?.fields.map(({ kind }) => kind)).toEqual([
"text",
"password",
"text",
]);
});
it("preserves long field descriptions without treating them as input limits", () => {
// Given
const description = openaiSchema.description;
// When
const result = parseRegistryCredentialSchema({
...schema,
properties: { token: { type: "string", description } },
required: ["token"],
});
// Then
expect(result?.fields[0].description).toBe(description);
});
it("accepts the observed flat credential schema and preserves property order", () => {
// Given
const result = parseRegistryCredentialSchema(schema);
// When / Then
expect(result?.fields.map(({ name, kind }) => [name, kind])).toEqual([
["api_key", "password"],
["scheme", "select"],
["notes", "textarea"],
]);
expect(result?.fields[0]).toMatchObject({
description: "The key.",
label: "API Key",
required: true,
});
});
it.each([
["$ref", { $ref: "#/$defs/credential" }],
["$defs", { $defs: {} }],
["definitions", { definitions: {} }],
["combinators", { anyOf: [] }],
["additional properties", { additionalProperties: true }],
])("rejects risky root keywords: %s", (_name, keyword) => {
expect(parseRegistryCredentialSchema({ ...schema, ...keyword })).toBeNull();
});
it.each([
["nested objects", { type: "object", properties: {} }],
["arrays", { type: "array" }],
["nullable unions", { type: ["string", "null"] }],
["unsupported formats", { type: "string", format: "email" }],
["passwords without writeOnly", { type: "string", format: "password" }],
["maps", { type: "string", additionalProperties: true }],
])("rejects unsupported fields: %s", (_name, apiKey) => {
expect(
parseRegistryCredentialSchema({
...schema,
properties: { ...schema.properties, api_key: apiKey },
}),
).toBeNull();
});
it.each([
{ type: "boolean", default: "true" },
{ type: "boolean", enum: [true] },
{ type: "integer", default: "30" },
{ type: "integer", default: 1.5 },
{ type: "integer", minimum: 1, default: 0 },
{ type: "integer", maximum: 300, default: 301 },
{ type: "integer", minimum: 10, maximum: 1 },
{ type: "integer", minimum: "1" },
{ type: "integer", maximum: Infinity },
{ type: "integer", multipleOf: 5 },
{ type: "string", examples: "not-an-array" },
{ type: "string", examples: [{ value: "unexpected" }] },
])(
"rejects malformed annotations or unsupported constraints: %j",
(property) => {
expect(
parseRegistryCredentialSchema({
type: "object",
properties: { field: property },
}),
).toBeNull();
},
);
it.each([
[
"invalid defaults",
{ type: "string", enum: ["bearer", "basic"], default: "token" },
],
["duplicate values", { type: "string", enum: ["bearer", "bearer"] }],
])("rejects enum definitions with %s", (_name, scheme) => {
expect(
parseRegistryCredentialSchema({
...schema,
properties: { ...schema.properties, scheme },
}),
).toBeNull();
});
it("rejects unsafe names, invalid required fields, and over-limit metadata", () => {
// Given
const fields = Object.fromEntries(
Array.from(
{ length: REGISTRY_CREDENTIAL_SCHEMA_LIMITS.MAX_FIELDS + 1 },
(_, index) => [`field${index}`, { type: "string" }],
),
);
const cases = [
{ ...schema, required: ["missing"] },
{ ...schema, description: { invalid: "not text" } },
{
...schema,
properties: { api_key: { type: "string", description: 123 } },
},
JSON.parse(
'{"type":"object","properties":{"__proto__":{"type":"string"}}}',
),
{ type: "object", properties: fields },
{
...schema,
properties: {
...schema.properties,
notes: {
...schema.properties.notes,
title: "a".repeat(
REGISTRY_CREDENTIAL_SCHEMA_LIMITS.MAX_TEXT_LENGTH + 1,
),
},
},
},
];
// When / Then
expect(cases.map(parseRegistryCredentialSchema)).toEqual([
null,
null,
null,
null,
null,
null,
]);
});
});