Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ async function generateForOperations(
): Promise<SingleFunctionRenderType[]> {
const renders: SingleFunctionRenderType[] = [];
const {generator, payloads} = context;
const functionTypeMapping = generator.functionTypeMapping[channel.id()];
const functionTypeMapping = generator.functionTypeMapping?.[channel.id()];
const exchangeName = channel.bindings().get('amqp')?.value()?.exchange?.name;

for (const operation of channel.operations().all()) {
Expand Down Expand Up @@ -192,7 +192,7 @@ async function generateForChannels(
const {generator, payloads} = context;
const functionTypeMapping =
getFunctionTypeMappingFromAsyncAPI(channel) ??
generator.functionTypeMapping[channel.id()];
generator.functionTypeMapping?.[channel.id()];
const exchangeName = channel.bindings().get('amqp')?.value()?.exchange?.name;

const payload = payloads.channelModels[channel.id()];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ async function generateForOperations(
): Promise<SingleFunctionRenderType[]> {
const renders: SingleFunctionRenderType[] = [];
const {generator, payloads} = context;
const functionTypeMapping = generator.functionTypeMapping[channel.id()];
const functionTypeMapping = generator.functionTypeMapping?.[channel.id()];

for (const operation of channel.operations().all()) {
const updatedFunctionTypeMapping =
Expand Down Expand Up @@ -188,7 +188,7 @@ async function generateForChannels(
const {generator, payloads} = context;
const functionTypeMapping =
getFunctionTypeMappingFromAsyncAPI(channel) ??
generator.functionTypeMapping[channel.id()];
generator.functionTypeMapping?.[channel.id()];

const payload = payloads.channelModels[channel.id()];
if (!payload) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ function generateForOperations(
): HttpRenderType[] {
const renders: HttpRenderType[] = [];
const {generator, payloads} = context;
const functionTypeMapping = generator.functionTypeMapping[channel.id()];
const functionTypeMapping = generator.functionTypeMapping?.[channel.id()];

for (const operation of channel.operations().all()) {
const updatedFunctionTypeMapping =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ async function generateForOperations(
): Promise<SingleFunctionRenderType[]> {
const renders: SingleFunctionRenderType[] = [];
const {generator, payloads} = context;
const functionTypeMapping = generator.functionTypeMapping[channel.id()];
const functionTypeMapping = generator.functionTypeMapping?.[channel.id()];

for (const operation of channel.operations().all()) {
const updatedFunctionTypeMapping =
Expand Down Expand Up @@ -182,7 +182,7 @@ async function generateForChannels(
const {generator, payloads} = context;
const functionTypeMapping =
getFunctionTypeMappingFromAsyncAPI(channel) ??
generator.functionTypeMapping[channel.id()];
generator.functionTypeMapping?.[channel.id()];

const payload = payloads.channelModels[channel.id()];
if (!payload) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ function generateForOperations(
): SingleFunctionRenderType[] {
const renders: SingleFunctionRenderType[] = [];
const {generator, payloads} = context;
const functionTypeMapping = generator.functionTypeMapping[channel.id()];
const functionTypeMapping = generator.functionTypeMapping?.[channel.id()];

for (const operation of channel.operations().all()) {
const updatedFunctionTypeMapping =
Expand Down Expand Up @@ -157,7 +157,7 @@ function generateForChannels(
): SingleFunctionRenderType[] {
const renders: SingleFunctionRenderType[] = [];
const {generator, payloads} = context;
const functionTypeMapping = generator.functionTypeMapping[channel.id()];
const functionTypeMapping = generator.functionTypeMapping?.[channel.id()];

const updatedFunctionTypeMapping =
getFunctionTypeMappingFromAsyncAPI(channel) ?? functionTypeMapping;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ async function generateForOperations(
): Promise<SingleFunctionRenderType[]> {
const renders: SingleFunctionRenderType[] = [];
const {generator, payloads} = context;
const functionTypeMapping = generator.functionTypeMapping[channel.id()];
const functionTypeMapping = generator.functionTypeMapping?.[channel.id()];

for (const operation of channel.operations().all()) {
const updatedFunctionTypeMapping =
Expand Down Expand Up @@ -325,7 +325,7 @@ async function generateForChannels(
const {generator, payloads} = context;
const functionTypeMapping =
getFunctionTypeMappingFromAsyncAPI(channel) ??
generator.functionTypeMapping[channel.id()];
generator.functionTypeMapping?.[channel.id()];

const payload = payloads.channelModels[channel.id()];
if (!payload) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ async function generateForOperations(
): Promise<SingleFunctionRenderType[]> {
const renders: SingleFunctionRenderType[] = [];
const {generator, payloads} = context;
const functionTypeMapping = generator.functionTypeMapping[channel.id()];
const functionTypeMapping = generator.functionTypeMapping?.[channel.id()];

for (const operation of channel.operations().all()) {
const updatedFunctionTypeMapping =
Expand Down Expand Up @@ -208,7 +208,7 @@ async function generateForChannels(
const {generator, payloads} = context;
const functionTypeMapping =
getFunctionTypeMappingFromAsyncAPI(channel) ??
generator.functionTypeMapping[channel.id()];
generator.functionTypeMapping?.[channel.id()];

const payload = payloads.channelModels[channel.id()];
if (!payload) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
/**
* Tests that all protocol generators handle undefined functionTypeMapping gracefully.
*
* Bug: When generator.functionTypeMapping is undefined (instead of {}), accessing
* generator.functionTypeMapping[channel.id()] throws:
* "Cannot read properties of undefined (reading 'channel_id')"
*
* This test verifies that all 7 protocol generators have defensive access via optional chaining.
*/
import path from 'node:path';
import {
defaultTypeScriptChannelsGenerator,
generateTypeScriptChannels,
TypeScriptParameterRenderType
} from '../../../../../../src/codegen/generators';
import {loadAsyncapiDocument} from '../../../../../../src/codegen/inputs/asyncapi';
import {
ConstrainedAnyModel,
ConstrainedObjectModel,
OutputModel
} from '@asyncapi/modelina';
import {TypeScriptPayloadRenderType} from '../../../../../../src/codegen/generators/typescript/payloads';
import {TypeScriptHeadersRenderType} from '../../../../../../src/codegen/generators/typescript/headers';

jest.mock('node:fs/promises', () => ({
writeFile: jest.fn().mockResolvedValue(undefined),
mkdir: jest.fn().mockResolvedValue(undefined)
}));

describe('functionTypeMapping undefined bug', () => {
const payloadModel = new OutputModel(
'',
new ConstrainedAnyModel('TestPayloadModel', undefined, {}, 'Payload'),
'TestPayloadModel',
{models: {}, originalInput: undefined},
[]
);

const createHeadersDependency = (): TypeScriptHeadersRenderType => ({
channelModels: {},
generator: {outputPath: './test'} as any
});

// Helper to create a generator with functionTypeMapping explicitly set to undefined
// This simulates the bug condition where spread operator preserves undefined
const createGeneratorWithUndefinedFunctionTypeMapping = (protocols: string[]) => {
const generator = {
...defaultTypeScriptChannelsGenerator,
outputPath: path.resolve(__dirname, './output'),
id: 'test',
asyncapiGenerateForOperations: false,
protocols
};
// Explicitly set to undefined to trigger the bug
(generator as any).functionTypeMapping = undefined;
return generator;
};

let parsedAsyncAPIDocument: any;
let parametersDependency: TypeScriptParameterRenderType;
let payloadsDependency: TypeScriptPayloadRenderType;

beforeAll(async () => {
parsedAsyncAPIDocument = await loadAsyncapiDocument(
path.resolve(__dirname, '../../../../../configs/asyncapi.yaml')
);

const parameterModel = new OutputModel(
'',
new ConstrainedObjectModel(
'TestParameter',
undefined,
{},
'Parameter',
{}
),
'TestParameter',
{models: {}, originalInput: undefined},
[]
);

parametersDependency = {
channelModels: {
'user/signedup': parameterModel
},
generator: {outputPath: './test'} as any
};

payloadsDependency = {
channelModels: {
'user/signedup': {
messageModel: payloadModel,
messageType: 'MessageType'
}
},
operationModels: {},
otherModels: [],
generator: {outputPath: './test'} as any
};
});

describe('should handle undefined functionTypeMapping without crashing', () => {
it('NATS generator', async () => {
await expect(
generateTypeScriptChannels({
generator: createGeneratorWithUndefinedFunctionTypeMapping(['nats']),
inputType: 'asyncapi',
asyncapiDocument: parsedAsyncAPIDocument,
dependencyOutputs: {
'parameters-typescript': parametersDependency,
'payloads-typescript': payloadsDependency,
'headers-typescript': createHeadersDependency()
}
})
).resolves.not.toThrow();
});

it('Kafka generator', async () => {
await expect(
generateTypeScriptChannels({
generator: createGeneratorWithUndefinedFunctionTypeMapping(['kafka']),
inputType: 'asyncapi',
asyncapiDocument: parsedAsyncAPIDocument,
dependencyOutputs: {
'parameters-typescript': parametersDependency,
'payloads-typescript': payloadsDependency,
'headers-typescript': createHeadersDependency()
}
})
).resolves.not.toThrow();
});

it('MQTT generator', async () => {
await expect(
generateTypeScriptChannels({
generator: createGeneratorWithUndefinedFunctionTypeMapping(['mqtt']),
inputType: 'asyncapi',
asyncapiDocument: parsedAsyncAPIDocument,
dependencyOutputs: {
'parameters-typescript': parametersDependency,
'payloads-typescript': payloadsDependency,
'headers-typescript': createHeadersDependency()
}
})
).resolves.not.toThrow();
});

it('AMQP generator', async () => {
await expect(
generateTypeScriptChannels({
generator: createGeneratorWithUndefinedFunctionTypeMapping(['amqp']),
inputType: 'asyncapi',
asyncapiDocument: parsedAsyncAPIDocument,
dependencyOutputs: {
'parameters-typescript': parametersDependency,
'payloads-typescript': payloadsDependency,
'headers-typescript': createHeadersDependency()
}
})
).resolves.not.toThrow();
});

it('WebSocket generator', async () => {
await expect(
generateTypeScriptChannels({
generator: createGeneratorWithUndefinedFunctionTypeMapping([
'websocket'
]),
inputType: 'asyncapi',
asyncapiDocument: parsedAsyncAPIDocument,
dependencyOutputs: {
'parameters-typescript': parametersDependency,
'payloads-typescript': payloadsDependency,
'headers-typescript': createHeadersDependency()
}
})
).resolves.not.toThrow();
});

it('EventSource generator', async () => {
await expect(
generateTypeScriptChannels({
generator: createGeneratorWithUndefinedFunctionTypeMapping([
'event_source'
]),
inputType: 'asyncapi',
asyncapiDocument: parsedAsyncAPIDocument,
dependencyOutputs: {
'parameters-typescript': parametersDependency,
'payloads-typescript': payloadsDependency,
'headers-typescript': createHeadersDependency()
}
})
).resolves.not.toThrow();
});

it('HTTP client generator', async () => {
// HTTP client needs request/reply pattern, use asyncapi-request.yaml
const requestDoc = await loadAsyncapiDocument(
path.resolve(__dirname, '../../../../../configs/asyncapi-request.yaml')
);

const httpPayloadsDependency: TypeScriptPayloadRenderType = {
channelModels: {
ping: {
messageModel: payloadModel,
messageType: 'MessageType'
}
},
operationModels: {
pingRequest: {
messageModel: payloadModel,
messageType: 'MessageType'
},
pongResponse: {
messageModel: payloadModel,
messageType: 'MessageType'
},
pingRequest_reply: {
messageModel: payloadModel,
messageType: 'MessageType'
}
},
otherModels: [],
generator: {outputPath: './test'} as any
};

const httpGenerator = createGeneratorWithUndefinedFunctionTypeMapping([
'http_client'
]);
// HTTP requires asyncapiGenerateForOperations: true
httpGenerator.asyncapiGenerateForOperations = true;

await expect(
generateTypeScriptChannels({
generator: httpGenerator,
inputType: 'asyncapi',
asyncapiDocument: requestDoc,
dependencyOutputs: {
'parameters-typescript': {
channelModels: {},
generator: {outputPath: './test'} as any
},
'payloads-typescript': httpPayloadsDependency,
'headers-typescript': createHeadersDependency()
}
})
).resolves.not.toThrow();
});

it('all protocols together', async () => {
// Test all AsyncAPI-compatible protocols together (excluding http_client which needs special setup)
await expect(
generateTypeScriptChannels({
generator: createGeneratorWithUndefinedFunctionTypeMapping([
'nats',
'kafka',
'mqtt',
'amqp',
'websocket',
'event_source'
]),
inputType: 'asyncapi',
asyncapiDocument: parsedAsyncAPIDocument,
dependencyOutputs: {
'parameters-typescript': parametersDependency,
'payloads-typescript': payloadsDependency,
'headers-typescript': createHeadersDependency()
}
})
).resolves.not.toThrow();
});
});
});
Loading