Skip to content

Commit 167e694

Browse files
committed
Increase test coverage
1 parent 3f09851 commit 167e694

File tree

9 files changed

+115
-20
lines changed

9 files changed

+115
-20
lines changed

src/service/emit/admin/html/form-component-html.builder.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,39 @@ export function generateFormComponentHtml(resource: Resource, parser: SwaggerPar
6464
const typeName = option.name; // e.g., 'cat' or 'dog'
6565
const subSchema = option.schema;
6666

67+
// ====================================================================
68+
// THE FIX: Create a helper function to recursively merge properties from `allOf`.
69+
// This ensures inherited properties (like `name` from `BasePet`) are included.
70+
// ====================================================================
71+
const getAllProperties = (schema: SwaggerDefinition): Record<string, SwaggerDefinition> => {
72+
let allProperties: Record<string, SwaggerDefinition> = { ...schema.properties };
73+
if (schema.allOf) {
74+
for (const sub of schema.allOf) {
75+
const resolvedSub = parser.resolve<SwaggerDefinition>(sub);
76+
if (resolvedSub) {
77+
const subProps = getAllProperties(resolvedSub); // Recurse
78+
// Merge base properties first, so subtype properties take precedence
79+
allProperties = { ...subProps, ...allProperties };
80+
}
81+
}
82+
}
83+
return allProperties;
84+
};
85+
86+
6787
// Create a container that will be controlled by an Angular `@if` block.
6888
// This relies on the `isPetType(type: string)` method existing in the component class.
6989
const ifContainer = _.create('div');
7090

7191
// Create the sub-form container with the `formGroupName` directive.
7292
const formGroupContainer = _.create('div').setAttribute('formGroupName', typeName);
7393

94+
// Use the helper to get all properties, including inherited ones.
95+
const allSubSchemaProperties = getAllProperties(subSchema);
96+
7497
// Generate controls for all properties of this specific subtype.
7598
// It is crucial to filter out the discriminator property itself to avoid rendering it twice.
76-
Object.entries(subSchema.properties!)
99+
Object.entries(allSubSchemaProperties)
77100
.filter(([key, schema]) => key !== dPropName && !schema.readOnly)
78101
.forEach(([key, schema]) => {
79102
const control = buildFormControl({ name: key, schema: schema as SwaggerDefinition });
@@ -106,9 +129,9 @@ export function generateFormComponentHtml(resource: Resource, parser: SwaggerPar
106129
.setAttribute('[disabled]', 'form.invalid || form.pristine');
107130

108131
// Dynamically change the save button's text based on whether we are creating or editing.
109-
const saveButtonContent = `\n@if (isEditMode()) {
132+
const saveButtonContent = `\n@if (isEditMode()) {
110133
<span>Save Changes</span>
111-
} @else {
134+
} @else {
112135
<span>Create ${resource.modelName}</span>
113136
}\n`;
114137
saveButton.setInnerHtml(saveButtonContent);

src/service/emit/admin/list-component.generator.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -307,9 +307,8 @@ this.subscriptions.push(sub);`;
307307
if (allProps.some(p => p.name === 'id')) {
308308
return 'id';
309309
}
310-
if (allProps.length === 0) {
311-
return 'id';
312-
}
310+
// `resource.formProperties` is guaranteed by the discovery phase
311+
// to have at least one property (falling back to a default 'id').
313312
return allProps[0].name;
314313
}
315314

src/service/emit/utility/auth-interceptor.generator.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// src/service/emit/utility/auth-interceptor.generator.ts
12
import * as path from 'path';
23
import { Project, Scope } from 'ts-morph';
34
import { SwaggerParser } from '../../../core/parser.js';
@@ -93,6 +94,7 @@ export class AuthInterceptorGenerator {
9394
}
9495

9596
let statementsBody = 'let authReq = req;';
97+
let bearerLogicAdded = false;
9698

9799
const uniqueSchemes = Array.from(new Set(securitySchemes.map(s => JSON.stringify(s)))).map(s => JSON.parse(s));
98100

@@ -104,8 +106,9 @@ export class AuthInterceptorGenerator {
104106
statementsBody += `\nif (this.apiKey) { authReq = authReq.clone({ setParams: { ...authReq.params.keys().reduce((acc, key) => ({ ...acc, [key]: authReq.params.getAll(key) }), {}), '${scheme.name}': this.apiKey } }); }`;
105107
}
106108
} else if ((scheme.type === 'http' && scheme.scheme === 'bearer') || scheme.type === 'oauth2') {
107-
if (!statementsBody.includes('this.bearerToken')) {
109+
if (!bearerLogicAdded) {
108110
statementsBody += `\nif (this.bearerToken) { const token = typeof this.bearerToken === 'function' ? this.bearerToken() : this.bearerToken; if (token) { authReq = authReq.clone({ setHeaders: { ...authReq.headers.keys().reduce((acc, key) => ({ ...acc, [key]: authReq.headers.getAll(key) }), {}), 'Authorization': \`Bearer \${token}\` } }); } }`;
111+
bearerLogicAdded = true;
109112
}
110113
}
111114
}

src/service/emit/utility/index.generator.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// src/service/emit/utility/index.generator.ts
12
import { Project, SourceFile } from "ts-morph";
23
import * as path from "node:path";
34
import { GeneratorConfig } from '../../../core/types.js';
@@ -103,11 +104,11 @@ export class ServiceIndexGenerator {
103104

104105
for (const serviceFile of serviceFiles) {
105106
const serviceClass = serviceFile.getClasses()[0];
106-
if (serviceClass && serviceClass.isExported()) {
107-
const className = serviceClass.getName();
107+
const className = serviceClass?.getName();
108+
if (serviceClass && serviceClass.isExported() && className) {
108109
const moduleSpecifier = `./${path.basename(serviceFile.getFilePath(), '.ts')}`;
109110
sourceFile.addExportDeclaration({
110-
namedExports: [className!],
111+
namedExports: [className],
111112
moduleSpecifier,
112113
});
113114
}

src/service/emit/utility/provider.generator.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// src/service/emit/utility/provider.generator.ts
12
import { Project, SourceFile, InterfaceDeclaration } from 'ts-morph';
23
import * as path from 'path';
34
import { GeneratorConfig } from '../../../core/types.js';
@@ -25,7 +26,7 @@ export class ProviderGenerator {
2526
*/
2627
constructor(private parser: SwaggerParser, private project: Project, private tokenNames: string[] = []) {
2728
this.config = parser.config;
28-
this.clientName = this.config.clientName || 'default';
29+
this.clientName = this.config.clientName ? this.config.clientName : 'default';
2930
this.capitalizedClientName = pascalCase(this.clientName);
3031
this.hasApiKey = this.tokenNames.includes('apiKey');
3132
this.hasBearer = this.tokenNames.includes('bearerToken');

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, it, expect } from 'vitest';
22
import * as utils from '../../src/core/utils.js';
3-
import { GeneratorConfig } from '../../src/core/types.js';
3+
import { GeneratorConfig, SwaggerDefinition } from '../../src/core/types.js';
44
import { MethodDeclaration } from 'ts-morph';
55

66
describe('Core: utils.ts', () => {
@@ -42,10 +42,9 @@ describe('Core: utils.ts', () => {
4242
});
4343

4444
describe('Type Resolution', () => {
45-
it('should return "Record<string, any>" for object without properties', () => {
46-
const schema = { type: 'object' };
47-
const type = utils.getTypeScriptType(schema as any, config);
48-
expect(type).toBe('Record<string, any>');
45+
it('should return `Record<string, any>` for an object schema with no properties', () => {
46+
const schema: SwaggerDefinition = { type: 'object' };
47+
expect(utils.getTypeScriptType(schema, config, [])).toBe('Record<string, any>');
4948
});
5049

5150
it('should return "any" for unknown schema types', () => {

tests/50-emit-admin/09-resource-discovery-coverage.spec.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ describe('Admin: resource-discovery (Coverage)', () => {
3131
expect(prop?.schema.properties).toHaveProperty('prop');
3232
});
3333

34-
3534
it('should not classify custom actions like "addItem" as a standard "create"', () => {
3635
const resources = runDiscovery(branchCoverageSpec);
3736
const resource = resources.find(r => r.name === 'widgets')!;
@@ -57,4 +56,53 @@ describe('Admin: resource-discovery (Coverage)', () => {
5756
expect(resource.formProperties.length).toBeGreaterThan(0);
5857
expect(resource.formProperties[0].name).toBe('name');
5958
});
59+
60+
it('should collect properties from swagger 2.0 formData parameters', () => {
61+
const spec = {
62+
swagger: '2.0',
63+
paths: {
64+
'/form-data': {
65+
post: {
66+
tags: ['FormData'],
67+
consumes: ['multipart/form-data'],
68+
parameters: [
69+
{ name: 'file', in: 'formData', type: 'file' },
70+
{ name: 'metadata', in: 'formData', type: 'string' }
71+
]
72+
}
73+
}
74+
}
75+
};
76+
const resources = runDiscovery(spec);
77+
const resource = resources.find(r => r.name === 'formData');
78+
expect(resource).toBeDefined();
79+
// formDataProperties are merged into the final list.
80+
expect(resource!.formProperties.some(p => p.name === 'file')).toBe(true);
81+
expect(resource!.formProperties.some(p => p.name === 'metadata')).toBe(true);
82+
});
83+
84+
it('should handle unresolvable schemas gracefully', () => {
85+
const spec = {
86+
paths: {
87+
'/bad-ref': {
88+
get: {
89+
tags: ['BadRef'],
90+
responses: { '200': { content: { 'application/json': { schema: { $ref: '#/non/existent' } } } } }
91+
}
92+
}
93+
}
94+
};
95+
const resources = runDiscovery(spec);
96+
const resource = resources.find(r => r.name === 'badRef');
97+
expect(resource).toBeDefined();
98+
// The discovery should not crash and should produce a resource with fallback properties.
99+
expect(resource!.formProperties).toEqual([{ name: 'id', schema: { type: 'string' } }]);
100+
});
101+
102+
it('should correctly identify a resource with PATCH as editable', () => {
103+
const resources = runDiscovery(branchCoverageSpec);
104+
const resource = resources.find(r => r.name === 'patchResource');
105+
expect(resource).toBeDefined();
106+
expect(resource!.isEditable).toBe(true);
107+
});
60108
});

tests/60-e2e/01-admin-full.e2e.spec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ describe('E2E: Admin UI Generation', () => {
7777
expect(html).toContain("@if (isPetType('dog'))");
7878
expect(html).toContain('formGroupName="dog"');
7979
expect(html).toContain('formControlName="barkingLevel"');
80+
81+
expect(html).toContain("@if (isPetType('lizard'))");
82+
expect(html).toContain('formGroupName="lizard"');
83+
expect(html).toContain('formControlName="name"'); // from BasePet
84+
expect(html).not.toContain('formControlName="unsupportedField"');
8085
});
8186
});
8287
});

tests/shared/specs.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ export const polymorphismSpec = {
396396
schemas: {
397397
Pet: {
398398
type: 'object', required: ['petType'],
399-
oneOf: [{ $ref: '#/components/schemas/Cat' }, { $ref: '#/components/schemas/Dog' }],
399+
oneOf: [{ $ref: '#/components/schemas/Cat' }, { $ref: '#/components/schemas/Dog' }, { $ref: '#/components/schemas/Lizard' }],
400400
discriminator: { propertyName: 'petType' },
401401
properties: {
402402
petType: { type: 'string' }
@@ -418,7 +418,16 @@ export const polymorphismSpec = {
418418
required: ['petType'],
419419
properties: { petType: { type: 'string', enum: ['dog'] }, barkingLevel: { type: 'integer' } }
420420
},
421-
BasePet: { type: 'object', properties: { name: { type: 'string' } } }
421+
BasePet: { type: 'object', properties: { name: { type: 'string' } } },
422+
Lizard: {
423+
type: 'object',
424+
allOf: [{ $ref: '#/components/schemas/BasePet' }],
425+
required: ['petType'],
426+
properties: {
427+
petType: { type: 'string', enum: ['lizard'] },
428+
unsupportedField: { type: 'object' } // This will not generate a control
429+
}
430+
}
422431
}
423432
}
424433
};
@@ -502,7 +511,14 @@ export const finalCoverageSpec = {
502511
operationId: 'getOAS2NoSchema',
503512
responses: { '200': { description: 'Success' } }
504513
}
505-
}
514+
},
515+
'/patch-resource/{id}': {
516+
patch: {
517+
tags: ['PatchResource'],
518+
parameters: [{ name: 'id', in: 'path' }],
519+
requestBody: { content: { 'application/json': { schema: { type: 'object' } } } }
520+
}
521+
},
506522
},
507523
components: {
508524
schemas: {

0 commit comments

Comments
 (0)