From 220dbb579f70a029d7e1a99311151290a41826b3 Mon Sep 17 00:00:00 2001 From: Bryan Thompson Date: Thu, 12 Feb 2026 17:43:09 -0800 Subject: [PATCH 1/4] Expand CLI check spec unit tests (#353) Add 10 new tests covering validation scenarios documented in the check command README that were previously untested: - Schema: simple type match, enum match, base extra enums, missing optional prop, default additionalProperties - Routes: extra status codes, extra/missing mime types, extra/missing optional query params --- .../check/utils/check-matching-routes.test.ts | 290 ++++++++++++++++++ .../utils/check-schema-compatibility.test.ts | 149 +++++++++ 2 files changed, 439 insertions(+) diff --git a/lib/cli/src/__tests__/commands/check/utils/check-matching-routes.test.ts b/lib/cli/src/__tests__/commands/check/utils/check-matching-routes.test.ts index a65831ac3..1d990f6de 100644 --- a/lib/cli/src/__tests__/commands/check/utils/check-matching-routes.test.ts +++ b/lib/cli/src/__tests__/commands/check/utils/check-matching-routes.test.ts @@ -766,4 +766,294 @@ describe("checkMatchingRoutes", () => { }) ); }); + + // ############################################################ + // Status code validation - extra status codes are allowed + // ############################################################ + + it("should ignore extra status codes in implementation", () => { + // Arrange - Base has 200 and 400, impl adds 401 and 403 + const baseDoc: OpenAPIV3.Document = { + openapi: "3.0.0", + info: { title: "Base", version: "1.0.0" }, + paths: { + "/foo": { + get: { + responses: { + "200": { description: "OK" }, + "400": { description: "Bad Request" }, + }, + }, + }, + }, + }; + + const implDoc: OpenAPIV3.Document = { + openapi: "3.0.0", + info: { title: "Impl", version: "1.0.0" }, + paths: { + "/foo": { + get: { + responses: { + "200": { description: "OK" }, + "400": { description: "Bad Request" }, + "401": { description: "Unauthorized" }, + "403": { description: "Forbidden" }, + }, + }, + }, + }, + }; + + // Act + const errors = checkMatchingRoutes(baseDoc, implDoc); + + // Assert - No errors because extra status codes are allowed + expect(errors.getErrorCount()).toBe(0); + }); + + // ############################################################ + // MIME type validation - extra mime types are allowed + // ############################################################ + + it("should ignore extra mime types in implementation response", () => { + // Arrange - Base has application/json, impl adds application/xml + const baseDoc: OpenAPIV3.Document = { + openapi: "3.0.0", + info: { title: "Base", version: "1.0.0" }, + paths: { + "/foo": { + get: { + responses: { + "200": { + description: "OK", + content: { + "application/json": { + schema: { type: "object" }, + }, + }, + }, + }, + }, + }, + }, + }; + + const implDoc: OpenAPIV3.Document = { + openapi: "3.0.0", + info: { title: "Impl", version: "1.0.0" }, + paths: { + "/foo": { + get: { + responses: { + "200": { + description: "OK", + content: { + "application/json": { + schema: { type: "object" }, + }, + "application/xml": { + schema: { type: "object" }, + }, + }, + }, + }, + }, + }, + }, + }; + + // Act + const errors = checkMatchingRoutes(baseDoc, implDoc); + + // Assert - No errors because extra mime types are allowed + expect(errors.getErrorCount()).toBe(0); + }); + + // ############################################################ + // MIME type validation - missing mime type is an error + // ############################################################ + + it("should detect missing mime type in implementation response", () => { + // Arrange - Base has json and xml, impl only has json + const baseDoc: OpenAPIV3.Document = { + openapi: "3.0.0", + info: { title: "Base", version: "1.0.0" }, + paths: { + "/foo": { + get: { + responses: { + "200": { + description: "OK", + content: { + "application/json": { + schema: { type: "object" }, + }, + "application/xml": { + schema: { type: "object" }, + }, + }, + }, + }, + }, + }, + }, + }; + + const implDoc: OpenAPIV3.Document = { + openapi: "3.0.0", + info: { title: "Impl", version: "1.0.0" }, + paths: { + "/foo": { + get: { + responses: { + "200": { + description: "OK", + content: { + "application/json": { + schema: { type: "object" }, + }, + }, + }, + }, + }, + }, + }, + }; + + // Act + const errors = checkMatchingRoutes(baseDoc, implDoc); + + // Assert + expect(errors.getErrorCount()).toBe(1); + expect(errors.get(0)).toEqual( + expect.objectContaining({ + type: "ROUTE_CONFLICT", + subType: "RESPONSE_BODY_CONFLICT", + endpoint: "GET /foo", + message: expect.stringMatching(/Implementation missing schema for expected mime type \[application\/xml\]/), + }) + ); + }); + + // ############################################################ + // Query parameter validation - extra params not flagged + // ############################################################ + + it("should not flag extra query parameters in implementation", () => { + // Arrange - Impl has an extra parameter not in base + const baseDoc: OpenAPIV3.Document = { + openapi: "3.0.0", + info: { title: "Base", version: "1.0.0" }, + paths: { + "/search": { + get: { + parameters: [ + { + name: "required_param", + in: "query", + required: true, + schema: { type: "string" }, + }, + ], + responses: { + "200": { description: "OK" }, + }, + }, + }, + }, + }; + + const implDoc: OpenAPIV3.Document = { + openapi: "3.0.0", + info: { title: "Impl", version: "1.0.0" }, + paths: { + "/search": { + get: { + parameters: [ + { + name: "required_param", + in: "query", + required: true, + schema: { type: "string" }, + }, + { + name: "extra_param", + in: "query", + required: false, + schema: { type: "string" }, + }, + ], + responses: { + "200": { description: "OK" }, + }, + }, + }, + }, + }; + + // Act + const errors = checkMatchingRoutes(baseDoc, implDoc); + + // Assert - No errors; extra params are currently not flagged + expect(errors.getErrorCount()).toBe(0); + }); + + // ############################################################ + // Query parameter validation - missing optional param + // ############################################################ + + it("should flag missing optional query parameter", () => { + // Arrange - Base has an optional param, impl doesn't have it + const baseDoc: OpenAPIV3.Document = { + openapi: "3.0.0", + info: { title: "Base", version: "1.0.0" }, + paths: { + "/search": { + get: { + parameters: [ + { + name: "optional_param", + in: "query", + required: false, + schema: { type: "string" }, + }, + ], + responses: { + "200": { description: "OK" }, + }, + }, + }, + }, + }; + + const implDoc: OpenAPIV3.Document = { + openapi: "3.0.0", + info: { title: "Impl", version: "1.0.0" }, + paths: { + "/search": { + get: { + parameters: [], + responses: { + "200": { description: "OK" }, + }, + }, + }, + }, + }; + + // Act + const errors = checkMatchingRoutes(baseDoc, implDoc); + + // Assert - Current impl flags all missing params (required or optional) + expect(errors.getErrorCount()).toBe(1); + expect(errors.get(0)).toEqual( + expect.objectContaining({ + type: "ROUTE_CONFLICT", + subType: "QUERY_PARAM_CONFLICT", + endpoint: "GET /search", + message: expect.stringMatching(/Missing required query parameter \[optional_param\]/), + }) + ); + }); }); diff --git a/lib/cli/src/__tests__/commands/check/utils/check-schema-compatibility.test.ts b/lib/cli/src/__tests__/commands/check/utils/check-schema-compatibility.test.ts index 1c711e9de..d44340536 100644 --- a/lib/cli/src/__tests__/commands/check/utils/check-schema-compatibility.test.ts +++ b/lib/cli/src/__tests__/commands/check/utils/check-schema-compatibility.test.ts @@ -544,4 +544,153 @@ describe("Schema Compatibility Checks", () => { }) ); }); + + // ############################################################ + // Type checking - simple type matches (README: Type case 3) + // ############################################################ + + it("should pass when simple types match exactly", () => { + // Arrange - Both base and impl have the same string type + // README: Type case 3 - Simple type matches -> Ignore + const baseSchema: OpenAPIV3.SchemaObject = { + type: "object", + properties: { + name: { type: "string" }, + }, + }; + + const implSchema: OpenAPIV3.SchemaObject = { + type: "object", + properties: { + name: { type: "string" }, + }, + }; + + // Act + const errors = checkSchemaCompatibility(location, baseSchema, implSchema, ctx); + + // Assert - No errors because types match + expect(errors.getErrorCount()).toBe(0); + }); + + // ############################################################ + // Enum validation - enums match (README: Enum case 1) + // ############################################################ + + it("should pass when enum values match exactly", () => { + // Arrange - Both base and impl have the same enum values + // README: Enum case 1 - Enums match -> Ignore + const baseSchema: OpenAPIV3.SchemaObject = { + type: "string", + enum: ["active", "inactive"], + }; + + const implSchema: OpenAPIV3.SchemaObject = { + type: "string", + enum: ["active", "inactive"], + }; + + // Act + const errors = checkSchemaCompatibility(location, baseSchema, implSchema, ctx); + + // Assert - No errors because enums match + expect(errors.getErrorCount()).toBe(0); + }); + + // ############################################################ + // Enum validation - base has extra values (README: Enum case 2) + // ############################################################ + + it("should pass when base has more enum values than impl", () => { + // Arrange - Base has extra enum value "pending" not in impl + // README: Enum case 2 - Base type has extra -> Ignore + // Impl can support a subset because a valid impl input is still valid for base + const baseSchema: OpenAPIV3.SchemaObject = { + type: "string", + enum: ["active", "inactive", "pending"], + }; + + const implSchema: OpenAPIV3.SchemaObject = { + type: "string", + enum: ["active", "inactive"], + }; + + // Act + const errors = checkSchemaCompatibility(location, baseSchema, implSchema, ctx); + + // Assert - No errors because impl is a subset of base + expect(errors.getErrorCount()).toBe(0); + }); + + // ############################################################ + // Missing optional property emits warning (README: Schema case 2.2) + // ############################################################ + + it("should not error when missing property is optional in base schema", () => { + // Arrange - "description" is optional (not in required array) + // README: Schema case 2.2 - Missing optional prop -> Warn + // NOTE: Current implementation silently ignores missing optional props + // rather than emitting a warning. This test documents the current behavior. + const baseSchema: OpenAPIV3.SchemaObject = { + type: "object", + properties: { + name: { type: "string" }, + description: { type: "string" }, + }, + // 'description' is NOT in required, so it's optional + }; + + const implSchema: OpenAPIV3.SchemaObject = { + type: "object", + properties: { + name: { type: "string" }, + // missing optional 'description' property + }, + }; + + // Act + const errors = checkSchemaCompatibility(location, baseSchema, implSchema, ctx); + + // Assert - No errors because 'description' is optional + expect(errors.getErrorCount()).toBe(0); + }); + + // ############################################################ + // Additional properties - default (undefined) allows extras + // (README: Schema case 1.1 variant - additionalProperties not set) + // ############################################################ + + it("should allow extra properties when additionalProperties is not set (default)", () => { + // Arrange - additionalProperties is undefined (defaults to allowing extras) + // README: Schema case 1.1 - Additional props allowed -> Ignore + const baseSchema: OpenAPIV3.SchemaObject = { + type: "object", + properties: { + name: { type: "string" }, + }, + // additionalProperties is not set (defaults to allowing extras) + }; + + const implSchema: OpenAPIV3.SchemaObject = { + type: "object", + properties: { + name: { type: "string" }, + extra_prop: { type: "string" }, + }, + }; + + // Act + const errors = checkSchemaCompatibility(location, baseSchema, implSchema, ctx); + + // Assert - Extra props flagged because current impl treats undefined as disallowed + // NOTE: The README says undefined should allow extras, but the current code + // flags them as errors. This documents the current behavior. + expect(errors.getErrorCount()).toBe(1); + expect(errors.get(0)).toEqual( + expect.objectContaining({ + conflictType: "EXTRA_FIELD", + location: `${location}.extra_prop`, + }) + ); + }); }); From cf72078232ddf5a706ed90c1a943d75d021ab036 Mon Sep 17 00:00:00 2001 From: Bryan Thompson Date: Thu, 12 Feb 2026 17:56:51 -0800 Subject: [PATCH 2/4] fix: clarify test names for known spec-implementation deviations Rename tests that document behavior diverging from the README spec to explicitly note the deviation, and add TODO comments linking to the specific README cases so the tests get updated when the impl is fixed. --- .../check/utils/check-matching-routes.test.ts | 14 ++++++++++---- .../check/utils/check-schema-compatibility.test.ts | 14 +++++++------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/lib/cli/src/__tests__/commands/check/utils/check-matching-routes.test.ts b/lib/cli/src/__tests__/commands/check/utils/check-matching-routes.test.ts index 1d990f6de..723b3d1ef 100644 --- a/lib/cli/src/__tests__/commands/check/utils/check-matching-routes.test.ts +++ b/lib/cli/src/__tests__/commands/check/utils/check-matching-routes.test.ts @@ -940,7 +940,10 @@ describe("checkMatchingRoutes", () => { // Query parameter validation - extra params not flagged // ############################################################ - it("should not flag extra query parameters in implementation", () => { + // TODO: README Query param case 1 says extra query params should produce a + // warning. Current impl only iterates over base params, so extras are silently + // ignored. Update test to assert a warning when impl is fixed. + it("should not flag extra query parameters in implementation (known deviation from README spec)", () => { // Arrange - Impl has an extra parameter not in base const baseDoc: OpenAPIV3.Document = { openapi: "3.0.0", @@ -995,7 +998,7 @@ describe("checkMatchingRoutes", () => { // Act const errors = checkMatchingRoutes(baseDoc, implDoc); - // Assert - No errors; extra params are currently not flagged + // Assert expect(errors.getErrorCount()).toBe(0); }); @@ -1003,7 +1006,10 @@ describe("checkMatchingRoutes", () => { // Query parameter validation - missing optional param // ############################################################ - it("should flag missing optional query parameter", () => { + // TODO: README Query param case 2.2 says missing optional params should warn, + // not error. Current impl flags all missing params as "Missing required query + // parameter" regardless of required status. Update test when impl is fixed. + it("should flag missing optional query parameter as error (known deviation from README spec)", () => { // Arrange - Base has an optional param, impl doesn't have it const baseDoc: OpenAPIV3.Document = { openapi: "3.0.0", @@ -1045,7 +1051,7 @@ describe("checkMatchingRoutes", () => { // Act const errors = checkMatchingRoutes(baseDoc, implDoc); - // Assert - Current impl flags all missing params (required or optional) + // Assert expect(errors.getErrorCount()).toBe(1); expect(errors.get(0)).toEqual( expect.objectContaining({ diff --git a/lib/cli/src/__tests__/commands/check/utils/check-schema-compatibility.test.ts b/lib/cli/src/__tests__/commands/check/utils/check-schema-compatibility.test.ts index d44340536..1c2130f2f 100644 --- a/lib/cli/src/__tests__/commands/check/utils/check-schema-compatibility.test.ts +++ b/lib/cli/src/__tests__/commands/check/utils/check-schema-compatibility.test.ts @@ -660,15 +660,17 @@ describe("Schema Compatibility Checks", () => { // (README: Schema case 1.1 variant - additionalProperties not set) // ############################################################ - it("should allow extra properties when additionalProperties is not set (default)", () => { - // Arrange - additionalProperties is undefined (defaults to allowing extras) - // README: Schema case 1.1 - Additional props allowed -> Ignore + // TODO: README Schema case 1.1 says undefined additionalProperties should + // allow extras. Current impl treats undefined as disallowed and flags an + // error. See TODO in check-schema-compatibility.ts:198. Update test when fixed. + it("should flag extra properties when additionalProperties is not set (known deviation from README spec)", () => { + // Arrange - additionalProperties is undefined + // README says this should allow extras, but current impl flags them const baseSchema: OpenAPIV3.SchemaObject = { type: "object", properties: { name: { type: "string" }, }, - // additionalProperties is not set (defaults to allowing extras) }; const implSchema: OpenAPIV3.SchemaObject = { @@ -682,9 +684,7 @@ describe("Schema Compatibility Checks", () => { // Act const errors = checkSchemaCompatibility(location, baseSchema, implSchema, ctx); - // Assert - Extra props flagged because current impl treats undefined as disallowed - // NOTE: The README says undefined should allow extras, but the current code - // flags them as errors. This documents the current behavior. + // Assert expect(errors.getErrorCount()).toBe(1); expect(errors.get(0)).toEqual( expect.objectContaining({ From a5bb0a357cbb1c3bb5e9e48bae3cbb17b73957e2 Mon Sep 17 00:00:00 2001 From: Bryan Thompson Date: Fri, 13 Feb 2026 10:15:05 -0800 Subject: [PATCH 3/4] fix: resolve prettier formatting error in check-matching-routes test --- .../commands/check/utils/check-matching-routes.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/cli/src/__tests__/commands/check/utils/check-matching-routes.test.ts b/lib/cli/src/__tests__/commands/check/utils/check-matching-routes.test.ts index 723b3d1ef..3fbd6de2c 100644 --- a/lib/cli/src/__tests__/commands/check/utils/check-matching-routes.test.ts +++ b/lib/cli/src/__tests__/commands/check/utils/check-matching-routes.test.ts @@ -931,7 +931,9 @@ describe("checkMatchingRoutes", () => { type: "ROUTE_CONFLICT", subType: "RESPONSE_BODY_CONFLICT", endpoint: "GET /foo", - message: expect.stringMatching(/Implementation missing schema for expected mime type \[application\/xml\]/), + message: expect.stringMatching( + /Implementation missing schema for expected mime type \[application\/xml\]/ + ), }) ); }); From 20e341767f2e5d3d0e68bdf30e36b6562169d627 Mon Sep 17 00:00:00 2001 From: widal001 Date: Fri, 13 Feb 2026 17:37:38 -0500 Subject: [PATCH 4/4] ci: Updates event trigger for PR labeler Needs the pull_request_target: trigger to access the right permissions For more details see: https://github.com/actions/labeler/issues/399 --- .github/workflows/pr-labeler.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index f0fe72f60..98d0d354c 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -1,6 +1,6 @@ name: "PR: Labeler" on: - pull_request: + pull_request_target: permissions: contents: read