Browse Source

Echo declared Accept media types and degrade IRemoteStreamContent collections to any[] in Angular schematic

pull/25639/head
maliming 2 weeks ago
parent
commit
b0e4a9477f
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 3
      npm/ng-packs/packages/schematics/src/models/method.ts
  2. 67
      npm/ng-packs/packages/schematics/src/tests/action-to-body-mapper.spec.ts
  3. 14
      npm/ng-packs/packages/schematics/src/tests/proxy-service-template-render.spec.ts
  4. 32
      npm/ng-packs/packages/schematics/src/utils/service.ts

3
npm/ng-packs/packages/schematics/src/models/method.ts

@ -87,6 +87,9 @@ export class Body {
}
isBlobMethod() {
if (this.httpResponseType === 'blob') {
return true;
}
return VOLO_REMOTE_STREAM_CONTENT.some(x => x === this.responseTypeWithNamespace);
}

67
npm/ng-packs/packages/schematics/src/tests/action-to-body-mapper.spec.ts

@ -90,7 +90,7 @@ describe('createActionToBodyMapper — IRemoteStreamContent return value', () =>
expect(body.isBlobMethod()).toBe(true);
});
test('binary-only contentTypes also picks blob + octet-stream', () => {
test('binary-only contentTypes picks blob and echoes back the actual binary media type', () => {
const body = mapBody(
buildAction({
returnValue: {
@ -102,7 +102,7 @@ describe('createActionToBodyMapper — IRemoteStreamContent return value', () =>
);
expect(body.httpResponseType).toBe('blob');
expect(body.acceptHeader).toBe('application/octet-stream');
expect(body.acceptHeader).toBe('application/pdf');
});
});
@ -172,6 +172,40 @@ describe('createActionToBodyMapper — IsRemoteStream backend flag', () => {
expect(body.httpResponseType).toBe('blob');
expect(body.acceptHeader).toBe('application/octet-stream');
expect(body.isBlobMethod()).toBe(true);
});
test('[Volo.Abp.Content.IRemoteStreamContent] (real ABP square-bracket form) must NOT pick blob and degrades responseType to any[]', () => {
// ABP serialises collections as `[T]` (not `T[]`) — pin the on-the-wire shape.
const body = mapBody(
buildAction({
returnValue: {
type: 'System.Collections.Generic.IList<Volo.Abp.Content.IRemoteStreamContent>',
typeSimple: '[Volo.Abp.Content.IRemoteStreamContent]',
},
} as Partial<Action>),
);
expect(body.httpResponseType).toBeUndefined();
expect(body.acceptHeader).toBeUndefined();
expect(body.isBlobMethod()).toBe(false);
expect(body.responseType).toBe('any[]');
});
test('IRemoteStreamContent[] array return must NOT pick blob (server falls back to JSON metadata)', () => {
const body = mapBody(
buildAction({
returnValue: {
type: 'Volo.Abp.Content.IRemoteStreamContent[]',
typeSimple: 'Volo.Abp.Content.IRemoteStreamContent[]',
},
} as Partial<Action>),
);
expect(body.httpResponseType).toBeUndefined();
expect(body.acceptHeader).toBeUndefined();
expect(body.isBlobMethod()).toBe(false);
expect(body.responseType).toBe('any[]');
});
test('isRemoteStream=false with stream-content type-name still detected by type name (legacy)', () => {
@ -192,7 +226,7 @@ describe('createActionToBodyMapper — IsRemoteStream backend flag', () => {
describe('createActionToBodyMapper — +json suffix detection', () => {
const mapBody = createActionToBodyMapper();
test('application/problem+json picked as json', () => {
test('application/problem+json echoes back as Accept (decoder stays json)', () => {
const body = mapBody(
buildAction({
returnValue: {
@ -204,10 +238,10 @@ describe('createActionToBodyMapper — +json suffix detection', () => {
);
expect(body.httpResponseType).toBe('json');
expect(body.acceptHeader).toBe('application/json');
expect(body.acceptHeader).toBe('application/problem+json');
});
test('text/json picked as json (informal but real-world)', () => {
test('text/json echoes back as text/json (decoder stays json)', () => {
const body = mapBody(
buildAction({
returnValue: {
@ -219,10 +253,10 @@ describe('createActionToBodyMapper — +json suffix detection', () => {
);
expect(body.httpResponseType).toBe('json');
expect(body.acceptHeader).toBe('application/json');
expect(body.acceptHeader).toBe('text/json');
});
test('application/vnd.api+json picked as json', () => {
test('application/vnd.api+json echoes back as Accept (decoder stays json)', () => {
const body = mapBody(
buildAction({
returnValue: {
@ -234,6 +268,7 @@ describe('createActionToBodyMapper — +json suffix detection', () => {
);
expect(body.httpResponseType).toBe('json');
expect(body.acceptHeader).toBe('application/vnd.api+json');
});
});
@ -287,7 +322,7 @@ describe('createActionToBodyMapper — contentTypes precedence and edge cases',
expect(body.acceptHeader).toBe('application/octet-stream');
});
test('IRemoteStreamContent[] array also picked as blob', () => {
test('IRemoteStreamContent[] array falls through to defaults (server returns JSON metadata, not binary)', () => {
const body = mapBody(
buildAction({
returnValue: {
@ -297,8 +332,8 @@ describe('createActionToBodyMapper — contentTypes precedence and edge cases',
} as Partial<Action>),
);
expect(body.httpResponseType).toBe('blob');
expect(body.acceptHeader).toBe('application/octet-stream');
expect(body.httpResponseType).toBeUndefined();
expect(body.acceptHeader).toBeUndefined();
});
test('case-insensitive json detection (APPLICATION/JSON)', () => {
@ -316,7 +351,7 @@ describe('createActionToBodyMapper — contentTypes precedence and edge cases',
expect(body.acceptHeader).toBe('application/json');
});
test('image/* contentTypes alone picks blob', () => {
test('image/* contentTypes alone picks blob and echoes back the first image type', () => {
const body = mapBody(
buildAction({
returnValue: {
@ -328,7 +363,7 @@ describe('createActionToBodyMapper — contentTypes precedence and edge cases',
);
expect(body.httpResponseType).toBe('blob');
expect(body.acceptHeader).toBe('application/octet-stream');
expect(body.acceptHeader).toBe('image/png');
});
test('video/* and audio/* picked as blob', () => {
@ -370,7 +405,7 @@ describe('createActionToBodyMapper — contentTypes precedence and edge cases',
expect(body.acceptHeader).toBeUndefined();
});
test('mixed text/* and application/json picks json', () => {
test('mixed text/* and application/json picks json and echoes the first json-shaped media type', () => {
const body = mapBody(
buildAction({
returnValue: {
@ -382,7 +417,7 @@ describe('createActionToBodyMapper — contentTypes precedence and edge cases',
);
expect(body.httpResponseType).toBe('json');
expect(body.acceptHeader).toBe('application/json');
expect(body.acceptHeader).toBe('text/json');
});
test('contentTypes with json-suffix variants still picks json', () => {
@ -458,7 +493,7 @@ describe('createActionToBodyMapper — contentTypes precedence and edge cases',
expect(body.httpResponseType).toBe('json');
});
test('text/csv only (custom text format) picks text', () => {
test('text/csv only (custom text format) picks text and echoes the content type back', () => {
const body = mapBody(
buildAction({
returnValue: {
@ -470,7 +505,7 @@ describe('createActionToBodyMapper — contentTypes precedence and edge cases',
);
expect(body.httpResponseType).toBe('text');
expect(body.acceptHeader).toBe('text/plain');
expect(body.acceptHeader).toBe('text/csv');
});
});

14
npm/ng-packs/packages/schematics/src/tests/proxy-service-template-render.spec.ts

@ -138,6 +138,20 @@ describe('proxy service template — rendered output', () => {
expect(output).toContain("headers: { Accept: 'application/octet-stream' }");
});
test('IRemoteStreamContent[] degradation renders any[] return type and does not reference IRemoteStreamContent', () => {
const output = render(buildContext({
responseType: 'any[]',
responseTypeWithNamespace: '[Volo.Abp.Content.IRemoteStreamContent]',
isBlobMethod: () => false,
httpResponseType: undefined,
acceptHeader: undefined,
}));
expect(output).toContain('any[]');
expect(output).not.toContain('IRemoteStreamContent');
expect(output).not.toContain("responseType: 'blob'");
});
test('arraybuffer httpResponseType emits responseType', () => {
const output = render(buildContext({
responseType: 'ArrayBuffer',

32
npm/ng-packs/packages/schematics/src/utils/service.ts

@ -96,6 +96,9 @@ export function createActionToBodyMapper() {
responseType = adaptType(normalizedType);
}
}
if (isRemoteStreamContentArray(returnValue.typeSimple)) {
responseType = 'any[]';
}
const responseTypeWithNamespace = returnValue.typeSimple;
const { httpResponseType, acceptHeader } = resolveHttpResponseAndAccept(
responseType,
@ -175,28 +178,24 @@ function resolveHttpResponseAndAccept(
contentTypes: string[] | undefined,
isRemoteStreamFlag: boolean | undefined,
): { httpResponseType?: 'json' | 'text' | 'blob' | 'arraybuffer'; acceptHeader?: string } {
if (
isRemoteStreamFlag ||
isRemoteStreamContent(responseTypeWithNamespace) ||
isRemoteStreamContentArray(responseTypeWithNamespace)
) {
if (isRemoteStreamFlag || isRemoteStreamContent(responseTypeWithNamespace)) {
return { httpResponseType: 'blob', acceptHeader: 'application/octet-stream' };
}
if (contentTypes && contentTypes.length > 0) {
const normalized = contentTypes.map(normalizeMediaType);
if (normalized.some(isJsonMediaType)) {
return { httpResponseType: 'json', acceptHeader: 'application/json' };
const firstJsonShaped = normalized.find(isJsonMediaType);
if (firstJsonShaped) {
return { httpResponseType: 'json', acceptHeader: firstJsonShaped };
}
if (normalized.every(ct => ct.startsWith('text/'))) {
return { httpResponseType: 'text', acceptHeader: 'text/plain' };
return { httpResponseType: 'text', acceptHeader: normalized[0] };
}
if (normalized.every(isBinaryMediaType)) {
return { httpResponseType: 'blob', acceptHeader: 'application/octet-stream' };
return { httpResponseType: 'blob', acceptHeader: normalized[0] };
}
return { acceptHeader: normalized[0] };
}
if (responseType === 'string') {
@ -247,13 +246,18 @@ export function isRemoteStreamContent(type: string) {
}
export function isRemoteStreamContentArray(type: string) {
// Check for array types like Volo.Abp.Content.IRemoteStreamContent[]
if (VOLO_REMOTE_STREAM_CONTENT.map(x => `${x}[]`).some(x => x === type)) {
return true;
}
// Check for collection types like List<T>, IEnumerable<T>, ICollection<T>, Collection<T>, IList<T>
// This matches any generic type from System.Collections.Generic that implements IEnumerable<T>
// ABP serialises collections as `[T]` (see ApiTypeNameHelper.GetSimpleTypeName).
if (type.startsWith('[') && type.endsWith(']')) {
const inner = type.slice(1, -1);
if (VOLO_REMOTE_STREAM_CONTENT.includes(inner)) {
return true;
}
}
if (isCollectionType(type)) {
const { generics } = extractGenerics(type);
if (generics.length > 0 && VOLO_REMOTE_STREAM_CONTENT.includes(generics[0])) {

Loading…
Cancel
Save