From a954384ad70e262467940ab5607bddfc4e5b24f6 Mon Sep 17 00:00:00 2001 From: AkhillaIlla <36493984+AkhilaIlla@users.noreply.github.com> Date: Wed, 18 Sep 2024 12:20:28 -0500 Subject: [PATCH] Add QueryParametersInCollectionGet linter rule (#745) * Add QueryParametersInCollectionGet linter rule * Update logic to figure out list op paths * Add staging only * Address comments --------- Co-authored-by: akhilailla --- docs/query-parameters-in-collection-get.md | 78 ++++ docs/rules.md | 6 + .../rulesets/generated/spectral/az-arm.js | 323 ++++++++------ .../src/native/utilities/arm-helper.ts | 4 +- .../src/native/utilities/rules-helper.ts | 2 +- packages/rulesets/src/spectral/az-arm.ts | 17 +- .../query-parameters-in-collection-get.ts | 33 ++ .../functions/xms-pageable-for-list-calls.ts | 4 +- ...query-parameters-in-collection-get.test.ts | 406 ++++++++++++++++++ 9 files changed, 725 insertions(+), 148 deletions(-) create mode 100644 docs/query-parameters-in-collection-get.md create mode 100644 packages/rulesets/src/spectral/functions/query-parameters-in-collection-get.ts create mode 100644 packages/rulesets/src/spectral/test/query-parameters-in-collection-get.test.ts diff --git a/docs/query-parameters-in-collection-get.md b/docs/query-parameters-in-collection-get.md new file mode 100644 index 00000000..c8a65977 --- /dev/null +++ b/docs/query-parameters-in-collection-get.md @@ -0,0 +1,78 @@ +# QueryParametersInCollectionGet + +## Category + +ARM Error + +## Applies to + +ARM OpenAPI(swagger) specs + +## Related ARM Guideline Code + +- RPC-Get-V1-15 + +## Description + +Collection Get's/List operations MUST not have query parameters other than api-version & OData filter. + +## How to fix the violation + +Ensure that collection GET/List operations do not include any query parameters other than api-version and the OData $filter. + +## Good Examples + +```json + "Microsoft.Music/Songs": { + "get": { + "operationId": "Foo_Update", + "description": "Test Description", + "parameters": [ + { + "name": "api-version", + "in": "query" + }, + ], + }, + }, +``` + +```json + "Microsoft.Music/Songs": { + "get": { + "operationId": "Foo_Update", + "description": "Test Description", + "parameters": [ + { + "name": "api-version", + "in": "query" + }, + { + "name": "$filter", + "in": "query", + "required": false, + "type": "string", + }, + ], + }, + }, +``` + +## Bad Examples + +```json + "Microsoft.Music/Songs": { + "get": { + "operationId": "Foo_Update", + "description": "Test Description", + "parameters": [ + { + "name": "name", + "in": "query", + "required": false, + "type": "string", + }, + ], + }, + }, +``` diff --git a/docs/rules.md b/docs/rules.md index b57c9937..2702a001 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -1040,6 +1040,12 @@ Synchronous and long-running PUT operations must have responses with 200, 201 an Please refer to [put-response-codes.md](./put-response-codes.md) for details. +### QueryParametersInCollectionGet + +Collection Get's/List operations MUST not have query parameters other than api-version & OData filter. + +Please refer to [query-parameters-in-collection-get.md](./query-parameters-in-collection-get.md) for details. + ### RepeatedPathInfo Information in the URI must not be repeated in the request body (i.e. subscription ID, resource group name, resource name). diff --git a/packages/rulesets/generated/spectral/az-arm.js b/packages/rulesets/generated/spectral/az-arm.js index 9156d5f1..b384155f 100644 --- a/packages/rulesets/generated/spectral/az-arm.js +++ b/packages/rulesets/generated/spectral/az-arm.js @@ -2539,6 +2539,173 @@ const PutResponseCodes = (putOp, _opts, ctx) => { return errors; }; +const parseJsonRef = (ref) => { + return ref.split("#"); +}; + +var Workspace; +(function (Workspace) { + function getSchemaByName(modelName, swaggerPath, inventory) { + var _a; + const root = inventory.getDocuments(swaggerPath); + if (!root || !modelName) { + return undefined; + } + return (_a = root === null || root === void 0 ? void 0 : root.definitions) === null || _a === void 0 ? void 0 : _a[modelName]; + } + Workspace.getSchemaByName = getSchemaByName; + function jsonPath(paths, document) { + let root = document; + for (const seg of paths) { + root = root[seg]; + if (!root) { + break; + } + } + return root; + } + Workspace.jsonPath = jsonPath; + function resolveRef(schema, inventory) { + function getRef(refValue, swaggerPath) { + const root = inventory.getDocuments(swaggerPath); + if (refValue.startsWith("/")) { + refValue = refValue.substring(1); + } + const segments = refValue.split("/"); + return jsonPath(segments, root); + } + let currentSpec = schema.file; + let currentSchema = schema.value; + while (currentSchema && currentSchema.$ref) { + const slices = parseJsonRef(currentSchema.$ref); + currentSpec = slices[0] || currentSpec; + currentSchema = getRef(slices[1], currentSpec); + } + return { + file: currentSpec, + value: currentSchema, + }; + } + Workspace.resolveRef = resolveRef; + function getProperty(schema, propertyName, inventory) { + let source = schema; + const visited = new Set(); + while (source.value && source.value.$ref && !visited.has(source.value)) { + visited.add(source.value); + source = resolveRef(source, inventory); + } + if (!source || !source.value) { + return undefined; + } + const model = source.value; + if (model.properties && model.properties[propertyName]) { + return resolveRef(createEnhancedSchema(model.properties[propertyName], source.file), inventory); + } + if (model.allOf) { + for (const element of model.allOf) { + const property = getProperty({ file: source.file, value: element }, propertyName, inventory); + if (property) { + return property; + } + } + } + return undefined; + } + Workspace.getProperty = getProperty; + function getProperties(schema, inventory) { + let source = schema; + const visited = new Set(); + while (source.value && source.value.$ref && !visited.has(source.value)) { + visited.add(source.value); + source = resolveRef(source, inventory); + } + if (!source || !source.value) { + return []; + } + let result = {}; + const model = source.value; + if (model.properties) { + for (const propertyName of Object.keys(model.properties)) { + result[propertyName] = createEnhancedSchema(model.properties[propertyName], source.file); + } + } + if (model.allOf) { + for (const element of model.allOf) { + const properties = getProperties({ file: source.file, value: element }, inventory); + if (properties) { + result = { ...properties, ...result }; + } + } + } + return result; + } + Workspace.getProperties = getProperties; + function getAttribute(schema, attributeName, inventory) { + let source = schema; + const visited = new Set(); + while (source.value && source.value.$ref && !visited.has(source.value)) { + visited.add(source.value); + source = resolveRef(source, inventory); + } + if (!source) { + return undefined; + } + const attribute = source.value[attributeName]; + if (attribute) { + return createEnhancedSchema(attribute, source.file); + } + return undefined; + } + Workspace.getAttribute = getAttribute; + function createEnhancedSchema(schema, specPath) { + return { + file: specPath, + value: schema, + }; + } + Workspace.createEnhancedSchema = createEnhancedSchema; +})(Workspace || (Workspace = {})); + +require("string.prototype.matchall"); +function isListOperationPath(path) { + if (path.includes(".")) { + const splitNamespace = path.split("."); + if (path.includes("/")) { + const segments = splitNamespace[splitNamespace.length - 1].split("/"); + if (segments.length % 2 == 0) { + return true; + } + } + } + return false; +} + +const queryParametersInCollectionGet = (pathItem, _opts, ctx) => { + if (pathItem === null || typeof pathItem !== "object") { + return []; + } + const path = ctx.path || []; + const uris = Object.keys(pathItem); + if (uris.length < 1) { + return []; + } + const GET = "get"; + const errors = []; + for (const uri of uris) { + if (pathItem[uri][GET] && isListOperationPath(uri)) { + const params = pathItem[uri][GET]["parameters"]; + const queryParams = params === null || params === void 0 ? void 0 : params.filter((param) => param.in === "query" && param.name !== "api-version" && param.name !== "$filter"); + queryParams === null || queryParams === void 0 ? void 0 : queryParams.forEach((param) => { + errors.push({ + message: `Query parameter ${param.name} should be removed. Collection Get's/List operation MUST not have query parameters other than api version & OData filter.`, + path: [path, uri, GET, "parameters"], + }); + }); + } + } + return errors; +}; + const requestBodyMustExistForPutPatch = (putPatchOperationParameters, _opts, ctx) => { const errors = []; const path = ctx.path; @@ -2988,154 +3155,13 @@ const verifyXMSLongRunningOperationProperty = (pathItem, _opts, paths) => { return; }; -const parseJsonRef = (ref) => { - return ref.split("#"); -}; - -var Workspace; -(function (Workspace) { - function getSchemaByName(modelName, swaggerPath, inventory) { - var _a; - const root = inventory.getDocuments(swaggerPath); - if (!root || !modelName) { - return undefined; - } - return (_a = root === null || root === void 0 ? void 0 : root.definitions) === null || _a === void 0 ? void 0 : _a[modelName]; - } - Workspace.getSchemaByName = getSchemaByName; - function jsonPath(paths, document) { - let root = document; - for (const seg of paths) { - root = root[seg]; - if (!root) { - break; - } - } - return root; - } - Workspace.jsonPath = jsonPath; - function resolveRef(schema, inventory) { - function getRef(refValue, swaggerPath) { - const root = inventory.getDocuments(swaggerPath); - if (refValue.startsWith("/")) { - refValue = refValue.substring(1); - } - const segments = refValue.split("/"); - return jsonPath(segments, root); - } - let currentSpec = schema.file; - let currentSchema = schema.value; - while (currentSchema && currentSchema.$ref) { - const slices = parseJsonRef(currentSchema.$ref); - currentSpec = slices[0] || currentSpec; - currentSchema = getRef(slices[1], currentSpec); - } - return { - file: currentSpec, - value: currentSchema, - }; - } - Workspace.resolveRef = resolveRef; - function getProperty(schema, propertyName, inventory) { - let source = schema; - const visited = new Set(); - while (source.value && source.value.$ref && !visited.has(source.value)) { - visited.add(source.value); - source = resolveRef(source, inventory); - } - if (!source || !source.value) { - return undefined; - } - const model = source.value; - if (model.properties && model.properties[propertyName]) { - return resolveRef(createEnhancedSchema(model.properties[propertyName], source.file), inventory); - } - if (model.allOf) { - for (const element of model.allOf) { - const property = getProperty({ file: source.file, value: element }, propertyName, inventory); - if (property) { - return property; - } - } - } - return undefined; - } - Workspace.getProperty = getProperty; - function getProperties(schema, inventory) { - let source = schema; - const visited = new Set(); - while (source.value && source.value.$ref && !visited.has(source.value)) { - visited.add(source.value); - source = resolveRef(source, inventory); - } - if (!source || !source.value) { - return []; - } - let result = {}; - const model = source.value; - if (model.properties) { - for (const propertyName of Object.keys(model.properties)) { - result[propertyName] = createEnhancedSchema(model.properties[propertyName], source.file); - } - } - if (model.allOf) { - for (const element of model.allOf) { - const properties = getProperties({ file: source.file, value: element }, inventory); - if (properties) { - result = { ...properties, ...result }; - } - } - } - return result; - } - Workspace.getProperties = getProperties; - function getAttribute(schema, attributeName, inventory) { - let source = schema; - const visited = new Set(); - while (source.value && source.value.$ref && !visited.has(source.value)) { - visited.add(source.value); - source = resolveRef(source, inventory); - } - if (!source) { - return undefined; - } - const attribute = source.value[attributeName]; - if (attribute) { - return createEnhancedSchema(attribute, source.file); - } - return undefined; - } - Workspace.getAttribute = getAttribute; - function createEnhancedSchema(schema, specPath) { - return { - file: specPath, - value: schema, - }; - } - Workspace.createEnhancedSchema = createEnhancedSchema; -})(Workspace || (Workspace = {})); - -require("string.prototype.matchall"); -function isListOperation(path) { - if (path.includes(".")) { - const splitNamespace = path.split("."); - if (path.includes("/")) { - const segments = splitNamespace[splitNamespace.length - 1].split("/"); - if (segments.length % 2 == 0) { - return true; - } - } - } - return false; -} - const xmsPageableForListCalls = (swaggerObj, _opts, paths) => { if (swaggerObj === null) { return []; } const path = paths.path || []; if (!isNull(path[1])) { - if (!isListOperation(path[1].toString())) { + if (!isListOperationPath(path[1].toString())) { return; } } @@ -3459,6 +3485,19 @@ const ruleset = { function: falsy, }, }, + QueryParametersInCollectionGet: { + rpcGuidelineCode: "RPC-Get-V1-15", + description: "Collection Get's/List operations MUST not have query parameters other than api-version & OData filter.", + severity: "error", + message: "{{error}}", + stagingOnly: true, + resolved: true, + formats: [oas2], + given: "$[paths,'x-ms-paths']", + then: { + function: queryParametersInCollectionGet, + }, + }, PatchPropertiesCorrespondToPutProperties: { rpcGuidelineCode: "RPC-Patch-V1-01", description: "PATCH request body must only contain properties present in the corresponding PUT request body, and must contain at least one property.", diff --git a/packages/rulesets/src/native/utilities/arm-helper.ts b/packages/rulesets/src/native/utilities/arm-helper.ts index 58ef6e18..2adbd07a 100644 --- a/packages/rulesets/src/native/utilities/arm-helper.ts +++ b/packages/rulesets/src/native/utilities/arm-helper.ts @@ -5,7 +5,7 @@ import { ISwaggerInventory, parseJsonRef } from "@microsoft.azure/openapi-validator-core" import _ from "lodash" import { nodes } from "./jsonpath" -import { isListOperation } from "./rules-helper" +import { isListOperationPath } from "./rules-helper" import { SwaggerHelper } from "./swagger-helper" import { SwaggerWalker } from "./swagger-walker" import { Workspace } from "./swagger-workspace" @@ -298,7 +298,7 @@ export class ArmHelper { const resWithPutOrPatch = includeGet ? localResourceModels.filter((re) => re.operations.some( - (op) => (op.httpMethod === "get" && !isListOperation(op.apiPath)) || op.httpMethod === "put" || op.httpMethod == "patch", + (op) => (op.httpMethod === "get" && !isListOperationPath(op.apiPath)) || op.httpMethod === "put" || op.httpMethod == "patch", ), ) : localResourceModels.filter((re) => re.operations.some((op) => op.httpMethod === "put" || op.httpMethod == "patch")) diff --git a/packages/rulesets/src/native/utilities/rules-helper.ts b/packages/rulesets/src/native/utilities/rules-helper.ts index bca56456..79b14937 100644 --- a/packages/rulesets/src/native/utilities/rules-helper.ts +++ b/packages/rulesets/src/native/utilities/rules-helper.ts @@ -137,7 +137,7 @@ export function stringify(path: string[]) { return JSONPath.toPathString(pathWithRoot) } -export function isListOperation(path: string) { +export function isListOperationPath(path: string) { if (path.includes(".")) { // Get the portion of the api path to the right of the provider namespace by splitting the path by '.' and taking the last element const splitNamespace = path.split(".") diff --git a/packages/rulesets/src/spectral/az-arm.ts b/packages/rulesets/src/spectral/az-arm.ts index 3f62bf20..80291ac6 100644 --- a/packages/rulesets/src/spectral/az-arm.ts +++ b/packages/rulesets/src/spectral/az-arm.ts @@ -40,6 +40,7 @@ import { provisioningStateMustBeReadOnly } from "./functions/provisioning-state- import putGetPatchSchema from "./functions/put-get-patch-schema" import { putRequestResponseScheme } from "./functions/put-request-response-scheme" import { PutResponseCodes } from "./functions/put-response-codes" +import { queryParametersInCollectionGet } from "./functions/query-parameters-in-collection-get" import { requestBodyMustExistForPutPatch } from "./functions/request-body-must-exist-for-put-patch" import { reservedResourceNamesModelAsEnum } from "./functions/reserved-resource-names-model-as-enum" import resourceNameRestriction from "./functions/resource-name-restriction" @@ -449,6 +450,21 @@ const ruleset: any = { }, }, + // RPC Code: RPC-Get-V1-15 + QueryParametersInCollectionGet: { + rpcGuidelineCode: "RPC-Get-V1-15", + description: "Collection Get's/List operations MUST not have query parameters other than api-version & OData filter.", + severity: "error", + message: "{{error}}", + stagingOnly: true, + resolved: true, + formats: [oas2], + given: "$[paths,'x-ms-paths']", + then: { + function: queryParametersInCollectionGet, + }, + }, + /// /// ARM RPC rules for Patch patterns /// @@ -608,7 +624,6 @@ const ruleset: any = { then: { function: pattern, functionOptions: { - // match: ".*/providers/[\\w\\.]+(?:/\\w+/(default|{\\w+}))*/\\w+$", match: ".*/providers/\\w+.\\w+(/\\w+/(default|{\\w+}))+$", }, }, diff --git a/packages/rulesets/src/spectral/functions/query-parameters-in-collection-get.ts b/packages/rulesets/src/spectral/functions/query-parameters-in-collection-get.ts new file mode 100644 index 00000000..8d085734 --- /dev/null +++ b/packages/rulesets/src/spectral/functions/query-parameters-in-collection-get.ts @@ -0,0 +1,33 @@ +import { isListOperationPath } from "../../native/utilities/rules-helper" + +export const queryParametersInCollectionGet = (pathItem: any, _opts: any, ctx: any) => { + if (pathItem === null || typeof pathItem !== "object") { + return [] + } + + const path = ctx.path || [] + const uris = Object.keys(pathItem) + if (uris.length < 1) { + return [] + } + const GET = "get" + const errors: any[] = [] + + for (const uri of uris) { + //check if GET op is defined & the GET op is a collection get/list call + if (pathItem[uri][GET] && isListOperationPath(uri)) { + const params = pathItem[uri][GET]["parameters"] + const queryParams = params?.filter( + (param: { in: string; name: string }) => param.in === "query" && param.name !== "api-version" && param.name !== "$filter", + ) + queryParams?.forEach((param: { name: any }) => { + errors.push({ + message: `Query parameter ${param.name} should be removed. Collection Get's/List operation MUST not have query parameters other than api version & OData filter.`, + path: [path, uri, GET, "parameters"], + }) + }) + } + } + + return errors +} diff --git a/packages/rulesets/src/spectral/functions/xms-pageable-for-list-calls.ts b/packages/rulesets/src/spectral/functions/xms-pageable-for-list-calls.ts index 58756762..da13c2be 100644 --- a/packages/rulesets/src/spectral/functions/xms-pageable-for-list-calls.ts +++ b/packages/rulesets/src/spectral/functions/xms-pageable-for-list-calls.ts @@ -1,5 +1,5 @@ import { isNull } from "lodash" -import { isListOperation } from "../../native/utilities/rules-helper" +import { isListOperationPath } from "../../native/utilities/rules-helper" const xmsPageableForListCalls = (swaggerObj: any, _opts: any, paths: any) => { if (swaggerObj === null) { @@ -11,7 +11,7 @@ const xmsPageableForListCalls = (swaggerObj: any, _opts: any, paths: any) => { // 1 - /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Music/Configurations // 2 - get if (!isNull(path[1])) { - if (!isListOperation(path[1].toString())) { + if (!isListOperationPath(path[1].toString())) { return } } diff --git a/packages/rulesets/src/spectral/test/query-parameters-in-collection-get.test.ts b/packages/rulesets/src/spectral/test/query-parameters-in-collection-get.test.ts new file mode 100644 index 00000000..4eedf037 --- /dev/null +++ b/packages/rulesets/src/spectral/test/query-parameters-in-collection-get.test.ts @@ -0,0 +1,406 @@ +import { Spectral } from "@stoplight/spectral-core" +import linterForRule from "./utils" + +let linter: Spectral + +beforeAll(async () => { + linter = await linterForRule("QueryParametersInCollectionGet") + return linter +}) + +test("QueryParametersInCollectionGet should find errors for top level path", () => { + const myOpenApiDocument = { + swagger: "2.0", + paths: { + "/providers/Microsoft.Music/songs": { + get: { + operationId: "foo_post", + parameters: [ + { + $ref: "#/parameters/QuotaBucketNameParameter", + }, + ], + responses: { + 200: { + description: "Success", + }, + }, + }, + }, + }, + parameters: { + LoadTestNameParameter: { + in: "path", + name: "loadTestName", + description: "Load Test name.", + required: true, + "x-ms-parameter-location": "method", + type: "string", + }, + QuotaBucketNameParameter: { + in: "query", + name: "quotaBucketName", + description: "Quota Bucket name.", + required: true, + "x-ms-parameter-location": "method", + type: "string", + }, + }, + } + return linter.run(myOpenApiDocument).then((results) => { + expect(results.length).toBe(1) + expect(results[0].path.join(".")).toBe("paths./providers/Microsoft.Music/songs.get.parameters") + expect(results[0].message).toContain( + "Query parameter quotaBucketName should be removed. Collection Get's/List operation MUST not have query parameters other than api version & OData filter.", + ) + }) +}) + +test("QueryParametersInCollectionGet should find errors for nested path", () => { + const myOpenApiDocument = { + swagger: "2.0", + paths: { + "/providers/Microsoft.Music/songs/{unstoppable}/artists": { + get: { + operationId: "foo_post", + parameters: [ + { + $ref: "src/spectral/test/resources/types.json#/parameters/SubscriptionIdParameter", + }, + { + $ref: "#/parameters/QuotaBucketNameParameter", + }, + ], + responses: { + 200: { + description: "Success", + }, + }, + }, + }, + }, + parameters: { + LoadTestNameParameter: { + in: "path", + name: "loadTestName", + description: "Load Test name.", + required: true, + "x-ms-parameter-location": "method", + type: "string", + }, + QuotaBucketNameParameter: { + in: "query", + name: "quotaBucketName", + description: "Quota Bucket name.", + required: true, + "x-ms-parameter-location": "method", + type: "string", + }, + }, + } + return linter.run(myOpenApiDocument).then((results) => { + expect(results.length).toBe(1) + expect(results[0].path.join(".")).toBe("paths./providers/Microsoft.Music/songs/{unstoppable}/artists.get.parameters") + expect(results[0].message).toContain( + "Query parameter quotaBucketName should be removed. Collection Get's/List operation MUST not have query parameters other than api version & OData filter.", + ) + }) +}) + +test("QueryParametersInCollectionGet should find errors for more than one query param", () => { + const myOpenApiDocument = { + swagger: "2.0", + paths: { + "/providers/Microsoft.Music/songs/{unstoppable}/artists": { + get: { + operationId: "foo_post", + parameters: [ + { + $ref: "#/parameters/LoadTestNameParameter", + }, + { + $ref: "#/parameters/QuotaBucketNameParameter", + }, + ], + responses: { + 200: { + description: "Success", + }, + }, + }, + }, + }, + parameters: { + LoadTestNameParameter: { + in: "query", + name: "loadTestName", + description: "Load Test name.", + required: true, + "x-ms-parameter-location": "method", + type: "string", + }, + QuotaBucketNameParameter: { + in: "query", + name: "quotaBucketName", + description: "Quota Bucket name.", + required: true, + "x-ms-parameter-location": "method", + type: "string", + }, + }, + } + return linter.run(myOpenApiDocument).then((results) => { + expect(results.length).toBe(2) + expect(results[0].path.join(".")).toBe("paths./providers/Microsoft.Music/songs/{unstoppable}/artists.get.parameters") + expect(results[0].message).toContain( + "Query parameter loadTestName should be removed. Collection Get's/List operation MUST not have query parameters other than api version & OData filter.", + ) + expect(results[1].path.join(".")).toBe("paths./providers/Microsoft.Music/songs/{unstoppable}/artists.get.parameters") + expect(results[1].message).toContain( + "Query parameter quotaBucketName should be removed. Collection Get's/List operation MUST not have query parameters other than api version & OData filter.", + ) + }) +}) + +test("QueryParametersInCollectionGet should flag error for other query params but should not flag error for api-version param", () => { + const myOpenApiDocument = { + swagger: "2.0", + paths: { + "/providers/Microsoft.Music/songs/{unstoppable}/artists": { + get: { + operationId: "foo_post", + parameters: [ + { + $ref: "src/spectral/test/resources/types.json#/parameters/ApiVersionParameter", + }, + { + $ref: "#/parameters/QuotaBucketNameParameter", + }, + ], + responses: { + 200: { + description: "Success", + }, + }, + }, + }, + }, + parameters: { + LoadTestNameParameter: { + in: "path", + name: "loadTestName", + description: "Load Test name.", + required: true, + "x-ms-parameter-location": "method", + type: "string", + }, + QuotaBucketNameParameter: { + in: "query", + name: "quotaBucketName", + description: "Quota Bucket name.", + required: true, + "x-ms-parameter-location": "method", + type: "string", + }, + }, + } + return linter.run(myOpenApiDocument).then((results) => { + expect(results.length).toBe(1) + expect(results[0].path.join(".")).toBe("paths./providers/Microsoft.Music/songs/{unstoppable}/artists.get.parameters") + expect(results[0].message).toContain( + "Query parameter quotaBucketName should be removed. Collection Get's/List operation MUST not have query parameters other than api version & OData filter.", + ) + }) +}) + +test("QueryParametersInCollectionGet should not flag error when only api-version and ODataFilter param are specified", () => { + const myOpenApiDocument = { + swagger: "2.0", + paths: { + "/providers/Microsoft.Music/songs/{unstoppable}": { + get: { + operationId: "foo_post", + parameters: [ + { + $ref: "src/spectral/test/resources/types.json#/parameters/ApiVersionParameter", + }, + { + name: "$filter", + in: "query", + required: false, + type: "string", + }, + ], + responses: { + 200: { + description: "Success", + }, + }, + }, + }, + }, + } + return linter.run(myOpenApiDocument).then((results) => { + expect(results.length).toBe(0) + }) +}) + +test("QueryParametersInCollectionGet should find no errors for a point get operation", () => { + const myOpenApiDocument = { + swagger: "2.0", + paths: { + "/providers/Microsoft.Music/songs/{unstoppable}": { + get: { + operationId: "foo_post", + parameters: [ + { + $ref: "src/spectral/test/resources/types.json#/parameters/ResourceGroupNameParameter", + }, + { + $ref: "#/parameters/QuotaBucketNameParameter", + }, + ], + responses: { + 200: { + description: "Success", + }, + }, + }, + }, + "/providers/Microsoft.Music/songs/{unstoppable}/artists/{Sia}": { + get: { + operationId: "foo_post", + parameters: [ + { + $ref: "src/spectral/test/resources/types.json#/parameters/ResourceGroupNameParameter", + }, + { + $ref: "#/parameters/QuotaBucketNameParameter", + }, + ], + responses: { + 200: { + description: "Success", + }, + }, + }, + }, + }, + parameters: { + QuotaBucketNameParameter: { + in: "query", + name: "quotaBucketName", + description: "Quota Bucket name.", + required: true, + "x-ms-parameter-location": "method", + type: "string", + }, + }, + } + return linter.run(myOpenApiDocument).then((results) => { + expect(results.length).toBe(0) + }) +}) + +test("QueryParametersInCollectionGet should find no errors for a non get operation", () => { + const myOpenApiDocument = { + swagger: "2.0", + paths: { + "/providers/Microsoft.Music/songs": { + post: { + operationId: "foo_post", + parameters: [ + { + $ref: "src/spectral/test/resources/types.json#/parameters/ResourceGroupNameParameter", + }, + { + $ref: "#/parameters/QuotaBucketNameParameter", + }, + ], + responses: { + 200: { + description: "Success", + }, + }, + }, + }, + }, + parameters: { + QuotaBucketNameParameter: { + in: "query", + name: "quotaBucketName", + description: "Quota Bucket name.", + required: true, + "x-ms-parameter-location": "method", + type: "string", + }, + }, + } + return linter.run(myOpenApiDocument).then((results) => { + expect(results.length).toBe(0) + }) +}) + +test("QueryParametersInCollectionGet should find errors for x-ms-paths", () => { + const myOpenApiDocument = { + swagger: "2.0", + "x-ms-paths": { + "/providers/Microsoft.Music/songs?disambiguation_dummy": { + get: { + operationId: "foo_post", + parameters: [ + { + $ref: "src/spectral/test/resources/types.json#/parameters/SubscriptionIdParameter", + }, + { + $ref: "#/parameters/QuotaBucketNameParameter", + }, + ], + responses: { + 200: { + description: "Success", + }, + }, + }, + }, + }, + parameters: { + QuotaBucketNameParameter: { + in: "query", + name: "quotaBucketName", + description: "Quota Bucket name.", + required: true, + "x-ms-parameter-location": "method", + type: "string", + }, + }, + } + return linter.run(myOpenApiDocument).then((results) => { + expect(results.length).toBe(1) + expect(results[0].path.join(".")).toBe("x-ms-paths./providers/Microsoft.Music/songs?disambiguation_dummy.get.parameters") + expect(results[0].message).toContain( + "Query parameter quotaBucketName should be removed. Collection Get's/List operation MUST not have query parameters other than api version & OData filter.", + ) + }) +}) + +test("QueryParametersInCollectionGet should find no errors if parameters is not defined", () => { + const myOpenApiDocument = { + swagger: "2.0", + paths: { + "/providers/Microsoft.Music/songs/{unstoppable}/artist/{sia}": { + get: { + operationId: "foo_post", + responses: { + 200: { + description: "Success", + }, + }, + }, + }, + }, + } + return linter.run(myOpenApiDocument).then((results) => { + expect(results.length).toBe(0) + }) +})