From 0de595af80b0b9bd34dd6d352f4fb3b540ec3d7c Mon Sep 17 00:00:00 2001 From: rentu <5545529+SLdragon@users.noreply.github.com> Date: Tue, 2 Apr 2024 17:12:22 +0800 Subject: [PATCH] perf(spec-parser): separate api validation logic (#11250) * perf(spec-parser): separate api validation logic * perf: remove unused code, add missing test file * perf: remove unused import --------- Co-authored-by: turenlong --- packages/spec-parser/src/specFilter.ts | 5 +- .../spec-parser/src/specParser.browser.ts | 56 +- packages/spec-parser/src/specParser.ts | 35 +- packages/spec-parser/src/utils.ts | 355 +- .../src/validators/copilotValidator.ts | 80 + .../src/validators/smeValidator.ts | 116 + .../src/validators/teamsAIValidator.ts | 44 + .../spec-parser/src/validators/validator.ts | 259 ++ .../src/validators/validatorFactory.ts | 23 + .../test/browser/specParser.browser.test.ts | 4 +- packages/spec-parser/test/specFilter.test.ts | 10 +- packages/spec-parser/test/utils.test.ts | 2919 ----------------- packages/spec-parser/test/validator.test.ts | 2742 ++++++++++++++++ 13 files changed, 3351 insertions(+), 3297 deletions(-) create mode 100644 packages/spec-parser/src/validators/copilotValidator.ts create mode 100644 packages/spec-parser/src/validators/smeValidator.ts create mode 100644 packages/spec-parser/src/validators/teamsAIValidator.ts create mode 100644 packages/spec-parser/src/validators/validator.ts create mode 100644 packages/spec-parser/src/validators/validatorFactory.ts create mode 100644 packages/spec-parser/test/validator.test.ts diff --git a/packages/spec-parser/src/specFilter.ts b/packages/spec-parser/src/specFilter.ts index 19f46620af..ce9e095007 100644 --- a/packages/spec-parser/src/specFilter.ts +++ b/packages/spec-parser/src/specFilter.ts @@ -7,6 +7,7 @@ import { Utils } from "./utils"; import { SpecParserError } from "./specParserError"; import { ErrorType, ParseOptions } from "./interfaces"; import { ConstantString } from "./constants"; +import { ValidatorFactory } from "./validators/validatorFactory"; export class SpecFilter { static specFilter( @@ -28,7 +29,9 @@ export class SpecFilter { pathObj && pathObj[methodName] ) { - const validateResult = Utils.isSupportedApi(methodName, path, resolvedSpec, options); + const validator = ValidatorFactory.create(resolvedSpec, options); + const validateResult = validator.validateAPI(methodName, path); + if (!validateResult.isValid) { continue; } diff --git a/packages/spec-parser/src/specParser.browser.ts b/packages/spec-parser/src/specParser.browser.ts index 9c4790e24c..56422642b4 100644 --- a/packages/spec-parser/src/specParser.browser.ts +++ b/packages/spec-parser/src/specParser.browser.ts @@ -13,10 +13,12 @@ import { ValidationStatus, ListAPIResult, ProjectType, + APIMap, } from "./interfaces"; import { SpecParserError } from "./specParserError"; import { Utils } from "./utils"; import { ConstantString } from "./constants"; +import { ValidatorFactory } from "./validators/validatorFactory"; /** * A class that parses an OpenAPI specification file and provides methods to validate, list, and generate artifacts. @@ -26,7 +28,7 @@ export class SpecParser { public readonly parser: SwaggerParser; public readonly options: Required; - private apiMap: { [key: string]: OpenAPIV3.PathItemObject } | undefined; + private apiMap: APIMap | undefined; private spec: OpenAPIV3.Document | undefined; private unResolveSpec: OpenAPIV3.Document | undefined; private isSwaggerFile: boolean | undefined; @@ -87,8 +89,15 @@ export class SpecParser { ], }; } - - return Utils.validateSpec(this.spec!, this.parser, !!this.isSwaggerFile, this.options); + const apiMap = this.getAPIs(this.spec!); + + return Utils.validateSpec( + this.spec!, + this.parser, + apiMap, + !!this.isSwaggerFile, + this.options + ); } catch (err) { throw new SpecParserError((err as Error).toString(), ErrorType.ValidateFailed); } @@ -97,19 +106,24 @@ export class SpecParser { async listSupportedAPIInfo(): Promise { try { await this.loadSpec(); - const apiMap = this.getAllSupportedAPIs(this.spec!); + const apiMap = this.getAPIs(this.spec!); const apiInfos: APIInfo[] = []; for (const key in apiMap) { - const pathObjectItem = apiMap[key]; + const { operation, isValid } = apiMap[key]; + + if (!isValid) { + continue; + } + const [method, path] = key.split(" "); - const operationId = pathObjectItem.operationId; + const operationId = operation.operationId; // In Browser environment, this api is by default not support api without operationId if (!operationId) { continue; } - const command = Utils.parseApiInfo(pathObjectItem, this.options); + const command = Utils.parseApiInfo(operation, this.options); const apiInfo: APIInfo = { method: method, @@ -199,34 +213,30 @@ export class SpecParser { } } - private getAllSupportedAPIs(spec: OpenAPIV3.Document): { - [key: string]: OpenAPIV3.OperationObject; - } { + private getAPIs(spec: OpenAPIV3.Document): APIMap { if (this.apiMap !== undefined) { return this.apiMap; } - const result = this.listSupportedAPIs(spec, this.options); + const result = this.listAPIs(spec); this.apiMap = result; return result; } - private listSupportedAPIs( - spec: OpenAPIV3.Document, - options: ParseOptions - ): { - [key: string]: OpenAPIV3.OperationObject; - } { + private listAPIs(spec: OpenAPIV3.Document): APIMap { const paths = spec.paths; - const result: { [key: string]: OpenAPIV3.OperationObject } = {}; + const result: APIMap = {}; for (const path in paths) { const methods = paths[path]; for (const method in methods) { const operationObject = (methods as any)[method] as OpenAPIV3.OperationObject; - if (options.allowMethods?.includes(method) && operationObject) { - const validateResult = Utils.isSupportedApi(method, path, spec, options); - if (validateResult.isValid) { - result[`${method.toUpperCase()} ${path}`] = operationObject; - } + if (this.options.allowMethods?.includes(method) && operationObject) { + const validator = ValidatorFactory.create(spec, this.options); + const validateResult = validator.validateAPI(method, path); + result[`${method.toUpperCase()} ${path}`] = { + operation: operationObject, + isValid: validateResult.isValid, + reason: validateResult.reason, + }; } } } diff --git a/packages/spec-parser/src/specParser.ts b/packages/spec-parser/src/specParser.ts index e146798f27..df6d7ef698 100644 --- a/packages/spec-parser/src/specParser.ts +++ b/packages/spec-parser/src/specParser.ts @@ -20,6 +20,7 @@ import { ProjectType, ValidateResult, ValidationStatus, + WarningResult, WarningType, } from "./interfaces"; import { ConstantString } from "./constants"; @@ -29,6 +30,7 @@ import { Utils } from "./utils"; import { ManifestUpdater } from "./manifestUpdater"; import { AdaptiveCardGenerator } from "./adaptiveCardGenerator"; import { wrapAdaptiveCard } from "./adaptiveCardWrapper"; +import { ValidatorFactory } from "./validators/validatorFactory"; /** * A class that parses an OpenAPI specification file and provides methods to validate, list, and generate artifacts. @@ -115,7 +117,15 @@ export class SpecParser { } } - return Utils.validateSpec(this.spec!, this.parser, !!this.isSwaggerFile, this.options); + const apiMap = this.getAPIs(this.spec!); + + return Utils.validateSpec( + this.spec!, + this.parser, + apiMap, + !!this.isSwaggerFile, + this.options + ); } catch (err) { throw new SpecParserError((err as Error).toString(), ErrorType.ValidateFailed); } @@ -426,8 +436,29 @@ export class SpecParser { if (this.apiMap !== undefined) { return this.apiMap; } - const result = Utils.listAPIs(spec, this.options); + const result = this.listAPIs(spec, this.options); this.apiMap = result; return result; } + + private listAPIs(spec: OpenAPIV3.Document, options: ParseOptions): APIMap { + const paths = spec.paths; + const result: APIMap = {}; + for (const path in paths) { + const methods = paths[path]; + for (const method in methods) { + const operationObject = (methods as any)[method] as OpenAPIV3.OperationObject; + if (options.allowMethods?.includes(method) && operationObject) { + const validator = ValidatorFactory.create(spec, options); + const validateResult = validator.validateAPI(method, path); + result[`${method.toUpperCase()} ${path}`] = { + operation: operationObject, + isValid: validateResult.isValid, + reason: validateResult.reason, + }; + } + } + } + return result; + } } diff --git a/packages/spec-parser/src/utils.ts b/packages/spec-parser/src/utils.ts index 6c7a2a4f3f..9293a9fcaf 100644 --- a/packages/spec-parser/src/utils.ts +++ b/packages/spec-parser/src/utils.ts @@ -7,13 +7,10 @@ import SwaggerParser from "@apidevtools/swagger-parser"; import { ConstantString } from "./constants"; import { APIMap, - APIValidationResult, AuthInfo, - CheckParamResult, ErrorResult, ErrorType, ParseOptions, - ProjectType, ValidateResult, ValidationStatus, WarningResult, @@ -34,320 +31,12 @@ export class Utils { return false; } - static checkParameters( - paramObject: OpenAPIV3.ParameterObject[], - isCopilot: boolean - ): CheckParamResult { - const paramResult: CheckParamResult = { - requiredNum: 0, - optionalNum: 0, - isValid: true, - reason: [], - }; - - if (!paramObject) { - return paramResult; - } - - for (let i = 0; i < paramObject.length; i++) { - const param = paramObject[i]; - const schema = param.schema as OpenAPIV3.SchemaObject; - - if (isCopilot && this.hasNestedObjectInSchema(schema)) { - paramResult.isValid = false; - paramResult.reason.push(ErrorType.ParamsContainsNestedObject); - continue; - } - - const isRequiredWithoutDefault = param.required && schema.default === undefined; - - if (isCopilot) { - if (isRequiredWithoutDefault) { - paramResult.requiredNum = paramResult.requiredNum + 1; - } else { - paramResult.optionalNum = paramResult.optionalNum + 1; - } - continue; - } - - if (param.in === "header" || param.in === "cookie") { - if (isRequiredWithoutDefault) { - paramResult.isValid = false; - paramResult.reason.push(ErrorType.ParamsContainRequiredUnsupportedSchema); - } - continue; - } - - if ( - schema.type !== "boolean" && - schema.type !== "string" && - schema.type !== "number" && - schema.type !== "integer" - ) { - if (isRequiredWithoutDefault) { - paramResult.isValid = false; - paramResult.reason.push(ErrorType.ParamsContainRequiredUnsupportedSchema); - } - continue; - } - - if (param.in === "query" || param.in === "path") { - if (isRequiredWithoutDefault) { - paramResult.requiredNum = paramResult.requiredNum + 1; - } else { - paramResult.optionalNum = paramResult.optionalNum + 1; - } - } - } - - return paramResult; - } - - static checkPostBody( - schema: OpenAPIV3.SchemaObject, - isRequired = false, - isCopilot = false - ): CheckParamResult { - const paramResult: CheckParamResult = { - requiredNum: 0, - optionalNum: 0, - isValid: true, - reason: [], - }; - - if (Object.keys(schema).length === 0) { - return paramResult; - } - - const isRequiredWithoutDefault = isRequired && schema.default === undefined; - - if (isCopilot && this.hasNestedObjectInSchema(schema)) { - paramResult.isValid = false; - paramResult.reason = [ErrorType.RequestBodyContainsNestedObject]; - return paramResult; - } - - if ( - schema.type === "string" || - schema.type === "integer" || - schema.type === "boolean" || - schema.type === "number" - ) { - if (isRequiredWithoutDefault) { - paramResult.requiredNum = paramResult.requiredNum + 1; - } else { - paramResult.optionalNum = paramResult.optionalNum + 1; - } - } else if (schema.type === "object") { - const { properties } = schema; - for (const property in properties) { - let isRequired = false; - if (schema.required && schema.required?.indexOf(property) >= 0) { - isRequired = true; - } - const result = Utils.checkPostBody( - properties[property] as OpenAPIV3.SchemaObject, - isRequired, - isCopilot - ); - paramResult.requiredNum += result.requiredNum; - paramResult.optionalNum += result.optionalNum; - paramResult.isValid = paramResult.isValid && result.isValid; - paramResult.reason.push(...result.reason); - } - } else { - if (isRequiredWithoutDefault && !isCopilot) { - paramResult.isValid = false; - paramResult.reason.push(ErrorType.PostBodyContainsRequiredUnsupportedSchema); - } - } - return paramResult; - } - static containMultipleMediaTypes( bodyObject: OpenAPIV3.RequestBodyObject | OpenAPIV3.ResponseObject ): boolean { return Object.keys(bodyObject?.content || {}).length > 1; } - /** - * Checks if the given API is supported. - * @param {string} method - The HTTP method of the API. - * @param {string} path - The path of the API. - * @param {OpenAPIV3.Document} spec - The OpenAPI specification document. - * @returns {boolean} - Returns true if the API is supported, false otherwise. - * @description The following APIs are supported: - * 1. only support Get/Post operation without auth property - * 2. parameter inside query or path only support string, number, boolean and integer - * 3. parameter inside post body only support string, number, boolean, integer and object - * 4. request body + required parameters <= 1 - * 5. response body should be “application/json” and not empty, and response code should be 20X - * 6. only support request body with “application/json” content type - */ - static isSupportedApi( - method: string, - path: string, - spec: OpenAPIV3.Document, - options: ParseOptions - ): APIValidationResult { - const result: APIValidationResult = { isValid: true, reason: [] }; - method = method.toLocaleLowerCase(); - - if (options.allowMethods && !options.allowMethods.includes(method)) { - result.isValid = false; - result.reason.push(ErrorType.MethodNotAllowed); - return result; - } - - const pathObj = spec.paths[path] as any; - - if (!pathObj || !pathObj[method]) { - result.isValid = false; - result.reason.push(ErrorType.UrlPathNotExist); - return result; - } - - const securities = pathObj[method].security; - - const isTeamsAi = options.projectType === ProjectType.TeamsAi; - const isCopilot = options.projectType === ProjectType.Copilot; - - // Teams AI project doesn't care about auth, it will use authProvider for user to implement - if (!isTeamsAi) { - const authArray = Utils.getAuthArray(securities, spec); - - const authCheckResult = Utils.isSupportedAuth(authArray, options); - if (!authCheckResult.isValid) { - result.reason.push(...authCheckResult.reason); - } - } - - const operationObject = pathObj[method] as OpenAPIV3.OperationObject; - if (!options.allowMissingId && !operationObject.operationId) { - result.reason.push(ErrorType.MissingOperationId); - } - - const rootServer = spec.servers && spec.servers[0]; - const methodServer = spec.paths[path]!.servers && spec.paths[path]?.servers![0]; - const operationServer = operationObject.servers && operationObject.servers[0]; - - const serverUrl = operationServer || methodServer || rootServer; - if (!serverUrl) { - result.reason.push(ErrorType.NoServerInformation); - } else { - const serverValidateResult = Utils.checkServerUrl([serverUrl]); - result.reason.push(...serverValidateResult.map((item) => item.type)); - } - - const paramObject = operationObject.parameters as OpenAPIV3.ParameterObject[]; - - const requestBody = operationObject.requestBody as OpenAPIV3.RequestBodyObject; - const requestJsonBody = requestBody?.content["application/json"]; - - if (!isTeamsAi && Utils.containMultipleMediaTypes(requestBody)) { - result.reason.push(ErrorType.PostBodyContainMultipleMediaTypes); - } - - const { json, multipleMediaType } = Utils.getResponseJson(operationObject, isTeamsAi); - - if (multipleMediaType && !isTeamsAi) { - result.reason.push(ErrorType.ResponseContainMultipleMediaTypes); - } else if (Object.keys(json).length === 0) { - result.reason.push(ErrorType.ResponseJsonIsEmpty); - } - - // Teams AI project doesn't care about request parameters/body - if (!isTeamsAi) { - let requestBodyParamResult: CheckParamResult = { - requiredNum: 0, - optionalNum: 0, - isValid: true, - reason: [], - }; - - if (requestJsonBody) { - const requestBodySchema = requestJsonBody.schema as OpenAPIV3.SchemaObject; - - if (isCopilot && requestBodySchema.type !== "object") { - result.reason.push(ErrorType.PostBodySchemaIsNotJson); - } - - requestBodyParamResult = Utils.checkPostBody( - requestBodySchema, - requestBody.required, - isCopilot - ); - - if (!requestBodyParamResult.isValid && requestBodyParamResult.reason) { - result.reason.push(...requestBodyParamResult.reason); - } - } - - const paramResult = Utils.checkParameters(paramObject, isCopilot); - - if (!paramResult.isValid && paramResult.reason) { - result.reason.push(...paramResult.reason); - } - - // Copilot support arbitrary parameters - if (!isCopilot && paramResult.isValid && requestBodyParamResult.isValid) { - const totalRequiredParams = requestBodyParamResult.requiredNum + paramResult.requiredNum; - const totalParams = - totalRequiredParams + requestBodyParamResult.optionalNum + paramResult.optionalNum; - - if (totalRequiredParams > 1) { - if ( - !options.allowMultipleParameters || - totalRequiredParams > ConstantString.SMERequiredParamsMaxNum - ) { - result.reason.push(ErrorType.ExceededRequiredParamsLimit); - } - } else if (totalParams === 0) { - result.reason.push(ErrorType.NoParameter); - } - } - } - - if (result.reason.length > 0) { - result.isValid = false; - } - - return result; - } - - static isSupportedAuth( - authSchemeArray: AuthInfo[][], - options: ParseOptions - ): APIValidationResult { - if (authSchemeArray.length === 0) { - return { isValid: true, reason: [] }; - } - - if (options.allowAPIKeyAuth || options.allowOauth2 || options.allowBearerTokenAuth) { - // Currently we don't support multiple auth in one operation - if (authSchemeArray.length > 0 && authSchemeArray.every((auths) => auths.length > 1)) { - return { - isValid: false, - reason: [ErrorType.MultipleAuthNotSupported], - }; - } - - for (const auths of authSchemeArray) { - if (auths.length === 1) { - if ( - (options.allowAPIKeyAuth && Utils.isAPIKeyAuth(auths[0].authScheme)) || - (options.allowOauth2 && Utils.isOAuthWithAuthCodeFlow(auths[0].authScheme)) || - (options.allowBearerTokenAuth && Utils.isBearerTokenAuth(auths[0].authScheme)) - ) { - return { isValid: true, reason: [] }; - } - } - } - } - - return { isValid: false, reason: [ErrorType.AuthTypeIsNotSupported] }; - } - static isBearerTokenAuth(authScheme: OpenAPIV3.SecuritySchemeObject): boolean { return authScheme.type === "http" && authScheme.scheme === "bearer"; } @@ -357,11 +46,11 @@ export class Utils { } static isOAuthWithAuthCodeFlow(authScheme: OpenAPIV3.SecuritySchemeObject): boolean { - if (authScheme.type === "oauth2" && authScheme.flows && authScheme.flows.authorizationCode) { - return true; - } - - return false; + return !!( + authScheme.type === "oauth2" && + authScheme.flows && + authScheme.flows.authorizationCode + ); } static getAuthArray( @@ -398,10 +87,10 @@ export class Utils { return str.charAt(0).toUpperCase() + str.slice(1); } - static getResponseJson( - operationObject: OpenAPIV3.OperationObject | undefined, - isTeamsAiProject = false - ): { json: OpenAPIV3.MediaTypeObject; multipleMediaType: boolean } { + static getResponseJson(operationObject: OpenAPIV3.OperationObject | undefined): { + json: OpenAPIV3.MediaTypeObject; + multipleMediaType: boolean; + } { let json: OpenAPIV3.MediaTypeObject = {}; let multipleMediaType = false; @@ -413,10 +102,6 @@ export class Utils { json = responseObject.content["application/json"]; if (Utils.containMultipleMediaTypes(responseObject)) { multipleMediaType = true; - - if (isTeamsAiProject) { - break; - } json = {}; } else { break; @@ -705,35 +390,15 @@ export class Utils { return command; } - static listAPIs(spec: OpenAPIV3.Document, options: ParseOptions): APIMap { - const paths = spec.paths; - const result: APIMap = {}; - for (const path in paths) { - const methods = paths[path]; - for (const method in methods) { - const operationObject = (methods as any)[method] as OpenAPIV3.OperationObject; - if (options.allowMethods?.includes(method) && operationObject) { - const validateResult = Utils.isSupportedApi(method, path, spec, options); - result[`${method.toUpperCase()} ${path}`] = { - operation: operationObject, - isValid: validateResult.isValid, - reason: validateResult.reason, - }; - } - } - } - return result; - } - static validateSpec( spec: OpenAPIV3.Document, parser: SwaggerParser, + apiMap: APIMap, isSwaggerFile: boolean, options: ParseOptions ): ValidateResult { const errors: ErrorResult[] = []; const warnings: WarningResult[] = []; - const apiMap = Utils.listAPIs(spec, options); if (isSwaggerFile) { warnings.push({ diff --git a/packages/spec-parser/src/validators/copilotValidator.ts b/packages/spec-parser/src/validators/copilotValidator.ts new file mode 100644 index 0000000000..59ff1ddcd9 --- /dev/null +++ b/packages/spec-parser/src/validators/copilotValidator.ts @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +"use strict"; + +import { OpenAPIV3 } from "openapi-types"; +import { ParseOptions, APIValidationResult, ErrorType, ProjectType } from "../interfaces"; +import { Validator } from "./validator"; +import { Utils } from "../utils"; + +export class CopilotValidator extends Validator { + constructor(spec: OpenAPIV3.Document, options: ParseOptions) { + super(); + this.projectType = ProjectType.Copilot; + this.options = options; + this.spec = spec; + } + + validateAPI(method: string, path: string): APIValidationResult { + const result: APIValidationResult = { isValid: true, reason: [] }; + method = method.toLocaleLowerCase(); + + // validate method and path + const methodAndPathResult = this.validateMethodAndPath(method, path); + if (!methodAndPathResult.isValid) { + return methodAndPathResult; + } + + const operationObject = (this.spec.paths[path] as any)[method] as OpenAPIV3.OperationObject; + + // validate auth + const authCheckResult = this.validateAuth(method, path); + result.reason.push(...authCheckResult.reason); + + // validate operationId + if (!this.options.allowMissingId && !operationObject.operationId) { + result.reason.push(ErrorType.MissingOperationId); + } + + // validate server + const validateServerResult = this.validateServer(method, path); + result.reason.push(...validateServerResult.reason); + + // validate response + const validateResponseResult = this.validateResponse(method, path); + result.reason.push(...validateResponseResult.reason); + + // validate requestBody + const requestBody = operationObject.requestBody as OpenAPIV3.RequestBodyObject; + const requestJsonBody = requestBody?.content["application/json"]; + + if (Utils.containMultipleMediaTypes(requestBody)) { + result.reason.push(ErrorType.PostBodyContainMultipleMediaTypes); + } + + if (requestJsonBody) { + const requestBodySchema = requestJsonBody.schema as OpenAPIV3.SchemaObject; + + if (requestBodySchema.type !== "object") { + result.reason.push(ErrorType.PostBodySchemaIsNotJson); + } + + const requestBodyParamResult = this.checkPostBodySchema( + requestBodySchema, + requestBody.required + ); + result.reason.push(...requestBodyParamResult.reason); + } + + // validate parameters + const paramObject = operationObject.parameters as OpenAPIV3.ParameterObject[]; + const paramResult = this.checkParamSchema(paramObject); + result.reason.push(...paramResult.reason); + + if (result.reason.length > 0) { + result.isValid = false; + } + + return result; + } +} diff --git a/packages/spec-parser/src/validators/smeValidator.ts b/packages/spec-parser/src/validators/smeValidator.ts new file mode 100644 index 0000000000..000776b84d --- /dev/null +++ b/packages/spec-parser/src/validators/smeValidator.ts @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +"use strict"; + +import { OpenAPIV3 } from "openapi-types"; +import { + ParseOptions, + APIValidationResult, + ErrorType, + ProjectType, + CheckParamResult, +} from "../interfaces"; +import { Validator } from "./validator"; +import { Utils } from "../utils"; + +export class SMEValidator extends Validator { + private static readonly SMERequiredParamsMaxNum = 5; + + constructor(spec: OpenAPIV3.Document, options: ParseOptions) { + super(); + this.projectType = ProjectType.SME; + this.options = options; + this.spec = spec; + } + + validateAPI(method: string, path: string): APIValidationResult { + const result: APIValidationResult = { isValid: true, reason: [] }; + method = method.toLocaleLowerCase(); + + // validate method and path + const methodAndPathResult = this.validateMethodAndPath(method, path); + if (!methodAndPathResult.isValid) { + return methodAndPathResult; + } + + const operationObject = (this.spec.paths[path] as any)[method] as OpenAPIV3.OperationObject; + + // validate auth + const authCheckResult = this.validateAuth(method, path); + result.reason.push(...authCheckResult.reason); + + // validate operationId + if (!this.options.allowMissingId && !operationObject.operationId) { + result.reason.push(ErrorType.MissingOperationId); + } + + // validate server + const validateServerResult = this.validateServer(method, path); + result.reason.push(...validateServerResult.reason); + + // validate response + const validateResponseResult = this.validateResponse(method, path); + result.reason.push(...validateResponseResult.reason); + + let postBodyResult: CheckParamResult = { + requiredNum: 0, + optionalNum: 0, + isValid: true, + reason: [], + }; + + // validate requestBody + const requestBody = operationObject.requestBody as OpenAPIV3.RequestBodyObject; + const requestJsonBody = requestBody?.content["application/json"]; + + if (Utils.containMultipleMediaTypes(requestBody)) { + result.reason.push(ErrorType.PostBodyContainMultipleMediaTypes); + } + + if (requestJsonBody) { + const requestBodySchema = requestJsonBody.schema as OpenAPIV3.SchemaObject; + + postBodyResult = this.checkPostBodySchema(requestBodySchema, requestBody.required); + result.reason.push(...postBodyResult.reason); + } + + // validate parameters + const paramObject = operationObject.parameters as OpenAPIV3.ParameterObject[]; + const paramResult = this.checkParamSchema(paramObject); + result.reason.push(...paramResult.reason); + + // validate total parameters count + if (paramResult.isValid && postBodyResult.isValid) { + const paramCountResult = this.validateParamCount(postBodyResult, paramResult); + result.reason.push(...paramCountResult.reason); + } + + if (result.reason.length > 0) { + result.isValid = false; + } + + return result; + } + + private validateParamCount( + postBodyResult: CheckParamResult, + paramResult: CheckParamResult + ): APIValidationResult { + const result: APIValidationResult = { isValid: true, reason: [] }; + const totalRequiredParams = postBodyResult.requiredNum + paramResult.requiredNum; + const totalParams = totalRequiredParams + postBodyResult.optionalNum + paramResult.optionalNum; + + if (totalRequiredParams > 1) { + if ( + !this.options.allowMultipleParameters || + totalRequiredParams > SMEValidator.SMERequiredParamsMaxNum + ) { + result.reason.push(ErrorType.ExceededRequiredParamsLimit); + } + } else if (totalParams === 0) { + result.reason.push(ErrorType.NoParameter); + } + + return result; + } +} diff --git a/packages/spec-parser/src/validators/teamsAIValidator.ts b/packages/spec-parser/src/validators/teamsAIValidator.ts new file mode 100644 index 0000000000..fb792cba24 --- /dev/null +++ b/packages/spec-parser/src/validators/teamsAIValidator.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +"use strict"; + +import { OpenAPIV3 } from "openapi-types"; +import { ParseOptions, APIValidationResult, ErrorType, ProjectType } from "../interfaces"; +import { Validator } from "./validator"; + +export class TeamsAIValidator extends Validator { + constructor(spec: OpenAPIV3.Document, options: ParseOptions) { + super(); + this.projectType = ProjectType.TeamsAi; + this.options = options; + this.spec = spec; + } + + validateAPI(method: string, path: string): APIValidationResult { + const result: APIValidationResult = { isValid: true, reason: [] }; + method = method.toLocaleLowerCase(); + + // validate method and path + const methodAndPathResult = this.validateMethodAndPath(method, path); + if (!methodAndPathResult.isValid) { + return methodAndPathResult; + } + + const operationObject = (this.spec.paths[path] as any)[method] as OpenAPIV3.OperationObject; + + // validate operationId + if (!this.options.allowMissingId && !operationObject.operationId) { + result.reason.push(ErrorType.MissingOperationId); + } + + // validate server + const validateServerResult = this.validateServer(method, path); + result.reason.push(...validateServerResult.reason); + + if (result.reason.length > 0) { + result.isValid = false; + } + + return result; + } +} diff --git a/packages/spec-parser/src/validators/validator.ts b/packages/spec-parser/src/validators/validator.ts new file mode 100644 index 0000000000..b8a8f6a1fd --- /dev/null +++ b/packages/spec-parser/src/validators/validator.ts @@ -0,0 +1,259 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +"use strict"; + +import { OpenAPIV3 } from "openapi-types"; +import { + ParseOptions, + APIValidationResult, + ErrorType, + CheckParamResult, + ProjectType, +} from "../interfaces"; +import { Utils } from "../utils"; + +export abstract class Validator { + projectType!: ProjectType; + spec!: OpenAPIV3.Document; + options!: ParseOptions; + + abstract validateAPI(method: string, path: string): APIValidationResult; + + validateMethodAndPath(method: string, path: string): APIValidationResult { + const result: APIValidationResult = { isValid: true, reason: [] }; + + if (this.options.allowMethods && !this.options.allowMethods.includes(method)) { + result.isValid = false; + result.reason.push(ErrorType.MethodNotAllowed); + return result; + } + + const pathObj = this.spec.paths[path] as any; + + if (!pathObj || !pathObj[method]) { + result.isValid = false; + result.reason.push(ErrorType.UrlPathNotExist); + return result; + } + + return result; + } + + validateResponse(method: string, path: string): APIValidationResult { + const result: APIValidationResult = { isValid: true, reason: [] }; + + const operationObject = (this.spec.paths[path] as any)[method] as OpenAPIV3.OperationObject; + + const { json, multipleMediaType } = Utils.getResponseJson(operationObject); + + // only support response body only contains “application/json” content type + if (multipleMediaType) { + result.reason.push(ErrorType.ResponseContainMultipleMediaTypes); + } else if (Object.keys(json).length === 0) { + // response body should not be empty + result.reason.push(ErrorType.ResponseJsonIsEmpty); + } + + return result; + } + + validateServer(method: string, path: string): APIValidationResult { + const pathObj = this.spec.paths[path] as any; + + const result: APIValidationResult = { isValid: true, reason: [] }; + const operationObject = pathObj[method] as OpenAPIV3.OperationObject; + + const rootServer = this.spec.servers && this.spec.servers[0]; + const methodServer = this.spec.paths[path]!.servers && this.spec.paths[path]!.servers![0]; + const operationServer = operationObject.servers && operationObject.servers[0]; + + const serverUrl = operationServer || methodServer || rootServer; + if (!serverUrl) { + // should contain server URL + result.reason.push(ErrorType.NoServerInformation); + } else { + // server url should be absolute url with https protocol + const serverValidateResult = Utils.checkServerUrl([serverUrl]); + result.reason.push(...serverValidateResult.map((item) => item.type)); + } + + return result; + } + + validateAuth(method: string, path: string): APIValidationResult { + const pathObj = this.spec.paths[path] as any; + const operationObject = pathObj[method] as OpenAPIV3.OperationObject; + + const securities = operationObject.security; + const authSchemeArray = Utils.getAuthArray(securities, this.spec); + + if (authSchemeArray.length === 0) { + return { isValid: true, reason: [] }; + } + + if ( + this.options.allowAPIKeyAuth || + this.options.allowOauth2 || + this.options.allowBearerTokenAuth + ) { + // Currently we don't support multiple auth in one operation + if (authSchemeArray.length > 0 && authSchemeArray.every((auths) => auths.length > 1)) { + return { + isValid: false, + reason: [ErrorType.MultipleAuthNotSupported], + }; + } + + for (const auths of authSchemeArray) { + if (auths.length === 1) { + if ( + (this.options.allowAPIKeyAuth && Utils.isAPIKeyAuth(auths[0].authScheme)) || + (this.options.allowOauth2 && Utils.isOAuthWithAuthCodeFlow(auths[0].authScheme)) || + (this.options.allowBearerTokenAuth && Utils.isBearerTokenAuth(auths[0].authScheme)) + ) { + return { isValid: true, reason: [] }; + } + } + } + } + + return { isValid: false, reason: [ErrorType.AuthTypeIsNotSupported] }; + } + + checkPostBodySchema(schema: OpenAPIV3.SchemaObject, isRequired = false): CheckParamResult { + const paramResult: CheckParamResult = { + requiredNum: 0, + optionalNum: 0, + isValid: true, + reason: [], + }; + + if (Object.keys(schema).length === 0) { + return paramResult; + } + + const isRequiredWithoutDefault = isRequired && schema.default === undefined; + const isCopilot = this.projectType === ProjectType.Copilot; + + if (isCopilot && this.hasNestedObjectInSchema(schema)) { + paramResult.isValid = false; + paramResult.reason = [ErrorType.RequestBodyContainsNestedObject]; + return paramResult; + } + + if ( + schema.type === "string" || + schema.type === "integer" || + schema.type === "boolean" || + schema.type === "number" + ) { + if (isRequiredWithoutDefault) { + paramResult.requiredNum = paramResult.requiredNum + 1; + } else { + paramResult.optionalNum = paramResult.optionalNum + 1; + } + } else if (schema.type === "object") { + const { properties } = schema; + for (const property in properties) { + let isRequired = false; + if (schema.required && schema.required?.indexOf(property) >= 0) { + isRequired = true; + } + const result = this.checkPostBodySchema( + properties[property] as OpenAPIV3.SchemaObject, + isRequired + ); + paramResult.requiredNum += result.requiredNum; + paramResult.optionalNum += result.optionalNum; + paramResult.isValid = paramResult.isValid && result.isValid; + paramResult.reason.push(...result.reason); + } + } else { + if (isRequiredWithoutDefault && !isCopilot) { + paramResult.isValid = false; + paramResult.reason.push(ErrorType.PostBodyContainsRequiredUnsupportedSchema); + } + } + return paramResult; + } + + checkParamSchema(paramObject: OpenAPIV3.ParameterObject[]): CheckParamResult { + const paramResult: CheckParamResult = { + requiredNum: 0, + optionalNum: 0, + isValid: true, + reason: [], + }; + + if (!paramObject) { + return paramResult; + } + + const isCopilot = this.projectType === ProjectType.Copilot; + + for (let i = 0; i < paramObject.length; i++) { + const param = paramObject[i]; + const schema = param.schema as OpenAPIV3.SchemaObject; + + if (isCopilot && this.hasNestedObjectInSchema(schema)) { + paramResult.isValid = false; + paramResult.reason.push(ErrorType.ParamsContainsNestedObject); + continue; + } + + const isRequiredWithoutDefault = param.required && schema.default === undefined; + + if (isCopilot) { + if (isRequiredWithoutDefault) { + paramResult.requiredNum = paramResult.requiredNum + 1; + } else { + paramResult.optionalNum = paramResult.optionalNum + 1; + } + continue; + } + + if (param.in === "header" || param.in === "cookie") { + if (isRequiredWithoutDefault) { + paramResult.isValid = false; + paramResult.reason.push(ErrorType.ParamsContainRequiredUnsupportedSchema); + } + continue; + } + + if ( + schema.type !== "boolean" && + schema.type !== "string" && + schema.type !== "number" && + schema.type !== "integer" + ) { + if (isRequiredWithoutDefault) { + paramResult.isValid = false; + paramResult.reason.push(ErrorType.ParamsContainRequiredUnsupportedSchema); + } + continue; + } + + if (param.in === "query" || param.in === "path") { + if (isRequiredWithoutDefault) { + paramResult.requiredNum = paramResult.requiredNum + 1; + } else { + paramResult.optionalNum = paramResult.optionalNum + 1; + } + } + } + + return paramResult; + } + + private hasNestedObjectInSchema(schema: OpenAPIV3.SchemaObject): boolean { + if (schema.type === "object") { + for (const property in schema.properties) { + const nestedSchema = schema.properties[property] as OpenAPIV3.SchemaObject; + if (nestedSchema.type === "object") { + return true; + } + } + } + return false; + } +} diff --git a/packages/spec-parser/src/validators/validatorFactory.ts b/packages/spec-parser/src/validators/validatorFactory.ts new file mode 100644 index 0000000000..ad8ee858e7 --- /dev/null +++ b/packages/spec-parser/src/validators/validatorFactory.ts @@ -0,0 +1,23 @@ +import { OpenAPIV3 } from "openapi-types"; +import { ParseOptions, ProjectType } from "../interfaces"; +import { CopilotValidator } from "./copilotValidator"; +import { SMEValidator } from "./smeValidator"; +import { TeamsAIValidator } from "./teamsAIValidator"; +import { Validator } from "./validator"; + +export class ValidatorFactory { + static create(spec: OpenAPIV3.Document, options: ParseOptions): Validator { + const type = options.projectType ?? ProjectType.SME; + + switch (type) { + case ProjectType.SME: + return new SMEValidator(spec, options); + case ProjectType.Copilot: + return new CopilotValidator(spec, options); + case ProjectType.TeamsAi: + return new TeamsAIValidator(spec, options); + default: + throw new Error(`Invalid project type: ${type}`); + } + } +} diff --git a/packages/spec-parser/test/browser/specParser.browser.test.ts b/packages/spec-parser/test/browser/specParser.browser.test.ts index 8b8bb28651..01cac7d339 100644 --- a/packages/spec-parser/test/browser/specParser.browser.test.ts +++ b/packages/spec-parser/test/browser/specParser.browser.test.ts @@ -248,7 +248,7 @@ describe("SpecParser in Browser", () => { const parseStub = sinon.stub(specParser.parser, "parse").resolves(spec as any); const dereferenceStub = sinon.stub(specParser.parser, "dereference").resolves(spec as any); - const listSupportedAPIsSyp = sinon.spy(specParser as any, "listSupportedAPIs"); + const listAPIsSyp = sinon.spy(specParser as any, "listAPIs"); let result = await specParser.listSupportedAPIInfo(); result = await specParser.listSupportedAPIInfo(); expect(result).to.deep.equal([ @@ -267,7 +267,7 @@ describe("SpecParser in Browser", () => { description: "Get user by user id, balabala", }, ]); - expect(listSupportedAPIsSyp.callCount).to.equal(1); + expect(listAPIsSyp.callCount).to.equal(1); }); it("should not list api without operationId with allowMissingId is true", async () => { diff --git a/packages/spec-parser/test/specFilter.test.ts b/packages/spec-parser/test/specFilter.test.ts index db44056024..491d21cbdd 100644 --- a/packages/spec-parser/test/specFilter.test.ts +++ b/packages/spec-parser/test/specFilter.test.ts @@ -9,6 +9,7 @@ import sinon from "sinon"; import { SpecParserError } from "../src/specParserError"; import { ErrorType, ParseOptions, ProjectType } from "../src/interfaces"; import { Utils } from "../src/utils"; +import { ValidatorFactory } from "../src/validators/validatorFactory"; describe("specFilter", () => { afterEach(() => { @@ -479,7 +480,7 @@ describe("specFilter", () => { expect(clonedSpec).to.deep.equal(unResolveSpec); }); - it("should throw a SpecParserError if isSupportedApi throws an error", () => { + it("should throw a SpecParserError if ValidatorFactory throws an error", () => { const filter = ["GET /hello"]; const unResolveSpec = { openapi: "3.0.0", @@ -500,9 +501,8 @@ describe("specFilter", () => { }, }, } as any; - const isSupportedApiStub = sinon - .stub(Utils, "isSupportedApi") - .throws(new Error("isSupportedApi error")); + + sinon.stub(ValidatorFactory, "create").throws(new Error("ValidatorFactory create error")); try { const options: ParseOptions = { @@ -519,7 +519,7 @@ describe("specFilter", () => { } catch (err: any) { expect(err).to.be.instanceOf(SpecParserError); expect(err.errorType).to.equal(ErrorType.FilterSpecFailed); - expect(err.message).to.equal("Error: isSupportedApi error"); + expect(err.message).to.equal("Error: ValidatorFactory create error"); } }); }); diff --git a/packages/spec-parser/test/utils.test.ts b/packages/spec-parser/test/utils.test.ts index b6c3a18b56..add04359a9 100644 --- a/packages/spec-parser/test/utils.test.ts +++ b/packages/spec-parser/test/utils.test.ts @@ -65,2656 +65,6 @@ describe("utils", () => { }); }); - describe("isSupportedApi", () => { - it("should return true if method is GET, path is valid, and parameter is supported", () => { - const method = "GET"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - get: { - parameters: [ - { - in: "query", - schema: { type: "string" }, - required: true, - }, - ], - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const { isValid } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, true); - }); - - it("should return false if have no operationId with allowMissingId is false", () => { - const method = "GET"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - get: { - parameters: [ - { - in: "query", - schema: { type: "string" }, - required: true, - }, - ], - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: false, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const { isValid, reason } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, false); - assert.deepEqual(reason, [ErrorType.MissingOperationId]); - }); - - it("should return true if method is POST, path is valid, and no required parameters", () => { - const method = "POST"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - post: { - parameters: [ - { - in: "query", - required: false, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const { isValid } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, true); - }); - - it("should return false if method is POST, path is valid, parameter is supported and only one required param in parameters but contains auth", () => { - const method = "POST"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - components: { - securitySchemes: { - api_key: { - type: "apiKey", - name: "api_key", - in: "header", - }, - api_key2: { - type: "apiKey", - name: "api_key2", - in: "header", - }, - }, - }, - paths: { - "/users": { - post: { - security: [ - { - api_key2: [], - }, - ], - parameters: [ - { - in: "query", - required: false, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const { isValid, reason } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, false); - assert.deepEqual(reason, [ErrorType.AuthTypeIsNotSupported]); - }); - - it("should return true if allowBearerTokenAuth is true and contains bearer token auth", () => { - const method = "POST"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - components: { - securitySchemes: { - bearer_token1: { - type: "http", - scheme: "bearer", - }, - bearer_token2: { - type: "http", - scheme: "bearer", - }, - }, - }, - paths: { - "/users": { - post: { - security: [ - { - bearer_token2: [], - }, - ], - parameters: [ - { - in: "query", - required: false, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowBearerTokenAuth: true, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const { isValid } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, true); - }); - - it("should return true if allowAPIKeyAuth is true and contains apiKey auth", () => { - const method = "POST"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - components: { - securitySchemes: { - api_key: { - type: "apiKey", - name: "api_key", - in: "header", - }, - api_key2: { - type: "apiKey", - name: "api_key2", - in: "header", - }, - }, - }, - paths: { - "/users": { - post: { - security: [ - { - api_key2: [], - }, - ], - parameters: [ - { - in: "query", - required: false, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: true, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const { isValid } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, true); - }); - - it("should return false if allowAPIKeyAuth is true but contains multiple apiKey auth", () => { - const method = "POST"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - components: { - securitySchemes: { - api_key: { - type: "apiKey", - name: "api_key", - in: "header", - }, - api_key2: { - type: "apiKey", - name: "api_key2", - in: "header", - }, - }, - }, - paths: { - "/users": { - post: { - security: [ - { - api_key2: [], - api_key: [], - }, - ], - parameters: [ - { - in: "query", - required: false, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: true, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const { isValid, reason } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, false); - assert.deepEqual(reason, [ErrorType.MultipleAuthNotSupported]); - }); - - it("should return true if allowOauth2 is true and contains aad auth", () => { - const method = "POST"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - components: { - securitySchemes: { - oauth: { - type: "oauth2", - flows: { - authorizationCode: { - authorizationUrl: "https://example.com/api/oauth/dialog", - tokenUrl: "https://example.com/api/oauth/token", - refreshUrl: "https://example.com/api/outh/refresh", - scopes: { - "write:pets": "modify pets in your account", - "read:pets": "read your pets", - }, - }, - }, - }, - }, - }, - paths: { - "/users": { - post: { - security: [ - { - oauth: ["read:pets"], - }, - ], - parameters: [ - { - in: "query", - required: false, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: true, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const { isValid } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, true); - }); - - it("should return false if allowAPIKeyAuth is true, allowOauth2 is false, but contain oauth", () => { - const method = "POST"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - components: { - securitySchemes: { - api_key: { - type: "apiKey", - name: "api_key", - in: "header", - }, - oauth: { - type: "oauth2", - flows: { - authorizationCode: { - authorizationUrl: "https://example.com/api/oauth/dialog", - tokenUrl: "https://example.com/api/oauth/token", - refreshUrl: "https://example.com/api/outh/refresh", - scopes: { - "write:pets": "modify pets in your account", - "read:pets": "read your pets", - }, - }, - }, - }, - }, - }, - paths: { - "/users": { - post: { - security: [ - { - oauth: ["read:pets"], - }, - ], - parameters: [ - { - in: "query", - required: false, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: true, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const { isValid, reason } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, false); - assert.deepEqual(reason, [ErrorType.AuthTypeIsNotSupported]); - }); - - it("should return false if allowAPIKeyAuth is true, allowOauth2 is true, but not auth code flow", () => { - const method = "POST"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - components: { - securitySchemes: { - api_key: { - type: "apiKey", - name: "api_key", - in: "header", - }, - oauth: { - type: "oauth2", - flows: { - implicit: { - authorizationUrl: "https://example.com/api/oauth/dialog", - scopes: { - "write:pets": "modify pets in your account", - "read:pets": "read your pets", - }, - }, - }, - }, - }, - }, - paths: { - "/users": { - post: { - security: [ - { - oauth: ["read:pets"], - }, - ], - parameters: [ - { - in: "query", - required: false, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: true, - allowMultipleParameters: false, - allowOauth2: true, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const { isValid, reason } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, false); - assert.deepEqual(reason, [ErrorType.AuthTypeIsNotSupported]); - }); - - it("should return true if allowAPIKeyAuth is true, allowOauth2 is true, but not auth code flow for teams ai project", () => { - const method = "POST"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - components: { - securitySchemes: { - api_key: { - type: "apiKey", - name: "api_key", - in: "header", - }, - oauth: { - type: "oauth2", - flows: { - implicit: { - authorizationUrl: "https://example.com/api/oauth/dialog", - scopes: { - "write:pets": "modify pets in your account", - "read:pets": "read your pets", - }, - }, - }, - }, - }, - }, - paths: { - "/users": { - post: { - security: [ - { - oauth: ["read:pets"], - }, - ], - parameters: [ - { - in: "query", - required: false, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: true, - allowMultipleParameters: false, - allowOauth2: true, - projectType: ProjectType.TeamsAi, - allowMethods: ["get", "post"], - }; - - const { isValid } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, true); - }); - - it("should return true if method is POST, path is valid, parameter is supported and only one required param in parameters", () => { - const method = "POST"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - post: { - parameters: [ - { - in: "query", - required: false, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const { isValid } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, true); - }); - - it("should return false if method is POST, path is valid, parameter is supported and both postBody and parameters contains required param", () => { - const method = "POST"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - post: { - parameters: [ - { - in: "query", - required: true, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const { isValid, reason } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, false); - assert.deepEqual(reason, [ErrorType.ExceededRequiredParamsLimit]); - }); - - it("should return true if method is POST, path is valid, parameter is supported and both postBody and parameters contains multiple required param for copilot", () => { - const method = "POST"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - post: { - parameters: [ - { - in: "query", - required: true, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: true, - allowOauth2: false, - projectType: ProjectType.Copilot, - allowMethods: ["get", "post"], - }; - - const { isValid } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, true); - }); - - it("should support multiple required parameters", () => { - const method = "POST"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - post: { - parameters: [ - { - in: "query", - required: true, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: true, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - const { isValid } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, true); - }); - - it("should not support multiple required parameters count larger than 5", () => { - const method = "POST"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - post: { - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["id1", "id2", "id3", "id4", "id5", "id6"], - properties: { - id1: { - type: "string", - }, - id2: { - type: "string", - }, - id3: { - type: "string", - }, - id4: { - type: "string", - }, - id5: { - type: "string", - }, - id6: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: true, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const { isValid, reason } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, false); - assert.deepEqual(reason, [ErrorType.ExceededRequiredParamsLimit]); - }); - - it("should support multiple required parameters count larger than 5 for teams ai project", () => { - const method = "POST"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - post: { - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["id1", "id2", "id3", "id4", "id5", "id6"], - properties: { - id1: { - type: "string", - }, - id2: { - type: "string", - }, - id3: { - type: "string", - }, - id4: { - type: "string", - }, - id5: { - type: "string", - }, - id6: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: true, - allowOauth2: false, - projectType: ProjectType.TeamsAi, - allowMethods: ["get", "post"], - }; - - const { isValid } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, true); - }); - - it("should not support multiple required parameters count larger than 5 for copilot", () => { - const method = "POST"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - post: { - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["id1", "id2", "id3", "id4", "id5", "id6"], - properties: { - id1: { - type: "string", - }, - id2: { - type: "string", - }, - id3: { - type: "string", - }, - id4: { - type: "string", - }, - id5: { - type: "string", - }, - id6: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: true, - allowOauth2: false, - projectType: ProjectType.Copilot, - allowMethods: ["get", "post"], - }; - - const { isValid } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, true); - }); - - it("should return false if method is POST, but requestBody contains unsupported parameter and required", () => { - const method = "POST"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - post: { - parameters: [ - { - in: "query", - required: true, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "array", - items: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: true, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const { isValid, reason } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, false); - assert.deepEqual(reason, [ErrorType.PostBodyContainsRequiredUnsupportedSchema]); - }); - - it("should return true if method is POST, but requestBody contains unsupported parameter and required but has default value", () => { - const method = "POST"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - post: { - parameters: [ - { - in: "query", - required: true, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "array", - default: ["item"], - items: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: true, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const { isValid } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, true); - }); - - it("should return false if method is POST, parameters contain nested object, and request body is not json", () => { - const method = "POST"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - post: { - parameters: [ - { - in: "query", - required: true, - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "object", - properties: { - id: { - type: "string", - }, - }, - }, - }, - }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "string", - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.Copilot, - allowMethods: ["get", "post"], - }; - - const { isValid, reason } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, false); - expect(reason).to.have.members([ - ErrorType.ParamsContainsNestedObject, - ErrorType.PostBodySchemaIsNotJson, - ]); - expect(reason.length).equals(2); - }); - - it("should return false if method is POST, but requestBody contain nested object", () => { - const method = "POST"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - post: { - parameters: [ - { - in: "query", - required: true, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "object", - properties: { - id: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.Copilot, - allowMethods: ["get", "post"], - }; - - const { isValid, reason } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, false); - assert.deepEqual(reason, [ErrorType.RequestBodyContainsNestedObject]); - }); - - it("should return true if method is POST, path is valid, parameter is supported and only one required param in postBody", () => { - const method = "POST"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - post: { - parameters: [ - { - in: "query", - required: true, - schema: { type: "string" }, - }, - ], - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const { isValid } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, true); - }); - - it("should return false if method is GET, path is valid, parameter is supported, but response is empty", () => { - const method = "GET"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - get: { - parameters: [ - { - in: "query", - schema: { type: "string" }, - required: true, - }, - ], - responses: { - 400: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const { isValid, reason } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, false); - assert.deepEqual(reason, [ErrorType.ResponseJsonIsEmpty]); - }); - - it("should return false if method is not GET or POST", () => { - const method = "PUT"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - get: { - parameters: [ - { - in: "query", - schema: { type: "string" }, - }, - ], - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const { isValid, reason } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, false); - assert.deepEqual(reason, [ErrorType.MethodNotAllowed]); - }); - - it("should return false if path is not valid", () => { - const method = "GET"; - const path = "/invalid"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - get: { - parameters: [ - { - in: "query", - schema: { type: "string" }, - }, - ], - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const { isValid, reason } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, false); - assert.deepEqual(reason, [ErrorType.UrlPathNotExist]); - }); - - it("should return false if parameter is not supported and required", () => { - const method = "GET"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - get: { - parameters: [ - { - in: "query", - required: true, - schema: { type: "object" }, - }, - ], - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const { isValid, reason } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, false); - assert.deepEqual(reason, [ErrorType.ParamsContainRequiredUnsupportedSchema]); - }); - - it("should return false due to ignore unsupported schema type with default value", () => { - const method = "GET"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - get: { - parameters: [ - { - in: "query", - required: true, - schema: { type: "object", default: { name: "test" } }, - }, - ], - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const { isValid, reason } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, false); - assert.deepEqual(reason, [ErrorType.NoParameter]); - }); - - it("should return false if parameter is in header and required", () => { - const method = "GET"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - get: { - parameters: [ - { - in: "header", - required: true, - schema: { type: "string" }, - }, - ], - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const { isValid, reason } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, false); - assert.deepEqual(reason, [ErrorType.ParamsContainRequiredUnsupportedSchema]); - }); - - it("should return true if parameter is in header and required for copilot", () => { - const method = "GET"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - get: { - parameters: [ - { - in: "header", - required: true, - schema: { type: "string" }, - }, - { - in: "query", - schema: { type: "string" }, - }, - ], - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.Copilot, - allowMethods: ["get", "post"], - }; - - const { isValid } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, true); - }); - - it("should return false if there is no parameters", () => { - const method = "GET"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - get: { - parameters: [], - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const { isValid, reason } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, false); - assert.deepEqual(reason, [ErrorType.NoParameter]); - }); - - it("should return true if there is no parameters for copilot", () => { - const method = "GET"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - get: { - parameters: [], - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.Copilot, - allowMethods: ["get", "post"], - }; - - const { isValid } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, true); - }); - - it("should return false if parameters is null", () => { - const method = "GET"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - get: { - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const { isValid, reason } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, false); - assert.deepEqual(reason, [ErrorType.NoParameter]); - }); - - it("should return false if has parameters but no 20X response", () => { - const method = "GET"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - get: { - parameters: [ - { - in: "query", - schema: { type: "object" }, - }, - ], - responses: { - 404: { - content: { - "application/json": { - schema: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const { isValid, reason } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, false); - - // NoParameter because object is not supported and there is no required parameters - expect(reason).to.have.members([ErrorType.NoParameter, ErrorType.ResponseJsonIsEmpty]); - expect(reason.length).equals(2); - }); - - it("should return false if method is POST, but request body contains media type other than application/json", () => { - const method = "POST"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - post: { - parameters: [ - { - in: "query", - required: true, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - "application/xml": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - const { isValid, reason } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, false); - assert.deepEqual(reason, [ - ErrorType.PostBodyContainMultipleMediaTypes, - ErrorType.ExceededRequiredParamsLimit, - ]); - }); - - it("should return true if method is POST, and request body contains media type other than application/json for teams ai project", () => { - const method = "POST"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - post: { - parameters: [ - { - in: "query", - required: true, - schema: { type: "string" }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - "application/xml": { - schema: { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.TeamsAi, - allowMethods: ["get", "post"], - }; - const { isValid } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, true); - }); - - it("should return false if method is POST, and request body schema is not object", () => { - const method = "POST"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - post: { - requestBody: { - content: { - "application/json": { - schema: { - type: "string", - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.Copilot, - allowMethods: ["get", "post"], - }; - - const { isValid, reason } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, false); - assert.deepEqual(reason, [ErrorType.PostBodySchemaIsNotJson]); - }); - - it("should return false if method is GET, but response body contains media type other than application/json", () => { - const method = "GET"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - get: { - parameters: [ - { - in: "query", - required: true, - schema: { type: "string" }, - }, - ], - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - "application/xml": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.SME, - allowMethods: ["get", "post"], - }; - - const { isValid, reason } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, false); - assert.deepEqual(reason, [ErrorType.ResponseContainMultipleMediaTypes]); - }); - - it("should return true if method is GET, and response body contains media type other than application/json for teams ai project", () => { - const method = "GET"; - const path = "/users"; - const spec = { - servers: [ - { - url: "https://example.com", - }, - ], - paths: { - "/users": { - get: { - parameters: [ - { - in: "query", - required: true, - schema: { type: "string" }, - }, - ], - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - "application/xml": { - schema: { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }; - const options: ParseOptions = { - allowMissingId: true, - allowAPIKeyAuth: false, - allowMultipleParameters: false, - allowOauth2: false, - projectType: ProjectType.TeamsAi, - allowMethods: ["get", "post"], - }; - - const { isValid } = Utils.isSupportedApi(method, path, spec as any, options); - assert.strictEqual(isValid, true); - }); - }); - describe("getUrlProtocol", () => { it("should return the protocol of a valid URL", () => { const url = "https://example.com/path/to/file"; @@ -2741,238 +91,6 @@ describe("utils", () => { }); }); - describe("checkRequiredParameters", () => { - it("should valid if there is only one required parameter", () => { - const paramObject = [ - { in: "query", required: true, schema: { type: "string" } }, - { in: "path", required: false, schema: { type: "string" } }, - ]; - const result = Utils.checkParameters(paramObject as OpenAPIV3.ParameterObject[], false); - assert.strictEqual(result.isValid, true); - }); - - it("should valid if there are multiple required parameters", () => { - const paramObject = [ - { in: "query", required: true, schema: { type: "string" } }, - { in: "path", required: true, schema: { type: "string" } }, - ]; - const result = Utils.checkParameters(paramObject as OpenAPIV3.ParameterObject[], false); - assert.strictEqual(result.isValid, true); - assert.strictEqual(result.requiredNum, 2); - assert.strictEqual(result.optionalNum, 0); - }); - - it("should not valid if any required parameter is in header or cookie and is required", () => { - const paramObject = [ - { in: "query", required: true, schema: { type: "string" } }, - { in: "path", required: false, schema: { type: "string" } }, - { in: "header", required: true, schema: { type: "string" } }, - ]; - const result = Utils.checkParameters(paramObject as OpenAPIV3.ParameterObject[], false); - assert.strictEqual(result.isValid, false); - }); - - it("should valid if parameter in header or cookie is required but have default value", () => { - const paramObject = [ - { in: "query", required: true, schema: { type: "string" } }, - { in: "path", required: false, schema: { type: "string" } }, - { in: "header", required: true, schema: { type: "string", default: "value" } }, - ]; - const result = Utils.checkParameters(paramObject as OpenAPIV3.ParameterObject[], false); - assert.strictEqual(result.isValid, true); - // header param is ignored - assert.strictEqual(result.requiredNum, 1); - assert.strictEqual(result.optionalNum, 1); - }); - - it("should treat required param with default value as optional param", () => { - const paramObject = [ - { in: "query", required: true, schema: { type: "string", default: "value" } }, - { in: "path", required: false, schema: { type: "string" } }, - { in: "query", required: true, schema: { type: "string" } }, - ]; - const result = Utils.checkParameters(paramObject as OpenAPIV3.ParameterObject[], false); - assert.strictEqual(result.isValid, true); - assert.strictEqual(result.requiredNum, 1); - assert.strictEqual(result.optionalNum, 2); - }); - - it("should ignore required query param with default value and array type", () => { - const paramObject = [ - { in: "query", required: true, schema: { type: "string" } }, - { in: "path", required: false, schema: { type: "string" } }, - { in: "query", required: true, schema: { type: "array", default: ["item"] } }, - ]; - const result = Utils.checkParameters(paramObject as OpenAPIV3.ParameterObject[], false); - assert.strictEqual(result.isValid, true); - assert.strictEqual(result.requiredNum, 1); - assert.strictEqual(result.optionalNum, 1); - }); - - it("should ignore in header or cookie if is not required", () => { - const paramObject = [ - { in: "query", required: true, schema: { type: "string" } }, - { in: "path", required: false, schema: { type: "string" } }, - { in: "header", required: false, schema: { type: "string" } }, - ]; - const result = Utils.checkParameters(paramObject as OpenAPIV3.ParameterObject[], false); - assert.strictEqual(result.isValid, true); - assert.strictEqual(result.requiredNum, 1); - assert.strictEqual(result.optionalNum, 1); - }); - - it("should return false if any schema is array and required", () => { - const paramObject = [ - { in: "query", required: true, schema: { type: "string" } }, - { in: "path", required: true, schema: { type: "array" } }, - ]; - const result = Utils.checkParameters(paramObject as OpenAPIV3.ParameterObject[], false); - assert.strictEqual(result.isValid, false); - }); - - it("should return false if any schema is object and required", () => { - const paramObject = [ - { in: "query", required: false, schema: { type: "string" } }, - { in: "path", required: true, schema: { type: "object" } }, - ]; - const result = Utils.checkParameters(paramObject as OpenAPIV3.ParameterObject[], false); - assert.strictEqual(result.isValid, false); - }); - - it("should return valid if any schema is object but optional", () => { - const paramObject = [ - { in: "query", required: false, schema: { type: "string" } }, - { in: "path", required: false, schema: { type: "object" } }, - ]; - const result = Utils.checkParameters(paramObject as OpenAPIV3.ParameterObject[], false); - assert.strictEqual(result.isValid, true); - assert.strictEqual(result.requiredNum, 0); - assert.strictEqual(result.optionalNum, 1); - }); - }); - - describe("checkPostBodyRequiredParameters", () => { - it("should return 0 for an empty schema", () => { - const schema = {}; - const result = Utils.checkPostBody(schema as any); - assert.strictEqual(result.requiredNum, 0); - assert.strictEqual(result.optionalNum, 0); - }); - - it("should treat required schema with default value as optional param", () => { - const schema = { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - default: "value", - }, - }, - }; - const result = Utils.checkPostBody(schema as any); - assert.strictEqual(result.requiredNum, 0); - assert.strictEqual(result.optionalNum, 1); - assert.strictEqual(result.isValid, true); - }); - - it("should return 1 if the schema has a required string property", () => { - const schema = { - type: "object", - required: ["name"], - properties: { - name: { - type: "string", - }, - }, - }; - const result = Utils.checkPostBody(schema as any); - assert.strictEqual(result.requiredNum, 1); - assert.strictEqual(result.optionalNum, 0); - assert.strictEqual(result.isValid, true); - }); - - it("should return 0 if the schema has an optional string property", () => { - const schema = { - type: "object", - properties: { - name: { - type: "string", - }, - }, - }; - const result = Utils.checkPostBody(schema as any); - assert.strictEqual(result.requiredNum, 0); - assert.strictEqual(result.optionalNum, 1); - assert.strictEqual(result.isValid, true); - }); - - it("should return the correct count for a nested schema", () => { - const schema = { - type: "object", - required: ["name", "address"], - properties: { - name: { - type: "string", - }, - address: { - type: "object", - required: ["street"], - properties: { - street: { - type: "string", - }, - city: { - type: "string", - }, - }, - }, - }, - }; - const result = Utils.checkPostBody(schema as any); - assert.strictEqual(result.requiredNum, 2); - assert.strictEqual(result.optionalNum, 1); - assert.strictEqual(result.isValid, true); - }); - - it("should return not valid for an unsupported schema type", () => { - const schema = { - type: "object", - required: ["name"], - properties: { - name: { - type: "array", - items: { - type: "string", - }, - }, - }, - }; - const result = Utils.checkPostBody(schema as any); - assert.strictEqual(result.isValid, false); - }); - - it("should return valid for an unsupported schema type but it is required with default value", () => { - const schema = { - type: "object", - required: ["name"], - properties: { - name: { - type: "array", - default: ["item"], - items: { - type: "string", - }, - }, - }, - }; - const result = Utils.checkPostBody(schema as any); - assert.strictEqual(result.isValid, true); - assert.strictEqual(result.requiredNum, 0); - assert.strictEqual(result.optionalNum, 0); - }); - }); - describe("checkServerUrl", () => { it("should return an empty array if the server URL is valid", () => { const servers = [{ url: "https://example.com" }]; @@ -3340,43 +458,6 @@ describe("utils", () => { expect(multipleMediaType).to.be.true; }); - it("should return JSON response for status code 200 with multiple media type when it is teams ai project", () => { - const operationObject = { - responses: { - "200": { - content: { - "application/json": { - schema: { - type: "object", - properties: { - message: { type: "string" }, - }, - }, - }, - "application/xml": { - schema: { - type: "object", - properties: { - message: { type: "string" }, - }, - }, - }, - }, - }, - }, - } as any; - const { json, multipleMediaType } = Utils.getResponseJson(operationObject, true); - expect(json).to.deep.equal({ - schema: { - type: "object", - properties: { - message: { type: "string" }, - }, - }, - }); - expect(multipleMediaType).to.be.true; - }); - it("should return the JSON response for status code 201", () => { const operationObject = { responses: { diff --git a/packages/spec-parser/test/validator.test.ts b/packages/spec-parser/test/validator.test.ts new file mode 100644 index 0000000000..051e855ae7 --- /dev/null +++ b/packages/spec-parser/test/validator.test.ts @@ -0,0 +1,2742 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { assert, expect } from "chai"; +import "mocha"; +import { ErrorType, ProjectType, ParseOptions } from "../src/interfaces"; +import { ValidatorFactory } from "../src/validators/validatorFactory"; +import { SMEValidator } from "../src/validators/smeValidator"; +import { CopilotValidator } from "../src/validators/copilotValidator"; +import { TeamsAIValidator } from "../src/validators/teamsAIValidator"; + +describe("Validator", () => { + describe("ValidatorFactory", () => { + it("should create validator correctly", () => { + const options: ParseOptions = { + projectType: undefined, + }; + + let validator = ValidatorFactory.create({} as any, options); + assert.instanceOf(validator, SMEValidator); + + options.projectType = ProjectType.SME; + validator = ValidatorFactory.create({} as any, options); + assert.instanceOf(validator, SMEValidator); + + options.projectType = ProjectType.Copilot; + + validator = ValidatorFactory.create({} as any, options); + assert.instanceOf(validator, CopilotValidator); + + options.projectType = ProjectType.TeamsAi; + validator = ValidatorFactory.create({} as any, options); + assert.instanceOf(validator, TeamsAIValidator); + }); + + it("should throw error if project type is unknown", () => { + const options: ParseOptions = { + projectType: "test" as any, + }; + + assert.throws( + () => { + ValidatorFactory.create({} as any, options); + }, + Error, + "Invalid project type: test" + ); + }); + }); + describe("SMEValidator", () => { + it("should return true if method is GET, path is valid, and parameter is supported", () => { + const method = "GET"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + parameters: [ + { + in: "query", + schema: { type: "string" }, + required: true, + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should return false if have no operationId with allowMissingId is false", () => { + const method = "GET"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + parameters: [ + { + in: "query", + schema: { type: "string" }, + required: true, + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: false, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.MissingOperationId]); + }); + + it("should return true if method is POST, path is valid, and no required parameters", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + parameters: [ + { + in: "query", + required: false, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should return false if method is POST, path is valid, parameter is supported and only one required param in parameters but contains auth", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + components: { + securitySchemes: { + api_key: { + type: "apiKey", + name: "api_key", + in: "header", + }, + api_key2: { + type: "apiKey", + name: "api_key2", + in: "header", + }, + }, + }, + paths: { + "/users": { + post: { + security: [ + { + api_key2: [], + }, + ], + parameters: [ + { + in: "query", + required: false, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.AuthTypeIsNotSupported]); + }); + + it("should return true if allowBearerTokenAuth is true and contains bearer token auth", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + components: { + securitySchemes: { + bearer_token1: { + type: "http", + scheme: "bearer", + }, + bearer_token2: { + type: "http", + scheme: "bearer", + }, + }, + }, + paths: { + "/users": { + post: { + security: [ + { + bearer_token2: [], + }, + ], + parameters: [ + { + in: "query", + required: false, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } as any; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowBearerTokenAuth: true, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should return true if allowAPIKeyAuth is true and contains apiKey auth", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + components: { + securitySchemes: { + api_key: { + type: "apiKey", + name: "api_key", + in: "header", + }, + api_key2: { + type: "apiKey", + name: "api_key2", + in: "header", + }, + }, + }, + paths: { + "/users": { + post: { + security: [ + { + api_key2: [], + }, + ], + parameters: [ + { + in: "query", + required: false, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } as any; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: true, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should return false if allowAPIKeyAuth is true but contains multiple apiKey auth", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + components: { + securitySchemes: { + api_key: { + type: "apiKey", + name: "api_key", + in: "header", + }, + api_key2: { + type: "apiKey", + name: "api_key2", + in: "header", + }, + }, + }, + paths: { + "/users": { + post: { + security: [ + { + api_key2: [], + api_key: [], + }, + ], + parameters: [ + { + in: "query", + required: false, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: true, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.MultipleAuthNotSupported]); + }); + + it("should return true if allowOauth2 is true and contains aad auth", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + components: { + securitySchemes: { + oauth: { + type: "oauth2", + flows: { + authorizationCode: { + authorizationUrl: "https://example.com/api/oauth/dialog", + tokenUrl: "https://example.com/api/oauth/token", + refreshUrl: "https://example.com/api/outh/refresh", + scopes: { + "write:pets": "modify pets in your account", + "read:pets": "read your pets", + }, + }, + }, + }, + }, + }, + paths: { + "/users": { + post: { + security: [ + { + oauth: ["read:pets"], + }, + ], + parameters: [ + { + in: "query", + required: false, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } as any; + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: true, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should return false if allowAPIKeyAuth is true, allowOauth2 is false, but contain oauth", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + components: { + securitySchemes: { + api_key: { + type: "apiKey", + name: "api_key", + in: "header", + }, + oauth: { + type: "oauth2", + flows: { + authorizationCode: { + authorizationUrl: "https://example.com/api/oauth/dialog", + tokenUrl: "https://example.com/api/oauth/token", + refreshUrl: "https://example.com/api/outh/refresh", + scopes: { + "write:pets": "modify pets in your account", + "read:pets": "read your pets", + }, + }, + }, + }, + }, + }, + paths: { + "/users": { + post: { + security: [ + { + oauth: ["read:pets"], + }, + ], + parameters: [ + { + in: "query", + required: false, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: true, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.AuthTypeIsNotSupported]); + }); + + it("should return false if allowAPIKeyAuth is true, allowOauth2 is true, but not auth code flow", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + components: { + securitySchemes: { + api_key: { + type: "apiKey", + name: "api_key", + in: "header", + }, + oauth: { + type: "oauth2", + flows: { + implicit: { + authorizationUrl: "https://example.com/api/oauth/dialog", + scopes: { + "write:pets": "modify pets in your account", + "read:pets": "read your pets", + }, + }, + }, + }, + }, + }, + paths: { + "/users": { + post: { + security: [ + { + oauth: ["read:pets"], + }, + ], + parameters: [ + { + in: "query", + required: false, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: true, + allowMultipleParameters: false, + allowOauth2: true, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.AuthTypeIsNotSupported]); + }); + + it("should return true if method is POST, path is valid, parameter is supported and only one required param in parameters", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + parameters: [ + { + in: "query", + required: false, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should return false if method is POST, path is valid, parameter is supported and both postBody and parameters contains required param", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + parameters: [ + { + in: "query", + required: true, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.ExceededRequiredParamsLimit]); + }); + + it("should support multiple required parameters", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + parameters: [ + { + in: "query", + required: true, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: true, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should not support multiple required parameters count larger than 5", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["id1", "id2", "id3", "id4", "id5", "id6"], + properties: { + id1: { + type: "string", + }, + id2: { + type: "string", + }, + id3: { + type: "string", + }, + id4: { + type: "string", + }, + id5: { + type: "string", + }, + id6: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: true, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.ExceededRequiredParamsLimit]); + }); + + it("should return false if method is POST, but requestBody contains unsupported parameter and required", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + parameters: [ + { + in: "query", + required: true, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "array", + items: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: true, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.PostBodyContainsRequiredUnsupportedSchema]); + }); + + it("should return true if method is POST, but requestBody contains unsupported parameter and required but has default value", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + parameters: [ + { + in: "query", + required: true, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "array", + default: ["item"], + items: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: true, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should return true if method is POST, path is valid, parameter is supported and only one required param in postBody", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + parameters: [ + { + in: "query", + required: true, + schema: { type: "string" }, + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should return false if method is GET, path is valid, parameter is supported, but response is empty", () => { + const method = "GET"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + parameters: [ + { + in: "query", + schema: { type: "string" }, + required: true, + }, + ], + responses: { + 400: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.ResponseJsonIsEmpty]); + }); + + it("should return false if method is not GET or POST", () => { + const method = "PUT"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + parameters: [ + { + in: "query", + schema: { type: "string" }, + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.MethodNotAllowed]); + }); + + it("should return false if path is not valid", () => { + const method = "GET"; + const path = "/invalid"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + parameters: [ + { + in: "query", + schema: { type: "string" }, + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.UrlPathNotExist]); + }); + + it("should return false if parameter is not supported and required", () => { + const method = "GET"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + parameters: [ + { + in: "query", + required: true, + schema: { type: "object" }, + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.ParamsContainRequiredUnsupportedSchema]); + }); + + it("should return false due to ignore unsupported schema type with default value", () => { + const method = "GET"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + parameters: [ + { + in: "query", + required: true, + schema: { type: "object", default: { name: "test" } }, + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.NoParameter]); + }); + + it("should return false if parameter is in header and required", () => { + const method = "GET"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + parameters: [ + { + in: "header", + required: true, + schema: { type: "string" }, + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.ParamsContainRequiredUnsupportedSchema]); + }); + + it("should return false if there is no parameters", () => { + const method = "GET"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + parameters: [], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.NoParameter]); + }); + + it("should return false if parameters is null", () => { + const method = "GET"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.NoParameter]); + }); + + it("should return false if has parameters but no 20X response", () => { + const method = "GET"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + parameters: [ + { + in: "query", + schema: { type: "object" }, + }, + ], + responses: { + 404: { + content: { + "application/json": { + schema: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + + // NoParameter because object is not supported and there is no required parameters + expect(reason).to.have.members([ErrorType.NoParameter, ErrorType.ResponseJsonIsEmpty]); + expect(reason.length).equals(2); + }); + + it("should return false if method is POST, but request body contains media type other than application/json", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + parameters: [ + { + in: "query", + required: true, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + "application/xml": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ + ErrorType.PostBodyContainMultipleMediaTypes, + ErrorType.ExceededRequiredParamsLimit, + ]); + }); + + it("should return false if method is GET, but response body contains media type other than application/json", () => { + const method = "GET"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + parameters: [ + { + in: "query", + required: true, + schema: { type: "string" }, + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + "application/xml": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.SME, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.ResponseContainMultipleMediaTypes]); + }); + }); + + describe("CopilotValidator", () => { + it("should return true if method is POST, path is valid, parameter is supported and both postBody and parameters contains multiple required param for copilot", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + parameters: [ + { + in: "query", + required: true, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: true, + allowOauth2: false, + projectType: ProjectType.Copilot, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should return false if method is POST, and request body schema is not object", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + requestBody: { + content: { + "application/json": { + schema: { + type: "string", + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.Copilot, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.PostBodySchemaIsNotJson]); + }); + + it("should return true if there is no parameters for copilot", () => { + const method = "GET"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + parameters: [], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.Copilot, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should return true if parameter is in header and required for copilot", () => { + const method = "GET"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + parameters: [ + { + in: "header", + required: true, + schema: { type: "string" }, + }, + { + in: "query", + schema: { type: "string" }, + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.Copilot, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should not support multiple required parameters count larger than 5 for copilot", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["id1", "id2", "id3", "id4", "id5", "id6"], + properties: { + id1: { + type: "string", + }, + id2: { + type: "string", + }, + id3: { + type: "string", + }, + id4: { + type: "string", + }, + id5: { + type: "string", + }, + id6: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: true, + allowOauth2: false, + projectType: ProjectType.Copilot, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should return false if method is POST, parameters contain nested object, and request body is not json", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + parameters: [ + { + in: "query", + required: true, + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "object", + properties: { + id: { + type: "string", + }, + }, + }, + }, + }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "string", + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.Copilot, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + expect(reason).to.have.members([ + ErrorType.ParamsContainsNestedObject, + ErrorType.PostBodySchemaIsNotJson, + ]); + expect(reason.length).equals(2); + }); + + it("should return false if method is POST, but requestBody contain nested object", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + parameters: [ + { + in: "query", + required: true, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "object", + properties: { + id: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.Copilot, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid, reason } = validator.validateAPI(method, path); + assert.strictEqual(isValid, false); + assert.deepEqual(reason, [ErrorType.RequestBodyContainsNestedObject]); + }); + }); + + describe("TeamsAIValidator", () => { + it("should return true if allowAPIKeyAuth is true, allowOauth2 is true, but not auth code flow for teams ai project", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + components: { + securitySchemes: { + api_key: { + type: "apiKey", + name: "api_key", + in: "header", + }, + oauth: { + type: "oauth2", + flows: { + implicit: { + authorizationUrl: "https://example.com/api/oauth/dialog", + scopes: { + "write:pets": "modify pets in your account", + "read:pets": "read your pets", + }, + }, + }, + }, + }, + }, + paths: { + "/users": { + post: { + security: [ + { + oauth: ["read:pets"], + }, + ], + parameters: [ + { + in: "query", + required: false, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: true, + allowMultipleParameters: false, + allowOauth2: true, + projectType: ProjectType.TeamsAi, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should support multiple required parameters count larger than 5 for teams ai project", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["id1", "id2", "id3", "id4", "id5", "id6"], + properties: { + id1: { + type: "string", + }, + id2: { + type: "string", + }, + id3: { + type: "string", + }, + id4: { + type: "string", + }, + id5: { + type: "string", + }, + id6: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: true, + allowOauth2: false, + projectType: ProjectType.TeamsAi, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should return true if method is POST, and request body contains media type other than application/json for teams ai project", () => { + const method = "POST"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + post: { + parameters: [ + { + in: "query", + required: true, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + "application/xml": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.TeamsAi, + allowMethods: ["get", "post"], + }; + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + + it("should return true if method is GET, and response body contains media type other than application/json for teams ai project", () => { + const method = "GET"; + const path = "/users"; + const spec = { + servers: [ + { + url: "https://example.com", + }, + ], + paths: { + "/users": { + get: { + parameters: [ + { + in: "query", + required: true, + schema: { type: "string" }, + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + "application/xml": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + const options: ParseOptions = { + allowMissingId: true, + allowAPIKeyAuth: false, + allowMultipleParameters: false, + allowOauth2: false, + projectType: ProjectType.TeamsAi, + allowMethods: ["get", "post"], + }; + + const validator = ValidatorFactory.create(spec as any, options); + const { isValid } = validator.validateAPI(method, path); + assert.strictEqual(isValid, true); + }); + }); +});