11import * as path from 'path' ;
2- import { Project , ClassDeclaration , SourceFile } from 'ts-morph' ;
2+ import { Project , SourceFile } from 'ts-morph' ;
33import { SwaggerParser } from '../../../core/parser.js' ;
44import { GeneratorConfig , PathInfo } from '../../../core/types.js' ;
55import {
@@ -11,6 +11,12 @@ import {
1111} from '../../../core/utils.js' ;
1212import { MockDataGenerator } from './mock-data.generator.js' ;
1313
14+ /**
15+ * Generates Angular service test files (`.spec.ts`) for each generated service.
16+ * This class creates a standard Angular testing setup with `TestBed`, mocks for `HttpClient`,
17+ * and generates `describe` and `it` blocks for each service method, covering both
18+ * success and error scenarios.
19+ */
1420export class ServiceTestGenerator {
1521 private mockDataGenerator : MockDataGenerator ;
1622
@@ -22,6 +28,12 @@ export class ServiceTestGenerator {
2228 this . mockDataGenerator = new MockDataGenerator ( parser ) ;
2329 }
2430
31+ /**
32+ * Generates a complete test file for a single service (controller).
33+ * @param controllerName The PascalCase name of the controller (e.g., 'Users').
34+ * @param operations The list of operations belonging to this controller.
35+ * @param outputDir The directory where the `services` folder is located.
36+ */
2537 public generateServiceTestFile ( controllerName : string , operations : PathInfo [ ] , outputDir : string ) : void {
2638 const serviceName = `${ pascalCase ( controllerName ) } Service` ;
2739 const testFileName = `${ camelCase ( controllerName ) } .service.spec.ts` ;
@@ -63,6 +75,12 @@ export class ServiceTestGenerator {
6375 sourceFile . formatText ( ) ;
6476 }
6577
78+ /**
79+ * Generates the `describe` and `it` blocks for each method in a service.
80+ * @param operations The operations to generate tests for.
81+ * @returns An array of strings, each representing a line in the generated test file.
82+ * @private
83+ */
6684 private generateMethodTests ( operations : PathInfo [ ] ) : string [ ] {
6785 const tests : string [ ] = [ ] ;
6886 for ( const op of operations ) {
@@ -87,16 +105,19 @@ export class ServiceTestGenerator {
87105 // Happy Path Test
88106 tests . push ( ` it('should return ${ responseType } on success', () => {` ) ;
89107 if ( responseModel ) {
90- const mockResponse = this . mockDataGenerator . generate ( responseModel ) ;
91- tests . push ( ` const mockResponse: ${ responseModel } = ${ mockResponse } ;` ) ;
108+ const singleMock = this . mockDataGenerator . generate ( responseModel ) ;
109+ // FIX: Check if the full response type is an array and wrap the mock data accordingly.
110+ const isArray = responseType . endsWith ( '[]' ) ;
111+ const mockResponse = isArray ? `[${ singleMock } ]` : singleMock ;
112+ tests . push ( ` const mockResponse: ${ responseType } = ${ mockResponse } ;` ) ;
92113 } else {
93114 tests . push ( ` const mockResponse = null;` ) ;
94115 }
95116 if ( bodyParam ?. model ) {
96117 const mockBody = this . mockDataGenerator . generate ( bodyParam . model ) ;
97118 tests . push ( ` const ${ bodyParam . name } : ${ bodyParam . model } = ${ mockBody } ;` ) ;
98119 } else if ( bodyParam ) {
99- tests . push ( ` const ${ bodyParam . name } = 'test-body';` ) ;
120+ tests . push ( ` const ${ bodyParam . name } = { data: 'test-body' } ;` ) ;
100121 }
101122
102123 params . forEach ( p => tests . push ( ` const ${ p . name } = ${ p . value } ;` ) ) ;
@@ -122,7 +143,7 @@ export class ServiceTestGenerator {
122143 const mockBody = this . mockDataGenerator . generate ( bodyParam . model ) ;
123144 tests . push ( ` const ${ bodyParam . name } : ${ bodyParam . model } = ${ mockBody } ;` ) ;
124145 } else if ( bodyParam ) {
125- tests . push ( ` const ${ bodyParam . name } = 'test-body';` ) ;
146+ tests . push ( ` const ${ bodyParam . name } = { data: 'test-body' } ;` ) ;
126147 }
127148 params . forEach ( p => tests . push ( ` const ${ p . name } = ${ p . value } ;` ) ) ;
128149
@@ -142,6 +163,12 @@ export class ServiceTestGenerator {
142163 return tests ;
143164 }
144165
166+ /**
167+ * Adds all necessary import statements to the test file.
168+ * @param sourceFile The ts-morph SourceFile object.
169+ * @param serviceName The name of the service class being tested.
170+ * @param modelImports A list of model names that need to be imported.
171+ */
145172 private addImports ( sourceFile : SourceFile , serviceName : string , modelImports : string [ ] ) : void {
146173 sourceFile . addImportDeclarations ( [
147174 { moduleSpecifier : '@angular/core/testing' , namedImports : [ 'TestBed' ] } ,
@@ -152,19 +179,33 @@ export class ServiceTestGenerator {
152179 ] ) ;
153180 }
154181
182+ /**
183+ * Extracts the TypeScript type names for a method's response and request body.
184+ * @param op The operation to analyze.
185+ * @returns An object containing the model names for the response and body, if they are complex types.
186+ * @private
187+ */
155188 private getMethodTypes ( op : PathInfo ) : { responseModel ?: string , responseType : string , bodyModel ?: string } {
156189 const knownTypes = this . parser . schemas . map ( s => s . name ) ;
157190 const successResponseSchema = op . responses ?. [ '200' ] ?. content ?. [ 'application/json' ] ?. schema ;
158191 const responseType = successResponseSchema ? getTypeScriptType ( successResponseSchema as any , this . config , knownTypes ) : 'any' ;
159- const responseModel = isDataTypeInterface ( responseType . replace ( / \[ \] | \| n u l l / g, '' ) ) ? responseType . replace ( / \[ \] | \| n u l l / g, '' ) : undefined ;
192+ const responseModelType = responseType . replace ( / \[ \] | \| n u l l / g, '' ) ;
193+ const responseModel = isDataTypeInterface ( responseModelType ) ? responseModelType : undefined ;
160194
161195 const requestBodySchema = op . requestBody ?. content ?. [ 'application/json' ] ?. schema ;
162196 const bodyType = requestBodySchema ? getTypeScriptType ( requestBodySchema as any , this . config , knownTypes ) : 'any' ;
163- const bodyModel = isDataTypeInterface ( bodyType . replace ( / \[ \] | \| n u l l / g, '' ) ) ? bodyType . replace ( / \[ \] | \| n u l l / g, '' ) : undefined ;
197+ const bodyModelType = bodyType . replace ( / \[ \] | \| n u l l / g, '' ) ;
198+ const bodyModel = isDataTypeInterface ( bodyModelType ) ? bodyModelType : undefined ;
164199
165200 return { responseModel, responseType, bodyModel } ;
166201 }
167202
203+ /**
204+ * Scans all operations for a service to collect a unique set of model names that need to be imported.
205+ * @param operations The list of operations for the service.
206+ * @returns A Set containing the names of all required model imports.
207+ * @private
208+ */
168209 private collectModelImports ( operations : PathInfo [ ] ) : Set < string > {
169210 const modelImports = new Set < string > ( ) ;
170211 for ( const op of operations ) {
0 commit comments