From feaad19dbf2074a79741598815054128207527bb Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Fri, 13 Mar 2026 11:05:26 +0400 Subject: [PATCH 01/29] feat: Build changelog for AsyncAPI specifications --- package.json | 2 +- src/apitypes/async/async.changes.ts | 75 +++++- src/apitypes/async/async.utils.ts | 4 +- test/asyncapi-changes.test.ts | 247 +++++++++++++----- .../after.yaml | 42 +++ .../before.yaml | 34 +++ .../channel/add-message/after.yaml | 33 +++ .../add-message}/before.yaml | 0 .../{change => change-address}/after.yaml | 0 .../{change => change-address}/before.yaml | 0 .../{add => change-reference}/after.yaml | 0 .../{add => change-reference}/before.yaml | 0 .../channel/remove-unused/after.yaml | 25 ++ .../channel/remove-unused/before.yaml | 30 +++ .../after.yaml | 43 +++ .../before.yaml | 42 +++ .../message/add-to-operation/after.yaml | 34 +++ .../message/add-to-operation/before.yaml | 33 +++ .../add-unused-component-message/after.yaml | 31 +++ .../add-unused-component-message/before.yaml | 25 ++ .../after.yaml | 36 +++ .../before.yaml | 36 +++ .../message/change-content-type/after.yaml | 26 ++ .../message/change-content-type/before.yaml | 26 ++ .../after.yaml | 42 +++ .../before.yaml | 43 +++ .../message/remove-from-operation/after.yaml | 33 +++ .../message/remove-from-operation/before.yaml | 34 +++ .../after.yaml | 25 ++ .../before.yaml | 31 +++ .../remove-unused-from-channel/after.yaml | 31 +++ .../remove-unused-from-channel/before.yaml | 33 +++ .../{change => change-action}/after.yaml | 0 .../operation/change-action/before.yaml | 26 ++ .../after.yaml | 35 +++ .../before.yaml | 35 +++ .../schema/add-property/after.yaml | 27 ++ .../schema/add-property/before.yaml | 25 ++ .../schema/change-property-type/after.yaml | 25 ++ .../schema/change-property-type/before.yaml | 25 ++ .../schema/remove-property/after.yaml | 25 ++ .../schema/remove-property/before.yaml | 27 ++ .../server/remove-from-channel/before.yaml | 2 + .../projects/asyncapi-changes/tags/after.yaml | 10 + .../asyncapi-changes/tags/before.yaml | 10 + 45 files changed, 1290 insertions(+), 78 deletions(-) create mode 100644 test/projects/asyncapi-changes/channel/add-message-with-multiple-operations/after.yaml create mode 100644 test/projects/asyncapi-changes/channel/add-message-with-multiple-operations/before.yaml create mode 100644 test/projects/asyncapi-changes/channel/add-message/after.yaml rename test/projects/asyncapi-changes/{operation/change => channel/add-message}/before.yaml (100%) rename test/projects/asyncapi-changes/channel/{change => change-address}/after.yaml (100%) rename test/projects/asyncapi-changes/channel/{change => change-address}/before.yaml (100%) rename test/projects/asyncapi-changes/channel/{add => change-reference}/after.yaml (100%) rename test/projects/asyncapi-changes/channel/{add => change-reference}/before.yaml (100%) create mode 100644 test/projects/asyncapi-changes/channel/remove-unused/after.yaml create mode 100644 test/projects/asyncapi-changes/channel/remove-unused/before.yaml create mode 100644 test/projects/asyncapi-changes/message/add-to-operation-with-existing-messages/after.yaml create mode 100644 test/projects/asyncapi-changes/message/add-to-operation-with-existing-messages/before.yaml create mode 100644 test/projects/asyncapi-changes/message/add-to-operation/after.yaml create mode 100644 test/projects/asyncapi-changes/message/add-to-operation/before.yaml create mode 100644 test/projects/asyncapi-changes/message/add-unused-component-message/after.yaml create mode 100644 test/projects/asyncapi-changes/message/add-unused-component-message/before.yaml create mode 100644 test/projects/asyncapi-changes/message/change-content-type-with-multiple-messages/after.yaml create mode 100644 test/projects/asyncapi-changes/message/change-content-type-with-multiple-messages/before.yaml create mode 100644 test/projects/asyncapi-changes/message/change-content-type/after.yaml create mode 100644 test/projects/asyncapi-changes/message/change-content-type/before.yaml create mode 100644 test/projects/asyncapi-changes/message/remove-from-operation-with-remaining-messages/after.yaml create mode 100644 test/projects/asyncapi-changes/message/remove-from-operation-with-remaining-messages/before.yaml create mode 100644 test/projects/asyncapi-changes/message/remove-from-operation/after.yaml create mode 100644 test/projects/asyncapi-changes/message/remove-from-operation/before.yaml create mode 100644 test/projects/asyncapi-changes/message/remove-unused-component-message/after.yaml create mode 100644 test/projects/asyncapi-changes/message/remove-unused-component-message/before.yaml create mode 100644 test/projects/asyncapi-changes/message/remove-unused-from-channel/after.yaml create mode 100644 test/projects/asyncapi-changes/message/remove-unused-from-channel/before.yaml rename test/projects/asyncapi-changes/operation/{change => change-action}/after.yaml (100%) create mode 100644 test/projects/asyncapi-changes/operation/change-action/before.yaml create mode 100644 test/projects/asyncapi-changes/operation/change-description-with-multiple-messages/after.yaml create mode 100644 test/projects/asyncapi-changes/operation/change-description-with-multiple-messages/before.yaml create mode 100644 test/projects/asyncapi-changes/schema/add-property/after.yaml create mode 100644 test/projects/asyncapi-changes/schema/add-property/before.yaml create mode 100644 test/projects/asyncapi-changes/schema/change-property-type/after.yaml create mode 100644 test/projects/asyncapi-changes/schema/change-property-type/before.yaml create mode 100644 test/projects/asyncapi-changes/schema/remove-property/after.yaml create mode 100644 test/projects/asyncapi-changes/schema/remove-property/before.yaml diff --git a/package.json b/package.json index 42555fa6..d1511416 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ }, "dependencies": { "@asyncapi/parser": "^3.0.0", - "@netcracker/qubership-apihub-api-diff": "feature-asyncapi-basic-e2e", + "@netcracker/qubership-apihub-api-diff": "feature-schema-rules", "@netcracker/qubership-apihub-api-unifier": "feature-asyncapi-basic-e2e", "@netcracker/qubership-apihub-graphapi": "dev", "@netcracker/qubership-apihub-json-crawl": "dev", diff --git a/src/apitypes/async/async.changes.ts b/src/apitypes/async/async.changes.ts index 9319a4dd..058aaf5c 100644 --- a/src/apitypes/async/async.changes.ts +++ b/src/apitypes/async/async.changes.ts @@ -25,6 +25,7 @@ import { import { AFTER_VALUE_NORMALIZED_PROPERTY, BEFORE_VALUE_NORMALIZED_PROPERTY, + FIRST_REFERENCE_KEY_PROPERTY, NORMALIZE_OPTIONS, ORIGINS_SYMBOL, } from '../../consts' @@ -89,6 +90,7 @@ export const compareDocuments: DocumentsCompare = async ( normalizedResult: true, afterValueNormalizedProperty: AFTER_VALUE_NORMALIZED_PROPERTY, beforeValueNormalizedProperty: BEFORE_VALUE_NORMALIZED_PROPERTY, + firstReferenceKeyProperty: FIRST_REFERENCE_KEY_PROPERTY, }, ) as { merged: AsyncAPIV3.AsyncAPIObject; diffs: Diff[] } @@ -101,6 +103,49 @@ export const compareDocuments: DocumentsCompare = async ( const tags = new Set() const operationChanges: OperationChanges[] = [] + /** + * Aggregated diffs on the operation level include diffs from ALL messages. + * Since each apihub operation maps to a specific operation + message pair, + * diffs from sibling messages must be excluded to prevent them from leaking + * into unrelated apihub operations. + * + * Collects two kinds of diffs from other messages: + * 1. Aggregated content diffs from each sibling message object + * 2. Array-level diffs for adding/removing sibling messages from the messages list + */ + function collectOtherMessageDiffs(messages: AsyncAPIV3.MessageObject[], currentMessageIndex: number): Set { + const otherDiffs = new Set() + for (const [idx, msg] of messages.entries()) { + if (idx === currentMessageIndex) continue + const msgDiffs = (msg as WithAggregatedDiffs)[DIFFS_AGGREGATED_META_KEY] + if (msgDiffs) { + for (const d of msgDiffs) { otherDiffs.add(d) } + } + } + const messagesArrayMeta = (messages as WithDiffMetaRecord)[DIFF_META_KEY] + if (messagesArrayMeta) { + for (const key in messagesArrayMeta) { + if (Number(key) !== currentMessageIndex) { + otherDiffs.add(messagesArrayMeta[key]) + } + } + } + return otherDiffs + } + + // todo del after fix api-diff + function getOperationId(operationsMap: OperationsMap, asyncOperationId: string, index: number): string { + const keys = Object.keys(operationsMap) + + const matchingOperations = keys.filter(key => { + const operation = operationsMap[key] + return operation?.previous?.metadata?.asyncOperationId === asyncOperationId || operation?.current?.metadata?.asyncOperationId === asyncOperationId + }) + const operation = matchingOperations.find(matchingOperation=> matchingOperation.endsWith(String(index+1))) + + return operation || matchingOperations[index] + } + // Iterate through operations in merged document const { operations } = merged if (operations && isObject(operations)) { @@ -114,16 +159,17 @@ export const compareDocuments: DocumentsCompare = async ( if (!Array.isArray(messages) || messages.length === 0) { continue } - // Extract action and channel from operation + const { action, channel: operationChannel } = operationObject if (!action || !operationChannel) { continue } - for (const message of messages) { - // Use simple operation ID (no normalization needed for AsyncAPI) - const messageId = getAsyncMessageId(message) - const operationId = calculateAsyncOperationId(asyncOperationId, messageId) + for (const [messageIndex, message] of messages.entries()) { + const messageId = getAsyncMessageId(message) + // todo fix it + // const operationId = calculateAsyncOperationId(asyncOperationId, messageId) + const operationId = getOperationId(operationsMap, asyncOperationId, messageIndex) const { current, previous, @@ -137,18 +183,19 @@ export const compareDocuments: DocumentsCompare = async ( let operationDiffs: Diff[] = [] if (operationPotentiallyChanged) { - operationDiffs = [ - ...(operationObject as WithAggregatedDiffs)[DIFFS_AGGREGATED_META_KEY] ?? [], - // TODO: check - // ...extractAsyncApiVersionDiff(merged), - // ...extractRootServersDiffs(merged), - // ...extractChannelsDiffs(merged, operationChannel), - ] + const allOperationDiffs = (operationObject as WithAggregatedDiffs)[DIFFS_AGGREGATED_META_KEY] ?? [] + + const otherMessageDiffs = collectOtherMessageDiffs(messages, messageIndex) + operationDiffs = [...allOperationDiffs].filter(d => !otherMessageDiffs.has(d)) } if (operationAddedOrRemoved) { + // Level 1: message added/removed within an existing operation (analogous to REST method within path) + const messageAddedOrRemovedDiff = (messages as WithDiffMetaRecord)[DIFF_META_KEY]?.[messageIndex] + // Level 2: entire operation added/removed (analogous to REST entire path) const operationAddedOrRemovedDiff = (operations as WithDiffMetaRecord)[DIFF_META_KEY]?.[asyncOperationId] - if (operationAddedOrRemovedDiff) { - operationDiffs.push(operationAddedOrRemovedDiff) + const diff = messageAddedOrRemovedDiff ?? operationAddedOrRemovedDiff + if (diff) { + operationDiffs.push(diff) } } diff --git a/src/apitypes/async/async.utils.ts b/src/apitypes/async/async.utils.ts index 1a70564a..e315338d 100644 --- a/src/apitypes/async/async.utils.ts +++ b/src/apitypes/async/async.utils.ts @@ -22,7 +22,8 @@ import { getSymbolValueIfDefined, isObject, isReferenceObject, - setValueByPath, takeIfDefined, + setValueByPath, + takeIfDefined, } from '../../utils' import type * as TYPE from './async.types' import { @@ -267,3 +268,4 @@ export const resolveAsyncApiOperationIdsFromRefs = ( return resolved } + diff --git a/test/asyncapi-changes.test.ts b/test/asyncapi-changes.test.ts index c1e06b97..81bd45c4 100644 --- a/test/asyncapi-changes.test.ts +++ b/test/asyncapi-changes.test.ts @@ -9,67 +9,48 @@ import { import { ANNOTATION_CHANGE_TYPE, ASYNCAPI_API_TYPE, - BREAKING_CHANGE_TYPE, + BREAKING_CHANGE_TYPE, BuildResult, EMPTY_CHANGE_SUMMARY, NON_BREAKING_CHANGE_TYPE, UNCLASSIFIED_CHANGE_TYPE, } from '../src' -// TODO Enable tests when changes are added -describe.skip('AsyncAPI 3.0 Changelog', () => { +describe('AsyncAPI 3.0 Changelog tests', () => { - test('no changes', async () => { + const expectNoChanges = (result: BuildResult): void => { + expect(result).toEqual(noChangesMatcher(ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher(EMPTY_CHANGE_SUMMARY, ASYNCAPI_API_TYPE)) + } + + test('should detect no changes for identical documents', async () => { const result = await buildChangelogPackageDefaultConfig( 'asyncapi-changes/no-changes', [{ fileId: 'before.yaml', publish: true }], [{ fileId: 'before.yaml' }], ) - expect(result).toEqual(noChangesMatcher(ASYNCAPI_API_TYPE)) - }) - - describe('Channels', () => { - test('add channel', async () => { - const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/add') - - expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - }) - - test('remove channel', async () => { - const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/remove') - - expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - }) - - test('change channel address', async () => { - const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/change') - - expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - }) + expectNoChanges(result) }) describe('Operations tests', () => { - test('add operation', async () => { + test('should detect added operation', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/add') expect(result).toEqual(changesSummaryMatcher({ [NON_BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) expect(result).toEqual(numberOfImpactedOperationsMatcher({ [NON_BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) - test('add multiple operations', async () => { + test('should detect multiple added operations', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/add-multiple') expect(result).toEqual(changesSummaryMatcher({ [NON_BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) expect(result).toEqual(numberOfImpactedOperationsMatcher({ [NON_BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) }) - test('remove operation', async () => { + test('should detect removed operation', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/remove') expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) - test('add and remove operations', async () => { + test('should detect simultaneously added and removed operations', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/add-remove') expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1, @@ -81,72 +62,213 @@ describe.skip('AsyncAPI 3.0 Changelog', () => { }, ASYNCAPI_API_TYPE)) }) - test('change operation', async () => { - const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/change') + test('should detect renamed operation as add and remove', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/rename') + expect(result).toEqual(changesSummaryMatcher({ + [BREAKING_CHANGE_TYPE]: 2, + }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ + [BREAKING_CHANGE_TYPE]: 2, + }, ASYNCAPI_API_TYPE)) + }) + + test('should detect changed operation action type', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/change-action') - expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) - test('renamed operation as add/remove', async () => { - const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/rename') + test('should detect changed operation description with multiple messages', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/change-description-with-multiple-messages') + expect(result).toEqual(changesSummaryMatcher({ - [BREAKING_CHANGE_TYPE]: 1, - [NON_BREAKING_CHANGE_TYPE]: 1, + [ANNOTATION_CHANGE_TYPE]: 1, }, ASYNCAPI_API_TYPE)) expect(result).toEqual(numberOfImpactedOperationsMatcher({ - [BREAKING_CHANGE_TYPE]: 1, - [NON_BREAKING_CHANGE_TYPE]: 1, + [ANNOTATION_CHANGE_TYPE]: 2, }, ASYNCAPI_API_TYPE)) }) }) - describe('Servers', () => { - test('add server', async () => { - const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/server/add') + describe('Channels tests', () => { + test('should detect changed operation channel reference', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/change-reference') + + expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('should detect removed channel', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/remove') + + expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('should not detect changes when removing unused channel', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/remove-unused') + + expectNoChanges(result) + }) + + test('should detect changed channel address', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/change-address') expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) - test('remove server', async () => { - const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/server/remove') + test('should detect added message definition in channel', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/add-message') expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) - test('change server', async () => { - const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/server/change') + test('should detect added message definition in channel with multiple apihub operations', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/add-message-with-multiple-operations') - expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) }) + }) - test('add root servers', async () => { + describe('Servers tests', () => { + test('should detect added server in channel', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/server/add-to-channel') + + expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('should detect removed server from channel', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/server/remove-from-channel') + + expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('should not detect changes when adding root servers', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/server/add-root') - expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expectNoChanges(result) }) - test('remove root servers', async () => { + test('should not detect changes when removing root servers', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/server/remove-root') - expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expectNoChanges(result) }) - test('change root servers', async () => { + test('should not detect changes when changing root servers', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/server/change-root') - expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expectNoChanges(result) + }) + }) + + describe('Messages tests', () => { + test('should detect added message reference in operation', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/message/add-to-operation') + + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('should detect removed message reference from operation', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/message/remove-from-operation') + + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('should detect changed message content type', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/message/change-content-type') + + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('should not add changes to existing messages when adding new message to operation', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/message/add-to-operation-with-existing-messages') + + // Adding message3 to operation with existing message1 and message2 + // should only impact 1 new apihub operation (operation1-message3), + // not the existing operation1-message1 and operation1-message2 + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('should not add changes to remaining messages when removing message from operation', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/message/remove-from-operation-with-remaining-messages') + + // Removing message2 from operation with message1, message2, message3 + // should only impact 1 removed apihub operation (operation1-message2), + // not the remaining operation1-message1 and operation1-message3 + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('should only impact changed message when changing content type with multiple messages', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/message/change-content-type-with-multiple-messages') + + // Changing contentType of message1 in operation with message1 and message2 + // should only impact operation1-message1, not operation1-message2 + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('should not detect changes when removing unused message from channel', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/message/remove-unused-from-channel') + + // Removing a message definition from channel that no operation references + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('should not detect changes when adding unused component message', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/message/add-unused-component-message') + + // Adding a message to components/messages that is not referenced by any channel or operation + // should not impact any apihub operations + expectNoChanges(result) + }) + + test('should not detect changes when removing unused component message', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/message/remove-unused-component-message') + + // Removing a message from components/messages that is not referenced by any operation + // should not impact any apihub operations + expectNoChanges(result) + }) + }) + + describe('Schema tests', () => { + test('should detect added property in message schema', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/schema/add-property') + + expect(result).toEqual(changesSummaryMatcher({ [NON_BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [NON_BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('should detect removed property from message schema', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/schema/remove-property') + + expect(result).toEqual(changesSummaryMatcher({ [NON_BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [NON_BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('should detect changed property type in message schema', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/schema/change-property-type') + + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) }) - describe('tags', () => { - test('Tags are not duplicated', async () => { + describe('Tags tests', () => { + test('should not duplicate tags', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/tags') expect(result).toEqual(operationTypeMatcher({ @@ -157,6 +279,7 @@ describe.skip('AsyncAPI 3.0 Changelog', () => { 'sameTagInDifferentSiblings1', 'sameTagInDifferentSiblings2', 'sameTagInDifferentSiblings3', + 'sameTagInOperationSiblings1', 'tag', ]), })) diff --git a/test/projects/asyncapi-changes/channel/add-message-with-multiple-operations/after.yaml b/test/projects/asyncapi-changes/channel/add-message-with-multiple-operations/after.yaml new file mode 100644 index 00000000..a3a1cc89 --- /dev/null +++ b/test/projects/asyncapi-changes/channel/add-message-with-multiple-operations/after.yaml @@ -0,0 +1,42 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' + message3: + $ref: '#/components/messages/message3' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + - $ref: '#/channels/channel1/messages/message2' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string + message3: + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-changes/channel/add-message-with-multiple-operations/before.yaml b/test/projects/asyncapi-changes/channel/add-message-with-multiple-operations/before.yaml new file mode 100644 index 00000000..a6ae7999 --- /dev/null +++ b/test/projects/asyncapi-changes/channel/add-message-with-multiple-operations/before.yaml @@ -0,0 +1,34 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + - $ref: '#/channels/channel1/messages/message2' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-changes/channel/add-message/after.yaml b/test/projects/asyncapi-changes/channel/add-message/after.yaml new file mode 100644 index 00000000..dbe6803c --- /dev/null +++ b/test/projects/asyncapi-changes/channel/add-message/after.yaml @@ -0,0 +1,33 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-changes/operation/change/before.yaml b/test/projects/asyncapi-changes/channel/add-message/before.yaml similarity index 100% rename from test/projects/asyncapi-changes/operation/change/before.yaml rename to test/projects/asyncapi-changes/channel/add-message/before.yaml diff --git a/test/projects/asyncapi-changes/channel/change/after.yaml b/test/projects/asyncapi-changes/channel/change-address/after.yaml similarity index 100% rename from test/projects/asyncapi-changes/channel/change/after.yaml rename to test/projects/asyncapi-changes/channel/change-address/after.yaml diff --git a/test/projects/asyncapi-changes/channel/change/before.yaml b/test/projects/asyncapi-changes/channel/change-address/before.yaml similarity index 100% rename from test/projects/asyncapi-changes/channel/change/before.yaml rename to test/projects/asyncapi-changes/channel/change-address/before.yaml diff --git a/test/projects/asyncapi-changes/channel/add/after.yaml b/test/projects/asyncapi-changes/channel/change-reference/after.yaml similarity index 100% rename from test/projects/asyncapi-changes/channel/add/after.yaml rename to test/projects/asyncapi-changes/channel/change-reference/after.yaml diff --git a/test/projects/asyncapi-changes/channel/add/before.yaml b/test/projects/asyncapi-changes/channel/change-reference/before.yaml similarity index 100% rename from test/projects/asyncapi-changes/channel/add/before.yaml rename to test/projects/asyncapi-changes/channel/change-reference/before.yaml diff --git a/test/projects/asyncapi-changes/channel/remove-unused/after.yaml b/test/projects/asyncapi-changes/channel/remove-unused/after.yaml new file mode 100644 index 00000000..4bc38c26 --- /dev/null +++ b/test/projects/asyncapi-changes/channel/remove-unused/after.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changes/channel/remove-unused/before.yaml b/test/projects/asyncapi-changes/channel/remove-unused/before.yaml new file mode 100644 index 00000000..04bed214 --- /dev/null +++ b/test/projects/asyncapi-changes/channel/remove-unused/before.yaml @@ -0,0 +1,30 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + unusedChannel: + address: unused + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changes/message/add-to-operation-with-existing-messages/after.yaml b/test/projects/asyncapi-changes/message/add-to-operation-with-existing-messages/after.yaml new file mode 100644 index 00000000..1fbdc18a --- /dev/null +++ b/test/projects/asyncapi-changes/message/add-to-operation-with-existing-messages/after.yaml @@ -0,0 +1,43 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' + message3: + $ref: '#/components/messages/message3' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + - $ref: '#/channels/channel1/messages/message2' + - $ref: '#/channels/channel1/messages/message3' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string + message3: + payload: + type: object + properties: + productId: + type: string diff --git a/test/projects/asyncapi-changes/message/add-to-operation-with-existing-messages/before.yaml b/test/projects/asyncapi-changes/message/add-to-operation-with-existing-messages/before.yaml new file mode 100644 index 00000000..d06f58e4 --- /dev/null +++ b/test/projects/asyncapi-changes/message/add-to-operation-with-existing-messages/before.yaml @@ -0,0 +1,42 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' + message3: + $ref: '#/components/messages/message3' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + - $ref: '#/channels/channel1/messages/message2' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string + message3: + payload: + type: object + properties: + productId: + type: string diff --git a/test/projects/asyncapi-changes/message/add-to-operation/after.yaml b/test/projects/asyncapi-changes/message/add-to-operation/after.yaml new file mode 100644 index 00000000..a6ae7999 --- /dev/null +++ b/test/projects/asyncapi-changes/message/add-to-operation/after.yaml @@ -0,0 +1,34 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + - $ref: '#/channels/channel1/messages/message2' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-changes/message/add-to-operation/before.yaml b/test/projects/asyncapi-changes/message/add-to-operation/before.yaml new file mode 100644 index 00000000..dbe6803c --- /dev/null +++ b/test/projects/asyncapi-changes/message/add-to-operation/before.yaml @@ -0,0 +1,33 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-changes/message/add-unused-component-message/after.yaml b/test/projects/asyncapi-changes/message/add-unused-component-message/after.yaml new file mode 100644 index 00000000..d8a4af2d --- /dev/null +++ b/test/projects/asyncapi-changes/message/add-unused-component-message/after.yaml @@ -0,0 +1,31 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + unusedMessage: + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-changes/message/add-unused-component-message/before.yaml b/test/projects/asyncapi-changes/message/add-unused-component-message/before.yaml new file mode 100644 index 00000000..4bc38c26 --- /dev/null +++ b/test/projects/asyncapi-changes/message/add-unused-component-message/before.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changes/message/change-content-type-with-multiple-messages/after.yaml b/test/projects/asyncapi-changes/message/change-content-type-with-multiple-messages/after.yaml new file mode 100644 index 00000000..0b56932f --- /dev/null +++ b/test/projects/asyncapi-changes/message/change-content-type-with-multiple-messages/after.yaml @@ -0,0 +1,36 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + - $ref: '#/channels/channel1/messages/message2' +components: + messages: + message1: + contentType: application/xml + payload: + type: object + properties: + userId: + type: string + message2: + contentType: application/json + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-changes/message/change-content-type-with-multiple-messages/before.yaml b/test/projects/asyncapi-changes/message/change-content-type-with-multiple-messages/before.yaml new file mode 100644 index 00000000..9964ded9 --- /dev/null +++ b/test/projects/asyncapi-changes/message/change-content-type-with-multiple-messages/before.yaml @@ -0,0 +1,36 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + - $ref: '#/channels/channel1/messages/message2' +components: + messages: + message1: + contentType: application/json + payload: + type: object + properties: + userId: + type: string + message2: + contentType: application/json + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-changes/message/change-content-type/after.yaml b/test/projects/asyncapi-changes/message/change-content-type/after.yaml new file mode 100644 index 00000000..69007815 --- /dev/null +++ b/test/projects/asyncapi-changes/message/change-content-type/after.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + contentType: application/xml + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changes/message/change-content-type/before.yaml b/test/projects/asyncapi-changes/message/change-content-type/before.yaml new file mode 100644 index 00000000..0dae41b1 --- /dev/null +++ b/test/projects/asyncapi-changes/message/change-content-type/before.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + contentType: application/json + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changes/message/remove-from-operation-with-remaining-messages/after.yaml b/test/projects/asyncapi-changes/message/remove-from-operation-with-remaining-messages/after.yaml new file mode 100644 index 00000000..de517899 --- /dev/null +++ b/test/projects/asyncapi-changes/message/remove-from-operation-with-remaining-messages/after.yaml @@ -0,0 +1,42 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' + message3: + $ref: '#/components/messages/message3' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + - $ref: '#/channels/channel1/messages/message3' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string + message3: + payload: + type: object + properties: + productId: + type: string diff --git a/test/projects/asyncapi-changes/message/remove-from-operation-with-remaining-messages/before.yaml b/test/projects/asyncapi-changes/message/remove-from-operation-with-remaining-messages/before.yaml new file mode 100644 index 00000000..1fbdc18a --- /dev/null +++ b/test/projects/asyncapi-changes/message/remove-from-operation-with-remaining-messages/before.yaml @@ -0,0 +1,43 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' + message3: + $ref: '#/components/messages/message3' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + - $ref: '#/channels/channel1/messages/message2' + - $ref: '#/channels/channel1/messages/message3' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string + message3: + payload: + type: object + properties: + productId: + type: string diff --git a/test/projects/asyncapi-changes/message/remove-from-operation/after.yaml b/test/projects/asyncapi-changes/message/remove-from-operation/after.yaml new file mode 100644 index 00000000..dbe6803c --- /dev/null +++ b/test/projects/asyncapi-changes/message/remove-from-operation/after.yaml @@ -0,0 +1,33 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-changes/message/remove-from-operation/before.yaml b/test/projects/asyncapi-changes/message/remove-from-operation/before.yaml new file mode 100644 index 00000000..a6ae7999 --- /dev/null +++ b/test/projects/asyncapi-changes/message/remove-from-operation/before.yaml @@ -0,0 +1,34 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + - $ref: '#/channels/channel1/messages/message2' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-changes/message/remove-unused-component-message/after.yaml b/test/projects/asyncapi-changes/message/remove-unused-component-message/after.yaml new file mode 100644 index 00000000..4bc38c26 --- /dev/null +++ b/test/projects/asyncapi-changes/message/remove-unused-component-message/after.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changes/message/remove-unused-component-message/before.yaml b/test/projects/asyncapi-changes/message/remove-unused-component-message/before.yaml new file mode 100644 index 00000000..d8a4af2d --- /dev/null +++ b/test/projects/asyncapi-changes/message/remove-unused-component-message/before.yaml @@ -0,0 +1,31 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + unusedMessage: + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-changes/message/remove-unused-from-channel/after.yaml b/test/projects/asyncapi-changes/message/remove-unused-from-channel/after.yaml new file mode 100644 index 00000000..d8a4af2d --- /dev/null +++ b/test/projects/asyncapi-changes/message/remove-unused-from-channel/after.yaml @@ -0,0 +1,31 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + unusedMessage: + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-changes/message/remove-unused-from-channel/before.yaml b/test/projects/asyncapi-changes/message/remove-unused-from-channel/before.yaml new file mode 100644 index 00000000..0e3b8290 --- /dev/null +++ b/test/projects/asyncapi-changes/message/remove-unused-from-channel/before.yaml @@ -0,0 +1,33 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + unusedMessage: + $ref: '#/components/messages/unusedMessage' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + unusedMessage: + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-changes/operation/change/after.yaml b/test/projects/asyncapi-changes/operation/change-action/after.yaml similarity index 100% rename from test/projects/asyncapi-changes/operation/change/after.yaml rename to test/projects/asyncapi-changes/operation/change-action/after.yaml diff --git a/test/projects/asyncapi-changes/operation/change-action/before.yaml b/test/projects/asyncapi-changes/operation/change-action/before.yaml new file mode 100644 index 00000000..8139850a --- /dev/null +++ b/test/projects/asyncapi-changes/operation/change-action/before.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + diff --git a/test/projects/asyncapi-changes/operation/change-description-with-multiple-messages/after.yaml b/test/projects/asyncapi-changes/operation/change-description-with-multiple-messages/after.yaml new file mode 100644 index 00000000..3f04fd84 --- /dev/null +++ b/test/projects/asyncapi-changes/operation/change-description-with-multiple-messages/after.yaml @@ -0,0 +1,35 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + description: test2 + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + - $ref: '#/channels/channel1/messages/message2' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-changes/operation/change-description-with-multiple-messages/before.yaml b/test/projects/asyncapi-changes/operation/change-description-with-multiple-messages/before.yaml new file mode 100644 index 00000000..87e3276d --- /dev/null +++ b/test/projects/asyncapi-changes/operation/change-description-with-multiple-messages/before.yaml @@ -0,0 +1,35 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + description: test1 + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + - $ref: '#/channels/channel1/messages/message2' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-changes/schema/add-property/after.yaml b/test/projects/asyncapi-changes/schema/add-property/after.yaml new file mode 100644 index 00000000..4ce84ce2 --- /dev/null +++ b/test/projects/asyncapi-changes/schema/add-property/after.yaml @@ -0,0 +1,27 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + email: + type: string diff --git a/test/projects/asyncapi-changes/schema/add-property/before.yaml b/test/projects/asyncapi-changes/schema/add-property/before.yaml new file mode 100644 index 00000000..4bc38c26 --- /dev/null +++ b/test/projects/asyncapi-changes/schema/add-property/before.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changes/schema/change-property-type/after.yaml b/test/projects/asyncapi-changes/schema/change-property-type/after.yaml new file mode 100644 index 00000000..54c81d62 --- /dev/null +++ b/test/projects/asyncapi-changes/schema/change-property-type/after.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: integer diff --git a/test/projects/asyncapi-changes/schema/change-property-type/before.yaml b/test/projects/asyncapi-changes/schema/change-property-type/before.yaml new file mode 100644 index 00000000..4bc38c26 --- /dev/null +++ b/test/projects/asyncapi-changes/schema/change-property-type/before.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changes/schema/remove-property/after.yaml b/test/projects/asyncapi-changes/schema/remove-property/after.yaml new file mode 100644 index 00000000..4bc38c26 --- /dev/null +++ b/test/projects/asyncapi-changes/schema/remove-property/after.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changes/schema/remove-property/before.yaml b/test/projects/asyncapi-changes/schema/remove-property/before.yaml new file mode 100644 index 00000000..4ce84ce2 --- /dev/null +++ b/test/projects/asyncapi-changes/schema/remove-property/before.yaml @@ -0,0 +1,27 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + email: + type: string diff --git a/test/projects/asyncapi-changes/server/remove-from-channel/before.yaml b/test/projects/asyncapi-changes/server/remove-from-channel/before.yaml index 1ee10c9d..3a7cf703 100644 --- a/test/projects/asyncapi-changes/server/remove-from-channel/before.yaml +++ b/test/projects/asyncapi-changes/server/remove-from-channel/before.yaml @@ -9,6 +9,8 @@ servers: channels: channel1: address: channel1 + servers: + - $ref: '#/servers/production' messages: message1: $ref: '#/components/messages/message1' diff --git a/test/projects/asyncapi-changes/tags/after.yaml b/test/projects/asyncapi-changes/tags/after.yaml index 3d3c7f00..322bf105 100644 --- a/test/projects/asyncapi-changes/tags/after.yaml +++ b/test/projects/asyncapi-changes/tags/after.yaml @@ -36,6 +36,8 @@ operations: tags: - name: sameTagInDifferentChannels1 - name: sameTagInDifferentChannels3 + messages: + - $ref: '#/channels/userSignup/messages/userSignedUp' added2: action: send channel: @@ -44,12 +46,16 @@ operations: - name: sameTagInDifferentChannels1 - name: sameTagInDifferentChannels3 - name: sameTagInDifferentSiblings2 + messages: + - $ref: '#/channels/userUpdate/messages/userUpdated' added3: action: receive channel: $ref: '#/channels/orderCreate' tags: - name: sameTagInDifferentSiblings2 + messages: + - $ref: '#/channels/orderCreate/messages/orderCreated' changed1: action: send channel: @@ -58,12 +64,16 @@ operations: - name: sameTagInOperationSiblings1 - name: sameTagInDifferentSiblings3 - name: tag + messages: + - $ref: '#/channels/orderUpdate/messages/orderUpdated' changed2: action: receive channel: $ref: '#/channels/orderDelete' tags: - name: sameTagInDifferentSiblings3 + messages: + - $ref: '#/channels/orderDelete/messages/orderDeleted' components: messages: UserSignedUp: diff --git a/test/projects/asyncapi-changes/tags/before.yaml b/test/projects/asyncapi-changes/tags/before.yaml index f04171f2..191311c6 100644 --- a/test/projects/asyncapi-changes/tags/before.yaml +++ b/test/projects/asyncapi-changes/tags/before.yaml @@ -36,6 +36,8 @@ operations: tags: - name: sameTagInDifferentChannels1 - name: sameTagInDifferentChannels2 + messages: + - $ref: '#/channels/userSignup/messages/userSignedUp' removed2: action: send channel: @@ -44,12 +46,16 @@ operations: - name: sameTagInDifferentChannels1 - name: sameTagInDifferentChannels2 - name: sameTagInDifferentSiblings1 + messages: + - $ref: '#/channels/userUpdate/messages/userUpdated' removed3: action: receive channel: $ref: '#/channels/orderCreate' tags: - name: sameTagInDifferentSiblings1 + messages: + - $ref: '#/channels/orderCreate/messages/orderCreated' changed1: action: send channel: @@ -58,12 +64,16 @@ operations: - name: sameTagInOperationSiblings1 - name: tag - name: missingByDesign + messages: + - $ref: '#/channels/orderUpdate/messages/orderUpdated' changed2: action: receive channel: $ref: '#/channels/orderDelete' tags: - name: missingByDesign + messages: + - $ref: '#/channels/orderDelete/messages/orderDeleted' components: messages: UserSignedUp: From 67cdc6471bd7880533943057b0b4a9525bdfbb9d Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Fri, 13 Mar 2026 12:07:54 +0400 Subject: [PATCH 02/29] feat: More tests --- src/apitypes/async/async.changes.ts | 14 +++-- src/apitypes/async/async.utils.ts | 23 ++++++-- test/asyncapi-changes.test.ts | 56 ++++++++++++++++++- .../after.yaml | 42 ++++++++++++++ .../before.yaml | 42 ++++++++++++++ .../change-address-shared-channel/after.yaml | 39 +++++++++++++ .../change-address-shared-channel/before.yaml | 39 +++++++++++++ .../info/change-title/after.yaml | 25 +++++++++ .../info/change-title/before.yaml | 25 +++++++++ .../info/change-version/after.yaml | 25 +++++++++ .../info/change-version/before.yaml | 25 +++++++++ .../after.yaml | 51 +++++++++++++++++ .../before.yaml | 50 +++++++++++++++++ .../after.yaml | 42 ++++++++++++++ .../before.yaml | 42 ++++++++++++++ 15 files changed, 530 insertions(+), 10 deletions(-) create mode 100644 test/projects/asyncapi-changes/channel/change-address-no-impact-on-other-channel/after.yaml create mode 100644 test/projects/asyncapi-changes/channel/change-address-no-impact-on-other-channel/before.yaml create mode 100644 test/projects/asyncapi-changes/channel/change-address-shared-channel/after.yaml create mode 100644 test/projects/asyncapi-changes/channel/change-address-shared-channel/before.yaml create mode 100644 test/projects/asyncapi-changes/info/change-title/after.yaml create mode 100644 test/projects/asyncapi-changes/info/change-title/before.yaml create mode 100644 test/projects/asyncapi-changes/info/change-version/after.yaml create mode 100644 test/projects/asyncapi-changes/info/change-version/before.yaml create mode 100644 test/projects/asyncapi-changes/message/add-to-one-of-multiple-operations/after.yaml create mode 100644 test/projects/asyncapi-changes/message/add-to-one-of-multiple-operations/before.yaml create mode 100644 test/projects/asyncapi-changes/operation/change-action-no-impact-on-other/after.yaml create mode 100644 test/projects/asyncapi-changes/operation/change-action-no-impact-on-other/before.yaml diff --git a/src/apitypes/async/async.changes.ts b/src/apitypes/async/async.changes.ts index 058aaf5c..c3e275d9 100644 --- a/src/apitypes/async/async.changes.ts +++ b/src/apitypes/async/async.changes.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { calculateAsyncOperationId, isEmpty, isObject } from '../../utils' +import { isEmpty, isObject } from '../../utils' import { aggregateDiffsWithRollup, apiDiff, @@ -47,7 +47,7 @@ import { OperationsMap, } from '../../components' import { v3 as AsyncAPIV3 } from '@asyncapi/parser/esm/spec-types' -import { getAsyncMessageId } from './async.utils' +import { extractAsyncApiVersionDiff, extractInfoDiffs, getAsyncMessageId } from './async.utils' export const compareDocuments: DocumentsCompare = async ( operationsMap: OperationsMap, @@ -87,7 +87,7 @@ export const compareDocuments: DocumentsCompare = async ( ...NORMALIZE_OPTIONS, metaKey: DIFF_META_KEY, originsFlag: ORIGINS_SYMBOL, - normalizedResult: true, + normalizedResult: false, afterValueNormalizedProperty: AFTER_VALUE_NORMALIZED_PROPERTY, beforeValueNormalizedProperty: BEFORE_VALUE_NORMALIZED_PROPERTY, firstReferenceKeyProperty: FIRST_REFERENCE_KEY_PROPERTY, @@ -141,7 +141,7 @@ export const compareDocuments: DocumentsCompare = async ( const operation = operationsMap[key] return operation?.previous?.metadata?.asyncOperationId === asyncOperationId || operation?.current?.metadata?.asyncOperationId === asyncOperationId }) - const operation = matchingOperations.find(matchingOperation=> matchingOperation.endsWith(String(index+1))) + const operation = matchingOperations.find(matchingOperation => matchingOperation.endsWith(String(index + 1))) return operation || matchingOperations[index] } @@ -186,7 +186,11 @@ export const compareDocuments: DocumentsCompare = async ( const allOperationDiffs = (operationObject as WithAggregatedDiffs)[DIFFS_AGGREGATED_META_KEY] ?? [] const otherMessageDiffs = collectOtherMessageDiffs(messages, messageIndex) - operationDiffs = [...allOperationDiffs].filter(d => !otherMessageDiffs.has(d)) + operationDiffs = [ + ...([...allOperationDiffs].filter(d => !otherMessageDiffs.has(d))), + ...extractAsyncApiVersionDiff(merged), + ...extractInfoDiffs(merged), + ] } if (operationAddedOrRemoved) { // Level 1: message added/removed within an existing operation (analogous to REST method within path) diff --git a/src/apitypes/async/async.utils.ts b/src/apitypes/async/async.utils.ts index e315338d..e3c27338 100644 --- a/src/apitypes/async/async.utils.ts +++ b/src/apitypes/async/async.utils.ts @@ -43,6 +43,8 @@ import { FIRST_REFERENCE_KEY_PROPERTY, INLINE_REFS_FLAG, } from '../../consts' +import { WithAggregatedDiffs, WithDiffMetaRecord } from '../../types' +import { Diff, DIFF_META_KEY, DIFFS_AGGREGATED_META_KEY } from '@netcracker/qubership-apihub-api-diff' // Re-export shared utilities export { dump, getCustomTags, resolveApiAudience } from '../../utils/apihubSpecificationExtensions' @@ -147,8 +149,8 @@ export const createBaseAsyncApiSpec = ( ): TYPE.AsyncOperationData => ({ asyncapi: document.asyncapi || '3.0.0', info: document.info, - ...takeIfDefined({id: document.id}), - ...takeIfDefined({defaultContentType: document.defaultContentType}), + ...takeIfDefined({ id: document.id }), + ...takeIfDefined({ defaultContentType: document.defaultContentType }), operations, }) @@ -237,11 +239,11 @@ export const resolveAsyncApiOperationIdsFromRefs = ( } for (const message of messages) { - if(!isMessageObject(message)){ + if (!isMessageObject(message)) { continue } const inlineRefs = getSymbolValueIfDefined(message, INLINE_REFS_FLAG) as string[] | undefined - if (!inlineRefs || inlineRefs.length === 0){ + if (!inlineRefs || inlineRefs.length === 0) { continue } const lastInlineRef = inlineRefs.at(-1) @@ -269,3 +271,16 @@ export const resolveAsyncApiOperationIdsFromRefs = ( return resolved } +export function extractAsyncApiVersionDiff(doc: AsyncAPIV3.AsyncAPIObject): Diff[] { + const diff = (doc as WithDiffMetaRecord)[DIFF_META_KEY]?.asyncapi + return diff ? [diff] : [] +} + +export function extractInfoDiffs(doc: AsyncAPIV3.AsyncAPIObject): Diff[] { + const addOrRemoveInfoDiff = (doc as WithDiffMetaRecord)[DIFF_META_KEY]?.info + const infoInternalDiffs = (doc.info as WithAggregatedDiffs)?.[DIFFS_AGGREGATED_META_KEY] ?? [] + return [ + ...(addOrRemoveInfoDiff ? [addOrRemoveInfoDiff] : []), + ...infoInternalDiffs, + ] +} diff --git a/test/asyncapi-changes.test.ts b/test/asyncapi-changes.test.ts index 81bd45c4..93213dc1 100644 --- a/test/asyncapi-changes.test.ts +++ b/test/asyncapi-changes.test.ts @@ -25,7 +25,7 @@ describe('AsyncAPI 3.0 Changelog tests', () => { const result = await buildChangelogPackageDefaultConfig( 'asyncapi-changes/no-changes', [{ fileId: 'before.yaml', publish: true }], - [{ fileId: 'before.yaml' }], + [{ fileId: 'before.yaml', publish: true }], ) expectNoChanges(result) @@ -79,6 +79,15 @@ describe('AsyncAPI 3.0 Changelog tests', () => { expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) + test('should not impact other operation when changing action type', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/change-action-no-impact-on-other') + + // operation1 action changed (receive -> send), operation2 unchanged + // should only impact 1 apihub operation (operation1-message1) + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + test('should detect changed operation description with multiple messages', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/change-description-with-multiple-messages') @@ -126,6 +135,24 @@ describe('AsyncAPI 3.0 Changelog tests', () => { expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) + test('should impact all operations on shared channel when changing address', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/change-address-shared-channel') + + // operation1 and operation2 both reference channel1, address changed + // both apihub operations should be impacted + expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) + }) + + test('should not impact operation on other channel when changing address', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/change-address-no-impact-on-other-channel') + + // channel1 address changed, channel2 unchanged + // only operation1 (on channel1) should be impacted, not operation2 (on channel2) + expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + test('should detect added message definition in channel with multiple apihub operations', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/add-message-with-multiple-operations') @@ -190,6 +217,15 @@ describe('AsyncAPI 3.0 Changelog tests', () => { expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) + test('should not impact other operation when adding message to one', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/message/add-to-one-of-multiple-operations') + + // message2 added to operation1, operation2 unchanged + // should only impact 1 new apihub operation (operation1-message2) + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + test('should not add changes to existing messages when adding new message to operation', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/message/add-to-operation-with-existing-messages') @@ -267,6 +303,24 @@ describe('AsyncAPI 3.0 Changelog tests', () => { }) }) + describe('Info tests', () => { + test('should detect changed info version', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/info/change-version') + + // info.version changed (1.0.0 -> 2.0.0) — should be detected as a change in every apihub operation + expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('should detect changed info title', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/info/change-title') + + // info.title changed — should be detected as a change in every apihub operation + expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + }) + describe('Tags tests', () => { test('should not duplicate tags', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/tags') diff --git a/test/projects/asyncapi-changes/channel/change-address-no-impact-on-other-channel/after.yaml b/test/projects/asyncapi-changes/channel/change-address-no-impact-on-other-channel/after.yaml new file mode 100644 index 00000000..baef25a0 --- /dev/null +++ b/test/projects/asyncapi-changes/channel/change-address-no-impact-on-other-channel/after.yaml @@ -0,0 +1,42 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: new-address + messages: + message1: + $ref: '#/components/messages/message1' + channel2: + address: channel2 + messages: + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + operation2: + action: receive + channel: + $ref: '#/channels/channel2' + messages: + - $ref: '#/channels/channel2/messages/message2' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-changes/channel/change-address-no-impact-on-other-channel/before.yaml b/test/projects/asyncapi-changes/channel/change-address-no-impact-on-other-channel/before.yaml new file mode 100644 index 00000000..b3b61c0a --- /dev/null +++ b/test/projects/asyncapi-changes/channel/change-address-no-impact-on-other-channel/before.yaml @@ -0,0 +1,42 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: old-address + messages: + message1: + $ref: '#/components/messages/message1' + channel2: + address: channel2 + messages: + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + operation2: + action: receive + channel: + $ref: '#/channels/channel2' + messages: + - $ref: '#/channels/channel2/messages/message2' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-changes/channel/change-address-shared-channel/after.yaml b/test/projects/asyncapi-changes/channel/change-address-shared-channel/after.yaml new file mode 100644 index 00000000..b6f43d0b --- /dev/null +++ b/test/projects/asyncapi-changes/channel/change-address-shared-channel/after.yaml @@ -0,0 +1,39 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: new-address + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + operation2: + action: send + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message2' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-changes/channel/change-address-shared-channel/before.yaml b/test/projects/asyncapi-changes/channel/change-address-shared-channel/before.yaml new file mode 100644 index 00000000..55b18758 --- /dev/null +++ b/test/projects/asyncapi-changes/channel/change-address-shared-channel/before.yaml @@ -0,0 +1,39 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: old-address + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + operation2: + action: send + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message2' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-changes/info/change-title/after.yaml b/test/projects/asyncapi-changes/info/change-title/after.yaml new file mode 100644 index 00000000..2d22df2a --- /dev/null +++ b/test/projects/asyncapi-changes/info/change-title/after.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: New Title + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changes/info/change-title/before.yaml b/test/projects/asyncapi-changes/info/change-title/before.yaml new file mode 100644 index 00000000..873caf9e --- /dev/null +++ b/test/projects/asyncapi-changes/info/change-title/before.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Old Title + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changes/info/change-version/after.yaml b/test/projects/asyncapi-changes/info/change-version/after.yaml new file mode 100644 index 00000000..b728733f --- /dev/null +++ b/test/projects/asyncapi-changes/info/change-version/after.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 2.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changes/info/change-version/before.yaml b/test/projects/asyncapi-changes/info/change-version/before.yaml new file mode 100644 index 00000000..4bc38c26 --- /dev/null +++ b/test/projects/asyncapi-changes/info/change-version/before.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changes/message/add-to-one-of-multiple-operations/after.yaml b/test/projects/asyncapi-changes/message/add-to-one-of-multiple-operations/after.yaml new file mode 100644 index 00000000..382d8dff --- /dev/null +++ b/test/projects/asyncapi-changes/message/add-to-one-of-multiple-operations/after.yaml @@ -0,0 +1,51 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' + channel2: + address: channel2 + messages: + message3: + $ref: '#/components/messages/message3' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + - $ref: '#/channels/channel1/messages/message2' + operation2: + action: receive + channel: + $ref: '#/channels/channel2' + messages: + - $ref: '#/channels/channel2/messages/message3' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string + message3: + payload: + type: object + properties: + productId: + type: string diff --git a/test/projects/asyncapi-changes/message/add-to-one-of-multiple-operations/before.yaml b/test/projects/asyncapi-changes/message/add-to-one-of-multiple-operations/before.yaml new file mode 100644 index 00000000..99b18fe5 --- /dev/null +++ b/test/projects/asyncapi-changes/message/add-to-one-of-multiple-operations/before.yaml @@ -0,0 +1,50 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' + channel2: + address: channel2 + messages: + message3: + $ref: '#/components/messages/message3' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + operation2: + action: receive + channel: + $ref: '#/channels/channel2' + messages: + - $ref: '#/channels/channel2/messages/message3' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string + message3: + payload: + type: object + properties: + productId: + type: string diff --git a/test/projects/asyncapi-changes/operation/change-action-no-impact-on-other/after.yaml b/test/projects/asyncapi-changes/operation/change-action-no-impact-on-other/after.yaml new file mode 100644 index 00000000..35fb33d7 --- /dev/null +++ b/test/projects/asyncapi-changes/operation/change-action-no-impact-on-other/after.yaml @@ -0,0 +1,42 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + channel2: + address: channel2 + messages: + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: send + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + operation2: + action: receive + channel: + $ref: '#/channels/channel2' + messages: + - $ref: '#/channels/channel2/messages/message2' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-changes/operation/change-action-no-impact-on-other/before.yaml b/test/projects/asyncapi-changes/operation/change-action-no-impact-on-other/before.yaml new file mode 100644 index 00000000..c0441ac1 --- /dev/null +++ b/test/projects/asyncapi-changes/operation/change-action-no-impact-on-other/before.yaml @@ -0,0 +1,42 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + channel2: + address: channel2 + messages: + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + operation2: + action: receive + channel: + $ref: '#/channels/channel2' + messages: + - $ref: '#/channels/channel2/messages/message2' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string From 00ff16fcde4b5405f32cd95bf6638db763affc8b Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Mon, 16 Mar 2026 09:54:20 +0400 Subject: [PATCH 03/29] feat: Async api kind --- src/apitypes/async/async.changes.ts | 2 + src/components/compare/bwc.validation.ts | 71 ++++++++++++ test/asyncapi-apikind.test.ts | 136 +++++++++++++++++++++++ 3 files changed, 209 insertions(+) diff --git a/src/apitypes/async/async.changes.ts b/src/apitypes/async/async.changes.ts index c3e275d9..e32321d5 100644 --- a/src/apitypes/async/async.changes.ts +++ b/src/apitypes/async/async.changes.ts @@ -46,6 +46,7 @@ import { getOperationTags, OperationsMap, } from '../../components' +import { createAsyncApiCompatibilityScopeFunction } from '../../components/compare/bwc.validation' import { v3 as AsyncAPIV3 } from '@asyncapi/parser/esm/spec-types' import { extractAsyncApiVersionDiff, extractInfoDiffs, getAsyncMessageId } from './async.utils' @@ -91,6 +92,7 @@ export const compareDocuments: DocumentsCompare = async ( afterValueNormalizedProperty: AFTER_VALUE_NORMALIZED_PROPERTY, beforeValueNormalizedProperty: BEFORE_VALUE_NORMALIZED_PROPERTY, firstReferenceKeyProperty: FIRST_REFERENCE_KEY_PROPERTY, + apiCompatibilityScopeFunction: createAsyncApiCompatibilityScopeFunction(), }, ) as { merged: AsyncAPIV3.AsyncAPIObject; diffs: Diff[] } diff --git a/src/components/compare/bwc.validation.ts b/src/components/compare/bwc.validation.ts index f100a24c..24215446 100644 --- a/src/components/compare/bwc.validation.ts +++ b/src/components/compare/bwc.validation.ts @@ -30,6 +30,7 @@ import { } from '@netcracker/qubership-apihub-api-diff' import { getApiKindProperty } from '../document' import { OpenAPIV3 } from 'openapi-types' +import { v3 as AsyncAPIV3 } from '@asyncapi/parser/esm/spec-types' export const calculateOperationApiCompatibilityKind = ( beforeOperationObject: OpenAPIV3.OperationObject | undefined, @@ -158,3 +159,73 @@ export const createApihubApiCompatibilityScopeFunction = ( return undefined } } + +const ASYNC_OPERATION_PATH_LENGTH = 2 // operations/ +const ASYNC_CHANNEL_PATH_LENGTH = 2 // channels/ + +/** + * Creates an ApiCompatibilityScopeFunction for AsyncAPI documents. + * + * AsyncAPI has no document-level x-api-kind. The hierarchy is: + * Channel (x-api-kind) → Operation (x-api-kind override) → Messages + * + * The scope function receives normalized before/after objects where $refs are resolved, + * so operation.channel is the resolved channel object with all its properties. + * + * - operations/: operation x-api-kind overrides channel x-api-kind, defaults to bwc + * - channels/: channel's own x-api-kind, defaults to bwc + */ +export const createAsyncApiCompatibilityScopeFunction = (): ApiCompatibilityScopeFunction => { + return ( + path?: JsonPath, + beforeJso?: unknown, + afterJso?: unknown, + ): ApiCompatibilityKind | undefined => { + const pathLength = path?.length ?? 0 + + // Root level: default to BWC (no document-level x-api-kind in AsyncAPI) + if (pathLength === ROOT_PATH_LENGTH) { + return API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE + } + + const firstSegment = path?.[0] + + // operations/: resolve api-kind from operation x-api-kind with channel fallback + if (firstSegment === 'operations' && pathLength === ASYNC_OPERATION_PATH_LENGTH) { + // In normalized documents, operation.channel is the resolved channel object + const beforeChannelKind = getApiKindProperty((beforeJso as AsyncAPIV3.OperationObject | undefined)?.channel) + const afterChannelKind = getApiKindProperty((afterJso as AsyncAPIV3.OperationObject | undefined)?.channel) + + // Operation's own x-api-kind takes priority, falls back to channel's x-api-kind + const beforeOperationKind = getApiKindProperty(beforeJso, beforeChannelKind) ?? APIHUB_API_COMPATIBILITY_KIND_BWC + const afterOperationKind = getApiKindProperty(afterJso, afterChannelKind) ?? APIHUB_API_COMPATIBILITY_KIND_BWC + + const isRemoved = isObject(beforeJso) && !isObject(afterJso) + if (isRemoved) { + return beforeOperationKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC + ? API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE + : API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE + } + + if (beforeOperationKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC || afterOperationKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC) { + return API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE + } + + return API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE + } + + // channels/: use channel's own x-api-kind + if (firstSegment === 'channels' && pathLength === ASYNC_CHANNEL_PATH_LENGTH) { + const beforeChannelKind = getApiKindProperty(beforeJso) ?? APIHUB_API_COMPATIBILITY_KIND_BWC + const afterChannelKind = getApiKindProperty(afterJso) ?? APIHUB_API_COMPATIBILITY_KIND_BWC + + if (beforeChannelKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC || afterChannelKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC) { + return API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE + } + + return API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE + } + + return undefined + } +} diff --git a/test/asyncapi-apikind.test.ts b/test/asyncapi-apikind.test.ts index 58f9ab91..342d5110 100644 --- a/test/asyncapi-apikind.test.ts +++ b/test/asyncapi-apikind.test.ts @@ -6,6 +6,11 @@ import { } from '../src' import { calculateAsyncApiKind } from '../src/apitypes/async/async.utils' import { buildPackageWithDefaultConfig } from './helpers' +import { createAsyncApiCompatibilityScopeFunction } from '../src/components/compare/bwc.validation' +import { + API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE, + API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE, +} from '@netcracker/qubership-apihub-api-diff' describe('AsyncAPI apiKind calculation', () => { describe('Unit tests', () => { @@ -27,6 +32,137 @@ describe('AsyncAPI apiKind calculation', () => { }) }) + describe('Async createAsyncApiCompatibilityScopeFunction unit tests', () => { + const scopeFunction = createAsyncApiCompatibilityScopeFunction() + + describe('Root level', () => { + it('should return BWC for root path', () => { + expect(scopeFunction([], {}, {})).toBe(API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE) + }) + + it('should return BWC for undefined path', () => { + expect(scopeFunction(undefined, {}, {})).toBe(API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE) + }) + }) + + describe('Channels scope', () => { + it('should return BWC when channel has no x-api-kind', () => { + const before = { address: 'channel1' } + const after = { address: 'channel1' } + expect(scopeFunction(['channels', 'ch1'], before, after)).toBe(API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE) + }) + + it('should return no-BWC when before channel has x-api-kind no-BWC', () => { + const before = { address: 'channel1', 'x-api-kind': 'no-BWC' } + const after = { address: 'channel1' } + expect(scopeFunction(['channels', 'ch1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) + }) + + it('should return no-BWC when after channel has x-api-kind no-BWC', () => { + const before = { address: 'channel1' } + const after = { address: 'channel1', 'x-api-kind': 'no-BWC' } + expect(scopeFunction(['channels', 'ch1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) + }) + + it('should return 1', () => { + const before = { address: 'channel1', 'x-api-kind': 'BWC'} + const after = { address: 'channel1', 'x-api-kind': 'no-BWC' } + expect(scopeFunction(['channels', 'ch1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) + }) + + it('should return 2', () => { + const before = { address: 'channel1', 'x-api-kind': 'no-BWC'} + const after = { address: 'channel1', 'x-api-kind': 'BWC'} + expect(scopeFunction(['channels', 'ch1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) + }) + + it('should return 3', () => { + const before = { address: 'channel1', 'x-api-kind': 'BWC'} + const after = { address: 'channel1', 'x-api-kind': 'BWC'} + expect(scopeFunction(['channels', 'ch1'], before, after)).toBe(API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE) + }) + + it('should return 4', () => { + const before = { address: 'channel1', 'x-api-kind': 'no-BWC'} + const after = { address: 'channel1', 'x-api-kind': 'no-BWC'} + expect(scopeFunction(['channels', 'ch1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) + }) + }) + + describe('Operations scope', () => { + it('should return BWC when operation and channel have no x-api-kind', () => { + const before = { action: 'receive', channel: {} } + const after = { action: 'receive', channel: {} } + expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE) + }) + + it('should return no-BWC when before operation channel has x-api-kind no-BWC', () => { + const before = { action: 'receive', channel: { 'x-api-kind': 'no-BWC' } } + const after = { action: 'receive', channel: {} } + expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) + }) + + it('should return no-BWC when after operation channel has x-api-kind no-BWC', () => { + const before = { action: 'receive', channel: {} } + const after = { action: 'receive', channel: { 'x-api-kind': 'no-BWC' } } + expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) + }) + + it('should return no-BWC when before operation has x-api-kind no-BWC', () => { + const before = { action: 'receive', 'x-api-kind': 'no-BWC', channel: {} } + const after = { action: 'receive', channel: {} } + expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) + }) + + it('should return no-BWC when after operation has x-api-kind no-BWC', () => { + const before = { action: 'receive', channel: {} } + const after = { action: 'receive', 'x-api-kind': 'no-BWC', channel: {} } + expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) + }) + + it('should return BWC when operation x-api-kind BWC overrides channel x-api-kind no-BWC', () => { + const before = { action: 'receive', 'x-api-kind': 'BWC', channel: { 'x-api-kind': 'no-BWC' } } + const after = { action: 'receive', 'x-api-kind': 'BWC', channel: { 'x-api-kind': 'no-BWC' } } + expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE) + }) + + it('should return no-BWC when operation x-api-kind no-BWC overrides channel x-api-kind BWC', () => { + const before = { action: 'receive', 'x-api-kind': 'no-BWC', channel: { 'x-api-kind': 'BWC' } } + const after = { action: 'receive', 'x-api-kind': 'no-BWC', channel: { 'x-api-kind': 'BWC' } } + expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) + }) + + it('should return BWC when removed operation has no x-api-kind', () => { + const before = { action: 'receive', channel: {} } + expect(scopeFunction(['operations', 'op1'], before, undefined)).toBe(API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE) + }) + + it('should return no-BWC when removed operation has x-api-kind no-BWC', () => { + const before = { action: 'receive', 'x-api-kind': 'no-BWC', channel: {} } + expect(scopeFunction(['operations', 'op1'], before, undefined)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) + }) + + it('should return no-BWC when removed operation channel has x-api-kind no-BWC', () => { + const before = { action: 'receive', channel: { 'x-api-kind': 'no-BWC' } } + expect(scopeFunction(['operations', 'op1'], before, undefined)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) + }) + }) + + describe('Other paths', () => { + it('should return undefined for non-operations/non-channels paths', () => { + expect(scopeFunction(['components', 'messages'], {}, {})).toBeUndefined() + }) + + it('should return undefined for deeper operation paths', () => { + expect(scopeFunction(['operations', 'op1', 'channel'], {}, {})).toBeUndefined() + }) + + it('should return undefined for deeper channel paths', () => { + expect(scopeFunction(['channels', 'ch1', 'messages'], {}, {})).toBeUndefined() + }) + }) + }) + describe('AsyncAPI operation/channel compatibility apiKind application', () => { let operation: ApiOperation let operationWithChannelBwc: ApiOperation From 5bb3a3b37787e01de7f2805a8ab2b816946e5c3b Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Mon, 16 Mar 2026 16:30:31 +0400 Subject: [PATCH 04/29] feat: Review --- src/apitypes/async/async.changes.ts | 48 +++++-- src/apitypes/async/async.utils.ts | 19 +++ test/asyncapi-changes.test.ts | 133 ++++++++---------- .../channel/remove/before.yaml | 36 ----- .../change-default-content-type}/after.yaml | 9 +- .../change-default-content-type/before.yaml} | 11 +- .../rename => info/change-id}/after.yaml | 6 +- .../rename => info/change-id}/before.yaml | 4 +- .../after.yaml | 25 ++-- .../before.yaml | 24 ++-- .../remove-unused-from-channel/before.yaml | 33 ----- .../after.yaml | 42 ------ .../before.yaml | 42 ------ 13 files changed, 138 insertions(+), 294 deletions(-) delete mode 100644 test/projects/asyncapi-changes/channel/remove/before.yaml rename test/projects/asyncapi-changes/{message/remove-unused-from-channel => info/change-default-content-type}/after.yaml (76%) rename test/projects/asyncapi-changes/{channel/remove/after.yaml => info/change-default-content-type/before.yaml} (69%) rename test/projects/asyncapi-changes/{operation/rename => info/change-id}/after.yaml (89%) rename test/projects/asyncapi-changes/{operation/rename => info/change-id}/before.yaml (92%) rename test/projects/asyncapi-changes/message/{add-to-operation-with-existing-messages => change-shared-payload-with-multiple-messages}/after.yaml (57%) rename test/projects/asyncapi-changes/message/{add-to-operation-with-existing-messages => change-shared-payload-with-multiple-messages}/before.yaml (60%) delete mode 100644 test/projects/asyncapi-changes/message/remove-unused-from-channel/before.yaml delete mode 100644 test/projects/asyncapi-changes/operation/change-action-no-impact-on-other/after.yaml delete mode 100644 test/projects/asyncapi-changes/operation/change-action-no-impact-on-other/before.yaml diff --git a/src/apitypes/async/async.changes.ts b/src/apitypes/async/async.changes.ts index e32321d5..afbeeacf 100644 --- a/src/apitypes/async/async.changes.ts +++ b/src/apitypes/async/async.changes.ts @@ -48,7 +48,14 @@ import { } from '../../components' import { createAsyncApiCompatibilityScopeFunction } from '../../components/compare/bwc.validation' import { v3 as AsyncAPIV3 } from '@asyncapi/parser/esm/spec-types' -import { extractAsyncApiVersionDiff, extractInfoDiffs, getAsyncMessageId } from './async.utils' +import { + extractAsyncApiVersionDiff, + extractDefaultContentTypeDiff, + extractIdDiff, + extractInfoDiffs, + extractRootServersDiffs, + getAsyncMessageId, +} from './async.utils' export const compareDocuments: DocumentsCompare = async ( operationsMap: OperationsMap, @@ -111,17 +118,27 @@ export const compareDocuments: DocumentsCompare = async ( * diffs from sibling messages must be excluded to prevent them from leaking * into unrelated apihub operations. * - * Collects two kinds of diffs from other messages: + * Collects two kinds of diffs that belong exclusively to other messages: * 1. Aggregated content diffs from each sibling message object * 2. Array-level diffs for adding/removing sibling messages from the messages list + * + * Diffs shared between the current message and sibling messages (e.g. from a shared + * component schema) are NOT included, so they won't be incorrectly filtered out. */ - function collectOtherMessageDiffs(messages: AsyncAPIV3.MessageObject[], currentMessageIndex: number): Set { + function collectExclusiveOtherMessageDiffs(messages: AsyncAPIV3.MessageObject[], currentMessageIndex: number): Set { + const currentMessageDiffsArr = (messages[currentMessageIndex] as WithAggregatedDiffs)[DIFFS_AGGREGATED_META_KEY] + const currentMessageDiffs = new Set(currentMessageDiffsArr ?? []) + const otherDiffs = new Set() - for (const [idx, msg] of messages.entries()) { - if (idx === currentMessageIndex) continue - const msgDiffs = (msg as WithAggregatedDiffs)[DIFFS_AGGREGATED_META_KEY] - if (msgDiffs) { - for (const d of msgDiffs) { otherDiffs.add(d) } + for (const [messageIndex, message] of messages.entries()) { + if (messageIndex === currentMessageIndex) continue + const messageDiffs = (message as WithAggregatedDiffs)[DIFFS_AGGREGATED_META_KEY] + if (messageDiffs) { + for (const messageDiff of messageDiffs) { + if (!currentMessageDiffs.has(messageDiff)) { + otherDiffs.add(messageDiff) + } + } } } const messagesArrayMeta = (messages as WithDiffMetaRecord)[DIFF_META_KEY] @@ -149,9 +166,9 @@ export const compareDocuments: DocumentsCompare = async ( } // Iterate through operations in merged document - const { operations } = merged - if (operations && isObject(operations)) { - for (const [asyncOperationId, operationData] of Object.entries(operations)) { + const { operations: asyncOperation} = merged + if (asyncOperation && isObject(asyncOperation)) { + for (const [asyncOperationId, operationData] of Object.entries(asyncOperation)) { if (!operationData || !isObject(operationData)) { continue } @@ -187,18 +204,21 @@ export const compareDocuments: DocumentsCompare = async ( if (operationPotentiallyChanged) { const allOperationDiffs = (operationObject as WithAggregatedDiffs)[DIFFS_AGGREGATED_META_KEY] ?? [] - const otherMessageDiffs = collectOtherMessageDiffs(messages, messageIndex) + const otherMessageDiffs = collectExclusiveOtherMessageDiffs(messages, messageIndex) operationDiffs = [ - ...([...allOperationDiffs].filter(d => !otherMessageDiffs.has(d))), + ...([...allOperationDiffs].filter(diff => !otherMessageDiffs.has(diff))), ...extractAsyncApiVersionDiff(merged), ...extractInfoDiffs(merged), + ...extractIdDiff(merged), + ...extractDefaultContentTypeDiff(merged), + ...extractRootServersDiffs(merged), ] } if (operationAddedOrRemoved) { // Level 1: message added/removed within an existing operation (analogous to REST method within path) const messageAddedOrRemovedDiff = (messages as WithDiffMetaRecord)[DIFF_META_KEY]?.[messageIndex] // Level 2: entire operation added/removed (analogous to REST entire path) - const operationAddedOrRemovedDiff = (operations as WithDiffMetaRecord)[DIFF_META_KEY]?.[asyncOperationId] + const operationAddedOrRemovedDiff = (asyncOperation as WithDiffMetaRecord)[DIFF_META_KEY]?.[asyncOperationId] const diff = messageAddedOrRemovedDiff ?? operationAddedOrRemovedDiff if (diff) { operationDiffs.push(diff) diff --git a/src/apitypes/async/async.utils.ts b/src/apitypes/async/async.utils.ts index e3c27338..5acd1336 100644 --- a/src/apitypes/async/async.utils.ts +++ b/src/apitypes/async/async.utils.ts @@ -284,3 +284,22 @@ export function extractInfoDiffs(doc: AsyncAPIV3.AsyncAPIObject): Diff[] { ...infoInternalDiffs, ] } + +export function extractIdDiff(doc: AsyncAPIV3.AsyncAPIObject): Diff[] { + const diff = (doc as WithDiffMetaRecord)[DIFF_META_KEY]?.id + return diff ? [diff] : [] +} + +export function extractDefaultContentTypeDiff(doc: AsyncAPIV3.AsyncAPIObject): Diff[] { + const diff = (doc as WithDiffMetaRecord)[DIFF_META_KEY]?.defaultContentType + return diff ? [diff] : [] +} + +export function extractRootServersDiffs(doc: AsyncAPIV3.AsyncAPIObject): Diff[] { + const addOrRemoveServersDiff = (doc as WithDiffMetaRecord)[DIFF_META_KEY]?.servers + const serversInternalDiffs = (doc.servers as WithAggregatedDiffs | undefined)?.[DIFFS_AGGREGATED_META_KEY] ?? [] + return [ + ...(addOrRemoveServersDiff ? [addOrRemoveServersDiff] : []), + ...serversInternalDiffs, + ] +} diff --git a/test/asyncapi-changes.test.ts b/test/asyncapi-changes.test.ts index 93213dc1..1d0d8950 100644 --- a/test/asyncapi-changes.test.ts +++ b/test/asyncapi-changes.test.ts @@ -21,7 +21,7 @@ describe('AsyncAPI 3.0 Changelog tests', () => { expect(result).toEqual(numberOfImpactedOperationsMatcher(EMPTY_CHANGE_SUMMARY, ASYNCAPI_API_TYPE)) } - test('should detect no changes for identical documents', async () => { + test('should report no changes for identical documents', async () => { const result = await buildChangelogPackageDefaultConfig( 'asyncapi-changes/no-changes', [{ fileId: 'before.yaml', publish: true }], @@ -32,25 +32,25 @@ describe('AsyncAPI 3.0 Changelog tests', () => { }) describe('Operations tests', () => { - test('should detect added operation', async () => { + test('should report added operation', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/add') expect(result).toEqual(changesSummaryMatcher({ [NON_BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) expect(result).toEqual(numberOfImpactedOperationsMatcher({ [NON_BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) - test('should detect multiple added operations', async () => { + test('should report multiple added operations', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/add-multiple') expect(result).toEqual(changesSummaryMatcher({ [NON_BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) expect(result).toEqual(numberOfImpactedOperationsMatcher({ [NON_BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) }) - test('should detect removed operation', async () => { + test('should report removed operation', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/remove') expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) - test('should detect simultaneously added and removed operations', async () => { + test('should report simultaneously added and removed operations', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/add-remove') expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1, @@ -62,33 +62,14 @@ describe('AsyncAPI 3.0 Changelog tests', () => { }, ASYNCAPI_API_TYPE)) }) - test('should detect renamed operation as add and remove', async () => { - const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/rename') - expect(result).toEqual(changesSummaryMatcher({ - [BREAKING_CHANGE_TYPE]: 2, - }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ - [BREAKING_CHANGE_TYPE]: 2, - }, ASYNCAPI_API_TYPE)) - }) - - test('should detect changed operation action type', async () => { + test('should report changed operation action type', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/change-action') expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) - test('should not impact other operation when changing action type', async () => { - const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/change-action-no-impact-on-other') - - // operation1 action changed (receive -> send), operation2 unchanged - // should only impact 1 apihub operation (operation1-message1) - expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - }) - - test('should detect changed operation description with multiple messages', async () => { + test('should report changed operation description with multiple messages', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/change-description-with-multiple-messages') expect(result).toEqual(changesSummaryMatcher({ @@ -101,34 +82,27 @@ describe('AsyncAPI 3.0 Changelog tests', () => { }) describe('Channels tests', () => { - test('should detect changed operation channel reference', async () => { + test('should be tolerant to channel reference change', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/change-reference') expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) - test('should detect removed channel', async () => { - const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/remove') - - expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - }) - - test('should not detect changes when removing unused channel', async () => { + test('should not report changes when removing unused channel', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/remove-unused') expectNoChanges(result) }) - test('should detect changed channel address', async () => { + test('should report changed channel address', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/change-address') expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) - test('should detect added message definition in channel', async () => { + test('should report added message definition in channel', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/add-message') expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) @@ -153,7 +127,7 @@ describe('AsyncAPI 3.0 Changelog tests', () => { expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) - test('should detect added message definition in channel with multiple apihub operations', async () => { + test('should report added message definition in channel with multiple apihub operations', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/add-message-with-multiple-operations') expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) @@ -162,55 +136,60 @@ describe('AsyncAPI 3.0 Changelog tests', () => { }) describe('Servers tests', () => { - test('should detect added server in channel', async () => { + test('should report added server in channel', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/server/add-to-channel') - expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + // channel-level servers diff (unclassified) + root servers diff (annotation) + expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1, [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1, [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) - test('should detect removed server from channel', async () => { + test('should report removed server from channel', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/server/remove-from-channel') - expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + // channel-level servers diff (unclassified) + root servers diff (annotation) + expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1, [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1, [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) - test('should not detect changes when adding root servers', async () => { + test('should report added root servers', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/server/add-root') - expectNoChanges(result) + expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) - test('should not detect changes when removing root servers', async () => { + test('should report removed root servers', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/server/remove-root') - expectNoChanges(result) + expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) - test('should not detect changes when changing root servers', async () => { + test('should report changed root servers', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/server/change-root') - expectNoChanges(result) + expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) }) describe('Messages tests', () => { - test('should detect added message reference in operation', async () => { + test('should report added APIHUB operation when message reference is added to async operation', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/message/add-to-operation') expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) - test('should detect removed message reference from operation', async () => { + test('should report removed APIHUB operation when message reference is removed to async operation', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/message/remove-from-operation') expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) - test('should detect changed message content type', async () => { + test('should report changed message content type', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/message/change-content-type') expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) @@ -226,16 +205,6 @@ describe('AsyncAPI 3.0 Changelog tests', () => { expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) - test('should not add changes to existing messages when adding new message to operation', async () => { - const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/message/add-to-operation-with-existing-messages') - - // Adding message3 to operation with existing message1 and message2 - // should only impact 1 new apihub operation (operation1-message3), - // not the existing operation1-message1 and operation1-message2 - expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - }) - test('should not add changes to remaining messages when removing message from operation', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/message/remove-from-operation-with-remaining-messages') @@ -255,15 +224,16 @@ describe('AsyncAPI 3.0 Changelog tests', () => { expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) - test('should not detect changes when removing unused message from channel', async () => { - const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/message/remove-unused-from-channel') + test('should impact both operations when changing shared payload with multiple messages', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/message/change-shared-payload-with-multiple-messages') - // Removing a message definition from channel that no operation references + // message1 and message2 both reference SharedPayload schema + // changing SharedPayload type should impact both apihub operations expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) }) - test('should not detect changes when adding unused component message', async () => { + test('should not report changes when adding unused component message', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/message/add-unused-component-message') // Adding a message to components/messages that is not referenced by any channel or operation @@ -271,7 +241,7 @@ describe('AsyncAPI 3.0 Changelog tests', () => { expectNoChanges(result) }) - test('should not detect changes when removing unused component message', async () => { + test('should not report changes when removing unused component message', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/message/remove-unused-component-message') // Removing a message from components/messages that is not referenced by any operation @@ -281,21 +251,21 @@ describe('AsyncAPI 3.0 Changelog tests', () => { }) describe('Schema tests', () => { - test('should detect added property in message schema', async () => { + test('should report added property in message schema', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/schema/add-property') expect(result).toEqual(changesSummaryMatcher({ [NON_BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) expect(result).toEqual(numberOfImpactedOperationsMatcher({ [NON_BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) - test('should detect removed property from message schema', async () => { + test('should report removed property from message schema', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/schema/remove-property') expect(result).toEqual(changesSummaryMatcher({ [NON_BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) expect(result).toEqual(numberOfImpactedOperationsMatcher({ [NON_BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) - test('should detect changed property type in message schema', async () => { + test('should report changed property type in message schema', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/schema/change-property-type') expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) @@ -304,7 +274,7 @@ describe('AsyncAPI 3.0 Changelog tests', () => { }) describe('Info tests', () => { - test('should detect changed info version', async () => { + test('should report changed info version', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/info/change-version') // info.version changed (1.0.0 -> 2.0.0) — should be detected as a change in every apihub operation @@ -312,13 +282,28 @@ describe('AsyncAPI 3.0 Changelog tests', () => { expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) - test('should detect changed info title', async () => { + test('should report changed info title', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/info/change-title') // info.title changed — should be detected as a change in every apihub operation expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) + + test('should report changed document id', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/info/change-id') + + expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('should report changed defaultContentType', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/info/change-default-content-type') + + // Root-level defaultContentType change + propagated breaking change in message contentType + expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1, [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1, [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) }) describe('Tags tests', () => { diff --git a/test/projects/asyncapi-changes/channel/remove/before.yaml b/test/projects/asyncapi-changes/channel/remove/before.yaml deleted file mode 100644 index fcd6da1c..00000000 --- a/test/projects/asyncapi-changes/channel/remove/before.yaml +++ /dev/null @@ -1,36 +0,0 @@ -asyncapi: 3.0.0 -info: - title: Test AsyncAPI - version: 1.0.0 -channels: - channel1: - address: address1 - messages: - message1: - $ref: '#/components/messages/message1' - channel2: - address: address2 - messages: - message1: - $ref: '#/components/messages/message1' -operations: - operation1: - action: receive - channel: - $ref: '#/channels/channel1' - messages: - - $ref: '#/channels/channel1/messages/message1' - operation2: - action: receive - channel: - $ref: '#/channels/channel2' - messages: - - $ref: '#/channels/channel2/messages/message1' -components: - messages: - message1: - payload: - type: object - properties: - userId: - type: string diff --git a/test/projects/asyncapi-changes/message/remove-unused-from-channel/after.yaml b/test/projects/asyncapi-changes/info/change-default-content-type/after.yaml similarity index 76% rename from test/projects/asyncapi-changes/message/remove-unused-from-channel/after.yaml rename to test/projects/asyncapi-changes/info/change-default-content-type/after.yaml index d8a4af2d..cd55ff34 100644 --- a/test/projects/asyncapi-changes/message/remove-unused-from-channel/after.yaml +++ b/test/projects/asyncapi-changes/info/change-default-content-type/after.yaml @@ -1,7 +1,8 @@ asyncapi: 3.0.0 info: - title: Test AsyncAPI + title: Test version: 1.0.0 +defaultContentType: application/xml channels: channel1: address: channel1 @@ -23,9 +24,3 @@ components: properties: userId: type: string - unusedMessage: - payload: - type: object - properties: - orderId: - type: string diff --git a/test/projects/asyncapi-changes/channel/remove/after.yaml b/test/projects/asyncapi-changes/info/change-default-content-type/before.yaml similarity index 69% rename from test/projects/asyncapi-changes/channel/remove/after.yaml rename to test/projects/asyncapi-changes/info/change-default-content-type/before.yaml index b94ac3ac..faa6b464 100644 --- a/test/projects/asyncapi-changes/channel/remove/after.yaml +++ b/test/projects/asyncapi-changes/info/change-default-content-type/before.yaml @@ -1,10 +1,11 @@ asyncapi: 3.0.0 info: - title: Test AsyncAPI + title: Test version: 1.0.0 +defaultContentType: application/json channels: channel1: - address: address1 + address: channel1 messages: message1: $ref: '#/components/messages/message1' @@ -15,12 +16,6 @@ operations: $ref: '#/channels/channel1' messages: - $ref: '#/channels/channel1/messages/message1' - operation2: - action: receive - channel: - $ref: '#/channels/channel1' - messages: - - $ref: '#/channels/channel1/messages/message1' components: messages: message1: diff --git a/test/projects/asyncapi-changes/operation/rename/after.yaml b/test/projects/asyncapi-changes/info/change-id/after.yaml similarity index 89% rename from test/projects/asyncapi-changes/operation/rename/after.yaml rename to test/projects/asyncapi-changes/info/change-id/after.yaml index c486def8..dd85dcf8 100644 --- a/test/projects/asyncapi-changes/operation/rename/after.yaml +++ b/test/projects/asyncapi-changes/info/change-id/after.yaml @@ -1,6 +1,7 @@ asyncapi: 3.0.0 +id: urn:example:com:new info: - title: Test AsyncAPI + title: Test version: 1.0.0 channels: channel1: @@ -9,7 +10,7 @@ channels: message1: $ref: '#/components/messages/message1' operations: - new-operation1: + operation1: action: receive channel: $ref: '#/channels/channel1' @@ -23,4 +24,3 @@ components: properties: userId: type: string - diff --git a/test/projects/asyncapi-changes/operation/rename/before.yaml b/test/projects/asyncapi-changes/info/change-id/before.yaml similarity index 92% rename from test/projects/asyncapi-changes/operation/rename/before.yaml rename to test/projects/asyncapi-changes/info/change-id/before.yaml index 8139850a..60389aeb 100644 --- a/test/projects/asyncapi-changes/operation/rename/before.yaml +++ b/test/projects/asyncapi-changes/info/change-id/before.yaml @@ -1,6 +1,7 @@ asyncapi: 3.0.0 +id: urn:example:com:old info: - title: Test AsyncAPI + title: Test version: 1.0.0 channels: channel1: @@ -23,4 +24,3 @@ components: properties: userId: type: string - diff --git a/test/projects/asyncapi-changes/message/add-to-operation-with-existing-messages/after.yaml b/test/projects/asyncapi-changes/message/change-shared-payload-with-multiple-messages/after.yaml similarity index 57% rename from test/projects/asyncapi-changes/message/add-to-operation-with-existing-messages/after.yaml rename to test/projects/asyncapi-changes/message/change-shared-payload-with-multiple-messages/after.yaml index 1fbdc18a..2dac214b 100644 --- a/test/projects/asyncapi-changes/message/add-to-operation-with-existing-messages/after.yaml +++ b/test/projects/asyncapi-changes/message/change-shared-payload-with-multiple-messages/after.yaml @@ -10,8 +10,6 @@ channels: $ref: '#/components/messages/message1' message2: $ref: '#/components/messages/message2' - message3: - $ref: '#/components/messages/message3' operations: operation1: action: receive @@ -20,24 +18,17 @@ operations: messages: - $ref: '#/channels/channel1/messages/message1' - $ref: '#/channels/channel1/messages/message2' - - $ref: '#/channels/channel1/messages/message3' components: + schemas: + SharedPayload: + type: object + properties: + userId: + type: string messages: message1: payload: - type: object - properties: - userId: - type: string + $ref: '#/components/schemas/SharedPayload' message2: payload: - type: object - properties: - orderId: - type: string - message3: - payload: - type: object - properties: - productId: - type: string + $ref: '#/components/schemas/SharedPayload' diff --git a/test/projects/asyncapi-changes/message/add-to-operation-with-existing-messages/before.yaml b/test/projects/asyncapi-changes/message/change-shared-payload-with-multiple-messages/before.yaml similarity index 60% rename from test/projects/asyncapi-changes/message/add-to-operation-with-existing-messages/before.yaml rename to test/projects/asyncapi-changes/message/change-shared-payload-with-multiple-messages/before.yaml index d06f58e4..1813f211 100644 --- a/test/projects/asyncapi-changes/message/add-to-operation-with-existing-messages/before.yaml +++ b/test/projects/asyncapi-changes/message/change-shared-payload-with-multiple-messages/before.yaml @@ -10,8 +10,6 @@ channels: $ref: '#/components/messages/message1' message2: $ref: '#/components/messages/message2' - message3: - $ref: '#/components/messages/message3' operations: operation1: action: receive @@ -21,22 +19,16 @@ operations: - $ref: '#/channels/channel1/messages/message1' - $ref: '#/channels/channel1/messages/message2' components: + schemas: + SharedPayload: + type: object + properties: + userId: + type: number messages: message1: payload: - type: object - properties: - userId: - type: string + $ref: '#/components/schemas/SharedPayload' message2: payload: - type: object - properties: - orderId: - type: string - message3: - payload: - type: object - properties: - productId: - type: string + $ref: '#/components/schemas/SharedPayload' diff --git a/test/projects/asyncapi-changes/message/remove-unused-from-channel/before.yaml b/test/projects/asyncapi-changes/message/remove-unused-from-channel/before.yaml deleted file mode 100644 index 0e3b8290..00000000 --- a/test/projects/asyncapi-changes/message/remove-unused-from-channel/before.yaml +++ /dev/null @@ -1,33 +0,0 @@ -asyncapi: 3.0.0 -info: - title: Test AsyncAPI - version: 1.0.0 -channels: - channel1: - address: channel1 - messages: - message1: - $ref: '#/components/messages/message1' - unusedMessage: - $ref: '#/components/messages/unusedMessage' -operations: - operation1: - action: receive - channel: - $ref: '#/channels/channel1' - messages: - - $ref: '#/channels/channel1/messages/message1' -components: - messages: - message1: - payload: - type: object - properties: - userId: - type: string - unusedMessage: - payload: - type: object - properties: - orderId: - type: string diff --git a/test/projects/asyncapi-changes/operation/change-action-no-impact-on-other/after.yaml b/test/projects/asyncapi-changes/operation/change-action-no-impact-on-other/after.yaml deleted file mode 100644 index 35fb33d7..00000000 --- a/test/projects/asyncapi-changes/operation/change-action-no-impact-on-other/after.yaml +++ /dev/null @@ -1,42 +0,0 @@ -asyncapi: 3.0.0 -info: - title: Test AsyncAPI - version: 1.0.0 -channels: - channel1: - address: channel1 - messages: - message1: - $ref: '#/components/messages/message1' - channel2: - address: channel2 - messages: - message2: - $ref: '#/components/messages/message2' -operations: - operation1: - action: send - channel: - $ref: '#/channels/channel1' - messages: - - $ref: '#/channels/channel1/messages/message1' - operation2: - action: receive - channel: - $ref: '#/channels/channel2' - messages: - - $ref: '#/channels/channel2/messages/message2' -components: - messages: - message1: - payload: - type: object - properties: - userId: - type: string - message2: - payload: - type: object - properties: - orderId: - type: string diff --git a/test/projects/asyncapi-changes/operation/change-action-no-impact-on-other/before.yaml b/test/projects/asyncapi-changes/operation/change-action-no-impact-on-other/before.yaml deleted file mode 100644 index c0441ac1..00000000 --- a/test/projects/asyncapi-changes/operation/change-action-no-impact-on-other/before.yaml +++ /dev/null @@ -1,42 +0,0 @@ -asyncapi: 3.0.0 -info: - title: Test AsyncAPI - version: 1.0.0 -channels: - channel1: - address: channel1 - messages: - message1: - $ref: '#/components/messages/message1' - channel2: - address: channel2 - messages: - message2: - $ref: '#/components/messages/message2' -operations: - operation1: - action: receive - channel: - $ref: '#/channels/channel1' - messages: - - $ref: '#/channels/channel1/messages/message1' - operation2: - action: receive - channel: - $ref: '#/channels/channel2' - messages: - - $ref: '#/channels/channel2/messages/message2' -components: - messages: - message1: - payload: - type: object - properties: - userId: - type: string - message2: - payload: - type: object - properties: - orderId: - type: string From 76aaf9d07825ca3aa225fa8dff48248e2879932f Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Mon, 16 Mar 2026 17:14:49 +0400 Subject: [PATCH 05/29] feat: ApiKind tests refactoring --- test/asyncapi-apikind.test.ts | 274 +++++++++++++++++----------------- 1 file changed, 138 insertions(+), 136 deletions(-) diff --git a/test/asyncapi-apikind.test.ts b/test/asyncapi-apikind.test.ts index 342d5110..cb996004 100644 --- a/test/asyncapi-apikind.test.ts +++ b/test/asyncapi-apikind.test.ts @@ -14,7 +14,7 @@ import { describe('AsyncAPI apiKind calculation', () => { describe('Unit tests', () => { - it('should calculate apiKind from operation and channel values', () => { + it('should resolve effective apiKind from operation and channel x-api-kind', () => { const data = [ // Operation ApiKind, Channel ApiKnd, Result [undefined, undefined, APIHUB_API_COMPATIBILITY_KIND_BWC], @@ -30,135 +30,135 @@ describe('AsyncAPI apiKind calculation', () => { expect(result).toBe(expected) }) }) - }) - - describe('Async createAsyncApiCompatibilityScopeFunction unit tests', () => { - const scopeFunction = createAsyncApiCompatibilityScopeFunction() - - describe('Root level', () => { - it('should return BWC for root path', () => { - expect(scopeFunction([], {}, {})).toBe(API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE) - }) - it('should return BWC for undefined path', () => { - expect(scopeFunction(undefined, {}, {})).toBe(API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE) - }) - }) - - describe('Channels scope', () => { - it('should return BWC when channel has no x-api-kind', () => { - const before = { address: 'channel1' } - const after = { address: 'channel1' } - expect(scopeFunction(['channels', 'ch1'], before, after)).toBe(API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE) - }) - - it('should return no-BWC when before channel has x-api-kind no-BWC', () => { - const before = { address: 'channel1', 'x-api-kind': 'no-BWC' } - const after = { address: 'channel1' } - expect(scopeFunction(['channels', 'ch1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) - }) - - it('should return no-BWC when after channel has x-api-kind no-BWC', () => { - const before = { address: 'channel1' } - const after = { address: 'channel1', 'x-api-kind': 'no-BWC' } - expect(scopeFunction(['channels', 'ch1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) - }) - - it('should return 1', () => { - const before = { address: 'channel1', 'x-api-kind': 'BWC'} - const after = { address: 'channel1', 'x-api-kind': 'no-BWC' } - expect(scopeFunction(['channels', 'ch1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) - }) - - it('should return 2', () => { - const before = { address: 'channel1', 'x-api-kind': 'no-BWC'} - const after = { address: 'channel1', 'x-api-kind': 'BWC'} - expect(scopeFunction(['channels', 'ch1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) - }) - - it('should return 3', () => { - const before = { address: 'channel1', 'x-api-kind': 'BWC'} - const after = { address: 'channel1', 'x-api-kind': 'BWC'} - expect(scopeFunction(['channels', 'ch1'], before, after)).toBe(API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE) - }) - - it('should return 4', () => { - const before = { address: 'channel1', 'x-api-kind': 'no-BWC'} - const after = { address: 'channel1', 'x-api-kind': 'no-BWC'} - expect(scopeFunction(['channels', 'ch1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) - }) - }) - - describe('Operations scope', () => { - it('should return BWC when operation and channel have no x-api-kind', () => { - const before = { action: 'receive', channel: {} } - const after = { action: 'receive', channel: {} } - expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE) - }) - - it('should return no-BWC when before operation channel has x-api-kind no-BWC', () => { - const before = { action: 'receive', channel: { 'x-api-kind': 'no-BWC' } } - const after = { action: 'receive', channel: {} } - expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) - }) - - it('should return no-BWC when after operation channel has x-api-kind no-BWC', () => { - const before = { action: 'receive', channel: {} } - const after = { action: 'receive', channel: { 'x-api-kind': 'no-BWC' } } - expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) - }) - - it('should return no-BWC when before operation has x-api-kind no-BWC', () => { - const before = { action: 'receive', 'x-api-kind': 'no-BWC', channel: {} } - const after = { action: 'receive', channel: {} } - expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) - }) - - it('should return no-BWC when after operation has x-api-kind no-BWC', () => { - const before = { action: 'receive', channel: {} } - const after = { action: 'receive', 'x-api-kind': 'no-BWC', channel: {} } - expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) - }) - - it('should return BWC when operation x-api-kind BWC overrides channel x-api-kind no-BWC', () => { - const before = { action: 'receive', 'x-api-kind': 'BWC', channel: { 'x-api-kind': 'no-BWC' } } - const after = { action: 'receive', 'x-api-kind': 'BWC', channel: { 'x-api-kind': 'no-BWC' } } - expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE) - }) - - it('should return no-BWC when operation x-api-kind no-BWC overrides channel x-api-kind BWC', () => { - const before = { action: 'receive', 'x-api-kind': 'no-BWC', channel: { 'x-api-kind': 'BWC' } } - const after = { action: 'receive', 'x-api-kind': 'no-BWC', channel: { 'x-api-kind': 'BWC' } } - expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) - }) - - it('should return BWC when removed operation has no x-api-kind', () => { - const before = { action: 'receive', channel: {} } - expect(scopeFunction(['operations', 'op1'], before, undefined)).toBe(API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE) - }) - - it('should return no-BWC when removed operation has x-api-kind no-BWC', () => { - const before = { action: 'receive', 'x-api-kind': 'no-BWC', channel: {} } - expect(scopeFunction(['operations', 'op1'], before, undefined)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) - }) - - it('should return no-BWC when removed operation channel has x-api-kind no-BWC', () => { - const before = { action: 'receive', channel: { 'x-api-kind': 'no-BWC' } } - expect(scopeFunction(['operations', 'op1'], before, undefined)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) - }) - }) - - describe('Other paths', () => { - it('should return undefined for non-operations/non-channels paths', () => { - expect(scopeFunction(['components', 'messages'], {}, {})).toBeUndefined() - }) - - it('should return undefined for deeper operation paths', () => { - expect(scopeFunction(['operations', 'op1', 'channel'], {}, {})).toBeUndefined() - }) - - it('should return undefined for deeper channel paths', () => { - expect(scopeFunction(['channels', 'ch1', 'messages'], {}, {})).toBeUndefined() + describe('Changelog backward compatibility scope function', () => { + const scopeFunction = createAsyncApiCompatibilityScopeFunction() + + describe('Root level', () => { + it('should return BWC for root path', () => { + expect(scopeFunction([], {}, {})).toBe(API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE) + }) + + it('should return BWC for undefined path', () => { + expect(scopeFunction(undefined, {}, {})).toBe(API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE) + }) + }) + + describe('Channels scope', () => { + it('should return BWC when channel has no x-api-kind', () => { + const before = { address: 'channel1' } + const after = { address: 'channel1' } + expect(scopeFunction(['channels', 'ch1'], before, after)).toBe(API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE) + }) + + it('should return no-BWC when before channel has x-api-kind no-BWC', () => { + const before = { address: 'channel1', 'x-api-kind': 'no-BWC' } + const after = { address: 'channel1' } + expect(scopeFunction(['channels', 'ch1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) + }) + + it('should return no-BWC when after channel has x-api-kind no-BWC', () => { + const before = { address: 'channel1' } + const after = { address: 'channel1', 'x-api-kind': 'no-BWC' } + expect(scopeFunction(['channels', 'ch1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) + }) + + it('should return no-BWC when before channel BWC and after channel no-BWC', () => { + const before = { address: 'channel1', 'x-api-kind': 'BWC'} + const after = { address: 'channel1', 'x-api-kind': 'no-BWC' } + expect(scopeFunction(['channels', 'ch1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) + }) + + it('should return no-BWC when before channel no-BWC and after channel BWC', () => { + const before = { address: 'channel1', 'x-api-kind': 'no-BWC'} + const after = { address: 'channel1', 'x-api-kind': 'BWC'} + expect(scopeFunction(['channels', 'ch1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) + }) + + it('should return BWC when both channels have x-api-kind BWC', () => { + const before = { address: 'channel1', 'x-api-kind': 'BWC'} + const after = { address: 'channel1', 'x-api-kind': 'BWC'} + expect(scopeFunction(['channels', 'ch1'], before, after)).toBe(API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE) + }) + + it('should return no-BWC when both channels have x-api-kind no-BWC', () => { + const before = { address: 'channel1', 'x-api-kind': 'no-BWC'} + const after = { address: 'channel1', 'x-api-kind': 'no-BWC'} + expect(scopeFunction(['channels', 'ch1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) + }) + }) + + describe('Operations scope', () => { + it('should return BWC when operation and channel have no x-api-kind', () => { + const before = { action: 'receive', channel: {} } + const after = { action: 'receive', channel: {} } + expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE) + }) + + it('should return no-BWC when before operation channel has x-api-kind no-BWC', () => { + const before = { action: 'receive', channel: { 'x-api-kind': 'no-BWC' } } + const after = { action: 'receive', channel: {} } + expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) + }) + + it('should return no-BWC when after operation channel has x-api-kind no-BWC', () => { + const before = { action: 'receive', channel: {} } + const after = { action: 'receive', channel: { 'x-api-kind': 'no-BWC' } } + expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) + }) + + it('should return no-BWC when before operation has x-api-kind no-BWC', () => { + const before = { action: 'receive', 'x-api-kind': 'no-BWC', channel: {} } + const after = { action: 'receive', channel: {} } + expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) + }) + + it('should return no-BWC when after operation has x-api-kind no-BWC', () => { + const before = { action: 'receive', channel: {} } + const after = { action: 'receive', 'x-api-kind': 'no-BWC', channel: {} } + expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) + }) + + it('should return BWC when operation x-api-kind BWC overrides channel x-api-kind no-BWC', () => { + const before = { action: 'receive', 'x-api-kind': 'BWC', channel: { 'x-api-kind': 'no-BWC' } } + const after = { action: 'receive', 'x-api-kind': 'BWC', channel: { 'x-api-kind': 'no-BWC' } } + expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE) + }) + + it('should return no-BWC when operation x-api-kind no-BWC overrides channel x-api-kind BWC', () => { + const before = { action: 'receive', 'x-api-kind': 'no-BWC', channel: { 'x-api-kind': 'BWC' } } + const after = { action: 'receive', 'x-api-kind': 'no-BWC', channel: { 'x-api-kind': 'BWC' } } + expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) + }) + + it('should return BWC when removed operation has no x-api-kind', () => { + const before = { action: 'receive', channel: {} } + expect(scopeFunction(['operations', 'op1'], before, undefined)).toBe(API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE) + }) + + it('should return no-BWC when removed operation has x-api-kind no-BWC', () => { + const before = { action: 'receive', 'x-api-kind': 'no-BWC', channel: {} } + expect(scopeFunction(['operations', 'op1'], before, undefined)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) + }) + + it('should return no-BWC when removed operation channel has x-api-kind no-BWC', () => { + const before = { action: 'receive', channel: { 'x-api-kind': 'no-BWC' } } + expect(scopeFunction(['operations', 'op1'], before, undefined)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) + }) + }) + + describe('Other paths', () => { + it('should return undefined for non-operations/non-channels paths', () => { + expect(scopeFunction(['components', 'messages'], {}, {})).toBeUndefined() + }) + + it('should return undefined for deeper operation paths', () => { + expect(scopeFunction(['operations', 'op1', 'channel'], {}, {})).toBeUndefined() + }) + + it('should return undefined for deeper channel paths', () => { + expect(scopeFunction(['channels', 'ch1', 'messages'], {}, {})).toBeUndefined() + }) }) }) }) @@ -234,16 +234,18 @@ describe('AsyncAPI apiKind calculation', () => { }) describe('Labels should not redefine AsyncAPI apiKind', () => { - it('should not override default apiKind by Label', async () => { + it('should not override default BWC apiKind with no-BWC label', async () => { const result = await buildPackageWithDefaultConfig('asyncapi/api-kind/base', ['apihub/x-api-kind: no-BWC']) - const [operation] = Array.from(result.operations.values()) - expect(operation.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_BWC) + const operations = Array.from(result.operations.values()) + // First operation has no x-api-kind on operation or channel — should stay BWC despite label + expect(operations[0].apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_BWC) }) - it('should not override operation/channel apiKind by Label', async () => { - const result = await buildPackageWithDefaultConfig('asyncapi/api-kind/base', ['apihub/x-api-kind: no-BWC']) - const [operationWithCannelBwc] = Array.from(result.operations.values()) - expect(operationWithCannelBwc.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_BWC) + it('should not override channel NO_BWC apiKind with BWC label', async () => { + const result = await buildPackageWithDefaultConfig('asyncapi/api-kind/base', ['apihub/x-api-kind: BWC']) + const operations = Array.from(result.operations.values()) + // Third operation uses channel-no-bwc — should stay NO_BWC despite BWC label + expect(operations[2].apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_NO_BWC) }) }) }) From 8cf4b49ced60936c21c858ad8da328bd4e985734 Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Mon, 16 Mar 2026 17:49:54 +0400 Subject: [PATCH 06/29] feat: asyncapi-changes tests refactoring --- test/asyncapi-changes.test.ts | 9 ++++++ .../schema/add-property/after.yaml | 15 +++++---- .../schema/add-property/before.yaml | 11 ++++--- .../schema/change-property-type/after.yaml | 11 ++++--- .../schema/change-property-type/before.yaml | 11 ++++--- .../schema/remove-property/after.yaml | 11 ++++--- .../schema/remove-property/before.yaml | 15 +++++---- .../server/change-in-channel/after.yaml | 31 +++++++++++++++++++ .../server/change-in-channel/before.yaml | 31 +++++++++++++++++++ 9 files changed, 117 insertions(+), 28 deletions(-) create mode 100644 test/projects/asyncapi-changes/server/change-in-channel/after.yaml create mode 100644 test/projects/asyncapi-changes/server/change-in-channel/before.yaml diff --git a/test/asyncapi-changes.test.ts b/test/asyncapi-changes.test.ts index 1d0d8950..2186be66 100644 --- a/test/asyncapi-changes.test.ts +++ b/test/asyncapi-changes.test.ts @@ -152,6 +152,15 @@ describe('AsyncAPI 3.0 Changelog tests', () => { expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1, [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) + test('should report changed server used in channel', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/server/change-in-channel') + + // Server host changed; server is at root level but referenced by channel + // root-level diff (annotation) + operation-level resolved diff (unclassified) + expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1, [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 1, [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + test('should report added root servers', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/server/add-root') diff --git a/test/projects/asyncapi-changes/schema/add-property/after.yaml b/test/projects/asyncapi-changes/schema/add-property/after.yaml index 4ce84ce2..e6c75af8 100644 --- a/test/projects/asyncapi-changes/schema/add-property/after.yaml +++ b/test/projects/asyncapi-changes/schema/add-property/after.yaml @@ -19,9 +19,12 @@ components: messages: message1: payload: - type: object - properties: - userId: - type: string - email: - type: string + $ref: '#/components/schemas/Message1Payload' + schemas: + Message1Payload: + type: object + properties: + userId: + type: string + email: + type: string diff --git a/test/projects/asyncapi-changes/schema/add-property/before.yaml b/test/projects/asyncapi-changes/schema/add-property/before.yaml index 4bc38c26..48bde53a 100644 --- a/test/projects/asyncapi-changes/schema/add-property/before.yaml +++ b/test/projects/asyncapi-changes/schema/add-property/before.yaml @@ -19,7 +19,10 @@ components: messages: message1: payload: - type: object - properties: - userId: - type: string + $ref: '#/components/schemas/Message1Payload' + schemas: + Message1Payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changes/schema/change-property-type/after.yaml b/test/projects/asyncapi-changes/schema/change-property-type/after.yaml index 54c81d62..917ad86f 100644 --- a/test/projects/asyncapi-changes/schema/change-property-type/after.yaml +++ b/test/projects/asyncapi-changes/schema/change-property-type/after.yaml @@ -19,7 +19,10 @@ components: messages: message1: payload: - type: object - properties: - userId: - type: integer + $ref: '#/components/schemas/Message1Payload' + schemas: + Message1Payload: + type: object + properties: + userId: + type: integer diff --git a/test/projects/asyncapi-changes/schema/change-property-type/before.yaml b/test/projects/asyncapi-changes/schema/change-property-type/before.yaml index 4bc38c26..48bde53a 100644 --- a/test/projects/asyncapi-changes/schema/change-property-type/before.yaml +++ b/test/projects/asyncapi-changes/schema/change-property-type/before.yaml @@ -19,7 +19,10 @@ components: messages: message1: payload: - type: object - properties: - userId: - type: string + $ref: '#/components/schemas/Message1Payload' + schemas: + Message1Payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changes/schema/remove-property/after.yaml b/test/projects/asyncapi-changes/schema/remove-property/after.yaml index 4bc38c26..48bde53a 100644 --- a/test/projects/asyncapi-changes/schema/remove-property/after.yaml +++ b/test/projects/asyncapi-changes/schema/remove-property/after.yaml @@ -19,7 +19,10 @@ components: messages: message1: payload: - type: object - properties: - userId: - type: string + $ref: '#/components/schemas/Message1Payload' + schemas: + Message1Payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changes/schema/remove-property/before.yaml b/test/projects/asyncapi-changes/schema/remove-property/before.yaml index 4ce84ce2..e6c75af8 100644 --- a/test/projects/asyncapi-changes/schema/remove-property/before.yaml +++ b/test/projects/asyncapi-changes/schema/remove-property/before.yaml @@ -19,9 +19,12 @@ components: messages: message1: payload: - type: object - properties: - userId: - type: string - email: - type: string + $ref: '#/components/schemas/Message1Payload' + schemas: + Message1Payload: + type: object + properties: + userId: + type: string + email: + type: string diff --git a/test/projects/asyncapi-changes/server/change-in-channel/after.yaml b/test/projects/asyncapi-changes/server/change-in-channel/after.yaml new file mode 100644 index 00000000..5cdd55c7 --- /dev/null +++ b/test/projects/asyncapi-changes/server/change-in-channel/after.yaml @@ -0,0 +1,31 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +servers: + production: + protocol: amqp + host: api.production.example.com +channels: + channel1: + address: channel1 + servers: + - $ref: '#/servers/production' + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changes/server/change-in-channel/before.yaml b/test/projects/asyncapi-changes/server/change-in-channel/before.yaml new file mode 100644 index 00000000..aa7418d1 --- /dev/null +++ b/test/projects/asyncapi-changes/server/change-in-channel/before.yaml @@ -0,0 +1,31 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +servers: + production: + protocol: amqp + host: api.example.com +channels: + channel1: + address: channel1 + servers: + - $ref: '#/servers/production' + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string From cd4024fbb8c3a841b0261b822c8a6032b9c88508 Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Tue, 17 Mar 2026 17:43:42 +0400 Subject: [PATCH 07/29] feat: added asyncapi-changes tests --- test/asyncapi-changes.test.ts | 17 ++++++++ .../add-message-not-in-operation/after.yaml | 38 ++++++++++++++++++ .../add-message-not-in-operation/before.yaml | 36 +++++++++++++++++ .../operation/add-multiple/after.yaml | 1 - .../operation/add-multiple/before.yaml | 1 - .../add-with-changed-message/after.yaml | 34 ++++++++++++++++ .../add-with-changed-message/before.yaml | 28 +++++++++++++ .../add-with-multiple-messages/after.yaml | 40 +++++++++++++++++++ .../add-with-multiple-messages/before.yaml | 33 +++++++++++++++ 9 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 test/projects/asyncapi-changes/channel/add-message-not-in-operation/after.yaml create mode 100644 test/projects/asyncapi-changes/channel/add-message-not-in-operation/before.yaml create mode 100644 test/projects/asyncapi-changes/operation/add-with-changed-message/after.yaml create mode 100644 test/projects/asyncapi-changes/operation/add-with-changed-message/before.yaml create mode 100644 test/projects/asyncapi-changes/operation/add-with-multiple-messages/after.yaml create mode 100644 test/projects/asyncapi-changes/operation/add-with-multiple-messages/before.yaml diff --git a/test/asyncapi-changes.test.ts b/test/asyncapi-changes.test.ts index 2186be66..80d29ad9 100644 --- a/test/asyncapi-changes.test.ts +++ b/test/asyncapi-changes.test.ts @@ -44,6 +44,18 @@ describe('AsyncAPI 3.0 Changelog tests', () => { expect(result).toEqual(numberOfImpactedOperationsMatcher({ [NON_BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) }) + test('should report added operation with multiple messages', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/add-with-multiple-messages') + expect(result).toEqual(changesSummaryMatcher({ [NON_BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [NON_BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) + }) + + test('should report added operation without message change diffs', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/add-with-changed-message') + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1, [NON_BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1, [NON_BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + test('should report removed operation', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/remove') expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) @@ -109,6 +121,11 @@ describe('AsyncAPI 3.0 Changelog tests', () => { expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) + test('should ignore message added to channel but not referenced in operation', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/add-message-not-in-operation') + expectNoChanges(result) + }) + test('should impact all operations on shared channel when changing address', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/change-address-shared-channel') diff --git a/test/projects/asyncapi-changes/channel/add-message-not-in-operation/after.yaml b/test/projects/asyncapi-changes/channel/add-message-not-in-operation/after.yaml new file mode 100644 index 00000000..71cd5a68 --- /dev/null +++ b/test/projects/asyncapi-changes/channel/add-message-not-in-operation/after.yaml @@ -0,0 +1,38 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + $ref: '#/components/schemas/Message1Payload' + message2: + payload: + $ref: '#/components/schemas/Message2Payload' + schemas: + Message1Payload: + type: object + properties: + userId: + type: string + Message2Payload: + type: object + properties: + orderId: + type: integer diff --git a/test/projects/asyncapi-changes/channel/add-message-not-in-operation/before.yaml b/test/projects/asyncapi-changes/channel/add-message-not-in-operation/before.yaml new file mode 100644 index 00000000..95f05a98 --- /dev/null +++ b/test/projects/asyncapi-changes/channel/add-message-not-in-operation/before.yaml @@ -0,0 +1,36 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + $ref: '#/components/schemas/Message1Payload' + message2: + payload: + $ref: '#/components/schemas/Message2Payload' + schemas: + Message1Payload: + type: object + properties: + userId: + type: string + Message2Payload: + type: object + properties: + orderId: + type: integer diff --git a/test/projects/asyncapi-changes/operation/add-multiple/after.yaml b/test/projects/asyncapi-changes/operation/add-multiple/after.yaml index 8cfc4dbe..5d55e1dc 100644 --- a/test/projects/asyncapi-changes/operation/add-multiple/after.yaml +++ b/test/projects/asyncapi-changes/operation/add-multiple/after.yaml @@ -35,4 +35,3 @@ components: properties: userId: type: string - diff --git a/test/projects/asyncapi-changes/operation/add-multiple/before.yaml b/test/projects/asyncapi-changes/operation/add-multiple/before.yaml index 8139850a..4bc38c26 100644 --- a/test/projects/asyncapi-changes/operation/add-multiple/before.yaml +++ b/test/projects/asyncapi-changes/operation/add-multiple/before.yaml @@ -23,4 +23,3 @@ components: properties: userId: type: string - diff --git a/test/projects/asyncapi-changes/operation/add-with-changed-message/after.yaml b/test/projects/asyncapi-changes/operation/add-with-changed-message/after.yaml new file mode 100644 index 00000000..4c536eb4 --- /dev/null +++ b/test/projects/asyncapi-changes/operation/add-with-changed-message/after.yaml @@ -0,0 +1,34 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + operation2: + action: send + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + $ref: '#/components/schemas/Message1Payload' + schemas: + Message1Payload: + type: object + properties: + userId: + type: integer diff --git a/test/projects/asyncapi-changes/operation/add-with-changed-message/before.yaml b/test/projects/asyncapi-changes/operation/add-with-changed-message/before.yaml new file mode 100644 index 00000000..48bde53a --- /dev/null +++ b/test/projects/asyncapi-changes/operation/add-with-changed-message/before.yaml @@ -0,0 +1,28 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + $ref: '#/components/schemas/Message1Payload' + schemas: + Message1Payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changes/operation/add-with-multiple-messages/after.yaml b/test/projects/asyncapi-changes/operation/add-with-multiple-messages/after.yaml new file mode 100644 index 00000000..6ebe752f --- /dev/null +++ b/test/projects/asyncapi-changes/operation/add-with-multiple-messages/after.yaml @@ -0,0 +1,40 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + operation2: + action: send + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + - $ref: '#/channels/channel1/messages/message2' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: integer diff --git a/test/projects/asyncapi-changes/operation/add-with-multiple-messages/before.yaml b/test/projects/asyncapi-changes/operation/add-with-multiple-messages/before.yaml new file mode 100644 index 00000000..7a12b6cc --- /dev/null +++ b/test/projects/asyncapi-changes/operation/add-with-multiple-messages/before.yaml @@ -0,0 +1,33 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: integer From 42885cd2c06d720d6e20c7ae424da5af10a2a49a Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Wed, 18 Mar 2026 08:29:24 +0400 Subject: [PATCH 08/29] feat: added tests for asyncapi duplication --- test/asyncapi-deduplication.test.ts | 115 ++++++++++++++++++ .../add-message-no-sibling-impact/after.yaml | 43 +++++++ .../add-message-no-sibling-impact/before.yaml | 42 +++++++ .../cross-document-dedup/after1.yaml | 25 ++++ .../cross-document-dedup/after2.yaml | 25 ++++ .../cross-document-dedup/before1.yaml | 25 ++++ .../cross-document-dedup/before2.yaml | 25 ++++ .../after.yaml | 37 ++++++ .../before.yaml | 37 ++++++ .../after.yaml | 42 +++++++ .../before.yaml | 42 +++++++ .../after.yaml | 46 +++++++ .../before.yaml | 46 +++++++ .../after.yaml | 42 +++++++ .../before.yaml | 42 +++++++ 15 files changed, 634 insertions(+) create mode 100644 test/asyncapi-deduplication.test.ts create mode 100644 test/projects/asyncapi-deduplication/add-message-no-sibling-impact/after.yaml create mode 100644 test/projects/asyncapi-deduplication/add-message-no-sibling-impact/before.yaml create mode 100644 test/projects/asyncapi-deduplication/cross-document-dedup/after1.yaml create mode 100644 test/projects/asyncapi-deduplication/cross-document-dedup/after2.yaml create mode 100644 test/projects/asyncapi-deduplication/cross-document-dedup/before1.yaml create mode 100644 test/projects/asyncapi-deduplication/cross-document-dedup/before2.yaml create mode 100644 test/projects/asyncapi-deduplication/mixed-operation-and-message-changes/after.yaml create mode 100644 test/projects/asyncapi-deduplication/mixed-operation-and-message-changes/before.yaml create mode 100644 test/projects/asyncapi-deduplication/root-info-change-multiple-operations/after.yaml create mode 100644 test/projects/asyncapi-deduplication/root-info-change-multiple-operations/before.yaml create mode 100644 test/projects/asyncapi-deduplication/root-server-change-multiple-operations/after.yaml create mode 100644 test/projects/asyncapi-deduplication/root-server-change-multiple-operations/before.yaml create mode 100644 test/projects/asyncapi-deduplication/shared-schema-across-operations/after.yaml create mode 100644 test/projects/asyncapi-deduplication/shared-schema-across-operations/before.yaml diff --git a/test/asyncapi-deduplication.test.ts b/test/asyncapi-deduplication.test.ts new file mode 100644 index 00000000..bf17fcba --- /dev/null +++ b/test/asyncapi-deduplication.test.ts @@ -0,0 +1,115 @@ + +import { + buildChangelogPackageDefaultConfig, + changesSummaryMatcher, + numberOfImpactedOperationsMatcher, +} from './helpers' +import { + ANNOTATION_CHANGE_TYPE, + ASYNCAPI_API_TYPE, + BREAKING_CHANGE_TYPE, +} from '../src' + +/** + * Tests for AsyncAPI diff deduplication. + * + * Deduplication in AsyncAPI has two levels: + * + * Level 1 (within one document pair, by reference identity): + * - `aggregateDiffsWithRollup` propagates diffs bottom-up via Set + * - Same Diff instance can appear in multiple operations (e.g. shared component schema) + * - `comparePairedDocs` deduplicates via `new Set(allDiffs)` — reference identity + * - `collectExclusiveOtherMessageDiffs` filters sibling message diffs so they don't + * leak into unrelated (operation, message) pairs + * + * Level 2 (across multiple document pairs, by content hash): + * - Rare case: one operation in multiple documents → multiple apiDiff() calls + * - Uses `removeObjectDuplicates(diffs, calculateDiffId)` for content-based dedup + */ +describe('AsyncAPI deduplication tests', () => { + + describe('Level 1: Shared schema deduplication across operations', () => { + test('should produce per-scope diffs when shared schema changes across operations with different scopes', async () => { + // Two operations (operation1=receive, operation2=send) with different messages, + // both referencing SharedPayload. Changing SharedPayload type (number → string). + // apiDiff resolves $refs and creates separate diff instances per scope (receive, send), + // so changesSummary counts 2 breaking (one per scope). Both operations are impacted. + const result = await buildChangelogPackageDefaultConfig('asyncapi-deduplication/shared-schema-across-operations') + + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) + }) + }) + + describe('Level 1: Root-level change deduplication', () => { + test('should count info.version change once in changesSummary but impact all operations', async () => { + // Two operations. info.version changed (1.0.0 → 2.0.0). + // The info diff is extracted and added to every operation via extractInfoDiffs(), + // but it's the same semantic change — changesSummary should count 1, impacted 2. + const result = await buildChangelogPackageDefaultConfig('asyncapi-deduplication/root-info-change-multiple-operations') + + expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) + }) + + test('should count root server change once in changesSummary but impact all operations', async () => { + // Two operations. Root server host changed. + // Root-level server diff is extracted via extractRootServersDiffs() for every operation, + // but it's one unique change — changesSummary should count 1, impacted 2. + const result = await buildChangelogPackageDefaultConfig('asyncapi-deduplication/root-server-change-multiple-operations') + + expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) + }) + }) + + describe('Level 1: Message-level isolation (collectExclusiveOtherMessageDiffs)', () => { + test('should not leak add-message diff to existing sibling message operations', async () => { + // operation1 has message1, message2. message3 is added. + // The array-level diff for adding message3 should only appear on the new operation1-message3, + // not on operation1-message1 or operation1-message2. + // collectExclusiveOtherMessageDiffs filters out the sibling's array-level diff. + const result = await buildChangelogPackageDefaultConfig('asyncapi-deduplication/add-message-no-sibling-impact') + + // Only 1 breaking change (the added message3 operation), impacting 1 operation + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('should correctly separate operation-level and message-level changes', async () => { + // operation1 with message1 and message2. + // Changes: operation description changed (annotation, shared by both messages) + // + message1 contentType changed (breaking, specific to message1 only) + // + // Expected: annotation (description) = 1 in summary, impacted 2 (both messages) + // breaking (contentType) = 1 in summary, impacted 1 (only message1) + const result = await buildChangelogPackageDefaultConfig('asyncapi-deduplication/mixed-operation-and-message-changes') + + expect(result).toEqual(changesSummaryMatcher({ + [ANNOTATION_CHANGE_TYPE]: 1, + [BREAKING_CHANGE_TYPE]: 1, + }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ + [ANNOTATION_CHANGE_TYPE]: 2, + [BREAKING_CHANGE_TYPE]: 1, + }, ASYNCAPI_API_TYPE)) + }) + }) + + describe('Level 2: Cross-document deduplication', () => { + test('should deduplicate diffs when same operation appears in multiple document pairs', async () => { + // Same operation (operation1-message1) described in two documents: + // before1.yaml/after1.yaml and before2.yaml/after2.yaml. + // Both document pairs produce identical semantic changes (userId type: number → string). + // Level 2 dedup via calculateDiffId should ensure diffs are not counted twice. + const result = await buildChangelogPackageDefaultConfig( + 'asyncapi-deduplication/cross-document-dedup', + [{ fileId: 'before1.yaml', publish: true }, { fileId: 'before2.yaml', publish: true }], + [{ fileId: 'after1.yaml' }, { fileId: 'after2.yaml' }], + ) + + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + }) +}) diff --git a/test/projects/asyncapi-deduplication/add-message-no-sibling-impact/after.yaml b/test/projects/asyncapi-deduplication/add-message-no-sibling-impact/after.yaml new file mode 100644 index 00000000..1fbdc18a --- /dev/null +++ b/test/projects/asyncapi-deduplication/add-message-no-sibling-impact/after.yaml @@ -0,0 +1,43 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' + message3: + $ref: '#/components/messages/message3' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + - $ref: '#/channels/channel1/messages/message2' + - $ref: '#/channels/channel1/messages/message3' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string + message3: + payload: + type: object + properties: + productId: + type: string diff --git a/test/projects/asyncapi-deduplication/add-message-no-sibling-impact/before.yaml b/test/projects/asyncapi-deduplication/add-message-no-sibling-impact/before.yaml new file mode 100644 index 00000000..d06f58e4 --- /dev/null +++ b/test/projects/asyncapi-deduplication/add-message-no-sibling-impact/before.yaml @@ -0,0 +1,42 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' + message3: + $ref: '#/components/messages/message3' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + - $ref: '#/channels/channel1/messages/message2' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string + message3: + payload: + type: object + properties: + productId: + type: string diff --git a/test/projects/asyncapi-deduplication/cross-document-dedup/after1.yaml b/test/projects/asyncapi-deduplication/cross-document-dedup/after1.yaml new file mode 100644 index 00000000..95df4997 --- /dev/null +++ b/test/projects/asyncapi-deduplication/cross-document-dedup/after1.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI Doc1 + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-deduplication/cross-document-dedup/after2.yaml b/test/projects/asyncapi-deduplication/cross-document-dedup/after2.yaml new file mode 100644 index 00000000..0f976837 --- /dev/null +++ b/test/projects/asyncapi-deduplication/cross-document-dedup/after2.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI Doc2 + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-deduplication/cross-document-dedup/before1.yaml b/test/projects/asyncapi-deduplication/cross-document-dedup/before1.yaml new file mode 100644 index 00000000..c7bfb00e --- /dev/null +++ b/test/projects/asyncapi-deduplication/cross-document-dedup/before1.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI Doc1 + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-deduplication/cross-document-dedup/before2.yaml b/test/projects/asyncapi-deduplication/cross-document-dedup/before2.yaml new file mode 100644 index 00000000..b8e88116 --- /dev/null +++ b/test/projects/asyncapi-deduplication/cross-document-dedup/before2.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI Doc2 + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-deduplication/mixed-operation-and-message-changes/after.yaml b/test/projects/asyncapi-deduplication/mixed-operation-and-message-changes/after.yaml new file mode 100644 index 00000000..20a039ba --- /dev/null +++ b/test/projects/asyncapi-deduplication/mixed-operation-and-message-changes/after.yaml @@ -0,0 +1,37 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + description: updated description + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + - $ref: '#/channels/channel1/messages/message2' +components: + messages: + message1: + contentType: application/xml + payload: + type: object + properties: + userId: + type: string + message2: + contentType: application/json + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-deduplication/mixed-operation-and-message-changes/before.yaml b/test/projects/asyncapi-deduplication/mixed-operation-and-message-changes/before.yaml new file mode 100644 index 00000000..7315b9fe --- /dev/null +++ b/test/projects/asyncapi-deduplication/mixed-operation-and-message-changes/before.yaml @@ -0,0 +1,37 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + description: original description + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + - $ref: '#/channels/channel1/messages/message2' +components: + messages: + message1: + contentType: application/json + payload: + type: object + properties: + userId: + type: string + message2: + contentType: application/json + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-deduplication/root-info-change-multiple-operations/after.yaml b/test/projects/asyncapi-deduplication/root-info-change-multiple-operations/after.yaml new file mode 100644 index 00000000..bde349bd --- /dev/null +++ b/test/projects/asyncapi-deduplication/root-info-change-multiple-operations/after.yaml @@ -0,0 +1,42 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 2.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + channel2: + address: channel2 + messages: + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + operation2: + action: send + channel: + $ref: '#/channels/channel2' + messages: + - $ref: '#/channels/channel2/messages/message2' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-deduplication/root-info-change-multiple-operations/before.yaml b/test/projects/asyncapi-deduplication/root-info-change-multiple-operations/before.yaml new file mode 100644 index 00000000..8c736e79 --- /dev/null +++ b/test/projects/asyncapi-deduplication/root-info-change-multiple-operations/before.yaml @@ -0,0 +1,42 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + channel2: + address: channel2 + messages: + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + operation2: + action: send + channel: + $ref: '#/channels/channel2' + messages: + - $ref: '#/channels/channel2/messages/message2' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-deduplication/root-server-change-multiple-operations/after.yaml b/test/projects/asyncapi-deduplication/root-server-change-multiple-operations/after.yaml new file mode 100644 index 00000000..eb63c0db --- /dev/null +++ b/test/projects/asyncapi-deduplication/root-server-change-multiple-operations/after.yaml @@ -0,0 +1,46 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +servers: + production: + host: new.example.com + protocol: kafka +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + channel2: + address: channel2 + messages: + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + operation2: + action: send + channel: + $ref: '#/channels/channel2' + messages: + - $ref: '#/channels/channel2/messages/message2' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-deduplication/root-server-change-multiple-operations/before.yaml b/test/projects/asyncapi-deduplication/root-server-change-multiple-operations/before.yaml new file mode 100644 index 00000000..117aa3f4 --- /dev/null +++ b/test/projects/asyncapi-deduplication/root-server-change-multiple-operations/before.yaml @@ -0,0 +1,46 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +servers: + production: + host: old.example.com + protocol: kafka +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + channel2: + address: channel2 + messages: + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + operation2: + action: send + channel: + $ref: '#/channels/channel2' + messages: + - $ref: '#/channels/channel2/messages/message2' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-deduplication/shared-schema-across-operations/after.yaml b/test/projects/asyncapi-deduplication/shared-schema-across-operations/after.yaml new file mode 100644 index 00000000..b9246ffb --- /dev/null +++ b/test/projects/asyncapi-deduplication/shared-schema-across-operations/after.yaml @@ -0,0 +1,42 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + channel2: + address: channel2 + messages: + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + operation2: + action: send + channel: + $ref: '#/channels/channel2' + messages: + - $ref: '#/channels/channel2/messages/message2' +components: + schemas: + SharedPayload: + type: object + properties: + userId: + type: string + messages: + message1: + payload: + $ref: '#/components/schemas/SharedPayload' + message2: + payload: + $ref: '#/components/schemas/SharedPayload' diff --git a/test/projects/asyncapi-deduplication/shared-schema-across-operations/before.yaml b/test/projects/asyncapi-deduplication/shared-schema-across-operations/before.yaml new file mode 100644 index 00000000..23db4f22 --- /dev/null +++ b/test/projects/asyncapi-deduplication/shared-schema-across-operations/before.yaml @@ -0,0 +1,42 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + channel2: + address: channel2 + messages: + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + operation2: + action: send + channel: + $ref: '#/channels/channel2' + messages: + - $ref: '#/channels/channel2/messages/message2' +components: + schemas: + SharedPayload: + type: object + properties: + userId: + type: number + messages: + message1: + payload: + $ref: '#/components/schemas/SharedPayload' + message2: + payload: + $ref: '#/components/schemas/SharedPayload' From beeae429707721dc0ebf497c1cdd8e61a4ef8efc Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Wed, 18 Mar 2026 10:55:32 +0400 Subject: [PATCH 09/29] feat: fixed ChannelMessage --- package.json | 2 +- src/apitypes/async/async.changes.ts | 39 +++++++++++++++++------------ test/asyncapi-changes.test.ts | 14 ++++++----- 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index d1511416..51463f91 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ }, "dependencies": { "@asyncapi/parser": "^3.0.0", - "@netcracker/qubership-apihub-api-diff": "feature-schema-rules", + "@netcracker/qubership-apihub-api-diff": "feature-first-reference-key-in-merged", "@netcracker/qubership-apihub-api-unifier": "feature-asyncapi-basic-e2e", "@netcracker/qubership-apihub-graphapi": "dev", "@netcracker/qubership-apihub-json-crawl": "dev", diff --git a/src/apitypes/async/async.changes.ts b/src/apitypes/async/async.changes.ts index afbeeacf..7dd81607 100644 --- a/src/apitypes/async/async.changes.ts +++ b/src/apitypes/async/async.changes.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { isEmpty, isObject } from '../../utils' +import { calculateAsyncOperationId, isEmpty, isObject } from '../../utils' import { aggregateDiffsWithRollup, apiDiff, @@ -152,17 +152,25 @@ export const compareDocuments: DocumentsCompare = async ( return otherDiffs } - // todo del after fix api-diff - function getOperationId(operationsMap: OperationsMap, asyncOperationId: string, index: number): string { - const keys = Object.keys(operationsMap) - - const matchingOperations = keys.filter(key => { - const operation = operationsMap[key] - return operation?.previous?.metadata?.asyncOperationId === asyncOperationId || operation?.current?.metadata?.asyncOperationId === asyncOperationId - }) - const operation = matchingOperations.find(matchingOperation => matchingOperation.endsWith(String(index + 1))) - - return operation || matchingOperations[index] + /** + * Collects diffs for adding/removing message definitions in channel.messages. + * These are channel-level definition changes that should not propagate to operations, + * because what matters is whether the operation's own messages array references a message, + * not whether the channel defines it. + */ + function collectChannelMessageDefinitionDiffs(operationChannel: AsyncAPIV3.ChannelObject): Set { + const channelMessages = (operationChannel as Record).messages + if (!channelMessages || !isObject(channelMessages)) { + return new Set() + } + const diffs = new Set() + const messagesMeta = (channelMessages as WithDiffMetaRecord)[DIFF_META_KEY] + if (messagesMeta) { + for (const key in messagesMeta) { + diffs.add(messagesMeta[key]) + } + } + return diffs } // Iterate through operations in merged document @@ -186,9 +194,7 @@ export const compareDocuments: DocumentsCompare = async ( for (const [messageIndex, message] of messages.entries()) { const messageId = getAsyncMessageId(message) - // todo fix it - // const operationId = calculateAsyncOperationId(asyncOperationId, messageId) - const operationId = getOperationId(operationsMap, asyncOperationId, messageIndex) + const operationId = calculateAsyncOperationId(asyncOperationId, messageId) const { current, previous, @@ -205,8 +211,9 @@ export const compareDocuments: DocumentsCompare = async ( const allOperationDiffs = (operationObject as WithAggregatedDiffs)[DIFFS_AGGREGATED_META_KEY] ?? [] const otherMessageDiffs = collectExclusiveOtherMessageDiffs(messages, messageIndex) + const channelMessageDiffs = collectChannelMessageDefinitionDiffs(operationChannel as AsyncAPIV3.ChannelObject) operationDiffs = [ - ...([...allOperationDiffs].filter(diff => !otherMessageDiffs.has(diff))), + ...([...allOperationDiffs].filter(diff => !otherMessageDiffs.has(diff) && !channelMessageDiffs.has(diff))), ...extractAsyncApiVersionDiff(merged), ...extractInfoDiffs(merged), ...extractIdDiff(merged), diff --git a/test/asyncapi-changes.test.ts b/test/asyncapi-changes.test.ts index 80d29ad9..66228129 100644 --- a/test/asyncapi-changes.test.ts +++ b/test/asyncapi-changes.test.ts @@ -114,11 +114,12 @@ describe('AsyncAPI 3.0 Changelog tests', () => { expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) - test('should report added message definition in channel', async () => { + test('should not report changes when adding message definition in channel without referencing it in operation', async () => { + // message2 added to channel1.messages but operation1.messages still only references message1 + // channel.messages add/remove diffs are filtered out — only operation.messages references matter const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/add-message') - expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expectNoChanges(result) }) test('should ignore message added to channel but not referenced in operation', async () => { @@ -144,11 +145,12 @@ describe('AsyncAPI 3.0 Changelog tests', () => { expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) - test('should report added message definition in channel with multiple apihub operations', async () => { + test('should not report changes when adding message definition in channel with multiple apihub operations', async () => { + // message3 added to channel1.messages but operation1.messages still references only message1 and message2 + // channel.messages add/remove diffs are filtered out — only operation.messages references matter const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/add-message-with-multiple-operations') - expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) + expectNoChanges(result) }) }) From 7dc82e6d653b1dd30e01763a58a5edd34f5802f8 Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Wed, 18 Mar 2026 16:57:27 +0400 Subject: [PATCH 10/29] feat: Added tests + refactoring --- test/asyncapi-changes.test.ts | 30 +++++++----- test/asyncapi-deduplication.test.ts | 49 ++++++++++++++----- .../shared-schema-cross-specs/after1.yaml | 28 +++++++++++ .../shared-schema-cross-specs/after2.yaml | 28 +++++++++++ .../shared-schema-cross-specs/before1.yaml | 28 +++++++++++ .../shared-schema-cross-specs/before2.yaml | 28 +++++++++++ .../shared-schema-same-scope/after.yaml | 42 ++++++++++++++++ .../shared-schema-same-scope/before.yaml | 42 ++++++++++++++++ 8 files changed, 251 insertions(+), 24 deletions(-) create mode 100644 test/projects/asyncapi-deduplication/shared-schema-cross-specs/after1.yaml create mode 100644 test/projects/asyncapi-deduplication/shared-schema-cross-specs/after2.yaml create mode 100644 test/projects/asyncapi-deduplication/shared-schema-cross-specs/before1.yaml create mode 100644 test/projects/asyncapi-deduplication/shared-schema-cross-specs/before2.yaml create mode 100644 test/projects/asyncapi-deduplication/shared-schema-same-scope/after.yaml create mode 100644 test/projects/asyncapi-deduplication/shared-schema-same-scope/before.yaml diff --git a/test/asyncapi-changes.test.ts b/test/asyncapi-changes.test.ts index 66228129..175e0ab2 100644 --- a/test/asyncapi-changes.test.ts +++ b/test/asyncapi-changes.test.ts @@ -51,6 +51,9 @@ describe('AsyncAPI 3.0 Changelog tests', () => { }) test('should report added operation without message change diffs', async () => { + // operation2 (send) added + message1 payload userId type changed (string → integer). + // The added operation should only produce "add" diff, + // not inherit the breaking payload change from message1. const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/add-with-changed-message') expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1, [NON_BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1, [NON_BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) @@ -82,6 +85,9 @@ describe('AsyncAPI 3.0 Changelog tests', () => { }) test('should report changed operation description with multiple messages', async () => { + // operation1 has message1 and message2. Only operation description changed (test1 → test2). + // The annotation diff is counted once in changesSummary but impacts 2 apihub operations + // (one per message: operation1-message1, operation1-message2). const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/change-description-with-multiple-messages') expect(result).toEqual(changesSummaryMatcher({ @@ -114,6 +120,15 @@ describe('AsyncAPI 3.0 Changelog tests', () => { expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) + test('should impact all operations on shared channel when changing address', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/change-address-shared-channel') + + // operation1 and operation2 both reference channel1, address changed + // both apihub operations should be impacted + expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) + }) + test('should not report changes when adding message definition in channel without referencing it in operation', async () => { // message2 added to channel1.messages but operation1.messages still only references message1 // channel.messages add/remove diffs are filtered out — only operation.messages references matter @@ -127,15 +142,6 @@ describe('AsyncAPI 3.0 Changelog tests', () => { expectNoChanges(result) }) - test('should impact all operations on shared channel when changing address', async () => { - const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/change-address-shared-channel') - - // operation1 and operation2 both reference channel1, address changed - // both apihub operations should be impacted - expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) - }) - test('should not impact operation on other channel when changing address', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/change-address-no-impact-on-other-channel') @@ -158,7 +164,7 @@ describe('AsyncAPI 3.0 Changelog tests', () => { test('should report added server in channel', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/server/add-to-channel') - // channel-level servers diff (unclassified) + root servers diff (annotation) + // channel-level servers diff + root servers diff expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1, [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1, [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) @@ -166,7 +172,7 @@ describe('AsyncAPI 3.0 Changelog tests', () => { test('should report removed server from channel', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/server/remove-from-channel') - // channel-level servers diff (unclassified) + root servers diff (annotation) + // channel-level servers diff + root servers diff expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1, [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1, [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) @@ -175,7 +181,7 @@ describe('AsyncAPI 3.0 Changelog tests', () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/server/change-in-channel') // Server host changed; server is at root level but referenced by channel - // root-level diff (annotation) + operation-level resolved diff (unclassified) + // root-level diff + operation-level resolved diff expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1, [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 1, [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) diff --git a/test/asyncapi-deduplication.test.ts b/test/asyncapi-deduplication.test.ts index bf17fcba..cc2d77d0 100644 --- a/test/asyncapi-deduplication.test.ts +++ b/test/asyncapi-deduplication.test.ts @@ -13,35 +13,60 @@ import { /** * Tests for AsyncAPI diff deduplication. * - * Deduplication in AsyncAPI has two levels: + * Deduplication in AsyncAPI works at two stages: * - * Level 1 (within one document pair, by reference identity): + * Within one document pair (by reference identity): * - `aggregateDiffsWithRollup` propagates diffs bottom-up via Set * - Same Diff instance can appear in multiple operations (e.g. shared component schema) * - `comparePairedDocs` deduplicates via `new Set(allDiffs)` — reference identity * - `collectExclusiveOtherMessageDiffs` filters sibling message diffs so they don't * leak into unrelated (operation, message) pairs * - * Level 2 (across multiple document pairs, by content hash): + * Across multiple document pairs (by content hash): * - Rare case: one operation in multiple documents → multiple apiDiff() calls * - Uses `removeObjectDuplicates(diffs, calculateDiffId)` for content-based dedup */ describe('AsyncAPI deduplication tests', () => { - describe('Level 1: Shared schema deduplication across operations', () => { - test('should produce per-scope diffs when shared schema changes across operations with different scopes', async () => { - // Two operations (operation1=receive, operation2=send) with different messages, - // both referencing SharedPayload. Changing SharedPayload type (number → string). - // apiDiff resolves $refs and creates separate diff instances per scope (receive, send), - // so changesSummary counts 2 breaking (one per scope). Both operations are impacted. + describe('Shared entities in the same specification', () => { + test('shared schema, different scopes (receive vs send)', async () => { + // Two operations (operation1=receive, operation2=send) on different channels, + // both messages referencing SharedPayload. Changing SharedPayload type (number → string). + // apiDiff resolves $refs per scope → separate diff instances per scope. const result = await buildChangelogPackageDefaultConfig('asyncapi-deduplication/shared-schema-across-operations') expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) }) + + test('shared schema, same scope (both receive)', async () => { + // Two operations (both receive) on different channels, + // both messages referencing SharedPayload. Changing SharedPayload type (number → string). + // Same scope → diffs should be deduplicated by reference identity within one apiDiff call. + const result = await buildChangelogPackageDefaultConfig('asyncapi-deduplication/shared-schema-same-scope') + + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) + }) + }) + + describe('Shared entities across different specifications', () => { + test('shared schema name in two specs, different scopes (receive vs send)', async () => { + // operation1 (receive) in doc1, operation2 (send) in doc2. + // Both specs define SharedPayload with same change (number → string). + // Different apiDiff calls → different diff instances. Different operations → no cross-operation dedup. + const result = await buildChangelogPackageDefaultConfig( + 'asyncapi-deduplication/shared-schema-cross-specs', + [{ fileId: 'before1.yaml', publish: true }, { fileId: 'before2.yaml', publish: true }], + [{ fileId: 'after1.yaml' }, { fileId: 'after2.yaml' }], + ) + + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) + }) }) - describe('Level 1: Root-level change deduplication', () => { + describe('Root-level change deduplication', () => { test('should count info.version change once in changesSummary but impact all operations', async () => { // Two operations. info.version changed (1.0.0 → 2.0.0). // The info diff is extracted and added to every operation via extractInfoDiffs(), @@ -63,7 +88,7 @@ describe('AsyncAPI deduplication tests', () => { }) }) - describe('Level 1: Message-level isolation (collectExclusiveOtherMessageDiffs)', () => { + describe('Message-level isolation', () => { test('should not leak add-message diff to existing sibling message operations', async () => { // operation1 has message1, message2. message3 is added. // The array-level diff for adding message3 should only appear on the new operation1-message3, @@ -96,7 +121,7 @@ describe('AsyncAPI deduplication tests', () => { }) }) - describe('Level 2: Cross-document deduplication', () => { + describe('Cross-document deduplication', () => { test('should deduplicate diffs when same operation appears in multiple document pairs', async () => { // Same operation (operation1-message1) described in two documents: // before1.yaml/after1.yaml and before2.yaml/after2.yaml. diff --git a/test/projects/asyncapi-deduplication/shared-schema-cross-specs/after1.yaml b/test/projects/asyncapi-deduplication/shared-schema-cross-specs/after1.yaml new file mode 100644 index 00000000..bc7cc96c --- /dev/null +++ b/test/projects/asyncapi-deduplication/shared-schema-cross-specs/after1.yaml @@ -0,0 +1,28 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI Doc1 + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + schemas: + SharedPayload: + type: object + properties: + userId: + type: string + messages: + message1: + payload: + $ref: '#/components/schemas/SharedPayload' diff --git a/test/projects/asyncapi-deduplication/shared-schema-cross-specs/after2.yaml b/test/projects/asyncapi-deduplication/shared-schema-cross-specs/after2.yaml new file mode 100644 index 00000000..3b166833 --- /dev/null +++ b/test/projects/asyncapi-deduplication/shared-schema-cross-specs/after2.yaml @@ -0,0 +1,28 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI Doc2 + version: 1.0.0 +channels: + channel2: + address: channel2 + messages: + message2: + $ref: '#/components/messages/message2' +operations: + operation2: + action: send + channel: + $ref: '#/channels/channel2' + messages: + - $ref: '#/channels/channel2/messages/message2' +components: + schemas: + SharedPayload: + type: object + properties: + userId: + type: string + messages: + message2: + payload: + $ref: '#/components/schemas/SharedPayload' diff --git a/test/projects/asyncapi-deduplication/shared-schema-cross-specs/before1.yaml b/test/projects/asyncapi-deduplication/shared-schema-cross-specs/before1.yaml new file mode 100644 index 00000000..fce21230 --- /dev/null +++ b/test/projects/asyncapi-deduplication/shared-schema-cross-specs/before1.yaml @@ -0,0 +1,28 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI Doc1 + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + schemas: + SharedPayload: + type: object + properties: + userId: + type: number + messages: + message1: + payload: + $ref: '#/components/schemas/SharedPayload' diff --git a/test/projects/asyncapi-deduplication/shared-schema-cross-specs/before2.yaml b/test/projects/asyncapi-deduplication/shared-schema-cross-specs/before2.yaml new file mode 100644 index 00000000..b40b9d86 --- /dev/null +++ b/test/projects/asyncapi-deduplication/shared-schema-cross-specs/before2.yaml @@ -0,0 +1,28 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI Doc2 + version: 1.0.0 +channels: + channel2: + address: channel2 + messages: + message2: + $ref: '#/components/messages/message2' +operations: + operation2: + action: send + channel: + $ref: '#/channels/channel2' + messages: + - $ref: '#/channels/channel2/messages/message2' +components: + schemas: + SharedPayload: + type: object + properties: + userId: + type: number + messages: + message2: + payload: + $ref: '#/components/schemas/SharedPayload' diff --git a/test/projects/asyncapi-deduplication/shared-schema-same-scope/after.yaml b/test/projects/asyncapi-deduplication/shared-schema-same-scope/after.yaml new file mode 100644 index 00000000..33563b89 --- /dev/null +++ b/test/projects/asyncapi-deduplication/shared-schema-same-scope/after.yaml @@ -0,0 +1,42 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + channel2: + address: channel2 + messages: + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + operation2: + action: receive + channel: + $ref: '#/channels/channel2' + messages: + - $ref: '#/channels/channel2/messages/message2' +components: + schemas: + SharedPayload: + type: object + properties: + userId: + type: string + messages: + message1: + payload: + $ref: '#/components/schemas/SharedPayload' + message2: + payload: + $ref: '#/components/schemas/SharedPayload' diff --git a/test/projects/asyncapi-deduplication/shared-schema-same-scope/before.yaml b/test/projects/asyncapi-deduplication/shared-schema-same-scope/before.yaml new file mode 100644 index 00000000..c083e434 --- /dev/null +++ b/test/projects/asyncapi-deduplication/shared-schema-same-scope/before.yaml @@ -0,0 +1,42 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + channel2: + address: channel2 + messages: + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + operation2: + action: receive + channel: + $ref: '#/channels/channel2' + messages: + - $ref: '#/channels/channel2/messages/message2' +components: + schemas: + SharedPayload: + type: object + properties: + userId: + type: number + messages: + message1: + payload: + $ref: '#/components/schemas/SharedPayload' + message2: + payload: + $ref: '#/components/schemas/SharedPayload' From cfe4eca2f0c781e3d2fab3f92f9a92d5268f0b82 Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Thu, 19 Mar 2026 14:14:43 +0400 Subject: [PATCH 11/29] feat: Update async server --- src/apitypes/async/async.changes.ts | 4 +- test/asyncapi-changes.test.ts | 65 +++++++++++------ test/asyncapi-deduplication.test.ts | 19 +++-- test/asyncapi-operation.test.ts | 71 +++++++++++++++++-- .../server/change-isolated-servers/after.yaml | 53 ++++++++++++++ .../change-isolated-servers/before.yaml | 53 ++++++++++++++ .../after.yaml | 4 ++ .../before.yaml | 4 ++ .../root-servers-no-channel-servers.yaml | 32 +++++++++ 9 files changed, 265 insertions(+), 40 deletions(-) create mode 100644 test/projects/asyncapi-changes/server/change-isolated-servers/after.yaml create mode 100644 test/projects/asyncapi-changes/server/change-isolated-servers/before.yaml create mode 100644 test/projects/asyncapi/operations/root-servers-no-channel-servers.yaml diff --git a/src/apitypes/async/async.changes.ts b/src/apitypes/async/async.changes.ts index 7dd81607..7b54e9ee 100644 --- a/src/apitypes/async/async.changes.ts +++ b/src/apitypes/async/async.changes.ts @@ -53,7 +53,6 @@ import { extractDefaultContentTypeDiff, extractIdDiff, extractInfoDiffs, - extractRootServersDiffs, getAsyncMessageId, } from './async.utils' @@ -174,7 +173,7 @@ export const compareDocuments: DocumentsCompare = async ( } // Iterate through operations in merged document - const { operations: asyncOperation} = merged + const { operations: asyncOperation } = merged if (asyncOperation && isObject(asyncOperation)) { for (const [asyncOperationId, operationData] of Object.entries(asyncOperation)) { if (!operationData || !isObject(operationData)) { @@ -218,7 +217,6 @@ export const compareDocuments: DocumentsCompare = async ( ...extractInfoDiffs(merged), ...extractIdDiff(merged), ...extractDefaultContentTypeDiff(merged), - ...extractRootServersDiffs(merged), ] } if (operationAddedOrRemoved) { diff --git a/test/asyncapi-changes.test.ts b/test/asyncapi-changes.test.ts index 175e0ab2..8a9a2411 100644 --- a/test/asyncapi-changes.test.ts +++ b/test/asyncapi-changes.test.ts @@ -1,4 +1,3 @@ - import { buildChangelogPackageDefaultConfig, changesSummaryMatcher, @@ -9,7 +8,9 @@ import { import { ANNOTATION_CHANGE_TYPE, ASYNCAPI_API_TYPE, - BREAKING_CHANGE_TYPE, BuildResult, EMPTY_CHANGE_SUMMARY, + BREAKING_CHANGE_TYPE, + BuildResult, + EMPTY_CHANGE_SUMMARY, NON_BREAKING_CHANGE_TYPE, UNCLASSIFIED_CHANGE_TYPE, } from '../src' @@ -162,49 +163,71 @@ describe('AsyncAPI 3.0 Changelog tests', () => { describe('Servers tests', () => { test('should report added server in channel', async () => { + // Server reference added to channel1.servers — channel-level diff only const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/server/add-to-channel') - // channel-level servers diff + root servers diff - expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1, [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1, [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) test('should report removed server from channel', async () => { + // Server reference removed from channel1.servers — channel-level diff only const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/server/remove-from-channel') - // channel-level servers diff + root servers diff - expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1, [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1, [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) test('should report changed server used in channel', async () => { + // Server host changed (api.example.com → api.production.example.com). + // Server is referenced by channel1.servers → diff propagates via channel aggregation. const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/server/change-in-channel') - // Server host changed; server is at root level but referenced by channel - // root-level diff + operation-level resolved diff - expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1, [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 1, [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('should only impact operation whose channel references the changed server', async () => { + // Two channels, two servers: channel1→server1, channel2→server2. + // Only server1 host changed (api1 → new-api1). + // operation1 (on channel1) should get the diff, operation2 (on channel2) should not. + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/server/change-isolated-servers') + + expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) - test('should report added root servers', async () => { + // TODO: unskip when root servers propagate to channel.servers during normalization + test.skip('should report added server on operation when channel has no explicit servers', async () => { + // servers.production added at root, channel1 has no explicit servers. + // If channel.servers is absent, all root servers apply to the channel. + // The add diff should reach the operation via channel.servers aggregation. const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/server/add-root') - expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) - test('should report removed root servers', async () => { + // TODO: unskip when root servers propagate to channel.servers during normalization + test.skip('should report removed server on operation when channel has no explicit servers', async () => { + // servers.production removed from root, channel1 has no explicit servers. + // If channel.servers is absent, all root servers apply to the channel. + // The remove diff should reach the operation via channel.servers aggregation. const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/server/remove-root') - expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) - test('should report changed root servers', async () => { + // TODO: unskip when root servers propagate to channel.servers during normalization + test.skip('should report changed server on operation when channel has no explicit servers', async () => { + // servers.production.host changed, channel1 has no explicit servers. + // If channel.servers is absent, all root servers apply to the channel. + // The host change diff should reach the operation via channel.servers aggregation. const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/server/change-root') - expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) }) diff --git a/test/asyncapi-deduplication.test.ts b/test/asyncapi-deduplication.test.ts index cc2d77d0..0c98078e 100644 --- a/test/asyncapi-deduplication.test.ts +++ b/test/asyncapi-deduplication.test.ts @@ -1,14 +1,9 @@ - import { buildChangelogPackageDefaultConfig, changesSummaryMatcher, numberOfImpactedOperationsMatcher, } from './helpers' -import { - ANNOTATION_CHANGE_TYPE, - ASYNCAPI_API_TYPE, - BREAKING_CHANGE_TYPE, -} from '../src' +import { ANNOTATION_CHANGE_TYPE, ASYNCAPI_API_TYPE, BREAKING_CHANGE_TYPE, UNCLASSIFIED_CHANGE_TYPE } from '../src' /** * Tests for AsyncAPI diff deduplication. @@ -78,13 +73,15 @@ describe('AsyncAPI deduplication tests', () => { }) test('should count root server change once in changesSummary but impact all operations', async () => { - // Two operations. Root server host changed. - // Root-level server diff is extracted via extractRootServersDiffs() for every operation, - // but it's one unique change — changesSummary should count 1, impacted 2. + // Two operations on different channels, both channels reference servers.production. + // Root server host changed (old → new). + // Each scope (receive/send) gets its own unclassified diff via channel.servers aggregation. + // No root-level diffs — server diffs come only through channel aggregation. + // unclassified: 2 in summary (one per scope), impacted 2. const result = await buildChangelogPackageDefaultConfig('asyncapi-deduplication/root-server-change-multiple-operations') - expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) }) }) diff --git a/test/asyncapi-operation.test.ts b/test/asyncapi-operation.test.ts index a25ab1c3..43e35a33 100644 --- a/test/asyncapi-operation.test.ts +++ b/test/asyncapi-operation.test.ts @@ -24,6 +24,13 @@ import { FIRST_REFERENCE_KEY_PROPERTY, INLINE_REFS_FLAG } from '../src/consts' import { ASYNC_EFFECTIVE_NORMALIZE_OPTIONS } from '../src' import { normalize } from '@netcracker/qubership-apihub-api-unifier' +const normalizeAsyncApiDocument = (doc: AsyncAPIV3.AsyncAPIObject): AsyncAPIV3.AsyncAPIObject => + normalize(doc, { + ...ASYNC_EFFECTIVE_NORMALIZE_OPTIONS, + firstReferenceKeyProperty: FIRST_REFERENCE_KEY_PROPERTY, + inlineRefsFlag: INLINE_REFS_FLAG, + }) as AsyncAPIV3.AsyncAPIObject + describe('AsyncAPI 3.0 Operation Tests', () => { describe('Building Package with Operations', () => { @@ -250,11 +257,7 @@ describe('AsyncAPI 3.0 Operation Tests', () => { baseDocument = await loadYamlFile('asyncapi/operations/base.yaml') - normalizedDocument = normalize(baseDocument, { - ...ASYNC_EFFECTIVE_NORMALIZE_OPTIONS, - firstReferenceKeyProperty: FIRST_REFERENCE_KEY_PROPERTY, - inlineRefsFlag: INLINE_REFS_FLAG, - }) as AsyncAPIV3.AsyncAPIObject + normalizedDocument = normalizeAsyncApiDocument(baseDocument) }) test('should select a single operation by key', () => { @@ -372,4 +375,62 @@ describe('AsyncAPI 3.0 Operation Tests', () => { expect(operationKeys).not.toContain(OPERATION_KEY_2) }) }) + + // TODO: unskip after api-unifier propagates root servers to channels during normalization. + describe('Root servers propagation to channels without explicit servers', () => { + const ROOT_SERVERS_DOC_PATH = 'asyncapi/operations/root-servers-no-channel-servers.yaml' + const OP_KEY = 'operation1' + const MSG_ID = 'message1' + let rootServersDoc: AsyncAPIV3.AsyncAPIObject + let rootServersNormalizedDoc: AsyncAPIV3.AsyncAPIObject + let rootServersOpId: string + + const createRefsMsg = (messageId: string, inlineRefs: string[]): Record => { + const message: Record = {} + message[FIRST_REFERENCE_KEY_PROPERTY] = messageId + message[INLINE_REFS_FLAG] = inlineRefs + return message + } + + beforeAll(async () => { + rootServersOpId = calculateAsyncOperationId(OP_KEY, MSG_ID) + rootServersDoc = await loadYamlFile(ROOT_SERVERS_DOC_PATH) + rootServersNormalizedDoc = normalizeAsyncApiDocument(rootServersDoc) + }) + + test.skip('createOperationSpec should include root servers when channel has no explicit servers', () => { + // After api-unifier normalization, channel1.servers should contain all root servers. + // createOperationSpec works on the normalized document, so the operation spec + // should include the servers that were propagated to the channel. + const result = createOperationSpec(rootServersNormalizedDoc, rootServersOpId) + + expect(result.servers).toBeDefined() + expect(Object.keys(result.servers!)).toEqual(expect.arrayContaining(['production', 'staging'])) + }) + + test.skip('createOperationSpecWithInlineRefs should include root servers when channel has no explicit servers', () => { + // Same as above but via createOperationSpecWithInlineRefs path. + const refsOnlyDocument = { + operations: { + [OP_KEY]: { + messages: [ + createRefsMsg(MSG_ID, ['#/channels/channel1/messages/message1']), + ], + }, + }, + [INLINE_REFS_FLAG]: [ + '#/servers/production', + '#/servers/staging', + '#/channels/channel1', + '#/channels/channel1/messages/message1', + '#/components/messages/message1', + ], + } as unknown as AsyncAPIV3.AsyncAPIObject + + const result = createOperationSpecWithInlineRefs(rootServersDoc, rootServersOpId, refsOnlyDocument) + + expect(result.servers).toBeDefined() + expect(Object.keys(result.servers!)).toEqual(expect.arrayContaining(['production', 'staging'])) + }) + }) }) diff --git a/test/projects/asyncapi-changes/server/change-isolated-servers/after.yaml b/test/projects/asyncapi-changes/server/change-isolated-servers/after.yaml new file mode 100644 index 00000000..8c9cd195 --- /dev/null +++ b/test/projects/asyncapi-changes/server/change-isolated-servers/after.yaml @@ -0,0 +1,53 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +servers: + server1: + protocol: amqp + host: new-api1.example.com + server2: + protocol: kafka + host: api2.example.com +channels: + channel1: + address: channel1 + servers: + - $ref: '#/servers/server1' + messages: + message1: + $ref: '#/components/messages/message1' + channel2: + address: channel2 + servers: + - $ref: '#/servers/server2' + messages: + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + operation2: + action: send + channel: + $ref: '#/channels/channel2' + messages: + - $ref: '#/channels/channel2/messages/message2' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-changes/server/change-isolated-servers/before.yaml b/test/projects/asyncapi-changes/server/change-isolated-servers/before.yaml new file mode 100644 index 00000000..c0d7cc36 --- /dev/null +++ b/test/projects/asyncapi-changes/server/change-isolated-servers/before.yaml @@ -0,0 +1,53 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +servers: + server1: + protocol: amqp + host: api1.example.com + server2: + protocol: kafka + host: api2.example.com +channels: + channel1: + address: channel1 + servers: + - $ref: '#/servers/server1' + messages: + message1: + $ref: '#/components/messages/message1' + channel2: + address: channel2 + servers: + - $ref: '#/servers/server2' + messages: + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + operation2: + action: send + channel: + $ref: '#/channels/channel2' + messages: + - $ref: '#/channels/channel2/messages/message2' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-deduplication/root-server-change-multiple-operations/after.yaml b/test/projects/asyncapi-deduplication/root-server-change-multiple-operations/after.yaml index eb63c0db..2cdd3a62 100644 --- a/test/projects/asyncapi-deduplication/root-server-change-multiple-operations/after.yaml +++ b/test/projects/asyncapi-deduplication/root-server-change-multiple-operations/after.yaml @@ -9,11 +9,15 @@ servers: channels: channel1: address: channel1 + servers: + - $ref: '#/servers/production' messages: message1: $ref: '#/components/messages/message1' channel2: address: channel2 + servers: + - $ref: '#/servers/production' messages: message2: $ref: '#/components/messages/message2' diff --git a/test/projects/asyncapi-deduplication/root-server-change-multiple-operations/before.yaml b/test/projects/asyncapi-deduplication/root-server-change-multiple-operations/before.yaml index 117aa3f4..3d0a1cb5 100644 --- a/test/projects/asyncapi-deduplication/root-server-change-multiple-operations/before.yaml +++ b/test/projects/asyncapi-deduplication/root-server-change-multiple-operations/before.yaml @@ -9,11 +9,15 @@ servers: channels: channel1: address: channel1 + servers: + - $ref: '#/servers/production' messages: message1: $ref: '#/components/messages/message1' channel2: address: channel2 + servers: + - $ref: '#/servers/production' messages: message2: $ref: '#/components/messages/message2' diff --git a/test/projects/asyncapi/operations/root-servers-no-channel-servers.yaml b/test/projects/asyncapi/operations/root-servers-no-channel-servers.yaml new file mode 100644 index 00000000..72f08c72 --- /dev/null +++ b/test/projects/asyncapi/operations/root-servers-no-channel-servers.yaml @@ -0,0 +1,32 @@ +asyncapi: 3.0.0 +info: + title: Root Servers Test + version: 1.0.0 +servers: + production: + host: api.example.com + protocol: amqp + staging: + host: staging.example.com + protocol: amqp +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string From ff2e65b0397f895cd7ee783b6befc2a3f5b5ef24 Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Thu, 19 Mar 2026 14:17:50 +0400 Subject: [PATCH 12/29] feat: Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 51463f91..234eb7bb 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "dependencies": { "@asyncapi/parser": "^3.0.0", "@netcracker/qubership-apihub-api-diff": "feature-first-reference-key-in-merged", - "@netcracker/qubership-apihub-api-unifier": "feature-asyncapi-basic-e2e", + "@netcracker/qubership-apihub-api-unifier": "dev", "@netcracker/qubership-apihub-graphapi": "dev", "@netcracker/qubership-apihub-json-crawl": "dev", "adm-zip": "0.5.10", From 7c94586ce8c3a2fe360056055a1024bd4367b351 Mon Sep 17 00:00:00 2001 From: b41ex Date: Thu, 19 Mar 2026 17:19:40 +0300 Subject: [PATCH 13/29] chore: set dev version for dependencies --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 234eb7bb..0094046b 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ }, "dependencies": { "@asyncapi/parser": "^3.0.0", - "@netcracker/qubership-apihub-api-diff": "feature-first-reference-key-in-merged", + "@netcracker/qubership-apihub-api-diff": "dev", "@netcracker/qubership-apihub-api-unifier": "dev", "@netcracker/qubership-apihub-graphapi": "dev", "@netcracker/qubership-apihub-json-crawl": "dev", From 7bff2805ef3611bf30b5a30b72aaa9dad6c2d291 Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Thu, 19 Mar 2026 18:26:20 +0400 Subject: [PATCH 14/29] feat: Update package.json and tests --- package.json | 2 +- test/asyncapi-changes.test.ts | 16 ++++++++-------- test/asyncapi-deduplication.test.ts | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 234eb7bb..0094046b 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ }, "dependencies": { "@asyncapi/parser": "^3.0.0", - "@netcracker/qubership-apihub-api-diff": "feature-first-reference-key-in-merged", + "@netcracker/qubership-apihub-api-diff": "dev", "@netcracker/qubership-apihub-api-unifier": "dev", "@netcracker/qubership-apihub-graphapi": "dev", "@netcracker/qubership-apihub-json-crawl": "dev", diff --git a/test/asyncapi-changes.test.ts b/test/asyncapi-changes.test.ts index 8a9a2411..a996a27c 100644 --- a/test/asyncapi-changes.test.ts +++ b/test/asyncapi-changes.test.ts @@ -235,15 +235,15 @@ describe('AsyncAPI 3.0 Changelog tests', () => { test('should report added APIHUB operation when message reference is added to async operation', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/message/add-to-operation') - expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) test('should report removed APIHUB operation when message reference is removed to async operation', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/message/remove-from-operation') - expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) test('should report changed message content type', async () => { @@ -258,8 +258,8 @@ describe('AsyncAPI 3.0 Changelog tests', () => { // message2 added to operation1, operation2 unchanged // should only impact 1 new apihub operation (operation1-message2) - expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) test('should not add changes to remaining messages when removing message from operation', async () => { @@ -268,8 +268,8 @@ describe('AsyncAPI 3.0 Changelog tests', () => { // Removing message2 from operation with message1, message2, message3 // should only impact 1 removed apihub operation (operation1-message2), // not the remaining operation1-message1 and operation1-message3 - expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) test('should only impact changed message when changing content type with multiple messages', async () => { diff --git a/test/asyncapi-deduplication.test.ts b/test/asyncapi-deduplication.test.ts index 0c98078e..d0d18a85 100644 --- a/test/asyncapi-deduplication.test.ts +++ b/test/asyncapi-deduplication.test.ts @@ -94,8 +94,8 @@ describe('AsyncAPI deduplication tests', () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-deduplication/add-message-no-sibling-impact') // Only 1 breaking change (the added message3 operation), impacting 1 operation - expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) test('should correctly separate operation-level and message-level changes', async () => { From 5e1d2e9be43b6774a3f9aa40b3d9d207f94bb60f Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Mon, 23 Mar 2026 15:46:22 +0400 Subject: [PATCH 15/29] feat: Update tests, moved bwc validation to different files rest, async --- src/apitypes/async/async.changes.ts | 10 +- src/apitypes/rest/rest.changes.ts | 4 +- .../compare/bwc.validation.async.ts | 101 ++++++++++++++++++ ...c.validation.ts => bwc.validation.rest.ts} | 73 +------------ test/apiKinds.test.ts | 4 +- test/asyncapi-apikind.test.ts | 2 +- test/asyncapi-changes.test.ts | 14 +-- .../add-message-not-in-operation/after.yaml | 21 ++-- .../add-message-not-in-operation/before.yaml | 19 +--- .../after.yaml | 42 -------- .../before.yaml | 34 ------ .../channel/add-message/after.yaml | 33 ------ .../channel/add-message/before.yaml | 26 ----- 13 files changed, 125 insertions(+), 258 deletions(-) create mode 100644 src/components/compare/bwc.validation.async.ts rename src/components/compare/{bwc.validation.ts => bwc.validation.rest.ts} (65%) delete mode 100644 test/projects/asyncapi-changes/channel/add-message-with-multiple-operations/after.yaml delete mode 100644 test/projects/asyncapi-changes/channel/add-message-with-multiple-operations/before.yaml delete mode 100644 test/projects/asyncapi-changes/channel/add-message/after.yaml delete mode 100644 test/projects/asyncapi-changes/channel/add-message/before.yaml diff --git a/src/apitypes/async/async.changes.ts b/src/apitypes/async/async.changes.ts index 7b54e9ee..8adcc6b6 100644 --- a/src/apitypes/async/async.changes.ts +++ b/src/apitypes/async/async.changes.ts @@ -46,7 +46,7 @@ import { getOperationTags, OperationsMap, } from '../../components' -import { createAsyncApiCompatibilityScopeFunction } from '../../components/compare/bwc.validation' +import { createAsyncApiCompatibilityScopeFunction } from '../../components/compare/bwc.validation.async' import { v3 as AsyncAPIV3 } from '@asyncapi/parser/esm/spec-types' import { extractAsyncApiVersionDiff, @@ -173,9 +173,9 @@ export const compareDocuments: DocumentsCompare = async ( } // Iterate through operations in merged document - const { operations: asyncOperation } = merged - if (asyncOperation && isObject(asyncOperation)) { - for (const [asyncOperationId, operationData] of Object.entries(asyncOperation)) { + const { operations: asyncOperations } = merged + if (asyncOperations && isObject(asyncOperations)) { + for (const [asyncOperationId, operationData] of Object.entries(asyncOperations)) { if (!operationData || !isObject(operationData)) { continue } @@ -223,7 +223,7 @@ export const compareDocuments: DocumentsCompare = async ( // Level 1: message added/removed within an existing operation (analogous to REST method within path) const messageAddedOrRemovedDiff = (messages as WithDiffMetaRecord)[DIFF_META_KEY]?.[messageIndex] // Level 2: entire operation added/removed (analogous to REST entire path) - const operationAddedOrRemovedDiff = (asyncOperation as WithDiffMetaRecord)[DIFF_META_KEY]?.[asyncOperationId] + const operationAddedOrRemovedDiff = (asyncOperations as WithDiffMetaRecord)[DIFF_META_KEY]?.[asyncOperationId] const diff = messageAddedOrRemovedDiff ?? operationAddedOrRemovedDiff if (diff) { operationDiffs.push(diff) diff --git a/src/apitypes/rest/rest.changes.ts b/src/apitypes/rest/rest.changes.ts index 106e1f55..577cbb37 100644 --- a/src/apitypes/rest/rest.changes.ts +++ b/src/apitypes/rest/rest.changes.ts @@ -81,7 +81,7 @@ import { getOperationTags, OperationsMap, } from '../../components' -import { createApihubApiCompatibilityScopeFunction } from '../../components/compare/bwc.validation' +import { createRestApiCompatibilityScopeFunction } from '../../components/compare/bwc.validation.rest' import { calculateApiKindFromLabels, getApiKindProperty } from '../../components/document' export const compareDocuments: DocumentsCompare = async ( @@ -140,7 +140,7 @@ export const compareDocuments: DocumentsCompare = async ( normalizedResult: false, afterValueNormalizedProperty: AFTER_VALUE_NORMALIZED_PROPERTY, beforeValueNormalizedProperty: BEFORE_VALUE_NORMALIZED_PROPERTY, - apiCompatibilityScopeFunction: createApihubApiCompatibilityScopeFunction(prevDocumentApiKind, currDocumentApiKind), + apiCompatibilityScopeFunction: createRestApiCompatibilityScopeFunction(prevDocumentApiKind, currDocumentApiKind), openApiPathItemPerOperationDiffs: true, }, ) as { merged: OpenAPIV3.Document; diffs: Diff[] } diff --git a/src/components/compare/bwc.validation.async.ts b/src/components/compare/bwc.validation.async.ts new file mode 100644 index 00000000..c11bb86d --- /dev/null +++ b/src/components/compare/bwc.validation.async.ts @@ -0,0 +1,101 @@ +/** + * Copyright 2024-2025 NetCracker Technology Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + APIHUB_API_COMPATIBILITY_KIND_BWC, + APIHUB_API_COMPATIBILITY_KIND_NO_BWC, +} from '../../consts' +import { isObject } from '../../utils' +import { JsonPath } from '@netcracker/qubership-apihub-json-crawl' +import { + API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE, + API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE, + ApiCompatibilityKind, + ApiCompatibilityScopeFunction, +} from '@netcracker/qubership-apihub-api-diff' +import { getApiKindProperty } from '../document' +import { v3 as AsyncAPIV3 } from '@asyncapi/parser/esm/spec-types' + +const ROOT_PATH_LENGTH = 0 +const ASYNC_OPERATION_PATH_LENGTH = 2 // operations/ +const ASYNC_CHANNEL_PATH_LENGTH = 2 // channels/ + +/** + * Creates an ApiCompatibilityScopeFunction for AsyncAPI documents. + * + * AsyncAPI has no document-level x-api-kind. The hierarchy is: + * Channel (x-api-kind) → Operation (x-api-kind override) → Messages + * + * The scope function receives normalized before/after objects where $refs are resolved, + * so operation.channel is the resolved channel object with all its properties. + * + * - operations/: operation x-api-kind overrides channel x-api-kind, defaults to bwc + * - channels/: channel's own x-api-kind, defaults to bwc + */ +export const createAsyncApiCompatibilityScopeFunction = (): ApiCompatibilityScopeFunction => { + return ( + path?: JsonPath, + beforeJso?: unknown, + afterJso?: unknown, + ): ApiCompatibilityKind | undefined => { + const pathLength = path?.length ?? 0 + + // Root level: default to BWC (no document-level x-api-kind in AsyncAPI) + if (pathLength === ROOT_PATH_LENGTH) { + return API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE + } + + const firstSegment = path?.[0] + + // operations/: resolve api-kind from operation x-api-kind with channel fallback + if (firstSegment === 'operations' && pathLength === ASYNC_OPERATION_PATH_LENGTH) { + // In normalized documents, operation.channel is the resolved channel object + const beforeChannelKind = getApiKindProperty((beforeJso as AsyncAPIV3.OperationObject | undefined)?.channel) + const afterChannelKind = getApiKindProperty((afterJso as AsyncAPIV3.OperationObject | undefined)?.channel) + + // Operation's own x-api-kind takes priority, falls back to channel's x-api-kind + const beforeOperationKind = getApiKindProperty(beforeJso, beforeChannelKind) ?? APIHUB_API_COMPATIBILITY_KIND_BWC + const afterOperationKind = getApiKindProperty(afterJso, afterChannelKind) ?? APIHUB_API_COMPATIBILITY_KIND_BWC + + const isRemoved = isObject(beforeJso) && !isObject(afterJso) + if (isRemoved) { + return beforeOperationKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC + ? API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE + : API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE + } + + if (beforeOperationKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC || afterOperationKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC) { + return API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE + } + + return API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE + } + + // channels/: use channel's own x-api-kind + if (firstSegment === 'channels' && pathLength === ASYNC_CHANNEL_PATH_LENGTH) { + const beforeChannelKind = getApiKindProperty(beforeJso) ?? APIHUB_API_COMPATIBILITY_KIND_BWC + const afterChannelKind = getApiKindProperty(afterJso) ?? APIHUB_API_COMPATIBILITY_KIND_BWC + + if (beforeChannelKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC || afterChannelKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC) { + return API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE + } + + return API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE + } + + return undefined + } +} diff --git a/src/components/compare/bwc.validation.ts b/src/components/compare/bwc.validation.rest.ts similarity index 65% rename from src/components/compare/bwc.validation.ts rename to src/components/compare/bwc.validation.rest.ts index 24215446..dedaf80b 100644 --- a/src/components/compare/bwc.validation.ts +++ b/src/components/compare/bwc.validation.rest.ts @@ -30,7 +30,6 @@ import { } from '@netcracker/qubership-apihub-api-diff' import { getApiKindProperty } from '../document' import { OpenAPIV3 } from 'openapi-types' -import { v3 as AsyncAPIV3 } from '@asyncapi/parser/esm/spec-types' export const calculateOperationApiCompatibilityKind = ( beforeOperationObject: OpenAPIV3.OperationObject | undefined, @@ -95,7 +94,7 @@ const ROOT_PATH_LENGTH = 0 const PATH_ITEM_PATH_LENGTH = 2 const OPERATION_OBJECT_PATH_LENGTH = 3 -export const createApihubApiCompatibilityScopeFunction = ( +export const createRestApiCompatibilityScopeFunction = ( prevDocumentApiKind: ApihubApiCompatibilityKind = APIHUB_API_COMPATIBILITY_KIND_BWC, currDocumentApiKind: ApihubApiCompatibilityKind = APIHUB_API_COMPATIBILITY_KIND_BWC, ): ApiCompatibilityScopeFunction => { @@ -159,73 +158,3 @@ export const createApihubApiCompatibilityScopeFunction = ( return undefined } } - -const ASYNC_OPERATION_PATH_LENGTH = 2 // operations/ -const ASYNC_CHANNEL_PATH_LENGTH = 2 // channels/ - -/** - * Creates an ApiCompatibilityScopeFunction for AsyncAPI documents. - * - * AsyncAPI has no document-level x-api-kind. The hierarchy is: - * Channel (x-api-kind) → Operation (x-api-kind override) → Messages - * - * The scope function receives normalized before/after objects where $refs are resolved, - * so operation.channel is the resolved channel object with all its properties. - * - * - operations/: operation x-api-kind overrides channel x-api-kind, defaults to bwc - * - channels/: channel's own x-api-kind, defaults to bwc - */ -export const createAsyncApiCompatibilityScopeFunction = (): ApiCompatibilityScopeFunction => { - return ( - path?: JsonPath, - beforeJso?: unknown, - afterJso?: unknown, - ): ApiCompatibilityKind | undefined => { - const pathLength = path?.length ?? 0 - - // Root level: default to BWC (no document-level x-api-kind in AsyncAPI) - if (pathLength === ROOT_PATH_LENGTH) { - return API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE - } - - const firstSegment = path?.[0] - - // operations/: resolve api-kind from operation x-api-kind with channel fallback - if (firstSegment === 'operations' && pathLength === ASYNC_OPERATION_PATH_LENGTH) { - // In normalized documents, operation.channel is the resolved channel object - const beforeChannelKind = getApiKindProperty((beforeJso as AsyncAPIV3.OperationObject | undefined)?.channel) - const afterChannelKind = getApiKindProperty((afterJso as AsyncAPIV3.OperationObject | undefined)?.channel) - - // Operation's own x-api-kind takes priority, falls back to channel's x-api-kind - const beforeOperationKind = getApiKindProperty(beforeJso, beforeChannelKind) ?? APIHUB_API_COMPATIBILITY_KIND_BWC - const afterOperationKind = getApiKindProperty(afterJso, afterChannelKind) ?? APIHUB_API_COMPATIBILITY_KIND_BWC - - const isRemoved = isObject(beforeJso) && !isObject(afterJso) - if (isRemoved) { - return beforeOperationKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC - ? API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE - : API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE - } - - if (beforeOperationKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC || afterOperationKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC) { - return API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE - } - - return API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE - } - - // channels/: use channel's own x-api-kind - if (firstSegment === 'channels' && pathLength === ASYNC_CHANNEL_PATH_LENGTH) { - const beforeChannelKind = getApiKindProperty(beforeJso) ?? APIHUB_API_COMPATIBILITY_KIND_BWC - const afterChannelKind = getApiKindProperty(afterJso) ?? APIHUB_API_COMPATIBILITY_KIND_BWC - - if (beforeChannelKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC || afterChannelKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC) { - return API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE - } - - return API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE - } - - return undefined - } -} diff --git a/test/apiKinds.test.ts b/test/apiKinds.test.ts index 073b2118..4caebfe6 100644 --- a/test/apiKinds.test.ts +++ b/test/apiKinds.test.ts @@ -29,8 +29,8 @@ import { import { jest } from '@jest/globals' import { changesSummaryMatcher, Editor, LocalRegistry, serializedComparisonDocumentMatcher } from './helpers' import { takeIfDefined } from '../src/utils' -import * as bwcValidation from '../src/components/compare/bwc.validation' -import { calculateOperationApiCompatibilityKind } from '../src/components/compare/bwc.validation' +import * as bwcValidation from '../src/components/compare/bwc.validation.rest' +import { calculateOperationApiCompatibilityKind } from '../src/components/compare/bwc.validation.rest' let afterPackage: LocalRegistry const AFTER_PACKAGE_ID = 'api-kinds' diff --git a/test/asyncapi-apikind.test.ts b/test/asyncapi-apikind.test.ts index cb996004..55326cd9 100644 --- a/test/asyncapi-apikind.test.ts +++ b/test/asyncapi-apikind.test.ts @@ -6,7 +6,7 @@ import { } from '../src' import { calculateAsyncApiKind } from '../src/apitypes/async/async.utils' import { buildPackageWithDefaultConfig } from './helpers' -import { createAsyncApiCompatibilityScopeFunction } from '../src/components/compare/bwc.validation' +import { createAsyncApiCompatibilityScopeFunction } from '../src/components/compare/bwc.validation.async' import { API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE, API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE, diff --git a/test/asyncapi-changes.test.ts b/test/asyncapi-changes.test.ts index a996a27c..5dfb75dc 100644 --- a/test/asyncapi-changes.test.ts +++ b/test/asyncapi-changes.test.ts @@ -133,13 +133,8 @@ describe('AsyncAPI 3.0 Changelog tests', () => { test('should not report changes when adding message definition in channel without referencing it in operation', async () => { // message2 added to channel1.messages but operation1.messages still only references message1 // channel.messages add/remove diffs are filtered out — only operation.messages references matter - const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/add-message') - - expectNoChanges(result) - }) - - test('should ignore message added to channel but not referenced in operation', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/add-message-not-in-operation') + expectNoChanges(result) }) @@ -152,13 +147,6 @@ describe('AsyncAPI 3.0 Changelog tests', () => { expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) - test('should not report changes when adding message definition in channel with multiple apihub operations', async () => { - // message3 added to channel1.messages but operation1.messages still references only message1 and message2 - // channel.messages add/remove diffs are filtered out — only operation.messages references matter - const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/add-message-with-multiple-operations') - - expectNoChanges(result) - }) }) describe('Servers tests', () => { diff --git a/test/projects/asyncapi-changes/channel/add-message-not-in-operation/after.yaml b/test/projects/asyncapi-changes/channel/add-message-not-in-operation/after.yaml index 71cd5a68..dbe6803c 100644 --- a/test/projects/asyncapi-changes/channel/add-message-not-in-operation/after.yaml +++ b/test/projects/asyncapi-changes/channel/add-message-not-in-operation/after.yaml @@ -21,18 +21,13 @@ components: messages: message1: payload: - $ref: '#/components/schemas/Message1Payload' + type: object + properties: + userId: + type: string message2: payload: - $ref: '#/components/schemas/Message2Payload' - schemas: - Message1Payload: - type: object - properties: - userId: - type: string - Message2Payload: - type: object - properties: - orderId: - type: integer + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-changes/channel/add-message-not-in-operation/before.yaml b/test/projects/asyncapi-changes/channel/add-message-not-in-operation/before.yaml index 95f05a98..4bc38c26 100644 --- a/test/projects/asyncapi-changes/channel/add-message-not-in-operation/before.yaml +++ b/test/projects/asyncapi-changes/channel/add-message-not-in-operation/before.yaml @@ -19,18 +19,7 @@ components: messages: message1: payload: - $ref: '#/components/schemas/Message1Payload' - message2: - payload: - $ref: '#/components/schemas/Message2Payload' - schemas: - Message1Payload: - type: object - properties: - userId: - type: string - Message2Payload: - type: object - properties: - orderId: - type: integer + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changes/channel/add-message-with-multiple-operations/after.yaml b/test/projects/asyncapi-changes/channel/add-message-with-multiple-operations/after.yaml deleted file mode 100644 index a3a1cc89..00000000 --- a/test/projects/asyncapi-changes/channel/add-message-with-multiple-operations/after.yaml +++ /dev/null @@ -1,42 +0,0 @@ -asyncapi: 3.0.0 -info: - title: Test AsyncAPI - version: 1.0.0 -channels: - channel1: - address: channel1 - messages: - message1: - $ref: '#/components/messages/message1' - message2: - $ref: '#/components/messages/message2' - message3: - $ref: '#/components/messages/message3' -operations: - operation1: - action: receive - channel: - $ref: '#/channels/channel1' - messages: - - $ref: '#/channels/channel1/messages/message1' - - $ref: '#/channels/channel1/messages/message2' -components: - messages: - message1: - payload: - type: object - properties: - userId: - type: string - message2: - payload: - type: object - properties: - orderId: - type: string - message3: - payload: - type: object - properties: - orderId: - type: string diff --git a/test/projects/asyncapi-changes/channel/add-message-with-multiple-operations/before.yaml b/test/projects/asyncapi-changes/channel/add-message-with-multiple-operations/before.yaml deleted file mode 100644 index a6ae7999..00000000 --- a/test/projects/asyncapi-changes/channel/add-message-with-multiple-operations/before.yaml +++ /dev/null @@ -1,34 +0,0 @@ -asyncapi: 3.0.0 -info: - title: Test AsyncAPI - version: 1.0.0 -channels: - channel1: - address: channel1 - messages: - message1: - $ref: '#/components/messages/message1' - message2: - $ref: '#/components/messages/message2' -operations: - operation1: - action: receive - channel: - $ref: '#/channels/channel1' - messages: - - $ref: '#/channels/channel1/messages/message1' - - $ref: '#/channels/channel1/messages/message2' -components: - messages: - message1: - payload: - type: object - properties: - userId: - type: string - message2: - payload: - type: object - properties: - orderId: - type: string diff --git a/test/projects/asyncapi-changes/channel/add-message/after.yaml b/test/projects/asyncapi-changes/channel/add-message/after.yaml deleted file mode 100644 index dbe6803c..00000000 --- a/test/projects/asyncapi-changes/channel/add-message/after.yaml +++ /dev/null @@ -1,33 +0,0 @@ -asyncapi: 3.0.0 -info: - title: Test AsyncAPI - version: 1.0.0 -channels: - channel1: - address: channel1 - messages: - message1: - $ref: '#/components/messages/message1' - message2: - $ref: '#/components/messages/message2' -operations: - operation1: - action: receive - channel: - $ref: '#/channels/channel1' - messages: - - $ref: '#/channels/channel1/messages/message1' -components: - messages: - message1: - payload: - type: object - properties: - userId: - type: string - message2: - payload: - type: object - properties: - orderId: - type: string diff --git a/test/projects/asyncapi-changes/channel/add-message/before.yaml b/test/projects/asyncapi-changes/channel/add-message/before.yaml deleted file mode 100644 index 8139850a..00000000 --- a/test/projects/asyncapi-changes/channel/add-message/before.yaml +++ /dev/null @@ -1,26 +0,0 @@ -asyncapi: 3.0.0 -info: - title: Test AsyncAPI - version: 1.0.0 -channels: - channel1: - address: channel1 - messages: - message1: - $ref: '#/components/messages/message1' -operations: - operation1: - action: receive - channel: - $ref: '#/channels/channel1' - messages: - - $ref: '#/channels/channel1/messages/message1' -components: - messages: - message1: - payload: - type: object - properties: - userId: - type: string - From 69a6b5a2d3234e866f9bd601490468bfc484991a Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Tue, 24 Mar 2026 11:04:11 +0400 Subject: [PATCH 16/29] feat: Update bwc logic --- .../compare/bwc.validation.async.ts | 43 +++++++++---------- src/components/compare/bwc.validation.rest.ts | 10 ++--- .../compare/bwc.validation.types.ts | 7 +++ test/asyncapi-apikind.test.ts | 20 ++++----- 4 files changed, 43 insertions(+), 37 deletions(-) create mode 100644 src/components/compare/bwc.validation.types.ts diff --git a/src/components/compare/bwc.validation.async.ts b/src/components/compare/bwc.validation.async.ts index c11bb86d..2b3cdad7 100644 --- a/src/components/compare/bwc.validation.async.ts +++ b/src/components/compare/bwc.validation.async.ts @@ -18,16 +18,15 @@ import { APIHUB_API_COMPATIBILITY_KIND_BWC, APIHUB_API_COMPATIBILITY_KIND_NO_BWC, } from '../../consts' -import { isObject } from '../../utils' import { JsonPath } from '@netcracker/qubership-apihub-json-crawl' import { API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE, API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE, ApiCompatibilityKind, - ApiCompatibilityScopeFunction, } from '@netcracker/qubership-apihub-api-diff' import { getApiKindProperty } from '../document' import { v3 as AsyncAPIV3 } from '@asyncapi/parser/esm/spec-types' +import { ApiCompatibilityScopeFunctionFactory } from './bwc.validation.types' const ROOT_PATH_LENGTH = 0 const ASYNC_OPERATION_PATH_LENGTH = 2 // operations/ @@ -36,16 +35,24 @@ const ASYNC_CHANNEL_PATH_LENGTH = 2 // channels/ /** * Creates an ApiCompatibilityScopeFunction for AsyncAPI documents. * - * AsyncAPI has no document-level x-api-kind. The hierarchy is: - * Channel (x-api-kind) → Operation (x-api-kind override) → Messages + * The hierarchy is: + * Document (x-api-kind) → Channel (x-api-kind) → Operation (x-api-kind override) → Messages * * The scope function receives normalized before/after objects where $refs are resolved, * so operation.channel is the resolved channel object with all its properties. * - * - operations/: operation x-api-kind overrides channel x-api-kind, defaults to bwc - * - channels/: channel's own x-api-kind, defaults to bwc + * - root: document-level api-kind (from params), defaults to bwc + * - operations/: operation x-api-kind overrides channel x-api-kind + * - channels/: channel's own x-api-kind */ -export const createAsyncApiCompatibilityScopeFunction = (): ApiCompatibilityScopeFunction => { +export const createAsyncApiCompatibilityScopeFunction: ApiCompatibilityScopeFunctionFactory = ( + prevDocumentApiKind = APIHUB_API_COMPATIBILITY_KIND_BWC, + currDocumentApiKind = APIHUB_API_COMPATIBILITY_KIND_BWC, +) => { + const defaultApiCompatibilityKind = (prevDocumentApiKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC || currDocumentApiKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC) + ? API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE + : API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE + return ( path?: JsonPath, beforeJso?: unknown, @@ -53,9 +60,8 @@ export const createAsyncApiCompatibilityScopeFunction = (): ApiCompatibilityScop ): ApiCompatibilityKind | undefined => { const pathLength = path?.length ?? 0 - // Root level: default to BWC (no document-level x-api-kind in AsyncAPI) if (pathLength === ROOT_PATH_LENGTH) { - return API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE + return defaultApiCompatibilityKind } const firstSegment = path?.[0] @@ -67,33 +73,26 @@ export const createAsyncApiCompatibilityScopeFunction = (): ApiCompatibilityScop const afterChannelKind = getApiKindProperty((afterJso as AsyncAPIV3.OperationObject | undefined)?.channel) // Operation's own x-api-kind takes priority, falls back to channel's x-api-kind - const beforeOperationKind = getApiKindProperty(beforeJso, beforeChannelKind) ?? APIHUB_API_COMPATIBILITY_KIND_BWC - const afterOperationKind = getApiKindProperty(afterJso, afterChannelKind) ?? APIHUB_API_COMPATIBILITY_KIND_BWC - - const isRemoved = isObject(beforeJso) && !isObject(afterJso) - if (isRemoved) { - return beforeOperationKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC - ? API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE - : API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE - } + const beforeOperationKind = getApiKindProperty(beforeJso, beforeChannelKind) + const afterOperationKind = getApiKindProperty(afterJso, afterChannelKind) if (beforeOperationKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC || afterOperationKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC) { return API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE } - return API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE + return undefined } // channels/: use channel's own x-api-kind if (firstSegment === 'channels' && pathLength === ASYNC_CHANNEL_PATH_LENGTH) { - const beforeChannelKind = getApiKindProperty(beforeJso) ?? APIHUB_API_COMPATIBILITY_KIND_BWC - const afterChannelKind = getApiKindProperty(afterJso) ?? APIHUB_API_COMPATIBILITY_KIND_BWC + const beforeChannelKind = getApiKindProperty(beforeJso) + const afterChannelKind = getApiKindProperty(afterJso) if (beforeChannelKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC || afterChannelKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC) { return API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE } - return API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE + return undefined } return undefined diff --git a/src/components/compare/bwc.validation.rest.ts b/src/components/compare/bwc.validation.rest.ts index dedaf80b..a90bf6f8 100644 --- a/src/components/compare/bwc.validation.rest.ts +++ b/src/components/compare/bwc.validation.rest.ts @@ -26,10 +26,10 @@ import { API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE, API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE, ApiCompatibilityKind, - ApiCompatibilityScopeFunction, } from '@netcracker/qubership-apihub-api-diff' import { getApiKindProperty } from '../document' import { OpenAPIV3 } from 'openapi-types' +import { ApiCompatibilityScopeFunctionFactory } from './bwc.validation.types' export const calculateOperationApiCompatibilityKind = ( beforeOperationObject: OpenAPIV3.OperationObject | undefined, @@ -94,10 +94,10 @@ const ROOT_PATH_LENGTH = 0 const PATH_ITEM_PATH_LENGTH = 2 const OPERATION_OBJECT_PATH_LENGTH = 3 -export const createRestApiCompatibilityScopeFunction = ( - prevDocumentApiKind: ApihubApiCompatibilityKind = APIHUB_API_COMPATIBILITY_KIND_BWC, - currDocumentApiKind: ApihubApiCompatibilityKind = APIHUB_API_COMPATIBILITY_KIND_BWC, -): ApiCompatibilityScopeFunction => { +export const createRestApiCompatibilityScopeFunction: ApiCompatibilityScopeFunctionFactory = ( + prevDocumentApiKind = APIHUB_API_COMPATIBILITY_KIND_BWC, + currDocumentApiKind = APIHUB_API_COMPATIBILITY_KIND_BWC, +) => { const defaultApiCompatibilityKind = (prevDocumentApiKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC || currDocumentApiKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC) ? API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE : API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE diff --git a/src/components/compare/bwc.validation.types.ts b/src/components/compare/bwc.validation.types.ts new file mode 100644 index 00000000..f27597dd --- /dev/null +++ b/src/components/compare/bwc.validation.types.ts @@ -0,0 +1,7 @@ +import { ApihubApiCompatibilityKind } from '../../consts' +import { ApiCompatibilityScopeFunction } from '@netcracker/qubership-apihub-api-diff' + +export type ApiCompatibilityScopeFunctionFactory = ( + prevDocumentApiKind?: ApihubApiCompatibilityKind, + currDocumentApiKind?: ApihubApiCompatibilityKind, +) => ApiCompatibilityScopeFunction diff --git a/test/asyncapi-apikind.test.ts b/test/asyncapi-apikind.test.ts index 55326cd9..e18ad769 100644 --- a/test/asyncapi-apikind.test.ts +++ b/test/asyncapi-apikind.test.ts @@ -45,10 +45,10 @@ describe('AsyncAPI apiKind calculation', () => { }) describe('Channels scope', () => { - it('should return BWC when channel has no x-api-kind', () => { + it('should return undefined when channel has no x-api-kind', () => { const before = { address: 'channel1' } const after = { address: 'channel1' } - expect(scopeFunction(['channels', 'ch1'], before, after)).toBe(API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE) + expect(scopeFunction(['channels', 'ch1'], before, after)).toBeUndefined() }) it('should return no-BWC when before channel has x-api-kind no-BWC', () => { @@ -75,10 +75,10 @@ describe('AsyncAPI apiKind calculation', () => { expect(scopeFunction(['channels', 'ch1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) }) - it('should return BWC when both channels have x-api-kind BWC', () => { + it('should return undefined when both channels have x-api-kind BWC', () => { const before = { address: 'channel1', 'x-api-kind': 'BWC'} const after = { address: 'channel1', 'x-api-kind': 'BWC'} - expect(scopeFunction(['channels', 'ch1'], before, after)).toBe(API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE) + expect(scopeFunction(['channels', 'ch1'], before, after)).toBeUndefined() }) it('should return no-BWC when both channels have x-api-kind no-BWC', () => { @@ -89,10 +89,10 @@ describe('AsyncAPI apiKind calculation', () => { }) describe('Operations scope', () => { - it('should return BWC when operation and channel have no x-api-kind', () => { + it('should return undefined when operation and channel have no x-api-kind', () => { const before = { action: 'receive', channel: {} } const after = { action: 'receive', channel: {} } - expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE) + expect(scopeFunction(['operations', 'op1'], before, after)).toBeUndefined() }) it('should return no-BWC when before operation channel has x-api-kind no-BWC', () => { @@ -119,10 +119,10 @@ describe('AsyncAPI apiKind calculation', () => { expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) }) - it('should return BWC when operation x-api-kind BWC overrides channel x-api-kind no-BWC', () => { + it('should return undefined when operation x-api-kind BWC overrides channel x-api-kind no-BWC', () => { const before = { action: 'receive', 'x-api-kind': 'BWC', channel: { 'x-api-kind': 'no-BWC' } } const after = { action: 'receive', 'x-api-kind': 'BWC', channel: { 'x-api-kind': 'no-BWC' } } - expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE) + expect(scopeFunction(['operations', 'op1'], before, after)).toBeUndefined() }) it('should return no-BWC when operation x-api-kind no-BWC overrides channel x-api-kind BWC', () => { @@ -131,9 +131,9 @@ describe('AsyncAPI apiKind calculation', () => { expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) }) - it('should return BWC when removed operation has no x-api-kind', () => { + it('should return undefined when removed operation has no x-api-kind', () => { const before = { action: 'receive', channel: {} } - expect(scopeFunction(['operations', 'op1'], before, undefined)).toBe(API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE) + expect(scopeFunction(['operations', 'op1'], before, undefined)).toBeUndefined() }) it('should return no-BWC when removed operation has x-api-kind no-BWC', () => { From 00f8e606a42d757e5e15c46b5e0c94707aa8a99b Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Tue, 24 Mar 2026 14:44:25 +0400 Subject: [PATCH 17/29] feat: Update bwc tests --- test/asyncapi-apikind.test.ts | 212 ++++++++++-------- .../projects/asyncapi/api-kind/base/spec.yaml | 4 +- 2 files changed, 118 insertions(+), 98 deletions(-) diff --git a/test/asyncapi-apikind.test.ts b/test/asyncapi-apikind.test.ts index e18ad769..cd377354 100644 --- a/test/asyncapi-apikind.test.ts +++ b/test/asyncapi-apikind.test.ts @@ -1,4 +1,5 @@ import { + API_KIND_SPECIFICATION_EXTENSION, APIHUB_API_COMPATIBILITY_KIND_BWC, APIHUB_API_COMPATIBILITY_KIND_NO_BWC, ApihubApiCompatibilityKind, @@ -32,122 +33,141 @@ describe('AsyncAPI apiKind calculation', () => { }) describe('Changelog backward compatibility scope function', () => { - const scopeFunction = createAsyncApiCompatibilityScopeFunction() + const createChannel = (apiKind: string | undefined): { + address: string + [API_KIND_SPECIFICATION_EXTENSION]?: ApihubApiCompatibilityKind + } => { + return apiKind + ? { address: 'channel1', [API_KIND_SPECIFICATION_EXTENSION]: apiKind as ApihubApiCompatibilityKind } + : { address: 'channel1' } + } + + const createOperation = (apiKind: string | undefined): { + action: string + channel: unknown + [API_KIND_SPECIFICATION_EXTENSION]?: ApihubApiCompatibilityKind + } => { + return apiKind + ? { + action: 'receive', + [API_KIND_SPECIFICATION_EXTENSION]: apiKind as ApihubApiCompatibilityKind, + channel: {}, + } + : { action: 'receive', channel: {} } + } + + // short names + const BWC = API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE + const NOT_BWC = API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE describe('Root level', () => { - it('should return BWC for root path', () => { - expect(scopeFunction([], {}, {})).toBe(API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE) - }) - - it('should return BWC for undefined path', () => { - expect(scopeFunction(undefined, {}, {})).toBe(API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE) + it.each([ + // prev | curr | expected + ['bwc', 'bwc', BWC], + ['bwc', 'no-bwc', NOT_BWC], + ['no-bwc', 'bwc', NOT_BWC], + ['no-bwc', 'no-bwc', NOT_BWC], + ])('prev: %s, curr: %s → should return %s', (prev, curr, expected) => { + const scopeFunction = createAsyncApiCompatibilityScopeFunction( + prev as ApihubApiCompatibilityKind, + curr as ApihubApiCompatibilityKind, + ) + expect(scopeFunction([], {}, {})).toBe(expected) }) }) describe('Channels scope', () => { - it('should return undefined when channel has no x-api-kind', () => { - const before = { address: 'channel1' } - const after = { address: 'channel1' } - expect(scopeFunction(['channels', 'ch1'], before, after)).toBeUndefined() - }) - - it('should return no-BWC when before channel has x-api-kind no-BWC', () => { - const before = { address: 'channel1', 'x-api-kind': 'no-BWC' } - const after = { address: 'channel1' } - expect(scopeFunction(['channels', 'ch1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) - }) - - it('should return no-BWC when after channel has x-api-kind no-BWC', () => { - const before = { address: 'channel1' } - const after = { address: 'channel1', 'x-api-kind': 'no-BWC' } - expect(scopeFunction(['channels', 'ch1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) - }) - - it('should return no-BWC when before channel BWC and after channel no-BWC', () => { - const before = { address: 'channel1', 'x-api-kind': 'BWC'} - const after = { address: 'channel1', 'x-api-kind': 'no-BWC' } - expect(scopeFunction(['channels', 'ch1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) - }) - - it('should return no-BWC when before channel no-BWC and after channel BWC', () => { - const before = { address: 'channel1', 'x-api-kind': 'no-BWC'} - const after = { address: 'channel1', 'x-api-kind': 'BWC'} - expect(scopeFunction(['channels', 'ch1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) - }) - - it('should return undefined when both channels have x-api-kind BWC', () => { - const before = { address: 'channel1', 'x-api-kind': 'BWC'} - const after = { address: 'channel1', 'x-api-kind': 'BWC'} - expect(scopeFunction(['channels', 'ch1'], before, after)).toBeUndefined() - }) - - it('should return no-BWC when both channels have x-api-kind no-BWC', () => { - const before = { address: 'channel1', 'x-api-kind': 'no-BWC'} - const after = { address: 'channel1', 'x-api-kind': 'no-BWC'} - expect(scopeFunction(['channels', 'ch1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) + it.each([ + // default | before | after | expected + ['bwc', 'undefined', 'undefined', undefined], + ['bwc', 'undefined', 'bwc', undefined], + ['bwc', 'undefined', 'no-bwc', NOT_BWC], + ['bwc', 'bwc', 'undefined', undefined], + ['bwc', 'bwc', 'bwc', undefined], + ['bwc', 'bwc', 'no-bwc', NOT_BWC], + ['bwc', 'no-bwc', 'undefined', NOT_BWC], + ['bwc', 'no-bwc', 'bwc', NOT_BWC], + ['bwc', 'no-bwc', 'no-bwc', NOT_BWC], + ['no-bwc', 'undefined', 'undefined', undefined], + ['no-bwc', 'undefined', 'bwc', undefined], + ['no-bwc', 'undefined', 'no-bwc', NOT_BWC], + ['no-bwc', 'bwc', 'undefined', undefined], + ['no-bwc', 'bwc', 'bwc', undefined], + ['no-bwc', 'bwc', 'no-bwc', NOT_BWC], + ['no-bwc', 'no-bwc', 'undefined', NOT_BWC], + ['no-bwc', 'no-bwc', 'bwc', NOT_BWC], + ['no-bwc', 'no-bwc', 'no-bwc', NOT_BWC], + ] as const)('documentApiKind: %s, before: %s, after: %s', ( + documentApiKind, + beforeKind, + afterKind, + expected, + ) => { + const scopeFunction = createAsyncApiCompatibilityScopeFunction(documentApiKind) + expect(scopeFunction(['channels', 'ch1'], createChannel(beforeKind), createChannel(afterKind))).toBe(expected) }) }) describe('Operations scope', () => { - it('should return undefined when operation and channel have no x-api-kind', () => { - const before = { action: 'receive', channel: {} } - const after = { action: 'receive', channel: {} } - expect(scopeFunction(['operations', 'op1'], before, after)).toBeUndefined() + it.each([ + // default | before | after | expected + ['bwc', 'undefined', 'undefined', undefined], + ['bwc', 'undefined', 'bwc', undefined], + ['bwc', 'undefined', 'no-bwc', NOT_BWC], + ['bwc', 'bwc', 'undefined', undefined], + ['bwc', 'bwc', 'bwc', undefined], + ['bwc', 'bwc', 'no-bwc', NOT_BWC], + ['bwc', 'no-bwc', 'undefined', NOT_BWC], + ['bwc', 'no-bwc', 'bwc', NOT_BWC], + ['bwc', 'no-bwc', 'no-bwc', NOT_BWC], + ['no-bwc', 'undefined', 'undefined', undefined], + ['no-bwc', 'undefined', 'bwc', undefined], + ['no-bwc', 'undefined', 'no-bwc', NOT_BWC], + ['no-bwc', 'bwc', 'undefined', undefined], + ['no-bwc', 'bwc', 'bwc', undefined], + ['no-bwc', 'bwc', 'no-bwc', NOT_BWC], + ['no-bwc', 'no-bwc', 'undefined', NOT_BWC], + ['no-bwc', 'no-bwc', 'bwc', NOT_BWC], + ['no-bwc', 'no-bwc', 'no-bwc', NOT_BWC], + ] as const)('documentApiKind: %s, before: %s, after: %s', ( + documentApiKind, + beforeKind, + afterKind, + expected, + ) => { + const scopeFunction = createAsyncApiCompatibilityScopeFunction(documentApiKind) + expect(scopeFunction(['operations', 'op1'], createOperation(beforeKind), createOperation(afterKind))).toBe(expected) }) - it('should return no-BWC when before operation channel has x-api-kind no-BWC', () => { - const before = { action: 'receive', channel: { 'x-api-kind': 'no-BWC' } } + it('should use channel x-api-kind as fallback when operation has no x-api-kind', () => { + const scopeFunction = createAsyncApiCompatibilityScopeFunction() + const before = { + action: 'receive', + channel: { [API_KIND_SPECIFICATION_EXTENSION]: APIHUB_API_COMPATIBILITY_KIND_NO_BWC }, + } const after = { action: 'receive', channel: {} } - expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) + expect(scopeFunction(['operations', 'op1'], before, after)).toBe(NOT_BWC) }) - it('should return no-BWC when after operation channel has x-api-kind no-BWC', () => { - const before = { action: 'receive', channel: {} } - const after = { action: 'receive', channel: { 'x-api-kind': 'no-BWC' } } - expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) - }) - - it('should return no-BWC when before operation has x-api-kind no-BWC', () => { - const before = { action: 'receive', 'x-api-kind': 'no-BWC', channel: {} } - const after = { action: 'receive', channel: {} } - expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) - }) - - it('should return no-BWC when after operation has x-api-kind no-BWC', () => { - const before = { action: 'receive', channel: {} } - const after = { action: 'receive', 'x-api-kind': 'no-BWC', channel: {} } - expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) - }) - - it('should return undefined when operation x-api-kind BWC overrides channel x-api-kind no-BWC', () => { - const before = { action: 'receive', 'x-api-kind': 'BWC', channel: { 'x-api-kind': 'no-BWC' } } - const after = { action: 'receive', 'x-api-kind': 'BWC', channel: { 'x-api-kind': 'no-BWC' } } + it('should let operation x-api-kind override channel x-api-kind', () => { + const scopeFunction = createAsyncApiCompatibilityScopeFunction() + const before = { + action: 'receive', + [API_KIND_SPECIFICATION_EXTENSION]: APIHUB_API_COMPATIBILITY_KIND_BWC, + channel: { [API_KIND_SPECIFICATION_EXTENSION]: APIHUB_API_COMPATIBILITY_KIND_NO_BWC }, + } + const after = { + action: 'receive', + [API_KIND_SPECIFICATION_EXTENSION]: APIHUB_API_COMPATIBILITY_KIND_BWC, + channel: { [API_KIND_SPECIFICATION_EXTENSION]: APIHUB_API_COMPATIBILITY_KIND_NO_BWC }, + } expect(scopeFunction(['operations', 'op1'], before, after)).toBeUndefined() }) - - it('should return no-BWC when operation x-api-kind no-BWC overrides channel x-api-kind BWC', () => { - const before = { action: 'receive', 'x-api-kind': 'no-BWC', channel: { 'x-api-kind': 'BWC' } } - const after = { action: 'receive', 'x-api-kind': 'no-BWC', channel: { 'x-api-kind': 'BWC' } } - expect(scopeFunction(['operations', 'op1'], before, after)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) - }) - - it('should return undefined when removed operation has no x-api-kind', () => { - const before = { action: 'receive', channel: {} } - expect(scopeFunction(['operations', 'op1'], before, undefined)).toBeUndefined() - }) - - it('should return no-BWC when removed operation has x-api-kind no-BWC', () => { - const before = { action: 'receive', 'x-api-kind': 'no-BWC', channel: {} } - expect(scopeFunction(['operations', 'op1'], before, undefined)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) - }) - - it('should return no-BWC when removed operation channel has x-api-kind no-BWC', () => { - const before = { action: 'receive', channel: { 'x-api-kind': 'no-BWC' } } - expect(scopeFunction(['operations', 'op1'], before, undefined)).toBe(API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE) - }) }) describe('Other paths', () => { + const scopeFunction = createAsyncApiCompatibilityScopeFunction() + it('should return undefined for non-operations/non-channels paths', () => { expect(scopeFunction(['components', 'messages'], {}, {})).toBeUndefined() }) diff --git a/test/projects/asyncapi/api-kind/base/spec.yaml b/test/projects/asyncapi/api-kind/base/spec.yaml index 50863103..a2a998f7 100644 --- a/test/projects/asyncapi/api-kind/base/spec.yaml +++ b/test/projects/asyncapi/api-kind/base/spec.yaml @@ -28,13 +28,13 @@ operations: $ref: '#/channels/channel' messages: - $ref: '#/channels/channel/messages/UserSignedUp' - operation-with-cannel-bwc: + operation-with-channel-bwc: action: send channel: $ref: '#/channels/channel-bwc' messages: - $ref: '#/channels/channel-bwc/messages/UserSignedUp' - operation-with-cannel-no-bwc: + operation-with-channel-no-bwc: action: send channel: $ref: '#/channels/channel-no-bwc' From 90966ccd8640ad2c06d9d920c639c6bd2c32c085 Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Tue, 24 Mar 2026 16:28:13 +0400 Subject: [PATCH 18/29] feat: test review --- test/asyncapi-apikind.test.ts | 72 ++++++++++++++--------------- test/asyncapi-changes.test.ts | 45 +++++++++++++++--- test/asyncapi-deduplication.test.ts | 65 ++++++++++++++++++-------- 3 files changed, 122 insertions(+), 60 deletions(-) diff --git a/test/asyncapi-apikind.test.ts b/test/asyncapi-apikind.test.ts index cd377354..9526cb98 100644 --- a/test/asyncapi-apikind.test.ts +++ b/test/asyncapi-apikind.test.ts @@ -184,65 +184,65 @@ describe('AsyncAPI apiKind calculation', () => { }) describe('AsyncAPI operation/channel compatibility apiKind application', () => { - let operation: ApiOperation - let operationWithChannelBwc: ApiOperation - let operationWithChannelNoBwc: ApiOperation - let operationBwc: ApiOperation - let operationNoBwc: ApiOperation - let operationBwcWithChannelBwc: ApiOperation - let operationBwcWithChannelNoBwc: ApiOperation - let operationNoBwcWithChannelBwc: ApiOperation - let operationNoBwcWithChannelNoBwc: ApiOperation + let operationNoKindChannelNoKind: ApiOperation + let operationNoKindChannelBWC: ApiOperation + let operationNoKindChannelNoBWC: ApiOperation + let operationBWCChannelNoKind: ApiOperation + let operationNoBWCChannelNoKind: ApiOperation + let operationBWCChannelBWC: ApiOperation + let operationBWCChannelNoBWC: ApiOperation + let operationNoBWCChannelBWC: ApiOperation + let operationNoBWCChannelNoBWC: ApiOperation beforeAll(async () => { const result = await buildPackageWithDefaultConfig('asyncapi/api-kind/base') ;[ - operation, - operationWithChannelBwc, - operationWithChannelNoBwc, - operationBwc, - operationNoBwc, - operationBwcWithChannelBwc, - operationBwcWithChannelNoBwc, - operationNoBwcWithChannelBwc, - operationNoBwcWithChannelNoBwc, + operationNoKindChannelNoKind, + operationNoKindChannelBWC, + operationNoKindChannelNoBWC, + operationBWCChannelNoKind, + operationNoBWCChannelNoKind, + operationBWCChannelBWC, + operationBWCChannelNoBWC, + operationNoBWCChannelBWC, + operationNoBWCChannelNoBWC, ] = Array.from(result.operations.values()) }) - it('should apply BWC apiKind when both operation and channel have no apiKind', () => { - expect(operation.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_BWC) + it('operationNoKindChannelNoKind → BWC', () => { + expect(operationNoKindChannelNoKind.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_BWC) }) - it('should apply BWC apiKind when channel apiKind is BWC and operation has no apiKind', () => { - expect(operationWithChannelBwc.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_BWC) + it('operationNoKindChannelBWC → BWC', () => { + expect(operationNoKindChannelBWC.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_BWC) }) - it('should apply NO_BWC apiKind when channel apiKind is NO_BWC and operation has no apiKind', () => { - expect(operationWithChannelNoBwc.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_NO_BWC) + it('operationNoKindChannelNoBWC → NoBWC', () => { + expect(operationNoKindChannelNoBWC.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_NO_BWC) }) - it('should apply BWC apiKind when operation apiKind is BWC and channel has no apiKind', () => { - expect(operationBwc.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_BWC) + it('operationBWCChannelNoKind → BWC', () => { + expect(operationBWCChannelNoKind.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_BWC) }) - it('should apply NO_BWC apiKind when operation apiKind is NO_BWC and channel has no apiKind', () => { - expect(operationNoBwc.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_NO_BWC) + it('operationNoBWCChannelNoKind → NoBWC', () => { + expect(operationNoBWCChannelNoKind.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_NO_BWC) }) - it('should apply BWC apiKind when both operation and channel apiKind are BWC', () => { - expect(operationBwcWithChannelBwc.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_BWC) + it('operationBWCChannelBWC → BWC', () => { + expect(operationBWCChannelBWC.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_BWC) }) - it('should apply BWC apiKind when operation apiKind is BWC and channel apiKind is NO_BWC', () => { - expect(operationBwcWithChannelNoBwc.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_BWC) + it('operationBWCChannelNoBWC → BWC', () => { + expect(operationBWCChannelNoBWC.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_BWC) }) - it('should apply NO_BWC apiKind when operation apiKind is NO_BWC and channel apiKind is BWC', () => { - expect(operationNoBwcWithChannelBwc.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_NO_BWC) + it('operationNoBWCChannelBWC → NoBWC', () => { + expect(operationNoBWCChannelBWC.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_NO_BWC) }) - it('should apply NO_BWC apiKind when both operation and channel apiKind are NO_BWC', () => { - expect(operationNoBwcWithChannelNoBwc.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_NO_BWC) + it('operationNoBWCChannelNoBWC → NoBWC', () => { + expect(operationNoBWCChannelNoBWC.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_NO_BWC) }) }) diff --git a/test/asyncapi-changes.test.ts b/test/asyncapi-changes.test.ts index 5dfb75dc..7252a794 100644 --- a/test/asyncapi-changes.test.ts +++ b/test/asyncapi-changes.test.ts @@ -3,6 +3,7 @@ import { changesSummaryMatcher, noChangesMatcher, numberOfImpactedOperationsMatcher, + operationChangesMatcher, operationTypeMatcher, } from './helpers' import { @@ -144,7 +145,12 @@ describe('AsyncAPI 3.0 Changelog tests', () => { // channel1 address changed, channel2 unchanged // only operation1 (on channel1) should be impacted, not operation2 (on channel2) expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ + operationId: 'operation1-message1', + previousOperationId: 'operation1-message1', + }), + ])) }) }) @@ -182,7 +188,12 @@ describe('AsyncAPI 3.0 Changelog tests', () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/server/change-isolated-servers') expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ + operationId: 'operation1-message1', + previousOperationId: 'operation1-message1', + }), + ])) }) // TODO: unskip when root servers propagate to channel.servers during normalization @@ -247,7 +258,11 @@ describe('AsyncAPI 3.0 Changelog tests', () => { // message2 added to operation1, operation2 unchanged // should only impact 1 new apihub operation (operation1-message2) expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ + operationId: 'operation1-message2', + }), + ])) }) test('should not add changes to remaining messages when removing message from operation', async () => { @@ -257,7 +272,11 @@ describe('AsyncAPI 3.0 Changelog tests', () => { // should only impact 1 removed apihub operation (operation1-message2), // not the remaining operation1-message1 and operation1-message3 expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ + previousOperationId: 'operation1-message2', + }), + ])) }) test('should only impact changed message when changing content type with multiple messages', async () => { @@ -266,7 +285,12 @@ describe('AsyncAPI 3.0 Changelog tests', () => { // Changing contentType of message1 in operation with message1 and message2 // should only impact operation1-message1, not operation1-message2 expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ + operationId: 'operation1-message1', + previousOperationId: 'operation1-message1', + }), + ])) }) test('should impact both operations when changing shared payload with multiple messages', async () => { @@ -275,7 +299,16 @@ describe('AsyncAPI 3.0 Changelog tests', () => { // message1 and message2 both reference SharedPayload schema // changing SharedPayload type should impact both apihub operations expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ + operationId: 'operation1-message1', + previousOperationId: 'operation1-message1', + }), + expect.objectContaining({ + operationId: 'operation1-message2', + previousOperationId: 'operation1-message2', + }), + ])) }) test('should not report changes when adding unused component message', async () => { diff --git a/test/asyncapi-deduplication.test.ts b/test/asyncapi-deduplication.test.ts index d0d18a85..c5a75ebc 100644 --- a/test/asyncapi-deduplication.test.ts +++ b/test/asyncapi-deduplication.test.ts @@ -43,25 +43,7 @@ describe('AsyncAPI deduplication tests', () => { expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) }) - }) - describe('Shared entities across different specifications', () => { - test('shared schema name in two specs, different scopes (receive vs send)', async () => { - // operation1 (receive) in doc1, operation2 (send) in doc2. - // Both specs define SharedPayload with same change (number → string). - // Different apiDiff calls → different diff instances. Different operations → no cross-operation dedup. - const result = await buildChangelogPackageDefaultConfig( - 'asyncapi-deduplication/shared-schema-cross-specs', - [{ fileId: 'before1.yaml', publish: true }, { fileId: 'before2.yaml', publish: true }], - [{ fileId: 'after1.yaml' }, { fileId: 'after2.yaml' }], - ) - - expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) - }) - }) - - describe('Root-level change deduplication', () => { test('should count info.version change once in changesSummary but impact all operations', async () => { // Two operations. info.version changed (1.0.0 → 2.0.0). // The info diff is extracted and added to every operation via extractInfoDiffs(), @@ -85,6 +67,53 @@ describe('AsyncAPI deduplication tests', () => { }) }) + describe('Shared entities across different specifications', () => { + test('shared schema name in two specs, different scopes (receive vs send)', async () => { + // operation1 (receive) in doc1, operation2 (send) in doc2. + // Both specs define SharedPayload with same change (number → string). + // Different scopes → separate apiDiff calls → separate diff instances. + // Cross-document content-based dedup (calculateDiffId) applies per operation, + // but these are different operations so both changes are counted. + const result = await buildChangelogPackageDefaultConfig( + 'asyncapi-deduplication/shared-schema-cross-specs', + [{ fileId: 'before1.yaml', publish: true }, { fileId: 'before2.yaml', publish: true }], + [{ fileId: 'after1.yaml' }, { fileId: 'after2.yaml' }], + ) + + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) + }) + + test('shared schema name in two specs, same scope (both receive)', async () => { + // operation1 (receive) in doc1, operation2 (receive) in doc2. + // Both specs define SharedPayload with same change (number → string). + // Same scope → same group, but different documents → separate doc pairs → two apiDiff calls. + // Cross-document content-based dedup via calculateDiffId merges identical diffs. + const result = await buildChangelogPackageDefaultConfig( + 'asyncapi-deduplication/shared-schema-cross-specs-same-scope', + [{ fileId: 'before1.yaml', publish: true }, { fileId: 'before2.yaml', publish: true }], + [{ fileId: 'after1.yaml' }, { fileId: 'after2.yaml' }], + ) + + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) + }) + + test('same schema name in two specs but different content should not deduplicate', async () => { + // operation1 in doc1 with SharedPayload{userId: number→string}, + // operation2 in doc2 with SharedPayload{orderId: integer→string}. + // Same schema name but different properties/changes → no dedup, both counted separately. + const result = await buildChangelogPackageDefaultConfig( + 'asyncapi-deduplication/shared-schema-cross-specs-different-content', + [{ fileId: 'before1.yaml', publish: true }, { fileId: 'before2.yaml', publish: true }], + [{ fileId: 'after1.yaml' }, { fileId: 'after2.yaml' }], + ) + + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) + }) + }) + describe('Message-level isolation', () => { test('should not leak add-message diff to existing sibling message operations', async () => { // operation1 has message1, message2. message3 is added. From 255bec1c901d5d41660a713de11cbe6a99c61ef4 Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Wed, 25 Mar 2026 11:10:56 +0400 Subject: [PATCH 19/29] feat: Tests refactoring --- .../compare/bwc.validation.async.ts | 8 ++-- test/asyncapi-changes.test.ts | 36 ++++++++++++++++ test/asyncapi-deduplication.test.ts | 37 ---------------- .../add-message-no-sibling-impact/after.yaml | 43 ------------------- .../add-message-no-sibling-impact/before.yaml | 42 ------------------ .../after.yaml | 37 ---------------- .../before.yaml | 37 ---------------- 7 files changed, 41 insertions(+), 199 deletions(-) delete mode 100644 test/projects/asyncapi-deduplication/add-message-no-sibling-impact/after.yaml delete mode 100644 test/projects/asyncapi-deduplication/add-message-no-sibling-impact/before.yaml delete mode 100644 test/projects/asyncapi-deduplication/mixed-operation-and-message-changes/after.yaml delete mode 100644 test/projects/asyncapi-deduplication/mixed-operation-and-message-changes/before.yaml diff --git a/src/components/compare/bwc.validation.async.ts b/src/components/compare/bwc.validation.async.ts index 2b3cdad7..ac27c6ae 100644 --- a/src/components/compare/bwc.validation.async.ts +++ b/src/components/compare/bwc.validation.async.ts @@ -35,13 +35,15 @@ const ASYNC_CHANNEL_PATH_LENGTH = 2 // channels/ /** * Creates an ApiCompatibilityScopeFunction for AsyncAPI documents. * - * The hierarchy is: - * Document (x-api-kind) → Channel (x-api-kind) → Operation (x-api-kind override) → Messages + * Resolution order: + * Channel (x-api-kind) → Operation (x-api-kind override) → Messages + * Document-level x-api-kind is not supported for AsyncAPI yet; + * prevDocumentApiKind/currDocumentApiKind params are reserved for future use. * * The scope function receives normalized before/after objects where $refs are resolved, * so operation.channel is the resolved channel object with all its properties. * - * - root: document-level api-kind (from params), defaults to bwc + * - root: defaults to bwc (document-level api-kind reserved for future use) * - operations/: operation x-api-kind overrides channel x-api-kind * - channels/: channel's own x-api-kind */ diff --git a/test/asyncapi-changes.test.ts b/test/asyncapi-changes.test.ts index 7252a794..cb1ba891 100644 --- a/test/asyncapi-changes.test.ts +++ b/test/asyncapi-changes.test.ts @@ -311,6 +311,42 @@ describe('AsyncAPI 3.0 Changelog tests', () => { ])) }) + test('should not leak add-message diff to existing sibling message operations', async () => { + // operation1 has message1, message2. message3 is added to operation1. + // The add diff should only appear on the new operation1-message3, + // not on existing operation1-message1 or operation1-message2. + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/message/add-message-no-sibling-impact') + + expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ + operationId: 'operation1-message3', + }), + ])) + }) + + test('should correctly separate operation-level and message-level changes', async () => { + // operation1 with message1 and message2. + // Changes: operation description changed (annotation, shared by both messages) + // + message1 contentType changed (breaking, specific to message1 only) + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/message/mixed-operation-and-message-changes') + + expect(result).toEqual(changesSummaryMatcher({ + [ANNOTATION_CHANGE_TYPE]: 1, + [BREAKING_CHANGE_TYPE]: 1, + }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ + operationId: 'operation1-message1', + previousOperationId: 'operation1-message1', + }), + expect.objectContaining({ + operationId: 'operation1-message2', + previousOperationId: 'operation1-message2', + }), + ])) + }) + test('should not report changes when adding unused component message', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/message/add-unused-component-message') diff --git a/test/asyncapi-deduplication.test.ts b/test/asyncapi-deduplication.test.ts index c5a75ebc..f1dde96f 100644 --- a/test/asyncapi-deduplication.test.ts +++ b/test/asyncapi-deduplication.test.ts @@ -14,8 +14,6 @@ import { ANNOTATION_CHANGE_TYPE, ASYNCAPI_API_TYPE, BREAKING_CHANGE_TYPE, UNCLAS * - `aggregateDiffsWithRollup` propagates diffs bottom-up via Set * - Same Diff instance can appear in multiple operations (e.g. shared component schema) * - `comparePairedDocs` deduplicates via `new Set(allDiffs)` — reference identity - * - `collectExclusiveOtherMessageDiffs` filters sibling message diffs so they don't - * leak into unrelated (operation, message) pairs * * Across multiple document pairs (by content hash): * - Rare case: one operation in multiple documents → multiple apiDiff() calls @@ -112,42 +110,7 @@ describe('AsyncAPI deduplication tests', () => { expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) }) - }) - - describe('Message-level isolation', () => { - test('should not leak add-message diff to existing sibling message operations', async () => { - // operation1 has message1, message2. message3 is added. - // The array-level diff for adding message3 should only appear on the new operation1-message3, - // not on operation1-message1 or operation1-message2. - // collectExclusiveOtherMessageDiffs filters out the sibling's array-level diff. - const result = await buildChangelogPackageDefaultConfig('asyncapi-deduplication/add-message-no-sibling-impact') - - // Only 1 breaking change (the added message3 operation), impacting 1 operation - expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - }) - - test('should correctly separate operation-level and message-level changes', async () => { - // operation1 with message1 and message2. - // Changes: operation description changed (annotation, shared by both messages) - // + message1 contentType changed (breaking, specific to message1 only) - // - // Expected: annotation (description) = 1 in summary, impacted 2 (both messages) - // breaking (contentType) = 1 in summary, impacted 1 (only message1) - const result = await buildChangelogPackageDefaultConfig('asyncapi-deduplication/mixed-operation-and-message-changes') - - expect(result).toEqual(changesSummaryMatcher({ - [ANNOTATION_CHANGE_TYPE]: 1, - [BREAKING_CHANGE_TYPE]: 1, - }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ - [ANNOTATION_CHANGE_TYPE]: 2, - [BREAKING_CHANGE_TYPE]: 1, - }, ASYNCAPI_API_TYPE)) - }) - }) - describe('Cross-document deduplication', () => { test('should deduplicate diffs when same operation appears in multiple document pairs', async () => { // Same operation (operation1-message1) described in two documents: // before1.yaml/after1.yaml and before2.yaml/after2.yaml. diff --git a/test/projects/asyncapi-deduplication/add-message-no-sibling-impact/after.yaml b/test/projects/asyncapi-deduplication/add-message-no-sibling-impact/after.yaml deleted file mode 100644 index 1fbdc18a..00000000 --- a/test/projects/asyncapi-deduplication/add-message-no-sibling-impact/after.yaml +++ /dev/null @@ -1,43 +0,0 @@ -asyncapi: 3.0.0 -info: - title: Test AsyncAPI - version: 1.0.0 -channels: - channel1: - address: channel1 - messages: - message1: - $ref: '#/components/messages/message1' - message2: - $ref: '#/components/messages/message2' - message3: - $ref: '#/components/messages/message3' -operations: - operation1: - action: receive - channel: - $ref: '#/channels/channel1' - messages: - - $ref: '#/channels/channel1/messages/message1' - - $ref: '#/channels/channel1/messages/message2' - - $ref: '#/channels/channel1/messages/message3' -components: - messages: - message1: - payload: - type: object - properties: - userId: - type: string - message2: - payload: - type: object - properties: - orderId: - type: string - message3: - payload: - type: object - properties: - productId: - type: string diff --git a/test/projects/asyncapi-deduplication/add-message-no-sibling-impact/before.yaml b/test/projects/asyncapi-deduplication/add-message-no-sibling-impact/before.yaml deleted file mode 100644 index d06f58e4..00000000 --- a/test/projects/asyncapi-deduplication/add-message-no-sibling-impact/before.yaml +++ /dev/null @@ -1,42 +0,0 @@ -asyncapi: 3.0.0 -info: - title: Test AsyncAPI - version: 1.0.0 -channels: - channel1: - address: channel1 - messages: - message1: - $ref: '#/components/messages/message1' - message2: - $ref: '#/components/messages/message2' - message3: - $ref: '#/components/messages/message3' -operations: - operation1: - action: receive - channel: - $ref: '#/channels/channel1' - messages: - - $ref: '#/channels/channel1/messages/message1' - - $ref: '#/channels/channel1/messages/message2' -components: - messages: - message1: - payload: - type: object - properties: - userId: - type: string - message2: - payload: - type: object - properties: - orderId: - type: string - message3: - payload: - type: object - properties: - productId: - type: string diff --git a/test/projects/asyncapi-deduplication/mixed-operation-and-message-changes/after.yaml b/test/projects/asyncapi-deduplication/mixed-operation-and-message-changes/after.yaml deleted file mode 100644 index 20a039ba..00000000 --- a/test/projects/asyncapi-deduplication/mixed-operation-and-message-changes/after.yaml +++ /dev/null @@ -1,37 +0,0 @@ -asyncapi: 3.0.0 -info: - title: Test AsyncAPI - version: 1.0.0 -channels: - channel1: - address: channel1 - messages: - message1: - $ref: '#/components/messages/message1' - message2: - $ref: '#/components/messages/message2' -operations: - operation1: - description: updated description - action: receive - channel: - $ref: '#/channels/channel1' - messages: - - $ref: '#/channels/channel1/messages/message1' - - $ref: '#/channels/channel1/messages/message2' -components: - messages: - message1: - contentType: application/xml - payload: - type: object - properties: - userId: - type: string - message2: - contentType: application/json - payload: - type: object - properties: - orderId: - type: string diff --git a/test/projects/asyncapi-deduplication/mixed-operation-and-message-changes/before.yaml b/test/projects/asyncapi-deduplication/mixed-operation-and-message-changes/before.yaml deleted file mode 100644 index 7315b9fe..00000000 --- a/test/projects/asyncapi-deduplication/mixed-operation-and-message-changes/before.yaml +++ /dev/null @@ -1,37 +0,0 @@ -asyncapi: 3.0.0 -info: - title: Test AsyncAPI - version: 1.0.0 -channels: - channel1: - address: channel1 - messages: - message1: - $ref: '#/components/messages/message1' - message2: - $ref: '#/components/messages/message2' -operations: - operation1: - description: original description - action: receive - channel: - $ref: '#/channels/channel1' - messages: - - $ref: '#/channels/channel1/messages/message1' - - $ref: '#/channels/channel1/messages/message2' -components: - messages: - message1: - contentType: application/json - payload: - type: object - properties: - userId: - type: string - message2: - contentType: application/json - payload: - type: object - properties: - orderId: - type: string From e8afd831780dacf313837891da0fa0a88edce311 Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Wed, 25 Mar 2026 15:50:11 +0400 Subject: [PATCH 20/29] feat: Review --- src/strategies/build.strategy.ts | 28 +++++++++++++++++++++++++--- src/utils/document.ts | 22 +++++++++++++--------- test/asyncapi-changes.test.ts | 2 +- test/asyncapi-deduplication.test.ts | 15 +++++---------- 4 files changed, 44 insertions(+), 23 deletions(-) diff --git a/src/strategies/build.strategy.ts b/src/strategies/build.strategy.ts index 9ad8b0f0..715c6bdc 100644 --- a/src/strategies/build.strategy.ts +++ b/src/strategies/build.strategy.ts @@ -16,11 +16,32 @@ import { BuildConfig, BuilderStrategy, BuildResult, BuildTypeContexts, VersionCache } from '../types' import { compareVersions } from '../components/compare' -import { getOperationsList, setDocument } from '../utils' +import { DuplicateOperationHandler, getOperationsList, setDocument } from '../utils' import { buildFiles } from '../components/files' import { calculateHistoryForDeprecatedItems } from '../components/deprecated' import { asyncDebugPerformance, DebugPerformanceContext } from '../utils/logs' -import { REST_API_TYPE } from '../consts' +import { ASYNCAPI_API_TYPE, MESSAGE_SEVERITY, REST_API_TYPE } from '../consts' + +/** + * Handles duplicate operationIds found across different documents during build. + * - AsyncAPI: throws an error, since duplicate operationIds across documents are not allowed. + * - REST: adds a warning notification, since existing published specs may already have duplicates. + */ +const createDuplicateOperationHandler = (buildResult: BuildResult): DuplicateOperationHandler => (existing, duplicate) => { + if (duplicate.apiType === ASYNCAPI_API_TYPE) { + throw new Error( + `Duplicated operationId '${duplicate.operationId}' found in different documents: ` + + `'${existing.documentId}' and '${duplicate.documentId}'`, + ) + } + buildResult.notifications.push({ + severity: MESSAGE_SEVERITY.Warning, + message: `Duplicated operationId '${duplicate.operationId}' found in different documents: ` + + `'${existing.documentId}' and '${duplicate.documentId}'`, + operationId: duplicate.operationId, + fileId: duplicate.documentId, + }) +} export class BuildStrategy implements BuilderStrategy { async execute(config: BuildConfig, buildResult: BuildResult, contexts: BuildTypeContexts, debugContext: DebugPerformanceContext): Promise { @@ -48,8 +69,9 @@ export class BuildStrategy implements BuilderStrategy { if (files?.length) { const buildFilesResult = await buildFiles(files, builderContextObject, debugContext) + const handleDuplicateOperation = createDuplicateOperationHandler(buildResult) for (const { document, operations = [] } of buildFilesResult) { - setDocument(buildResult, document, operations) + setDocument(buildResult, document, operations, handleDuplicateOperation) } if (!builderContextObject.builderRunOptions.withoutDeprecatedDepth && previousVersionCache) { diff --git a/src/utils/document.ts b/src/utils/document.ts index d75bf02f..5513523d 100644 --- a/src/utils/document.ts +++ b/src/utils/document.ts @@ -100,18 +100,22 @@ export function toPackageDocument(document: VersionDocument): PackageDocument { } } -export function setDocument(buildResult: BuildResult, document: VersionDocument, operations: ApiOperation[] = []): void { +export type DuplicateOperationHandler = ( + existing: ApiOperation, + duplicate: ApiOperation, +) => void + +export function setDocument( + buildResult: BuildResult, + document: VersionDocument, + operations: ApiOperation[] = [], + handleDuplicateOperation?: DuplicateOperationHandler, +): void { buildResult.documents.set(document.fileId, document) for (const operation of operations) { const existingOperation = buildResult.operations.get(operation.operationId) - if (existingOperation) { - buildResult.notifications.push({ - severity: MESSAGE_SEVERITY.Error, - message: `Duplicated operationId '${operation.operationId}' found in different documents: ` + - `'${existingOperation.documentId}' and '${operation.documentId}'`, - operationId: operation.operationId, - fileId: operation.documentId, - }) + if (existingOperation && handleDuplicateOperation) { + handleDuplicateOperation(existingOperation, operation) } buildResult.operations.set(operation.operationId, operation) } diff --git a/test/asyncapi-changes.test.ts b/test/asyncapi-changes.test.ts index cb1ba891..0d66e6f3 100644 --- a/test/asyncapi-changes.test.ts +++ b/test/asyncapi-changes.test.ts @@ -238,7 +238,7 @@ describe('AsyncAPI 3.0 Changelog tests', () => { expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) - test('should report removed APIHUB operation when message reference is removed to async operation', async () => { + test('should report removed APIHUB operation when message reference is removed from async operation', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/message/remove-from-operation') expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) diff --git a/test/asyncapi-deduplication.test.ts b/test/asyncapi-deduplication.test.ts index f1dde96f..0f38e2d9 100644 --- a/test/asyncapi-deduplication.test.ts +++ b/test/asyncapi-deduplication.test.ts @@ -111,19 +111,14 @@ describe('AsyncAPI deduplication tests', () => { expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) }) - test('should deduplicate diffs when same operation appears in multiple document pairs', async () => { - // Same operation (operation1-message1) described in two documents: - // before1.yaml/after1.yaml and before2.yaml/after2.yaml. - // Both document pairs produce identical semantic changes (userId type: number → string). - // Level 2 dedup via calculateDiffId should ensure diffs are not counted twice. - const result = await buildChangelogPackageDefaultConfig( + test('should throw error when same operationId appears in multiple documents', async () => { + // Same operation (operation1-message1) described in two documents. + // AsyncAPI does not allow duplicate operationIds across documents — must throw. + await expect(buildChangelogPackageDefaultConfig( 'asyncapi-deduplication/cross-document-dedup', [{ fileId: 'before1.yaml', publish: true }, { fileId: 'before2.yaml', publish: true }], [{ fileId: 'after1.yaml' }, { fileId: 'after2.yaml' }], - ) - - expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + )).rejects.toThrow(/Duplicated operationId 'operation1-message1'/) }) }) }) From 12006f380efd089be21b897aa118f8a338448bfd Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Wed, 25 Mar 2026 15:58:11 +0400 Subject: [PATCH 21/29] feat: Fix duplicate operation rest severity --- src/strategies/build.strategy.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/strategies/build.strategy.ts b/src/strategies/build.strategy.ts index 715c6bdc..e4ca2a41 100644 --- a/src/strategies/build.strategy.ts +++ b/src/strategies/build.strategy.ts @@ -25,7 +25,7 @@ import { ASYNCAPI_API_TYPE, MESSAGE_SEVERITY, REST_API_TYPE } from '../consts' /** * Handles duplicate operationIds found across different documents during build. * - AsyncAPI: throws an error, since duplicate operationIds across documents are not allowed. - * - REST: adds a warning notification, since existing published specs may already have duplicates. + * - REST: adds an error notification (non-fatal), since existing published specs may already have duplicates. */ const createDuplicateOperationHandler = (buildResult: BuildResult): DuplicateOperationHandler => (existing, duplicate) => { if (duplicate.apiType === ASYNCAPI_API_TYPE) { @@ -35,7 +35,7 @@ const createDuplicateOperationHandler = (buildResult: BuildResult): DuplicateOpe ) } buildResult.notifications.push({ - severity: MESSAGE_SEVERITY.Warning, + severity: MESSAGE_SEVERITY.Error, message: `Duplicated operationId '${duplicate.operationId}' found in different documents: ` + `'${existing.documentId}' and '${duplicate.documentId}'`, operationId: duplicate.operationId, From 7a4cafab280ebe6318579140b3ffe54aa7c822e9 Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Wed, 25 Mar 2026 16:27:48 +0400 Subject: [PATCH 22/29] feat: added test data --- .../add-message-no-sibling-impact/after.yaml | 43 +++++++++++++++++++ .../add-message-no-sibling-impact/before.yaml | 42 ++++++++++++++++++ .../after.yaml | 37 ++++++++++++++++ .../before.yaml | 37 ++++++++++++++++ .../after1.yaml | 28 ++++++++++++ .../after2.yaml | 28 ++++++++++++ .../before1.yaml | 28 ++++++++++++ .../before2.yaml | 28 ++++++++++++ .../after1.yaml | 28 ++++++++++++ .../after2.yaml | 28 ++++++++++++ .../before1.yaml | 28 ++++++++++++ .../before2.yaml | 28 ++++++++++++ 12 files changed, 383 insertions(+) create mode 100644 test/projects/asyncapi-changes/message/add-message-no-sibling-impact/after.yaml create mode 100644 test/projects/asyncapi-changes/message/add-message-no-sibling-impact/before.yaml create mode 100644 test/projects/asyncapi-changes/message/mixed-operation-and-message-changes/after.yaml create mode 100644 test/projects/asyncapi-changes/message/mixed-operation-and-message-changes/before.yaml create mode 100644 test/projects/asyncapi-deduplication/shared-schema-cross-specs-different-content/after1.yaml create mode 100644 test/projects/asyncapi-deduplication/shared-schema-cross-specs-different-content/after2.yaml create mode 100644 test/projects/asyncapi-deduplication/shared-schema-cross-specs-different-content/before1.yaml create mode 100644 test/projects/asyncapi-deduplication/shared-schema-cross-specs-different-content/before2.yaml create mode 100644 test/projects/asyncapi-deduplication/shared-schema-cross-specs-same-scope/after1.yaml create mode 100644 test/projects/asyncapi-deduplication/shared-schema-cross-specs-same-scope/after2.yaml create mode 100644 test/projects/asyncapi-deduplication/shared-schema-cross-specs-same-scope/before1.yaml create mode 100644 test/projects/asyncapi-deduplication/shared-schema-cross-specs-same-scope/before2.yaml diff --git a/test/projects/asyncapi-changes/message/add-message-no-sibling-impact/after.yaml b/test/projects/asyncapi-changes/message/add-message-no-sibling-impact/after.yaml new file mode 100644 index 00000000..1fbdc18a --- /dev/null +++ b/test/projects/asyncapi-changes/message/add-message-no-sibling-impact/after.yaml @@ -0,0 +1,43 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' + message3: + $ref: '#/components/messages/message3' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + - $ref: '#/channels/channel1/messages/message2' + - $ref: '#/channels/channel1/messages/message3' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string + message3: + payload: + type: object + properties: + productId: + type: string diff --git a/test/projects/asyncapi-changes/message/add-message-no-sibling-impact/before.yaml b/test/projects/asyncapi-changes/message/add-message-no-sibling-impact/before.yaml new file mode 100644 index 00000000..d06f58e4 --- /dev/null +++ b/test/projects/asyncapi-changes/message/add-message-no-sibling-impact/before.yaml @@ -0,0 +1,42 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' + message3: + $ref: '#/components/messages/message3' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + - $ref: '#/channels/channel1/messages/message2' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string + message3: + payload: + type: object + properties: + productId: + type: string diff --git a/test/projects/asyncapi-changes/message/mixed-operation-and-message-changes/after.yaml b/test/projects/asyncapi-changes/message/mixed-operation-and-message-changes/after.yaml new file mode 100644 index 00000000..20a039ba --- /dev/null +++ b/test/projects/asyncapi-changes/message/mixed-operation-and-message-changes/after.yaml @@ -0,0 +1,37 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + description: updated description + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + - $ref: '#/channels/channel1/messages/message2' +components: + messages: + message1: + contentType: application/xml + payload: + type: object + properties: + userId: + type: string + message2: + contentType: application/json + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-changes/message/mixed-operation-and-message-changes/before.yaml b/test/projects/asyncapi-changes/message/mixed-operation-and-message-changes/before.yaml new file mode 100644 index 00000000..7315b9fe --- /dev/null +++ b/test/projects/asyncapi-changes/message/mixed-operation-and-message-changes/before.yaml @@ -0,0 +1,37 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + description: original description + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + - $ref: '#/channels/channel1/messages/message2' +components: + messages: + message1: + contentType: application/json + payload: + type: object + properties: + userId: + type: string + message2: + contentType: application/json + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-deduplication/shared-schema-cross-specs-different-content/after1.yaml b/test/projects/asyncapi-deduplication/shared-schema-cross-specs-different-content/after1.yaml new file mode 100644 index 00000000..bc7cc96c --- /dev/null +++ b/test/projects/asyncapi-deduplication/shared-schema-cross-specs-different-content/after1.yaml @@ -0,0 +1,28 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI Doc1 + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + schemas: + SharedPayload: + type: object + properties: + userId: + type: string + messages: + message1: + payload: + $ref: '#/components/schemas/SharedPayload' diff --git a/test/projects/asyncapi-deduplication/shared-schema-cross-specs-different-content/after2.yaml b/test/projects/asyncapi-deduplication/shared-schema-cross-specs-different-content/after2.yaml new file mode 100644 index 00000000..2e81e1c5 --- /dev/null +++ b/test/projects/asyncapi-deduplication/shared-schema-cross-specs-different-content/after2.yaml @@ -0,0 +1,28 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI Doc2 + version: 1.0.0 +channels: + channel2: + address: channel2 + messages: + message2: + $ref: '#/components/messages/message2' +operations: + operation2: + action: receive + channel: + $ref: '#/channels/channel2' + messages: + - $ref: '#/channels/channel2/messages/message2' +components: + schemas: + SharedPayload: + type: object + properties: + orderId: + type: string + messages: + message2: + payload: + $ref: '#/components/schemas/SharedPayload' diff --git a/test/projects/asyncapi-deduplication/shared-schema-cross-specs-different-content/before1.yaml b/test/projects/asyncapi-deduplication/shared-schema-cross-specs-different-content/before1.yaml new file mode 100644 index 00000000..fce21230 --- /dev/null +++ b/test/projects/asyncapi-deduplication/shared-schema-cross-specs-different-content/before1.yaml @@ -0,0 +1,28 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI Doc1 + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + schemas: + SharedPayload: + type: object + properties: + userId: + type: number + messages: + message1: + payload: + $ref: '#/components/schemas/SharedPayload' diff --git a/test/projects/asyncapi-deduplication/shared-schema-cross-specs-different-content/before2.yaml b/test/projects/asyncapi-deduplication/shared-schema-cross-specs-different-content/before2.yaml new file mode 100644 index 00000000..92e08261 --- /dev/null +++ b/test/projects/asyncapi-deduplication/shared-schema-cross-specs-different-content/before2.yaml @@ -0,0 +1,28 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI Doc2 + version: 1.0.0 +channels: + channel2: + address: channel2 + messages: + message2: + $ref: '#/components/messages/message2' +operations: + operation2: + action: receive + channel: + $ref: '#/channels/channel2' + messages: + - $ref: '#/channels/channel2/messages/message2' +components: + schemas: + SharedPayload: + type: object + properties: + orderId: + type: integer + messages: + message2: + payload: + $ref: '#/components/schemas/SharedPayload' diff --git a/test/projects/asyncapi-deduplication/shared-schema-cross-specs-same-scope/after1.yaml b/test/projects/asyncapi-deduplication/shared-schema-cross-specs-same-scope/after1.yaml new file mode 100644 index 00000000..bc7cc96c --- /dev/null +++ b/test/projects/asyncapi-deduplication/shared-schema-cross-specs-same-scope/after1.yaml @@ -0,0 +1,28 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI Doc1 + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + schemas: + SharedPayload: + type: object + properties: + userId: + type: string + messages: + message1: + payload: + $ref: '#/components/schemas/SharedPayload' diff --git a/test/projects/asyncapi-deduplication/shared-schema-cross-specs-same-scope/after2.yaml b/test/projects/asyncapi-deduplication/shared-schema-cross-specs-same-scope/after2.yaml new file mode 100644 index 00000000..c8698486 --- /dev/null +++ b/test/projects/asyncapi-deduplication/shared-schema-cross-specs-same-scope/after2.yaml @@ -0,0 +1,28 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI Doc2 + version: 1.0.0 +channels: + channel2: + address: channel2 + messages: + message2: + $ref: '#/components/messages/message2' +operations: + operation2: + action: receive + channel: + $ref: '#/channels/channel2' + messages: + - $ref: '#/channels/channel2/messages/message2' +components: + schemas: + SharedPayload: + type: object + properties: + userId: + type: string + messages: + message2: + payload: + $ref: '#/components/schemas/SharedPayload' diff --git a/test/projects/asyncapi-deduplication/shared-schema-cross-specs-same-scope/before1.yaml b/test/projects/asyncapi-deduplication/shared-schema-cross-specs-same-scope/before1.yaml new file mode 100644 index 00000000..fce21230 --- /dev/null +++ b/test/projects/asyncapi-deduplication/shared-schema-cross-specs-same-scope/before1.yaml @@ -0,0 +1,28 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI Doc1 + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + schemas: + SharedPayload: + type: object + properties: + userId: + type: number + messages: + message1: + payload: + $ref: '#/components/schemas/SharedPayload' diff --git a/test/projects/asyncapi-deduplication/shared-schema-cross-specs-same-scope/before2.yaml b/test/projects/asyncapi-deduplication/shared-schema-cross-specs-same-scope/before2.yaml new file mode 100644 index 00000000..d25efdf0 --- /dev/null +++ b/test/projects/asyncapi-deduplication/shared-schema-cross-specs-same-scope/before2.yaml @@ -0,0 +1,28 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI Doc2 + version: 1.0.0 +channels: + channel2: + address: channel2 + messages: + message2: + $ref: '#/components/messages/message2' +operations: + operation2: + action: receive + channel: + $ref: '#/channels/channel2' + messages: + - $ref: '#/channels/channel2/messages/message2' +components: + schemas: + SharedPayload: + type: object + properties: + userId: + type: number + messages: + message2: + payload: + $ref: '#/components/schemas/SharedPayload' From 67c31b0167ac2dfa4935aade40106402792d7324 Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Wed, 25 Mar 2026 17:18:49 +0400 Subject: [PATCH 23/29] feat: Fixed async defaultContentType --- src/apitypes/async/async.changes.ts | 83 ++++--------------- src/apitypes/async/async.utils.ts | 79 ++++++++++++++++-- test/asyncapi-changes.test.ts | 19 ++++- .../after.yaml | 44 ++++++++++ .../before.yaml | 44 ++++++++++ 5 files changed, 194 insertions(+), 75 deletions(-) create mode 100644 test/projects/asyncapi-changes/info/change-default-content-type-with-explicit-override/after.yaml create mode 100644 test/projects/asyncapi-changes/info/change-default-content-type-with-explicit-override/before.yaml diff --git a/src/apitypes/async/async.changes.ts b/src/apitypes/async/async.changes.ts index 8adcc6b6..9df4c48f 100644 --- a/src/apitypes/async/async.changes.ts +++ b/src/apitypes/async/async.changes.ts @@ -49,11 +49,14 @@ import { import { createAsyncApiCompatibilityScopeFunction } from '../../components/compare/bwc.validation.async' import { v3 as AsyncAPIV3 } from '@asyncapi/parser/esm/spec-types' import { + collectChannelMessageDefinitionDiffs, + collectExclusiveOtherMessageDiffs, extractAsyncApiVersionDiff, extractDefaultContentTypeDiff, extractIdDiff, extractInfoDiffs, getAsyncMessageId, + hasExplicitContentType, } from './async.utils' export const compareDocuments: DocumentsCompare = async ( @@ -111,66 +114,11 @@ export const compareDocuments: DocumentsCompare = async ( const tags = new Set() const operationChanges: OperationChanges[] = [] - /** - * Aggregated diffs on the operation level include diffs from ALL messages. - * Since each apihub operation maps to a specific operation + message pair, - * diffs from sibling messages must be excluded to prevent them from leaking - * into unrelated apihub operations. - * - * Collects two kinds of diffs that belong exclusively to other messages: - * 1. Aggregated content diffs from each sibling message object - * 2. Array-level diffs for adding/removing sibling messages from the messages list - * - * Diffs shared between the current message and sibling messages (e.g. from a shared - * component schema) are NOT included, so they won't be incorrectly filtered out. - */ - function collectExclusiveOtherMessageDiffs(messages: AsyncAPIV3.MessageObject[], currentMessageIndex: number): Set { - const currentMessageDiffsArr = (messages[currentMessageIndex] as WithAggregatedDiffs)[DIFFS_AGGREGATED_META_KEY] - const currentMessageDiffs = new Set(currentMessageDiffsArr ?? []) - - const otherDiffs = new Set() - for (const [messageIndex, message] of messages.entries()) { - if (messageIndex === currentMessageIndex) continue - const messageDiffs = (message as WithAggregatedDiffs)[DIFFS_AGGREGATED_META_KEY] - if (messageDiffs) { - for (const messageDiff of messageDiffs) { - if (!currentMessageDiffs.has(messageDiff)) { - otherDiffs.add(messageDiff) - } - } - } - } - const messagesArrayMeta = (messages as WithDiffMetaRecord)[DIFF_META_KEY] - if (messagesArrayMeta) { - for (const key in messagesArrayMeta) { - if (Number(key) !== currentMessageIndex) { - otherDiffs.add(messagesArrayMeta[key]) - } - } - } - return otherDiffs - } - - /** - * Collects diffs for adding/removing message definitions in channel.messages. - * These are channel-level definition changes that should not propagate to operations, - * because what matters is whether the operation's own messages array references a message, - * not whether the channel defines it. - */ - function collectChannelMessageDefinitionDiffs(operationChannel: AsyncAPIV3.ChannelObject): Set { - const channelMessages = (operationChannel as Record).messages - if (!channelMessages || !isObject(channelMessages)) { - return new Set() - } - const diffs = new Set() - const messagesMeta = (channelMessages as WithDiffMetaRecord)[DIFF_META_KEY] - if (messagesMeta) { - for (const key in messagesMeta) { - diffs.add(messagesMeta[key]) - } - } - return diffs - } + // Precompute root-level diffs once (shared across all operations) + const asyncApiVersionDiffs = extractAsyncApiVersionDiff(merged) + const infoDiffs = extractInfoDiffs(merged) + const idDiffs = extractIdDiff(merged) + const defaultContentTypeDiffs = extractDefaultContentTypeDiff(merged) // Iterate through operations in merged document const { operations: asyncOperations } = merged @@ -211,12 +159,17 @@ export const compareDocuments: DocumentsCompare = async ( const otherMessageDiffs = collectExclusiveOtherMessageDiffs(messages, messageIndex) const channelMessageDiffs = collectChannelMessageDefinitionDiffs(operationChannel as AsyncAPIV3.ChannelObject) + // defaultContentType only affects messages without explicit contentType + const messageAffectedByDefaultContentType = + !hasExplicitContentType(prevDocData, messageId) || + !hasExplicitContentType(currDocData, messageId) + operationDiffs = [ ...([...allOperationDiffs].filter(diff => !otherMessageDiffs.has(diff) && !channelMessageDiffs.has(diff))), - ...extractAsyncApiVersionDiff(merged), - ...extractInfoDiffs(merged), - ...extractIdDiff(merged), - ...extractDefaultContentTypeDiff(merged), + ...asyncApiVersionDiffs, + ...infoDiffs, + ...idDiffs, + ...(messageAffectedByDefaultContentType ? defaultContentTypeDiffs : []), ] } if (operationAddedOrRemoved) { @@ -250,7 +203,7 @@ export const compareDocuments: DocumentsCompare = async ( return { operationChanges, tags, - ...(comparisonDocument) ? { comparisonDocument } : {}, + ...(comparisonDocument ? { comparisonDocument } : {}), } } diff --git a/src/apitypes/async/async.utils.ts b/src/apitypes/async/async.utils.ts index 5acd1336..ae64bfa5 100644 --- a/src/apitypes/async/async.utils.ts +++ b/src/apitypes/async/async.utils.ts @@ -271,6 +271,76 @@ export const resolveAsyncApiOperationIdsFromRefs = ( return resolved } +/** + * Checks whether a message has explicit `contentType` in the raw (pre-normalization) document. + * Messages without explicit contentType inherit from `defaultContentType`. + */ +export function hasExplicitContentType(doc: AsyncAPIV3.AsyncAPIObject | undefined | null, messageId: string): boolean { + const message = getKeyValue(doc, 'components', 'messages', messageId) + return isObject(message) && !isReferenceObject(message) && 'contentType' in message +} + +/** + * Aggregated diffs on the operation level include diffs from ALL messages. + * Since each apihub operation maps to a specific operation + message pair, + * diffs from sibling messages must be excluded to prevent them from leaking + * into unrelated apihub operations. + * + * Collects two kinds of diffs that belong exclusively to other messages: + * 1. Aggregated content diffs from each sibling message object + * 2. Array-level diffs for adding/removing sibling messages from the messages list + * + * Diffs shared between the current message and sibling messages (e.g. from a shared + * component schema) are NOT included, so they won't be incorrectly filtered out. + */ +export function collectExclusiveOtherMessageDiffs(messages: AsyncAPIV3.MessageObject[], currentMessageIndex: number): Set { + const currentMessageDiffsArr = (messages[currentMessageIndex] as WithAggregatedDiffs)[DIFFS_AGGREGATED_META_KEY] + const currentMessageDiffs = new Set(currentMessageDiffsArr ?? []) + + const otherDiffs = new Set() + for (const [messageIndex, message] of messages.entries()) { + if (messageIndex === currentMessageIndex) continue + const messageDiffs = (message as WithAggregatedDiffs)[DIFFS_AGGREGATED_META_KEY] + if (messageDiffs) { + for (const messageDiff of messageDiffs) { + if (!currentMessageDiffs.has(messageDiff)) { + otherDiffs.add(messageDiff) + } + } + } + } + const messagesArrayMeta = (messages as WithDiffMetaRecord)[DIFF_META_KEY] + if (messagesArrayMeta) { + for (const key in messagesArrayMeta) { + if (Number(key) !== currentMessageIndex) { + otherDiffs.add(messagesArrayMeta[key]) + } + } + } + return otherDiffs +} + +/** + * Collects diffs for adding/removing message definitions in channel.messages. + * These are channel-level definition changes that should not propagate to operations, + * because what matters is whether the operation's own messages array references a message, + * not whether the channel defines it. + */ +export function collectChannelMessageDefinitionDiffs(operationChannel: AsyncAPIV3.ChannelObject): Set { + const channelMessages = (operationChannel as Record).messages + if (!isObject(channelMessages)) { + return new Set() + } + const diffs = new Set() + const messagesMeta = (channelMessages as WithDiffMetaRecord)[DIFF_META_KEY] + if (messagesMeta) { + for (const key in messagesMeta) { + diffs.add(messagesMeta[key]) + } + } + return diffs +} + export function extractAsyncApiVersionDiff(doc: AsyncAPIV3.AsyncAPIObject): Diff[] { const diff = (doc as WithDiffMetaRecord)[DIFF_META_KEY]?.asyncapi return diff ? [diff] : [] @@ -294,12 +364,3 @@ export function extractDefaultContentTypeDiff(doc: AsyncAPIV3.AsyncAPIObject): D const diff = (doc as WithDiffMetaRecord)[DIFF_META_KEY]?.defaultContentType return diff ? [diff] : [] } - -export function extractRootServersDiffs(doc: AsyncAPIV3.AsyncAPIObject): Diff[] { - const addOrRemoveServersDiff = (doc as WithDiffMetaRecord)[DIFF_META_KEY]?.servers - const serversInternalDiffs = (doc.servers as WithAggregatedDiffs | undefined)?.[DIFFS_AGGREGATED_META_KEY] ?? [] - return [ - ...(addOrRemoveServersDiff ? [addOrRemoveServersDiff] : []), - ...serversInternalDiffs, - ] -} diff --git a/test/asyncapi-changes.test.ts b/test/asyncapi-changes.test.ts index 0d66e6f3..1f7a228c 100644 --- a/test/asyncapi-changes.test.ts +++ b/test/asyncapi-changes.test.ts @@ -414,10 +414,27 @@ describe('AsyncAPI 3.0 Changelog tests', () => { test('should report changed defaultContentType', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/info/change-default-content-type') - // Root-level defaultContentType change + propagated breaking change in message contentType + // Root-level defaultContentType change + propagated breaking change in message contentType. + // message1 has no explicit contentType → affected by defaultContentType change. expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1, [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1, [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) }) + + test('should not report defaultContentType change for message with explicit contentType', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/info/change-default-content-type-with-explicit-override') + + // defaultContentType changed (json → xml). + // message1 has explicit contentType → not affected, no changes reported. + // message2 has no explicit contentType → affected by defaultContentType change. + // Only operation2-message2 should be impacted. + expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1, [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ + operationId: 'operation2-message2', + previousOperationId: 'operation2-message2', + }), + ])) + }) }) describe('Tags tests', () => { diff --git a/test/projects/asyncapi-changes/info/change-default-content-type-with-explicit-override/after.yaml b/test/projects/asyncapi-changes/info/change-default-content-type-with-explicit-override/after.yaml new file mode 100644 index 00000000..16f62a5d --- /dev/null +++ b/test/projects/asyncapi-changes/info/change-default-content-type-with-explicit-override/after.yaml @@ -0,0 +1,44 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +defaultContentType: application/xml +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + channel2: + address: channel2 + messages: + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + operation2: + action: receive + channel: + $ref: '#/channels/channel2' + messages: + - $ref: '#/channels/channel2/messages/message2' +components: + messages: + message1: + contentType: application/json + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-changes/info/change-default-content-type-with-explicit-override/before.yaml b/test/projects/asyncapi-changes/info/change-default-content-type-with-explicit-override/before.yaml new file mode 100644 index 00000000..65b0785a --- /dev/null +++ b/test/projects/asyncapi-changes/info/change-default-content-type-with-explicit-override/before.yaml @@ -0,0 +1,44 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +defaultContentType: application/json +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + channel2: + address: channel2 + messages: + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + operation2: + action: receive + channel: + $ref: '#/channels/channel2' + messages: + - $ref: '#/channels/channel2/messages/message2' +components: + messages: + message1: + contentType: application/json + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string From e6f72f0f5a99634f621171f588028173101beb5c Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Thu, 26 Mar 2026 12:59:03 +0400 Subject: [PATCH 24/29] feat: Fixed async apikind --- src/apitypes/async/async.changes.ts | 2 +- src/apitypes/rest/rest.changes.ts | 2 +- ...ation.async.ts => async.bwc.validation.ts} | 16 +- ...idation.rest.ts => rest.bwc.validation.ts} | 0 test/apiKinds.test.ts | 4 +- test/asyncapi-apikind.test.ts | 149 ++++++++++-------- 6 files changed, 94 insertions(+), 79 deletions(-) rename src/components/compare/{bwc.validation.async.ts => async.bwc.validation.ts} (91%) rename src/components/compare/{bwc.validation.rest.ts => rest.bwc.validation.ts} (100%) diff --git a/src/apitypes/async/async.changes.ts b/src/apitypes/async/async.changes.ts index 9df4c48f..5a44c21d 100644 --- a/src/apitypes/async/async.changes.ts +++ b/src/apitypes/async/async.changes.ts @@ -46,7 +46,7 @@ import { getOperationTags, OperationsMap, } from '../../components' -import { createAsyncApiCompatibilityScopeFunction } from '../../components/compare/bwc.validation.async' +import { createAsyncApiCompatibilityScopeFunction } from '../../components/compare/async.bwc.validation' import { v3 as AsyncAPIV3 } from '@asyncapi/parser/esm/spec-types' import { collectChannelMessageDefinitionDiffs, diff --git a/src/apitypes/rest/rest.changes.ts b/src/apitypes/rest/rest.changes.ts index 577cbb37..a0ac311a 100644 --- a/src/apitypes/rest/rest.changes.ts +++ b/src/apitypes/rest/rest.changes.ts @@ -81,7 +81,7 @@ import { getOperationTags, OperationsMap, } from '../../components' -import { createRestApiCompatibilityScopeFunction } from '../../components/compare/bwc.validation.rest' +import { createRestApiCompatibilityScopeFunction } from '../../components/compare/rest.bwc.validation' import { calculateApiKindFromLabels, getApiKindProperty } from '../../components/document' export const compareDocuments: DocumentsCompare = async ( diff --git a/src/components/compare/bwc.validation.async.ts b/src/components/compare/async.bwc.validation.ts similarity index 91% rename from src/components/compare/bwc.validation.async.ts rename to src/components/compare/async.bwc.validation.ts index ac27c6ae..b7c90c05 100644 --- a/src/components/compare/bwc.validation.async.ts +++ b/src/components/compare/async.bwc.validation.ts @@ -14,10 +14,8 @@ * limitations under the License. */ -import { - APIHUB_API_COMPATIBILITY_KIND_BWC, - APIHUB_API_COMPATIBILITY_KIND_NO_BWC, -} from '../../consts' +import { APIHUB_API_COMPATIBILITY_KIND_BWC, APIHUB_API_COMPATIBILITY_KIND_NO_BWC } from '../../consts' +import { isObject } from '../../utils' import { JsonPath } from '@netcracker/qubership-apihub-json-crawl' import { API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE, @@ -67,6 +65,12 @@ export const createAsyncApiCompatibilityScopeFunction: ApiCompatibilityScopeFunc } const firstSegment = path?.[0] + const beforeExists = isObject(beforeJso) + const afterExists = isObject(afterJso) + + if (!beforeExists && !afterExists) { + return undefined + } // operations/: resolve api-kind from operation x-api-kind with channel fallback if (firstSegment === 'operations' && pathLength === ASYNC_OPERATION_PATH_LENGTH) { @@ -82,7 +86,7 @@ export const createAsyncApiCompatibilityScopeFunction: ApiCompatibilityScopeFunc return API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE } - return undefined + return API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE } // channels/: use channel's own x-api-kind @@ -94,7 +98,7 @@ export const createAsyncApiCompatibilityScopeFunction: ApiCompatibilityScopeFunc return API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE } - return undefined + return API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE } return undefined diff --git a/src/components/compare/bwc.validation.rest.ts b/src/components/compare/rest.bwc.validation.ts similarity index 100% rename from src/components/compare/bwc.validation.rest.ts rename to src/components/compare/rest.bwc.validation.ts diff --git a/test/apiKinds.test.ts b/test/apiKinds.test.ts index 4caebfe6..47ca97bf 100644 --- a/test/apiKinds.test.ts +++ b/test/apiKinds.test.ts @@ -29,8 +29,8 @@ import { import { jest } from '@jest/globals' import { changesSummaryMatcher, Editor, LocalRegistry, serializedComparisonDocumentMatcher } from './helpers' import { takeIfDefined } from '../src/utils' -import * as bwcValidation from '../src/components/compare/bwc.validation.rest' -import { calculateOperationApiCompatibilityKind } from '../src/components/compare/bwc.validation.rest' +import * as bwcValidation from '../src/components/compare/rest.bwc.validation' +import { calculateOperationApiCompatibilityKind } from '../src/components/compare/rest.bwc.validation' let afterPackage: LocalRegistry const AFTER_PACKAGE_ID = 'api-kinds' diff --git a/test/asyncapi-apikind.test.ts b/test/asyncapi-apikind.test.ts index 9526cb98..d82555c8 100644 --- a/test/asyncapi-apikind.test.ts +++ b/test/asyncapi-apikind.test.ts @@ -7,11 +7,12 @@ import { } from '../src' import { calculateAsyncApiKind } from '../src/apitypes/async/async.utils' import { buildPackageWithDefaultConfig } from './helpers' -import { createAsyncApiCompatibilityScopeFunction } from '../src/components/compare/bwc.validation.async' +import { createAsyncApiCompatibilityScopeFunction } from '../src/components/compare/async.bwc.validation' import { API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE, API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE, } from '@netcracker/qubership-apihub-api-diff' +import { v3 as AsyncAPIV3 } from '@asyncapi/parser/esm/spec-types' describe('AsyncAPI apiKind calculation', () => { describe('Unit tests', () => { @@ -33,33 +34,22 @@ describe('AsyncAPI apiKind calculation', () => { }) describe('Changelog backward compatibility scope function', () => { - const createChannel = (apiKind: string | undefined): { - address: string - [API_KIND_SPECIFICATION_EXTENSION]?: ApihubApiCompatibilityKind - } => { - return apiKind - ? { address: 'channel1', [API_KIND_SPECIFICATION_EXTENSION]: apiKind as ApihubApiCompatibilityKind } - : { address: 'channel1' } - } - - const createOperation = (apiKind: string | undefined): { - action: string - channel: unknown - [API_KIND_SPECIFICATION_EXTENSION]?: ApihubApiCompatibilityKind - } => { - return apiKind - ? { - action: 'receive', - [API_KIND_SPECIFICATION_EXTENSION]: apiKind as ApihubApiCompatibilityKind, - channel: {}, - } - : { action: 'receive', channel: {} } - } - // short names const BWC = API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE const NOT_BWC = API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE + // Factories return a fresh object each time so that before/after are distinct instances. + // Currently the scope function only reads properties and doesn't compare by reference, + // but unique instances prevent false positives if the implementation ever starts + // distinguishing "same object" from "equal objects" (e.g. identity checks or mutation). + const channel = (): AsyncAPIV3.ChannelObject => ({ address: 'channel1' }) + const bwcChannel = (): AsyncAPIV3.ChannelObject => ({ ...channel(), [API_KIND_SPECIFICATION_EXTENSION]: APIHUB_API_COMPATIBILITY_KIND_BWC }) + const noBwcChannel = (): AsyncAPIV3.ChannelObject => ({ ...channel(), [API_KIND_SPECIFICATION_EXTENSION]: APIHUB_API_COMPATIBILITY_KIND_NO_BWC }) + + const operation = (): AsyncAPIV3.OperationObject => ({ action: 'receive', channel: {} }) + const bwcOperation = (): AsyncAPIV3.OperationObject => ({ ...operation(), [API_KIND_SPECIFICATION_EXTENSION]: APIHUB_API_COMPATIBILITY_KIND_BWC }) + const noBwcOperation = (): AsyncAPIV3.OperationObject => ({ ...operation(), [API_KIND_SPECIFICATION_EXTENSION]: APIHUB_API_COMPATIBILITY_KIND_NO_BWC }) + describe('Root level', () => { it.each([ // prev | curr | expected @@ -78,65 +68,86 @@ describe('AsyncAPI apiKind calculation', () => { describe('Channels scope', () => { it.each([ - // default | before | after | expected - ['bwc', 'undefined', 'undefined', undefined], - ['bwc', 'undefined', 'bwc', undefined], - ['bwc', 'undefined', 'no-bwc', NOT_BWC], - ['bwc', 'bwc', 'undefined', undefined], - ['bwc', 'bwc', 'bwc', undefined], - ['bwc', 'bwc', 'no-bwc', NOT_BWC], - ['bwc', 'no-bwc', 'undefined', NOT_BWC], - ['bwc', 'no-bwc', 'bwc', NOT_BWC], - ['bwc', 'no-bwc', 'no-bwc', NOT_BWC], - ['no-bwc', 'undefined', 'undefined', undefined], - ['no-bwc', 'undefined', 'bwc', undefined], - ['no-bwc', 'undefined', 'no-bwc', NOT_BWC], - ['no-bwc', 'bwc', 'undefined', undefined], - ['no-bwc', 'bwc', 'bwc', undefined], - ['no-bwc', 'bwc', 'no-bwc', NOT_BWC], - ['no-bwc', 'no-bwc', 'undefined', NOT_BWC], - ['no-bwc', 'no-bwc', 'bwc', NOT_BWC], - ['no-bwc', 'no-bwc', 'no-bwc', NOT_BWC], + // default | before | after | expected + // undefined JSO = channel added/removed + ['bwc', channel(), channel(), BWC], + ['bwc', channel(), bwcChannel(), BWC], + ['bwc', channel(), noBwcChannel(), NOT_BWC], + ['bwc', bwcChannel(), channel(), BWC], + ['bwc', bwcChannel(), bwcChannel(), BWC], + ['bwc', bwcChannel(), noBwcChannel(), NOT_BWC], + ['bwc', noBwcChannel(), channel(), NOT_BWC], + ['bwc', noBwcChannel(), bwcChannel(), NOT_BWC], + ['bwc', noBwcChannel(), noBwcChannel(), NOT_BWC], + ['bwc', undefined, channel(), BWC], + ['bwc', undefined, noBwcChannel(), NOT_BWC], + ['bwc', channel(), undefined, BWC], + ['bwc', noBwcChannel(), undefined, NOT_BWC], + ['bwc', undefined, undefined, undefined], + ['no-bwc', channel(), channel(), BWC], + ['no-bwc', channel(), bwcChannel(), BWC], + ['no-bwc', channel(), noBwcChannel(), NOT_BWC], + ['no-bwc', bwcChannel(), channel(), BWC], + ['no-bwc', bwcChannel(), bwcChannel(), BWC], + ['no-bwc', bwcChannel(), noBwcChannel(), NOT_BWC], + ['no-bwc', noBwcChannel(), channel(), NOT_BWC], + ['no-bwc', noBwcChannel(), bwcChannel(), NOT_BWC], + ['no-bwc', noBwcChannel(), noBwcChannel(), NOT_BWC], + ['no-bwc', undefined, channel(), BWC], + ['no-bwc', undefined, noBwcChannel(), NOT_BWC], + ['no-bwc', channel(), undefined, BWC], + ['no-bwc', noBwcChannel(), undefined, NOT_BWC], + ['no-bwc', undefined, undefined, undefined], ] as const)('documentApiKind: %s, before: %s, after: %s', ( documentApiKind, - beforeKind, - afterKind, + beforeJso, + afterJso, expected, ) => { const scopeFunction = createAsyncApiCompatibilityScopeFunction(documentApiKind) - expect(scopeFunction(['channels', 'ch1'], createChannel(beforeKind), createChannel(afterKind))).toBe(expected) + expect(scopeFunction(['channels', 'ch1'], beforeJso, afterJso)).toBe(expected) }) }) describe('Operations scope', () => { it.each([ - // default | before | after | expected - ['bwc', 'undefined', 'undefined', undefined], - ['bwc', 'undefined', 'bwc', undefined], - ['bwc', 'undefined', 'no-bwc', NOT_BWC], - ['bwc', 'bwc', 'undefined', undefined], - ['bwc', 'bwc', 'bwc', undefined], - ['bwc', 'bwc', 'no-bwc', NOT_BWC], - ['bwc', 'no-bwc', 'undefined', NOT_BWC], - ['bwc', 'no-bwc', 'bwc', NOT_BWC], - ['bwc', 'no-bwc', 'no-bwc', NOT_BWC], - ['no-bwc', 'undefined', 'undefined', undefined], - ['no-bwc', 'undefined', 'bwc', undefined], - ['no-bwc', 'undefined', 'no-bwc', NOT_BWC], - ['no-bwc', 'bwc', 'undefined', undefined], - ['no-bwc', 'bwc', 'bwc', undefined], - ['no-bwc', 'bwc', 'no-bwc', NOT_BWC], - ['no-bwc', 'no-bwc', 'undefined', NOT_BWC], - ['no-bwc', 'no-bwc', 'bwc', NOT_BWC], - ['no-bwc', 'no-bwc', 'no-bwc', NOT_BWC], + // default | before | after | expected + ['bwc', operation(), operation(), BWC], + ['bwc', operation(), bwcOperation(), BWC], + ['bwc', operation(), noBwcOperation(), NOT_BWC], + ['bwc', bwcOperation(), operation(), BWC], + ['bwc', bwcOperation(), bwcOperation(), BWC], + ['bwc', bwcOperation(), noBwcOperation(), NOT_BWC], + ['bwc', noBwcOperation(), operation(), NOT_BWC], + ['bwc', noBwcOperation(), bwcOperation(), NOT_BWC], + ['bwc', noBwcOperation(), noBwcOperation(), NOT_BWC], + ['bwc', undefined, operation(), BWC], + ['bwc', undefined, noBwcOperation(), NOT_BWC], + ['bwc', operation(), undefined, BWC], + ['bwc', noBwcOperation(), undefined, NOT_BWC], + ['bwc', undefined, undefined, undefined], + ['no-bwc', operation(), operation(), BWC], + ['no-bwc', operation(), bwcOperation(), BWC], + ['no-bwc', operation(), noBwcOperation(), NOT_BWC], + ['no-bwc', bwcOperation(), operation(), BWC], + ['no-bwc', bwcOperation(), bwcOperation(), BWC], + ['no-bwc', bwcOperation(), noBwcOperation(), NOT_BWC], + ['no-bwc', noBwcOperation(), operation(), NOT_BWC], + ['no-bwc', noBwcOperation(), bwcOperation(), NOT_BWC], + ['no-bwc', noBwcOperation(), noBwcOperation(), NOT_BWC], + ['no-bwc', undefined, operation(), BWC], + ['no-bwc', undefined, noBwcOperation(), NOT_BWC], + ['no-bwc', operation(), undefined, BWC], + ['no-bwc', noBwcOperation(), undefined, NOT_BWC], + ['no-bwc', undefined, undefined, undefined], ] as const)('documentApiKind: %s, before: %s, after: %s', ( documentApiKind, - beforeKind, - afterKind, + beforeJso, + afterJso, expected, ) => { const scopeFunction = createAsyncApiCompatibilityScopeFunction(documentApiKind) - expect(scopeFunction(['operations', 'op1'], createOperation(beforeKind), createOperation(afterKind))).toBe(expected) + expect(scopeFunction(['operations', 'op1'], beforeJso, afterJso)).toBe(expected) }) it('should use channel x-api-kind as fallback when operation has no x-api-kind', () => { @@ -161,7 +172,7 @@ describe('AsyncAPI apiKind calculation', () => { [API_KIND_SPECIFICATION_EXTENSION]: APIHUB_API_COMPATIBILITY_KIND_BWC, channel: { [API_KIND_SPECIFICATION_EXTENSION]: APIHUB_API_COMPATIBILITY_KIND_NO_BWC }, } - expect(scopeFunction(['operations', 'op1'], before, after)).toBeUndefined() + expect(scopeFunction(['operations', 'op1'], before, after)).toBe(BWC) }) }) From bc6c7eb13f48400cf30e652d735089c77ecb15c8 Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Thu, 26 Mar 2026 16:22:43 +0400 Subject: [PATCH 25/29] feat: Fixed tests. Added duplicate operationId validation tests --- test/asyncapi-changes.test.ts | 176 +++++++++++++++--- test/asyncapi-deduplication.test.ts | 11 +- test/asyncapi-operation.test.ts | 22 ++- .../duplicate-cross-document/spec1.yaml | 25 +++ .../duplicate-cross-document/spec2.yaml | 25 +++ 5 files changed, 218 insertions(+), 41 deletions(-) create mode 100644 test/projects/asyncapi-changes/operation/duplicate-cross-document/spec1.yaml create mode 100644 test/projects/asyncapi-changes/operation/duplicate-cross-document/spec2.yaml diff --git a/test/asyncapi-changes.test.ts b/test/asyncapi-changes.test.ts index 1f7a228c..78fc1d29 100644 --- a/test/asyncapi-changes.test.ts +++ b/test/asyncapi-changes.test.ts @@ -37,19 +37,27 @@ describe('AsyncAPI 3.0 Changelog tests', () => { test('should report added operation', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/add') expect(result).toEqual(changesSummaryMatcher({ [NON_BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [NON_BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ operationId: 'operation2-message1' }), + ])) }) test('should report multiple added operations', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/add-multiple') expect(result).toEqual(changesSummaryMatcher({ [NON_BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [NON_BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ operationId: 'operation2-message1' }), + expect.objectContaining({ operationId: 'operation3-message1' }), + ])) }) test('should report added operation with multiple messages', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/add-with-multiple-messages') expect(result).toEqual(changesSummaryMatcher({ [NON_BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [NON_BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ operationId: 'operation2-message1' }), + expect.objectContaining({ operationId: 'operation2-message2' }), + ])) }) test('should report added operation without message change diffs', async () => { @@ -58,13 +66,21 @@ describe('AsyncAPI 3.0 Changelog tests', () => { // not inherit the breaking payload change from message1. const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/add-with-changed-message') expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1, [NON_BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1, [NON_BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ + operationId: 'operation1-message1', + previousOperationId: 'operation1-message1', + }), + expect.objectContaining({ operationId: 'operation2-message1' }), + ])) }) test('should report removed operation', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/remove') expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ previousOperationId: 'operation2-message1' }), + ])) }) test('should report simultaneously added and removed operations', async () => { @@ -73,17 +89,22 @@ describe('AsyncAPI 3.0 Changelog tests', () => { [BREAKING_CHANGE_TYPE]: 1, [NON_BREAKING_CHANGE_TYPE]: 1, }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ - [BREAKING_CHANGE_TYPE]: 1, - [NON_BREAKING_CHANGE_TYPE]: 1, - }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ previousOperationId: 'old-operation-message1' }), + expect.objectContaining({ operationId: 'new-operation-message1' }), + ])) }) test('should report changed operation action type', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/operation/change-action') expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ + operationId: 'operation1-message1', + previousOperationId: 'operation1-message1', + }), + ])) }) test('should report changed operation description with multiple messages', async () => { @@ -95,9 +116,16 @@ describe('AsyncAPI 3.0 Changelog tests', () => { expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1, }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ - [ANNOTATION_CHANGE_TYPE]: 2, - }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ + operationId: 'operation1-message1', + previousOperationId: 'operation1-message1', + }), + expect.objectContaining({ + operationId: 'operation1-message2', + previousOperationId: 'operation1-message2', + }), + ])) }) }) @@ -106,7 +134,12 @@ describe('AsyncAPI 3.0 Changelog tests', () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/change-reference') expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ + operationId: 'operation2-message1', + previousOperationId: 'operation2-message1', + }), + ])) }) test('should not report changes when removing unused channel', async () => { @@ -119,7 +152,12 @@ describe('AsyncAPI 3.0 Changelog tests', () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/channel/change-address') expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ + operationId: 'operation1-message1', + previousOperationId: 'operation1-message1', + }), + ])) }) test('should impact all operations on shared channel when changing address', async () => { @@ -128,7 +166,16 @@ describe('AsyncAPI 3.0 Changelog tests', () => { // operation1 and operation2 both reference channel1, address changed // both apihub operations should be impacted expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ + operationId: 'operation1-message1', + previousOperationId: 'operation1-message1', + }), + expect.objectContaining({ + operationId: 'operation2-message2', + previousOperationId: 'operation2-message2', + }), + ])) }) test('should not report changes when adding message definition in channel without referencing it in operation', async () => { @@ -161,7 +208,12 @@ describe('AsyncAPI 3.0 Changelog tests', () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/server/add-to-channel') expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ + operationId: 'operation1-message1', + previousOperationId: 'operation1-message1', + }), + ])) }) test('should report removed server from channel', async () => { @@ -169,7 +221,12 @@ describe('AsyncAPI 3.0 Changelog tests', () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/server/remove-from-channel') expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ + operationId: 'operation1-message1', + previousOperationId: 'operation1-message1', + }), + ])) }) test('should report changed server used in channel', async () => { @@ -178,7 +235,12 @@ describe('AsyncAPI 3.0 Changelog tests', () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/server/change-in-channel') expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ + operationId: 'operation1-message1', + previousOperationId: 'operation1-message1', + }), + ])) }) test('should only impact operation whose channel references the changed server', async () => { @@ -235,21 +297,30 @@ describe('AsyncAPI 3.0 Changelog tests', () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/message/add-to-operation') expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ operationId: 'operation1-message2' }), + ])) }) test('should report removed APIHUB operation when message reference is removed from async operation', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/message/remove-from-operation') expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ previousOperationId: 'operation1-message2' }), + ])) }) test('should report changed message content type', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/message/change-content-type') expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ + operationId: 'operation1-message1', + previousOperationId: 'operation1-message1', + }), + ])) }) test('should not impact other operation when adding message to one', async () => { @@ -369,21 +440,36 @@ describe('AsyncAPI 3.0 Changelog tests', () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/schema/add-property') expect(result).toEqual(changesSummaryMatcher({ [NON_BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [NON_BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ + operationId: 'operation1-message1', + previousOperationId: 'operation1-message1', + }), + ])) }) test('should report removed property from message schema', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/schema/remove-property') expect(result).toEqual(changesSummaryMatcher({ [NON_BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [NON_BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ + operationId: 'operation1-message1', + previousOperationId: 'operation1-message1', + }), + ])) }) test('should report changed property type in message schema', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/schema/change-property-type') expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ + operationId: 'operation1-message1', + previousOperationId: 'operation1-message1', + }), + ])) }) }) @@ -393,7 +479,12 @@ describe('AsyncAPI 3.0 Changelog tests', () => { // info.version changed (1.0.0 -> 2.0.0) — should be detected as a change in every apihub operation expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ + operationId: 'operation1-message1', + previousOperationId: 'operation1-message1', + }), + ])) }) test('should report changed info title', async () => { @@ -401,14 +492,24 @@ describe('AsyncAPI 3.0 Changelog tests', () => { // info.title changed — should be detected as a change in every apihub operation expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ + operationId: 'operation1-message1', + previousOperationId: 'operation1-message1', + }), + ])) }) test('should report changed document id', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/info/change-id') expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ + operationId: 'operation1-message1', + previousOperationId: 'operation1-message1', + }), + ])) }) test('should report changed defaultContentType', async () => { @@ -417,7 +518,12 @@ describe('AsyncAPI 3.0 Changelog tests', () => { // Root-level defaultContentType change + propagated breaking change in message contentType. // message1 has no explicit contentType → affected by defaultContentType change. expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1, [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1, [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ + operationId: 'operation1-message1', + previousOperationId: 'operation1-message1', + }), + ])) }) test('should not report defaultContentType change for message with explicit contentType', async () => { @@ -455,4 +561,16 @@ describe('AsyncAPI 3.0 Changelog tests', () => { })) }) }) + + describe('Duplicate operationId across documents', () => { + test('should throw error during changelog when same operationId appears in multiple documents', async () => { + // Same operation (operation1-message1) described in two documents. + // AsyncAPI does not allow duplicate operationIds across documents — must throw. + await expect(buildChangelogPackageDefaultConfig( + 'asyncapi-deduplication/cross-document-dedup', + [{ fileId: 'before1.yaml', publish: true }, { fileId: 'before2.yaml', publish: true }], + [{ fileId: 'after1.yaml' }, { fileId: 'after2.yaml' }], + )).rejects.toThrow(/Duplicated operationId 'operation1-message1'/) + }) + }) }) diff --git a/test/asyncapi-deduplication.test.ts b/test/asyncapi-deduplication.test.ts index 0f38e2d9..b0278151 100644 --- a/test/asyncapi-deduplication.test.ts +++ b/test/asyncapi-deduplication.test.ts @@ -71,7 +71,7 @@ describe('AsyncAPI deduplication tests', () => { // Both specs define SharedPayload with same change (number → string). // Different scopes → separate apiDiff calls → separate diff instances. // Cross-document content-based dedup (calculateDiffId) applies per operation, - // but these are different operations so both changes are counted. + // but these operations have different scope, so both changes are counted. const result = await buildChangelogPackageDefaultConfig( 'asyncapi-deduplication/shared-schema-cross-specs', [{ fileId: 'before1.yaml', publish: true }, { fileId: 'before2.yaml', publish: true }], @@ -111,14 +111,5 @@ describe('AsyncAPI deduplication tests', () => { expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) }) - test('should throw error when same operationId appears in multiple documents', async () => { - // Same operation (operation1-message1) described in two documents. - // AsyncAPI does not allow duplicate operationIds across documents — must throw. - await expect(buildChangelogPackageDefaultConfig( - 'asyncapi-deduplication/cross-document-dedup', - [{ fileId: 'before1.yaml', publish: true }, { fileId: 'before2.yaml', publish: true }], - [{ fileId: 'after1.yaml' }, { fileId: 'after2.yaml' }], - )).rejects.toThrow(/Duplicated operationId 'operation1-message1'/) - }) }) }) diff --git a/test/asyncapi-operation.test.ts b/test/asyncapi-operation.test.ts index 43e35a33..4aef3fd9 100644 --- a/test/asyncapi-operation.test.ts +++ b/test/asyncapi-operation.test.ts @@ -18,10 +18,10 @@ import { beforeAll, describe, expect, it, test } from '@jest/globals' import { v3 as AsyncAPIV3 } from '@asyncapi/parser/esm/spec-types' import { createOperationSpec, createOperationSpecWithInlineRefs } from '../src/apitypes/async/async.operation' import { calculateAsyncOperationId } from '../src/utils' -import { buildPackageWithDefaultConfig, cloneDocument, loadYamlFile } from './helpers' +import { buildPackageWithDefaultConfig, cloneDocument, loadYamlFile, LocalRegistry } from './helpers' import { extractProtocol } from '../src/apitypes/async/async.utils' import { FIRST_REFERENCE_KEY_PROPERTY, INLINE_REFS_FLAG } from '../src/consts' -import { ASYNC_EFFECTIVE_NORMALIZE_OPTIONS } from '../src' +import { ASYNC_EFFECTIVE_NORMALIZE_OPTIONS, BUILD_TYPE, VERSION_STATUS } from '../src' import { normalize } from '@netcracker/qubership-apihub-api-unifier' const normalizeAsyncApiDocument = (doc: AsyncAPIV3.AsyncAPIObject): AsyncAPIV3.AsyncAPIObject => @@ -433,4 +433,22 @@ describe('AsyncAPI 3.0 Operation Tests', () => { expect(Object.keys(result.servers!)).toEqual(expect.arrayContaining(['production', 'staging'])) }) }) + + describe('Duplicate operationId validation', () => { + test('should throw error during build when same operationId appears in multiple documents', async () => { + const packageId = 'asyncapi-changes/operation/duplicate-cross-document' + const portal = new LocalRegistry(packageId) + + await expect(portal.publish(packageId, { + packageId, + version: 'v1', + status: VERSION_STATUS.RELEASE, + buildType: BUILD_TYPE.BUILD, + files: [ + { fileId: 'spec1.yaml', publish: true }, + { fileId: 'spec2.yaml', publish: true }, + ], + })).rejects.toThrow(/Duplicated operationId 'operation1-message1'/) + }) + }) }) diff --git a/test/projects/asyncapi-changes/operation/duplicate-cross-document/spec1.yaml b/test/projects/asyncapi-changes/operation/duplicate-cross-document/spec1.yaml new file mode 100644 index 00000000..c7bfb00e --- /dev/null +++ b/test/projects/asyncapi-changes/operation/duplicate-cross-document/spec1.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI Doc1 + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-changes/operation/duplicate-cross-document/spec2.yaml b/test/projects/asyncapi-changes/operation/duplicate-cross-document/spec2.yaml new file mode 100644 index 00000000..b8e88116 --- /dev/null +++ b/test/projects/asyncapi-changes/operation/duplicate-cross-document/spec2.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI Doc2 + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number From 757832d0579b135198c5ea9018b4b5c34b9e2eb0 Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Fri, 27 Mar 2026 10:56:02 +0400 Subject: [PATCH 26/29] feat: Fixed DefaultContentType and message ContentType --- src/apitypes/async/async.changes.ts | 11 ++--- src/apitypes/async/async.utils.ts | 11 +---- test/asyncapi-changes.test.ts | 44 ++++++++++++++++--- .../after.yaml | 36 +++++++++++++++ .../before.yaml | 36 +++++++++++++++ .../after.yaml | 26 +++++++++++ .../before.yaml | 27 ++++++++++++ 7 files changed, 167 insertions(+), 24 deletions(-) create mode 100644 test/projects/asyncapi-changes/info/change-default-content-type-mixed-messages/after.yaml create mode 100644 test/projects/asyncapi-changes/info/change-default-content-type-mixed-messages/before.yaml create mode 100644 test/projects/asyncapi-changes/info/change-default-content-type-with-removed-override/after.yaml create mode 100644 test/projects/asyncapi-changes/info/change-default-content-type-with-removed-override/before.yaml diff --git a/src/apitypes/async/async.changes.ts b/src/apitypes/async/async.changes.ts index 5a44c21d..23196980 100644 --- a/src/apitypes/async/async.changes.ts +++ b/src/apitypes/async/async.changes.ts @@ -52,11 +52,9 @@ import { collectChannelMessageDefinitionDiffs, collectExclusiveOtherMessageDiffs, extractAsyncApiVersionDiff, - extractDefaultContentTypeDiff, extractIdDiff, extractInfoDiffs, getAsyncMessageId, - hasExplicitContentType, } from './async.utils' export const compareDocuments: DocumentsCompare = async ( @@ -118,7 +116,9 @@ export const compareDocuments: DocumentsCompare = async ( const asyncApiVersionDiffs = extractAsyncApiVersionDiff(merged) const infoDiffs = extractInfoDiffs(merged) const idDiffs = extractIdDiff(merged) - const defaultContentTypeDiffs = extractDefaultContentTypeDiff(merged) + // Note: defaultContentType changes are handled by normalization inside apiDiff. + // Messages without explicit contentType inherit from defaultContentType during normalization, + // so changes to defaultContentType automatically appear in allOperationDiffs for affected messages. // Iterate through operations in merged document const { operations: asyncOperations } = merged @@ -159,17 +159,12 @@ export const compareDocuments: DocumentsCompare = async ( const otherMessageDiffs = collectExclusiveOtherMessageDiffs(messages, messageIndex) const channelMessageDiffs = collectChannelMessageDefinitionDiffs(operationChannel as AsyncAPIV3.ChannelObject) - // defaultContentType only affects messages without explicit contentType - const messageAffectedByDefaultContentType = - !hasExplicitContentType(prevDocData, messageId) || - !hasExplicitContentType(currDocData, messageId) operationDiffs = [ ...([...allOperationDiffs].filter(diff => !otherMessageDiffs.has(diff) && !channelMessageDiffs.has(diff))), ...asyncApiVersionDiffs, ...infoDiffs, ...idDiffs, - ...(messageAffectedByDefaultContentType ? defaultContentTypeDiffs : []), ] } if (operationAddedOrRemoved) { diff --git a/src/apitypes/async/async.utils.ts b/src/apitypes/async/async.utils.ts index ae64bfa5..f0aa4047 100644 --- a/src/apitypes/async/async.utils.ts +++ b/src/apitypes/async/async.utils.ts @@ -44,7 +44,7 @@ import { INLINE_REFS_FLAG, } from '../../consts' import { WithAggregatedDiffs, WithDiffMetaRecord } from '../../types' -import { Diff, DIFF_META_KEY, DIFFS_AGGREGATED_META_KEY } from '@netcracker/qubership-apihub-api-diff' +import { Diff, DiffAction, DIFF_META_KEY, DIFFS_AGGREGATED_META_KEY } from '@netcracker/qubership-apihub-api-diff' // Re-export shared utilities export { dump, getCustomTags, resolveApiAudience } from '../../utils/apihubSpecificationExtensions' @@ -271,15 +271,6 @@ export const resolveAsyncApiOperationIdsFromRefs = ( return resolved } -/** - * Checks whether a message has explicit `contentType` in the raw (pre-normalization) document. - * Messages without explicit contentType inherit from `defaultContentType`. - */ -export function hasExplicitContentType(doc: AsyncAPIV3.AsyncAPIObject | undefined | null, messageId: string): boolean { - const message = getKeyValue(doc, 'components', 'messages', messageId) - return isObject(message) && !isReferenceObject(message) && 'contentType' in message -} - /** * Aggregated diffs on the operation level include diffs from ALL messages. * Since each apihub operation maps to a specific operation + message pair, diff --git a/test/asyncapi-changes.test.ts b/test/asyncapi-changes.test.ts index 78fc1d29..42685778 100644 --- a/test/asyncapi-changes.test.ts +++ b/test/asyncapi-changes.test.ts @@ -515,9 +515,10 @@ describe('AsyncAPI 3.0 Changelog tests', () => { test('should report changed defaultContentType', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/info/change-default-content-type') - // Root-level defaultContentType change + propagated breaking change in message contentType. - // message1 has no explicit contentType → affected by defaultContentType change. - expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1, [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + // defaultContentType changed (json → xml). + // message1 has no explicit contentType → normalization resolves defaultContentType + // into effective contentType, so the change propagates as a breaking diff. + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) expect(result).toEqual(operationChangesMatcher([ expect.objectContaining({ operationId: 'operation1-message1', @@ -530,10 +531,10 @@ describe('AsyncAPI 3.0 Changelog tests', () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/info/change-default-content-type-with-explicit-override') // defaultContentType changed (json → xml). - // message1 has explicit contentType → not affected, no changes reported. - // message2 has no explicit contentType → affected by defaultContentType change. + // message1 has explicit contentType → not affected by defaultContentType change. + // message2 has no explicit contentType → affected via normalization. // Only operation2-message2 should be impacted. - expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1, [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) expect(result).toEqual(operationChangesMatcher([ expect.objectContaining({ operationId: 'operation2-message2', @@ -541,6 +542,37 @@ describe('AsyncAPI 3.0 Changelog tests', () => { }), ])) }) + + test('should only affect message without explicit contentType when defaultContentType changes in same operation', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/info/change-default-content-type-mixed-messages') + + // One operation with two messages: + // message1 has explicit contentType → not affected by defaultContentType change. + // message2 has no explicit contentType → affected via normalization. + // Only operation1-message2 should be impacted. + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ + operationId: 'operation1-message2', + previousOperationId: 'operation1-message2', + }), + ])) + }) + + test('should report defaultContentType change when message explicit contentType is removed', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/info/change-default-content-type-with-removed-override') + + // Before: message1 has explicit contentType (json), defaultContentType is json. + // After: message1 explicit contentType removed, defaultContentType changed to xml. + // message1 now inherits from defaultContentType → affected by the effective contentType change. + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(operationChangesMatcher([ + expect.objectContaining({ + operationId: 'operation1-message1', + previousOperationId: 'operation1-message1', + }), + ])) + }) }) describe('Tags tests', () => { diff --git a/test/projects/asyncapi-changes/info/change-default-content-type-mixed-messages/after.yaml b/test/projects/asyncapi-changes/info/change-default-content-type-mixed-messages/after.yaml new file mode 100644 index 00000000..a64187a4 --- /dev/null +++ b/test/projects/asyncapi-changes/info/change-default-content-type-mixed-messages/after.yaml @@ -0,0 +1,36 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +defaultContentType: application/xml +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + - $ref: '#/channels/channel1/messages/message2' +components: + messages: + message1: + contentType: application/json + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-changes/info/change-default-content-type-mixed-messages/before.yaml b/test/projects/asyncapi-changes/info/change-default-content-type-mixed-messages/before.yaml new file mode 100644 index 00000000..ec39e3a7 --- /dev/null +++ b/test/projects/asyncapi-changes/info/change-default-content-type-mixed-messages/before.yaml @@ -0,0 +1,36 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +defaultContentType: application/json +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + - $ref: '#/channels/channel1/messages/message2' +components: + messages: + message1: + contentType: application/json + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-changes/info/change-default-content-type-with-removed-override/after.yaml b/test/projects/asyncapi-changes/info/change-default-content-type-with-removed-override/after.yaml new file mode 100644 index 00000000..cd55ff34 --- /dev/null +++ b/test/projects/asyncapi-changes/info/change-default-content-type-with-removed-override/after.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +defaultContentType: application/xml +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changes/info/change-default-content-type-with-removed-override/before.yaml b/test/projects/asyncapi-changes/info/change-default-content-type-with-removed-override/before.yaml new file mode 100644 index 00000000..a0f70b48 --- /dev/null +++ b/test/projects/asyncapi-changes/info/change-default-content-type-with-removed-override/before.yaml @@ -0,0 +1,27 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +defaultContentType: application/json +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + contentType: application/json + payload: + type: object + properties: + userId: + type: string From 4ca9d20ae5d3440c63fc97d066b5f11414260a92 Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Fri, 27 Mar 2026 11:38:45 +0400 Subject: [PATCH 27/29] feat: Fixed detect to report in tests --- test/asyncapi-changes.test.ts | 4 ++-- test/asyncapi-deprecated.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/asyncapi-changes.test.ts b/test/asyncapi-changes.test.ts index 42685778..23cc09fc 100644 --- a/test/asyncapi-changes.test.ts +++ b/test/asyncapi-changes.test.ts @@ -477,7 +477,7 @@ describe('AsyncAPI 3.0 Changelog tests', () => { test('should report changed info version', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/info/change-version') - // info.version changed (1.0.0 -> 2.0.0) — should be detected as a change in every apihub operation + // info.version changed (1.0.0 -> 2.0.0) — should be reported as a change in every apihub operation expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) expect(result).toEqual(operationChangesMatcher([ expect.objectContaining({ @@ -490,7 +490,7 @@ describe('AsyncAPI 3.0 Changelog tests', () => { test('should report changed info title', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/info/change-title') - // info.title changed — should be detected as a change in every apihub operation + // info.title changed — should be reported as a change in every apihub operation expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) expect(result).toEqual(operationChangesMatcher([ expect.objectContaining({ diff --git a/test/asyncapi-deprecated.test.ts b/test/asyncapi-deprecated.test.ts index 370ea132..83299488 100644 --- a/test/asyncapi-deprecated.test.ts +++ b/test/asyncapi-deprecated.test.ts @@ -50,7 +50,7 @@ describe('AsyncAPI 3.0 Deprecated tests', () => { deprecatedItems = Array.from(result.operations.values()).flatMap(operation => operation.deprecatedItems ?? []) }) - test('should detect deprecated messages', async () => { + test('should report deprecated messages', async () => { const [deprecatedItem] = deprecatedItems expect(deprecatedItems.length).toBeGreaterThan(0) @@ -79,7 +79,7 @@ describe('AsyncAPI 3.0 Deprecated tests', () => { expect(operation.deprecated).toBe(true) }) - test('should detect deprecated schemas (flag "deprecated" in payload schema)', async () => { + test('should report deprecated schemas (flag "deprecated" in payload schema)', async () => { const result = await buildPackageWithDefaultConfig('asyncapi/deprecated/schemas') const deprecatedItems = Array.from(result.operations.values()).flatMap(operation => operation.deprecatedItems ?? []) expect(deprecatedItems.length).toBeGreaterThan(0) From 6a3218edc0a20503243851843b9d9d366c5df584 Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Mon, 30 Mar 2026 16:41:35 +0400 Subject: [PATCH 28/29] feat: Added asyncapi-changelog-apikind tests. Review --- ...test.ts => asyncapi-build-apikind.test.ts} | 0 test/asyncapi-changelog-apikind.test.ts | 108 ++++++++++++++++++ test/asyncapi-changes.test.ts | 16 --- test/asyncapi-deduplication.test.ts | 10 ++ .../channel/bwc-to-bwc/after.yaml | 26 +++++ .../channel/bwc-to-bwc/before.yaml | 26 +++++ .../channel/bwc-to-nobwc/after.yaml | 26 +++++ .../channel/bwc-to-nobwc/before.yaml | 26 +++++ .../channel/bwc-to-none/after.yaml | 25 ++++ .../channel/bwc-to-none/before.yaml | 26 +++++ .../channel/nobwc-to-bwc/after.yaml | 26 +++++ .../channel/nobwc-to-bwc/before.yaml | 26 +++++ .../channel/nobwc-to-nobwc/after.yaml | 26 +++++ .../channel/nobwc-to-nobwc/before.yaml | 26 +++++ .../channel/nobwc-to-none/after.yaml | 25 ++++ .../channel/nobwc-to-none/before.yaml | 26 +++++ .../channel/none-to-bwc/after.yaml | 26 +++++ .../channel/none-to-bwc/before.yaml | 25 ++++ .../channel/none-to-nobwc/after.yaml | 26 +++++ .../channel/none-to-nobwc/before.yaml | 25 ++++ .../channel/none-to-none/after.yaml | 25 ++++ .../channel/none-to-none/before.yaml | 25 ++++ .../operation/bwc-to-bwc/after.yaml | 26 +++++ .../operation/bwc-to-bwc/before.yaml | 26 +++++ .../operation/bwc-to-nobwc/after.yaml | 26 +++++ .../operation/bwc-to-nobwc/before.yaml | 26 +++++ .../operation/bwc-to-none/after.yaml | 25 ++++ .../operation/bwc-to-none/before.yaml | 26 +++++ .../operation/nobwc-to-bwc/after.yaml | 26 +++++ .../operation/nobwc-to-bwc/before.yaml | 26 +++++ .../operation/nobwc-to-nobwc/after.yaml | 26 +++++ .../operation/nobwc-to-nobwc/before.yaml | 26 +++++ .../operation/nobwc-to-none/after.yaml | 25 ++++ .../operation/nobwc-to-none/before.yaml | 26 +++++ .../operation/none-to-bwc/after.yaml | 26 +++++ .../operation/none-to-bwc/before.yaml | 25 ++++ .../operation/none-to-nobwc/after.yaml | 26 +++++ .../operation/none-to-nobwc/before.yaml | 25 ++++ .../operation/none-to-none/after.yaml | 25 ++++ .../operation/none-to-none/before.yaml | 25 ++++ .../after.yaml | 27 +++++ .../before.yaml} | 16 ++- .../after.yaml | 26 +++++ .../before.yaml | 40 +++++++ .../after.yaml | 27 +++++ .../before.yaml | 42 +++++++ .../after.yaml | 26 +++++ .../before.yaml | 40 +++++++ .../remove/remove-operation-bwc/after.yaml | 26 +++++ .../remove/remove-operation-bwc/before.yaml | 41 +++++++ .../remove/remove-operation-nobwc/after.yaml | 26 +++++ .../remove/remove-operation-nobwc/before.yaml | 41 +++++++ .../remove/remove-operation-none/after.yaml | 25 ++++ .../remove/remove-operation-none/before.yaml | 39 +++++++ .../after.yaml | 35 ++++++ .../before.yaml | 13 +-- 56 files changed, 1512 insertions(+), 36 deletions(-) rename test/{asyncapi-apikind.test.ts => asyncapi-build-apikind.test.ts} (100%) create mode 100644 test/asyncapi-changelog-apikind.test.ts create mode 100644 test/projects/asyncapi-changelog-apikind/channel/bwc-to-bwc/after.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/channel/bwc-to-bwc/before.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/channel/bwc-to-nobwc/after.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/channel/bwc-to-nobwc/before.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/channel/bwc-to-none/after.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/channel/bwc-to-none/before.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/channel/nobwc-to-bwc/after.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/channel/nobwc-to-bwc/before.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/channel/nobwc-to-nobwc/after.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/channel/nobwc-to-nobwc/before.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/channel/nobwc-to-none/after.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/channel/nobwc-to-none/before.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/channel/none-to-bwc/after.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/channel/none-to-bwc/before.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/channel/none-to-nobwc/after.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/channel/none-to-nobwc/before.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/channel/none-to-none/after.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/channel/none-to-none/before.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/operation/bwc-to-bwc/after.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/operation/bwc-to-bwc/before.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/operation/bwc-to-nobwc/after.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/operation/bwc-to-nobwc/before.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/operation/bwc-to-none/after.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/operation/bwc-to-none/before.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/operation/nobwc-to-bwc/after.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/operation/nobwc-to-bwc/before.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/operation/nobwc-to-nobwc/after.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/operation/nobwc-to-nobwc/before.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/operation/nobwc-to-none/after.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/operation/nobwc-to-none/before.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/operation/none-to-bwc/after.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/operation/none-to-bwc/before.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/operation/none-to-nobwc/after.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/operation/none-to-nobwc/before.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/operation/none-to-none/after.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/operation/none-to-none/before.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/remove/remove-channel-bwc-operation-nobwc/after.yaml rename test/projects/{asyncapi-changes/info/change-default-content-type-with-explicit-override/after.yaml => asyncapi-changelog-apikind/remove/remove-channel-bwc-operation-nobwc/before.yaml} (71%) create mode 100644 test/projects/asyncapi-changelog-apikind/remove/remove-channel-bwc-operation-none/after.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/remove/remove-channel-bwc-operation-none/before.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/remove/remove-channel-nobwc-operation-bwc/after.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/remove/remove-channel-nobwc-operation-bwc/before.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/remove/remove-channel-nobwc-operation-none/after.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/remove/remove-channel-nobwc-operation-none/before.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/remove/remove-operation-bwc/after.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/remove/remove-operation-bwc/before.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/remove/remove-operation-nobwc/after.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/remove/remove-operation-nobwc/before.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/remove/remove-operation-none/after.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/remove/remove-operation-none/before.yaml create mode 100644 test/projects/asyncapi-deduplication/default-content-type-multiple-messages/after.yaml rename test/projects/{asyncapi-changes/info/change-default-content-type-with-explicit-override => asyncapi-deduplication/default-content-type-multiple-messages}/before.yaml (72%) diff --git a/test/asyncapi-apikind.test.ts b/test/asyncapi-build-apikind.test.ts similarity index 100% rename from test/asyncapi-apikind.test.ts rename to test/asyncapi-build-apikind.test.ts diff --git a/test/asyncapi-changelog-apikind.test.ts b/test/asyncapi-changelog-apikind.test.ts new file mode 100644 index 00000000..ffd9c3c0 --- /dev/null +++ b/test/asyncapi-changelog-apikind.test.ts @@ -0,0 +1,108 @@ +import { + buildChangelogPackageDefaultConfig, + changesSummaryMatcher, + numberOfImpactedOperationsMatcher, +} from './helpers' +import { ASYNCAPI_API_TYPE, BREAKING_CHANGE_TYPE, RISKY_CHANGE_TYPE, UNCLASSIFIED_CHANGE_TYPE } from '../src' + +describe('AsyncAPI changelog api-kind tests', () => { + type ApiKindCase = [string, string, Record] + + const runApiKindCases = (scope: string, cases: ApiKindCase[]): void => { + test.each(cases)('%s (%s)', async (_description, caseName, expected) => { + const result = await buildChangelogPackageDefaultConfig(`asyncapi-changelog-apikind/${scope}/${caseName}`) + expect(result).toEqual(changesSummaryMatcher(expected, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher(expected, ASYNCAPI_API_TYPE)) + }) + } + + const apiKindTransitionCases: ApiKindCase[] = [ + ['should apply BWC by default when no x-api-kind is set', 'none-to-none', { [BREAKING_CHANGE_TYPE]: 1 }], + ['should apply BWC when x-api-kind: BWC added in current document', 'none-to-bwc', { [BREAKING_CHANGE_TYPE]: 1, [UNCLASSIFIED_CHANGE_TYPE]: 1 }], + ['should apply no-BWC when x-api-kind: no-BWC added in current document', 'none-to-nobwc', { [RISKY_CHANGE_TYPE]: 1, [UNCLASSIFIED_CHANGE_TYPE]: 1 }], + ['should apply BWC when x-api-kind: BWC removed in current document', 'bwc-to-none', { [BREAKING_CHANGE_TYPE]: 1, [UNCLASSIFIED_CHANGE_TYPE]: 1 }], + ['should apply BWC when x-api-kind: BWC in both documents', 'bwc-to-bwc', { [BREAKING_CHANGE_TYPE]: 1 }], + ['should apply no-BWC when x-api-kind changed from BWC to no-BWC', 'bwc-to-nobwc', { [RISKY_CHANGE_TYPE]: 1, [UNCLASSIFIED_CHANGE_TYPE]: 1 }], + ['should apply no-BWC when x-api-kind: no-BWC in previous document and removed in current', 'nobwc-to-none', { [RISKY_CHANGE_TYPE]: 1, [UNCLASSIFIED_CHANGE_TYPE]: 1 }], + ['should apply no-BWC when x-api-kind changed from no-BWC to BWC', 'nobwc-to-bwc', { [RISKY_CHANGE_TYPE]: 1, [UNCLASSIFIED_CHANGE_TYPE]: 1 }], + ['should apply no-BWC when x-api-kind: no-BWC in both documents', 'nobwc-to-nobwc', { [RISKY_CHANGE_TYPE]: 1 }], + ] + + describe('Operation-level x-api-kind', () => { + runApiKindCases('operation', apiKindTransitionCases) + }) + + describe('Channel-level x-api-kind', () => { + runApiKindCases('channel', apiKindTransitionCases) + }) + + describe('Operation + Channel x-api-kind', () => { + test('should apply BWC from channel when operation has no x-api-kind', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changelog-apikind/operation/channel-bwc-operation-none') + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('should apply no-BWC from channel when operation has no x-api-kind', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changelog-apikind/operation/channel-nobwc-operation-none') + expect(result).toEqual(changesSummaryMatcher({ [RISKY_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [RISKY_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('should prioritize operation no-BWC over channel BWC', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changelog-apikind/operation/channel-bwc-operation-nobwc') + expect(result).toEqual(changesSummaryMatcher({ [RISKY_CHANGE_TYPE]: 1, [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [RISKY_CHANGE_TYPE]: 1, [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('should prioritize operation BWC over channel no-BWC', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changelog-apikind/operation/channel-nobwc-operation-bwc') + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1, [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1, [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + }) + + describe('Remove operation tests', () => { + test('should apply removed operation as BWC when operation has x-api-kind: BWC', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changelog-apikind/remove/remove-operation-bwc') + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('should apply removed operation as no-BWC when operation has x-api-kind: no-BWC', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changelog-apikind/remove/remove-operation-nobwc') + expect(result).toEqual(changesSummaryMatcher({ [RISKY_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [RISKY_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('should apply removed operation as BWC by default when no x-api-kind is set', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changelog-apikind/remove/remove-operation-none') + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('should prioritize removed operation BWC over channel no-BWC', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changelog-apikind/remove/remove-channel-nobwc-operation-bwc') + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('should prioritize removed operation no-BWC over channel BWC', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changelog-apikind/remove/remove-channel-bwc-operation-nobwc') + expect(result).toEqual(changesSummaryMatcher({ [RISKY_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [RISKY_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('should fallback to channel no-BWC when removed operation has no x-api-kind', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changelog-apikind/remove/remove-channel-nobwc-operation-none') + expect(result).toEqual(changesSummaryMatcher({ [RISKY_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [RISKY_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('should fallback to channel BWC when removed operation has no x-api-kind', async () => { + const result = await buildChangelogPackageDefaultConfig('asyncapi-changelog-apikind/remove/remove-channel-bwc-operation-none') + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + }) +}) diff --git a/test/asyncapi-changes.test.ts b/test/asyncapi-changes.test.ts index 23cc09fc..7543b5d5 100644 --- a/test/asyncapi-changes.test.ts +++ b/test/asyncapi-changes.test.ts @@ -527,22 +527,6 @@ describe('AsyncAPI 3.0 Changelog tests', () => { ])) }) - test('should not report defaultContentType change for message with explicit contentType', async () => { - const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/info/change-default-content-type-with-explicit-override') - - // defaultContentType changed (json → xml). - // message1 has explicit contentType → not affected by defaultContentType change. - // message2 has no explicit contentType → affected via normalization. - // Only operation2-message2 should be impacted. - expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) - expect(result).toEqual(operationChangesMatcher([ - expect.objectContaining({ - operationId: 'operation2-message2', - previousOperationId: 'operation2-message2', - }), - ])) - }) - test('should only affect message without explicit contentType when defaultContentType changes in same operation', async () => { const result = await buildChangelogPackageDefaultConfig('asyncapi-changes/info/change-default-content-type-mixed-messages') diff --git a/test/asyncapi-deduplication.test.ts b/test/asyncapi-deduplication.test.ts index b0278151..23c2c3c6 100644 --- a/test/asyncapi-deduplication.test.ts +++ b/test/asyncapi-deduplication.test.ts @@ -63,6 +63,16 @@ describe('AsyncAPI deduplication tests', () => { expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) }) + + test('should deduplicate defaultContentType diff across multiple messages without explicit contentType', async () => { + // One operation with two messages, both without explicit contentType. + // defaultContentType changed (json → xml) — both messages inherit it. + // The diff should be counted once in changesSummary but impact both apihub operations. + const result = await buildChangelogPackageDefaultConfig('asyncapi-deduplication/default-content-type-multiple-messages') + + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) + }) }) describe('Shared entities across different specifications', () => { diff --git a/test/projects/asyncapi-changelog-apikind/channel/bwc-to-bwc/after.yaml b/test/projects/asyncapi-changelog-apikind/channel/bwc-to-bwc/after.yaml new file mode 100644 index 00000000..f4ecb13d --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/channel/bwc-to-bwc/after.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + x-api-kind: BWC + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changelog-apikind/channel/bwc-to-bwc/before.yaml b/test/projects/asyncapi-changelog-apikind/channel/bwc-to-bwc/before.yaml new file mode 100644 index 00000000..f4d418b5 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/channel/bwc-to-bwc/before.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + x-api-kind: BWC + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/channel/bwc-to-nobwc/after.yaml b/test/projects/asyncapi-changelog-apikind/channel/bwc-to-nobwc/after.yaml new file mode 100644 index 00000000..4260cd79 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/channel/bwc-to-nobwc/after.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + x-api-kind: no-BWC + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changelog-apikind/channel/bwc-to-nobwc/before.yaml b/test/projects/asyncapi-changelog-apikind/channel/bwc-to-nobwc/before.yaml new file mode 100644 index 00000000..f4d418b5 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/channel/bwc-to-nobwc/before.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + x-api-kind: BWC + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/channel/bwc-to-none/after.yaml b/test/projects/asyncapi-changelog-apikind/channel/bwc-to-none/after.yaml new file mode 100644 index 00000000..3ed5e13e --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/channel/bwc-to-none/after.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changelog-apikind/channel/bwc-to-none/before.yaml b/test/projects/asyncapi-changelog-apikind/channel/bwc-to-none/before.yaml new file mode 100644 index 00000000..f4d418b5 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/channel/bwc-to-none/before.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + x-api-kind: BWC + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/channel/nobwc-to-bwc/after.yaml b/test/projects/asyncapi-changelog-apikind/channel/nobwc-to-bwc/after.yaml new file mode 100644 index 00000000..f4ecb13d --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/channel/nobwc-to-bwc/after.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + x-api-kind: BWC + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changelog-apikind/channel/nobwc-to-bwc/before.yaml b/test/projects/asyncapi-changelog-apikind/channel/nobwc-to-bwc/before.yaml new file mode 100644 index 00000000..4ca386aa --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/channel/nobwc-to-bwc/before.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + x-api-kind: no-BWC + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/channel/nobwc-to-nobwc/after.yaml b/test/projects/asyncapi-changelog-apikind/channel/nobwc-to-nobwc/after.yaml new file mode 100644 index 00000000..4260cd79 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/channel/nobwc-to-nobwc/after.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + x-api-kind: no-BWC + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changelog-apikind/channel/nobwc-to-nobwc/before.yaml b/test/projects/asyncapi-changelog-apikind/channel/nobwc-to-nobwc/before.yaml new file mode 100644 index 00000000..4ca386aa --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/channel/nobwc-to-nobwc/before.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + x-api-kind: no-BWC + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/channel/nobwc-to-none/after.yaml b/test/projects/asyncapi-changelog-apikind/channel/nobwc-to-none/after.yaml new file mode 100644 index 00000000..3ed5e13e --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/channel/nobwc-to-none/after.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changelog-apikind/channel/nobwc-to-none/before.yaml b/test/projects/asyncapi-changelog-apikind/channel/nobwc-to-none/before.yaml new file mode 100644 index 00000000..4ca386aa --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/channel/nobwc-to-none/before.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + x-api-kind: no-BWC + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/channel/none-to-bwc/after.yaml b/test/projects/asyncapi-changelog-apikind/channel/none-to-bwc/after.yaml new file mode 100644 index 00000000..f4ecb13d --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/channel/none-to-bwc/after.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + x-api-kind: BWC + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changelog-apikind/channel/none-to-bwc/before.yaml b/test/projects/asyncapi-changelog-apikind/channel/none-to-bwc/before.yaml new file mode 100644 index 00000000..1b57b3d6 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/channel/none-to-bwc/before.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/channel/none-to-nobwc/after.yaml b/test/projects/asyncapi-changelog-apikind/channel/none-to-nobwc/after.yaml new file mode 100644 index 00000000..4260cd79 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/channel/none-to-nobwc/after.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + x-api-kind: no-BWC + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changelog-apikind/channel/none-to-nobwc/before.yaml b/test/projects/asyncapi-changelog-apikind/channel/none-to-nobwc/before.yaml new file mode 100644 index 00000000..1b57b3d6 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/channel/none-to-nobwc/before.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/channel/none-to-none/after.yaml b/test/projects/asyncapi-changelog-apikind/channel/none-to-none/after.yaml new file mode 100644 index 00000000..3ed5e13e --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/channel/none-to-none/after.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changelog-apikind/channel/none-to-none/before.yaml b/test/projects/asyncapi-changelog-apikind/channel/none-to-none/before.yaml new file mode 100644 index 00000000..1b57b3d6 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/channel/none-to-none/before.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/operation/bwc-to-bwc/after.yaml b/test/projects/asyncapi-changelog-apikind/operation/bwc-to-bwc/after.yaml new file mode 100644 index 00000000..414505a8 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/operation/bwc-to-bwc/after.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + x-api-kind: BWC + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changelog-apikind/operation/bwc-to-bwc/before.yaml b/test/projects/asyncapi-changelog-apikind/operation/bwc-to-bwc/before.yaml new file mode 100644 index 00000000..f980de0f --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/operation/bwc-to-bwc/before.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + x-api-kind: BWC + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/operation/bwc-to-nobwc/after.yaml b/test/projects/asyncapi-changelog-apikind/operation/bwc-to-nobwc/after.yaml new file mode 100644 index 00000000..5c2abbe5 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/operation/bwc-to-nobwc/after.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + x-api-kind: no-BWC + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changelog-apikind/operation/bwc-to-nobwc/before.yaml b/test/projects/asyncapi-changelog-apikind/operation/bwc-to-nobwc/before.yaml new file mode 100644 index 00000000..f980de0f --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/operation/bwc-to-nobwc/before.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + x-api-kind: BWC + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/operation/bwc-to-none/after.yaml b/test/projects/asyncapi-changelog-apikind/operation/bwc-to-none/after.yaml new file mode 100644 index 00000000..3ed5e13e --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/operation/bwc-to-none/after.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changelog-apikind/operation/bwc-to-none/before.yaml b/test/projects/asyncapi-changelog-apikind/operation/bwc-to-none/before.yaml new file mode 100644 index 00000000..f980de0f --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/operation/bwc-to-none/before.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + x-api-kind: BWC + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/operation/nobwc-to-bwc/after.yaml b/test/projects/asyncapi-changelog-apikind/operation/nobwc-to-bwc/after.yaml new file mode 100644 index 00000000..414505a8 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/operation/nobwc-to-bwc/after.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + x-api-kind: BWC + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changelog-apikind/operation/nobwc-to-bwc/before.yaml b/test/projects/asyncapi-changelog-apikind/operation/nobwc-to-bwc/before.yaml new file mode 100644 index 00000000..3d87cb75 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/operation/nobwc-to-bwc/before.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + x-api-kind: no-BWC + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/operation/nobwc-to-nobwc/after.yaml b/test/projects/asyncapi-changelog-apikind/operation/nobwc-to-nobwc/after.yaml new file mode 100644 index 00000000..5c2abbe5 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/operation/nobwc-to-nobwc/after.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + x-api-kind: no-BWC + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changelog-apikind/operation/nobwc-to-nobwc/before.yaml b/test/projects/asyncapi-changelog-apikind/operation/nobwc-to-nobwc/before.yaml new file mode 100644 index 00000000..3d87cb75 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/operation/nobwc-to-nobwc/before.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + x-api-kind: no-BWC + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/operation/nobwc-to-none/after.yaml b/test/projects/asyncapi-changelog-apikind/operation/nobwc-to-none/after.yaml new file mode 100644 index 00000000..3ed5e13e --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/operation/nobwc-to-none/after.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changelog-apikind/operation/nobwc-to-none/before.yaml b/test/projects/asyncapi-changelog-apikind/operation/nobwc-to-none/before.yaml new file mode 100644 index 00000000..3d87cb75 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/operation/nobwc-to-none/before.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + x-api-kind: no-BWC + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/operation/none-to-bwc/after.yaml b/test/projects/asyncapi-changelog-apikind/operation/none-to-bwc/after.yaml new file mode 100644 index 00000000..414505a8 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/operation/none-to-bwc/after.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + x-api-kind: BWC + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changelog-apikind/operation/none-to-bwc/before.yaml b/test/projects/asyncapi-changelog-apikind/operation/none-to-bwc/before.yaml new file mode 100644 index 00000000..1b57b3d6 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/operation/none-to-bwc/before.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/operation/none-to-nobwc/after.yaml b/test/projects/asyncapi-changelog-apikind/operation/none-to-nobwc/after.yaml new file mode 100644 index 00000000..5c2abbe5 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/operation/none-to-nobwc/after.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + x-api-kind: no-BWC + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changelog-apikind/operation/none-to-nobwc/before.yaml b/test/projects/asyncapi-changelog-apikind/operation/none-to-nobwc/before.yaml new file mode 100644 index 00000000..1b57b3d6 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/operation/none-to-nobwc/before.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/operation/none-to-none/after.yaml b/test/projects/asyncapi-changelog-apikind/operation/none-to-none/after.yaml new file mode 100644 index 00000000..3ed5e13e --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/operation/none-to-none/after.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changelog-apikind/operation/none-to-none/before.yaml b/test/projects/asyncapi-changelog-apikind/operation/none-to-none/before.yaml new file mode 100644 index 00000000..1b57b3d6 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/operation/none-to-none/before.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/remove/remove-channel-bwc-operation-nobwc/after.yaml b/test/projects/asyncapi-changelog-apikind/remove/remove-channel-bwc-operation-nobwc/after.yaml new file mode 100644 index 00000000..c5ff24b0 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/remove/remove-channel-bwc-operation-nobwc/after.yaml @@ -0,0 +1,27 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + x-api-kind: BWC + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + x-api-kind: no-BWC + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-changes/info/change-default-content-type-with-explicit-override/after.yaml b/test/projects/asyncapi-changelog-apikind/remove/remove-channel-bwc-operation-nobwc/before.yaml similarity index 71% rename from test/projects/asyncapi-changes/info/change-default-content-type-with-explicit-override/after.yaml rename to test/projects/asyncapi-changelog-apikind/remove/remove-channel-bwc-operation-nobwc/before.yaml index 16f62a5d..f9a62b05 100644 --- a/test/projects/asyncapi-changes/info/change-default-content-type-with-explicit-override/after.yaml +++ b/test/projects/asyncapi-changelog-apikind/remove/remove-channel-bwc-operation-nobwc/before.yaml @@ -2,43 +2,41 @@ asyncapi: 3.0.0 info: title: Test version: 1.0.0 -defaultContentType: application/xml channels: channel1: + x-api-kind: BWC address: channel1 messages: message1: $ref: '#/components/messages/message1' - channel2: - address: channel2 - messages: message2: $ref: '#/components/messages/message2' operations: operation1: + x-api-kind: no-BWC action: receive channel: $ref: '#/channels/channel1' messages: - $ref: '#/channels/channel1/messages/message1' operation2: + x-api-kind: no-BWC action: receive channel: - $ref: '#/channels/channel2' + $ref: '#/channels/channel1' messages: - - $ref: '#/channels/channel2/messages/message2' + - $ref: '#/channels/channel1/messages/message2' components: messages: message1: - contentType: application/json payload: type: object properties: userId: - type: string + type: number message2: payload: type: object properties: orderId: - type: string + type: number diff --git a/test/projects/asyncapi-changelog-apikind/remove/remove-channel-bwc-operation-none/after.yaml b/test/projects/asyncapi-changelog-apikind/remove/remove-channel-bwc-operation-none/after.yaml new file mode 100644 index 00000000..f4d418b5 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/remove/remove-channel-bwc-operation-none/after.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + x-api-kind: BWC + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/remove/remove-channel-bwc-operation-none/before.yaml b/test/projects/asyncapi-changelog-apikind/remove/remove-channel-bwc-operation-none/before.yaml new file mode 100644 index 00000000..3696306c --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/remove/remove-channel-bwc-operation-none/before.yaml @@ -0,0 +1,40 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + x-api-kind: BWC + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + operation2: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message2' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number + message2: + payload: + type: object + properties: + orderId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/remove/remove-channel-nobwc-operation-bwc/after.yaml b/test/projects/asyncapi-changelog-apikind/remove/remove-channel-nobwc-operation-bwc/after.yaml new file mode 100644 index 00000000..4b644d10 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/remove/remove-channel-nobwc-operation-bwc/after.yaml @@ -0,0 +1,27 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + x-api-kind: no-BWC + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + x-api-kind: BWC + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/remove/remove-channel-nobwc-operation-bwc/before.yaml b/test/projects/asyncapi-changelog-apikind/remove/remove-channel-nobwc-operation-bwc/before.yaml new file mode 100644 index 00000000..dd577566 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/remove/remove-channel-nobwc-operation-bwc/before.yaml @@ -0,0 +1,42 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + x-api-kind: no-BWC + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + x-api-kind: BWC + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + operation2: + x-api-kind: BWC + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message2' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number + message2: + payload: + type: object + properties: + orderId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/remove/remove-channel-nobwc-operation-none/after.yaml b/test/projects/asyncapi-changelog-apikind/remove/remove-channel-nobwc-operation-none/after.yaml new file mode 100644 index 00000000..4ca386aa --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/remove/remove-channel-nobwc-operation-none/after.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + x-api-kind: no-BWC + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/remove/remove-channel-nobwc-operation-none/before.yaml b/test/projects/asyncapi-changelog-apikind/remove/remove-channel-nobwc-operation-none/before.yaml new file mode 100644 index 00000000..732f52cb --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/remove/remove-channel-nobwc-operation-none/before.yaml @@ -0,0 +1,40 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + x-api-kind: no-BWC + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + operation2: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message2' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number + message2: + payload: + type: object + properties: + orderId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/remove/remove-operation-bwc/after.yaml b/test/projects/asyncapi-changelog-apikind/remove/remove-operation-bwc/after.yaml new file mode 100644 index 00000000..f980de0f --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/remove/remove-operation-bwc/after.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + x-api-kind: BWC + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/remove/remove-operation-bwc/before.yaml b/test/projects/asyncapi-changelog-apikind/remove/remove-operation-bwc/before.yaml new file mode 100644 index 00000000..a001f188 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/remove/remove-operation-bwc/before.yaml @@ -0,0 +1,41 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + x-api-kind: BWC + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + operation2: + x-api-kind: BWC + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message2' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number + message2: + payload: + type: object + properties: + orderId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/remove/remove-operation-nobwc/after.yaml b/test/projects/asyncapi-changelog-apikind/remove/remove-operation-nobwc/after.yaml new file mode 100644 index 00000000..3d87cb75 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/remove/remove-operation-nobwc/after.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + x-api-kind: no-BWC + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/remove/remove-operation-nobwc/before.yaml b/test/projects/asyncapi-changelog-apikind/remove/remove-operation-nobwc/before.yaml new file mode 100644 index 00000000..23be21dd --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/remove/remove-operation-nobwc/before.yaml @@ -0,0 +1,41 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + x-api-kind: no-BWC + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + operation2: + x-api-kind: no-BWC + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message2' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number + message2: + payload: + type: object + properties: + orderId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/remove/remove-operation-none/after.yaml b/test/projects/asyncapi-changelog-apikind/remove/remove-operation-none/after.yaml new file mode 100644 index 00000000..1b57b3d6 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/remove/remove-operation-none/after.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/remove/remove-operation-none/before.yaml b/test/projects/asyncapi-changelog-apikind/remove/remove-operation-none/before.yaml new file mode 100644 index 00000000..9aa04b51 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/remove/remove-operation-none/before.yaml @@ -0,0 +1,39 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + operation2: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message2' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number + message2: + payload: + type: object + properties: + orderId: + type: number diff --git a/test/projects/asyncapi-deduplication/default-content-type-multiple-messages/after.yaml b/test/projects/asyncapi-deduplication/default-content-type-multiple-messages/after.yaml new file mode 100644 index 00000000..0b99f5dc --- /dev/null +++ b/test/projects/asyncapi-deduplication/default-content-type-multiple-messages/after.yaml @@ -0,0 +1,35 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +defaultContentType: application/xml +channels: + channel1: + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' + message2: + $ref: '#/components/messages/message2' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' + - $ref: '#/channels/channel1/messages/message2' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + message2: + payload: + type: object + properties: + userId: + type: string \ No newline at end of file diff --git a/test/projects/asyncapi-changes/info/change-default-content-type-with-explicit-override/before.yaml b/test/projects/asyncapi-deduplication/default-content-type-multiple-messages/before.yaml similarity index 72% rename from test/projects/asyncapi-changes/info/change-default-content-type-with-explicit-override/before.yaml rename to test/projects/asyncapi-deduplication/default-content-type-multiple-messages/before.yaml index 65b0785a..f7153476 100644 --- a/test/projects/asyncapi-changes/info/change-default-content-type-with-explicit-override/before.yaml +++ b/test/projects/asyncapi-deduplication/default-content-type-multiple-messages/before.yaml @@ -9,9 +9,6 @@ channels: messages: message1: $ref: '#/components/messages/message1' - channel2: - address: channel2 - messages: message2: $ref: '#/components/messages/message2' operations: @@ -21,16 +18,10 @@ operations: $ref: '#/channels/channel1' messages: - $ref: '#/channels/channel1/messages/message1' - operation2: - action: receive - channel: - $ref: '#/channels/channel2' - messages: - - $ref: '#/channels/channel2/messages/message2' + - $ref: '#/channels/channel1/messages/message2' components: messages: message1: - contentType: application/json payload: type: object properties: @@ -40,5 +31,5 @@ components: payload: type: object properties: - orderId: + userId: type: string From 953959f1c6375ee9ee141446297eb9d27db2b5d3 Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Mon, 30 Mar 2026 17:26:30 +0400 Subject: [PATCH 29/29] feat: Added asyncapi-changelog-apikind tests v2 --- .../channel-bwc-operation-nobwc/after.yaml | 26 ++++++++++++++++++ .../channel-bwc-operation-nobwc/before.yaml | 27 +++++++++++++++++++ .../channel-bwc-operation-none/after.yaml | 26 ++++++++++++++++++ .../channel-bwc-operation-none/before.yaml | 26 ++++++++++++++++++ .../channel-nobwc-operation-bwc/after.yaml | 26 ++++++++++++++++++ .../channel-nobwc-operation-bwc/before.yaml | 27 +++++++++++++++++++ .../channel-nobwc-operation-none/after.yaml | 26 ++++++++++++++++++ .../channel-nobwc-operation-none/before.yaml | 26 ++++++++++++++++++ 8 files changed, 210 insertions(+) create mode 100644 test/projects/asyncapi-changelog-apikind/operation/channel-bwc-operation-nobwc/after.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/operation/channel-bwc-operation-nobwc/before.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/operation/channel-bwc-operation-none/after.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/operation/channel-bwc-operation-none/before.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/operation/channel-nobwc-operation-bwc/after.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/operation/channel-nobwc-operation-bwc/before.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/operation/channel-nobwc-operation-none/after.yaml create mode 100644 test/projects/asyncapi-changelog-apikind/operation/channel-nobwc-operation-none/before.yaml diff --git a/test/projects/asyncapi-changelog-apikind/operation/channel-bwc-operation-nobwc/after.yaml b/test/projects/asyncapi-changelog-apikind/operation/channel-bwc-operation-nobwc/after.yaml new file mode 100644 index 00000000..f4ecb13d --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/operation/channel-bwc-operation-nobwc/after.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + x-api-kind: BWC + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changelog-apikind/operation/channel-bwc-operation-nobwc/before.yaml b/test/projects/asyncapi-changelog-apikind/operation/channel-bwc-operation-nobwc/before.yaml new file mode 100644 index 00000000..c5ff24b0 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/operation/channel-bwc-operation-nobwc/before.yaml @@ -0,0 +1,27 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + x-api-kind: BWC + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + x-api-kind: no-BWC + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/operation/channel-bwc-operation-none/after.yaml b/test/projects/asyncapi-changelog-apikind/operation/channel-bwc-operation-none/after.yaml new file mode 100644 index 00000000..f4ecb13d --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/operation/channel-bwc-operation-none/after.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + x-api-kind: BWC + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changelog-apikind/operation/channel-bwc-operation-none/before.yaml b/test/projects/asyncapi-changelog-apikind/operation/channel-bwc-operation-none/before.yaml new file mode 100644 index 00000000..f4d418b5 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/operation/channel-bwc-operation-none/before.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + x-api-kind: BWC + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/operation/channel-nobwc-operation-bwc/after.yaml b/test/projects/asyncapi-changelog-apikind/operation/channel-nobwc-operation-bwc/after.yaml new file mode 100644 index 00000000..4260cd79 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/operation/channel-nobwc-operation-bwc/after.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + x-api-kind: no-BWC + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changelog-apikind/operation/channel-nobwc-operation-bwc/before.yaml b/test/projects/asyncapi-changelog-apikind/operation/channel-nobwc-operation-bwc/before.yaml new file mode 100644 index 00000000..4b644d10 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/operation/channel-nobwc-operation-bwc/before.yaml @@ -0,0 +1,27 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + x-api-kind: no-BWC + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + x-api-kind: BWC + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number diff --git a/test/projects/asyncapi-changelog-apikind/operation/channel-nobwc-operation-none/after.yaml b/test/projects/asyncapi-changelog-apikind/operation/channel-nobwc-operation-none/after.yaml new file mode 100644 index 00000000..4260cd79 --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/operation/channel-nobwc-operation-none/after.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + x-api-kind: no-BWC + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changelog-apikind/operation/channel-nobwc-operation-none/before.yaml b/test/projects/asyncapi-changelog-apikind/operation/channel-nobwc-operation-none/before.yaml new file mode 100644 index 00000000..4ca386aa --- /dev/null +++ b/test/projects/asyncapi-changelog-apikind/operation/channel-nobwc-operation-none/before.yaml @@ -0,0 +1,26 @@ +asyncapi: 3.0.0 +info: + title: Test + version: 1.0.0 +channels: + channel1: + x-api-kind: no-BWC + address: channel1 + messages: + message1: + $ref: '#/components/messages/message1' +operations: + operation1: + action: receive + channel: + $ref: '#/channels/channel1' + messages: + - $ref: '#/channels/channel1/messages/message1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: number