diff --git a/packages/spec-parser/src/constants.ts b/packages/spec-parser/src/constants.ts index 3dac9ab9ac..c596f62c01 100644 --- a/packages/spec-parser/src/constants.ts +++ b/packages/spec-parser/src/constants.ts @@ -30,8 +30,8 @@ export class ConstantString { static readonly SwaggerNotSupported = "Swagger 2.0 is not supported. Please convert to OpenAPI 3.0 manually before proceeding."; - static readonly MultipleAPIKeyNotSupported = - "Multiple API keys are not supported. Please make sure that all selected APIs use the same API key."; + static readonly MultipleAuthNotSupported = + "Multiple authentication methods are unsupported. Ensure all selected APIs use identical authentication."; static readonly UnsupportedSchema = "Unsupported schema in %s %s: %s"; @@ -48,6 +48,7 @@ export class ConstantString { static readonly TextBlockType = "TextBlock"; static readonly ContainerType = "Container"; static readonly RegistrationIdPostfix = "REGISTRATION_ID"; + static readonly OAuthRegistrationIdPostFix = "OAUTH_REGISTRATION_ID"; static readonly ResponseCodeFor20X = [ "200", "201", diff --git a/packages/spec-parser/src/interfaces.ts b/packages/spec-parser/src/interfaces.ts index d0f9e880c5..92101da86b 100644 --- a/packages/spec-parser/src/interfaces.ts +++ b/packages/spec-parser/src/interfaces.ts @@ -82,7 +82,7 @@ export enum ErrorType { NoExtraAPICanBeAdded = "no-extra-api-can-be-added", ResolveServerUrlFailed = "resolve-server-url-failed", SwaggerNotSupported = "swagger-not-supported", - MultipleAPIKeyNotSupported = "multiple-api-key-not-supported", + MultipleAuthNotSupported = "multiple-auth-not-supported", ListFailed = "list-failed", listSupportedAPIInfoFailed = "list-supported-api-info-failed", @@ -198,12 +198,12 @@ export interface ParseOptions { allowAPIKeyAuth?: boolean; /** - * If true, the parser will allow multiple parameters in the spec file. + * If true, the parser will allow multiple parameters in the spec file. Teams AI project would ignore this parameters and always true */ allowMultipleParameters?: boolean; /** - * If true, the parser will allow OAuth2 authentication in the spec file. + * If true, the parser will allow OAuth2 authentication in the spec file. Currently only support OAuth2 with auth code flow. */ allowOauth2?: boolean; @@ -242,7 +242,7 @@ export interface ListAPIResult { auth?: OpenAPIV3.SecuritySchemeObject; } -export interface AuthSchema { +export interface AuthInfo { authSchema: OpenAPIV3.SecuritySchemeObject; name: string; } diff --git a/packages/spec-parser/src/manifestUpdater.ts b/packages/spec-parser/src/manifestUpdater.ts index ad3f20eb5e..d3cabba943 100644 --- a/packages/spec-parser/src/manifestUpdater.ts +++ b/packages/spec-parser/src/manifestUpdater.ts @@ -5,7 +5,7 @@ import { OpenAPIV3 } from "openapi-types"; import fs from "fs-extra"; import path from "path"; -import { ErrorType, ParseOptions, ProjectType, WarningResult } from "./interfaces"; +import { AuthInfo, ErrorType, ParseOptions, ProjectType, WarningResult } from "./interfaces"; import { Utils } from "./utils"; import { SpecParserError } from "./specParserError"; import { ConstantString } from "./constants"; @@ -200,7 +200,7 @@ export class ManifestUpdater { spec: OpenAPIV3.Document, options: ParseOptions, adaptiveCardFolder?: string, - auth?: OpenAPIV3.SecuritySchemeObject + authInfo?: AuthInfo ): Promise<[TeamsAppManifest, WarningResult[]]> { try { const originalManifest: TeamsAppManifest = await fs.readJSON(manifestPath); @@ -224,11 +224,12 @@ export class ManifestUpdater { commands: commands, }; - if (auth) { + if (authInfo) { + let auth = authInfo.authSchema; if (Utils.isAPIKeyAuth(auth)) { auth = auth as OpenAPIV3.ApiKeySecurityScheme; const safeApiSecretRegistrationId = Utils.getSafeRegistrationIdEnvName( - `${auth.name}_${ConstantString.RegistrationIdPostfix}` + `${authInfo.name}_${ConstantString.RegistrationIdPostfix}` ); (composeExtension as any).authorization = { authType: "apiSecretServiceAuth", @@ -236,11 +237,15 @@ export class ManifestUpdater { apiSecretRegistrationId: `\${{${safeApiSecretRegistrationId}}}`, }, }; - } else if (Utils.isBearerTokenAuth(auth)) { + } else if (Utils.isOAuthWithAuthCodeFlow(auth)) { + const safeOAuth2RegistrationId = Utils.getSafeRegistrationIdEnvName( + `${authInfo.name}_${ConstantString.OAuthRegistrationIdPostFix}` + ); + (composeExtension as any).authorization = { - authType: "microsoftEntra", - microsoftEntraConfiguration: { - supportsSingleSignOn: true, + authType: "oAuth2.0", + oAuthConfiguration: { + oauthConfigurationId: `\${{${safeOAuth2RegistrationId}}}`, }, }; diff --git a/packages/spec-parser/src/specParser.ts b/packages/spec-parser/src/specParser.ts index 353806d450..f59313e557 100644 --- a/packages/spec-parser/src/specParser.ts +++ b/packages/spec-parser/src/specParser.ts @@ -10,6 +10,7 @@ import fs from "fs-extra"; import path from "path"; import { APIInfo, + AuthInfo, ErrorType, GenerateResult, ListAPIResult, @@ -282,8 +283,8 @@ export class SpecParser { const newUnResolvedSpec = newSpecs[0]; const newSpec = newSpecs[1]; - const AuthSet: Set = new Set(); - let hasMultipleAPIKeyAuth = false; + const authSet: Set = new Set(); + let hasMultipleAuth = false; for (const url in newSpec.paths) { for (const method in newSpec.paths[url]) { @@ -292,19 +293,19 @@ export class SpecParser { const authArray = Utils.getAuthArray(operation.security, newSpec); if (authArray && authArray.length > 0) { - AuthSet.add(authArray[0][0].authSchema); - if (AuthSet.size > 1) { - hasMultipleAPIKeyAuth = true; + authSet.add(authArray[0][0]); + if (authSet.size > 1) { + hasMultipleAuth = true; break; } } } } - if (hasMultipleAPIKeyAuth) { + if (hasMultipleAuth && this.options.projectType !== ProjectType.TeamsAi) { throw new SpecParserError( - ConstantString.MultipleAPIKeyNotSupported, - ErrorType.MultipleAPIKeyNotSupported + ConstantString.MultipleAuthNotSupported, + ErrorType.MultipleAuthNotSupported ); } @@ -349,14 +350,14 @@ export class SpecParser { throw new SpecParserError(ConstantString.CancelledMessage, ErrorType.Cancelled); } - const auth = Array.from(AuthSet)[0]; + const authInfo = Array.from(authSet)[0]; const [updatedManifest, warnings] = await ManifestUpdater.updateManifest( manifestPath, outputSpecPath, newSpec, this.options, adaptiveCardFolder, - auth + authInfo ); await fs.outputJSON(manifestPath, updatedManifest, { spaces: 2 }); diff --git a/packages/spec-parser/src/utils.ts b/packages/spec-parser/src/utils.ts index 2b04026887..6eb3bafdcc 100644 --- a/packages/spec-parser/src/utils.ts +++ b/packages/spec-parser/src/utils.ts @@ -6,7 +6,7 @@ import { OpenAPIV3 } from "openapi-types"; import SwaggerParser from "@apidevtools/swagger-parser"; import { ConstantString } from "./constants"; import { - AuthSchema, + AuthInfo, CheckParamResult, ErrorResult, ErrorType, @@ -155,6 +155,12 @@ export class Utils { 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. @@ -180,9 +186,17 @@ export class Utils { if (pathObj) { if (options.allowMethods?.includes(method) && pathObj[method]) { const securities = pathObj[method].security; - const authArray = Utils.getAuthArray(securities, spec); - if (!Utils.isSupportedAuth(authArray, options)) { - return false; + + 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); + + if (!Utils.isSupportedAuth(authArray, options)) { + return false; + } } const operationObject = pathObj[method] as OpenAPIV3.OperationObject; @@ -194,24 +208,27 @@ export class Utils { const requestBody = operationObject.requestBody as OpenAPIV3.RequestBodyObject; const requestJsonBody = requestBody?.content["application/json"]; - const mediaTypesCount = Object.keys(requestBody?.content || {}).length; - if (mediaTypesCount > 1) { + if (!isTeamsAi && Utils.containMultipleMediaTypes(requestBody)) { return false; } - const responseJson = Utils.getResponseJson(operationObject); + const responseJson = Utils.getResponseJson(operationObject, isTeamsAi); + if (Object.keys(responseJson).length === 0) { return false; } + // Teams AI project doesn't care about request parameters/body + if (isTeamsAi) { + return true; + } + let requestBodyParamResult = { requiredNum: 0, optionalNum: 0, isValid: true, }; - const isCopilot = options.projectType === ProjectType.Copilot; - if (requestJsonBody) { const requestBodySchema = requestJsonBody.schema as OpenAPIV3.SchemaObject; @@ -267,7 +284,7 @@ export class Utils { return false; } - static isSupportedAuth(authSchemaArray: AuthSchema[][], options: ParseOptions): boolean { + static isSupportedAuth(authSchemaArray: AuthInfo[][], options: ParseOptions): boolean { if (authSchemaArray.length === 0) { return true; } @@ -289,14 +306,14 @@ export class Utils { } else if ( !options.allowAPIKeyAuth && options.allowOauth2 && - Utils.isBearerTokenAuth(auths[0].authSchema) + Utils.isOAuthWithAuthCodeFlow(auths[0].authSchema) ) { return true; } else if ( options.allowAPIKeyAuth && options.allowOauth2 && (Utils.isAPIKeyAuth(auths[0].authSchema) || - Utils.isBearerTokenAuth(auths[0].authSchema)) + Utils.isOAuthWithAuthCodeFlow(auths[0].authSchema)) ) { return true; } @@ -311,25 +328,25 @@ export class Utils { return authSchema.type === "apiKey"; } - static isBearerTokenAuth(authSchema: OpenAPIV3.SecuritySchemeObject): boolean { - return ( - authSchema.type === "oauth2" || - authSchema.type === "openIdConnect" || - (authSchema.type === "http" && authSchema.scheme === "bearer") - ); + static isOAuthWithAuthCodeFlow(authSchema: OpenAPIV3.SecuritySchemeObject): boolean { + if (authSchema.type === "oauth2" && authSchema.flows && authSchema.flows.authorizationCode) { + return true; + } + + return false; } static getAuthArray( securities: OpenAPIV3.SecurityRequirementObject[] | undefined, spec: OpenAPIV3.Document - ): AuthSchema[][] { - const result: AuthSchema[][] = []; + ): AuthInfo[][] { + const result: AuthInfo[][] = []; const securitySchemas = spec.components?.securitySchemes; if (securities && securitySchemas) { for (let i = 0; i < securities.length; i++) { const security = securities[i]; - const authArray: AuthSchema[] = []; + const authArray: AuthInfo[] = []; for (const name in security) { const auth = securitySchemas[name] as OpenAPIV3.SecuritySchemeObject; authArray.push({ @@ -354,21 +371,21 @@ export class Utils { } static getResponseJson( - operationObject: OpenAPIV3.OperationObject | undefined + operationObject: OpenAPIV3.OperationObject | undefined, + isTeamsAiProject = false ): OpenAPIV3.MediaTypeObject { let json: OpenAPIV3.MediaTypeObject = {}; for (const code of ConstantString.ResponseCodeFor20X) { const responseObject = operationObject?.responses?.[code] as OpenAPIV3.ResponseObject; - const mediaTypesCount = Object.keys(responseObject?.content || {}).length; - if (mediaTypesCount > 1) { - return {}; - } - if (responseObject?.content?.["application/json"]) { json = responseObject.content["application/json"]; - break; + if (!isTeamsAiProject && Utils.containMultipleMediaTypes(responseObject)) { + json = {}; + } else { + break; + } } } @@ -674,7 +691,6 @@ export class Utils { for (const path in paths) { const methods = paths[path]; for (const method in methods) { - // For developer preview, only support GET operation with only 1 parameter without auth if (Utils.isSupportedApi(method, path, spec, options)) { const operationObject = (methods as any)[method] as OpenAPIV3.OperationObject; result[`${method.toUpperCase()} ${path}`] = operationObject; diff --git a/packages/spec-parser/test/adaptiveCardGenerator.test.ts b/packages/spec-parser/test/adaptiveCardGenerator.test.ts index dd8c33feaf..7a62baf0e7 100644 --- a/packages/spec-parser/test/adaptiveCardGenerator.test.ts +++ b/packages/spec-parser/test/adaptiveCardGenerator.test.ts @@ -351,27 +351,27 @@ describe("adaptiveCardGenerator", () => { expect(actual).to.deep.equal(expected); expect(jsonPath).to.equal("$"); }); - }); - it("should generate a card if schema is empty", () => { - const schema = {}; - const expected = { - type: "AdaptiveCard", - $schema: "http://adaptivecards.io/schemas/adaptive-card.json", - version: "1.5", - body: [ - { - type: "TextBlock", - text: "success", - wrap: true, - }, - ], - }; + it("should generate a card if schema is empty", () => { + const schema = {}; + const expected = { + type: "AdaptiveCard", + $schema: "http://adaptivecards.io/schemas/adaptive-card.json", + version: "1.5", + body: [ + { + type: "TextBlock", + text: "success", + wrap: true, + }, + ], + }; - const [actual, jsonPath] = AdaptiveCardGenerator.generateAdaptiveCard(schema); + const [actual, jsonPath] = AdaptiveCardGenerator.generateAdaptiveCard(schema); - expect(actual).to.deep.equal(expected); - expect(jsonPath).to.equal("$"); + expect(actual).to.deep.equal(expected); + expect(jsonPath).to.equal("$"); + }); }); describe("generateCardFromResponse", () => { diff --git a/packages/spec-parser/test/manifestUpdater.test.ts b/packages/spec-parser/test/manifestUpdater.test.ts index eaf55e46af..e26a5979f3 100644 --- a/packages/spec-parser/test/manifestUpdater.test.ts +++ b/packages/spec-parser/test/manifestUpdater.test.ts @@ -8,7 +8,7 @@ import os from "os"; import "mocha"; import { ManifestUpdater } from "../src/manifestUpdater"; import { SpecParserError } from "../src/specParserError"; -import { ErrorType, ParseOptions, ProjectType, WarningType } from "../src/interfaces"; +import { AuthInfo, ErrorType, ParseOptions, ProjectType, WarningType } from "../src/interfaces"; import { ConstantString } from "../src/constants"; import { Utils } from "../src/utils"; import { PluginManifestSchema } from "@microsoft/teams-manifest"; @@ -1143,7 +1143,7 @@ describe("manifestUpdater", () => { authorization: { authType: "apiSecretServiceAuth", apiSecretServiceAuthConfiguration: { - apiSecretRegistrationId: "${{API_KEY_NAME_REGISTRATION_ID}}", + apiSecretRegistrationId: "${{API_KEY_AUTH_REGISTRATION_ID}}", }, }, commands: [ @@ -1172,10 +1172,13 @@ describe("manifestUpdater", () => { ], }; const readJSONStub = sinon.stub(fs, "readJSON").resolves(originalManifest); - const apiKeyAuth = { - type: "apiKey" as const, - name: "api_key_name", - in: "header", + const apiKeyAuth: AuthInfo = { + authSchema: { + type: "apiKey" as const, + name: "api_key_name", + in: "header", + }, + name: "api_key_auth", }; const options: ParseOptions = { allowMultipleParameters: false, @@ -1196,7 +1199,7 @@ describe("manifestUpdater", () => { expect(warnings).to.deep.equal([]); }); - it("should contain auth property in manifest if pass the sso auth", async () => { + it("should contain auth property in manifest if pass the oauth2 with auth code flow", async () => { const manifestPath = "/path/to/your/manifest.json"; const outputSpecPath = "/path/to/your/spec/outputSpec.yaml"; const adaptiveCardFolder = "/path/to/your/adaptiveCards"; @@ -1214,9 +1217,9 @@ describe("manifestUpdater", () => { composeExtensionType: "apiBased", apiSpecificationFile: "spec/outputSpec.yaml", authorization: { - authType: "microsoftEntra", - microsoftEntraConfiguration: { - supportsSingleSignOn: true, + authType: "oAuth2.0", + oAuthConfiguration: { + oauthConfigurationId: "${{OAUTH_AUTH_OAUTH_REGISTRATION_ID}}", }, }, commands: [ @@ -1249,17 +1252,22 @@ describe("manifestUpdater", () => { }, }; const readJSONStub = sinon.stub(fs, "readJSON").resolves(originalManifest); - const oauth2 = { - type: "oauth2" as const, - flows: { - implicit: { - authorizationUrl: "https://example.com/api/oauth/dialog", - scopes: { - "write:pets": "modify pets in your account", - "read:pets": "read your pets", + const oauth2: AuthInfo = { + authSchema: { + 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", + }, }, }, }, + name: "oauth_auth", }; const options: ParseOptions = { allowMultipleParameters: false, @@ -1322,9 +1330,12 @@ describe("manifestUpdater", () => { ], }; const readJSONStub = sinon.stub(fs, "readJSON").resolves(originalManifest); - const basicAuth = { - type: "http" as const, - scheme: "basic", + const basicAuth: AuthInfo = { + authSchema: { + type: "http" as const, + scheme: "basic", + }, + name: "basic_auth", }; const options: ParseOptions = { allowMultipleParameters: false, @@ -1364,7 +1375,7 @@ describe("manifestUpdater", () => { authorization: { authType: "apiSecretServiceAuth", apiSecretServiceAuthConfiguration: { - apiSecretRegistrationId: "${{PREFIX__API_KEY_NAME_REGISTRATION_ID}}", + apiSecretRegistrationId: "${{PREFIX__API_KEY_AUTH_REGISTRATION_ID}}", }, }, commands: [ @@ -1393,10 +1404,13 @@ describe("manifestUpdater", () => { ], }; const readJSONStub = sinon.stub(fs, "readJSON").resolves(originalManifest); - const apiKeyAuth = { - type: "apiKey" as const, - name: "*api-key_name", - in: "header", + const apiKeyAuth: AuthInfo = { + authSchema: { + type: "apiKey" as const, + name: "key_name", + in: "header", + }, + name: "*api-key_auth", }; const options: ParseOptions = { diff --git a/packages/spec-parser/test/specParser.test.ts b/packages/spec-parser/test/specParser.test.ts index 74bcf6cbb1..a4e14ca72e 100644 --- a/packages/spec-parser/test/specParser.test.ts +++ b/packages/spec-parser/test/specParser.test.ts @@ -1265,11 +1265,128 @@ describe("SpecParser", () => { ); expect.fail("Expected generate to throw a SpecParserError"); } catch (err) { - expect((err as SpecParserError).message).contain(ConstantString.MultipleAPIKeyNotSupported); - expect((err as SpecParserError).errorType).to.equal(ErrorType.MultipleAPIKeyNotSupported); + expect((err as SpecParserError).message).contain(ConstantString.MultipleAuthNotSupported); + expect((err as SpecParserError).errorType).to.equal(ErrorType.MultipleAuthNotSupported); } }); + it("should work if contain multiple API key in spec when project Type is teams ai", async () => { + const specParser = new SpecParser("path/to/spec.yaml", { + allowAPIKeyAuth: true, + projectType: ProjectType.TeamsAi, + }); + const spec = { + openapi: "3.0.0", + components: { + securitySchemes: { + api_key: { + type: "apiKey", + name: "api_key", + in: "header", + }, + api_key2: { + type: "apiKey", + name: "api_key2", + in: "header", + }, + }, + }, + paths: { + "/hello": { + get: { + operationId: "getHello", + security: [ + { + api_key: [], + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + post: { + security: [ + { + api_key2: [], + }, + ], + operationId: "postHello", + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + const parseStub = sinon.stub(specParser.parser, "parse").resolves(spec as any); + const dereferenceStub = sinon.stub(specParser.parser, "dereference").resolves(spec as any); + const specFilterStub = sinon.stub(SpecFilter, "specFilter").returns({} as any); + const outputFileStub = sinon.stub(fs, "outputFile").resolves(); + const outputJSONStub = sinon.stub(fs, "outputJSON").resolves(); + const JsyamlSpy = sinon.spy(jsyaml, "dump"); + + const manifestUpdaterStub = sinon + .stub(ManifestUpdater, "updateManifest") + .resolves([{}, []] as any); + const generateAdaptiveCardStub = sinon + .stub(AdaptiveCardGenerator, "generateAdaptiveCard") + .returns([ + { + type: "AdaptiveCard", + $schema: "http://adaptivecards.io/schemas/adaptive-card.json", + version: "1.5", + body: [ + { + type: "TextBlock", + text: "id: ${id}", + wrap: true, + }, + ], + }, + "$", + ]); + + const filter = ["get /hello", "post /hello"]; + + const outputSpecPath = "path/to/output.yaml"; + const result = await specParser.generate("path/to/manifest.json", filter, outputSpecPath); + + expect(result.allSuccess).to.be.true; + expect(JsyamlSpy.calledOnce).to.be.true; + expect(specFilterStub.calledOnce).to.be.true; + expect(outputFileStub.calledOnce).to.be.true; + expect(manifestUpdaterStub.calledOnce).to.be.true; + expect(outputFileStub.firstCall.args[0]).to.equal(outputSpecPath); + expect(outputJSONStub.calledOnce).to.be.true; + expect(generateAdaptiveCardStub.notCalled).to.be.true; + }); + it("should contain warnings if generate adaptive card failed", async () => { const specParser = new SpecParser("path/to/spec.yaml"); const spec = { diff --git a/packages/spec-parser/test/utils.test.ts b/packages/spec-parser/test/utils.test.ts index 0658b635ba..91b6273657 100644 --- a/packages/spec-parser/test/utils.test.ts +++ b/packages/spec-parser/test/utils.test.ts @@ -478,8 +478,10 @@ describe("utils", () => { oauth: { type: "oauth2", flows: { - implicit: { + 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", @@ -552,7 +554,7 @@ describe("utils", () => { assert.strictEqual(result, true); }); - it("should return false if allowAPIKeyAuth is true, allowOauth2 is false, but contains aad auth", () => { + it("should return false if allowAPIKeyAuth is true, allowOauth2 is false, but contain oauth", () => { const method = "POST"; const path = "/users"; const spec = { @@ -566,8 +568,10 @@ describe("utils", () => { oauth: { type: "oauth2", flows: { - implicit: { + 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", @@ -640,7 +644,7 @@ describe("utils", () => { assert.strictEqual(result, false); }); - it("should return false if allowAPIKeyAuth is true, allowOauth2 is false, and contains aad auth", () => { + it("should return false if allowAPIKeyAuth is true, allowOauth2 is true, but not auth code flow", () => { const method = "POST"; const path = "/users"; const spec = { @@ -725,6 +729,95 @@ describe("utils", () => { allowMethods: ["get", "post"], }; + const result = Utils.isSupportedApi(method, path, spec as any, options); + assert.strictEqual(result, false); + }); + + 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 = { + 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 result = Utils.isSupportedApi(method, path, spec as any, options); assert.strictEqual(result, true); }); @@ -1051,6 +1144,77 @@ describe("utils", () => { assert.strictEqual(result, false); }); + it("should support multiple required parameters count larger than 5 for teams ai project", () => { + const method = "POST"; + const path = "/users"; + const spec = { + 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 result = Utils.isSupportedApi(method, path, spec as any, options); + assert.strictEqual(result, true); + }); + it("should not support multiple required parameters count larger than 5 for copilot", () => { const method = "POST"; const path = "/users"; @@ -2004,6 +2168,79 @@ describe("utils", () => { assert.strictEqual(result, false); }); + 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 = { + 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 result = Utils.isSupportedApi(method, path, spec as any, options); + assert.strictEqual(result, true); + }); + it("should return false if method is POST, and request body schema is not object", () => { const method = "POST"; const path = "/users"; @@ -2111,6 +2348,64 @@ describe("utils", () => { const result = Utils.isSupportedApi(method, path, spec as any, options); assert.strictEqual(result, false); }); + + 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 = { + 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 result = Utils.isSupportedApi(method, path, spec as any, options); + assert.strictEqual(result, true); + }); }); describe("getUrlProtocol", () => { @@ -2705,6 +3000,71 @@ describe("utils", () => { }); }); + it("should return empty JSON response for status code 200 with multiple media type", () => { + 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 = Utils.getResponseJson(operationObject); + expect(json).to.deep.equal({}); + }); + + 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 = Utils.getResponseJson(operationObject, true); + expect(json).to.deep.equal({ + schema: { + type: "object", + properties: { + message: { type: "string" }, + }, + }, + }); + }); + it("should return the JSON response for status code 201", () => { const operationObject = { responses: {