diff --git a/packages/kbn-openapi-bundler/src/bundler/merge_documents/create_blank_oas_document.ts b/packages/kbn-openapi-bundler/src/bundler/merge_documents/create_blank_oas_document.ts index 925471719b345..363888566aa31 100644 --- a/packages/kbn-openapi-bundler/src/bundler/merge_documents/create_blank_oas_document.ts +++ b/packages/kbn-openapi-bundler/src/bundler/merge_documents/create_blank_oas_document.ts @@ -10,12 +10,32 @@ import { OpenAPIV3 } from 'openapi-types'; export function createBlankOpenApiDocument( oasVersion: string, - info: OpenAPIV3.InfoObject + overrides?: Partial ): OpenAPIV3.Document { return { openapi: oasVersion, - info, - servers: [ + info: overrides?.info ?? { + title: 'Merged OpenAPI specs', + version: 'not specified', + }, + paths: overrides?.paths ?? {}, + components: { + schemas: overrides?.components?.schemas, + responses: overrides?.components?.responses, + parameters: overrides?.components?.parameters, + examples: overrides?.components?.examples, + requestBodies: overrides?.components?.requestBodies, + headers: overrides?.components?.headers, + securitySchemes: overrides?.components?.securitySchemes ?? { + BasicAuth: { + type: 'http', + scheme: 'basic', + }, + }, + links: overrides?.components?.links, + callbacks: overrides?.components?.callbacks, + }, + servers: overrides?.servers ?? [ { url: 'http://{kibana_host}:{port}', variables: { @@ -28,19 +48,12 @@ export function createBlankOpenApiDocument( }, }, ], - security: [ + security: overrides?.security ?? [ { BasicAuth: [], }, ], - paths: {}, - components: { - securitySchemes: { - BasicAuth: { - type: 'http', - scheme: 'basic', - }, - }, - }, + tags: overrides?.tags, + externalDocs: overrides?.externalDocs, }; } diff --git a/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_documents.ts b/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_documents.ts index 24e507bd0e283..b430ac174b370 100644 --- a/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_documents.ts +++ b/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_documents.ts @@ -17,8 +17,9 @@ import { mergeTags } from './merge_tags'; import { getOasVersion } from '../../utils/get_oas_version'; import { getOasDocumentVersion } from '../../utils/get_oas_document_version'; import { enrichWithVersionMimeParam } from './enrich_with_version_mime_param'; +import { MergeOptions } from './merge_options'; -export interface MergeDocumentsOptions { +interface MergeDocumentsOptions extends MergeOptions { splitDocumentsByVersion: boolean; } @@ -52,10 +53,20 @@ export async function mergeDocuments( ...documentsGroup, ]; - mergedDocument.servers = mergeServers(documentsToMerge); - mergedDocument.paths = mergePaths(documentsToMerge); - mergedDocument.components = mergeSharedComponents(documentsToMerge); - mergedDocument.security = mergeSecurityRequirements(documentsToMerge); + mergedDocument.paths = mergePaths(documentsToMerge, options); + mergedDocument.components = { + ...mergedDocument.components, + ...mergeSharedComponents(documentsToMerge, options), + }; + + if (!options.skipServers) { + mergedDocument.servers = mergeServers(documentsToMerge); + } + + if (!options.skipSecurity) { + mergedDocument.security = mergeSecurityRequirements(documentsToMerge); + } + mergedDocument.tags = mergeTags(documentsToMerge); mergedByVersion.set(mergedDocument.info.version, mergedDocument); diff --git a/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_operations.ts b/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_operations.ts index c7a4ae4edbd7f..e0b4c3972d6c1 100644 --- a/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_operations.ts +++ b/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_operations.ts @@ -11,10 +11,12 @@ import deepEqual from 'fast-deep-equal'; import { OpenAPIV3 } from 'openapi-types'; import { KNOWN_HTTP_METHODS } from './http_methods'; import { isRefNode } from '../process_document'; +import { MergeOptions } from './merge_options'; export function mergeOperations( sourcePathItem: OpenAPIV3.PathItemObject, - mergedPathItem: OpenAPIV3.PathItemObject + mergedPathItem: OpenAPIV3.PathItemObject, + options: MergeOptions ) { for (const httpMethod of KNOWN_HTTP_METHODS) { const sourceOperation = sourcePathItem[httpMethod]; @@ -24,12 +26,18 @@ export function mergeOperations( continue; } - if (!mergedOperation || deepEqual(sourceOperation, mergedOperation)) { - mergedPathItem[httpMethod] = sourceOperation; + const normalizedSourceOperation = { + ...sourceOperation, + ...(options.skipServers ? { servers: undefined } : { servers: sourceOperation.servers }), + ...(options.skipSecurity ? { security: undefined } : { security: sourceOperation.security }), + }; + + if (!mergedOperation || deepEqual(normalizedSourceOperation, mergedOperation)) { + mergedPathItem[httpMethod] = normalizedSourceOperation; continue; } - mergeOperation(sourceOperation, mergedOperation); + mergeOperation(normalizedSourceOperation, mergedOperation); } } diff --git a/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_options.ts b/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_options.ts new file mode 100644 index 0000000000000..24bb048b2a5ed --- /dev/null +++ b/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_options.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export interface MergeOptions { + skipServers: boolean; + skipSecurity: boolean; +} diff --git a/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_paths.ts b/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_paths.ts index d1a775e07b278..938da557aa07b 100644 --- a/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_paths.ts +++ b/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_paths.ts @@ -12,8 +12,12 @@ import { ResolvedDocument } from '../ref_resolver/resolved_document'; import { isRefNode } from '../process_document'; import { mergeOperations } from './merge_operations'; import { mergeArrays } from './merge_arrays'; +import { MergeOptions } from './merge_options'; -export function mergePaths(resolvedDocuments: ResolvedDocument[]): OpenAPIV3.PathsObject { +export function mergePaths( + resolvedDocuments: ResolvedDocument[], + options: MergeOptions +): OpenAPIV3.PathsObject { const mergedPaths: Record = {}; for (const { absolutePath, document } of resolvedDocuments) { @@ -60,7 +64,7 @@ export function mergePaths(resolvedDocuments: ResolvedDocument[]): OpenAPIV3.Pat } try { - mergeOperations(sourcePathItem, mergedPathItem); + mergeOperations(sourcePathItem, mergedPathItem, options); } catch (e) { throw new Error( `❌ Unable to merge ${chalk.bold(absolutePath)} due to an error in ${chalk.bold( @@ -69,7 +73,9 @@ export function mergePaths(resolvedDocuments: ResolvedDocument[]): OpenAPIV3.Pat ); } - mergePathItemServers(sourcePathItem, mergedPathItem); + if (!options.skipServers) { + mergePathItemServers(sourcePathItem, mergedPathItem); + } try { mergeParameters(sourcePathItem, mergedPathItem); diff --git a/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_shared_components.ts b/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_shared_components.ts index 2efddc09e1de0..10b9300f69935 100644 --- a/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_shared_components.ts +++ b/packages/kbn-openapi-bundler/src/bundler/merge_documents/merge_shared_components.ts @@ -12,6 +12,7 @@ import { OpenAPIV3 } from 'openapi-types'; import { ResolvedDocument } from '../ref_resolver/resolved_document'; import { extractObjectByJsonPointer } from '../../utils/extract_by_json_pointer'; import { logger } from '../../logger'; +import { MergeOptions } from './merge_options'; const MERGEABLE_COMPONENT_TYPES = [ 'schemas', @@ -26,11 +27,16 @@ const MERGEABLE_COMPONENT_TYPES = [ ] as const; export function mergeSharedComponents( - bundledDocuments: ResolvedDocument[] + bundledDocuments: ResolvedDocument[], + options: MergeOptions ): OpenAPIV3.ComponentsObject { const mergedComponents: Record = {}; for (const componentsType of MERGEABLE_COMPONENT_TYPES) { + if (options.skipSecurity && componentsType === 'securitySchemes') { + continue; + } + const mergedTypedComponents = mergeObjects(bundledDocuments, `/components/${componentsType}`); if (Object.keys(mergedTypedComponents).length === 0) { diff --git a/packages/kbn-openapi-bundler/src/openapi_bundler.ts b/packages/kbn-openapi-bundler/src/openapi_bundler.ts index 90382150400dd..44bca89194507 100644 --- a/packages/kbn-openapi-bundler/src/openapi_bundler.ts +++ b/packages/kbn-openapi-bundler/src/openapi_bundler.ts @@ -7,8 +7,6 @@ */ import chalk from 'chalk'; -import { isUndefined, omitBy } from 'lodash'; -import { OpenAPIV3 } from 'openapi-types'; import { basename, dirname } from 'path'; import { bundleDocument, SkipException } from './bundler/bundle_document'; import { mergeDocuments } from './bundler/merge_documents'; @@ -19,6 +17,8 @@ import { writeDocuments } from './utils/write_documents'; import { ResolvedDocument } from './bundler/ref_resolver/resolved_document'; import { resolveGlobs } from './utils/resolve_globs'; import { DEFAULT_BUNDLING_PROCESSORS, withIncludeLabelsProcessor } from './bundler/processor_sets'; +import { PrototypeDocument } from './prototype_document'; +import { validatePrototypeDocument } from './validate_prototype_document'; export interface BundlerConfig { sourceGlob: string; @@ -27,8 +27,15 @@ export interface BundlerConfig { } interface BundleOptions { + /** + * OpenAPI document itself or path to the document + */ + prototypeDocument?: PrototypeDocument | string; + /** + * When specified the produced bundle will contain only + * operations objects with matching labels + */ includeLabels?: string[]; - specInfo?: Omit, 'version'>; } export const bundle = async ({ @@ -36,6 +43,10 @@ export const bundle = async ({ outputFilePath = 'bundled-{version}.schema.yaml', options, }: BundlerConfig) => { + const prototypeDocument = options?.prototypeDocument + ? await validatePrototypeDocument(options?.prototypeDocument) + : undefined; + logger.debug(chalk.bold(`Bundling API route schemas`)); logger.debug(`👀 Searching for source files in ${chalk.underline(sourceGlob)}`); @@ -56,22 +67,21 @@ export const bundle = async ({ logger.success(`Processed ${bundledDocuments.length} schemas`); - const blankOasFactory = (oasVersion: string, apiVersion: string) => + const blankOasDocumentFactory = (oasVersion: string, apiVersion: string) => createBlankOpenApiDocument(oasVersion, { - version: apiVersion, - title: options?.specInfo?.title ?? 'Bundled OpenAPI specs', - ...omitBy( - { - description: options?.specInfo?.description, - termsOfService: options?.specInfo?.termsOfService, - contact: options?.specInfo?.contact, - license: options?.specInfo?.license, - }, - isUndefined - ), + info: prototypeDocument?.info + ? { ...DEFAULT_INFO, ...prototypeDocument.info, version: apiVersion } + : { ...DEFAULT_INFO, version: apiVersion }, + servers: prototypeDocument?.servers, + security: prototypeDocument?.security, + components: { + securitySchemes: prototypeDocument?.components?.securitySchemes, + }, }); - const resultDocumentsMap = await mergeDocuments(bundledDocuments, blankOasFactory, { + const resultDocumentsMap = await mergeDocuments(bundledDocuments, blankOasDocumentFactory, { splitDocumentsByVersion: true, + skipServers: Boolean(prototypeDocument?.servers), + skipSecurity: Boolean(prototypeDocument?.security), }); await writeDocuments(resultDocumentsMap, outputFilePath); @@ -130,3 +140,7 @@ function filterOutSkippedDocuments( return processedDocuments; } + +const DEFAULT_INFO = { + title: 'Bundled OpenAPI specs', +} as const; diff --git a/packages/kbn-openapi-bundler/src/openapi_merger.ts b/packages/kbn-openapi-bundler/src/openapi_merger.ts index a7ac3c3492dfe..d0f532ca47d06 100644 --- a/packages/kbn-openapi-bundler/src/openapi_merger.ts +++ b/packages/kbn-openapi-bundler/src/openapi_merger.ts @@ -7,7 +7,7 @@ */ import chalk from 'chalk'; -import { OpenAPIV3 } from 'openapi-types'; + import { mergeDocuments } from './bundler/merge_documents'; import { logger } from './logger'; import { createBlankOpenApiDocument } from './bundler/merge_documents/create_blank_oas_document'; @@ -16,13 +16,20 @@ import { writeDocuments } from './utils/write_documents'; import { resolveGlobs } from './utils/resolve_globs'; import { bundleDocument } from './bundler/bundle_document'; import { withNamespaceComponentsProcessor } from './bundler/processor_sets'; +import { PrototypeDocument } from './prototype_document'; +import { validatePrototypeDocument } from './validate_prototype_document'; export interface MergerConfig { sourceGlobs: string[]; outputFilePath: string; - options?: { - mergedSpecInfo?: Partial; - }; + options?: MergerOptions; +} + +interface MergerOptions { + /** + * OpenAPI document itself or path to the document + */ + prototypeDocument?: PrototypeDocument | string; } export const merge = async ({ @@ -34,6 +41,10 @@ export const merge = async ({ throw new Error('As minimum one source glob is expected'); } + const prototypeDocument = options?.prototypeDocument + ? await validatePrototypeDocument(options?.prototypeDocument) + : undefined; + logger.info(chalk.bold(`Merging OpenAPI specs`)); logger.info( `👀 Searching for source files in ${sourceGlobs @@ -52,13 +63,18 @@ export const merge = async ({ const blankOasDocumentFactory = (oasVersion: string) => createBlankOpenApiDocument(oasVersion, { - title: 'Merged OpenAPI specs', - version: 'not specified', - ...(options?.mergedSpecInfo ?? {}), + info: prototypeDocument?.info ? { ...DEFAULT_INFO, ...prototypeDocument.info } : DEFAULT_INFO, + servers: prototypeDocument?.servers, + security: prototypeDocument?.security, + components: { + securitySchemes: prototypeDocument?.components?.securitySchemes, + }, }); const resultDocumentsMap = await mergeDocuments(bundledDocuments, blankOasDocumentFactory, { splitDocumentsByVersion: false, + skipServers: Boolean(prototypeDocument?.servers), + skipSecurity: Boolean(prototypeDocument?.security), }); // Only one document is expected when `splitDocumentsByVersion` is set to `false` const mergedDocument = Array.from(resultDocumentsMap.values())[0]; @@ -80,3 +96,8 @@ async function bundleDocuments(schemaFilePaths: string[]): Promise; + servers?: OpenAPIV3.ServerObject[]; + security?: OpenAPIV3.SecurityRequirementObject[]; + components?: { + securitySchemes: Record; + }; +} diff --git a/packages/kbn-openapi-bundler/src/utils/read_document.ts b/packages/kbn-openapi-bundler/src/utils/read_document.ts index 019f5103cf621..49476c134e91f 100644 --- a/packages/kbn-openapi-bundler/src/utils/read_document.ts +++ b/packages/kbn-openapi-bundler/src/utils/read_document.ts @@ -7,37 +7,48 @@ */ import fs from 'fs/promises'; -import { load } from 'js-yaml'; import { basename, extname } from 'path'; +import { load } from 'js-yaml'; import chalk from 'chalk'; import { logger } from '../logger'; +import { isPlainObjectType } from './is_plain_object_type'; export async function readDocument(documentPath: string): Promise> { - const extension = extname(documentPath); - logger.debug(`Reading ${chalk.bold(basename(documentPath))}`); + const maybeDocument = await readFile(documentPath); + + if (!isPlainObjectType(maybeDocument)) { + throw new Error(`File at ${chalk.bold(documentPath)} is not valid OpenAPI document`); + } + + return maybeDocument; +} + +async function readFile(filePath: string): Promise { + const extension = extname(filePath); + switch (extension) { case '.yaml': case '.yml': - return await readYamlDocument(documentPath); + return await readYamlFile(filePath); case '.json': - return await readJsonDocument(documentPath); + return await readJsonFile(filePath); default: throw new Error(`${extension} files are not supported`); } } -async function readYamlDocument(filePath: string): Promise> { +async function readYamlFile(filePath: string): Promise> { // Typing load's result to Record is optimistic as we can't be sure // there is object inside a yaml file. We don't have this validation layer so far // but using JSON Schemas here should mitigate this problem. return load(await fs.readFile(filePath, { encoding: 'utf8' })); } -export async function readJsonDocument(filePath: string): Promise> { +async function readJsonFile(filePath: string): Promise> { // Typing load's result to Record is optimistic as we can't be sure // there is object inside a yaml file. We don't have this validation layer so far // but using JSON Schemas here should mitigate this problem. diff --git a/packages/kbn-openapi-bundler/src/validate_prototype_document.ts b/packages/kbn-openapi-bundler/src/validate_prototype_document.ts new file mode 100644 index 0000000000000..82bfc0b0a6097 --- /dev/null +++ b/packages/kbn-openapi-bundler/src/validate_prototype_document.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import chalk from 'chalk'; +import { PrototypeDocument } from './prototype_document'; +import { readDocument } from './utils/read_document'; + +/** + * Validates that passed `prototypeDocument` fulfills the requirements. + * + * In particular security requirements must be specified via `security` and + * `components.securitySchemes` properties. + * + */ +export async function validatePrototypeDocument( + prototypeDocumentOrString: PrototypeDocument | string +): Promise { + const prototypeDocument: PrototypeDocument | undefined = + typeof prototypeDocumentOrString === 'string' + ? await readDocument(prototypeDocumentOrString) + : prototypeDocumentOrString; + + if (prototypeDocument.security && !prototypeDocument.components?.securitySchemes) { + throw new Error( + `Prototype document must contain ${chalk.bold( + 'components.securitySchemes' + )} when security requirements are specified` + ); + } + + if (prototypeDocument.components?.securitySchemes && !prototypeDocument.security) { + throw new Error( + `Prototype document must have ${chalk.bold('security')} defined ${chalk.bold( + 'components.securitySchemes' + )} are specified` + ); + } + + return prototypeDocument; +} diff --git a/packages/kbn-openapi-bundler/tests/bundler/result_overrides/security.test.ts b/packages/kbn-openapi-bundler/tests/bundler/result_overrides/security.test.ts new file mode 100644 index 0000000000000..12f3f14ac45c2 --- /dev/null +++ b/packages/kbn-openapi-bundler/tests/bundler/result_overrides/security.test.ts @@ -0,0 +1,403 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import chalk from 'chalk'; +import { OpenAPIV3 } from 'openapi-types'; +import { createOASDocument } from '../../create_oas_document'; +import { bundleSpecs } from '../bundle_specs'; + +describe('OpenAPI Bundler - with security requirements overrides', () => { + describe('enabled', () => { + it('throws an error when security requirements are specified without components security schemes', async () => { + const spec1 = createOASDocument({ + paths: { + '/api/some_api': { + get: { + responses: {}, + }, + }, + }, + }); + + await expect( + bundleSpecs( + { + 1: spec1, + }, + { + prototypeDocument: { + security: [{ ShouldBeUsedSecurityRequirement: [] }], + }, + } + ) + ).rejects.toThrowError( + `Prototype document must contain ${chalk.bold( + 'components.securitySchemes' + )} when security requirements are specified` + ); + }); + + it('throws an error when components security schemes are specified without security requirements', async () => { + const spec1 = createOASDocument({ + paths: { + '/api/some_api': { + get: { + responses: {}, + }, + }, + }, + }); + + await expect( + bundleSpecs( + { + 1: spec1, + }, + { + prototypeDocument: { + components: { + securitySchemes: { + ShouldBeUsedSecurityRequirement: { + type: 'http', + scheme: 'basic', + }, + }, + }, + }, + } + ) + ).rejects.toThrowError( + `Prototype document must have ${chalk.bold('security')} defined ${chalk.bold( + 'components.securitySchemes' + )} are specified` + ); + }); + + it('overrides root level `security`', async () => { + const spec1 = createOASDocument({ + paths: { + '/api/some_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + security: [{ SomeSecurityRequirement: [] }], + }); + const spec2 = createOASDocument({ + paths: { + '/api/another_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + security: [{ AnotherSecurityRequirement: [] }, { AdditionalSecurityRequirement: [] }], + }); + + const [bundledSpec] = Object.values( + await bundleSpecs( + { + 1: spec1, + 2: spec2, + }, + { + prototypeDocument: { + security: [{ ShouldBeUsedSecurityRequirement: [] }], + components: { + securitySchemes: { + ShouldBeUsedSecurityRequirement: { + type: 'http', + scheme: 'basic', + }, + }, + }, + }, + } + ) + ); + + expect(bundledSpec.security).toEqual([{ ShouldBeUsedSecurityRequirement: [] }]); + expect(bundledSpec.components?.securitySchemes).toEqual({ + ShouldBeUsedSecurityRequirement: { + type: 'http', + scheme: 'basic', + }, + }); + }); + + it('drops operation level security requirements', async () => { + const spec1 = createOASDocument({ + paths: { + '/api/some_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + security: [{ SomeSecurityRequirement: [] }], + }, + }, + }, + }); + const spec2 = createOASDocument({ + paths: { + '/api/another_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + security: [{ AnotherSecurityRequirement: [] }, { AdditionalSecurityRequirement: [] }], + }, + }, + }, + }); + + const [bundledSpec] = Object.values( + await bundleSpecs( + { + 1: spec1, + 2: spec2, + }, + { + prototypeDocument: { + security: [{ ShouldBeUsedSecurityRequirement: [] }], + components: { + securitySchemes: { + ShouldBeUsedSecurityRequirement: { + type: 'http', + scheme: 'basic', + }, + }, + }, + }, + } + ) + ); + + expect(bundledSpec.paths['/api/some_api']?.get?.security).toBeUndefined(); + expect(bundledSpec.paths['/api/another_api']?.get?.security).toBeUndefined(); + }); + }); + + describe('disabled', () => { + it('bundles root level security requirements', async () => { + const spec1Security = [{ SomeSecurityRequirement: [] }]; + const spec1SecuritySchemes = { + SomeSecurityRequirement: { + type: 'http', + scheme: 'basic', + }, + } as const; + const spec1 = createOASDocument({ + paths: { + '/api/some_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + security: spec1Security, + components: { + securitySchemes: spec1SecuritySchemes, + }, + }); + const spec2Security: OpenAPIV3.SecurityRequirementObject[] = [ + { AnotherSecurityRequirement: [] }, + { AdditionalSecurityRequirement: [] }, + ]; + const spec2SecuritySchemes = { + AnotherSecurityRequirement: { + type: 'http', + scheme: 'basic', + }, + AdditionalSecurityRequirement: { + type: 'apiKey', + name: 'apiKey', + in: 'header', + }, + } as const; + const spec2 = createOASDocument({ + paths: { + '/api/another_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + security: spec2Security, + components: { + securitySchemes: spec2SecuritySchemes, + }, + }); + + const [bundledSpec] = Object.values( + await bundleSpecs({ + 1: spec1, + 2: spec2, + }) + ); + + expect(bundledSpec.security).toEqual( + expect.arrayContaining([...spec1Security, ...spec2Security]) + ); + expect(bundledSpec.components?.securitySchemes).toMatchObject({ + ...spec1SecuritySchemes, + ...spec2SecuritySchemes, + }); + }); + + it('bundles operation level security requirements', async () => { + const spec1Security = [{ SomeSecurityRequirement: [] }]; + const spec1SecuritySchemes = { + SomeSecurityRequirement: { + type: 'http', + scheme: 'basic', + }, + } as const; + const spec1 = createOASDocument({ + paths: { + '/api/some_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + security: spec1Security, + }, + }, + }, + components: { + securitySchemes: spec1SecuritySchemes, + }, + }); + const spec2Security: OpenAPIV3.SecurityRequirementObject[] = [ + { AnotherSecurityRequirement: [] }, + { AdditionalSecurityRequirement: [] }, + ]; + const spec2SecuritySchemes = { + AnotherSecurityRequirement: { + type: 'http', + scheme: 'basic', + }, + AdditionalSecurityRequirement: { + type: 'apiKey', + name: 'apiKey', + in: 'header', + }, + } as const; + const spec2 = createOASDocument({ + paths: { + '/api/another_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + security: spec2Security, + }, + }, + }, + components: { + securitySchemes: spec2SecuritySchemes, + }, + }); + + const [bundledSpec] = Object.values( + await bundleSpecs({ + 1: spec1, + 2: spec2, + }) + ); + + expect(bundledSpec.paths['/api/some_api']?.get?.security).toEqual(spec1Security); + expect(bundledSpec.paths['/api/another_api']?.get?.security).toEqual(spec2Security); + expect(bundledSpec.components?.securitySchemes).toMatchObject({ + ...spec1SecuritySchemes, + ...spec2SecuritySchemes, + }); + }); + }); +}); diff --git a/packages/kbn-openapi-bundler/tests/bundler/result_overrides/servers.test.ts b/packages/kbn-openapi-bundler/tests/bundler/result_overrides/servers.test.ts new file mode 100644 index 0000000000000..da22e2dcfc74c --- /dev/null +++ b/packages/kbn-openapi-bundler/tests/bundler/result_overrides/servers.test.ts @@ -0,0 +1,409 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { createOASDocument } from '../../create_oas_document'; +import { bundleSpecs } from '../bundle_specs'; + +describe('OpenAPI Bundler - with `servers` overrides', () => { + describe('enabled', () => { + it('overrides root level `servers`', async () => { + const spec1 = createOASDocument({ + paths: { + '/api/some_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + servers: [{ url: 'https://some-url' }], + }); + const spec2 = createOASDocument({ + paths: { + '/api/another_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + servers: [ + { url: 'https://another-url', description: 'some description' }, + { url: 'https://something-else-url', description: 'some description' }, + ], + }); + + const [bundledSpec] = Object.values( + await bundleSpecs( + { + 1: spec1, + 2: spec2, + }, + { + prototypeDocument: { + servers: [ + { url: 'https://should-be-used-url', description: 'Should be used description' }, + ], + }, + } + ) + ); + + expect(bundledSpec.servers).toEqual([ + { url: 'https://should-be-used-url', description: 'Should be used description' }, + ]); + }); + + it('drops path level `servers`', async () => { + const spec1 = createOASDocument({ + paths: { + '/api/some_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + servers: [{ url: 'https://some-url' }], + }, + }, + }); + const spec2 = createOASDocument({ + paths: { + '/api/another_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + servers: [ + { url: 'https://another-url', description: 'some description' }, + { url: 'https://something-else-url', description: 'some description' }, + ], + }, + }, + }); + + const [bundledSpec] = Object.values( + await bundleSpecs( + { + 1: spec1, + 2: spec2, + }, + { + prototypeDocument: { + servers: [ + { url: 'https://should-be-used-url', description: 'Should be used description' }, + ], + }, + } + ) + ); + + expect(bundledSpec.paths['/api/some_api']?.servers).toBeUndefined(); + expect(bundledSpec.paths['/api/another_api']?.servers).toBeUndefined(); + }); + + it('drops operation level `servers`', async () => { + const spec1 = createOASDocument({ + paths: { + '/api/some_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + servers: [{ url: 'https://some-url' }], + }, + }, + }, + }); + const spec2 = createOASDocument({ + paths: { + '/api/another_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + servers: [ + { url: 'https://another-url', description: 'some description' }, + { url: 'https://something-else-url', description: 'some description' }, + ], + }, + }, + }, + }); + + const [bundledSpec] = Object.values( + await bundleSpecs( + { + 1: spec1, + 2: spec2, + }, + { + prototypeDocument: { + servers: [ + { url: 'https://should-be-used-url', description: 'Should be used description' }, + ], + }, + } + ) + ); + + expect(bundledSpec.paths['/api/some_api']?.get?.servers).toBeUndefined(); + expect(bundledSpec.paths['/api/another_api']?.get?.servers).toBeUndefined(); + }); + }); + + describe('disabled', () => { + it('bundles root level `servers`', async () => { + const spec1Servers = [{ url: 'https://some-url' }]; + const spec1 = createOASDocument({ + paths: { + '/api/some_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + servers: spec1Servers, + }); + const spec2Servers = [ + { url: 'https://another-url', description: 'some description' }, + { url: 'https://something-else-url', description: 'some description' }, + ]; + const spec2 = createOASDocument({ + paths: { + '/api/another_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + servers: spec2Servers, + }); + + const [bundledSpec] = Object.values( + await bundleSpecs({ + 1: spec1, + 2: spec2, + }) + ); + + const DEFAULT_ENTRY = { + url: 'http://{kibana_host}:{port}', + variables: { + kibana_host: { + default: 'localhost', + }, + port: { + default: '5601', + }, + }, + }; + + expect(bundledSpec.servers).toEqual([DEFAULT_ENTRY, ...spec1Servers, ...spec2Servers]); + }); + + it('bundles path level `servers`', async () => { + const spec1Servers = [{ url: 'https://some-url' }]; + const spec1 = createOASDocument({ + paths: { + '/api/some_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + servers: spec1Servers, + }, + }, + }); + const spec2Servers = [ + { url: 'https://another-url', description: 'some description' }, + { url: 'https://something-else-url', description: 'some description' }, + ]; + const spec2 = createOASDocument({ + paths: { + '/api/another_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + servers: spec2Servers, + }, + }, + }); + + const [bundledSpec] = Object.values( + await bundleSpecs({ + 1: spec1, + 2: spec2, + }) + ); + + expect(bundledSpec.paths['/api/some_api']?.servers).toEqual(spec1Servers); + expect(bundledSpec.paths['/api/another_api']?.servers).toEqual(spec2Servers); + }); + + it('bundles operation level `servers`', async () => { + const spec1Servers = [{ url: 'https://some-url' }]; + const spec1 = createOASDocument({ + paths: { + '/api/some_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + servers: spec1Servers, + }, + }, + }, + }); + const spec2Servers = [ + { url: 'https://another-url', description: 'some description' }, + { url: 'https://something-else-url', description: 'some description' }, + ]; + const spec2 = createOASDocument({ + paths: { + '/api/another_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + servers: spec2Servers, + }, + }, + }, + }); + + const [bundledSpec] = Object.values( + await bundleSpecs({ + 1: spec1, + 2: spec2, + }) + ); + + expect(bundledSpec.paths['/api/some_api']?.get?.servers).toEqual(spec1Servers); + expect(bundledSpec.paths['/api/another_api']?.get?.servers).toEqual(spec2Servers); + }); + }); +}); diff --git a/packages/kbn-openapi-bundler/tests/merger/result_overrides/security.test.ts b/packages/kbn-openapi-bundler/tests/merger/result_overrides/security.test.ts new file mode 100644 index 0000000000000..f570416d48d75 --- /dev/null +++ b/packages/kbn-openapi-bundler/tests/merger/result_overrides/security.test.ts @@ -0,0 +1,415 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import chalk from 'chalk'; +import { createOASDocument } from '../../create_oas_document'; +import { mergeSpecs } from '../merge_specs'; + +// Disable naming convention check due to tests on spec title prefixes +// like Spec1_Something which violates that rule +/* eslint-disable @typescript-eslint/naming-convention */ + +describe('OpenAPI Merger - with security requirements overrides', () => { + describe('enabled', () => { + it('throws an error when security requirements are specified without components security schemes', async () => { + const spec1 = createOASDocument({ + paths: { + '/api/some_api': { + get: { + responses: {}, + }, + }, + }, + }); + + await expect( + mergeSpecs( + { + 1: spec1, + }, + { + prototypeDocument: { + security: [{ ShouldBeUsedSecurityRequirement: [] }], + }, + } + ) + ).rejects.toThrowError( + `Prototype document must contain ${chalk.bold( + 'components.securitySchemes' + )} when security requirements are specified` + ); + }); + + it('throws an error when components security schemes are specified without security requirements', async () => { + const spec1 = createOASDocument({ + paths: { + '/api/some_api': { + get: { + responses: {}, + }, + }, + }, + }); + + await expect( + mergeSpecs( + { + 1: spec1, + }, + { + prototypeDocument: { + components: { + securitySchemes: { + ShouldBeUsedSecurityRequirement: { + type: 'http', + scheme: 'basic', + }, + }, + }, + }, + } + ) + ).rejects.toThrowError( + `Prototype document must have ${chalk.bold('security')} defined ${chalk.bold( + 'components.securitySchemes' + )} are specified` + ); + }); + + it('overrides root level `security`', async () => { + const spec1 = createOASDocument({ + paths: { + '/api/some_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + security: [{ SomeSecurityRequirement: [] }], + }); + const spec2 = createOASDocument({ + paths: { + '/api/another_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + security: [{ AnotherSecurityRequirement: [] }, { AdditionalSecurityRequirement: [] }], + }); + + const [bundledSpec] = Object.values( + await mergeSpecs( + { + 1: spec1, + 2: spec2, + }, + { + prototypeDocument: { + security: [{ ShouldBeUsedSecurityRequirement: [] }], + components: { + securitySchemes: { + ShouldBeUsedSecurityRequirement: { + type: 'http', + scheme: 'basic', + }, + }, + }, + }, + } + ) + ); + + expect(bundledSpec.security).toEqual([{ ShouldBeUsedSecurityRequirement: [] }]); + expect(bundledSpec.components?.securitySchemes).toEqual({ + ShouldBeUsedSecurityRequirement: { + type: 'http', + scheme: 'basic', + }, + }); + }); + + it('drops operation level security requirements', async () => { + const spec1 = createOASDocument({ + paths: { + '/api/some_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + security: [{ SomeSecurityRequirement: [] }], + }, + }, + }, + }); + const spec2 = createOASDocument({ + paths: { + '/api/another_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + security: [{ AnotherSecurityRequirement: [] }, { AdditionalSecurityRequirement: [] }], + }, + }, + }, + }); + + const [bundledSpec] = Object.values( + await mergeSpecs( + { + 1: spec1, + 2: spec2, + }, + { + prototypeDocument: { + security: [{ ShouldBeUsedSecurityRequirement: [] }], + components: { + securitySchemes: { + ShouldBeUsedSecurityRequirement: { + type: 'http', + scheme: 'basic', + }, + }, + }, + }, + } + ) + ); + + expect(bundledSpec.paths['/api/some_api']?.get?.security).toBeUndefined(); + expect(bundledSpec.paths['/api/another_api']?.get?.security).toBeUndefined(); + }); + }); + + describe('disabled', () => { + it('bundles root level security requirements', async () => { + const spec1 = createOASDocument({ + info: { + title: 'Spec1', + }, + paths: { + '/api/some_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + security: [{ SomeSecurityRequirement: [] }], + components: { + securitySchemes: { + SomeSecurityRequirement: { + type: 'http', + scheme: 'basic', + }, + }, + }, + }); + const spec2 = createOASDocument({ + info: { + title: 'Spec2', + }, + paths: { + '/api/another_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + security: [{ AnotherSecurityRequirement: [] }, { AdditionalSecurityRequirement: [] }], + components: { + securitySchemes: { + AnotherSecurityRequirement: { + type: 'http', + scheme: 'basic', + }, + AdditionalSecurityRequirement: { + type: 'apiKey', + name: 'apiKey', + in: 'header', + }, + }, + }, + }); + + const [bundledSpec] = Object.values( + await mergeSpecs({ + 1: spec1, + 2: spec2, + }) + ); + + expect(bundledSpec.security).toEqual( + expect.arrayContaining([ + { Spec1_SomeSecurityRequirement: [] }, + { Spec2_AnotherSecurityRequirement: [] }, + { Spec2_AdditionalSecurityRequirement: [] }, + ]) + ); + expect(bundledSpec.components?.securitySchemes).toMatchObject({ + Spec1_SomeSecurityRequirement: expect.anything(), + Spec2_AnotherSecurityRequirement: expect.anything(), + Spec2_AdditionalSecurityRequirement: expect.anything(), + }); + }); + + it('bundles operation level security requirements', async () => { + const spec1 = createOASDocument({ + info: { + title: 'Spec1', + }, + paths: { + '/api/some_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + security: [{ SomeSecurityRequirement: [] }], + }, + }, + }, + components: { + securitySchemes: { + SomeSecurityRequirement: { + type: 'http', + scheme: 'basic', + }, + }, + }, + }); + const spec2 = createOASDocument({ + info: { + title: 'Spec2', + }, + paths: { + '/api/another_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + security: [{ AnotherSecurityRequirement: [] }, { AdditionalSecurityRequirement: [] }], + }, + }, + }, + components: { + securitySchemes: { + AnotherSecurityRequirement: { + type: 'http', + scheme: 'basic', + }, + AdditionalSecurityRequirement: { + type: 'apiKey', + name: 'apiKey', + in: 'header', + }, + }, + }, + }); + + const [bundledSpec] = Object.values( + await mergeSpecs({ + 1: spec1, + 2: spec2, + }) + ); + + expect(bundledSpec.paths['/api/some_api']?.get?.security).toEqual([ + { Spec1_SomeSecurityRequirement: [] }, + ]); + expect(bundledSpec.paths['/api/another_api']?.get?.security).toEqual([ + { Spec2_AnotherSecurityRequirement: [] }, + { Spec2_AdditionalSecurityRequirement: [] }, + ]); + expect(bundledSpec.components?.securitySchemes).toMatchObject({ + Spec1_SomeSecurityRequirement: expect.anything(), + Spec2_AnotherSecurityRequirement: expect.anything(), + Spec2_AdditionalSecurityRequirement: expect.anything(), + }); + }); + }); +}); diff --git a/packages/kbn-openapi-bundler/tests/merger/result_overrides/servers.test.ts b/packages/kbn-openapi-bundler/tests/merger/result_overrides/servers.test.ts new file mode 100644 index 0000000000000..0b2fe5a5ceb6c --- /dev/null +++ b/packages/kbn-openapi-bundler/tests/merger/result_overrides/servers.test.ts @@ -0,0 +1,409 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { createOASDocument } from '../../create_oas_document'; +import { mergeSpecs } from '../merge_specs'; + +describe('OpenAPI Merger - with `servers` overrides', () => { + describe('enabled', () => { + it('overrides root level `servers`', async () => { + const spec1 = createOASDocument({ + paths: { + '/api/some_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + servers: [{ url: 'https://some-url' }], + }); + const spec2 = createOASDocument({ + paths: { + '/api/another_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + servers: [ + { url: 'https://another-url', description: 'some description' }, + { url: 'https://something-else-url', description: 'some description' }, + ], + }); + + const [bundledSpec] = Object.values( + await mergeSpecs( + { + 1: spec1, + 2: spec2, + }, + { + prototypeDocument: { + servers: [ + { url: 'https://should-be-used-url', description: 'Should be used description' }, + ], + }, + } + ) + ); + + expect(bundledSpec.servers).toEqual([ + { url: 'https://should-be-used-url', description: 'Should be used description' }, + ]); + }); + + it('drops path level `servers`', async () => { + const spec1 = createOASDocument({ + paths: { + '/api/some_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + servers: [{ url: 'https://some-url' }], + }, + }, + }); + const spec2 = createOASDocument({ + paths: { + '/api/another_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + servers: [ + { url: 'https://another-url', description: 'some description' }, + { url: 'https://something-else-url', description: 'some description' }, + ], + }, + }, + }); + + const [bundledSpec] = Object.values( + await mergeSpecs( + { + 1: spec1, + 2: spec2, + }, + { + prototypeDocument: { + servers: [ + { url: 'https://should-be-used-url', description: 'Should be used description' }, + ], + }, + } + ) + ); + + expect(bundledSpec.paths['/api/some_api']?.servers).toBeUndefined(); + expect(bundledSpec.paths['/api/another_api']?.servers).toBeUndefined(); + }); + + it('drops operation level `servers`', async () => { + const spec1 = createOASDocument({ + paths: { + '/api/some_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + servers: [{ url: 'https://some-url' }], + }, + }, + }, + }); + const spec2 = createOASDocument({ + paths: { + '/api/another_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + servers: [ + { url: 'https://another-url', description: 'some description' }, + { url: 'https://something-else-url', description: 'some description' }, + ], + }, + }, + }, + }); + + const [bundledSpec] = Object.values( + await mergeSpecs( + { + 1: spec1, + 2: spec2, + }, + { + prototypeDocument: { + servers: [ + { url: 'https://should-be-used-url', description: 'Should be used description' }, + ], + }, + } + ) + ); + + expect(bundledSpec.paths['/api/some_api']?.get?.servers).toBeUndefined(); + expect(bundledSpec.paths['/api/another_api']?.get?.servers).toBeUndefined(); + }); + }); + + describe('disabled', () => { + it('bundles root level `servers`', async () => { + const spec1Servers = [{ url: 'https://some-url' }]; + const spec1 = createOASDocument({ + paths: { + '/api/some_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + servers: spec1Servers, + }); + const spec2Servers = [ + { url: 'https://another-url', description: 'some description' }, + { url: 'https://something-else-url', description: 'some description' }, + ]; + const spec2 = createOASDocument({ + paths: { + '/api/another_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + servers: spec2Servers, + }); + + const [bundledSpec] = Object.values( + await mergeSpecs({ + 1: spec1, + 2: spec2, + }) + ); + + const DEFAULT_ENTRY = { + url: 'http://{kibana_host}:{port}', + variables: { + kibana_host: { + default: 'localhost', + }, + port: { + default: '5601', + }, + }, + }; + + expect(bundledSpec.servers).toEqual([DEFAULT_ENTRY, ...spec1Servers, ...spec2Servers]); + }); + + it('bundles path level `servers`', async () => { + const spec1Servers = [{ url: 'https://some-url' }]; + const spec1 = createOASDocument({ + paths: { + '/api/some_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + servers: spec1Servers, + }, + }, + }); + const spec2Servers = [ + { url: 'https://another-url', description: 'some description' }, + { url: 'https://something-else-url', description: 'some description' }, + ]; + const spec2 = createOASDocument({ + paths: { + '/api/another_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + servers: spec2Servers, + }, + }, + }); + + const [bundledSpec] = Object.values( + await mergeSpecs({ + 1: spec1, + 2: spec2, + }) + ); + + expect(bundledSpec.paths['/api/some_api']?.servers).toEqual(spec1Servers); + expect(bundledSpec.paths['/api/another_api']?.servers).toEqual(spec2Servers); + }); + + it('bundles operation level `servers`', async () => { + const spec1Servers = [{ url: 'https://some-url' }]; + const spec1 = createOASDocument({ + paths: { + '/api/some_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + servers: spec1Servers, + }, + }, + }, + }); + const spec2Servers = [ + { url: 'https://another-url', description: 'some description' }, + { url: 'https://something-else-url', description: 'some description' }, + ]; + const spec2 = createOASDocument({ + paths: { + '/api/another_api': { + get: { + responses: { + '200': { + description: 'Successful response', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + servers: spec2Servers, + }, + }, + }, + }); + + const [bundledSpec] = Object.values( + await mergeSpecs({ + 1: spec1, + 2: spec2, + }) + ); + + expect(bundledSpec.paths['/api/some_api']?.get?.servers).toEqual(spec1Servers); + expect(bundledSpec.paths['/api/another_api']?.get?.servers).toEqual(spec2Servers); + }); + }); +}); diff --git a/packages/kbn-securitysolution-exceptions-common/scripts/openapi_bundle.js b/packages/kbn-securitysolution-exceptions-common/scripts/openapi_bundle.js index a3e82f172b05e..2ed569154bd4f 100644 --- a/packages/kbn-securitysolution-exceptions-common/scripts/openapi_bundle.js +++ b/packages/kbn-securitysolution-exceptions-common/scripts/openapi_bundle.js @@ -21,10 +21,12 @@ const ROOT = resolve(__dirname, '..'); ), options: { includeLabels: ['serverless'], - specInfo: { - title: 'Security Solution Exceptions API (Elastic Cloud Serverless)', - description: - "Exceptions API allows you to manage detection rule exceptions to prevent a rule from generating an alert from incoming events even when the rule's other criteria are met.", + prototypeDocument: { + info: { + title: 'Security Solution Exceptions API (Elastic Cloud Serverless)', + description: + "Exceptions API allows you to manage detection rule exceptions to prevent a rule from generating an alert from incoming events even when the rule's other criteria are met.", + }, }, }, }); @@ -37,10 +39,12 @@ const ROOT = resolve(__dirname, '..'); ), options: { includeLabels: ['ess'], - specInfo: { - title: 'Security Solution Exceptions API (Elastic Cloud and self-hosted)', - description: - "Exceptions API allows you to manage detection rule exceptions to prevent a rule from generating an alert from incoming events even when the rule's other criteria are met.", + prototypeDocument: { + info: { + title: 'Security Solution Exceptions API (Elastic Cloud and self-hosted)', + description: + "Exceptions API allows you to manage detection rule exceptions to prevent a rule from generating an alert from incoming events even when the rule's other criteria are met.", + }, }, }, }); diff --git a/packages/kbn-securitysolution-lists-common/scripts/openapi_bundle.js b/packages/kbn-securitysolution-lists-common/scripts/openapi_bundle.js index 9bed9a313882f..fccbf4cc34f64 100644 --- a/packages/kbn-securitysolution-lists-common/scripts/openapi_bundle.js +++ b/packages/kbn-securitysolution-lists-common/scripts/openapi_bundle.js @@ -21,9 +21,11 @@ const ROOT = resolve(__dirname, '..'); ), options: { includeLabels: ['serverless'], - specInfo: { - title: 'Security Solution Lists API (Elastic Cloud Serverless)', - description: 'Lists API allows you to manage lists of keywords, IPs or IP ranges items.', + prototypeDocument: { + info: { + title: 'Security Solution Lists API (Elastic Cloud Serverless)', + description: 'Lists API allows you to manage lists of keywords, IPs or IP ranges items.', + }, }, }, }); @@ -36,9 +38,11 @@ const ROOT = resolve(__dirname, '..'); ), options: { includeLabels: ['ess'], - specInfo: { - title: 'Security Solution Lists API (Elastic Cloud and self-hosted)', - description: 'Lists API allows you to manage lists of keywords, IPs or IP ranges items.', + prototypeDocument: { + info: { + title: 'Security Solution Lists API (Elastic Cloud and self-hosted)', + description: 'Lists API allows you to manage lists of keywords, IPs or IP ranges items.', + }, }, }, }); diff --git a/x-pack/packages/kbn-elastic-assistant-common/scripts/openapi/bundle.js b/x-pack/packages/kbn-elastic-assistant-common/scripts/openapi/bundle.js index eb45fe104ad48..49ff53134ebf9 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/scripts/openapi/bundle.js +++ b/x-pack/packages/kbn-elastic-assistant-common/scripts/openapi/bundle.js @@ -21,9 +21,11 @@ const ELASTIC_ASSISTANT_ROOT = resolve(__dirname, '../..'); ), options: { includeLabels: ['serverless'], - specInfo: { - title: 'Security AI Assistant API (Elastic Cloud Serverless)', - description: 'Manage and interact with Security Assistant resources.', + prototypeDocument: { + info: { + title: 'Security AI Assistant API (Elastic Cloud Serverless)', + description: 'Manage and interact with Security Assistant resources.', + }, }, }, }); @@ -36,9 +38,11 @@ const ELASTIC_ASSISTANT_ROOT = resolve(__dirname, '../..'); ), options: { includeLabels: ['ess'], - specInfo: { - title: 'Security AI Assistant API (Elastic Cloud & self-hosted)', - description: 'Manage and interact with Security Assistant resources.', + prototypeDocument: { + info: { + title: 'Security AI Assistant API (Elastic Cloud & self-hosted)', + description: 'Manage and interact with Security Assistant resources.', + }, }, }, }); diff --git a/x-pack/plugins/osquery/scripts/openapi/bundle.js b/x-pack/plugins/osquery/scripts/openapi/bundle.js index ac45505ae6f88..e68c7b4977154 100644 --- a/x-pack/plugins/osquery/scripts/openapi/bundle.js +++ b/x-pack/plugins/osquery/scripts/openapi/bundle.js @@ -20,9 +20,11 @@ const ELASTIC_ASSISTANT_ROOT = resolve(__dirname, '../..'); outputFilePath: 'docs/openapi/serverless/osquery_api_{version}.bundled.schema.yaml', options: { includeLabels: ['serverless'], - specInfo: { - title: 'Security Solution Osquery API (Elastic Cloud Serverless)', - description: 'Run live queries, manage packs and saved queries.', + prototypeDocument: { + info: { + title: 'Security Solution Osquery API (Elastic Cloud Serverless)', + description: 'Run live queries, manage packs and saved queries.', + }, }, }, }); @@ -33,9 +35,11 @@ const ELASTIC_ASSISTANT_ROOT = resolve(__dirname, '../..'); outputFilePath: 'docs/openapi/ess/osquery_api_{version}.bundled.schema.yaml', options: { includeLabels: ['ess'], - specInfo: { - title: 'Security Solution Osquery API (Elastic Cloud and self-hosted)', - description: 'Run live queries, manage packs and saved queries.', + prototypeDocument: { + info: { + title: 'Security Solution Osquery API (Elastic Cloud and self-hosted)', + description: 'Run live queries, manage packs and saved queries.', + }, }, }, }); diff --git a/x-pack/plugins/security_solution/scripts/openapi/bundle_detections.js b/x-pack/plugins/security_solution/scripts/openapi/bundle_detections.js index f79437c33222c..ff137076a74c4 100644 --- a/x-pack/plugins/security_solution/scripts/openapi/bundle_detections.js +++ b/x-pack/plugins/security_solution/scripts/openapi/bundle_detections.js @@ -20,10 +20,12 @@ const ROOT = resolve(__dirname, '../..'); ), options: { includeLabels: ['serverless'], - specInfo: { - title: 'Security Solution Detections API (Elastic Cloud Serverless)', - description: - 'You can create rules that automatically turn events and external alerts sent to Elastic Security into detection alerts. These alerts are displayed on the Detections page.', + prototypeDocument: { + info: { + title: 'Security Solution Detections API (Elastic Cloud Serverless)', + description: + 'You can create rules that automatically turn events and external alerts sent to Elastic Security into detection alerts. These alerts are displayed on the Detections page.', + }, }, }, }); @@ -36,10 +38,12 @@ const ROOT = resolve(__dirname, '../..'); ), options: { includeLabels: ['ess'], - specInfo: { - title: 'Security Solution Detections API (Elastic Cloud and self-hosted)', - description: - 'You can create rules that automatically turn events and external alerts sent to Elastic Security into detection alerts. These alerts are displayed on the Detections page.', + prototypeDocument: { + info: { + title: 'Security Solution Detections API (Elastic Cloud and self-hosted)', + description: + 'You can create rules that automatically turn events and external alerts sent to Elastic Security into detection alerts. These alerts are displayed on the Detections page.', + }, }, }, }); diff --git a/x-pack/plugins/security_solution/scripts/openapi/bundle_endpoint_management.js b/x-pack/plugins/security_solution/scripts/openapi/bundle_endpoint_management.js index d4d994b993057..130686ca5a690 100644 --- a/x-pack/plugins/security_solution/scripts/openapi/bundle_endpoint_management.js +++ b/x-pack/plugins/security_solution/scripts/openapi/bundle_endpoint_management.js @@ -20,9 +20,11 @@ const ROOT = resolve(__dirname, '../..'); ), options: { includeLabels: ['serverless'], - specInfo: { - title: 'Security Solution Endpoint Management API (Elastic Cloud Serverless)', - description: 'Interact with and manage endpoints running the Elastic Defend integration.', + prototypeDocument: { + info: { + title: 'Security Solution Endpoint Management API (Elastic Cloud Serverless)', + description: 'Interact with and manage endpoints running the Elastic Defend integration.', + }, }, }, }); @@ -35,9 +37,11 @@ const ROOT = resolve(__dirname, '../..'); ), options: { includeLabels: ['ess'], - specInfo: { - title: 'Security Solution Endpoint Management API (Elastic Cloud and self-hosted)', - description: 'Interact with and manage endpoints running the Elastic Defend integration.', + prototypeDocument: { + info: { + title: 'Security Solution Endpoint Management API (Elastic Cloud and self-hosted)', + description: 'Interact with and manage endpoints running the Elastic Defend integration.', + }, }, }, }); diff --git a/x-pack/plugins/security_solution/scripts/openapi/bundle_entity_analytics.js b/x-pack/plugins/security_solution/scripts/openapi/bundle_entity_analytics.js index 28131d418e09c..2e5413ce4a7d7 100644 --- a/x-pack/plugins/security_solution/scripts/openapi/bundle_entity_analytics.js +++ b/x-pack/plugins/security_solution/scripts/openapi/bundle_entity_analytics.js @@ -11,32 +11,38 @@ const { join, resolve } = require('path'); const ROOT = resolve(__dirname, '../..'); -bundle({ - sourceGlob: join(ROOT, 'common/api/entity_analytics/**/*.schema.yaml'), - outputFilePath: join( - ROOT, - 'docs/openapi/serverless/security_solution_entity_analytics_api_{version}.bundled.schema.yaml' - ), - options: { - includeLabels: ['serverless'], - specInfo: { - title: 'Security Solution Entity Analytics API (Elastic Cloud Serverless)', - description: '', +(async () => { + await bundle({ + sourceGlob: join(ROOT, 'common/api/entity_analytics/**/*.schema.yaml'), + outputFilePath: join( + ROOT, + 'docs/openapi/serverless/security_solution_entity_analytics_api_{version}.bundled.schema.yaml' + ), + options: { + includeLabels: ['serverless'], + prototypeDocument: { + info: { + title: 'Security Solution Entity Analytics API (Elastic Cloud Serverless)', + description: '', + }, + }, }, - }, -}); + }); -bundle({ - sourceGlob: join(ROOT, 'common/api/entity_analytics/**/*.schema.yaml'), - outputFilePath: join( - ROOT, - 'docs/openapi/ess/security_solution_entity_analytics_api_{version}.bundled.schema.yaml' - ), - options: { - includeLabels: ['ess'], - specInfo: { - title: 'Security Solution Entity Analytics API (Elastic Cloud and self-hosted)', - description: '', + await bundle({ + sourceGlob: join(ROOT, 'common/api/entity_analytics/**/*.schema.yaml'), + outputFilePath: join( + ROOT, + 'docs/openapi/ess/security_solution_entity_analytics_api_{version}.bundled.schema.yaml' + ), + options: { + includeLabels: ['ess'], + prototypeDocument: { + info: { + title: 'Security Solution Entity Analytics API (Elastic Cloud and self-hosted)', + description: '', + }, + }, }, - }, -}); + }); +})();