Skip to content

Commit 1c85502

Browse files
authored
fix(core,mock): generate Blob for $ref schemas with format: binary (#3406)
* fix(core,mock): recognize $ref schemas with format binary as binary responses * fix(mock): match preferredContentType against success responses only * test(core): add regression tests for contentMediaType isBlob detection * fix(mock): rewrite ref-alias return types to ArrayBuffer for binary responses * fix(mock): source binary ref names from imports to avoid union parsing * fix(mock): include import alias in binary type rewrite
1 parent a027006 commit 1c85502

9 files changed

Lines changed: 405 additions & 11 deletions

File tree

packages/core/src/getters/response.test.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,111 @@ describe('getResponse', () => {
246246

247247
expect(result.isBlob).toBe(true);
248248
});
249+
250+
it('should set isBlob to true for */* with inline format: binary schema', () => {
251+
const responses: OpenApiResponsesObject = {
252+
'200': {
253+
description: 'Binary file',
254+
content: {
255+
'*/*': {
256+
schema: { type: 'string', format: 'binary' },
257+
},
258+
},
259+
},
260+
};
261+
262+
const result = getResponse({
263+
responses,
264+
operationName: 'downloadFile',
265+
context,
266+
});
267+
268+
expect(result.isBlob).toBe(true);
269+
});
270+
271+
it('should set isBlob to true for $ref to a format: binary schema', () => {
272+
const refContext: ContextSpec = createTestContextSpec({
273+
target: 'spec',
274+
spec: {
275+
components: {
276+
schemas: {
277+
TestPdfFile: { type: 'string', format: 'binary' },
278+
},
279+
},
280+
},
281+
override: {
282+
formData: { arrayHandling: 'serialize', disabled: false },
283+
enumGenerationType: 'const',
284+
},
285+
});
286+
const responses: OpenApiResponsesObject = {
287+
'200': {
288+
description: 'Binary file',
289+
content: {
290+
'*/*': {
291+
schema: { $ref: '#/components/schemas/TestPdfFile' },
292+
},
293+
},
294+
},
295+
};
296+
297+
const result = getResponse({
298+
responses,
299+
operationName: 'downloadFile',
300+
context: refContext,
301+
});
302+
303+
expect(result.isBlob).toBe(true);
304+
});
305+
306+
it('should set isBlob to true for schema with contentMediaType: application/octet-stream', () => {
307+
const responses: OpenApiResponsesObject = {
308+
'200': {
309+
description: 'OK',
310+
content: {
311+
'application/json': {
312+
schema: {
313+
type: 'string',
314+
contentMediaType: 'application/octet-stream',
315+
},
316+
},
317+
},
318+
},
319+
};
320+
321+
const result = getResponse({
322+
responses,
323+
operationName: 'downloadFile',
324+
context,
325+
});
326+
327+
expect(result.isBlob).toBe(true);
328+
});
329+
330+
it('should set isBlob to false when contentMediaType has contentEncoding (base64-encoded string)', () => {
331+
const responses: OpenApiResponsesObject = {
332+
'200': {
333+
description: 'OK',
334+
content: {
335+
'application/json': {
336+
schema: {
337+
type: 'string',
338+
contentMediaType: 'application/octet-stream',
339+
contentEncoding: 'base64',
340+
},
341+
},
342+
},
343+
},
344+
};
345+
346+
const result = getResponse({
347+
responses,
348+
operationName: 'downloadFile',
349+
context,
350+
});
351+
352+
expect(result.isBlob).toBe(false);
353+
});
249354
});
250355

251356
describe('duplicate union types', () => {

packages/core/src/getters/response.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@ export function getResponse({
7474
isBlob: groupedByStatus.success.some(
7575
(t) =>
7676
(!!t.contentType && isBinaryContentType(t.contentType)) ||
77-
(t.value === 'Blob' && !t.isRef),
77+
t.originalSchema?.format === 'binary' ||
78+
(t.originalSchema?.contentMediaType === 'application/octet-stream' &&
79+
!t.originalSchema.contentEncoding),
7880
),
7981
types: groupedByStatus,
8082
contentTypes,

packages/mock/src/msw/index.test.ts

Lines changed: 174 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,127 @@ describe('generateMSW', () => {
318318
);
319319
});
320320

321+
it('should generate ArrayBuffer mock for $ref to a format: binary schema', () => {
322+
const refBinaryVerbOptions = {
323+
...mockVerbOptions,
324+
response: {
325+
imports: [],
326+
definition: { success: 'TestPdfFile' },
327+
types: {
328+
success: [
329+
{
330+
key: '200',
331+
value: 'TestPdfFile',
332+
contentType: '*/*',
333+
originalSchema: { type: 'string', format: 'binary' },
334+
imports: [{ name: 'TestPdfFile' }],
335+
schemas: [],
336+
type: 'string',
337+
isEnum: false,
338+
isRef: true,
339+
hasReadonlyProps: false,
340+
},
341+
],
342+
},
343+
contentTypes: ['*/*'],
344+
},
345+
} as unknown as GeneratorVerbOptions;
346+
347+
const result = generateMSW(refBinaryVerbOptions, baseOptions);
348+
349+
expect(result.implementation.handler).toContain(
350+
'HttpResponse.arrayBuffer',
351+
);
352+
expect(result.implementation.handler).not.toContain('JSON.stringify');
353+
// The ref alias return type must be rewritten to ArrayBuffer to stay consistent with the arrayBuffer body.
354+
expect(result.implementation.function).toContain(': ArrayBuffer');
355+
expect(result.implementation.function).not.toContain(': TestPdfFile');
356+
});
357+
358+
it('should rewrite aliased ref imports for $ref binary schemas', () => {
359+
const aliasedVerbOptions = {
360+
...mockVerbOptions,
361+
response: {
362+
imports: [],
363+
definition: { success: '__TestPdfFile' },
364+
types: {
365+
success: [
366+
{
367+
key: '200',
368+
value: '__TestPdfFile',
369+
contentType: '*/*',
370+
originalSchema: { type: 'string', format: 'binary' },
371+
imports: [{ name: 'TestPdfFile', alias: '__TestPdfFile' }],
372+
schemas: [],
373+
type: 'string',
374+
isEnum: false,
375+
isRef: true,
376+
hasReadonlyProps: false,
377+
},
378+
],
379+
},
380+
contentTypes: ['*/*'],
381+
},
382+
} as unknown as GeneratorVerbOptions;
383+
384+
const result = generateMSW(aliasedVerbOptions, baseOptions);
385+
386+
expect(result.implementation.function).toContain(': ArrayBuffer');
387+
expect(result.implementation.function).not.toContain(': __TestPdfFile');
388+
});
389+
390+
it('should not force binary path when preferredContentType narrows to a non-binary success variant', () => {
391+
const mixedVerbOptions = {
392+
...mockVerbOptions,
393+
response: {
394+
imports: [],
395+
definition: { success: 'Pet | Blob' },
396+
types: {
397+
success: [
398+
{
399+
key: '200',
400+
value: 'Pet',
401+
contentType: 'application/json',
402+
originalSchema: {
403+
type: 'object',
404+
properties: { name: { type: 'string' } },
405+
},
406+
imports: [],
407+
schemas: [],
408+
type: 'object',
409+
isEnum: false,
410+
isRef: true,
411+
hasReadonlyProps: false,
412+
},
413+
{
414+
key: '200',
415+
value: 'Blob',
416+
contentType: 'application/octet-stream',
417+
originalSchema: { type: 'string', format: 'binary' },
418+
imports: [],
419+
schemas: [],
420+
type: 'string',
421+
isEnum: false,
422+
isRef: false,
423+
hasReadonlyProps: false,
424+
},
425+
],
426+
},
427+
contentTypes: ['application/json', 'application/octet-stream'],
428+
},
429+
} as unknown as GeneratorVerbOptions;
430+
431+
const result = generateMSW(mixedVerbOptions, {
432+
...baseOptions,
433+
mock: { preferredContentType: 'application/json' },
434+
} as unknown as GeneratorOptions);
435+
436+
expect(result.implementation.handler).not.toContain(
437+
'HttpResponse.arrayBuffer',
438+
);
439+
expect(result.implementation.handler).toContain('HttpResponse.json');
440+
});
441+
321442
it('should type the info parameter in the handler callback', () => {
322443
const result = generate({
323444
mock: { type: OutputMockType.MSW, delay: 100 },
@@ -521,7 +642,16 @@ describe('generateMSW', () => {
521642
response: {
522643
...mockVerbOptions.response,
523644
definition: { success: 'Blob' },
524-
types: { success: [{ key: '200', value: 'Blob' }] },
645+
types: {
646+
success: [
647+
{
648+
key: '200',
649+
value: 'Blob',
650+
contentType: 'application/octet-stream',
651+
},
652+
{ key: '200', value: 'Blob', contentType: 'image/png' },
653+
],
654+
},
525655
contentTypes: ['application/octet-stream', 'image/png'],
526656
},
527657
} as GeneratorVerbOptions;
@@ -609,7 +739,12 @@ describe('generateMSW', () => {
609739
response: {
610740
...mockVerbOptions.response,
611741
definition: { success: 'string' },
612-
types: { success: [{ key: '200', value: 'string' }] },
742+
types: {
743+
success: [
744+
{ key: '200', value: 'string', contentType: 'application/xml' },
745+
{ key: '200', value: 'string', contentType: 'application/json' },
746+
],
747+
},
613748
contentTypes: ['application/xml', 'application/json'],
614749
},
615750
} as GeneratorVerbOptions;
@@ -664,7 +799,12 @@ describe('generateMSW', () => {
664799
response: {
665800
...mockVerbOptions.response,
666801
definition: { success: 'string' },
667-
types: { success: [{ key: '200', value: 'string' }] },
802+
types: {
803+
success: [
804+
{ key: '200', value: 'string', contentType: 'text/plain' },
805+
{ key: '200', value: 'string', contentType: 'text/html' },
806+
],
807+
},
668808
contentTypes: ['text/plain', 'text/html'],
669809
},
670810
} as GeneratorVerbOptions;
@@ -911,7 +1051,12 @@ describe('generateMSW', () => {
9111051
response: {
9121052
...mockVerbOptions.response,
9131053
definition: { success: 'string' },
914-
types: { success: [{ key: '200', value: 'string' }] },
1054+
types: {
1055+
success: [
1056+
{ key: '200', value: 'string', contentType: 'text/plain' },
1057+
{ key: '200', value: 'string', contentType: 'application/json' },
1058+
],
1059+
},
9151060
contentTypes: ['text/plain', 'application/json'],
9161061
},
9171062
} as GeneratorVerbOptions;
@@ -935,7 +1080,21 @@ describe('generateMSW', () => {
9351080
response: {
9361081
...mockVerbOptions.response,
9371082
definition: { success: 'string | Pet' },
938-
types: { success: [{ key: '200', value: 'string | Pet' }] },
1083+
types: {
1084+
success: [
1085+
{ key: '200', value: 'string | Pet', contentType: 'text/plain' },
1086+
{
1087+
key: '200',
1088+
value: 'string | Pet',
1089+
contentType: 'application/xml',
1090+
},
1091+
{
1092+
key: '200',
1093+
value: 'string | Pet',
1094+
contentType: 'application/json',
1095+
},
1096+
],
1097+
},
9391098
contentTypes: ['text/plain', 'application/xml', 'application/json'],
9401099
},
9411100
} as GeneratorVerbOptions;
@@ -1063,7 +1222,16 @@ describe('generateMSW', () => {
10631222
response: {
10641223
...mockVerbOptions.response,
10651224
definition: { success: 'string | Pet' },
1066-
types: { success: [{ key: '200', value: 'string | Pet' }] },
1225+
types: {
1226+
success: [
1227+
{ key: '200', value: 'string | Pet', contentType: 'text/plain' },
1228+
{
1229+
key: '200',
1230+
value: 'string | Pet',
1231+
contentType: 'application/json',
1232+
},
1233+
],
1234+
},
10671235
contentTypes: ['text/plain', 'application/json'],
10681236
},
10691237
} as GeneratorVerbOptions;

0 commit comments

Comments
 (0)