Skip to content

Commit 982504a

Browse files
committed
Increase test coverage
1 parent bd07bee commit 982504a

File tree

10 files changed

+353
-65
lines changed

10 files changed

+353
-65
lines changed

src/service/emit/service/service.generator.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ export class ServiceGenerator {
5151
}
5252

5353
(op.parameters ?? []).forEach(param => {
54-
const schemaObject = param.schema ? param.schema : param;
55-
const paramType = getTypeScriptType(schemaObject as any, this.config, knownTypes).replace(/\[\]| \| null/g, '');
54+
// The extractPaths function ensures that param.schema is always populated.
55+
const paramType = getTypeScriptType(param.schema as any, this.config, knownTypes).replace(/\[\]| \| null/g, '');
5656
if (isDataTypeInterface(paramType)) {
5757
modelImports.add(paramType);
5858
}

tests/00-core/02-utils-coverage.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ describe('Core: utils.ts (Coverage)', () => {
118118
const expected = "{ 'required-prop': string; 'optional-prop'?: string }";
119119
expect(utils.getTypeScriptType(schema, config, [])).toBe(expected);
120120
});
121+
122+
it('should correctly handle an integer schema type', () => {
123+
const schema: SwaggerDefinition = { type: 'integer' };
124+
expect(utils.getTypeScriptType(schema, config, [])).toBe('number');
125+
});
121126
});
122127

123128
describe('extractPaths', () => {
@@ -188,4 +193,9 @@ describe('Core: utils.ts (Coverage)', () => {
188193
const schema: SwaggerDefinition = { type: 'boolean' };
189194
expect(utils.getTypeScriptType(schema, config, [])).toBe('boolean');
190195
});
196+
197+
it('should handle single quotes in enum values', () => {
198+
const schema: SwaggerDefinition = { type: 'string', enum: ["it's a value"] };
199+
expect(utils.getTypeScriptType(schema, config, [])).toBe(`'it\\'s a value'`);
200+
});
191201
});

tests/30-emit-service/01-service-method-generator.spec.ts

Lines changed: 50 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,32 @@ const serviceMethodGenSpec = {
4141
parameters: [{ name: 'limit', in: 'query', type: 'integer' }] // No 'schema' key
4242
}
4343
},
44-
'/post-no-req-schema': {
44+
// NEW paths for coverage
45+
'/post-infer-return': {
46+
post: {
47+
tags: ['ResponseType'],
48+
operationId: 'postInferReturn',
49+
requestBody: { content: { 'application/json': { schema: { $ref: '#/components/schemas/BodyModel' } } } },
50+
responses: { '400': { description: 'Bad Request' } } // No 2xx response
51+
}
52+
},
53+
'/body-no-schema': {
4554
post: {
46-
operationId: 'postNoReqSchema',
4755
tags: ['ResponseType'],
56+
operationId: 'postBodyNoSchema',
4857
requestBody: { content: { 'application/json': {} } }, // Body exists, but no schema
4958
responses: { '204': {} }
5059
}
5160
},
52-
// NEW paths for coverage
61+
'/multipart-no-params': {
62+
post: {
63+
tags: ['FormData'],
64+
operationId: 'postMultipartNoParams',
65+
consumes: ['multipart/form-data'],
66+
// No `parameters` array with `in: 'formData'`
67+
responses: { '200': {} }
68+
}
69+
},
5370
'/all-required/{id}': {
5471
post: {
5572
tags: ['RequiredParams'],
@@ -172,9 +189,8 @@ describe('Emitter: ServiceMethodGenerator', () => {
172189
});
173190

174191
describe('Parameter and Body Generation', () => {
175-
const { methodGen, serviceClass, parser } = createTestEnvironment();
176-
177192
it('should handle multipart/form-data', () => {
193+
const { methodGen, serviceClass, parser } = createTestEnvironment();
178194
const op = parser.operations.find(o => o.operationId === 'postMultipart')!;
179195
op.methodName = 'postMultipart';
180196
methodGen.addServiceMethod(serviceClass, op);
@@ -184,7 +200,19 @@ describe('Emitter: ServiceMethodGenerator', () => {
184200
expect(body).toContain("return this.http.post(url, formData, requestOptions);");
185201
});
186202

203+
it('should handle multipart/form-data with no formData params', () => {
204+
const { methodGen, serviceClass, parser } = createTestEnvironment();
205+
const op = parser.operations.find(o => o.operationId === 'postMultipartNoParams')!;
206+
op.methodName = 'postMultipartNoParams';
207+
methodGen.addServiceMethod(serviceClass, op);
208+
const body = serviceClass.getMethodOrThrow('postMultipartNoParams').getBodyText()!;
209+
// It should not generate FormData logic and fall back to a null body.
210+
expect(body).not.toContain('new FormData()');
211+
expect(body).toContain('return this.http.post(url, null, requestOptions);');
212+
});
213+
187214
it('should handle application/x-www-form-urlencoded', () => {
215+
const { methodGen, serviceClass, parser } = createTestEnvironment();
188216
const op = parser.operations.find(o => o.operationId === 'postUrlencoded')!;
189217
op.methodName = 'postUrlencoded';
190218
methodGen.addServiceMethod(serviceClass, op);
@@ -195,6 +223,7 @@ describe('Emitter: ServiceMethodGenerator', () => {
195223
});
196224

197225
it('should handle Swagger 2.0 style parameters (without schema wrapper)', () => {
226+
const { methodGen, serviceClass, parser } = createTestEnvironment();
198227
const op = parser.operations.find(o => o.operationId === 'getWithSwagger2Param')!;
199228
op.methodName = 'getWithSwagger2Param';
200229
methodGen.addServiceMethod(serviceClass, op);
@@ -204,6 +233,7 @@ describe('Emitter: ServiceMethodGenerator', () => {
204233
});
205234

206235
it('should name the body parameter after the model type if it is an interface', () => {
236+
const { methodGen, serviceClass, parser } = createTestEnvironment();
207237
const op = parser.operations.find(o => o.operationId === 'postAndReturn')!;
208238
op.methodName = 'postAndReturn';
209239
methodGen.addServiceMethod(serviceClass, op);
@@ -212,23 +242,13 @@ describe('Emitter: ServiceMethodGenerator', () => {
212242
expect(param?.getType().getText()).toBe('BodyModel');
213243
});
214244

215-
it('should name the body parameter `body` for primitive types', () => {
216-
const operation = parser.operations.find(op => op.operationId === 'primitiveBody')!;
217-
operation.methodName = 'primitiveBody';
218-
methodGen.addServiceMethod(serviceClass, operation);
219-
const method = serviceClass.getMethodOrThrow('primitiveBody');
220-
const impl = method.getImplementation()!;
221-
const param = impl.getParameters().find(p => p.getType().getText() === 'string');
222-
expect(param?.getName()).toBe('body');
223-
});
224-
225-
it('should handle request body without a schema', () => {
245+
it('should handle request body without a schema by creating an "unknown" body param', () => {
226246
const { methodGen, serviceClass, parser } = createTestEnvironment();
227-
const op = parser.operations.find(o => o.operationId === 'postNoReqSchema')!;
228-
op.methodName = 'postNoReqSchema';
247+
const op = parser.operations.find(o => o.operationId === 'postBodyNoSchema')!;
248+
op.methodName = 'postBodyNoSchema';
229249
methodGen.addServiceMethod(serviceClass, op);
230250

231-
const method = serviceClass.getMethodOrThrow('postNoReqSchema');
251+
const method = serviceClass.getMethodOrThrow('postBodyNoSchema');
232252
const bodyParam = method.getParameters().find(p => p.getName() === 'body')!;
233253
expect(bodyParam).toBeDefined();
234254
expect(bodyParam.getType().getText()).toBe('unknown');
@@ -248,60 +268,37 @@ describe('Emitter: ServiceMethodGenerator', () => {
248268
});
249269

250270
describe('Response Type Resolution', () => {
251-
const { methodGen, serviceClass, parser } = createTestEnvironment();
252-
253-
it('should fall back to "any" when a POST has a request body but no schema', () => {
254-
const op = parser.operations.find(o => o.operationId === 'postNoReqSchema')!;
255-
op.methodName = 'postNoReqSchema';
256-
// It has a 204 response, which means `getResponseType` will return `void`.
257-
// Let's modify it to have no responses so it falls through.
258-
op.responses = {};
271+
it('should infer response type from request body on POST when no success response is defined', () => {
272+
const { methodGen, serviceClass, parser } = createTestEnvironment();
273+
const op = parser.operations.find(o => o.operationId === 'postInferReturn')!;
274+
op.methodName = 'postInferReturn';
259275
methodGen.addServiceMethod(serviceClass, op);
260-
const overload = serviceClass.getMethodOrThrow('postNoReqSchema').getOverloads()[0];
261-
expect(overload.getReturnType().getText()).toBe('Observable<any>');
262-
});
263-
264-
it('should correctly determine response type from requestBody for POST/PUT', () => {
265-
const operation = parser.operations.find(op => op.operationId === 'postAndReturn')!;
266-
operation.methodName = 'postAndReturn';
267-
methodGen.addServiceMethod(serviceClass, operation);
268-
const overload = serviceClass.getMethodOrThrow('postAndReturn').getOverloads()[0];
276+
const overload = serviceClass.getMethodOrThrow('postInferReturn').getOverloads()[0];
269277
expect(overload.getReturnType().getText()).toBe('Observable<BodyModel>');
270278
});
271279

272-
it("should fall back to 'any' for responseType when no success response is defined", () => {
273-
const operation = parser.operations.find(op => op.operationId === 'getOAS2NoSchema')!;
280+
it("should fall back to 'any' for responseType when no success response or request body schema is defined", () => {
281+
const { methodGen, serviceClass, parser } = createTestEnvironment();
282+
const operation = parser.operations.find(o => o.operationId === 'getOAS2NoSchema')!;
274283
operation.methodName = 'getOAS2NoSchema';
275284
methodGen.addServiceMethod(serviceClass, operation);
276285
const overload = serviceClass.getMethodOrThrow('getOAS2NoSchema').getOverloads()[0];
277286
expect(overload.getReturnType().getText()).toBe('Observable<any>');
278287
});
279288
});
280289

281-
it('should fall back to a generic `body: unknown` parameter for non-json content', () => {
282-
const { methodGen, serviceClass, parser } = createTestEnvironment();
283-
const operation = parser.operations.find(op => op.operationId === 'allParams')!;
284-
operation.methodName = 'allParams';
285-
methodGen.addServiceMethod(serviceClass, operation);
286-
const method = serviceClass.getMethodOrThrow('allParams');
287-
const impl = method.getImplementation()!;
288-
const param = impl.getParameters().find(p => p.getName() === 'body');
289-
expect(param?.getType().getText()).toBe('unknown');
290-
});
291-
292-
it('should generate query param logic when query params are present', () => {
290+
it('should generate query param logic with correct nullish coalescing for options.params', () => {
293291
const { methodGen, serviceClass, parser } = createTestEnvironment();
294292
const operation = parser.operations.find(op => op.operationId === 'withQuery')!;
295293
operation.methodName = 'withQuery';
296294
methodGen.addServiceMethod(serviceClass, operation);
297-
const method = serviceClass.getMethodOrThrow('withQuery');
298-
const body = method.getImplementation()?.getBodyText() ?? '';
295+
const body = serviceClass.getMethodOrThrow('withQuery').getImplementation()?.getBodyText() ?? '';
299296
expect(body).toContain(`let params = new HttpParams({ fromObject: options?.params ?? {} });`);
300297
expect(body).toContain(`if (search != null) { params = HttpParamsBuilder.addToHttpParams(params, search, 'search'); }`);
301298
expect(body).toContain(`params,`);
302299
});
303300

304-
it('should generate header param logic when header params are present', () => {
301+
it('should generate header param logic correctly', () => {
305302
const { methodGen, serviceClass, parser } = createTestEnvironment();
306303
const operation = parser.operations.find(op => op.operationId === 'withHeader')!;
307304
operation.methodName = 'withHeader';
@@ -312,13 +309,4 @@ describe('Emitter: ServiceMethodGenerator', () => {
312309
expect(body).toContain(`if (xCustomHeader != null) { headers = headers.set('X-Custom-Header', String(xCustomHeader)); }`);
313310
expect(body).toContain(`headers,`);
314311
});
315-
316-
it("should handle OAS2 `type: 'file'` by creating an 'any' type parameter", () => {
317-
const { methodGen, serviceClass, parser } = createTestEnvironment();
318-
const operation = parser.operations.find(op => op.operationId === 'uploadFile')!;
319-
operation.methodName = 'uploadFile';
320-
methodGen.addServiceMethod(serviceClass, operation);
321-
const param = serviceClass.getMethodOrThrow('uploadFile').getParameters().find(p => p.getName() === 'file');
322-
expect(param?.getType().getText()).toBe('any');
323-
});
324312
});

tests/30-emit-service/02-coverage.spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ describe('Emitter: Service Generators (Coverage)', () => {
2929
return project;
3030
};
3131

32+
it('should import models for parameter types that are interfaces', () => {
33+
const project = run(branchCoverageSpec);
34+
const serviceFile = project.getSourceFileOrThrow('/out/services/paramIsRef.service.ts');
35+
const modelImport = serviceFile.getImportDeclaration(imp => imp.getModuleSpecifierValue() === '../models');
36+
expect(modelImport).toBeDefined();
37+
expect(modelImport!.getNamedImports().map(i => i.getName())).toContain('User');
38+
});
39+
3240
it('should generate methods for multipart/form-data', () => {
3341
const project = run(coverageSpecPart2);
3442
const serviceFile = project.getSourceFileOrThrow('/out/services/formData.service.ts');

tests/40-emit-utility/06-provider-generator.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { AuthTokensGenerator } from '../../src/service/emit/utility/auth-tokens.
99
import { AuthInterceptorGenerator } from '../../src/service/emit/utility/auth-interceptor.generator.js';
1010
import { DateTransformerGenerator } from '../../src/service/emit/utility/date-transformer.generator.js';
1111
import { emptySpec, securitySpec } from '../shared/specs.js';
12+
import { createTestProject } from '../shared/helpers.js';
1213

1314
describe('Emitter: ProviderGenerator', () => {
1415
const runGenerator = (spec: object, config: Partial<GeneratorConfig> = {}) => {
@@ -123,4 +124,20 @@ describe('Emitter: ProviderGenerator', () => {
123124
);
124125
expect(fileContent).not.toContain('API_KEY_TOKEN');
125126
});
127+
128+
it('should return early if generateServices is explicitly false', () => {
129+
const project = createTestProject();
130+
const config: GeneratorConfig = {
131+
input: '',
132+
output: '/out',
133+
options: { generateServices: false, dateType: 'string', enumStyle: 'enum' },
134+
};
135+
const parser = new SwaggerParser(emptySpec as any, config);
136+
const generator = new ProviderGenerator(parser, project, []);
137+
138+
generator.generate('/out');
139+
140+
// If the 'return' statement was hit, the file should not have been created.
141+
expect(project.getSourceFile('/out/providers.ts')).toBeUndefined();
142+
});
126143
});

tests/50-emit-admin/08-coverage.spec.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,62 @@ describe('Admin Generators (Coverage)', () => {
5858
// This ensures the `if (hasActions)` branch is correctly NOT taken.
5959
expect(displayedColumns).not.toContain('actions');
6060
});
61+
62+
it('list-component-generator handles listable resource with no actions', () => {
63+
// This spec defines a resource that can be listed but has no edit/delete/custom actions.
64+
const spec = {
65+
paths: {
66+
'/reports': { get: { tags: ['Reports'], responses: { '200': { description: 'ok' } } } }
67+
}
68+
};
69+
const project = createTestProject();
70+
const parser = new SwaggerParser(spec as any, { options: { admin: true } } as any);
71+
const resource = discoverAdminResources(parser).find(r => r.name === 'reports')!;
72+
const generator = new ListComponentGenerator(project);
73+
74+
generator.generate(resource, '/admin');
75+
76+
const listClass = project.getSourceFileOrThrow('/admin/reports/reports-list/reports-list.component.ts').getClassOrThrow('ReportsListComponent');
77+
const displayedColumns = listClass.getProperty('displayedColumns')?.getInitializer()?.getText() as string;
78+
79+
// This ensures the `if (hasActions)` branch is correctly NOT taken.
80+
expect(displayedColumns).not.toContain('actions');
81+
});
82+
83+
it('list-component-generator handles resource with no actions and non-id primary key', () => {
84+
const spec = {
85+
paths: {
86+
'/diagnostics': {
87+
get: {
88+
tags: ['Diagnostics'],
89+
responses: {
90+
'200': {
91+
content: {
92+
'application/json': { schema: { $ref: '#/components/schemas/DiagnosticInfo' } }
93+
}
94+
}
95+
}
96+
}
97+
}
98+
},
99+
components: {
100+
schemas: {
101+
DiagnosticInfo: {
102+
type: 'object',
103+
properties: { event_id: { type: 'string' }, message: { type: 'string' } }
104+
}
105+
}
106+
}
107+
};
108+
const project = createTestProject();
109+
const parser = new SwaggerParser(spec as any, { options: { admin: true } } as any);
110+
const resource = discoverAdminResources(parser).find(r => r.name === 'diagnostics')!;
111+
const generator = new ListComponentGenerator(project);
112+
generator.generate(resource, '/admin');
113+
const listClass = project.getSourceFileOrThrow('/admin/diagnostics/diagnostics-list/diagnostics-list.component.ts').getClassOrThrow('DiagnosticsListComponent');
114+
const displayedColumns = listClass.getProperty('displayedColumns')?.getInitializer()?.getText() as string;
115+
expect(displayedColumns).not.toContain('actions');
116+
const idProperty = listClass.getProperty('idProperty')?.getInitializer()?.getText() as string;
117+
expect(idProperty).toBe(`'event_id'`);
118+
});
61119
});

0 commit comments

Comments
 (0)