Skip to content

Commit 9868804

Browse files
authored
feat: @res alias test case (#1694)
* test: @res alias test case * feat: logics for @res type alias * test: hapi, koa, openapi3 express integration rests * test: integration & unit test cases * test: apply alias to getController for test cases
1 parent 264be8d commit 9868804

File tree

8 files changed

+380
-95
lines changed

8 files changed

+380
-95
lines changed

packages/cli/src/metadataGeneration/parameterGenerator.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,16 +86,36 @@ export class ParameterGenerator {
8686
};
8787
}
8888

89+
private extractTsoaResponse(typeNode: ts.TypeNode | undefined): ts.TypeReferenceNode | undefined {
90+
if (!typeNode || !ts.isTypeReferenceNode(typeNode)) {
91+
return undefined;
92+
}
93+
if (typeNode.typeName.getText() === 'TsoaResponse') {
94+
return typeNode;
95+
}
96+
97+
const symbol = this.current.typeChecker.getTypeAtLocation(typeNode).aliasSymbol;
98+
if (!symbol || !symbol.declarations) {
99+
return undefined;
100+
}
101+
const declaration = symbol.declarations[0];
102+
if (!ts.isTypeAliasDeclaration(declaration) || !ts.isTypeReferenceNode(declaration.type)) {
103+
return undefined;
104+
}
105+
106+
return declaration.type.typeName.getText() === 'TsoaResponse' ? declaration.type : undefined;
107+
}
108+
89109
private getResParameters(parameter: ts.ParameterDeclaration): Tsoa.ResParameter[] {
90110
const parameterName = (parameter.name as ts.Identifier).text;
91111
const decorator = getNodeFirstDecoratorValue(this.parameter, this.current.typeChecker, ident => ident.text === 'Res') || parameterName;
92112
if (!decorator) {
93113
throw new GenerateMetadataError('Could not find Decorator', parameter);
94114
}
95115

96-
const typeNode = parameter.type;
116+
const typeNode = this.extractTsoaResponse(parameter.type);
97117

98-
if (!typeNode || !ts.isTypeReferenceNode(typeNode) || typeNode.typeName.getText() !== 'TsoaResponse') {
118+
if (!typeNode) {
99119
throw new GenerateMetadataError('@Res() requires the type to be TsoaResponse<HTTPStatusCode, ResBody>', parameter);
100120
}
101121

tests/fixtures/controllers/getController.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ import {
2020
} from '../testModel';
2121
import { ModelService } from './../services/modelService';
2222

23+
export type BadRequest = TsoaResponse<400, TestModel, { name: 'some_thing' }>;
24+
export type ForbiddenRequest = TsoaResponse<401, TestModel, { name: 'another some_thing' }>;
25+
export type BadAndInternalErrorRequest = TsoaResponse<400 | 500, TestModel, { name: 'combine' }>;
2326
export const PathFromConstant = 'PathFromConstantValue';
2427
export enum EnumPaths {
2528
PathFromEnum = 'PathFromEnumValue',
@@ -278,6 +281,14 @@ export class GetTestController extends Controller {
278281
res?.(400, new ModelService().getModel(), { 'custom-header': 'hello' });
279282
}
280283

284+
/**
285+
* @param res The alternate response
286+
*/
287+
@Get('Res_Alias')
288+
public async getResAlias(@Res() res: BadRequest): Promise<void> {
289+
res?.(400, new ModelService().getModel(), { name: 'some_thing' });
290+
}
291+
281292
/**
282293
* @param res The alternate response
283294
* @param res Another alternate response
@@ -291,6 +302,18 @@ export class GetTestController extends Controller {
291302
};
292303
}
293304

305+
/**
306+
* @param res The alternate response
307+
*/
308+
@Get('MultipleRes_Alias')
309+
public async multipleResAlias(@Res() res: BadRequest, @Res() anotherRes: ForbiddenRequest): Promise<Result> {
310+
res?.(400, new ModelService().getModel(), { name: 'some_thing' });
311+
anotherRes?.(401, new ModelService().getModel(), { name: 'another some_thing' });
312+
return {
313+
value: 'success',
314+
};
315+
}
316+
294317
/**
295318
* @param res The alternate response
296319
*/
@@ -299,6 +322,14 @@ export class GetTestController extends Controller {
299322
res?.(statusCode, new ModelService().getModel(), { 'custom-header': 'hello' });
300323
}
301324

325+
/**
326+
* @param res The alternate response
327+
*/
328+
@Get('MultipleStatusCodeRes_Alias')
329+
public async multipleStatusCodeResAlias(@Res() res: BadAndInternalErrorRequest, @Query('statusCode') statusCode: 400 | 500): Promise<void> {
330+
res?.(statusCode, new ModelService().getModel(), { name: 'combine' });
331+
}
332+
302333
@Get(PathFromConstant)
303334
public async getPathFromConstantValue(): Promise<TestModel> {
304335
return new ModelService().getModel();

tests/integration/express-server.spec.ts

Lines changed: 65 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -194,42 +194,80 @@ describe('Express Server', () => {
194194
});
195195
});
196196

197-
it('Should return on @Res', () => {
198-
return verifyGetRequest(
199-
basePath + '/GetTest/Res',
200-
(_err, res) => {
201-
const model = res.body as TestModel;
202-
expect(model.id).to.equal(1);
203-
expect(res.get('custom-header')).to.eq('hello');
204-
},
205-
400,
206-
);
207-
});
197+
describe('@Res', () => {
198+
it('Should return on @Res', () => {
199+
return verifyGetRequest(
200+
basePath + '/GetTest/Res',
201+
(_err, res) => {
202+
const model = res.body as TestModel;
203+
expect(model.id).to.equal(1);
204+
expect(res.get('custom-header')).to.eq('hello');
205+
},
206+
400,
207+
);
208+
});
209+
210+
it('Should return on @Res with alias', () => {
211+
return verifyGetRequest(
212+
basePath + '/GetTest/Res_Alias',
213+
(_err, res) => {
214+
const model = res.body as TestModel;
215+
expect(model.id).to.equal(1);
216+
expect(res.get('name')).to.equal('some_thing');
217+
},
218+
400,
219+
);
220+
});
221+
222+
[400, 500].forEach(statusCode => {
223+
it('Should support multiple status codes with the same @Res structure', () => {
224+
return verifyGetRequest(
225+
basePath + `/GetTest/MultipleStatusCodeRes?statusCode=${statusCode}`,
226+
(_err, res) => {
227+
const model = res.body as TestModel;
228+
expect(model.id).to.equal(1);
229+
expect(res.get('custom-header')).to.eq('hello');
230+
},
231+
statusCode,
232+
);
233+
});
234+
235+
it('Should support multiple status codes with the same @Res structure with alias', () => {
236+
return verifyGetRequest(
237+
basePath + `/GetTest/MultipleStatusCodeRes_Alias?statusCode=${statusCode}`,
238+
(_err, res) => {
239+
const model = res.body as TestModel;
240+
expect(model.id).to.equal(1);
241+
expect(res.get('name')).to.eq('combine');
242+
},
243+
statusCode,
244+
);
245+
});
246+
});
208247

209-
[400, 500].forEach(statusCode =>
210-
it('Should support multiple status codes with the same @Res structure', () => {
248+
it('Should not modify the response after headers sent', () => {
211249
return verifyGetRequest(
212-
basePath + `/GetTest/MultipleStatusCodeRes?statusCode=${statusCode}`,
250+
basePath + '/GetTest/MultipleRes',
213251
(_err, res) => {
214252
const model = res.body as TestModel;
215253
expect(model.id).to.equal(1);
216254
expect(res.get('custom-header')).to.eq('hello');
217255
},
218-
statusCode,
256+
400,
219257
);
220-
}),
221-
);
258+
});
222259

223-
it('Should not modify the response after headers sent', () => {
224-
return verifyGetRequest(
225-
basePath + '/GetTest/MultipleRes',
226-
(_err, res) => {
227-
const model = res.body as TestModel;
228-
expect(model.id).to.equal(1);
229-
expect(res.get('custom-header')).to.eq('hello');
230-
},
231-
400,
232-
);
260+
it('Should not modify the response after headers sent with alias', () => {
261+
return verifyGetRequest(
262+
basePath + '/GetTest/MultipleRes_Alias',
263+
(_err, res) => {
264+
const model = res.body as TestModel;
265+
expect(model.id).to.equal(1);
266+
expect(res.get('name')).to.eq('some_thing');
267+
},
268+
400,
269+
);
270+
});
233271
});
234272

235273
it('parses buffer parameter', () => {

tests/integration/hapi-server.spec.ts

Lines changed: 65 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1315,42 +1315,80 @@ describe('Hapi Server', () => {
13151315
});
13161316
});
13171317

1318-
it('Should return on @Res', () => {
1319-
return verifyGetRequest(
1320-
basePath + '/GetTest/Res',
1321-
(_err, res) => {
1322-
const model = res.body as TestModel;
1323-
expect(model.id).to.equal(1);
1324-
expect(res.get('custom-header')).to.eq('hello');
1325-
},
1326-
400,
1327-
);
1328-
});
1318+
describe('@Res', () => {
1319+
it('Should return on @Res', () => {
1320+
return verifyGetRequest(
1321+
basePath + '/GetTest/Res',
1322+
(_err, res) => {
1323+
const model = res.body as TestModel;
1324+
expect(model.id).to.equal(1);
1325+
expect(res.get('custom-header')).to.eq('hello');
1326+
},
1327+
400,
1328+
);
1329+
});
1330+
1331+
it('Should return on @Res with alias', () => {
1332+
return verifyGetRequest(
1333+
basePath + '/GetTest/Res_Alias',
1334+
(_err, res) => {
1335+
const model = res.body as TestModel;
1336+
expect(model.id).to.equal(1);
1337+
expect(res.get('name')).to.equal('some_thing');
1338+
},
1339+
400,
1340+
);
1341+
});
13291342

1330-
[400, 500].forEach(statusCode =>
1331-
it('Should support multiple status codes with the same @Res structure', () => {
1343+
[400, 500].forEach(statusCode => {
1344+
it('Should support multiple status codes with the same @Res structure', () => {
1345+
return verifyGetRequest(
1346+
basePath + `/GetTest/MultipleStatusCodeRes?statusCode=${statusCode}`,
1347+
(_err, res) => {
1348+
const model = res.body as TestModel;
1349+
expect(model.id).to.equal(1);
1350+
expect(res.get('custom-header')).to.eq('hello');
1351+
},
1352+
statusCode,
1353+
);
1354+
});
1355+
1356+
it('Should support multiple status codes with the same @Res structure with alias', () => {
1357+
return verifyGetRequest(
1358+
basePath + `/GetTest/MultipleStatusCodeRes_Alias?statusCode=${statusCode}`,
1359+
(_err, res) => {
1360+
const model = res.body as TestModel;
1361+
expect(model.id).to.equal(1);
1362+
expect(res.get('name')).to.eq('combine');
1363+
},
1364+
statusCode,
1365+
);
1366+
});
1367+
});
1368+
1369+
it('Should not modify the response after headers sent', () => {
13321370
return verifyGetRequest(
1333-
basePath + `/GetTest/MultipleStatusCodeRes?statusCode=${statusCode}`,
1371+
basePath + '/GetTest/MultipleRes',
13341372
(_err, res) => {
13351373
const model = res.body as TestModel;
13361374
expect(model.id).to.equal(1);
13371375
expect(res.get('custom-header')).to.eq('hello');
13381376
},
1339-
statusCode,
1377+
400,
13401378
);
1341-
}),
1342-
);
1379+
});
13431380

1344-
it('Should not modify the response after headers sent', () => {
1345-
return verifyGetRequest(
1346-
basePath + '/GetTest/MultipleRes',
1347-
(_err, res) => {
1348-
const model = res.body as TestModel;
1349-
expect(model.id).to.equal(1);
1350-
expect(res.get('custom-header')).to.eq('hello');
1351-
},
1352-
400,
1353-
);
1381+
it('Should not modify the response after headers sent with alias', () => {
1382+
return verifyGetRequest(
1383+
basePath + '/GetTest/MultipleRes_Alias',
1384+
(_err, res) => {
1385+
const model = res.body as TestModel;
1386+
expect(model.id).to.equal(1);
1387+
expect(res.get('name')).to.eq('some_thing');
1388+
},
1389+
400,
1390+
);
1391+
});
13541392
});
13551393
});
13561394

0 commit comments

Comments
 (0)