chrometools-mcp 3.3.8 → 3.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/README.md +129 -24
- package/SPEC-pom-integration.md +227 -0
- package/SPEC-swagger-api-tools.md +3101 -0
- package/index.js +503 -198
- package/package.json +2 -1
- package/pom/apom-tree-converter.js +5 -26
- package/recorder/page-object-generator.js +45 -1
- package/server/tool-definitions.js +54 -5
- package/server/tool-schemas.js +29 -0
- package/test-swagger-phase1.mjs +959 -0
- package/utils/api-generators/api-models-python.js +448 -0
- package/utils/api-generators/api-models-typescript.js +375 -0
- package/utils/code-generators/code-generator-base.js +111 -6
- package/utils/code-generators/playwright-python.js +74 -0
- package/utils/code-generators/playwright-typescript.js +69 -0
- package/utils/code-generators/pom-integrator.js +373 -0
- package/utils/code-generators/selenium-java.js +72 -0
- package/utils/code-generators/selenium-python.js +75 -0
- package/utils/hints-generator.js +114 -19
- package/utils/openapi/helpers.js +25 -0
- package/utils/openapi/parser.js +448 -0
- package/utils/openapi/ref-resolver.js +149 -0
- package/utils/openapi/type-mapper.js +174 -0
- package/nul +0 -0
|
@@ -0,0 +1,959 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Swagger/OpenAPI Phase 1: parser, ref-resolver, type-mapper, model generators
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { resolveAllRefs, resolveRef, mergeAllOf } from './utils/openapi/ref-resolver.js';
|
|
6
|
+
import { TypeMapper } from './utils/openapi/type-mapper.js';
|
|
7
|
+
import { OpenAPIParser } from './utils/openapi/parser.js';
|
|
8
|
+
import { ApiModelsTypeScriptGenerator } from './utils/api-generators/api-models-typescript.js';
|
|
9
|
+
import { ApiModelsPythonGenerator } from './utils/api-generators/api-models-python.js';
|
|
10
|
+
import yaml from 'js-yaml';
|
|
11
|
+
|
|
12
|
+
let passed = 0;
|
|
13
|
+
let failed = 0;
|
|
14
|
+
const asyncTests = [];
|
|
15
|
+
|
|
16
|
+
function test(name, fn) {
|
|
17
|
+
if (fn.constructor.name === 'AsyncFunction') {
|
|
18
|
+
asyncTests.push({ name, fn });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
fn();
|
|
23
|
+
passed++;
|
|
24
|
+
console.log(` ✓ ${name}`);
|
|
25
|
+
} catch (e) {
|
|
26
|
+
failed++;
|
|
27
|
+
console.log(` ✗ ${name}`);
|
|
28
|
+
console.log(` ${e.message}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function assert(condition, msg) {
|
|
33
|
+
if (!condition) throw new Error(msg || 'Assertion failed');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function assertIncludes(str, substr, msg) {
|
|
37
|
+
if (!str.includes(substr)) throw new Error(msg || `Expected to include: "${substr}"\nGot: ${str.slice(0, 200)}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function assertNotIncludes(str, substr, msg) {
|
|
41
|
+
if (str.includes(substr)) throw new Error(msg || `Expected NOT to include: "${substr}"`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ========== REF RESOLVER ==========
|
|
45
|
+
console.log('\n=== Ref Resolver ===');
|
|
46
|
+
|
|
47
|
+
test('resolveRef: simple internal ref', () => {
|
|
48
|
+
const spec = { components: { schemas: { Pet: { type: 'object', properties: { name: { type: 'string' } } } } } };
|
|
49
|
+
const result = resolveRef('#/components/schemas/Pet', spec);
|
|
50
|
+
assert(result.type === 'object');
|
|
51
|
+
assert(result.properties.name.type === 'string');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('resolveRef: circular ref detection', () => {
|
|
55
|
+
const spec = { components: { schemas: { Node: { type: 'object', properties: { children: { type: 'array', items: { $ref: '#/components/schemas/Node' } } } } } } };
|
|
56
|
+
const visited = new Set(['#/components/schemas/Node']);
|
|
57
|
+
const result = resolveRef('#/components/schemas/Node', spec, visited);
|
|
58
|
+
assert(result.$circularRef === 'Node');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('resolveRef: broken ref returns $brokenRef', () => {
|
|
62
|
+
const spec = { components: { schemas: {} } };
|
|
63
|
+
const result = resolveRef('#/components/schemas/Missing', spec);
|
|
64
|
+
assert(result.$brokenRef === '#/components/schemas/Missing');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('resolveRef: external ref returns warning', () => {
|
|
68
|
+
const result = resolveRef('http://example.com/schemas/Pet.json', {});
|
|
69
|
+
assert(result.$externalRef === 'http://example.com/schemas/Pet.json');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('resolveAllRefs: resolves nested refs', () => {
|
|
73
|
+
const spec = {
|
|
74
|
+
components: {
|
|
75
|
+
schemas: {
|
|
76
|
+
Tag: { type: 'object', properties: { id: { type: 'integer' } } },
|
|
77
|
+
Pet: { type: 'object', properties: { tag: { $ref: '#/components/schemas/Tag' } } }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const resolved = resolveAllRefs(spec);
|
|
82
|
+
assert(resolved.components.schemas.Pet.properties.tag.type === 'object');
|
|
83
|
+
assert(resolved.components.schemas.Pet.properties.tag.properties.id.type === 'integer');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('mergeAllOf: merges properties and required', () => {
|
|
87
|
+
const result = mergeAllOf([
|
|
88
|
+
{ type: 'object', properties: { a: { type: 'string' } }, required: ['a'] },
|
|
89
|
+
{ type: 'object', properties: { b: { type: 'integer' } }, required: ['b'] },
|
|
90
|
+
]);
|
|
91
|
+
assert(result.properties.a.type === 'string');
|
|
92
|
+
assert(result.properties.b.type === 'integer');
|
|
93
|
+
assert(result.required.includes('a'));
|
|
94
|
+
assert(result.required.includes('b'));
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ========== TYPE MAPPER ==========
|
|
98
|
+
console.log('\n=== Type Mapper ===');
|
|
99
|
+
|
|
100
|
+
test('TypeScript: string → string', () => {
|
|
101
|
+
assert(TypeMapper.toTypeScript({ type: 'string' }) === 'string');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('TypeScript: integer → number', () => {
|
|
105
|
+
assert(TypeMapper.toTypeScript({ type: 'integer' }) === 'number');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('TypeScript: number → number', () => {
|
|
109
|
+
assert(TypeMapper.toTypeScript({ type: 'number' }) === 'number');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('TypeScript: boolean → boolean', () => {
|
|
113
|
+
assert(TypeMapper.toTypeScript({ type: 'boolean' }) === 'boolean');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('TypeScript: string binary → Blob', () => {
|
|
117
|
+
assert(TypeMapper.toTypeScript({ type: 'string', format: 'binary' }) === 'Blob');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('TypeScript: array → T[]', () => {
|
|
121
|
+
assert(TypeMapper.toTypeScript({ type: 'array', items: { type: 'string' } }) === 'string[]');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('TypeScript: nullable → T | null', () => {
|
|
125
|
+
assert(TypeMapper.toTypeScript({ type: 'string', nullable: true }) === 'string | null');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('TypeScript: enum → union', () => {
|
|
129
|
+
const result = TypeMapper.toTypeScript({ type: 'string', enum: ['a', 'b', 'c'] });
|
|
130
|
+
assert(result === "'a' | 'b' | 'c'");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('TypeScript: oneOf → union', () => {
|
|
134
|
+
const result = TypeMapper.toTypeScript({ oneOf: [{ type: 'string' }, { type: 'number' }] });
|
|
135
|
+
assert(result === 'string | number');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('TypeScript: additionalProperties → Record', () => {
|
|
139
|
+
const result = TypeMapper.toTypeScript({ type: 'object', additionalProperties: { type: 'string' } });
|
|
140
|
+
assert(result === 'Record<string, string>');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('TypeScript: circularRef', () => {
|
|
144
|
+
assert(TypeMapper.toTypeScript({ $circularRef: 'Node' }) === 'Node');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('Python: string → str', () => {
|
|
148
|
+
assert(TypeMapper.toPython({ type: 'string' }) === 'str');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('Python: integer → int', () => {
|
|
152
|
+
assert(TypeMapper.toPython({ type: 'integer' }) === 'int');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('Python: number → float', () => {
|
|
156
|
+
assert(TypeMapper.toPython({ type: 'number' }) === 'float');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('Python: boolean → bool', () => {
|
|
160
|
+
assert(TypeMapper.toPython({ type: 'boolean' }) === 'bool');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('Python: string date-time → datetime', () => {
|
|
164
|
+
assert(TypeMapper.toPython({ type: 'string', format: 'date-time' }) === 'datetime');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('Python: string date → date', () => {
|
|
168
|
+
assert(TypeMapper.toPython({ type: 'string', format: 'date' }) === 'date');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('Python: string binary → bytes', () => {
|
|
172
|
+
assert(TypeMapper.toPython({ type: 'string', format: 'binary' }) === 'bytes');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('Python: array → List[T]', () => {
|
|
176
|
+
assert(TypeMapper.toPython({ type: 'array', items: { type: 'string' } }) === 'List[str]');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('Python: nullable → Optional[T]', () => {
|
|
180
|
+
assert(TypeMapper.toPython({ type: 'string', nullable: true }) === 'Optional[str]');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('Python: oneOf → Union', () => {
|
|
184
|
+
const result = TypeMapper.toPython({ oneOf: [{ type: 'string' }, { type: 'integer' }] });
|
|
185
|
+
assert(result === 'Union[str, int]');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('Python: additionalProperties → Dict', () => {
|
|
189
|
+
const result = TypeMapper.toPython({ type: 'object', additionalProperties: { type: 'integer' } });
|
|
190
|
+
assert(result === 'Dict[str, int]');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// ========== PARSER ==========
|
|
194
|
+
console.log('\n=== Parser ===');
|
|
195
|
+
|
|
196
|
+
const swagger20Spec = {
|
|
197
|
+
swagger: '2.0',
|
|
198
|
+
info: { title: 'Test API', version: '1.0' },
|
|
199
|
+
host: 'api.example.com',
|
|
200
|
+
basePath: '/v1',
|
|
201
|
+
schemes: ['https'],
|
|
202
|
+
securityDefinitions: {
|
|
203
|
+
basicAuth: { type: 'basic' },
|
|
204
|
+
apiKey: { type: 'apiKey', in: 'header', name: 'X-API-Key' },
|
|
205
|
+
oauth: { type: 'oauth2', flow: 'accessCode', authorizationUrl: 'https://auth.example.com', tokenUrl: 'https://token.example.com', scopes: { read: 'Read' } },
|
|
206
|
+
},
|
|
207
|
+
definitions: {
|
|
208
|
+
Pet: {
|
|
209
|
+
type: 'object',
|
|
210
|
+
required: ['id', 'name'],
|
|
211
|
+
properties: {
|
|
212
|
+
id: { type: 'integer', format: 'int64' },
|
|
213
|
+
name: { type: 'string' },
|
|
214
|
+
status: { type: 'string', enum: ['available', 'pending', 'sold'] },
|
|
215
|
+
tag: { $ref: '#/definitions/Tag' }
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
Tag: {
|
|
219
|
+
type: 'object',
|
|
220
|
+
properties: {
|
|
221
|
+
id: { type: 'integer' },
|
|
222
|
+
name: { type: 'string' }
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
Error: {
|
|
226
|
+
type: 'object',
|
|
227
|
+
required: ['code', 'message'],
|
|
228
|
+
properties: {
|
|
229
|
+
code: { type: 'integer' },
|
|
230
|
+
message: { type: 'string' }
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
paths: {
|
|
235
|
+
'/pets': {
|
|
236
|
+
get: {
|
|
237
|
+
operationId: 'listPets',
|
|
238
|
+
summary: 'List all pets',
|
|
239
|
+
tags: ['pets'],
|
|
240
|
+
parameters: [
|
|
241
|
+
{ name: 'limit', in: 'query', type: 'integer', required: false }
|
|
242
|
+
],
|
|
243
|
+
responses: {
|
|
244
|
+
'200': { description: 'OK', schema: { type: 'array', items: { $ref: '#/definitions/Pet' } } }
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
post: {
|
|
248
|
+
operationId: 'createPet',
|
|
249
|
+
summary: 'Create a pet',
|
|
250
|
+
tags: ['pets'],
|
|
251
|
+
parameters: [
|
|
252
|
+
{ name: 'body', in: 'body', required: true, schema: { $ref: '#/definitions/Pet' } }
|
|
253
|
+
],
|
|
254
|
+
responses: {
|
|
255
|
+
'201': { description: 'Created', schema: { $ref: '#/definitions/Pet' } }
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
'/pets/{petId}': {
|
|
260
|
+
get: {
|
|
261
|
+
operationId: 'getPet',
|
|
262
|
+
summary: 'Get a pet',
|
|
263
|
+
tags: ['pets'],
|
|
264
|
+
parameters: [
|
|
265
|
+
{ name: 'petId', in: 'path', type: 'integer', required: true }
|
|
266
|
+
],
|
|
267
|
+
responses: {
|
|
268
|
+
'200': { description: 'OK', schema: { $ref: '#/definitions/Pet' } },
|
|
269
|
+
'404': { description: 'Not found', schema: { $ref: '#/definitions/Error' } }
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
delete: {
|
|
273
|
+
operationId: 'deletePet',
|
|
274
|
+
tags: ['pets'],
|
|
275
|
+
parameters: [
|
|
276
|
+
{ name: 'petId', in: 'path', type: 'integer', required: true }
|
|
277
|
+
],
|
|
278
|
+
responses: {
|
|
279
|
+
'204': { description: 'Deleted' }
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
test('Parser: detect 2.0 version', () => {
|
|
287
|
+
const parser = new OpenAPIParser(swagger20Spec);
|
|
288
|
+
assert(parser.version === '2.0');
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test('Parser: normalize 2.0 base URL', () => {
|
|
292
|
+
const parser = new OpenAPIParser(swagger20Spec);
|
|
293
|
+
assert(parser.getBaseUrl() === 'https://api.example.com/v1');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('Parser: normalize security defs', () => {
|
|
297
|
+
const parser = new OpenAPIParser(swagger20Spec);
|
|
298
|
+
const schemes = parser.getSecuritySchemes();
|
|
299
|
+
assert(schemes.basicAuth.type === 'http');
|
|
300
|
+
assert(schemes.basicAuth.scheme === 'basic');
|
|
301
|
+
assert(schemes.apiKey.type === 'apiKey');
|
|
302
|
+
assert(schemes.apiKey.in === 'header');
|
|
303
|
+
assert(schemes.oauth.type === 'oauth2');
|
|
304
|
+
assert(schemes.oauth.flows.authorizationCode);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test('Parser: extract schemas from definitions', () => {
|
|
308
|
+
const parser = new OpenAPIParser(swagger20Spec);
|
|
309
|
+
const schemas = parser.getSchemas();
|
|
310
|
+
assert(schemas.Pet);
|
|
311
|
+
assert(schemas.Tag);
|
|
312
|
+
assert(schemas.Error);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test('Parser: $ref resolved in schemas', () => {
|
|
316
|
+
const parser = new OpenAPIParser(swagger20Spec);
|
|
317
|
+
const schemas = parser.getSchemas();
|
|
318
|
+
// Pet.tag should be resolved (no more $ref)
|
|
319
|
+
assert(schemas.Pet.properties.tag.type === 'object');
|
|
320
|
+
assert(schemas.Pet.properties.tag.properties.id.type === 'integer');
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test('Parser: extract endpoints', () => {
|
|
324
|
+
const parser = new OpenAPIParser(swagger20Spec);
|
|
325
|
+
const endpoints = parser.getEndpoints();
|
|
326
|
+
assert(endpoints.length === 4);
|
|
327
|
+
const listPets = endpoints.find(e => e.operationId === 'listPets');
|
|
328
|
+
assert(listPets.method === 'GET');
|
|
329
|
+
assert(listPets.path === '/pets');
|
|
330
|
+
assert(listPets.tags.includes('pets'));
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test('Parser: endpoint parameters', () => {
|
|
334
|
+
const parser = new OpenAPIParser(swagger20Spec);
|
|
335
|
+
const endpoints = parser.getEndpoints();
|
|
336
|
+
const listPets = endpoints.find(e => e.operationId === 'listPets');
|
|
337
|
+
assert(listPets.parameters.length === 1);
|
|
338
|
+
assert(listPets.parameters[0].name === 'limit');
|
|
339
|
+
assert(listPets.parameters[0].in === 'query');
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test('Parser: body param → requestBody', () => {
|
|
343
|
+
const parser = new OpenAPIParser(swagger20Spec);
|
|
344
|
+
const endpoints = parser.getEndpoints();
|
|
345
|
+
const createPet = endpoints.find(e => e.operationId === 'createPet');
|
|
346
|
+
assert(createPet.requestBody !== null);
|
|
347
|
+
assert(createPet.requestBody.required === true);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test('Parser: auth summary', () => {
|
|
351
|
+
const parser = new OpenAPIParser(swagger20Spec);
|
|
352
|
+
const summary = parser.getSummary();
|
|
353
|
+
assert(summary.auth.length === 3);
|
|
354
|
+
assert(summary.auth.find(a => a.name === 'basicAuth'));
|
|
355
|
+
assert(summary.auth.find(a => a.name === 'apiKey'));
|
|
356
|
+
assert(summary.auth.find(a => a.name === 'oauth'));
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test('Parser: full summary', () => {
|
|
360
|
+
const parser = new OpenAPIParser(swagger20Spec);
|
|
361
|
+
const summary = parser.getSummary();
|
|
362
|
+
assert(summary.endpointCount === 4);
|
|
363
|
+
assert(summary.schemaCount === 3);
|
|
364
|
+
assert(summary.title === 'Test API');
|
|
365
|
+
assert(summary.baseUrl === 'https://api.example.com/v1');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// OpenAPI 3.0 spec
|
|
369
|
+
const openapi30Spec = {
|
|
370
|
+
openapi: '3.0.3',
|
|
371
|
+
info: { title: 'Test API v3', version: '1.0' },
|
|
372
|
+
servers: [{ url: 'https://api.example.com/v3' }],
|
|
373
|
+
components: {
|
|
374
|
+
schemas: {
|
|
375
|
+
User: {
|
|
376
|
+
type: 'object',
|
|
377
|
+
required: ['id', 'email'],
|
|
378
|
+
properties: {
|
|
379
|
+
id: { type: 'integer' },
|
|
380
|
+
email: { type: 'string', format: 'email' },
|
|
381
|
+
role: { type: 'string', enum: ['admin', 'user', 'guest'] },
|
|
382
|
+
profile: { $ref: '#/components/schemas/Profile' }
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
Profile: {
|
|
386
|
+
type: 'object',
|
|
387
|
+
properties: {
|
|
388
|
+
bio: { type: 'string' },
|
|
389
|
+
avatar: { type: 'string', format: 'uri' }
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
securitySchemes: {
|
|
394
|
+
bearer: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
paths: {
|
|
398
|
+
'/users': {
|
|
399
|
+
get: {
|
|
400
|
+
operationId: 'listUsers',
|
|
401
|
+
summary: 'List users',
|
|
402
|
+
tags: ['users'],
|
|
403
|
+
parameters: [
|
|
404
|
+
{ name: 'page', in: 'query', schema: { type: 'integer' } }
|
|
405
|
+
],
|
|
406
|
+
responses: {
|
|
407
|
+
'200': {
|
|
408
|
+
description: 'OK',
|
|
409
|
+
content: { 'application/json': { schema: { type: 'array', items: { $ref: '#/components/schemas/User' } } } }
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
test('Parser: detect 3.0 version', () => {
|
|
418
|
+
const parser = new OpenAPIParser(openapi30Spec);
|
|
419
|
+
assert(parser.version === '3.0.3');
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test('Parser: 3.0 base URL', () => {
|
|
423
|
+
const parser = new OpenAPIParser(openapi30Spec);
|
|
424
|
+
assert(parser.getBaseUrl() === 'https://api.example.com/v3');
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test('Parser: 3.0 schemas', () => {
|
|
428
|
+
const parser = new OpenAPIParser(openapi30Spec);
|
|
429
|
+
const schemas = parser.getSchemas();
|
|
430
|
+
assert(schemas.User);
|
|
431
|
+
assert(schemas.Profile);
|
|
432
|
+
// $ref resolved
|
|
433
|
+
assert(schemas.User.properties.profile.type === 'object');
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test('Parser: 3.0 endpoints', () => {
|
|
437
|
+
const parser = new OpenAPIParser(openapi30Spec);
|
|
438
|
+
const endpoints = parser.getEndpoints();
|
|
439
|
+
assert(endpoints.length === 1);
|
|
440
|
+
assert(endpoints[0].operationId === 'listUsers');
|
|
441
|
+
assert(endpoints[0].parameters.length === 1);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test('Parser: 3.0 security schemes', () => {
|
|
445
|
+
const parser = new OpenAPIParser(openapi30Spec);
|
|
446
|
+
const schemes = parser.getSecuritySchemes();
|
|
447
|
+
assert(schemes.bearer.type === 'http');
|
|
448
|
+
assert(schemes.bearer.scheme === 'bearer');
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test('Parser: generateOperationId fallback', () => {
|
|
452
|
+
const parser = new OpenAPIParser(openapi30Spec);
|
|
453
|
+
assert(parser.generateOperationId('get', '/pets') === 'getPets');
|
|
454
|
+
assert(parser.generateOperationId('post', '/pets') === 'createPets');
|
|
455
|
+
assert(parser.generateOperationId('get', '/pets/{petId}') === 'getPetsByPetId');
|
|
456
|
+
assert(parser.generateOperationId('delete', '/users/{id}/orders') === 'deleteUsersByIdOrders');
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test('Parser: schemas summary', () => {
|
|
460
|
+
const parser = new OpenAPIParser(openapi30Spec);
|
|
461
|
+
const summary = parser.getSchemasSummary();
|
|
462
|
+
assert(summary.User);
|
|
463
|
+
assert(summary.User.properties.email);
|
|
464
|
+
assert(summary.User.properties.role.enum);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// ========== TypeScript Generator ==========
|
|
468
|
+
console.log('\n=== TypeScript Models Generator ===');
|
|
469
|
+
|
|
470
|
+
test('TS: generates interfaces', () => {
|
|
471
|
+
const parser = new OpenAPIParser(swagger20Spec);
|
|
472
|
+
const gen = new ApiModelsTypeScriptGenerator(parser.getSchemas(), { style: 'interface', includeEnums: true });
|
|
473
|
+
const code = gen.generate({ title: 'Test', source: 'test', version: '2.0' });
|
|
474
|
+
assertIncludes(code, 'export interface Pet {');
|
|
475
|
+
assertIncludes(code, 'id: number;');
|
|
476
|
+
assertIncludes(code, 'name: string;');
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test('TS: generates type aliases', () => {
|
|
480
|
+
const parser = new OpenAPIParser(swagger20Spec);
|
|
481
|
+
const gen = new ApiModelsTypeScriptGenerator(parser.getSchemas(), { style: 'type', includeEnums: true });
|
|
482
|
+
const code = gen.generate({ title: 'Test', source: 'test', version: '2.0' });
|
|
483
|
+
assertIncludes(code, 'export type Pet = {');
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test('TS: generates enums from string+enum', () => {
|
|
487
|
+
const parser = new OpenAPIParser(swagger20Spec);
|
|
488
|
+
const gen = new ApiModelsTypeScriptGenerator(parser.getSchemas(), { style: 'interface', includeEnums: true });
|
|
489
|
+
const code = gen.generate({ title: 'Test', source: 'test', version: '2.0' });
|
|
490
|
+
assertIncludes(code, 'export enum PetStatus {');
|
|
491
|
+
assertIncludes(code, "Available = 'available'");
|
|
492
|
+
assertIncludes(code, "Pending = 'pending'");
|
|
493
|
+
assertIncludes(code, "Sold = 'sold'");
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test('TS: required vs optional', () => {
|
|
497
|
+
const parser = new OpenAPIParser(swagger20Spec);
|
|
498
|
+
const gen = new ApiModelsTypeScriptGenerator(parser.getSchemas(), { style: 'interface', includeEnums: true });
|
|
499
|
+
const code = gen.generate({ title: 'Test', source: 'test', version: '2.0' });
|
|
500
|
+
// id and name are required, status is optional
|
|
501
|
+
assertIncludes(code, 'id: number;');
|
|
502
|
+
assertIncludes(code, 'name: string;');
|
|
503
|
+
assertIncludes(code, 'status?:');
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test('TS: Error interface', () => {
|
|
507
|
+
const parser = new OpenAPIParser(swagger20Spec);
|
|
508
|
+
const gen = new ApiModelsTypeScriptGenerator(parser.getSchemas(), { style: 'interface', includeEnums: true });
|
|
509
|
+
const code = gen.generate({ title: 'Test', source: 'test', version: '2.0' });
|
|
510
|
+
assertIncludes(code, 'export interface Error {');
|
|
511
|
+
assertIncludes(code, 'code: number;');
|
|
512
|
+
assertIncludes(code, 'message: string;');
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test('TS: header with metadata', () => {
|
|
516
|
+
const gen = new ApiModelsTypeScriptGenerator({}, {});
|
|
517
|
+
const code = gen.generate({ title: 'My API', source: 'http://example.com', version: '3.0.0' });
|
|
518
|
+
assertIncludes(code, 'Generated from My API');
|
|
519
|
+
assertIncludes(code, 'http://example.com');
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test('TS: nested object (Tag resolved)', () => {
|
|
523
|
+
const parser = new OpenAPIParser(swagger20Spec);
|
|
524
|
+
const gen = new ApiModelsTypeScriptGenerator(parser.getSchemas(), { style: 'interface', includeEnums: true });
|
|
525
|
+
const code = gen.generate({ title: 'Test', source: 'test', version: '2.0' });
|
|
526
|
+
assertIncludes(code, 'export interface Tag {');
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
test('TS: oneOf/anyOf generates union type', () => {
|
|
530
|
+
const schemas = {
|
|
531
|
+
Cat: { type: 'object', properties: { name: { type: 'string' } } },
|
|
532
|
+
Dog: { type: 'object', properties: { breed: { type: 'string' } } },
|
|
533
|
+
Animal: { oneOf: [{ type: 'object', properties: { name: { type: 'string' } } }, { type: 'object', properties: { breed: { type: 'string' } } }] }
|
|
534
|
+
};
|
|
535
|
+
const gen = new ApiModelsTypeScriptGenerator(schemas, {});
|
|
536
|
+
const code = gen.generate({});
|
|
537
|
+
assertIncludes(code, 'export type Animal =');
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
test('TS: allOf generates extends', () => {
|
|
541
|
+
const schemas = {
|
|
542
|
+
Base: { type: 'object', properties: { id: { type: 'integer' } }, required: ['id'] },
|
|
543
|
+
Extended: { allOf: [
|
|
544
|
+
{ type: 'object', properties: { id: { type: 'integer' } }, required: ['id'] },
|
|
545
|
+
{ type: 'object', properties: { extra: { type: 'string' } } }
|
|
546
|
+
]}
|
|
547
|
+
};
|
|
548
|
+
const gen = new ApiModelsTypeScriptGenerator(schemas, { style: 'interface' });
|
|
549
|
+
const code = gen.generate({});
|
|
550
|
+
assertIncludes(code, 'export interface Extended extends Base');
|
|
551
|
+
assertIncludes(code, 'extra');
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test('TS: no duplicate enums when values match top-level enum', () => {
|
|
555
|
+
const schemas = {
|
|
556
|
+
Status: { type: 'string', enum: ['active', 'inactive'], description: 'Status' },
|
|
557
|
+
User: { type: 'object', properties: { status: { type: 'string', enum: ['active', 'inactive'] } } }
|
|
558
|
+
};
|
|
559
|
+
const gen = new ApiModelsTypeScriptGenerator(schemas, { style: 'interface', includeEnums: true });
|
|
560
|
+
const code = gen.generate({});
|
|
561
|
+
// Should have Status enum but NOT UserStatus enum
|
|
562
|
+
assertIncludes(code, 'export enum Status {');
|
|
563
|
+
assertNotIncludes(code, 'export enum UserStatus {');
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
test('TS: empty schemas → no crash', () => {
|
|
567
|
+
const gen = new ApiModelsTypeScriptGenerator({}, {});
|
|
568
|
+
const code = gen.generate({});
|
|
569
|
+
assert(typeof code === 'string');
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
// ========== Python Generator ==========
|
|
573
|
+
console.log('\n=== Python Models Generator ===');
|
|
574
|
+
|
|
575
|
+
test('PY: generates dataclasses', () => {
|
|
576
|
+
const parser = new OpenAPIParser(swagger20Spec);
|
|
577
|
+
const gen = new ApiModelsPythonGenerator(parser.getSchemas(), { style: 'dataclass', includeEnums: true });
|
|
578
|
+
const code = gen.generate({ title: 'Test', source: 'test', version: '2.0' });
|
|
579
|
+
assertIncludes(code, '@dataclass');
|
|
580
|
+
assertIncludes(code, 'class Pet:');
|
|
581
|
+
assertIncludes(code, 'id: int');
|
|
582
|
+
assertIncludes(code, 'name: str');
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
test('PY: generates pydantic', () => {
|
|
586
|
+
const parser = new OpenAPIParser(swagger20Spec);
|
|
587
|
+
const gen = new ApiModelsPythonGenerator(parser.getSchemas(), { style: 'pydantic', includeEnums: true });
|
|
588
|
+
const code = gen.generate({ title: 'Test', source: 'test', version: '2.0' });
|
|
589
|
+
assertIncludes(code, 'from pydantic import BaseModel');
|
|
590
|
+
assertIncludes(code, 'class Pet(BaseModel):');
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
test('PY: generates typeddict', () => {
|
|
594
|
+
const parser = new OpenAPIParser(swagger20Spec);
|
|
595
|
+
const gen = new ApiModelsPythonGenerator(parser.getSchemas(), { style: 'typeddict', includeEnums: true });
|
|
596
|
+
const code = gen.generate({ title: 'Test', source: 'test', version: '2.0' });
|
|
597
|
+
assertIncludes(code, 'from typing import TypedDict');
|
|
598
|
+
assertIncludes(code, 'class Pet(TypedDict, total=False):');
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
test('PY: generates enums', () => {
|
|
602
|
+
const parser = new OpenAPIParser(swagger20Spec);
|
|
603
|
+
const gen = new ApiModelsPythonGenerator(parser.getSchemas(), { style: 'dataclass', includeEnums: true });
|
|
604
|
+
const code = gen.generate({ title: 'Test', source: 'test', version: '2.0' });
|
|
605
|
+
assertIncludes(code, 'class PetStatus(str, Enum):');
|
|
606
|
+
assertIncludes(code, "AVAILABLE = 'available'");
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
test('PY: optional fields use Optional', () => {
|
|
610
|
+
const parser = new OpenAPIParser(swagger20Spec);
|
|
611
|
+
const gen = new ApiModelsPythonGenerator(parser.getSchemas(), { style: 'dataclass', includeEnums: true });
|
|
612
|
+
const code = gen.generate({ title: 'Test', source: 'test', version: '2.0' });
|
|
613
|
+
assertIncludes(code, 'status: Optional[');
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
test('PY: required fields have no defaults in dataclass', () => {
|
|
617
|
+
const parser = new OpenAPIParser(swagger20Spec);
|
|
618
|
+
const gen = new ApiModelsPythonGenerator(parser.getSchemas(), { style: 'dataclass', includeEnums: true });
|
|
619
|
+
const code = gen.generate({ title: 'Test', source: 'test', version: '2.0' });
|
|
620
|
+
// Required fields should NOT have defaults (enforced at construction)
|
|
621
|
+
const lines = code.split('\n');
|
|
622
|
+
const idLine = lines.find(l => l.trim().startsWith('id: int'));
|
|
623
|
+
assert(idLine && !idLine.includes('='), 'Dataclass required int should not have default');
|
|
624
|
+
const nameLine = lines.find(l => l.trim().startsWith('name: str'));
|
|
625
|
+
assert(nameLine && !nameLine.includes('='), 'Dataclass required str should not have default');
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
test('PY: pydantic required fields no default', () => {
|
|
629
|
+
const parser = new OpenAPIParser(swagger20Spec);
|
|
630
|
+
const gen = new ApiModelsPythonGenerator(parser.getSchemas(), { style: 'pydantic', includeEnums: true });
|
|
631
|
+
const code = gen.generate({ title: 'Test', source: 'test', version: '2.0' });
|
|
632
|
+
// Pydantic required fields don't have = default
|
|
633
|
+
const lines = code.split('\n');
|
|
634
|
+
const idLine = lines.find(l => l.trim().startsWith('id: int'));
|
|
635
|
+
assert(idLine && !idLine.includes('= 0'), 'Pydantic required int should not have default');
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
test('PY: header with docstring', () => {
|
|
639
|
+
const gen = new ApiModelsPythonGenerator({}, {});
|
|
640
|
+
const code = gen.generate({ title: 'My API', source: 'http://example.com', version: '3.0.0' });
|
|
641
|
+
assertIncludes(code, '"""');
|
|
642
|
+
assertIncludes(code, 'Generated from My API');
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
test('PY: Tag class', () => {
|
|
646
|
+
const parser = new OpenAPIParser(swagger20Spec);
|
|
647
|
+
const gen = new ApiModelsPythonGenerator(parser.getSchemas(), { style: 'dataclass', includeEnums: true });
|
|
648
|
+
const code = gen.generate({ title: 'Test', source: 'test', version: '2.0' });
|
|
649
|
+
assertIncludes(code, 'class Tag:');
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
test('PY: no duplicate enums when values match top-level enum', () => {
|
|
653
|
+
const schemas = {
|
|
654
|
+
Status: { type: 'string', enum: ['active', 'inactive'], description: 'Status' },
|
|
655
|
+
User: { type: 'object', properties: { status: { type: 'string', enum: ['active', 'inactive'] } } }
|
|
656
|
+
};
|
|
657
|
+
const gen = new ApiModelsPythonGenerator(schemas, { style: 'dataclass', includeEnums: true });
|
|
658
|
+
const code = gen.generate({});
|
|
659
|
+
assertIncludes(code, 'class Status(str, Enum):');
|
|
660
|
+
assertNotIncludes(code, 'class UserStatus(str, Enum):');
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
test('PY: empty schemas → no crash', () => {
|
|
664
|
+
const gen = new ApiModelsPythonGenerator({}, {});
|
|
665
|
+
const code = gen.generate({});
|
|
666
|
+
assert(typeof code === 'string');
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
test('PY: imports include future annotations', () => {
|
|
670
|
+
const gen = new ApiModelsPythonGenerator({ X: { type: 'object', properties: {} } }, { style: 'dataclass' });
|
|
671
|
+
const code = gen.generate({});
|
|
672
|
+
assertIncludes(code, 'from __future__ import annotations');
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
test('PY: datetime imports when date-time used', () => {
|
|
676
|
+
const schemas = {
|
|
677
|
+
Event: { type: 'object', properties: { created: { type: 'string', format: 'date-time' } } }
|
|
678
|
+
};
|
|
679
|
+
const gen = new ApiModelsPythonGenerator(schemas, { style: 'dataclass' });
|
|
680
|
+
const code = gen.generate({});
|
|
681
|
+
assertIncludes(code, 'from datetime import datetime');
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
// ========== Integration: filtering schemas ==========
|
|
685
|
+
console.log('\n=== Integration ===');
|
|
686
|
+
|
|
687
|
+
test('Generator respects schema filtering', () => {
|
|
688
|
+
const parser = new OpenAPIParser(swagger20Spec);
|
|
689
|
+
const allSchemas = parser.getSchemas();
|
|
690
|
+
const filtered = { Pet: allSchemas.Pet };
|
|
691
|
+
const gen = new ApiModelsTypeScriptGenerator(filtered, {});
|
|
692
|
+
const code = gen.generate({});
|
|
693
|
+
assertIncludes(code, 'export interface Pet {');
|
|
694
|
+
assertNotIncludes(code, 'export interface Error {');
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
test('Topological sort: Tag before Pet', () => {
|
|
698
|
+
const parser = new OpenAPIParser(swagger20Spec);
|
|
699
|
+
const gen = new ApiModelsTypeScriptGenerator(parser.getSchemas(), {});
|
|
700
|
+
const code = gen.generate({});
|
|
701
|
+
const tagIndex = code.indexOf('export interface Tag');
|
|
702
|
+
const petIndex = code.indexOf('export interface Pet');
|
|
703
|
+
assert(tagIndex < petIndex, 'Tag should appear before Pet (dependency order)');
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
test('TS: suggested filename generation', () => {
|
|
707
|
+
const titleSlug = 'Pet Store API'.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
708
|
+
assert(titleSlug === 'pet-store-api');
|
|
709
|
+
assert(`${titleSlug}.models.ts` === 'pet-store-api.models.ts');
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
test('PY: suggested filename generation', () => {
|
|
713
|
+
const titleSlug = 'Pet Store API'.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '');
|
|
714
|
+
assert(titleSlug === 'pet_store_api');
|
|
715
|
+
assert(`${titleSlug}_models.py` === 'pet_store_api_models.py');
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
// ========== Bug fix regression tests ==========
|
|
719
|
+
console.log('\n=== Bug Fix Regressions ===');
|
|
720
|
+
|
|
721
|
+
test('Circular $ref produces $circularRef marker (not infinite JS refs)', () => {
|
|
722
|
+
const spec = {
|
|
723
|
+
openapi: '3.0.0', info: { title: 'Test' }, paths: {},
|
|
724
|
+
components: { schemas: {
|
|
725
|
+
TreeNode: {
|
|
726
|
+
type: 'object',
|
|
727
|
+
properties: {
|
|
728
|
+
name: { type: 'string' },
|
|
729
|
+
children: { type: 'array', items: { $ref: '#/components/schemas/TreeNode' } }
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}}
|
|
733
|
+
};
|
|
734
|
+
const parser = new OpenAPIParser(spec);
|
|
735
|
+
const schemas = parser.getSchemas();
|
|
736
|
+
// First level resolves fully, second level gets $circularRef marker
|
|
737
|
+
const resolved = schemas.TreeNode.properties.children.items;
|
|
738
|
+
assert(resolved.type === 'object', 'First level should be fully resolved');
|
|
739
|
+
const nested = resolved.properties.children.items;
|
|
740
|
+
assert(nested.$circularRef === 'TreeNode', `Expected $circularRef at nested level, got: ${JSON.stringify(nested)}`);
|
|
741
|
+
// Verify no infinite JS object references (JSON.stringify would throw)
|
|
742
|
+
const json = JSON.stringify(schemas.TreeNode);
|
|
743
|
+
assert(json.includes('"$circularRef":"TreeNode"'), 'Should contain $circularRef marker in serialized output');
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
test('Circular schemas do not crash generators', () => {
|
|
747
|
+
const spec = {
|
|
748
|
+
openapi: '3.0.0', info: { title: 'Test' }, paths: {},
|
|
749
|
+
components: { schemas: {
|
|
750
|
+
A: { type: 'object', properties: { b: { $ref: '#/components/schemas/B' } } },
|
|
751
|
+
B: { type: 'object', properties: { a: { $ref: '#/components/schemas/A' } } }
|
|
752
|
+
}}
|
|
753
|
+
};
|
|
754
|
+
const parser = new OpenAPIParser(spec);
|
|
755
|
+
const tsGen = new ApiModelsTypeScriptGenerator(parser.getSchemas(), {});
|
|
756
|
+
const tsCode = tsGen.generate({});
|
|
757
|
+
assertIncludes(tsCode, 'export interface A');
|
|
758
|
+
assertIncludes(tsCode, 'export interface B');
|
|
759
|
+
|
|
760
|
+
const pyGen = new ApiModelsPythonGenerator(parser.getSchemas(), { style: 'dataclass' });
|
|
761
|
+
const pyCode = pyGen.generate({});
|
|
762
|
+
assertIncludes(pyCode, 'class A:');
|
|
763
|
+
assertIncludes(pyCode, 'class B:');
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
test('Enum key starting with digit gets _ prefix', () => {
|
|
767
|
+
const schemas = {
|
|
768
|
+
Status: { type: 'string', enum: ['3xx', '4xx', '5xx'] }
|
|
769
|
+
};
|
|
770
|
+
const tsGen = new ApiModelsTypeScriptGenerator(schemas, { includeEnums: true });
|
|
771
|
+
const tsCode = tsGen.generate({});
|
|
772
|
+
assertIncludes(tsCode, "_3xx = '3xx'");
|
|
773
|
+
|
|
774
|
+
const pyGen = new ApiModelsPythonGenerator(schemas, { includeEnums: true });
|
|
775
|
+
const pyCode = pyGen.generate({});
|
|
776
|
+
assertIncludes(pyCode, "_3XX = '3xx'");
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
test('Path-level params overridden by operation-level params', () => {
|
|
780
|
+
const spec = {
|
|
781
|
+
swagger: '2.0', info: { title: 'Test' }, host: 'localhost', basePath: '/api',
|
|
782
|
+
paths: {
|
|
783
|
+
'/items/{id}': {
|
|
784
|
+
parameters: [
|
|
785
|
+
{ name: 'id', in: 'path', type: 'string', description: 'path-level' }
|
|
786
|
+
],
|
|
787
|
+
get: {
|
|
788
|
+
operationId: 'getItem',
|
|
789
|
+
parameters: [
|
|
790
|
+
{ name: 'id', in: 'path', type: 'integer', description: 'op-level override' }
|
|
791
|
+
],
|
|
792
|
+
responses: { '200': { description: 'OK' } }
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
const parser = new OpenAPIParser(spec);
|
|
798
|
+
const endpoints = parser.getEndpoints();
|
|
799
|
+
const getItem = endpoints.find(e => e.operationId === 'getItem');
|
|
800
|
+
// Operation-level param should win (type integer, not string)
|
|
801
|
+
const idParams = getItem.parameters.filter(p => p.name === 'id');
|
|
802
|
+
assert(idParams.length === 1, `Expected 1 id param, got ${idParams.length}`);
|
|
803
|
+
assert(idParams[0].type === 'integer', `Expected integer, got ${idParams[0].type}`);
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
test('_refName marker enables fast schema lookup', () => {
|
|
807
|
+
const spec = {
|
|
808
|
+
openapi: '3.0.0', info: { title: 'Test' }, paths: {},
|
|
809
|
+
components: { schemas: {
|
|
810
|
+
Address: { type: 'object', properties: { street: { type: 'string' }, city: { type: 'string' } } },
|
|
811
|
+
User: { type: 'object', properties: { name: { type: 'string' }, address: { $ref: '#/components/schemas/Address' } } }
|
|
812
|
+
}}
|
|
813
|
+
};
|
|
814
|
+
const parser = new OpenAPIParser(spec);
|
|
815
|
+
const schemas = parser.getSchemas();
|
|
816
|
+
// The resolved address property should be identified as Address
|
|
817
|
+
const gen = new ApiModelsTypeScriptGenerator(schemas, {});
|
|
818
|
+
const code = gen.generate({});
|
|
819
|
+
assertIncludes(code, 'address?: Address;');
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
test('Schema matching includes types, not just keys', () => {
|
|
823
|
+
const schemas = {
|
|
824
|
+
PersonName: { type: 'object', properties: { first: { type: 'string' }, last: { type: 'string' } } },
|
|
825
|
+
Coords: { type: 'object', properties: { first: { type: 'number' }, last: { type: 'number' } } },
|
|
826
|
+
};
|
|
827
|
+
const gen = new ApiModelsTypeScriptGenerator(schemas, {});
|
|
828
|
+
// Both have keys "first,last" but different types — should NOT be confused
|
|
829
|
+
const code = gen.generate({});
|
|
830
|
+
assertIncludes(code, 'export interface PersonName');
|
|
831
|
+
assertIncludes(code, 'export interface Coords');
|
|
832
|
+
// Both should exist independently
|
|
833
|
+
const personIdx = code.indexOf('export interface PersonName');
|
|
834
|
+
const coordsIdx = code.indexOf('export interface Coords');
|
|
835
|
+
assert(personIdx >= 0 && coordsIdx >= 0, 'Both schemas should be generated');
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
// ========== Remote URL loading ==========
|
|
839
|
+
console.log('\n=== Remote Loading ===');
|
|
840
|
+
|
|
841
|
+
test('Parser: load from Petstore 2.0 URL', async () => {
|
|
842
|
+
try {
|
|
843
|
+
const parser = await OpenAPIParser.load('https://petstore.swagger.io/v2/swagger.json');
|
|
844
|
+
assert(parser.version === '2.0');
|
|
845
|
+
const summary = parser.getSummary();
|
|
846
|
+
assert(summary.endpointCount > 0, `Expected >0 endpoints, got ${summary.endpointCount}`);
|
|
847
|
+
assert(summary.schemaCount > 0, `Expected >0 schemas, got ${summary.schemaCount}`);
|
|
848
|
+
assert(summary.title.toLowerCase().includes('pet'));
|
|
849
|
+
console.log(` (${summary.endpointCount} endpoints, ${summary.schemaCount} schemas)`);
|
|
850
|
+
} catch (e) {
|
|
851
|
+
// Network might be unavailable in CI
|
|
852
|
+
console.log(` (skipped: ${e.message})`);
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
test('Parser: load from Petstore 3.0 URL', async () => {
|
|
857
|
+
try {
|
|
858
|
+
const parser = await OpenAPIParser.load('https://petstore3.swagger.io/api/v3/openapi.json');
|
|
859
|
+
assert(parser.version.startsWith('3.'));
|
|
860
|
+
const summary = parser.getSummary();
|
|
861
|
+
assert(summary.endpointCount > 0);
|
|
862
|
+
assert(summary.schemaCount > 0);
|
|
863
|
+
console.log(` (${summary.endpointCount} endpoints, ${summary.schemaCount} schemas)`);
|
|
864
|
+
} catch (e) {
|
|
865
|
+
console.log(` (skipped: ${e.message})`);
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
test('TS: generate models from Petstore 2.0', async () => {
|
|
870
|
+
try {
|
|
871
|
+
const parser = await OpenAPIParser.load('https://petstore.swagger.io/v2/swagger.json');
|
|
872
|
+
const gen = new ApiModelsTypeScriptGenerator(parser.getSchemas(), { style: 'interface', includeEnums: true });
|
|
873
|
+
const code = gen.generate({ title: 'Petstore', source: 'petstore', version: parser.version });
|
|
874
|
+
assertIncludes(code, 'export interface Pet {');
|
|
875
|
+
assertIncludes(code, 'export interface Category {');
|
|
876
|
+
console.log(` (${code.split('\n').length} lines)`);
|
|
877
|
+
} catch (e) {
|
|
878
|
+
console.log(` (skipped: ${e.message})`);
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
test('PY: generate dataclasses from Petstore 2.0', async () => {
|
|
883
|
+
try {
|
|
884
|
+
const parser = await OpenAPIParser.load('https://petstore.swagger.io/v2/swagger.json');
|
|
885
|
+
const gen = new ApiModelsPythonGenerator(parser.getSchemas(), { style: 'dataclass', includeEnums: true });
|
|
886
|
+
const code = gen.generate({ title: 'Petstore', source: 'petstore', version: parser.version });
|
|
887
|
+
assertIncludes(code, '@dataclass');
|
|
888
|
+
assertIncludes(code, 'class Pet:');
|
|
889
|
+
console.log(` (${code.split('\n').length} lines)`);
|
|
890
|
+
} catch (e) {
|
|
891
|
+
console.log(` (skipped: ${e.message})`);
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
test('PY: generate pydantic from Petstore 2.0', async () => {
|
|
896
|
+
try {
|
|
897
|
+
const parser = await OpenAPIParser.load('https://petstore.swagger.io/v2/swagger.json');
|
|
898
|
+
const gen = new ApiModelsPythonGenerator(parser.getSchemas(), { style: 'pydantic', includeEnums: true });
|
|
899
|
+
const code = gen.generate({ title: 'Petstore', source: 'petstore', version: parser.version });
|
|
900
|
+
assertIncludes(code, 'class Pet(BaseModel):');
|
|
901
|
+
console.log(` (${code.split('\n').length} lines)`);
|
|
902
|
+
} catch (e) {
|
|
903
|
+
console.log(` (skipped: ${e.message})`);
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
// ========== YAML parsing ==========
|
|
908
|
+
console.log('\n=== YAML Parsing ===');
|
|
909
|
+
|
|
910
|
+
test('Parser: YAML format support', () => {
|
|
911
|
+
const yamlContent = `
|
|
912
|
+
openapi: '3.0.0'
|
|
913
|
+
info:
|
|
914
|
+
title: YAML API
|
|
915
|
+
version: '1.0'
|
|
916
|
+
servers:
|
|
917
|
+
- url: https://api.example.com
|
|
918
|
+
paths:
|
|
919
|
+
/items:
|
|
920
|
+
get:
|
|
921
|
+
operationId: listItems
|
|
922
|
+
responses:
|
|
923
|
+
'200':
|
|
924
|
+
description: OK
|
|
925
|
+
components:
|
|
926
|
+
schemas:
|
|
927
|
+
Item:
|
|
928
|
+
type: object
|
|
929
|
+
properties:
|
|
930
|
+
name:
|
|
931
|
+
type: string
|
|
932
|
+
`;
|
|
933
|
+
const spec = yaml.load(yamlContent);
|
|
934
|
+
const parser = new OpenAPIParser(spec);
|
|
935
|
+
assert(parser.version === '3.0.0');
|
|
936
|
+
assert(parser.spec.info.title === 'YAML API');
|
|
937
|
+
assert(parser.getSchemas().Item);
|
|
938
|
+
assert(parser.getEndpoints().length === 1);
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
// ========== Run async tests ==========
|
|
942
|
+
for (const { name, fn } of asyncTests) {
|
|
943
|
+
try {
|
|
944
|
+
await fn();
|
|
945
|
+
passed++;
|
|
946
|
+
console.log(` ✓ ${name}`);
|
|
947
|
+
} catch (e) {
|
|
948
|
+
failed++;
|
|
949
|
+
console.log(` ✗ ${name}`);
|
|
950
|
+
console.log(` ${e.message}`);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// ========== Summary ==========
|
|
955
|
+
console.log(`\n${'='.repeat(40)}`);
|
|
956
|
+
console.log(`Results: ${passed} passed, ${failed} failed (${passed + failed} total)`);
|
|
957
|
+
if (failed > 0) {
|
|
958
|
+
process.exit(1);
|
|
959
|
+
}
|