diff --git a/package.json b/package.json index f7bf82e..ddffa34 100644 --- a/package.json +++ b/package.json @@ -27,12 +27,12 @@ "update-lock-file": "update-lock-file @netcracker" }, "dependencies": { - "@netcracker/qubership-apihub-api-unifier": "2.4.0", + "@netcracker/qubership-apihub-api-unifier": "feature-oas-30-to-31-comparison", "@netcracker/qubership-apihub-json-crawl": "1.0.4", "fast-equals": "4.0.3" }, "devDependencies": { - "@netcracker/qubership-apihub-compatibility-suites": "2.3.0", + "@netcracker/qubership-apihub-compatibility-suites": "dev", "@netcracker/qubership-apihub-graphapi": "1.0.8", "@netcracker/qubership-apihub-npm-gitflow": "3.1.0", "@types/jest": "29.5.11", diff --git a/src/api.ts b/src/api.ts index cea5c5c..cf05934 100644 --- a/src/api.ts +++ b/src/api.ts @@ -26,11 +26,24 @@ function areSpecTypesCompatible(beforeType: SpecType, afterType: SpecType): bool if (beforeType === afterType) { return true } - + // Allow comparison between different OpenAPI versions return isOpenApiSpecVersion(beforeType) && isOpenApiSpecVersion(afterType) } +function selectEngineSpecType(beforeType: SpecType, afterType: SpecType): SpecType { + // For OpenAPI version comparisons, use the higher version + if (isOpenApiSpecVersion(beforeType) && isOpenApiSpecVersion(afterType)) { + if (beforeType === SPEC_TYPE_OPEN_API_31 || afterType === SPEC_TYPE_OPEN_API_31) { + return SPEC_TYPE_OPEN_API_31 + } + return SPEC_TYPE_OPEN_API_30 + } + + // For same spec types or other compatible types, use the before type + return beforeType +} + export const COMPARE_ENGINES_MAP: Record = { [SPEC_TYPE_JSON_SCHEMA_04]: compareJsonSchema(SPEC_TYPE_JSON_SCHEMA_04), [SPEC_TYPE_JSON_SCHEMA_06]: compareJsonSchema(SPEC_TYPE_JSON_SCHEMA_06), @@ -48,7 +61,7 @@ export function apiDiff(before: unknown, after: unknown, options: CompareOptions if (!areSpecTypesCompatible(beforeSpec.type, afterSpec.type)) { throw new Error(`Specification cannot be different. Got ${beforeSpec.type} and ${afterSpec.type}`) } - const engine = COMPARE_ENGINES_MAP[beforeSpec.type] + const engine = COMPARE_ENGINES_MAP[selectEngineSpecType(beforeSpec.type, afterSpec.type)] return engine(before, after, { mode: COMPARE_MODE_DEFAULT, normalizedResult: DEFAULT_NORMALIZED_RESULT, diff --git a/src/openapi/openapi3.schema.ts b/src/openapi/openapi3.schema.ts index f619d71..84c582e 100644 --- a/src/openapi/openapi3.schema.ts +++ b/src/openapi/openapi3.schema.ts @@ -16,19 +16,30 @@ import { transformCompareRules, } from '../core' import type { OpenApi3SchemaRulesOptions } from './openapi3.types' -import { CompareRules } from '../types' +import { AdapterContext, AdapterResolver, CompareRules } from '../types' import { + ChainItem, + cleanOrigins, + JSON_SCHEMA_NODE_TYPE_NULL, + JSON_SCHEMA_PROPERTY_ANY_OF, + JSON_SCHEMA_PROPERTY_NULLABLE, + JSON_SCHEMA_PROPERTY_ONE_OF, + JSON_SCHEMA_PROPERTY_TITLE, + JSON_SCHEMA_PROPERTY_TYPE, JsonSchemaSpecVersion, normalize, type OpenApiSpecVersion, OriginsMetaRecord, + setOrigins, SPEC_TYPE_JSON_SCHEMA_04, SPEC_TYPE_JSON_SCHEMA_07, SPEC_TYPE_OPEN_API_30, SPEC_TYPE_OPEN_API_31, } from '@netcracker/qubership-apihub-api-unifier' import { schemaParamsCalculator } from './openapi3.description.schema' +import { isArray, isObject } from '../utils' +const NULL_TYPE_COMBINERS = [JSON_SCHEMA_PROPERTY_ANY_OF, JSON_SCHEMA_PROPERTY_ONE_OF] as const const SPEC_TYPE_TO_VERSION: Record = { [SPEC_TYPE_OPEN_API_30]: '3.0.0', [SPEC_TYPE_OPEN_API_31]: '3.1.0', @@ -62,6 +73,112 @@ const openApiJsonSchemaAnyFactory: (version: OpenApiSpecVersion) => NativeAnySch return normalizedSpec.components.schemas.empty as Record } +const hasNullType = (schema: unknown): boolean => { + if (!isObject(schema)) { + return false + } + + const type = schema[JSON_SCHEMA_PROPERTY_TYPE] + if (type === JSON_SCHEMA_NODE_TYPE_NULL) { + return true + } + + if (isArray(type) && type.includes(JSON_SCHEMA_NODE_TYPE_NULL)) { + return true + } + + return NULL_TYPE_COMBINERS.some((combiner) => { + const variants = schema[combiner] + return isArray(variants) && variants.some(hasNullType) + }) +} + +const buildNullTypeWithOrigins = ( + valueWithoutNullable: Record, + context: AdapterContext, + factory: NativeAnySchemaFactory, +): Record => { + const { options, valueOrigins } = context + const { originsFlag, syntheticTitleFlag } = options + + const nullTypeObject = factory( + { [JSON_SCHEMA_PROPERTY_TYPE]: JSON_SCHEMA_NODE_TYPE_NULL }, + valueOrigins, + options, + ) + + const inputOrigins = valueWithoutNullable[originsFlag] as Record | undefined + const nullTypeOrigins = isObject(nullTypeObject[originsFlag]) ? nullTypeObject[originsFlag] as Record : {} + + const nullable = inputOrigins && inputOrigins.nullable as ChainItem[] + if (nullable) { + const getOriginParent = (item: ChainItem | undefined) => ({ + value: JSON_SCHEMA_PROPERTY_TYPE, + parent: item?.parent, + }) + + nullTypeOrigins[JSON_SCHEMA_PROPERTY_TYPE] = isArray(nullable) + ? (nullable).map(getOriginParent) + : getOriginParent(nullable) + } + + const title = valueWithoutNullable[JSON_SCHEMA_PROPERTY_TITLE] + if (title) { + nullTypeObject[JSON_SCHEMA_PROPERTY_TITLE] = title + + const titleOrigins = inputOrigins && inputOrigins[JSON_SCHEMA_PROPERTY_TITLE] + if (titleOrigins) { + nullTypeOrigins[JSON_SCHEMA_PROPERTY_TITLE] = titleOrigins + } + } + + if (syntheticTitleFlag && valueWithoutNullable[syntheticTitleFlag]) { + nullTypeObject[syntheticTitleFlag] = true + } + + return nullTypeObject +} + +const jsonSchemaOas30to31Adapter: (factory: NativeAnySchemaFactory) => AdapterResolver = (factory) => (value, reference, valueContext) => { + if (!isObject(value) || !isObject(reference)) { + return value + } + + if (value[JSON_SCHEMA_PROPERTY_NULLABLE] !== true) { + return value + } + + if (!hasNullType(reference)) { + return value + } + + const { originsFlag } = valueContext.options + + return valueContext.transformer(value, 'nullable-to-anyof', (current) => { + if (!isObject(current)) { + return current + } + const { + [JSON_SCHEMA_PROPERTY_NULLABLE]: _nullable, + ...valueWithoutNullable + } = current + + const nullTypeObject = buildNullTypeWithOrigins(valueWithoutNullable, valueContext, factory) + + cleanOrigins(valueWithoutNullable, JSON_SCHEMA_PROPERTY_NULLABLE, originsFlag) + + const anyOfArray = [valueWithoutNullable, nullTypeObject] + + const result: Record = { [JSON_SCHEMA_PROPERTY_ANY_OF]: anyOfArray } + + setOrigins(result, JSON_SCHEMA_PROPERTY_ANY_OF, originsFlag, valueContext.valueOrigins) + setOrigins(anyOfArray, 0, originsFlag, valueContext.valueOrigins) + setOrigins(anyOfArray, 1, originsFlag, valueContext.valueOrigins) + + return result + }) +} + export const openApiSchemaRules = (options: OpenApi3SchemaRulesOptions): CompareRules => { //todo find better way to remove this 'version specific' copy-paste with normalize const jsonSchemaVersion: JsonSchemaSpecVersion = options.version === SPEC_TYPE_OPEN_API_30 ? SPEC_TYPE_JSON_SCHEMA_04 : SPEC_TYPE_JSON_SCHEMA_07 @@ -69,6 +186,7 @@ export const openApiSchemaRules = (options: OpenApi3SchemaRulesOptions): Compare const schemaRules = jsonSchemaRules({ additionalRules: { adapter: [ + ...(options.version === SPEC_TYPE_OPEN_API_31 ? [jsonSchemaOas30to31Adapter(openApiJsonSchemaAnyFactory(options.version))] : []), jsonSchemaAdapter(openApiJsonSchemaAnyFactory(options.version)), ], descriptionParamCalculator: schemaParamsCalculator, @@ -83,7 +201,7 @@ export const openApiSchemaRules = (options: OpenApi3SchemaRulesOptions): Compare nonBreaking, ({ after }) => breakingIf(!!after.value), ], - description: diffDescription(resolveSchemaDescriptionTemplates('nullable status')) + description: diffDescription(resolveSchemaDescriptionTemplates('nullable status')), }, '/discriminator': { $: allUnclassified }, '/example': { $: allAnnotation, description: diffDescription(resolveSchemaDescriptionTemplates('example')) }, @@ -92,15 +210,15 @@ export const openApiSchemaRules = (options: OpenApi3SchemaRulesOptions): Compare description: diffDescription(resolveSchemaDescriptionTemplates('externalDocs')), '/description': { $: allAnnotation, - description: diffDescription(resolveSchemaDescriptionTemplates('description of externalDocs')) + description: diffDescription(resolveSchemaDescriptionTemplates('description of externalDocs')), }, '/url': { $: allAnnotation, - description: diffDescription(resolveSchemaDescriptionTemplates('url of externalDocs')) + description: diffDescription(resolveSchemaDescriptionTemplates('url of externalDocs')), }, '/*': { $: allAnnotation, - description: diffDescription(resolveSchemaDescriptionTemplates('externalDocs')) + description: diffDescription(resolveSchemaDescriptionTemplates('externalDocs')), }, }, '/xml': {}, diff --git a/test/helper/resources/openapi-3_0-to-3_1/could-compare-overridden-description-via-reference-object/after.json b/test/helper/resources/openapi-3_0-to-3_1/could-compare-overridden-description-via-reference-object/after.json new file mode 100644 index 0000000..aecb8a6 --- /dev/null +++ b/test/helper/resources/openapi-3_0-to-3_1/could-compare-overridden-description-via-reference-object/after.json @@ -0,0 +1,38 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Swagger Petstore - OpenAPI 3.1", + "version": "1.0.12" + }, + "paths": { + "/path1": { + "post": { + "responses": { + "200": { + "$ref": "#/components/responses/response200", + "description": "response description override" + } + } + } + } + }, + "components": { + "responses": { + "response200": { + "description": "response description from components", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + } + } + } + } + } + } + } +} diff --git a/test/helper/resources/openapi-3_0-to-3_1/could-compare-overridden-description-via-reference-object/before.json b/test/helper/resources/openapi-3_0-to-3_1/could-compare-overridden-description-via-reference-object/before.json new file mode 100644 index 0000000..4343825 --- /dev/null +++ b/test/helper/resources/openapi-3_0-to-3_1/could-compare-overridden-description-via-reference-object/before.json @@ -0,0 +1,37 @@ +{ + "openapi": "3.0.4", + "info": { + "title": "Swagger Petstore - OpenAPI 3.1", + "version": "1.0.12" + }, + "paths": { + "/path1": { + "post": { + "responses": { + "200": { + "$ref": "#/components/responses/response200" + } + } + } + } + }, + "components": { + "responses": { + "response200": { + "description": "response description from components", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + } + } + } + } + } + } + } +} diff --git a/test/helper/resources/openapi-3_0-to-3_1/empty-schemas-are-equivalent-between-versions/after.json b/test/helper/resources/openapi-3_0-to-3_1/empty-schemas-are-equivalent-between-versions/after.json new file mode 100644 index 0000000..fefb65c --- /dev/null +++ b/test/helper/resources/openapi-3_0-to-3_1/empty-schemas-are-equivalent-between-versions/after.json @@ -0,0 +1,23 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "test", + "version": "0.1.0" + }, + "paths": { + "/path1": { + "post": { + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + } + } +} diff --git a/test/helper/resources/openapi-3_0-to-3_1/empty-schemas-are-equivalent-between-versions/before.json b/test/helper/resources/openapi-3_0-to-3_1/empty-schemas-are-equivalent-between-versions/before.json new file mode 100644 index 0000000..30ffb38 --- /dev/null +++ b/test/helper/resources/openapi-3_0-to-3_1/empty-schemas-are-equivalent-between-versions/before.json @@ -0,0 +1,23 @@ +{ + "openapi": "3.0.4", + "info": { + "title": "test", + "version": "0.1.0" + }, + "paths": { + "/path1": { + "post": { + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + } + } +} diff --git a/test/helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-anyOf-with-null-type-for-schema-via-ref/after.json b/test/helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-anyOf-with-null-type-for-schema-via-ref/after.json new file mode 100644 index 0000000..aedab01 --- /dev/null +++ b/test/helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-anyOf-with-null-type-for-schema-via-ref/after.json @@ -0,0 +1,39 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "test", + "version": "0.1.0" + }, + "paths": { + "/path1": { + "get": { + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MySchema" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "MySchema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + } + } +} diff --git a/test/helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-anyOf-with-null-type-for-schema-via-ref/before.json b/test/helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-anyOf-with-null-type-for-schema-via-ref/before.json new file mode 100644 index 0000000..0c80189 --- /dev/null +++ b/test/helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-anyOf-with-null-type-for-schema-via-ref/before.json @@ -0,0 +1,33 @@ +{ + "openapi": "3.0.4", + "info": { + "title": "test", + "version": "0.1.0" + }, + "paths": { + "/path1": { + "get": { + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MySchema" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "MySchema": { + "type": "string", + "nullable": true + } + } + } +} diff --git a/test/helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-anyOf-with-null-type/after.json b/test/helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-anyOf-with-null-type/after.json new file mode 100644 index 0000000..45c7d3f --- /dev/null +++ b/test/helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-anyOf-with-null-type/after.json @@ -0,0 +1,32 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "test", + "version": "0.1.0" + }, + "paths": { + "/path1": { + "get": { + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + } + } + } + } + } + } + } +} diff --git a/test/helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-anyOf-with-null-type/before.json b/test/helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-anyOf-with-null-type/before.json new file mode 100644 index 0000000..c40a36a --- /dev/null +++ b/test/helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-anyOf-with-null-type/before.json @@ -0,0 +1,26 @@ +{ + "openapi": "3.0.4", + "info": { + "title": "test", + "version": "0.1.0" + }, + "paths": { + "/path1": { + "get": { + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "string", + "nullable": true + } + } + } + } + } + } + } + } +} diff --git a/test/helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-union-with-null-type-for-schema-via-ref/after.json b/test/helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-union-with-null-type-for-schema-via-ref/after.json new file mode 100644 index 0000000..706c855 --- /dev/null +++ b/test/helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-union-with-null-type-for-schema-via-ref/after.json @@ -0,0 +1,35 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "test", + "version": "0.1.0" + }, + "paths": { + "/path1": { + "get": { + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MySchema" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "MySchema": { + "type": [ + "string", + "null" + ] + } + } + } +} diff --git a/test/helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-union-with-null-type-for-schema-via-ref/before.json b/test/helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-union-with-null-type-for-schema-via-ref/before.json new file mode 100644 index 0000000..0c80189 --- /dev/null +++ b/test/helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-union-with-null-type-for-schema-via-ref/before.json @@ -0,0 +1,33 @@ +{ + "openapi": "3.0.4", + "info": { + "title": "test", + "version": "0.1.0" + }, + "paths": { + "/path1": { + "get": { + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MySchema" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "MySchema": { + "type": "string", + "nullable": true + } + } + } +} diff --git a/test/helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-union-with-null-type/after.json b/test/helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-union-with-null-type/after.json new file mode 100644 index 0000000..dfb0279 --- /dev/null +++ b/test/helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-union-with-null-type/after.json @@ -0,0 +1,25 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "test", + "version": "0.1.0" + }, + "paths": { + "/path1": { + "get": { + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": ["string", "null"] + } + } + } + } + } + } + } + } +} diff --git a/test/helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-union-with-null-type/before.json b/test/helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-union-with-null-type/before.json new file mode 100644 index 0000000..c40a36a --- /dev/null +++ b/test/helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-union-with-null-type/before.json @@ -0,0 +1,26 @@ +{ + "openapi": "3.0.4", + "info": { + "title": "test", + "version": "0.1.0" + }, + "paths": { + "/path1": { + "get": { + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "string", + "nullable": true + } + } + } + } + } + } + } + } +} diff --git a/test/oas-30-to-31.test.ts b/test/oas-30-to-31.test.ts new file mode 100644 index 0000000..8c9a1ba --- /dev/null +++ b/test/oas-30-to-31.test.ts @@ -0,0 +1,134 @@ +import { apiDiff, CompareOptions } from '../src' +import { TEST_DIFF_FLAG, TEST_ORIGINS_FLAG, TEST_SYNTHETIC_TITLE_FLAG } from './helper' +import { diffsMatcher } from './helper/matchers' + +import couldCompareOverriddenDescriptionViaReferenceObjectBefore from './helper/resources/openapi-3_0-to-3_1/could-compare-overridden-description-via-reference-object/before.json' +import couldCompareOverriddenDescriptionViaReferenceObjectAfter from './helper/resources/openapi-3_0-to-3_1/could-compare-overridden-description-via-reference-object/after.json' + +import nullableIsEquivalentToAnyOfWithNullTypeForSchemaViaRefBefore from './helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-anyOf-with-null-type-for-schema-via-ref/before.json' +import nullableIsEquivalentToAnyOfWithNullTypeForSchemaViaRefAfter from './helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-anyOf-with-null-type-for-schema-via-ref/after.json' + +import emptySchemasAreEquivalentBetweenVersionsBefore from './helper/resources/openapi-3_0-to-3_1/empty-schemas-are-equivalent-between-versions/before.json' +import emptySchemasAreEquivalentBetweenVersionsAfter from './helper/resources/openapi-3_0-to-3_1/empty-schemas-are-equivalent-between-versions/after.json' + +import nullableIsEquivalentToAnyOfWithNullTypeBefore from './helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-anyOf-with-null-type/before.json' +import nullableIsEquivalentToAnyOfWithNullTypeAfter from './helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-anyOf-with-null-type/after.json' + +import nullableIsEquivalentToUnionWithNullTypeBefore from './helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-union-with-null-type/before.json' +import nullableIsEquivalentToUnionWithNullTypeAfter from './helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-union-with-null-type/after.json' + +import nullableIsEquivalentToUnionWithNullTypeForSchemaViaRefBefore from './helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-union-with-null-type-for-schema-via-ref/before.json' +import nullableIsEquivalentToUnionWithNullTypeForSchemaViaRefAfter from './helper/resources/openapi-3_0-to-3_1/nullable-is-equivalent-to-union-with-null-type-for-schema-via-ref/after.json' + +const expectOpenApiVersionChange = (fromVersion: string = '3.0.4', toVersion: string = '3.1.0') => + expect.objectContaining({ + action: 'replace', + afterDeclarationPaths: [['openapi']], + afterValue: toVersion, + beforeDeclarationPaths: [['openapi']], + beforeValue: fromVersion, + type: 'annotation', + }) + +const TEST_NORMALIZE_OPTIONS: CompareOptions = { + validate: true, + liftCombiners: true, + syntheticTitleFlag: TEST_SYNTHETIC_TITLE_FLAG, + originsFlag: TEST_ORIGINS_FLAG, + metaKey: TEST_DIFF_FLAG, + unify: true, + allowNotValidSyntheticChanges: true, +} + +describe('OpenAPI 3.0 to 3.1 Comparison Tests', () => { + /* + Empty schema in 3.1 includes null type, while empty schema in 3.0 does not, + but we keep this expected result because it is more aligned with user expectations. + */ + test('empty schemas are equivalent between versions', () => { + const { diffs } = apiDiff( + emptySchemasAreEquivalentBetweenVersionsBefore, + emptySchemasAreEquivalentBetweenVersionsAfter, + TEST_NORMALIZE_OPTIONS + ) + + // only openapi version change + expect(diffs).toEqual(diffsMatcher([ + expectOpenApiVersionChange(), + ])) + }) + + test('could compare overridden description via reference object', () => { + const { diffs } = apiDiff( + couldCompareOverriddenDescriptionViaReferenceObjectBefore, + couldCompareOverriddenDescriptionViaReferenceObjectAfter, + TEST_NORMALIZE_OPTIONS + ) + + expect(diffs).toEqual(diffsMatcher([ + expectOpenApiVersionChange(), + expect.objectContaining({ + action: 'replace', + beforeValue: 'response description from components', + afterValue: 'response description override', + afterDeclarationPaths: [['paths', '/path1', 'post', 'responses', '200', 'description']], + beforeDeclarationPaths: [['components', 'responses', 'response200', 'description']], + type: 'annotation', + }), + ])) + }) + + describe('Comparison nullable and null type', () => { + test('nullable is equivalent to anyOf with null type', () => { + const { diffs } = apiDiff( + nullableIsEquivalentToAnyOfWithNullTypeBefore, + nullableIsEquivalentToAnyOfWithNullTypeAfter, + TEST_NORMALIZE_OPTIONS + ) + + expect(diffs.length).toBe(1) + expect(diffs).toEqual(diffsMatcher([ + expectOpenApiVersionChange(), + ])) + }) + + test('nullable is equivalent to union with null type', () => { + const { diffs } = apiDiff( + nullableIsEquivalentToUnionWithNullTypeBefore, + nullableIsEquivalentToUnionWithNullTypeAfter, + TEST_NORMALIZE_OPTIONS + ) + + expect(diffs.length).toBe(1) + expect(diffs).toEqual(diffsMatcher([ + expectOpenApiVersionChange(), + ])) + }) + + test('nullable is equivalent to anyOf with null type for schema defined via ref', () => { + const { diffs } = apiDiff( + nullableIsEquivalentToAnyOfWithNullTypeForSchemaViaRefBefore, + nullableIsEquivalentToAnyOfWithNullTypeForSchemaViaRefAfter, + TEST_NORMALIZE_OPTIONS + ) + + expect(diffs.length).toBe(1) + expect(diffs).toEqual(diffsMatcher([ + expectOpenApiVersionChange(), + ])) + }) + + test('nullable is equivalent to union with null type for schema defined via ref', () => { + const { diffs } = apiDiff( + nullableIsEquivalentToUnionWithNullTypeForSchemaViaRefBefore, + nullableIsEquivalentToUnionWithNullTypeForSchemaViaRefAfter, + TEST_NORMALIZE_OPTIONS + ) + + expect(diffs.length).toBe(1) + expect(diffs).toEqual(diffsMatcher([ + expectOpenApiVersionChange(), + ])) + }) + }) +})