mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-25 12:54:09 +02:00
Import OpenAPI security the way Yaak inherits it (#601)
This commit is contained in:
@@ -15,7 +15,7 @@ import YAML from "yaml";
|
|||||||
type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
|
type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
|
||||||
type UnknownRecord = Record<string, unknown>;
|
type UnknownRecord = Record<string, unknown>;
|
||||||
type ImportResources = {
|
type ImportResources = {
|
||||||
workspaces: AtLeast<Workspace, "name" | "id" | "model">[];
|
workspaces: AtLeast<Workspace, "name" | "id" | "model" | "authentication">[];
|
||||||
environments: AtLeast<Environment, "name" | "id" | "model" | "workspaceId" | "variables">[];
|
environments: AtLeast<Environment, "name" | "id" | "model" | "workspaceId" | "variables">[];
|
||||||
folders: AtLeast<Folder, "name" | "id" | "model" | "workspaceId">[];
|
folders: AtLeast<Folder, "name" | "id" | "model" | "workspaceId">[];
|
||||||
httpRequests: AtLeast<HttpRequest, "name" | "id" | "model" | "workspaceId">[];
|
httpRequests: AtLeast<HttpRequest, "name" | "id" | "model" | "workspaceId">[];
|
||||||
@@ -61,6 +61,7 @@ export async function convertOpenApi(contents: string): Promise<ImportPluginResp
|
|||||||
id: importState.generateId("workspace"),
|
id: importState.generateId("workspace"),
|
||||||
name: stringAt(spec.info, "title") ?? "OpenAPI Import",
|
name: stringAt(spec.info, "title") ?? "OpenAPI Import",
|
||||||
description: importInfoDescription(toRecord(spec.info)),
|
description: importInfoDescription(toRecord(spec.info)),
|
||||||
|
authentication: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const resources: ImportResources = {
|
const resources: ImportResources = {
|
||||||
@@ -89,6 +90,23 @@ export async function convertOpenApi(contents: string): Promise<ImportPluginResp
|
|||||||
sortPriority: importState.nextSortPriority(),
|
sortPriority: importState.nextSortPriority(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Spec-level security is the default for every operation, which is exactly
|
||||||
|
// Yaak's inheritance model: it lives on the workspace, and only operations
|
||||||
|
// that declare their own security carry per-request authentication. API keys
|
||||||
|
// materialized as headers or query parameters go onto inheriting requests
|
||||||
|
// individually — workspace headers would also reach operations that override
|
||||||
|
// or disable security, leaking the credential to endpoints that opted out.
|
||||||
|
const workspaceAuthentication = importAuthentication({
|
||||||
|
authenticationVariables,
|
||||||
|
importState,
|
||||||
|
oauthVariablesByScheme,
|
||||||
|
security: spec.security,
|
||||||
|
spec,
|
||||||
|
useDynamicServerUrls: serverEnvironments.length > 1,
|
||||||
|
});
|
||||||
|
workspace.authentication = workspaceAuthentication.authentication;
|
||||||
|
workspace.authenticationType = workspaceAuthentication.authenticationType;
|
||||||
|
|
||||||
const folderIdsByTag = new Map<string, string>();
|
const folderIdsByTag = new Map<string, string>();
|
||||||
const routeLabels = new Map<string, string>();
|
const routeLabels = new Map<string, string>();
|
||||||
for (const tag of toArray(spec.tags)) {
|
for (const tag of toArray(spec.tags)) {
|
||||||
@@ -125,6 +143,7 @@ export async function convertOpenApi(contents: string): Promise<ImportPluginResp
|
|||||||
|
|
||||||
const request = importOperation({
|
const request = importOperation({
|
||||||
importState,
|
importState,
|
||||||
|
inheritedAuthentication: workspaceAuthentication,
|
||||||
method,
|
method,
|
||||||
operation,
|
operation,
|
||||||
oauthVariablesByScheme,
|
oauthVariablesByScheme,
|
||||||
@@ -144,7 +163,8 @@ export async function convertOpenApi(contents: string): Promise<ImportPluginResp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resources.httpRequests.some((request) => request.authenticationType === "oauth2")) {
|
const authenticationConfigs = [workspace, ...resources.httpRequests];
|
||||||
|
if (authenticationConfigs.some((model) => model.authenticationType === "oauth2")) {
|
||||||
const variableNames = new Set(
|
const variableNames = new Set(
|
||||||
[...oauthVariablesByScheme.values()].flatMap(({ clientId, clientSecret }) => [
|
[...oauthVariablesByScheme.values()].flatMap(({ clientId, clientSecret }) => [
|
||||||
clientId,
|
clientId,
|
||||||
@@ -152,10 +172,10 @@ export async function convertOpenApi(contents: string): Promise<ImportPluginResp
|
|||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
resources.httpRequests.some(
|
authenticationConfigs.some(
|
||||||
(request) =>
|
(model) =>
|
||||||
request.authenticationType === "oauth2" &&
|
model.authenticationType === "oauth2" &&
|
||||||
Object.values(toRecord(request.authentication)).some(
|
Object.values(toRecord(model.authentication)).some(
|
||||||
(value) =>
|
(value) =>
|
||||||
typeof value === "string" && value.includes(templateVariable("baseUrlOrigin")),
|
typeof value === "string" && value.includes(templateVariable("baseUrlOrigin")),
|
||||||
),
|
),
|
||||||
@@ -255,6 +275,7 @@ function disambiguateNames(
|
|||||||
|
|
||||||
function importOperation({
|
function importOperation({
|
||||||
importState,
|
importState,
|
||||||
|
inheritedAuthentication,
|
||||||
method,
|
method,
|
||||||
operation,
|
operation,
|
||||||
oauthVariablesByScheme,
|
oauthVariablesByScheme,
|
||||||
@@ -270,6 +291,7 @@ function importOperation({
|
|||||||
authenticationVariables,
|
authenticationVariables,
|
||||||
}: {
|
}: {
|
||||||
importState: ImportState;
|
importState: ImportState;
|
||||||
|
inheritedAuthentication: ImportedAuthentication;
|
||||||
method: string;
|
method: string;
|
||||||
operation: UnknownRecord;
|
operation: UnknownRecord;
|
||||||
oauthVariablesByScheme: Map<string, OAuthVariableNames>;
|
oauthVariablesByScheme: Map<string, OAuthVariableNames>;
|
||||||
@@ -291,14 +313,23 @@ function importOperation({
|
|||||||
operationParameters: toArray(operation.parameters),
|
operationParameters: toArray(operation.parameters),
|
||||||
});
|
});
|
||||||
const body = importBody({ importState, operation, parameters, spec });
|
const body = importBody({ importState, operation, parameters, spec });
|
||||||
const authentication = importAuthentication({
|
// Operations without their own security inherit the workspace's (null
|
||||||
authenticationVariables,
|
// authenticationType), the same way an operation inherits spec security
|
||||||
importState,
|
const hasOwnSecurity = Array.isArray(operation.security);
|
||||||
oauthVariablesByScheme,
|
const authentication = hasOwnSecurity
|
||||||
operation,
|
? importAuthentication({
|
||||||
spec,
|
authenticationVariables,
|
||||||
useDynamicServerUrls,
|
importState,
|
||||||
});
|
oauthVariablesByScheme,
|
||||||
|
security: operation.security,
|
||||||
|
spec,
|
||||||
|
useDynamicServerUrls,
|
||||||
|
})
|
||||||
|
: {
|
||||||
|
...emptyAuthentication(),
|
||||||
|
headers: inheritedAuthentication.headers,
|
||||||
|
urlParameters: inheritedAuthentication.urlParameters,
|
||||||
|
};
|
||||||
const pathExampleValues = new Map(
|
const pathExampleValues = new Map(
|
||||||
parameters
|
parameters
|
||||||
.map((p) => importState.resolve(p))
|
.map((p) => importState.resolve(p))
|
||||||
@@ -792,18 +823,52 @@ function importUrlParameters({
|
|||||||
stringAt(p, "in") === "query" ||
|
stringAt(p, "in") === "query" ||
|
||||||
(stringAt(p, "in") === "path" && placeholderNames.has(stringAt(p, "name") ?? "")),
|
(stringAt(p, "in") === "path" && placeholderNames.has(stringAt(p, "name") ?? "")),
|
||||||
)
|
)
|
||||||
.map((p) => ({
|
.flatMap((p) => {
|
||||||
|
const name = stringAt(p, "name") ?? "";
|
||||||
|
if (name.length === 0) return [];
|
||||||
|
|
||||||
// Path parameters are required by definition, and a disabled one would
|
// Path parameters are required by definition, and a disabled one would
|
||||||
// leave the literal `:name` in the sent URL even for sloppy specs that
|
// leave the literal `:name` in the sent URL even for sloppy specs that
|
||||||
// omit `required: true`
|
// omit `required: true`
|
||||||
enabled: p.required === true || stringAt(p, "in") === "path",
|
const enabled = p.required === true || stringAt(p, "in") === "path";
|
||||||
name:
|
if (stringAt(p, "in") === "query") {
|
||||||
stringAt(p, "in") === "path"
|
const raw = rawParameterExample(p, importState);
|
||||||
? `:${stringAt(p, "name") ?? ""}`
|
if (Array.isArray(raw)) {
|
||||||
: (stringAt(p, "name") ?? ""),
|
const { separator } = queryArraySerialization(p);
|
||||||
value: parameterExample(p, importState),
|
if (separator == null) {
|
||||||
}))
|
return raw.map((item) => ({ enabled, name, value: stringifyExampleValue(item) }));
|
||||||
.filter(({ name }) => name.length > 0);
|
}
|
||||||
|
return [{ enabled, name, value: raw.map(stringifyExampleValue).join(separator) }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
enabled,
|
||||||
|
name: stringAt(p, "in") === "path" ? `:${name}` : name,
|
||||||
|
value: parameterExample(p, importState),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenAPI 3 query arrays default to form/explode, one parameter per item;
|
||||||
|
* Swagger 2 defaults to comma-separated unless collectionFormat says otherwise.
|
||||||
|
* A null separator means repeated parameters.
|
||||||
|
*/
|
||||||
|
function queryArraySerialization(parameter: UnknownRecord): { separator: string | null } {
|
||||||
|
const collectionFormat = stringAt(parameter, "collectionFormat");
|
||||||
|
if (collectionFormat != null || parameter.schema == null) {
|
||||||
|
if (collectionFormat === "multi") return { separator: null };
|
||||||
|
return {
|
||||||
|
separator: { csv: ",", ssv: " ", tsv: "\t", pipes: "|" }[collectionFormat ?? "csv"] ?? ",",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const style = stringAt(parameter, "style");
|
||||||
|
if (style === "spaceDelimited") return { separator: " " };
|
||||||
|
if (style === "pipeDelimited") return { separator: "|" };
|
||||||
|
return parameter.explode === false ? { separator: "," } : { separator: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
// The spec says header parameters with these names SHALL be ignored; Accept and
|
// The spec says header parameters with these names SHALL be ignored; Accept and
|
||||||
@@ -857,15 +922,23 @@ function importCookieHeader({
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function parameterExample(parameter: UnknownRecord, importState: ImportState): string {
|
function rawParameterExample(parameter: UnknownRecord, importState: ImportState): unknown {
|
||||||
const directExample = firstPresent(
|
const directExample = firstPresent(
|
||||||
parameter.example,
|
parameter.example,
|
||||||
firstExampleValue(parameter.examples, importState),
|
firstExampleValue(parameter.examples, importState),
|
||||||
);
|
);
|
||||||
if (directExample != null) return stringifyExampleValue(directExample);
|
if (directExample != null) return directExample;
|
||||||
const example = stringifyExampleValue(
|
// Swagger 2 parameters carry the schema keywords (type, items, default)
|
||||||
schemaToExample(importState.resolve(parameter.schema), importState),
|
// directly on the parameter object
|
||||||
);
|
return schemaToExample(importState.resolve(parameter.schema ?? parameter), importState);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parameterExample(parameter: UnknownRecord, importState: ImportState): string {
|
||||||
|
const raw = rawParameterExample(parameter, importState);
|
||||||
|
// Simple/csv style, the default everywhere but query strings
|
||||||
|
const example = Array.isArray(raw)
|
||||||
|
? raw.map(stringifyExampleValue).join(",")
|
||||||
|
: stringifyExampleValue(raw);
|
||||||
// An empty path segment makes a URL that matches nothing, so the name at
|
// An empty path segment makes a URL that matches nothing, so the name at
|
||||||
// least keeps the request sendable and shows what belongs there
|
// least keeps the request sendable and shows what belongs there
|
||||||
if (example === "" && stringAt(parameter, "in") === "path") {
|
if (example === "" && stringAt(parameter, "in") === "path") {
|
||||||
@@ -1158,34 +1231,28 @@ function inferSchemaType(schema: UnknownRecord): string {
|
|||||||
return "string";
|
return "string";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Security Requirement Objects are ordered alternatives, so the first one this
|
||||||
|
* importer can represent wins. That makes `[{bearer}, {}]` import the bearer
|
||||||
|
* auth the author listed first, while `[{}, {bearer}]` imports as anonymous.
|
||||||
|
*/
|
||||||
function importAuthentication({
|
function importAuthentication({
|
||||||
authenticationVariables,
|
authenticationVariables,
|
||||||
importState,
|
importState,
|
||||||
oauthVariablesByScheme,
|
oauthVariablesByScheme,
|
||||||
operation,
|
security,
|
||||||
spec,
|
spec,
|
||||||
useDynamicServerUrls,
|
useDynamicServerUrls,
|
||||||
}: {
|
}: {
|
||||||
authenticationVariables: AuthenticationVariableRegistry;
|
authenticationVariables: AuthenticationVariableRegistry;
|
||||||
importState: ImportState;
|
importState: ImportState;
|
||||||
oauthVariablesByScheme: Map<string, OAuthVariableNames>;
|
oauthVariablesByScheme: Map<string, OAuthVariableNames>;
|
||||||
operation: UnknownRecord;
|
security: unknown;
|
||||||
spec: UnknownRecord;
|
spec: UnknownRecord;
|
||||||
useDynamicServerUrls: boolean;
|
useDynamicServerUrls: boolean;
|
||||||
}): ImportedAuthentication {
|
}): ImportedAuthentication {
|
||||||
const security = operation.security ?? spec.security;
|
if (!Array.isArray(security)) return emptyAuthentication();
|
||||||
if (Array.isArray(operation.security) && operation.security.length === 0) {
|
if (security.length === 0) {
|
||||||
return { ...emptyAuthentication(), authenticationType: "none" };
|
|
||||||
}
|
|
||||||
if (!Array.isArray(security) || security.length === 0) {
|
|
||||||
return emptyAuthentication();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Security Requirement Objects are alternatives. If any alternative is
|
|
||||||
// empty, authentication is optional regardless of where it appears.
|
|
||||||
if (
|
|
||||||
security.some((requirement) => isRecord(requirement) && Object.keys(requirement).length === 0)
|
|
||||||
) {
|
|
||||||
return { ...emptyAuthentication(), authenticationType: "none" };
|
return { ...emptyAuthentication(), authenticationType: "none" };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1195,6 +1262,9 @@ function importAuthentication({
|
|||||||
};
|
};
|
||||||
for (const rawRequirement of security) {
|
for (const rawRequirement of security) {
|
||||||
if (!isRecord(rawRequirement)) continue;
|
if (!isRecord(rawRequirement)) continue;
|
||||||
|
if (Object.keys(rawRequirement).length === 0) {
|
||||||
|
return { ...emptyAuthentication(), authenticationType: "none" };
|
||||||
|
}
|
||||||
|
|
||||||
const imported = importSecurityRequirement({
|
const imported = importSecurityRequirement({
|
||||||
authenticationVariables,
|
authenticationVariables,
|
||||||
@@ -1208,7 +1278,9 @@ function importAuthentication({
|
|||||||
if (imported != null) return imported;
|
if (imported != null) return imported;
|
||||||
}
|
}
|
||||||
|
|
||||||
return emptyAuthentication();
|
// Declared security this importer cannot represent (e.g. mutualTLS alone)
|
||||||
|
// should not fall back to inheriting some other authentication
|
||||||
|
return { ...emptyAuthentication(), authenticationType: "none" };
|
||||||
}
|
}
|
||||||
|
|
||||||
function importSecurityRequirement({
|
function importSecurityRequirement({
|
||||||
|
|||||||
@@ -292,6 +292,8 @@ Responses:
|
|||||||
"websocketRequests": [],
|
"websocketRequests": [],
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
{
|
{
|
||||||
|
"authentication": {},
|
||||||
|
"authenticationType": "none",
|
||||||
"description": "Wikipedia for Web APIs. Repository of API definitions in OpenAPI format.
|
"description": "Wikipedia for Web APIs. Repository of API definitions in OpenAPI format.
|
||||||
**Warning**: If you want to be notified about changes in advance please join our [Slack channel](https://join.slack.com/t/mermade/shared_invite/zt-g78g7xir-MLE_CTCcXCdfJfG3CJe9qA).
|
**Warning**: If you want to be notified about changes in advance please join our [Slack channel](https://join.slack.com/t/mermade/shared_invite/zt-g78g7xir-MLE_CTCcXCdfJfG3CJe9qA).
|
||||||
Client sample: [[Demo]](https://apis.guru/simple-ui) [[Repo]](https://github.com/APIs-guru/simple-ui)
|
Client sample: [[Demo]](https://apis.guru/simple-ui) [[Repo]](https://github.com/APIs-guru/simple-ui)
|
||||||
@@ -2587,6 +2589,8 @@ Responses:
|
|||||||
"websocketRequests": [],
|
"websocketRequests": [],
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
{
|
{
|
||||||
|
"authentication": {},
|
||||||
|
"authenticationType": null,
|
||||||
"description": "A simple HTTP Request & Response Service.<br/> <br/> <b>Run locally: </b> <code>$ docker run -p 80:80 kennethreitz/httpbin</code>
|
"description": "A simple HTTP Request & Response Service.<br/> <br/> <b>Run locally: </b> <code>$ docker run -p 80:80 kennethreitz/httpbin</code>
|
||||||
|
|
||||||
Contact: me@kennethreitz.org",
|
Contact: me@kennethreitz.org",
|
||||||
@@ -2717,6 +2721,8 @@ Responses:
|
|||||||
"websocketRequests": [],
|
"websocketRequests": [],
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
{
|
{
|
||||||
|
"authentication": {},
|
||||||
|
"authenticationType": null,
|
||||||
"description": "This endpoint structures the APOD imagery and associated metadata so that it can be repurposed for other applications. In addition, if the concept_tags parameter is set to True, then keywords derived from the image explanation are returned. These keywords could be used as auto-generated hashtags for twitter or instagram feeds; but generally help with discoverability of relevant imagery
|
"description": "This endpoint structures the APOD imagery and associated metadata so that it can be repurposed for other applications. In addition, if the concept_tags parameter is set to True, then keywords derived from the image explanation are returned. These keywords could be used as auto-generated hashtags for twitter or instagram feeds; but generally help with discoverability of relevant imagery
|
||||||
|
|
||||||
Contact: evan.t.yates@nasa.gov
|
Contact: evan.t.yates@nasa.gov
|
||||||
@@ -2819,6 +2825,8 @@ Responses:
|
|||||||
"websocketRequests": [],
|
"websocketRequests": [],
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
{
|
{
|
||||||
|
"authentication": {},
|
||||||
|
"authenticationType": null,
|
||||||
"description": "Webcomic of romance, sarcasm, math, and language.",
|
"description": "Webcomic of romance, sarcasm, math, and language.",
|
||||||
"id": "GENERATE_ID::WORKSPACE_0",
|
"id": "GENERATE_ID::WORKSPACE_0",
|
||||||
"model": "workspace",
|
"model": "workspace",
|
||||||
|
|||||||
@@ -850,7 +850,7 @@ describe("importer-openapi", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Preserves anonymous security alternatives and explicit auth overrides", async () => {
|
test("Respects the order of security alternatives and explicit auth overrides", async () => {
|
||||||
const imported = await convertOpenApi(
|
const imported = await convertOpenApi(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
openapi: "3.1.0",
|
openapi: "3.1.0",
|
||||||
@@ -873,10 +873,158 @@ describe("importer-openapi", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
expect(imported?.resources.workspaces[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
authenticationType: "bearer",
|
||||||
|
authentication: { token: "${[auth_bearer_auth_token]}", prefix: "Bearer" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
expect(imported?.resources.httpRequests).toEqual([
|
expect(imported?.resources.httpRequests).toEqual([
|
||||||
|
// The author listed bearer before the anonymous alternative
|
||||||
|
expect.objectContaining({
|
||||||
|
authenticationType: "bearer",
|
||||||
|
authentication: { token: "${[auth_bearer_auth_token]}", prefix: "Bearer" },
|
||||||
|
}),
|
||||||
expect.objectContaining({ authenticationType: "none", authentication: {} }),
|
expect.objectContaining({ authenticationType: "none", authentication: {} }),
|
||||||
expect.objectContaining({ authenticationType: "none", authentication: {} }),
|
expect.objectContaining({ authenticationType: "none", authentication: {} }),
|
||||||
expect.objectContaining({ authenticationType: "none", authentication: {} }),
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Imports spec-level security as inherited workspace authentication", async () => {
|
||||||
|
const imported = await convertOpenApi(
|
||||||
|
JSON.stringify({
|
||||||
|
openapi: "3.1.0",
|
||||||
|
info: { title: "Inherited Auth", version: "1.0.0" },
|
||||||
|
security: [{ bearerAuth: [], queryKey: [], headerKey: [] }],
|
||||||
|
paths: {
|
||||||
|
"/inherits": { get: { responses: {} } },
|
||||||
|
"/own-auth": { get: { security: [{ otherKey: [] }], responses: {} } },
|
||||||
|
"/public": { get: { security: [], responses: {} } },
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
securitySchemes: {
|
||||||
|
bearerAuth: { type: "http", scheme: "bearer" },
|
||||||
|
queryKey: { type: "apiKey", in: "query", name: "api_key" },
|
||||||
|
headerKey: { type: "apiKey", in: "header", name: "X-Tenant" },
|
||||||
|
otherKey: { type: "apiKey", in: "header", name: "X-Other" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(imported?.resources.workspaces[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
authenticationType: "bearer",
|
||||||
|
authentication: { token: "${[auth_bearer_auth_token]}", prefix: "Bearer" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(imported?.resources.httpRequests).toEqual([
|
||||||
|
// No authenticationType means inherit; materialized API keys ride along
|
||||||
|
// on the request since headers or parameters at the workspace level
|
||||||
|
// would also reach operations that opted out
|
||||||
|
expect.objectContaining({
|
||||||
|
authentication: {},
|
||||||
|
headers: [{ enabled: true, name: "X-Tenant", value: "${[auth_header_key_key]}" }],
|
||||||
|
urlParameters: [{ enabled: true, name: "api_key", value: "${[auth_query_key_key]}" }],
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
authenticationType: "apikey",
|
||||||
|
authentication: expect.objectContaining({ key: "X-Other" }),
|
||||||
|
headers: [],
|
||||||
|
urlParameters: [],
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
authenticationType: "none",
|
||||||
|
headers: [],
|
||||||
|
urlParameters: [],
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
expect(imported?.resources.httpRequests[0]?.authenticationType).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Serializes array query parameters per style", async () => {
|
||||||
|
const imported = await convertOpenApi(
|
||||||
|
JSON.stringify({
|
||||||
|
openapi: "3.0.0",
|
||||||
|
info: { title: "Array Params", version: "1.0.0" },
|
||||||
|
paths: {
|
||||||
|
"/exploded": {
|
||||||
|
get: {
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: "tags",
|
||||||
|
in: "query",
|
||||||
|
required: true,
|
||||||
|
schema: { type: "array", items: { type: "string" }, example: ["a", "b"] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
responses: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/csv": {
|
||||||
|
get: {
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: "ids",
|
||||||
|
in: "query",
|
||||||
|
required: true,
|
||||||
|
explode: false,
|
||||||
|
schema: { type: "array", example: [1, 2, 3] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
responses: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(imported?.resources.httpRequests.map((r) => r.urlParameters)).toEqual([
|
||||||
|
[
|
||||||
|
{ enabled: true, name: "tags", value: "a" },
|
||||||
|
{ enabled: true, name: "tags", value: "b" },
|
||||||
|
],
|
||||||
|
[{ enabled: true, name: "ids", value: "1,2,3" }],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Serializes Swagger 2 array parameters per collectionFormat", async () => {
|
||||||
|
const imported = await convertOpenApi(
|
||||||
|
JSON.stringify({
|
||||||
|
swagger: "2.0",
|
||||||
|
info: { title: "Swagger Arrays", version: "1.0.0" },
|
||||||
|
host: "example.com",
|
||||||
|
paths: {
|
||||||
|
"/a": {
|
||||||
|
get: {
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: "tags",
|
||||||
|
in: "query",
|
||||||
|
required: true,
|
||||||
|
type: "array",
|
||||||
|
items: { type: "string", default: "x" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multi",
|
||||||
|
in: "query",
|
||||||
|
required: true,
|
||||||
|
type: "array",
|
||||||
|
collectionFormat: "multi",
|
||||||
|
items: { type: "string", enum: ["m1"] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
responses: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(imported?.resources.httpRequests[0]?.urlParameters).toEqual([
|
||||||
|
// Swagger defaults to csv
|
||||||
|
{ enabled: true, name: "tags", value: "x" },
|
||||||
|
{ enabled: true, name: "multi", value: "m1" },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user