@democratize-quality/mcp-server 1.1.8 → 1.2.0
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/LICENSE +658 -12
- package/README.md +11 -6
- package/dist/server.d.ts +41 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +225 -0
- package/dist/server.js.map +1 -0
- package/package.json +24 -25
- package/browserControl.js +0 -113
- package/cli.js +0 -228
- package/mcpServer.js +0 -335
- package/run-server.js +0 -140
- package/src/chatmodes//360/237/214/220 api-generator.chatmode.md" +0 -409
- package/src/chatmodes//360/237/214/220 api-healer.chatmode.md" +0 -494
- package/src/chatmodes//360/237/214/220 api-planner.chatmode.md" +0 -954
- package/src/config/environments/api-only.js +0 -53
- package/src/config/environments/development.js +0 -54
- package/src/config/environments/production.js +0 -69
- package/src/config/index.js +0 -341
- package/src/config/server.js +0 -41
- package/src/config/tools/api.js +0 -67
- package/src/config/tools/browser.js +0 -90
- package/src/config/tools/default.js +0 -32
- package/src/docs/Agent_README.md +0 -310
- package/src/docs/QUICK_REFERENCE.md +0 -111
- package/src/services/browserService.js +0 -325
- package/src/skills/api-planning/SKILL.md +0 -224
- package/src/skills/test-execution/SKILL.md +0 -728
- package/src/skills/test-generation/SKILL.md +0 -309
- package/src/skills/test-healing/SKILL.md +0 -405
- package/src/tools/api/api-generator.js +0 -1865
- package/src/tools/api/api-healer.js +0 -617
- package/src/tools/api/api-planner.js +0 -2598
- package/src/tools/api/api-project-setup.js +0 -313
- package/src/tools/api/api-request.js +0 -641
- package/src/tools/api/api-session-report.js +0 -1278
- package/src/tools/api/api-session-status.js +0 -395
- package/src/tools/api/prompts/README.md +0 -293
- package/src/tools/api/prompts/generation-prompts.js +0 -703
- package/src/tools/api/prompts/healing-prompts.js +0 -195
- package/src/tools/api/prompts/index.js +0 -25
- package/src/tools/api/prompts/orchestrator.js +0 -334
- package/src/tools/api/prompts/validation-rules.js +0 -339
- package/src/tools/base/ToolBase.js +0 -230
- package/src/tools/base/ToolRegistry.js +0 -269
- package/src/tools/browser/advanced/browser-console.js +0 -384
- package/src/tools/browser/advanced/browser-dialog.js +0 -319
- package/src/tools/browser/advanced/browser-evaluate.js +0 -337
- package/src/tools/browser/advanced/browser-file.js +0 -480
- package/src/tools/browser/advanced/browser-keyboard.js +0 -343
- package/src/tools/browser/advanced/browser-mouse.js +0 -332
- package/src/tools/browser/advanced/browser-network.js +0 -421
- package/src/tools/browser/advanced/browser-pdf.js +0 -407
- package/src/tools/browser/advanced/browser-tabs.js +0 -497
- package/src/tools/browser/advanced/browser-wait.js +0 -378
- package/src/tools/browser/click.js +0 -168
- package/src/tools/browser/close.js +0 -60
- package/src/tools/browser/dom.js +0 -70
- package/src/tools/browser/launch.js +0 -67
- package/src/tools/browser/navigate.js +0 -270
- package/src/tools/browser/screenshot.js +0 -351
- package/src/tools/browser/type.js +0 -174
- package/src/tools/index.js +0 -95
- package/src/utils/agentInstaller.js +0 -418
- package/src/utils/browserHelpers.js +0 -83
|
@@ -1,2598 +0,0 @@
|
|
|
1
|
-
const ToolBase = require('../base/ToolBase');
|
|
2
|
-
const https = require('https');
|
|
3
|
-
const http = require('http');
|
|
4
|
-
const { URL } = require('url');
|
|
5
|
-
const fs = require('fs').promises;
|
|
6
|
-
const path = require('path');
|
|
7
|
-
|
|
8
|
-
// Try to load GraphQL for SDL parsing
|
|
9
|
-
let graphql;
|
|
10
|
-
try {
|
|
11
|
-
graphql = require('graphql');
|
|
12
|
-
if (!graphql.buildSchema || !graphql.introspectionFromSchema || !graphql.getIntrospectionQuery) {
|
|
13
|
-
console.warn('GraphQL loaded but required functions not available');
|
|
14
|
-
graphql = null;
|
|
15
|
-
}
|
|
16
|
-
} catch (error) {
|
|
17
|
-
console.warn('GraphQL library not available, SDL files will not be supported:', error.message);
|
|
18
|
-
graphql = null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Try to load faker.js for enhanced sample data generation
|
|
22
|
-
let faker;
|
|
23
|
-
try {
|
|
24
|
-
const fakerModule = require('@faker-js/faker');
|
|
25
|
-
faker = fakerModule.faker || fakerModule.default?.faker || fakerModule;
|
|
26
|
-
if (!faker || typeof faker.person?.firstName !== 'function') {
|
|
27
|
-
console.warn('Faker.js loaded but API not as expected, using fallback');
|
|
28
|
-
faker = null;
|
|
29
|
-
}
|
|
30
|
-
} catch (error) {
|
|
31
|
-
console.warn('Faker.js not available, using built-in sample generation:', error.message);
|
|
32
|
-
faker = null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
let z;
|
|
36
|
-
try {
|
|
37
|
-
const zod = require('zod');
|
|
38
|
-
z = zod.z || zod.default?.z || zod;
|
|
39
|
-
if (!z || typeof z.object !== 'function') {
|
|
40
|
-
throw new Error('Zod not properly loaded');
|
|
41
|
-
}
|
|
42
|
-
} catch (error) {
|
|
43
|
-
console.error('Failed to load Zod:', error.message);
|
|
44
|
-
// Fallback: create a simple validation function
|
|
45
|
-
z = {
|
|
46
|
-
object: (schema) => ({ parse: (data) => data }),
|
|
47
|
-
string: () => ({ optional: () => ({}) }),
|
|
48
|
-
enum: () => ({ optional: () => ({}) }),
|
|
49
|
-
record: () => ({ optional: () => ({}) }),
|
|
50
|
-
any: () => ({ optional: () => ({}) }),
|
|
51
|
-
number: () => ({ optional: () => ({}) }),
|
|
52
|
-
array: () => ({ optional: () => ({}) }),
|
|
53
|
-
union: () => ({ optional: () => ({}) })
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Input schema for the API planner tool
|
|
58
|
-
const apiPlannerInputSchema = z.object({
|
|
59
|
-
schemaUrl: z.string().optional(),
|
|
60
|
-
schemaPath: z.string().optional(),
|
|
61
|
-
schemaType: z.enum(['openapi', 'swagger', 'graphql', 'auto']).optional(),
|
|
62
|
-
apiBaseUrl: z.string().optional(),
|
|
63
|
-
includeAuth: z.boolean().optional(),
|
|
64
|
-
includeSecurity: z.boolean().optional(),
|
|
65
|
-
includeErrorHandling: z.boolean().optional(),
|
|
66
|
-
outputPath: z.string().optional(),
|
|
67
|
-
testCategories: z.array(z.enum(['functional', 'security', 'performance', 'integration', 'edge-cases'])).optional(),
|
|
68
|
-
validateEndpoints: z.boolean().optional(),
|
|
69
|
-
validationSampleSize: z.number().optional(),
|
|
70
|
-
validationTimeout: z.number().optional()
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* API Planner Tool - Analyze API schemas and generate comprehensive test plans
|
|
75
|
-
*/
|
|
76
|
-
class ApiPlannerTool extends ToolBase {
|
|
77
|
-
static definition = {
|
|
78
|
-
name: "api_planner",
|
|
79
|
-
description: "Analyze API schemas (OpenAPI/Swagger, GraphQL) and generate comprehensive test plans with detailed scenarios covering functional, security, and edge case testing.",
|
|
80
|
-
input_schema: {
|
|
81
|
-
type: "object",
|
|
82
|
-
properties: {
|
|
83
|
-
schemaUrl: {
|
|
84
|
-
type: "string",
|
|
85
|
-
description: "URL to fetch the API schema/documentation (e.g., OpenAPI spec URL, GraphQL introspection endpoint)"
|
|
86
|
-
},
|
|
87
|
-
schemaPath: {
|
|
88
|
-
type: "string",
|
|
89
|
-
description: "File path to a local schema file (.graphql, .json, .yaml, .yml). GraphQL SDL files (.graphql, .gql) are automatically converted to introspection JSON. For large files, this is the recommended approach as it reads the complete file without truncation."
|
|
90
|
-
},
|
|
91
|
-
schemaType: {
|
|
92
|
-
type: "string",
|
|
93
|
-
enum: ["openapi", "swagger", "graphql", "auto"],
|
|
94
|
-
description: "Type of API schema - auto-detects if not specified"
|
|
95
|
-
},
|
|
96
|
-
apiBaseUrl: {
|
|
97
|
-
type: "string",
|
|
98
|
-
description: "Base URL of the API to be tested (overrides schema baseUrl if provided)"
|
|
99
|
-
},
|
|
100
|
-
includeAuth: {
|
|
101
|
-
type: "boolean",
|
|
102
|
-
description: "Include authentication testing scenarios (default: true)"
|
|
103
|
-
},
|
|
104
|
-
includeSecurity: {
|
|
105
|
-
type: "boolean",
|
|
106
|
-
description: "Include security testing scenarios (default: true)"
|
|
107
|
-
},
|
|
108
|
-
includeErrorHandling: {
|
|
109
|
-
type: "boolean",
|
|
110
|
-
description: "Include error handling and edge case scenarios (default: true)"
|
|
111
|
-
},
|
|
112
|
-
outputPath: {
|
|
113
|
-
type: "string",
|
|
114
|
-
description: "File path to save the generated test plan (default: ./api-test-plan.md)"
|
|
115
|
-
},
|
|
116
|
-
testCategories: {
|
|
117
|
-
type: "array",
|
|
118
|
-
items: {
|
|
119
|
-
type: "string",
|
|
120
|
-
enum: ["functional", "security", "performance", "integration", "edge-cases"]
|
|
121
|
-
},
|
|
122
|
-
description: "Categories of tests to include (default: all categories)"
|
|
123
|
-
},
|
|
124
|
-
validateEndpoints: {
|
|
125
|
-
type: "boolean",
|
|
126
|
-
description: "Validate endpoints by making actual API calls to verify they work (default: false)"
|
|
127
|
-
},
|
|
128
|
-
validationSampleSize: {
|
|
129
|
-
type: "number",
|
|
130
|
-
description: "Number of endpoints to validate when validateEndpoints is true (default: 3, use -1 for all)"
|
|
131
|
-
},
|
|
132
|
-
validationTimeout: {
|
|
133
|
-
type: "number",
|
|
134
|
-
description: "Timeout in milliseconds for each validation request (default: 5000)"
|
|
135
|
-
}
|
|
136
|
-
},
|
|
137
|
-
oneOf: [
|
|
138
|
-
{ required: ["schemaUrl"] },
|
|
139
|
-
{ required: ["schemaContent"] }
|
|
140
|
-
]
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
constructor() {
|
|
145
|
-
super();
|
|
146
|
-
this.fs = require('fs');
|
|
147
|
-
this.path = require('path');
|
|
148
|
-
this.yaml = this._loadYaml();
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
_loadYaml() {
|
|
152
|
-
try {
|
|
153
|
-
return require('yaml');
|
|
154
|
-
} catch (error) {
|
|
155
|
-
console.warn('[ApiPlanner] YAML library not available, JSON-only parsing');
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
async execute(parameters) {
|
|
161
|
-
try {
|
|
162
|
-
const params = apiPlannerInputSchema.parse(parameters);
|
|
163
|
-
|
|
164
|
-
// Validate apiBaseUrl if provided
|
|
165
|
-
if (params.apiBaseUrl && !params.apiBaseUrl.match(/^https?:\/\//)) {
|
|
166
|
-
throw new Error(
|
|
167
|
-
`Invalid apiBaseUrl: "${params.apiBaseUrl}". ` +
|
|
168
|
-
`The apiBaseUrl must be a full URL starting with http:// or https://, not a relative path. ` +
|
|
169
|
-
`Example: "https://petstore3.swagger.io/api/v3" instead of "/api/v3". ` +
|
|
170
|
-
`If you want to use the base URL from the OpenAPI schema, omit the apiBaseUrl parameter.`
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Set defaults
|
|
175
|
-
const options = {
|
|
176
|
-
schemaType: params.schemaType || 'auto',
|
|
177
|
-
includeAuth: params.includeAuth !== false,
|
|
178
|
-
includeSecurity: params.includeSecurity !== false,
|
|
179
|
-
includeErrorHandling: params.includeErrorHandling !== false,
|
|
180
|
-
outputPath: params.outputPath || './api-test-plan.md',
|
|
181
|
-
testCategories: params.testCategories || ['functional', 'security', 'performance', 'integration', 'edge-cases'],
|
|
182
|
-
apiBaseUrl: params.apiBaseUrl,
|
|
183
|
-
validateEndpoints: params.validateEndpoints || false,
|
|
184
|
-
validationSampleSize: params.validationSampleSize || 3,
|
|
185
|
-
validationTimeout: params.validationTimeout || 5000
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
// Fetch or parse schema
|
|
189
|
-
let schemaData;
|
|
190
|
-
if (params.schemaUrl) {
|
|
191
|
-
schemaData = await this._fetchSchema(params.schemaUrl, options.schemaType);
|
|
192
|
-
} else if (params.schemaPath) {
|
|
193
|
-
schemaData = await this._readSchemaFromFile(params.schemaPath, options.schemaType);
|
|
194
|
-
} else {
|
|
195
|
-
throw new Error('Either schemaUrl or schemaPath must be provided. For local schema files, use schemaPath parameter.');
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (!schemaData) {
|
|
199
|
-
throw new Error('Failed to load or parse API schema');
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Auto-detect schema type if needed
|
|
203
|
-
if (options.schemaType === 'auto') {
|
|
204
|
-
options.schemaType = this._detectSchemaType(schemaData);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Generate test plan based on schema type
|
|
208
|
-
let testPlan;
|
|
209
|
-
switch (options.schemaType) {
|
|
210
|
-
case 'openapi':
|
|
211
|
-
case 'swagger':
|
|
212
|
-
testPlan = await this._generateOpenApiTestPlan(schemaData, options);
|
|
213
|
-
break;
|
|
214
|
-
case 'graphql':
|
|
215
|
-
testPlan = await this._generateGraphQLTestPlan(schemaData, options);
|
|
216
|
-
break;
|
|
217
|
-
default:
|
|
218
|
-
throw new Error(`Unsupported schema type: ${options.schemaType}`);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Save test plan to file
|
|
222
|
-
if (options.outputPath) {
|
|
223
|
-
await this._saveTestPlan(testPlan, options.outputPath);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const result = {
|
|
227
|
-
success: true,
|
|
228
|
-
message: "API test plan generated successfully",
|
|
229
|
-
schemaType: options.schemaType,
|
|
230
|
-
outputPath: options.outputPath,
|
|
231
|
-
endpoints: testPlan.summary.totalEndpoints,
|
|
232
|
-
scenarios: testPlan.summary.totalScenarios,
|
|
233
|
-
testPlan: testPlan.content
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
// Add validation results if available
|
|
237
|
-
if (testPlan.validationSummary) {
|
|
238
|
-
result.validationSummary = testPlan.validationSummary;
|
|
239
|
-
result.message = `API test plan generated successfully. Validated ${testPlan.validationSummary.totalValidated} endpoints (${testPlan.validationSummary.successful} successful, ${testPlan.validationSummary.failed} failed)`;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return result;
|
|
243
|
-
|
|
244
|
-
} catch (error) {
|
|
245
|
-
return {
|
|
246
|
-
success: false,
|
|
247
|
-
error: error.message,
|
|
248
|
-
details: error.stack
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
async _fetchSchema(url, schemaType = 'auto') {
|
|
254
|
-
// If schemaType is 'graphql' or if URL suggests GraphQL endpoint, perform introspection
|
|
255
|
-
const isGraphQL = schemaType === 'graphql' ||
|
|
256
|
-
url.toLowerCase().includes('graphql') ||
|
|
257
|
-
url.toLowerCase().includes('/graph');
|
|
258
|
-
|
|
259
|
-
if (isGraphQL) {
|
|
260
|
-
return await this._fetchGraphQLSchema(url);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return new Promise((resolve, reject) => {
|
|
264
|
-
const urlObj = new URL(url);
|
|
265
|
-
const client = urlObj.protocol === 'https:' ? https : http;
|
|
266
|
-
|
|
267
|
-
const options = {
|
|
268
|
-
hostname: urlObj.hostname,
|
|
269
|
-
port: urlObj.port,
|
|
270
|
-
path: urlObj.pathname + urlObj.search,
|
|
271
|
-
method: 'GET',
|
|
272
|
-
headers: {
|
|
273
|
-
'Accept': 'application/json, application/yaml, text/yaml',
|
|
274
|
-
'User-Agent': 'Democratize-Quality-MCP-Server/1.0'
|
|
275
|
-
}
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
const req = client.request(options, (res) => {
|
|
279
|
-
let data = '';
|
|
280
|
-
|
|
281
|
-
res.on('data', (chunk) => {
|
|
282
|
-
data += chunk;
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
res.on('end', () => {
|
|
286
|
-
try {
|
|
287
|
-
const schema = this._parseSchemaContent(data, 'auto');
|
|
288
|
-
resolve(schema);
|
|
289
|
-
} catch (error) {
|
|
290
|
-
reject(new Error(`Failed to parse schema: ${error.message}`));
|
|
291
|
-
}
|
|
292
|
-
});
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
req.on('error', (error) => {
|
|
296
|
-
reject(new Error(`Failed to fetch schema: ${error.message}`));
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
req.setTimeout(30000, () => {
|
|
300
|
-
req.destroy();
|
|
301
|
-
reject(new Error('Schema fetch timeout'));
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
req.end();
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Fetch GraphQL schema via introspection query
|
|
310
|
-
* GENERIC - Works with any GraphQL endpoint
|
|
311
|
-
*/
|
|
312
|
-
async _fetchGraphQLSchema(url) {
|
|
313
|
-
return new Promise((resolve, reject) => {
|
|
314
|
-
const urlObj = new URL(url);
|
|
315
|
-
const client = urlObj.protocol === 'https:' ? https : http;
|
|
316
|
-
|
|
317
|
-
const introspectionQuery = this._getGraphQLIntrospectionQuery();
|
|
318
|
-
const postData = JSON.stringify({
|
|
319
|
-
query: introspectionQuery
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
const options = {
|
|
323
|
-
hostname: urlObj.hostname,
|
|
324
|
-
port: urlObj.port,
|
|
325
|
-
path: urlObj.pathname + urlObj.search,
|
|
326
|
-
method: 'POST',
|
|
327
|
-
headers: {
|
|
328
|
-
'Content-Type': 'application/json',
|
|
329
|
-
'Accept': 'application/json',
|
|
330
|
-
'User-Agent': 'Democratize-Quality-MCP-Server/1.0',
|
|
331
|
-
'Content-Length': Buffer.byteLength(postData)
|
|
332
|
-
}
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
const req = client.request(options, (res) => {
|
|
336
|
-
let data = '';
|
|
337
|
-
|
|
338
|
-
res.on('data', (chunk) => {
|
|
339
|
-
data += chunk;
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
res.on('end', () => {
|
|
343
|
-
try {
|
|
344
|
-
const response = JSON.parse(data);
|
|
345
|
-
|
|
346
|
-
// Check for GraphQL errors
|
|
347
|
-
if (response.errors) {
|
|
348
|
-
reject(new Error(`GraphQL introspection failed: ${response.errors[0].message}`));
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Return the introspection result
|
|
353
|
-
resolve(response);
|
|
354
|
-
} catch (error) {
|
|
355
|
-
reject(new Error(`Failed to parse GraphQL response: ${error.message}`));
|
|
356
|
-
}
|
|
357
|
-
});
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
req.on('error', (error) => {
|
|
361
|
-
reject(new Error(`Failed to fetch GraphQL schema: ${error.message}`));
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
req.setTimeout(30000, () => {
|
|
365
|
-
req.destroy();
|
|
366
|
-
reject(new Error('GraphQL introspection timeout'));
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
req.write(postData);
|
|
370
|
-
req.end();
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Read schema from local file system
|
|
376
|
-
*/
|
|
377
|
-
async _readSchemaFromFile(filePath, schemaType = 'auto') {
|
|
378
|
-
try {
|
|
379
|
-
// Resolve absolute path
|
|
380
|
-
const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
|
|
381
|
-
|
|
382
|
-
// Check file exists
|
|
383
|
-
try {
|
|
384
|
-
await fs.access(absolutePath);
|
|
385
|
-
} catch (error) {
|
|
386
|
-
throw new Error(`Schema file not found: ${absolutePath}`);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// Read file content
|
|
390
|
-
const content = await fs.readFile(absolutePath, 'utf-8');
|
|
391
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
392
|
-
|
|
393
|
-
// Handle .graphql files
|
|
394
|
-
if (ext === '.graphql' || ext === '.gql') {
|
|
395
|
-
// Check if content is GraphQL SDL or introspection JSON
|
|
396
|
-
const trimmedContent = content.trim();
|
|
397
|
-
|
|
398
|
-
// If it starts with '{', it's likely JSON introspection result
|
|
399
|
-
if (trimmedContent.startsWith('{')) {
|
|
400
|
-
try {
|
|
401
|
-
return JSON.parse(content);
|
|
402
|
-
} catch (error) {
|
|
403
|
-
throw new Error(`GraphQL file contains invalid JSON: ${error.message}`);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Otherwise it's SDL (Schema Definition Language) - convert it
|
|
408
|
-
console.log(`📝 Detected GraphQL SDL in ${path.basename(filePath)}, converting to introspection JSON...`);
|
|
409
|
-
|
|
410
|
-
if (!graphql) {
|
|
411
|
-
throw new Error(
|
|
412
|
-
`GraphQL SDL detected but 'graphql' package is not installed.\n` +
|
|
413
|
-
`Please install it: npm install graphql\n` +
|
|
414
|
-
`Or convert manually: npx graphql-cli introspect ${filePath}`
|
|
415
|
-
);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
try {
|
|
419
|
-
// Build schema from SDL
|
|
420
|
-
const schema = graphql.buildSchema(content);
|
|
421
|
-
|
|
422
|
-
// Generate introspection query result
|
|
423
|
-
const introspectionQuery = graphql.getIntrospectionQuery();
|
|
424
|
-
const introspectionResult = graphql.introspectionFromSchema(schema);
|
|
425
|
-
|
|
426
|
-
// Create output filename (e.g., schema.graphql -> schema.json)
|
|
427
|
-
const baseName = path.basename(filePath, ext);
|
|
428
|
-
const outputPath = path.join(path.dirname(absolutePath), `${baseName}.json`);
|
|
429
|
-
|
|
430
|
-
// Save introspection JSON file
|
|
431
|
-
await fs.writeFile(outputPath, JSON.stringify(introspectionResult, null, 2));
|
|
432
|
-
console.log(`✅ Introspection JSON saved to: ${outputPath}`);
|
|
433
|
-
|
|
434
|
-
return introspectionResult;
|
|
435
|
-
} catch (error) {
|
|
436
|
-
throw new Error(
|
|
437
|
-
`Failed to convert GraphQL SDL to introspection JSON: ${error.message}\n` +
|
|
438
|
-
`Please check that your SDL syntax is valid.`
|
|
439
|
-
);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Parse JSON or YAML content
|
|
444
|
-
return this._parseSchemaContent(content, schemaType);
|
|
445
|
-
} catch (error) {
|
|
446
|
-
// Re-throw with original message if it's already formatted
|
|
447
|
-
if (error.message.includes('GraphQL SDL') || error.message.includes('Schema file not found')) {
|
|
448
|
-
throw error;
|
|
449
|
-
}
|
|
450
|
-
throw new Error(`Failed to read schema file: ${error.message}`);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
/**
|
|
455
|
-
* Parse schema content (JSON/YAML) - Internal method used by _fetchSchema and _readSchemaFromFile
|
|
456
|
-
* Note: SDL conversion is handled in _readSchemaFromFile before reaching this method
|
|
457
|
-
*/
|
|
458
|
-
_parseSchemaContent(content, schemaType) {
|
|
459
|
-
const trimmedContent = content.trim();
|
|
460
|
-
|
|
461
|
-
try {
|
|
462
|
-
// Try JSON first
|
|
463
|
-
return JSON.parse(content);
|
|
464
|
-
} catch (jsonError) {
|
|
465
|
-
// Try YAML if available
|
|
466
|
-
if (this.yaml) {
|
|
467
|
-
try {
|
|
468
|
-
return this.yaml.parse(content);
|
|
469
|
-
} catch (yamlError) {
|
|
470
|
-
throw new Error(`Failed to parse as JSON or YAML: ${jsonError.message}`);
|
|
471
|
-
}
|
|
472
|
-
} else {
|
|
473
|
-
throw new Error(`Failed to parse as JSON: ${jsonError.message}`);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
/**
|
|
479
|
-
* Validate endpoints by making actual API calls
|
|
480
|
-
*/
|
|
481
|
-
async _validateEndpoints(testPlan, baseUrl, options) {
|
|
482
|
-
if (!options.validateEndpoints) {
|
|
483
|
-
return testPlan;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
console.log(`\n🔍 Validating endpoints (sample size: ${options.validationSampleSize})...`);
|
|
487
|
-
|
|
488
|
-
const validationResults = {
|
|
489
|
-
totalValidated: 0,
|
|
490
|
-
successful: 0,
|
|
491
|
-
failed: 0,
|
|
492
|
-
results: []
|
|
493
|
-
};
|
|
494
|
-
|
|
495
|
-
// Collect all scenarios that can be validated (happy path scenarios)
|
|
496
|
-
const validatableScenarios = [];
|
|
497
|
-
for (const section of testPlan.sections) {
|
|
498
|
-
if (section.scenarios) {
|
|
499
|
-
for (const scenario of section.scenarios) {
|
|
500
|
-
// Only validate happy path scenarios (not error scenarios)
|
|
501
|
-
if (scenario.title && scenario.title.includes('Happy Path')) {
|
|
502
|
-
validatableScenarios.push({ section, scenario });
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// Determine how many to validate
|
|
509
|
-
const sampleSize = options.validationSampleSize === -1
|
|
510
|
-
? validatableScenarios.length
|
|
511
|
-
: Math.min(options.validationSampleSize, validatableScenarios.length);
|
|
512
|
-
|
|
513
|
-
// Select sample scenarios (evenly distributed)
|
|
514
|
-
const step = Math.floor(validatableScenarios.length / sampleSize) || 1;
|
|
515
|
-
const selectedScenarios = [];
|
|
516
|
-
for (let i = 0; i < validatableScenarios.length && selectedScenarios.length < sampleSize; i += step) {
|
|
517
|
-
selectedScenarios.push(validatableScenarios[i]);
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// Validate each selected scenario
|
|
521
|
-
for (const { section, scenario } of selectedScenarios) {
|
|
522
|
-
const validationResult = await this._validateEndpoint(scenario, baseUrl, options);
|
|
523
|
-
validationResults.totalValidated++;
|
|
524
|
-
|
|
525
|
-
if (validationResult.success) {
|
|
526
|
-
validationResults.successful++;
|
|
527
|
-
scenario.validation = validationResult;
|
|
528
|
-
} else {
|
|
529
|
-
validationResults.failed++;
|
|
530
|
-
scenario.validation = validationResult;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
validationResults.results.push({
|
|
534
|
-
endpoint: `${scenario.method} ${scenario.endpoint}`,
|
|
535
|
-
...validationResult
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
console.log(` ${validationResult.success ? '✅' : '❌'} ${scenario.method} ${scenario.endpoint} - ${validationResult.success ? 'SUCCESS' : validationResult.error}`);
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// Add validation summary to test plan
|
|
542
|
-
testPlan.validationSummary = validationResults;
|
|
543
|
-
|
|
544
|
-
console.log(`\n✨ Validation complete: ${validationResults.successful}/${validationResults.totalValidated} successful\n`);
|
|
545
|
-
|
|
546
|
-
return testPlan;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
/**
|
|
550
|
-
* Validate a single endpoint by making an actual API call
|
|
551
|
-
*/
|
|
552
|
-
async _validateEndpoint(scenario, baseUrl, options) {
|
|
553
|
-
try {
|
|
554
|
-
// Construct full URL
|
|
555
|
-
const endpoint = scenario.endpoint || '';
|
|
556
|
-
// Replace path parameters with sample values
|
|
557
|
-
const processedEndpoint = endpoint.replace(/{([^}]+)}/g, (match, param) => {
|
|
558
|
-
// Use common test IDs
|
|
559
|
-
if (param.toLowerCase().includes('id')) return '1';
|
|
560
|
-
return 'test-value';
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
const fullUrl = baseUrl.replace(/\/$/, '') + processedEndpoint;
|
|
564
|
-
|
|
565
|
-
// Prepare headers - ensure Content-Type is set for requests with body
|
|
566
|
-
const headers = { ...(scenario.headers || {}) };
|
|
567
|
-
if (scenario.data && !headers['Content-Type'] && !headers['content-type']) {
|
|
568
|
-
headers['Content-Type'] = 'application/json';
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
// Prepare request
|
|
572
|
-
const requestData = {
|
|
573
|
-
method: scenario.method || 'GET',
|
|
574
|
-
url: fullUrl,
|
|
575
|
-
headers: headers,
|
|
576
|
-
expect: scenario.expect
|
|
577
|
-
};
|
|
578
|
-
|
|
579
|
-
// Only add data for methods that support request body
|
|
580
|
-
if (['POST', 'PUT', 'PATCH'].includes(scenario.method) && scenario.data) {
|
|
581
|
-
requestData.data = scenario.data;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// Add query parameters if present
|
|
585
|
-
if (scenario.query) {
|
|
586
|
-
requestData.query = scenario.query;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// Import api_request tool
|
|
590
|
-
const ApiRequestTool = require('./api-request.js');
|
|
591
|
-
const apiRequest = new ApiRequestTool();
|
|
592
|
-
|
|
593
|
-
const startTime = Date.now();
|
|
594
|
-
|
|
595
|
-
// Make the request with timeout
|
|
596
|
-
const timeoutPromise = new Promise((_, reject) =>
|
|
597
|
-
setTimeout(() => reject(new Error('Request timeout')), options.validationTimeout)
|
|
598
|
-
);
|
|
599
|
-
|
|
600
|
-
const requestPromise = apiRequest.execute(requestData);
|
|
601
|
-
const result = await Promise.race([requestPromise, timeoutPromise]);
|
|
602
|
-
|
|
603
|
-
const responseTime = Date.now() - startTime;
|
|
604
|
-
|
|
605
|
-
if (result.success) {
|
|
606
|
-
return {
|
|
607
|
-
success: true,
|
|
608
|
-
statusCode: result.statusCode,
|
|
609
|
-
responseTime: responseTime,
|
|
610
|
-
responseBody: result.body,
|
|
611
|
-
responseHeaders: result.headers
|
|
612
|
-
};
|
|
613
|
-
} else {
|
|
614
|
-
return {
|
|
615
|
-
success: false,
|
|
616
|
-
error: result.error || 'Unknown error',
|
|
617
|
-
responseTime: responseTime
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
} catch (error) {
|
|
621
|
-
return {
|
|
622
|
-
success: false,
|
|
623
|
-
error: error.message || 'Validation failed',
|
|
624
|
-
responseTime: 0
|
|
625
|
-
};
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
_detectSchemaType(schema) {
|
|
630
|
-
if (schema.openapi) {
|
|
631
|
-
return 'openapi';
|
|
632
|
-
} else if (schema.swagger) {
|
|
633
|
-
return 'swagger';
|
|
634
|
-
} else if (schema.__schema || schema.data?.__schema) {
|
|
635
|
-
return 'graphql';
|
|
636
|
-
} else {
|
|
637
|
-
throw new Error('Unable to detect schema type - must be OpenAPI/Swagger or GraphQL');
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
async _generateOpenApiTestPlan(schema, options) {
|
|
642
|
-
const baseUrl = options.apiBaseUrl || this._getBaseUrl(schema);
|
|
643
|
-
const paths = schema.paths || {};
|
|
644
|
-
const components = schema.components || {};
|
|
645
|
-
const security = schema.security || [];
|
|
646
|
-
|
|
647
|
-
const testPlan = {
|
|
648
|
-
summary: {
|
|
649
|
-
title: schema.info?.title || 'API Test Plan',
|
|
650
|
-
version: schema.info?.version || '1.0.0',
|
|
651
|
-
description: schema.info?.description || '',
|
|
652
|
-
baseUrl: baseUrl,
|
|
653
|
-
totalEndpoints: 0,
|
|
654
|
-
totalScenarios: 0
|
|
655
|
-
},
|
|
656
|
-
sections: []
|
|
657
|
-
};
|
|
658
|
-
|
|
659
|
-
// Authentication section
|
|
660
|
-
if (options.includeAuth && (security.length > 0 || components.securitySchemes)) {
|
|
661
|
-
const authSection = this._generateAuthTestSection(components.securitySchemes, security);
|
|
662
|
-
testPlan.sections.push(authSection);
|
|
663
|
-
testPlan.summary.totalScenarios += authSection.scenarios.length;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// Endpoint sections
|
|
667
|
-
for (const [path, pathItem] of Object.entries(paths)) {
|
|
668
|
-
for (const [method, operation] of Object.entries(pathItem)) {
|
|
669
|
-
if (['get', 'post', 'put', 'delete', 'patch', 'head', 'options'].includes(method.toLowerCase())) {
|
|
670
|
-
testPlan.summary.totalEndpoints++;
|
|
671
|
-
|
|
672
|
-
const endpointSection = this._generateEndpointTestSection(
|
|
673
|
-
path, method.toUpperCase(), operation, components, options
|
|
674
|
-
);
|
|
675
|
-
|
|
676
|
-
testPlan.sections.push(endpointSection);
|
|
677
|
-
testPlan.summary.totalScenarios += endpointSection.scenarios.length;
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
// Security testing section
|
|
683
|
-
if (options.includeSecurity) {
|
|
684
|
-
const securitySection = this._generateSecurityTestSection(schema, options);
|
|
685
|
-
testPlan.sections.push(securitySection);
|
|
686
|
-
testPlan.summary.totalScenarios += securitySection.scenarios.length;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// Integration testing section
|
|
690
|
-
if (options.testCategories.includes('integration')) {
|
|
691
|
-
const integrationSection = this._generateIntegrationTestSection(schema, options);
|
|
692
|
-
testPlan.sections.push(integrationSection);
|
|
693
|
-
testPlan.summary.totalScenarios += integrationSection.scenarios.length;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
// Validate endpoints if requested
|
|
697
|
-
if (options.validateEndpoints && baseUrl) {
|
|
698
|
-
await this._validateEndpoints(testPlan, baseUrl, options);
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
testPlan.content = this._generateMarkdownContent(testPlan);
|
|
702
|
-
return testPlan;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
_getBaseUrl(schema) {
|
|
706
|
-
if (schema.servers && schema.servers.length > 0) {
|
|
707
|
-
return schema.servers[0].url;
|
|
708
|
-
} else if (schema.host) {
|
|
709
|
-
const scheme = schema.schemes ? schema.schemes[0] : 'https';
|
|
710
|
-
const basePath = schema.basePath || '';
|
|
711
|
-
return `${scheme}://${schema.host}${basePath}`;
|
|
712
|
-
}
|
|
713
|
-
return 'https://api.example.com';
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
_generateAuthTestSection(securitySchemes, security) {
|
|
717
|
-
const scenarios = [];
|
|
718
|
-
|
|
719
|
-
if (securitySchemes) {
|
|
720
|
-
for (const [schemeName, scheme] of Object.entries(securitySchemes)) {
|
|
721
|
-
switch (scheme.type) {
|
|
722
|
-
case 'http':
|
|
723
|
-
if (scheme.scheme === 'bearer') {
|
|
724
|
-
scenarios.push(this._generateBearerTokenScenarios(schemeName));
|
|
725
|
-
} else if (scheme.scheme === 'basic') {
|
|
726
|
-
scenarios.push(this._generateBasicAuthScenarios(schemeName));
|
|
727
|
-
}
|
|
728
|
-
break;
|
|
729
|
-
case 'apiKey':
|
|
730
|
-
scenarios.push(this._generateApiKeyScenarios(schemeName, scheme));
|
|
731
|
-
break;
|
|
732
|
-
case 'oauth2':
|
|
733
|
-
scenarios.push(this._generateOAuth2Scenarios(schemeName, scheme));
|
|
734
|
-
break;
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
return {
|
|
740
|
-
title: 'Authentication Testing',
|
|
741
|
-
description: 'Test various authentication mechanisms and security scenarios',
|
|
742
|
-
scenarios: scenarios.flat()
|
|
743
|
-
};
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
_generateBearerTokenScenarios(schemeName) {
|
|
747
|
-
return [
|
|
748
|
-
{
|
|
749
|
-
title: `Valid ${schemeName} Token`,
|
|
750
|
-
method: 'GET',
|
|
751
|
-
endpoint: '/protected-endpoint',
|
|
752
|
-
headers: {
|
|
753
|
-
'Authorization': 'Bearer {{valid_token}}'
|
|
754
|
-
},
|
|
755
|
-
expect: {
|
|
756
|
-
status: 200
|
|
757
|
-
},
|
|
758
|
-
description: 'Test access with valid bearer token'
|
|
759
|
-
},
|
|
760
|
-
{
|
|
761
|
-
title: `Invalid ${schemeName} Token`,
|
|
762
|
-
method: 'GET',
|
|
763
|
-
endpoint: '/protected-endpoint',
|
|
764
|
-
headers: {
|
|
765
|
-
'Authorization': 'Bearer invalid_token'
|
|
766
|
-
},
|
|
767
|
-
expect: {
|
|
768
|
-
status: 401
|
|
769
|
-
},
|
|
770
|
-
description: 'Test rejection of invalid bearer token'
|
|
771
|
-
},
|
|
772
|
-
{
|
|
773
|
-
title: `Missing ${schemeName} Token`,
|
|
774
|
-
method: 'GET',
|
|
775
|
-
endpoint: '/protected-endpoint',
|
|
776
|
-
headers: {},
|
|
777
|
-
expect: {
|
|
778
|
-
status: 401
|
|
779
|
-
},
|
|
780
|
-
description: 'Test rejection when no authorization header provided'
|
|
781
|
-
}
|
|
782
|
-
];
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
_generateBasicAuthScenarios(schemeName) {
|
|
786
|
-
return [
|
|
787
|
-
{
|
|
788
|
-
title: `Valid ${schemeName} Credentials`,
|
|
789
|
-
method: 'GET',
|
|
790
|
-
endpoint: '/protected-endpoint',
|
|
791
|
-
headers: {
|
|
792
|
-
'Authorization': 'Basic {{valid_basic_auth}}'
|
|
793
|
-
},
|
|
794
|
-
expect: {
|
|
795
|
-
status: 200
|
|
796
|
-
},
|
|
797
|
-
description: 'Test access with valid basic auth credentials'
|
|
798
|
-
},
|
|
799
|
-
{
|
|
800
|
-
title: `Invalid ${schemeName} Credentials`,
|
|
801
|
-
method: 'GET',
|
|
802
|
-
endpoint: '/protected-endpoint',
|
|
803
|
-
headers: {
|
|
804
|
-
'Authorization': 'Basic aW52YWxpZDppbnZhbGlk'
|
|
805
|
-
},
|
|
806
|
-
expect: {
|
|
807
|
-
status: 401
|
|
808
|
-
},
|
|
809
|
-
description: 'Test rejection of invalid basic auth credentials'
|
|
810
|
-
}
|
|
811
|
-
];
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
_generateApiKeyScenarios(schemeName, scheme) {
|
|
815
|
-
const location = scheme.in;
|
|
816
|
-
const keyName = scheme.name;
|
|
817
|
-
|
|
818
|
-
const scenarios = [
|
|
819
|
-
{
|
|
820
|
-
title: `Valid ${schemeName} API Key`,
|
|
821
|
-
method: 'GET',
|
|
822
|
-
endpoint: '/protected-endpoint',
|
|
823
|
-
expect: {
|
|
824
|
-
status: 200
|
|
825
|
-
},
|
|
826
|
-
description: `Test access with valid API key in ${location}`
|
|
827
|
-
},
|
|
828
|
-
{
|
|
829
|
-
title: `Invalid ${schemeName} API Key`,
|
|
830
|
-
method: 'GET',
|
|
831
|
-
endpoint: '/protected-endpoint',
|
|
832
|
-
expect: {
|
|
833
|
-
status: 401
|
|
834
|
-
},
|
|
835
|
-
description: `Test rejection of invalid API key in ${location}`
|
|
836
|
-
}
|
|
837
|
-
];
|
|
838
|
-
|
|
839
|
-
// Add location-specific parameters
|
|
840
|
-
scenarios.forEach(scenario => {
|
|
841
|
-
if (location === 'header') {
|
|
842
|
-
scenario.headers = scenario.headers || {};
|
|
843
|
-
scenario.headers[keyName] = scenario.title.includes('Valid') ? '{{valid_api_key}}' : 'invalid_key';
|
|
844
|
-
} else if (location === 'query') {
|
|
845
|
-
scenario.query = scenario.query || {};
|
|
846
|
-
scenario.query[keyName] = scenario.title.includes('Valid') ? '{{valid_api_key}}' : 'invalid_key';
|
|
847
|
-
}
|
|
848
|
-
});
|
|
849
|
-
|
|
850
|
-
return scenarios;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
_generateOAuth2Scenarios(schemeName, scheme) {
|
|
854
|
-
return [
|
|
855
|
-
{
|
|
856
|
-
title: `OAuth2 Authorization Code Flow - ${schemeName}`,
|
|
857
|
-
method: 'POST',
|
|
858
|
-
endpoint: '/oauth/token',
|
|
859
|
-
data: {
|
|
860
|
-
grant_type: 'authorization_code',
|
|
861
|
-
code: '{{auth_code}}',
|
|
862
|
-
client_id: '{{client_id}}',
|
|
863
|
-
client_secret: '{{client_secret}}'
|
|
864
|
-
},
|
|
865
|
-
expect: {
|
|
866
|
-
status: 200,
|
|
867
|
-
body: {
|
|
868
|
-
access_token: 'string',
|
|
869
|
-
token_type: 'Bearer'
|
|
870
|
-
}
|
|
871
|
-
},
|
|
872
|
-
description: 'Test OAuth2 token exchange'
|
|
873
|
-
}
|
|
874
|
-
];
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
_generateEndpointTestSection(path, method, operation, components, options) {
|
|
878
|
-
const scenarios = [];
|
|
879
|
-
const operationName = operation.operationId || `${method.toLowerCase()}_${path.replace(/[^a-zA-Z0-9]/g, '_')}`;
|
|
880
|
-
|
|
881
|
-
// Happy path scenario
|
|
882
|
-
const happyPathScenario = {
|
|
883
|
-
title: `${operationName} - Happy Path`,
|
|
884
|
-
method: method,
|
|
885
|
-
endpoint: path,
|
|
886
|
-
description: operation.summary || operation.description || `Test successful ${method} request to ${path}`,
|
|
887
|
-
expect: {
|
|
888
|
-
status: this._getSuccessStatus(method, operation)
|
|
889
|
-
}
|
|
890
|
-
};
|
|
891
|
-
|
|
892
|
-
// Add request body for POST/PUT/PATCH
|
|
893
|
-
if (['POST', 'PUT', 'PATCH'].includes(method) && operation.requestBody) {
|
|
894
|
-
happyPathScenario.data = this._generateSampleRequestData(operation.requestBody, components);
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
// Add query parameters
|
|
898
|
-
if (operation.parameters) {
|
|
899
|
-
const queryParams = operation.parameters.filter(p => p.in === 'query');
|
|
900
|
-
if (queryParams.length > 0) {
|
|
901
|
-
happyPathScenario.query = this._generateSampleQueryParams(queryParams, components);
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
const headerParams = operation.parameters.filter(p => p.in === 'header');
|
|
905
|
-
if (headerParams.length > 0) {
|
|
906
|
-
happyPathScenario.headers = this._generateSampleHeaders(headerParams, components);
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
scenarios.push(happyPathScenario);
|
|
911
|
-
|
|
912
|
-
// Error scenarios
|
|
913
|
-
if (options.includeErrorHandling) {
|
|
914
|
-
scenarios.push(...this._generateErrorScenarios(path, method, operation, components));
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
// Edge case scenarios
|
|
918
|
-
if (options.testCategories.includes('edge-cases')) {
|
|
919
|
-
scenarios.push(...this._generateEdgeCaseScenarios(path, method, operation, components));
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
return {
|
|
923
|
-
title: `${method} ${path}`,
|
|
924
|
-
description: operation.summary || operation.description || '',
|
|
925
|
-
operationId: operationName,
|
|
926
|
-
scenarios: scenarios
|
|
927
|
-
};
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
_getSuccessStatus(method, operation) {
|
|
931
|
-
if (operation.responses) {
|
|
932
|
-
const successCodes = Object.keys(operation.responses).filter(code =>
|
|
933
|
-
code.startsWith('2') && code !== 'default'
|
|
934
|
-
);
|
|
935
|
-
if (successCodes.length > 0) {
|
|
936
|
-
return parseInt(successCodes[0]);
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
// Default success codes by method
|
|
941
|
-
switch (method) {
|
|
942
|
-
case 'POST': return 201;
|
|
943
|
-
case 'DELETE': return 204;
|
|
944
|
-
default: return 200;
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
_generateSampleRequestData(requestBody, components) {
|
|
949
|
-
const content = requestBody.content;
|
|
950
|
-
if (!content) {
|
|
951
|
-
return {};
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
// Priority order for content type selection
|
|
955
|
-
const contentTypes = Object.keys(content);
|
|
956
|
-
let selectedContentType = null;
|
|
957
|
-
let contentTypeCategory = null;
|
|
958
|
-
|
|
959
|
-
// 1. JSON types (highest priority for structured data)
|
|
960
|
-
selectedContentType = contentTypes.find(ct => {
|
|
961
|
-
const lower = ct.toLowerCase();
|
|
962
|
-
return lower === 'application/json' ||
|
|
963
|
-
lower.startsWith('application/json') ||
|
|
964
|
-
lower.startsWith('text/json') ||
|
|
965
|
-
lower.includes('+json') ||
|
|
966
|
-
lower.includes('json');
|
|
967
|
-
});
|
|
968
|
-
if (selectedContentType) contentTypeCategory = 'json';
|
|
969
|
-
|
|
970
|
-
// 2. XML types
|
|
971
|
-
if (!selectedContentType) {
|
|
972
|
-
selectedContentType = contentTypes.find(ct => {
|
|
973
|
-
const lower = ct.toLowerCase();
|
|
974
|
-
return lower === 'application/xml' ||
|
|
975
|
-
lower === 'text/xml' ||
|
|
976
|
-
lower.startsWith('application/xml') ||
|
|
977
|
-
lower.startsWith('text/xml') ||
|
|
978
|
-
lower.includes('+xml') ||
|
|
979
|
-
lower.includes('xml');
|
|
980
|
-
});
|
|
981
|
-
if (selectedContentType) contentTypeCategory = 'xml';
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
// 3. Form data types
|
|
985
|
-
if (!selectedContentType) {
|
|
986
|
-
selectedContentType = contentTypes.find(ct => {
|
|
987
|
-
const lower = ct.toLowerCase();
|
|
988
|
-
return lower === 'application/x-www-form-urlencoded' ||
|
|
989
|
-
lower.startsWith('application/x-www-form-urlencoded');
|
|
990
|
-
});
|
|
991
|
-
if (selectedContentType) contentTypeCategory = 'form';
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
// 4. Multipart types
|
|
995
|
-
if (!selectedContentType) {
|
|
996
|
-
selectedContentType = contentTypes.find(ct => {
|
|
997
|
-
const lower = ct.toLowerCase();
|
|
998
|
-
return lower === 'multipart/form-data' ||
|
|
999
|
-
lower.startsWith('multipart/form-data') ||
|
|
1000
|
-
lower.startsWith('multipart/');
|
|
1001
|
-
});
|
|
1002
|
-
if (selectedContentType) contentTypeCategory = 'multipart';
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
// 5. Plain text
|
|
1006
|
-
if (!selectedContentType) {
|
|
1007
|
-
selectedContentType = contentTypes.find(ct => {
|
|
1008
|
-
const lower = ct.toLowerCase();
|
|
1009
|
-
return lower === 'text/plain' || lower.startsWith('text/plain');
|
|
1010
|
-
});
|
|
1011
|
-
if (selectedContentType) contentTypeCategory = 'text';
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
// 6. Binary/file types (image, audio, video, application/octet-stream, etc.)
|
|
1015
|
-
if (!selectedContentType) {
|
|
1016
|
-
selectedContentType = contentTypes.find(ct => {
|
|
1017
|
-
const lower = ct.toLowerCase();
|
|
1018
|
-
return lower.startsWith('image/') ||
|
|
1019
|
-
lower.startsWith('audio/') ||
|
|
1020
|
-
lower.startsWith('video/') ||
|
|
1021
|
-
lower === 'application/octet-stream' ||
|
|
1022
|
-
lower === 'application/pdf' ||
|
|
1023
|
-
lower === 'application/zip';
|
|
1024
|
-
});
|
|
1025
|
-
if (selectedContentType) contentTypeCategory = 'binary';
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
// 7. Fallback: use first available content type
|
|
1029
|
-
if (!selectedContentType && contentTypes.length > 0) {
|
|
1030
|
-
selectedContentType = contentTypes[0];
|
|
1031
|
-
contentTypeCategory = 'unknown';
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
// Generate sample data based on content type category
|
|
1035
|
-
if (selectedContentType && content[selectedContentType]?.schema) {
|
|
1036
|
-
const schema = content[selectedContentType].schema;
|
|
1037
|
-
|
|
1038
|
-
switch (contentTypeCategory) {
|
|
1039
|
-
case 'json':
|
|
1040
|
-
return this._generateSampleFromSchema(schema, components);
|
|
1041
|
-
|
|
1042
|
-
case 'xml':
|
|
1043
|
-
return this._generateXmlSample(schema, components);
|
|
1044
|
-
|
|
1045
|
-
case 'form':
|
|
1046
|
-
case 'multipart':
|
|
1047
|
-
return this._generateFormSample(schema, components);
|
|
1048
|
-
|
|
1049
|
-
case 'text':
|
|
1050
|
-
return this._generateTextSample(schema, components);
|
|
1051
|
-
|
|
1052
|
-
case 'binary':
|
|
1053
|
-
return this._generateBinarySample(schema, selectedContentType);
|
|
1054
|
-
|
|
1055
|
-
default:
|
|
1056
|
-
// Unknown content type - try to generate from schema anyway
|
|
1057
|
-
return this._generateSampleFromSchema(schema, components);
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
return {};
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
/**
|
|
1065
|
-
* Generate sample XML data from schema
|
|
1066
|
-
*/
|
|
1067
|
-
_generateXmlSample(schema, components) {
|
|
1068
|
-
// For XML, we return a simplified representation
|
|
1069
|
-
// In practice, this would be converted to XML format
|
|
1070
|
-
const data = this._generateSampleFromSchema(schema, components);
|
|
1071
|
-
return {
|
|
1072
|
-
_comment: 'XML representation',
|
|
1073
|
-
_contentType: 'application/xml',
|
|
1074
|
-
data: data
|
|
1075
|
-
};
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
/**
|
|
1079
|
-
* Generate sample form data from schema
|
|
1080
|
-
*/
|
|
1081
|
-
_generateFormSample(schema, components) {
|
|
1082
|
-
// Form data is typically key-value pairs
|
|
1083
|
-
const data = this._generateSampleFromSchema(schema, components);
|
|
1084
|
-
|
|
1085
|
-
// Flatten nested objects for form data
|
|
1086
|
-
if (typeof data === 'object' && !Array.isArray(data)) {
|
|
1087
|
-
return data;
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
return { value: data };
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
/**
|
|
1094
|
-
* Generate sample plain text from schema
|
|
1095
|
-
*/
|
|
1096
|
-
_generateTextSample(schema, components) {
|
|
1097
|
-
if (schema.example !== undefined) return schema.example;
|
|
1098
|
-
if (schema.default !== undefined) return schema.default;
|
|
1099
|
-
|
|
1100
|
-
// If schema has properties, try to generate structured text
|
|
1101
|
-
if (schema.type === 'object' || schema.properties) {
|
|
1102
|
-
const data = this._generateSampleFromSchema(schema, components);
|
|
1103
|
-
return JSON.stringify(data, null, 2);
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
// For simple types, return a plain text value
|
|
1107
|
-
return 'Sample plain text content';
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
/**
|
|
1111
|
-
* Generate sample binary/file data representation
|
|
1112
|
-
*/
|
|
1113
|
-
_generateBinarySample(schema, contentType) {
|
|
1114
|
-
const lower = contentType.toLowerCase();
|
|
1115
|
-
|
|
1116
|
-
if (lower.startsWith('image/')) {
|
|
1117
|
-
return {
|
|
1118
|
-
_comment: 'Binary file upload',
|
|
1119
|
-
_contentType: contentType,
|
|
1120
|
-
_file: 'sample-image.jpg',
|
|
1121
|
-
_description: 'Upload image file here'
|
|
1122
|
-
};
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
if (lower.startsWith('audio/')) {
|
|
1126
|
-
return {
|
|
1127
|
-
_comment: 'Binary file upload',
|
|
1128
|
-
_contentType: contentType,
|
|
1129
|
-
_file: 'sample-audio.mp3',
|
|
1130
|
-
_description: 'Upload audio file here'
|
|
1131
|
-
};
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
if (lower.startsWith('video/')) {
|
|
1135
|
-
return {
|
|
1136
|
-
_comment: 'Binary file upload',
|
|
1137
|
-
_contentType: contentType,
|
|
1138
|
-
_file: 'sample-video.mp4',
|
|
1139
|
-
_description: 'Upload video file here'
|
|
1140
|
-
};
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
if (lower === 'application/pdf') {
|
|
1144
|
-
return {
|
|
1145
|
-
_comment: 'Binary file upload',
|
|
1146
|
-
_contentType: contentType,
|
|
1147
|
-
_file: 'sample-document.pdf',
|
|
1148
|
-
_description: 'Upload PDF file here'
|
|
1149
|
-
};
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
// Generic binary data
|
|
1153
|
-
return {
|
|
1154
|
-
_comment: 'Binary file upload',
|
|
1155
|
-
_contentType: contentType,
|
|
1156
|
-
_file: 'sample-file.bin',
|
|
1157
|
-
_description: 'Upload binary file here'
|
|
1158
|
-
};
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
_generateSampleQueryParams(queryParams, components) {
|
|
1162
|
-
const params = {};
|
|
1163
|
-
queryParams.forEach(param => {
|
|
1164
|
-
if (param.required || param.schema) {
|
|
1165
|
-
params[param.name] = this._generateSampleValue(param.schema || { type: 'string' }, components);
|
|
1166
|
-
}
|
|
1167
|
-
});
|
|
1168
|
-
return params;
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
_generateSampleHeaders(headerParams, components) {
|
|
1172
|
-
const headers = {};
|
|
1173
|
-
headerParams.forEach(param => {
|
|
1174
|
-
if (param.required || param.schema) {
|
|
1175
|
-
headers[param.name] = this._generateSampleValue(param.schema || { type: 'string' }, components);
|
|
1176
|
-
}
|
|
1177
|
-
});
|
|
1178
|
-
return headers;
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
_generateSampleFromSchema(schema, components, fieldName = '') {
|
|
1182
|
-
if (schema.$ref) {
|
|
1183
|
-
const refPath = schema.$ref.replace('#/', '').split('/');
|
|
1184
|
-
// If the refPath starts with 'components', skip it since we're already in components
|
|
1185
|
-
const pathToResolve = refPath[0] === 'components' ? refPath.slice(1) : refPath;
|
|
1186
|
-
const resolvedSchema = this._resolveRef(components, pathToResolve);
|
|
1187
|
-
return this._generateSampleFromSchema(resolvedSchema, components, fieldName);
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
switch (schema.type) {
|
|
1191
|
-
case 'object':
|
|
1192
|
-
const obj = {};
|
|
1193
|
-
if (schema.properties) {
|
|
1194
|
-
// Include ALL properties for complete test coverage
|
|
1195
|
-
// Tests should demonstrate all available fields, not randomly exclude them
|
|
1196
|
-
Object.entries(schema.properties).forEach(([key, propSchema]) => {
|
|
1197
|
-
obj[key] = this._generateSampleFromSchema(propSchema, components, key);
|
|
1198
|
-
});
|
|
1199
|
-
}
|
|
1200
|
-
return obj;
|
|
1201
|
-
|
|
1202
|
-
case 'array':
|
|
1203
|
-
return [this._generateSampleFromSchema(schema.items, components, fieldName)];
|
|
1204
|
-
|
|
1205
|
-
default:
|
|
1206
|
-
return this._generateSampleValue(schema, components, fieldName);
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
_generateSampleValue(schema, components, fieldName = '') {
|
|
1211
|
-
// Use explicit examples or defaults if provided
|
|
1212
|
-
if (schema.example !== undefined) return schema.example;
|
|
1213
|
-
if (schema.default !== undefined) return schema.default;
|
|
1214
|
-
|
|
1215
|
-
switch (schema.type) {
|
|
1216
|
-
case 'string':
|
|
1217
|
-
return this._generateStringValue(schema, fieldName);
|
|
1218
|
-
|
|
1219
|
-
case 'number':
|
|
1220
|
-
case 'integer':
|
|
1221
|
-
return this._generateNumericValue(schema, fieldName);
|
|
1222
|
-
|
|
1223
|
-
case 'boolean':
|
|
1224
|
-
return this._generateBooleanValue(schema, fieldName);
|
|
1225
|
-
|
|
1226
|
-
default:
|
|
1227
|
-
return 'value';
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
/**
|
|
1232
|
-
* Generate realistic string values based on field name and format
|
|
1233
|
-
*/
|
|
1234
|
-
_generateStringValue(schema, fieldName = '') {
|
|
1235
|
-
const lowerName = fieldName.toLowerCase();
|
|
1236
|
-
|
|
1237
|
-
// Use faker.js if available for high-quality data with graceful error handling
|
|
1238
|
-
if (faker) {
|
|
1239
|
-
try {
|
|
1240
|
-
// Email addresses
|
|
1241
|
-
if (schema.format === 'email' || lowerName.match(/email|e-mail|emailaddress/)) {
|
|
1242
|
-
return faker.internet.email();
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
// Names
|
|
1246
|
-
if (lowerName.match(/^(first|given)name$/)) return faker.person.firstName();
|
|
1247
|
-
if (lowerName.match(/^(last|sur|family)name$/)) return faker.person.lastName();
|
|
1248
|
-
if (lowerName.match(/^(full|complete)?name$/)) return faker.person.fullName();
|
|
1249
|
-
if (lowerName.match(/^(middle|mid)name$/)) return faker.person.middleName();
|
|
1250
|
-
|
|
1251
|
-
// Contact information
|
|
1252
|
-
if (lowerName.match(/phone|mobile|tel|telephone/)) return faker.phone.number();
|
|
1253
|
-
if (lowerName.match(/address|street|addr/)) return faker.location.streetAddress();
|
|
1254
|
-
if (lowerName.match(/city|town/)) return faker.location.city();
|
|
1255
|
-
if (lowerName.match(/state|province|region/)) return faker.location.state();
|
|
1256
|
-
if (lowerName.match(/country/)) return faker.location.country();
|
|
1257
|
-
if (lowerName.match(/zip|postal|postcode/)) return faker.location.zipCode();
|
|
1258
|
-
|
|
1259
|
-
// Internet & Technology
|
|
1260
|
-
if (schema.format === 'uri' || schema.format === 'url' || lowerName.match(/url|uri|link|website/)) {
|
|
1261
|
-
return faker.internet.url();
|
|
1262
|
-
}
|
|
1263
|
-
if (lowerName.match(/username|login|handle/)) return faker.internet.username();
|
|
1264
|
-
if (lowerName.match(/password|passwd|pwd/)) return faker.internet.password({ length: Math.max(schema.minLength || 8, 12) });
|
|
1265
|
-
if (lowerName.match(/ip|ipaddress/)) return faker.internet.ip();
|
|
1266
|
-
if (lowerName.match(/domain/)) return faker.internet.domainName();
|
|
1267
|
-
if (lowerName.match(/avatar|image|photo|picture/)) return faker.image.avatar();
|
|
1268
|
-
|
|
1269
|
-
// Business
|
|
1270
|
-
if (lowerName.match(/company|organization|org/)) return faker.company.name();
|
|
1271
|
-
if (lowerName.match(/job|position|title|role/)) return faker.person.jobTitle();
|
|
1272
|
-
if (lowerName.match(/department|dept/)) return faker.commerce.department();
|
|
1273
|
-
|
|
1274
|
-
// Identifiers
|
|
1275
|
-
if (lowerName.match(/uuid|guid/)) return faker.string.uuid();
|
|
1276
|
-
if (lowerName.match(/^id$|id$/)) return faker.string.alphanumeric(10); // Matches 'id', 'productId', 'userId', etc.
|
|
1277
|
-
if (lowerName.match(/token|key|secret/)) return faker.string.alphanumeric(32);
|
|
1278
|
-
|
|
1279
|
-
// Content
|
|
1280
|
-
if (lowerName.match(/description|desc|summary/)) return faker.lorem.sentence();
|
|
1281
|
-
if (lowerName.match(/comment|note|remark/)) return faker.lorem.paragraph();
|
|
1282
|
-
if (lowerName.match(/title|heading/)) return faker.lorem.words(3);
|
|
1283
|
-
if (lowerName.match(/tag|label/)) return faker.lorem.word();
|
|
1284
|
-
|
|
1285
|
-
// Dates & Times
|
|
1286
|
-
if (schema.format === 'date') return faker.date.recent().toISOString().split('T')[0];
|
|
1287
|
-
if (schema.format === 'date-time' || lowerName.match(/date|time|timestamp|created|updated/)) {
|
|
1288
|
-
return faker.date.recent().toISOString();
|
|
1289
|
-
}
|
|
1290
|
-
} catch (error) {
|
|
1291
|
-
// Graceful fallback: if faker fails for any reason, continue to built-in generation
|
|
1292
|
-
console.warn(`[ApiPlanner] Faker.js error for field "${fieldName}":`, error.message);
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
// Fallback to built-in semantic detection
|
|
1297
|
-
// Email addresses
|
|
1298
|
-
if (schema.format === 'email' || lowerName.match(/email|e-mail|emailaddress/)) {
|
|
1299
|
-
return 'john.doe@example.com';
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
// Names
|
|
1303
|
-
if (lowerName.match(/^(first|given)name$/)) return 'John';
|
|
1304
|
-
if (lowerName.match(/^(last|sur|family)name$/)) return 'Doe';
|
|
1305
|
-
if (lowerName.match(/^(full|complete)?name$/)) return 'John Doe';
|
|
1306
|
-
if (lowerName.match(/^(middle|mid)name$/)) return 'Michael';
|
|
1307
|
-
|
|
1308
|
-
// Contact information
|
|
1309
|
-
if (lowerName.match(/phone|mobile|tel|telephone/)) return '+1-555-0123';
|
|
1310
|
-
if (lowerName.match(/address|street|addr/)) return '123 Main Street';
|
|
1311
|
-
if (lowerName.match(/city|town/)) return 'New York';
|
|
1312
|
-
if (lowerName.match(/state|province|region/)) return 'NY';
|
|
1313
|
-
if (lowerName.match(/country/)) return 'United States';
|
|
1314
|
-
if (lowerName.match(/zip|postal|postcode/)) return '10001';
|
|
1315
|
-
|
|
1316
|
-
// Internet & Technology
|
|
1317
|
-
if (schema.format === 'uri' || schema.format === 'url' || lowerName.match(/url|uri|link|website/)) {
|
|
1318
|
-
return 'https://example.com';
|
|
1319
|
-
}
|
|
1320
|
-
if (lowerName.match(/username|login|handle/)) return 'johndoe';
|
|
1321
|
-
if (lowerName.match(/password|passwd|pwd/)) {
|
|
1322
|
-
const minLen = schema.minLength || 8;
|
|
1323
|
-
return 'SecurePass123!'.substring(0, Math.max(minLen, 14));
|
|
1324
|
-
}
|
|
1325
|
-
if (lowerName.match(/ip|ipaddress/)) return '192.168.1.1';
|
|
1326
|
-
if (lowerName.match(/domain/)) return 'example.com';
|
|
1327
|
-
if (lowerName.match(/avatar|image|photo|picture/)) return 'https://example.com/avatar.jpg';
|
|
1328
|
-
|
|
1329
|
-
// Business
|
|
1330
|
-
if (lowerName.match(/company|organization|org/)) return 'Acme Corp';
|
|
1331
|
-
if (lowerName.match(/job|position|title|role/)) return 'Software Engineer';
|
|
1332
|
-
if (lowerName.match(/department|dept/)) return 'Engineering';
|
|
1333
|
-
|
|
1334
|
-
// Identifiers
|
|
1335
|
-
if (lowerName.match(/uuid|guid/)) return '550e8400-e29b-41d4-a716-446655440000';
|
|
1336
|
-
if (lowerName.match(/^id$|id$/)) return 'abc123'; // Matches 'id', 'productId', 'userId', 'orderId', etc.
|
|
1337
|
-
if (lowerName.match(/token|key|secret/)) return 'sk_test_1234567890abcdef';
|
|
1338
|
-
|
|
1339
|
-
// Content
|
|
1340
|
-
if (lowerName.match(/description|desc|summary/)) return 'This is a sample description';
|
|
1341
|
-
if (lowerName.match(/comment|note|remark/)) return 'This is a sample comment';
|
|
1342
|
-
if (lowerName.match(/title|heading/)) return 'Sample Title';
|
|
1343
|
-
if (lowerName.match(/tag|label/)) return 'sample-tag';
|
|
1344
|
-
if (lowerName.match(/code/)) return 'CODE123';
|
|
1345
|
-
if (lowerName.match(/status/)) return 'active';
|
|
1346
|
-
if (lowerName.match(/type|kind|category/)) return 'standard';
|
|
1347
|
-
|
|
1348
|
-
// Dates & Times
|
|
1349
|
-
if (schema.format === 'date') return '2025-10-19';
|
|
1350
|
-
if (schema.format === 'date-time' || lowerName.match(/date|time|timestamp|created|updated/)) {
|
|
1351
|
-
return '2025-10-19T10:30:00Z';
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
// Colors
|
|
1355
|
-
if (lowerName.match(/color|colour/)) return '#3B82F6';
|
|
1356
|
-
|
|
1357
|
-
// Enum values
|
|
1358
|
-
if (schema.enum && schema.enum.length > 0) {
|
|
1359
|
-
return schema.enum[0];
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
// Pattern-based generation
|
|
1363
|
-
if (schema.pattern) {
|
|
1364
|
-
// Simple pattern detection for common cases
|
|
1365
|
-
if (schema.pattern.includes('uuid')) return '550e8400-e29b-41d4-a716-446655440000';
|
|
1366
|
-
if (schema.pattern.includes('[0-9]')) return '123456';
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
// Length constraints
|
|
1370
|
-
if (schema.minLength || schema.maxLength) {
|
|
1371
|
-
const minLen = schema.minLength || 1;
|
|
1372
|
-
const maxLen = schema.maxLength || minLen + 10;
|
|
1373
|
-
const targetLen = Math.min(maxLen, Math.max(minLen, 10));
|
|
1374
|
-
return 'sample_value'.substring(0, targetLen).padEnd(targetLen, '_');
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
// Generic fallback
|
|
1378
|
-
return 'string_value';
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
/**
|
|
1382
|
-
* Generate realistic numeric values based on field name and constraints
|
|
1383
|
-
*/
|
|
1384
|
-
_generateNumericValue(schema, fieldName = '') {
|
|
1385
|
-
const lowerName = fieldName.toLowerCase();
|
|
1386
|
-
const isInteger = schema.type === 'integer';
|
|
1387
|
-
|
|
1388
|
-
// Use faker.js if available with graceful error handling
|
|
1389
|
-
if (faker) {
|
|
1390
|
-
try {
|
|
1391
|
-
if (lowerName.match(/age/)) return faker.number.int({ min: 18, max: 80 });
|
|
1392
|
-
if (lowerName.match(/price|cost|amount|total/)) return parseFloat(faker.commerce.price());
|
|
1393
|
-
if (lowerName.match(/quantity|qty|count/)) return faker.number.int({ min: 1, max: 100 });
|
|
1394
|
-
if (lowerName.match(/rating|score/)) return faker.number.float({ min: 1, max: 5, multipleOf: 0.1 });
|
|
1395
|
-
if (lowerName.match(/percentage|percent/)) return faker.number.int({ min: 0, max: 100 });
|
|
1396
|
-
if (lowerName.match(/year/)) return faker.date.recent().getFullYear();
|
|
1397
|
-
if (lowerName.match(/month/)) return faker.number.int({ min: 1, max: 12 });
|
|
1398
|
-
if (lowerName.match(/day/)) return faker.number.int({ min: 1, max: 31 });
|
|
1399
|
-
if (lowerName.match(/latitude|lat/)) return parseFloat(faker.location.latitude());
|
|
1400
|
-
if (lowerName.match(/longitude|lng|lon/)) return parseFloat(faker.location.longitude());
|
|
1401
|
-
} catch (error) {
|
|
1402
|
-
// Graceful fallback: if faker fails, continue to built-in generation
|
|
1403
|
-
console.warn(`[ApiPlanner] Faker.js error for numeric field "${fieldName}":`, error.message);
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
// Built-in semantic detection
|
|
1408
|
-
if (lowerName.match(/age/)) return isInteger ? 25 : 25.0;
|
|
1409
|
-
if (lowerName.match(/price|cost|amount|total/)) return isInteger ? 1999 : 19.99;
|
|
1410
|
-
if (lowerName.match(/quantity|qty|count/)) return isInteger ? 10 : 10.0;
|
|
1411
|
-
if (lowerName.match(/rating|score/)) return isInteger ? 4 : 4.5;
|
|
1412
|
-
if (lowerName.match(/percentage|percent/)) return isInteger ? 75 : 75.0;
|
|
1413
|
-
if (lowerName.match(/year/)) return 2025;
|
|
1414
|
-
if (lowerName.match(/month/)) return 10;
|
|
1415
|
-
if (lowerName.match(/day/)) return 19;
|
|
1416
|
-
if (lowerName.match(/hour/)) return 10;
|
|
1417
|
-
if (lowerName.match(/minute|min/)) return 30;
|
|
1418
|
-
if (lowerName.match(/second|sec/)) return 45;
|
|
1419
|
-
if (lowerName.match(/latitude|lat/)) return 40.7128;
|
|
1420
|
-
if (lowerName.match(/longitude|lng|lon/)) return -74.0060;
|
|
1421
|
-
if (lowerName.match(/^id$/)) return isInteger ? 12345 : 12345.0;
|
|
1422
|
-
|
|
1423
|
-
// Use schema constraints
|
|
1424
|
-
if (schema.minimum !== undefined) {
|
|
1425
|
-
const min = schema.minimum;
|
|
1426
|
-
const max = schema.maximum !== undefined ? schema.maximum : min + 100;
|
|
1427
|
-
const value = min + (max - min) / 2;
|
|
1428
|
-
return isInteger ? Math.round(value) : parseFloat(value.toFixed(2));
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
if (schema.maximum !== undefined) {
|
|
1432
|
-
const max = schema.maximum;
|
|
1433
|
-
const value = max / 2;
|
|
1434
|
-
return isInteger ? Math.round(value) : parseFloat(value.toFixed(2));
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
|
-
// Default values
|
|
1438
|
-
return isInteger ? 123 : 123.45;
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
/**
|
|
1442
|
-
* Generate realistic boolean values based on field name
|
|
1443
|
-
*/
|
|
1444
|
-
_generateBooleanValue(schema, fieldName = '') {
|
|
1445
|
-
const lowerName = fieldName.toLowerCase();
|
|
1446
|
-
|
|
1447
|
-
// Semantic detection for booleans
|
|
1448
|
-
if (lowerName.match(/^is|^has|^can|^should|^will|^does/)) {
|
|
1449
|
-
// Common patterns suggest true
|
|
1450
|
-
if (lowerName.match(/active|enabled|verified|confirmed|published/)) return true;
|
|
1451
|
-
// Common patterns suggest false
|
|
1452
|
-
if (lowerName.match(/deleted|disabled|suspended|banned|archived/)) return false;
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
// Default to true for convenience
|
|
1456
|
-
return true;
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
_resolveRef(components, refPath) {
|
|
1460
|
-
let current = components;
|
|
1461
|
-
for (const segment of refPath) {
|
|
1462
|
-
if (current && current[segment]) {
|
|
1463
|
-
current = current[segment];
|
|
1464
|
-
} else {
|
|
1465
|
-
return {};
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
return current;
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
_generateErrorScenarios(path, method, operation, components) {
|
|
1472
|
-
const scenarios = [];
|
|
1473
|
-
|
|
1474
|
-
// 400 Bad Request scenarios
|
|
1475
|
-
if (['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
1476
|
-
scenarios.push({
|
|
1477
|
-
title: `${operation.operationId || method} - Invalid Request Data`,
|
|
1478
|
-
method: method,
|
|
1479
|
-
endpoint: path,
|
|
1480
|
-
data: { invalid: 'data' },
|
|
1481
|
-
expect: {
|
|
1482
|
-
status: 400
|
|
1483
|
-
},
|
|
1484
|
-
description: 'Test validation of malformed request data'
|
|
1485
|
-
});
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
// 404 Not Found scenarios
|
|
1489
|
-
if (path.includes('{')) {
|
|
1490
|
-
scenarios.push({
|
|
1491
|
-
title: `${operation.operationId || method} - Resource Not Found`,
|
|
1492
|
-
method: method,
|
|
1493
|
-
endpoint: path.replace(/{[^}]+}/g, '99999'),
|
|
1494
|
-
expect: {
|
|
1495
|
-
status: 404
|
|
1496
|
-
},
|
|
1497
|
-
description: 'Test behavior with non-existent resource ID'
|
|
1498
|
-
});
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
return scenarios;
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
_generateEdgeCaseScenarios(path, method, operation, components) {
|
|
1505
|
-
const scenarios = [];
|
|
1506
|
-
|
|
1507
|
-
// Large payload testing
|
|
1508
|
-
if (['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
1509
|
-
scenarios.push({
|
|
1510
|
-
title: `${operation.operationId || method} - Large Payload`,
|
|
1511
|
-
method: method,
|
|
1512
|
-
endpoint: path,
|
|
1513
|
-
data: { largeField: 'x'.repeat(10000) },
|
|
1514
|
-
expect: {
|
|
1515
|
-
status: [200, 201, 413] // Accept success or payload too large
|
|
1516
|
-
},
|
|
1517
|
-
description: 'Test handling of large request payloads'
|
|
1518
|
-
});
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
// Empty/null data testing
|
|
1522
|
-
if (['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
1523
|
-
scenarios.push({
|
|
1524
|
-
title: `${operation.operationId || method} - Empty Payload`,
|
|
1525
|
-
method: method,
|
|
1526
|
-
endpoint: path,
|
|
1527
|
-
data: {},
|
|
1528
|
-
expect: {
|
|
1529
|
-
status: [200, 201, 400] // May succeed or fail validation
|
|
1530
|
-
},
|
|
1531
|
-
description: 'Test handling of empty request payload'
|
|
1532
|
-
});
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
return scenarios;
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
_generateSecurityTestSection(schema, options) {
|
|
1539
|
-
const scenarios = [
|
|
1540
|
-
{
|
|
1541
|
-
title: 'SQL Injection Protection',
|
|
1542
|
-
method: 'GET',
|
|
1543
|
-
endpoint: '/vulnerable-endpoint',
|
|
1544
|
-
query: {
|
|
1545
|
-
search: "'; DROP TABLE users; --"
|
|
1546
|
-
},
|
|
1547
|
-
expect: {
|
|
1548
|
-
status: [200, 400]
|
|
1549
|
-
},
|
|
1550
|
-
description: 'Test protection against SQL injection attacks'
|
|
1551
|
-
},
|
|
1552
|
-
{
|
|
1553
|
-
title: 'XSS Protection',
|
|
1554
|
-
method: 'POST',
|
|
1555
|
-
endpoint: '/user-input',
|
|
1556
|
-
data: {
|
|
1557
|
-
content: '<script>alert("xss")</script>'
|
|
1558
|
-
},
|
|
1559
|
-
expect: {
|
|
1560
|
-
status: [200, 201, 400]
|
|
1561
|
-
},
|
|
1562
|
-
description: 'Test protection against XSS attacks'
|
|
1563
|
-
},
|
|
1564
|
-
{
|
|
1565
|
-
title: 'CSRF Protection',
|
|
1566
|
-
method: 'POST',
|
|
1567
|
-
endpoint: '/sensitive-action',
|
|
1568
|
-
headers: {
|
|
1569
|
-
'Origin': 'https://malicious-site.com'
|
|
1570
|
-
},
|
|
1571
|
-
expect: {
|
|
1572
|
-
status: [403, 400]
|
|
1573
|
-
},
|
|
1574
|
-
description: 'Test CSRF protection mechanisms'
|
|
1575
|
-
}
|
|
1576
|
-
];
|
|
1577
|
-
|
|
1578
|
-
return {
|
|
1579
|
-
title: 'Security Testing',
|
|
1580
|
-
description: 'Test security measures and vulnerability protections',
|
|
1581
|
-
scenarios: scenarios
|
|
1582
|
-
};
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
_generateIntegrationTestSection(schema, options) {
|
|
1586
|
-
const scenarios = [
|
|
1587
|
-
{
|
|
1588
|
-
title: 'End-to-End User Workflow',
|
|
1589
|
-
method: 'CHAIN',
|
|
1590
|
-
description: 'Test complete user journey from registration to data manipulation',
|
|
1591
|
-
chain: [
|
|
1592
|
-
{
|
|
1593
|
-
name: 'register',
|
|
1594
|
-
method: 'POST',
|
|
1595
|
-
endpoint: '/auth/register',
|
|
1596
|
-
data: {
|
|
1597
|
-
email: 'integration@test.com',
|
|
1598
|
-
password: 'TestPassword123'
|
|
1599
|
-
},
|
|
1600
|
-
expect: { status: 201 },
|
|
1601
|
-
extract: { userId: 'id' }
|
|
1602
|
-
},
|
|
1603
|
-
{
|
|
1604
|
-
name: 'login',
|
|
1605
|
-
method: 'POST',
|
|
1606
|
-
endpoint: '/auth/login',
|
|
1607
|
-
data: {
|
|
1608
|
-
email: 'integration@test.com',
|
|
1609
|
-
password: 'TestPassword123'
|
|
1610
|
-
},
|
|
1611
|
-
expect: { status: 200 },
|
|
1612
|
-
extract: { token: 'token' }
|
|
1613
|
-
},
|
|
1614
|
-
{
|
|
1615
|
-
name: 'create_resource',
|
|
1616
|
-
method: 'POST',
|
|
1617
|
-
endpoint: '/resources',
|
|
1618
|
-
headers: {
|
|
1619
|
-
'Authorization': 'Bearer {{ login.token }}'
|
|
1620
|
-
},
|
|
1621
|
-
data: {
|
|
1622
|
-
title: 'Integration Test Resource',
|
|
1623
|
-
ownerId: '{{ register.userId }}'
|
|
1624
|
-
},
|
|
1625
|
-
expect: { status: 201 },
|
|
1626
|
-
extract: { resourceId: 'id' }
|
|
1627
|
-
},
|
|
1628
|
-
{
|
|
1629
|
-
name: 'verify_resource',
|
|
1630
|
-
method: 'GET',
|
|
1631
|
-
endpoint: '/resources/{{ create_resource.resourceId }}',
|
|
1632
|
-
headers: {
|
|
1633
|
-
'Authorization': 'Bearer {{ login.token }}'
|
|
1634
|
-
},
|
|
1635
|
-
expect: {
|
|
1636
|
-
status: 200,
|
|
1637
|
-
body: { title: 'Integration Test Resource' }
|
|
1638
|
-
}
|
|
1639
|
-
}
|
|
1640
|
-
]
|
|
1641
|
-
}
|
|
1642
|
-
];
|
|
1643
|
-
|
|
1644
|
-
return {
|
|
1645
|
-
title: 'Integration Testing',
|
|
1646
|
-
description: 'Test complete workflows and cross-service interactions',
|
|
1647
|
-
scenarios: scenarios
|
|
1648
|
-
};
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
// ============================================================================
|
|
1652
|
-
// GRAPHQL SUPPORT - GENERIC IMPLEMENTATION
|
|
1653
|
-
// ============================================================================
|
|
1654
|
-
|
|
1655
|
-
/**
|
|
1656
|
-
* Standard GraphQL introspection query (works with ANY GraphQL endpoint)
|
|
1657
|
-
* Follows GraphQL specification: https://spec.graphql.org/October2021/#sec-Introspection
|
|
1658
|
-
*/
|
|
1659
|
-
_getGraphQLIntrospectionQuery() {
|
|
1660
|
-
return `
|
|
1661
|
-
query IntrospectionQuery {
|
|
1662
|
-
__schema {
|
|
1663
|
-
queryType { name }
|
|
1664
|
-
mutationType { name }
|
|
1665
|
-
subscriptionType { name }
|
|
1666
|
-
types {
|
|
1667
|
-
kind
|
|
1668
|
-
name
|
|
1669
|
-
description
|
|
1670
|
-
fields(includeDeprecated: true) {
|
|
1671
|
-
name
|
|
1672
|
-
description
|
|
1673
|
-
args {
|
|
1674
|
-
name
|
|
1675
|
-
description
|
|
1676
|
-
type { ...TypeRef }
|
|
1677
|
-
defaultValue
|
|
1678
|
-
}
|
|
1679
|
-
type { ...TypeRef }
|
|
1680
|
-
isDeprecated
|
|
1681
|
-
deprecationReason
|
|
1682
|
-
}
|
|
1683
|
-
inputFields {
|
|
1684
|
-
name
|
|
1685
|
-
description
|
|
1686
|
-
type { ...TypeRef }
|
|
1687
|
-
defaultValue
|
|
1688
|
-
}
|
|
1689
|
-
interfaces { ...TypeRef }
|
|
1690
|
-
enumValues(includeDeprecated: true) {
|
|
1691
|
-
name
|
|
1692
|
-
description
|
|
1693
|
-
isDeprecated
|
|
1694
|
-
deprecationReason
|
|
1695
|
-
}
|
|
1696
|
-
possibleTypes { ...TypeRef }
|
|
1697
|
-
}
|
|
1698
|
-
directives {
|
|
1699
|
-
name
|
|
1700
|
-
description
|
|
1701
|
-
locations
|
|
1702
|
-
args {
|
|
1703
|
-
name
|
|
1704
|
-
description
|
|
1705
|
-
type { ...TypeRef }
|
|
1706
|
-
defaultValue
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
fragment TypeRef on __Type {
|
|
1713
|
-
kind
|
|
1714
|
-
name
|
|
1715
|
-
ofType {
|
|
1716
|
-
kind
|
|
1717
|
-
name
|
|
1718
|
-
ofType {
|
|
1719
|
-
kind
|
|
1720
|
-
name
|
|
1721
|
-
ofType {
|
|
1722
|
-
kind
|
|
1723
|
-
name
|
|
1724
|
-
ofType {
|
|
1725
|
-
kind
|
|
1726
|
-
name
|
|
1727
|
-
ofType {
|
|
1728
|
-
kind
|
|
1729
|
-
name
|
|
1730
|
-
}
|
|
1731
|
-
}
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
}
|
|
1735
|
-
}
|
|
1736
|
-
`;
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
async _generateGraphQLTestPlan(schema, options) {
|
|
1740
|
-
// Parse schema (generic - works with any GraphQL schema)
|
|
1741
|
-
const parsedSchema = this._parseGraphQLSchema(schema);
|
|
1742
|
-
|
|
1743
|
-
// Discover operations (generic)
|
|
1744
|
-
const operations = this._discoverGraphQLOperations(parsedSchema);
|
|
1745
|
-
|
|
1746
|
-
const testPlan = {
|
|
1747
|
-
summary: {
|
|
1748
|
-
title: 'GraphQL API Test Plan',
|
|
1749
|
-
type: 'graphql',
|
|
1750
|
-
baseUrl: options.apiBaseUrl || 'https://graphql.example.com/graphql',
|
|
1751
|
-
totalQueries: operations.queries.length,
|
|
1752
|
-
totalMutations: operations.mutations.length,
|
|
1753
|
-
totalSubscriptions: operations.subscriptions.length,
|
|
1754
|
-
totalScenarios: 0,
|
|
1755
|
-
totalEndpoints: 1 // GraphQL typically has single endpoint
|
|
1756
|
-
},
|
|
1757
|
-
sections: []
|
|
1758
|
-
};
|
|
1759
|
-
|
|
1760
|
-
// Generate test scenarios for queries (generic)
|
|
1761
|
-
for (const query of operations.queries) {
|
|
1762
|
-
const section = this._generateGraphQLQueryTestSection(query, parsedSchema, options);
|
|
1763
|
-
testPlan.sections.push(section);
|
|
1764
|
-
testPlan.summary.totalScenarios += section.scenarios.length;
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
// Generate test scenarios for mutations (generic)
|
|
1768
|
-
for (const mutation of operations.mutations) {
|
|
1769
|
-
const section = this._generateGraphQLMutationTestSection(mutation, parsedSchema, options);
|
|
1770
|
-
testPlan.sections.push(section);
|
|
1771
|
-
testPlan.summary.totalScenarios += section.scenarios.length;
|
|
1772
|
-
}
|
|
1773
|
-
|
|
1774
|
-
// Generate test scenarios for subscriptions if requested
|
|
1775
|
-
if (options.testCategories.includes('subscriptions') && operations.subscriptions.length > 0) {
|
|
1776
|
-
for (const subscription of operations.subscriptions) {
|
|
1777
|
-
const section = this._generateGraphQLSubscriptionTestSection(subscription, parsedSchema, options);
|
|
1778
|
-
testPlan.sections.push(section);
|
|
1779
|
-
testPlan.summary.totalScenarios += section.scenarios.length;
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
testPlan.content = this._generateMarkdownContent(testPlan);
|
|
1784
|
-
return testPlan;
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
/**
|
|
1788
|
-
* Parse GraphQL introspection result into usable structure
|
|
1789
|
-
* GENERIC - Works with any GraphQL schema following the spec
|
|
1790
|
-
*/
|
|
1791
|
-
_parseGraphQLSchema(introspectionResult) {
|
|
1792
|
-
// Handle both direct introspection result and wrapped result
|
|
1793
|
-
const schemaData = introspectionResult.__schema || introspectionResult.data?.__schema;
|
|
1794
|
-
|
|
1795
|
-
if (!schemaData) {
|
|
1796
|
-
throw new Error('Invalid GraphQL introspection result - missing __schema');
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
const schema = {
|
|
1800
|
-
queryType: schemaData.queryType?.name,
|
|
1801
|
-
mutationType: schemaData.mutationType?.name,
|
|
1802
|
-
subscriptionType: schemaData.subscriptionType?.name,
|
|
1803
|
-
types: {},
|
|
1804
|
-
directives: schemaData.directives || []
|
|
1805
|
-
};
|
|
1806
|
-
|
|
1807
|
-
// Index all types by name (skip internal GraphQL types starting with __)
|
|
1808
|
-
for (const type of schemaData.types) {
|
|
1809
|
-
if (!type.name.startsWith('__')) {
|
|
1810
|
-
schema.types[type.name] = {
|
|
1811
|
-
kind: type.kind,
|
|
1812
|
-
name: type.name,
|
|
1813
|
-
description: type.description,
|
|
1814
|
-
fields: type.fields || [],
|
|
1815
|
-
inputFields: type.inputFields || [],
|
|
1816
|
-
enumValues: type.enumValues || [],
|
|
1817
|
-
interfaces: type.interfaces || [],
|
|
1818
|
-
possibleTypes: type.possibleTypes || []
|
|
1819
|
-
};
|
|
1820
|
-
}
|
|
1821
|
-
}
|
|
1822
|
-
|
|
1823
|
-
return schema;
|
|
1824
|
-
}
|
|
1825
|
-
|
|
1826
|
-
/**
|
|
1827
|
-
* Discover all queries, mutations, and subscriptions from schema
|
|
1828
|
-
* GENERIC - Extracts operations from any GraphQL schema
|
|
1829
|
-
*/
|
|
1830
|
-
_discoverGraphQLOperations(schema) {
|
|
1831
|
-
const operations = {
|
|
1832
|
-
queries: [],
|
|
1833
|
-
mutations: [],
|
|
1834
|
-
subscriptions: []
|
|
1835
|
-
};
|
|
1836
|
-
|
|
1837
|
-
// Find Query type operations
|
|
1838
|
-
if (schema.queryType && schema.types[schema.queryType]) {
|
|
1839
|
-
const queryType = schema.types[schema.queryType];
|
|
1840
|
-
operations.queries = (queryType.fields || []).map(field => ({
|
|
1841
|
-
name: field.name,
|
|
1842
|
-
description: field.description,
|
|
1843
|
-
args: field.args || [],
|
|
1844
|
-
returnType: field.type,
|
|
1845
|
-
isDeprecated: field.isDeprecated,
|
|
1846
|
-
deprecationReason: field.deprecationReason
|
|
1847
|
-
}));
|
|
1848
|
-
}
|
|
1849
|
-
|
|
1850
|
-
// Find Mutation type operations
|
|
1851
|
-
if (schema.mutationType && schema.types[schema.mutationType]) {
|
|
1852
|
-
const mutationType = schema.types[schema.mutationType];
|
|
1853
|
-
operations.mutations = (mutationType.fields || []).map(field => ({
|
|
1854
|
-
name: field.name,
|
|
1855
|
-
description: field.description,
|
|
1856
|
-
args: field.args || [],
|
|
1857
|
-
returnType: field.type,
|
|
1858
|
-
isDeprecated: field.isDeprecated,
|
|
1859
|
-
deprecationReason: field.deprecationReason
|
|
1860
|
-
}));
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
// Find Subscription type operations
|
|
1864
|
-
if (schema.subscriptionType && schema.types[schema.subscriptionType]) {
|
|
1865
|
-
const subscriptionType = schema.types[schema.subscriptionType];
|
|
1866
|
-
operations.subscriptions = (subscriptionType.fields || []).map(field => ({
|
|
1867
|
-
name: field.name,
|
|
1868
|
-
description: field.description,
|
|
1869
|
-
args: field.args || [],
|
|
1870
|
-
returnType: field.type,
|
|
1871
|
-
isDeprecated: field.isDeprecated,
|
|
1872
|
-
deprecationReason: field.deprecationReason
|
|
1873
|
-
}));
|
|
1874
|
-
}
|
|
1875
|
-
|
|
1876
|
-
return operations;
|
|
1877
|
-
}
|
|
1878
|
-
|
|
1879
|
-
/**
|
|
1880
|
-
* Generate realistic value for GraphQL scalar type
|
|
1881
|
-
* GENERIC - Handles built-in scalars and custom scalars using field name semantics
|
|
1882
|
-
*/
|
|
1883
|
-
_generateGraphQLScalarValue(scalarType, fieldName = '') {
|
|
1884
|
-
const scalarName = scalarType.name;
|
|
1885
|
-
|
|
1886
|
-
// ===== BUILT-IN SCALARS (GraphQL Spec - these 5 are always present) =====
|
|
1887
|
-
switch (scalarName) {
|
|
1888
|
-
case 'String':
|
|
1889
|
-
return this._generateStringValue({ type: 'string' }, fieldName);
|
|
1890
|
-
|
|
1891
|
-
case 'Int':
|
|
1892
|
-
return this._generateNumericValue({ type: 'integer' }, fieldName);
|
|
1893
|
-
|
|
1894
|
-
case 'Float':
|
|
1895
|
-
return this._generateNumericValue({ type: 'number' }, fieldName);
|
|
1896
|
-
|
|
1897
|
-
case 'Boolean':
|
|
1898
|
-
return this._generateBooleanValue({}, fieldName);
|
|
1899
|
-
|
|
1900
|
-
case 'ID':
|
|
1901
|
-
// Generic ID generation
|
|
1902
|
-
if (faker) {
|
|
1903
|
-
return faker.string.alphanumeric(16);
|
|
1904
|
-
}
|
|
1905
|
-
return `id_${Math.random().toString(36).substr(2, 9)}`;
|
|
1906
|
-
}
|
|
1907
|
-
|
|
1908
|
-
// ===== COMMON CUSTOM SCALAR PATTERNS (Generic Detection) =====
|
|
1909
|
-
const lowerScalar = scalarName.toLowerCase();
|
|
1910
|
-
|
|
1911
|
-
// Date/Time scalars (various naming conventions)
|
|
1912
|
-
if (lowerScalar.includes('date') || lowerScalar.includes('time')) {
|
|
1913
|
-
if (lowerScalar.includes('date') && !lowerScalar.includes('time')) {
|
|
1914
|
-
return this._generateStringValue({ format: 'date' }, fieldName);
|
|
1915
|
-
}
|
|
1916
|
-
return this._generateStringValue({ format: 'date-time' }, fieldName);
|
|
1917
|
-
}
|
|
1918
|
-
|
|
1919
|
-
// URL/URI scalars
|
|
1920
|
-
if (lowerScalar.includes('url') || lowerScalar.includes('uri')) {
|
|
1921
|
-
return this._generateStringValue({ format: 'uri' }, fieldName);
|
|
1922
|
-
}
|
|
1923
|
-
|
|
1924
|
-
// JSON scalars
|
|
1925
|
-
if (lowerScalar.includes('json')) {
|
|
1926
|
-
return { example: 'json_data' };
|
|
1927
|
-
}
|
|
1928
|
-
|
|
1929
|
-
// Email scalars
|
|
1930
|
-
if (lowerScalar.includes('email')) {
|
|
1931
|
-
return this._generateStringValue({ format: 'email' }, fieldName);
|
|
1932
|
-
}
|
|
1933
|
-
|
|
1934
|
-
// HTML/Markdown scalars
|
|
1935
|
-
if (lowerScalar.includes('html')) {
|
|
1936
|
-
return '<p>Sample HTML content</p>';
|
|
1937
|
-
}
|
|
1938
|
-
|
|
1939
|
-
if (lowerScalar.includes('markdown')) {
|
|
1940
|
-
return '# Sample Markdown\n\nThis is sample content.';
|
|
1941
|
-
}
|
|
1942
|
-
|
|
1943
|
-
// ===== FALLBACK: Use field name semantic analysis =====
|
|
1944
|
-
// This is the SECRET WEAPON - works for ANY custom scalar!
|
|
1945
|
-
// We analyze the field name to generate appropriate data
|
|
1946
|
-
return this._generateStringValue({ type: 'string' }, fieldName);
|
|
1947
|
-
}
|
|
1948
|
-
|
|
1949
|
-
/**
|
|
1950
|
-
* Recursively generate sample data from GraphQL type
|
|
1951
|
-
* GENERIC - Handles all GraphQL type kinds with circular reference protection
|
|
1952
|
-
*/
|
|
1953
|
-
_generateSampleFromGraphQLType(type, schema, fieldName = '', visited = new Set(), depth = 0) {
|
|
1954
|
-
// Prevent infinite recursion
|
|
1955
|
-
const MAX_DEPTH = 5;
|
|
1956
|
-
if (depth > MAX_DEPTH) {
|
|
1957
|
-
return null;
|
|
1958
|
-
}
|
|
1959
|
-
|
|
1960
|
-
// Circular reference detection
|
|
1961
|
-
const typeKey = type.name ? `${type.name}_${fieldName}` : `${type.kind}_${fieldName}`;
|
|
1962
|
-
if (visited.has(typeKey)) {
|
|
1963
|
-
return null;
|
|
1964
|
-
}
|
|
1965
|
-
|
|
1966
|
-
switch (type.kind) {
|
|
1967
|
-
case 'NON_NULL':
|
|
1968
|
-
// Unwrap non-null and continue
|
|
1969
|
-
return this._generateSampleFromGraphQLType(type.ofType, schema, fieldName, visited, depth);
|
|
1970
|
-
|
|
1971
|
-
case 'LIST':
|
|
1972
|
-
// Generate array with 1-2 sample items
|
|
1973
|
-
visited.add(typeKey);
|
|
1974
|
-
const itemValue = this._generateSampleFromGraphQLType(type.ofType, schema, fieldName, visited, depth + 1);
|
|
1975
|
-
visited.delete(typeKey);
|
|
1976
|
-
return itemValue !== null ? [itemValue] : [];
|
|
1977
|
-
|
|
1978
|
-
case 'SCALAR':
|
|
1979
|
-
return this._generateGraphQLScalarValue(type, fieldName);
|
|
1980
|
-
|
|
1981
|
-
case 'ENUM':
|
|
1982
|
-
// Pick first enum value
|
|
1983
|
-
const enumType = schema.types[type.name];
|
|
1984
|
-
if (enumType?.enumValues && enumType.enumValues.length > 0) {
|
|
1985
|
-
return enumType.enumValues[0].name;
|
|
1986
|
-
}
|
|
1987
|
-
return 'ENUM_VALUE';
|
|
1988
|
-
|
|
1989
|
-
case 'OBJECT':
|
|
1990
|
-
case 'INPUT_OBJECT':
|
|
1991
|
-
visited.add(typeKey);
|
|
1992
|
-
const objValue = this._generateGraphQLObject(type, schema, visited, depth + 1);
|
|
1993
|
-
visited.delete(typeKey);
|
|
1994
|
-
return objValue;
|
|
1995
|
-
|
|
1996
|
-
case 'INTERFACE':
|
|
1997
|
-
// For interfaces, generate fields from the interface itself
|
|
1998
|
-
visited.add(typeKey);
|
|
1999
|
-
const interfaceValue = this._generateGraphQLObject(type, schema, visited, depth + 1);
|
|
2000
|
-
visited.delete(typeKey);
|
|
2001
|
-
return interfaceValue;
|
|
2002
|
-
|
|
2003
|
-
case 'UNION':
|
|
2004
|
-
// Pick first possible type
|
|
2005
|
-
const unionType = schema.types[type.name];
|
|
2006
|
-
if (unionType?.possibleTypes && unionType.possibleTypes.length > 0) {
|
|
2007
|
-
const firstType = unionType.possibleTypes[0];
|
|
2008
|
-
return this._generateSampleFromGraphQLType(firstType, schema, fieldName, visited, depth + 1);
|
|
2009
|
-
}
|
|
2010
|
-
return null;
|
|
2011
|
-
|
|
2012
|
-
default:
|
|
2013
|
-
return null;
|
|
2014
|
-
}
|
|
2015
|
-
}
|
|
2016
|
-
|
|
2017
|
-
/**
|
|
2018
|
-
* Generate sample object with all fields
|
|
2019
|
-
* GENERIC - Works with any GraphQL object type
|
|
2020
|
-
*/
|
|
2021
|
-
_generateGraphQLObject(type, schema, visited, depth) {
|
|
2022
|
-
const typeDefinition = schema.types[type.name];
|
|
2023
|
-
if (!typeDefinition) {
|
|
2024
|
-
return {};
|
|
2025
|
-
}
|
|
2026
|
-
|
|
2027
|
-
const obj = {};
|
|
2028
|
-
const fields = typeDefinition.fields || typeDefinition.inputFields || [];
|
|
2029
|
-
|
|
2030
|
-
// Limit fields at deep nesting levels to prevent bloat
|
|
2031
|
-
const maxFieldsAtDepth = depth > 3 ? 3 : (depth > 2 ? 5 : 10);
|
|
2032
|
-
const fieldsToGenerate = fields.slice(0, maxFieldsAtDepth);
|
|
2033
|
-
|
|
2034
|
-
for (const field of fieldsToGenerate) {
|
|
2035
|
-
// Use field name for semantic data generation
|
|
2036
|
-
const value = this._generateSampleFromGraphQLType(
|
|
2037
|
-
field.type,
|
|
2038
|
-
schema,
|
|
2039
|
-
field.name,
|
|
2040
|
-
visited,
|
|
2041
|
-
depth
|
|
2042
|
-
);
|
|
2043
|
-
|
|
2044
|
-
if (value !== null) {
|
|
2045
|
-
obj[field.name] = value;
|
|
2046
|
-
}
|
|
2047
|
-
}
|
|
2048
|
-
|
|
2049
|
-
return obj;
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
/**
|
|
2053
|
-
* Build GraphQL query string with variables
|
|
2054
|
-
* GENERIC - Constructs valid GraphQL query/mutation syntax
|
|
2055
|
-
*/
|
|
2056
|
-
_buildGraphQLQueryString(operation, args, schema, operationType = 'query') {
|
|
2057
|
-
const operationName = this._capitalize(operation.name);
|
|
2058
|
-
|
|
2059
|
-
// Build variable definitions
|
|
2060
|
-
const variableDefinitions = [];
|
|
2061
|
-
const argumentsList = [];
|
|
2062
|
-
|
|
2063
|
-
for (const arg of args) {
|
|
2064
|
-
const varName = arg.name;
|
|
2065
|
-
const typeString = this._getGraphQLTypeString(arg.type);
|
|
2066
|
-
variableDefinitions.push(`$${varName}: ${typeString}`);
|
|
2067
|
-
argumentsList.push(`${arg.name}: $${varName}`);
|
|
2068
|
-
}
|
|
2069
|
-
|
|
2070
|
-
// Build field selection (simplified - select first level fields)
|
|
2071
|
-
const returnType = this._unwrapType(operation.returnType);
|
|
2072
|
-
const fields = this._buildFieldSelection(returnType, schema, 0);
|
|
2073
|
-
|
|
2074
|
-
// Construct query string
|
|
2075
|
-
let queryString = operationType;
|
|
2076
|
-
if (variableDefinitions.length > 0) {
|
|
2077
|
-
queryString += ` ${operationName}(${variableDefinitions.join(', ')})`;
|
|
2078
|
-
} else {
|
|
2079
|
-
queryString += ` ${operationName}`;
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
queryString += ' {\n';
|
|
2083
|
-
if (argumentsList.length > 0) {
|
|
2084
|
-
queryString += ` ${operation.name}(${argumentsList.join(', ')}) `;
|
|
2085
|
-
} else {
|
|
2086
|
-
queryString += ` ${operation.name} `;
|
|
2087
|
-
}
|
|
2088
|
-
queryString += fields;
|
|
2089
|
-
queryString += '\n}';
|
|
2090
|
-
|
|
2091
|
-
return queryString;
|
|
2092
|
-
}
|
|
2093
|
-
|
|
2094
|
-
/**
|
|
2095
|
-
* Build field selection for GraphQL query
|
|
2096
|
-
* GENERIC - Selects appropriate fields based on return type
|
|
2097
|
-
*/
|
|
2098
|
-
_buildFieldSelection(type, schema, depth = 0) {
|
|
2099
|
-
const MAX_DEPTH = 2;
|
|
2100
|
-
if (depth > MAX_DEPTH) {
|
|
2101
|
-
return '';
|
|
2102
|
-
}
|
|
2103
|
-
|
|
2104
|
-
const typeDefinition = schema.types[type.name];
|
|
2105
|
-
if (!typeDefinition || !typeDefinition.fields || typeDefinition.fields.length === 0) {
|
|
2106
|
-
return '';
|
|
2107
|
-
}
|
|
2108
|
-
|
|
2109
|
-
let selection = '{\n';
|
|
2110
|
-
const indent = ' '.repeat(depth + 2);
|
|
2111
|
-
|
|
2112
|
-
// Select up to 5 fields to keep queries manageable
|
|
2113
|
-
const fieldsToSelect = typeDefinition.fields.slice(0, 5);
|
|
2114
|
-
|
|
2115
|
-
for (const field of fieldsToSelect) {
|
|
2116
|
-
const fieldType = this._unwrapType(field.type);
|
|
2117
|
-
const fieldTypeDefinition = schema.types[fieldType.name];
|
|
2118
|
-
|
|
2119
|
-
// If field is scalar or enum, just select it
|
|
2120
|
-
if (!fieldTypeDefinition || fieldType.kind === 'SCALAR' || fieldType.kind === 'ENUM') {
|
|
2121
|
-
selection += `${indent}${field.name}\n`;
|
|
2122
|
-
} else if (fieldType.kind === 'OBJECT' && depth < MAX_DEPTH) {
|
|
2123
|
-
// If field is object, recursively build selection
|
|
2124
|
-
const subSelection = this._buildFieldSelection(fieldType, schema, depth + 1);
|
|
2125
|
-
if (subSelection) {
|
|
2126
|
-
selection += `${indent}${field.name} ${subSelection}`;
|
|
2127
|
-
} else {
|
|
2128
|
-
selection += `${indent}${field.name}\n`;
|
|
2129
|
-
}
|
|
2130
|
-
}
|
|
2131
|
-
}
|
|
2132
|
-
|
|
2133
|
-
selection += ' '.repeat(depth + 1) + '}';
|
|
2134
|
-
return selection;
|
|
2135
|
-
}
|
|
2136
|
-
|
|
2137
|
-
/**
|
|
2138
|
-
* Get GraphQL type string (e.g., "String!", "[ID!]!")
|
|
2139
|
-
* GENERIC - Constructs type strings following GraphQL syntax
|
|
2140
|
-
*/
|
|
2141
|
-
_getGraphQLTypeString(type) {
|
|
2142
|
-
if (type.kind === 'NON_NULL') {
|
|
2143
|
-
return this._getGraphQLTypeString(type.ofType) + '!';
|
|
2144
|
-
}
|
|
2145
|
-
if (type.kind === 'LIST') {
|
|
2146
|
-
return '[' + this._getGraphQLTypeString(type.ofType) + ']';
|
|
2147
|
-
}
|
|
2148
|
-
return type.name;
|
|
2149
|
-
}
|
|
2150
|
-
|
|
2151
|
-
/**
|
|
2152
|
-
* Unwrap type to get base type (remove NON_NULL and LIST wrappers)
|
|
2153
|
-
*/
|
|
2154
|
-
_unwrapType(type) {
|
|
2155
|
-
if (type.kind === 'NON_NULL' || type.kind === 'LIST') {
|
|
2156
|
-
return this._unwrapType(type.ofType);
|
|
2157
|
-
}
|
|
2158
|
-
return type;
|
|
2159
|
-
}
|
|
2160
|
-
|
|
2161
|
-
/**
|
|
2162
|
-
* Capitalize first letter
|
|
2163
|
-
*/
|
|
2164
|
-
_capitalize(str) {
|
|
2165
|
-
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
2166
|
-
}
|
|
2167
|
-
|
|
2168
|
-
/**
|
|
2169
|
-
* Generate test section for GraphQL query
|
|
2170
|
-
* GENERIC - Creates test scenarios for any GraphQL query operation
|
|
2171
|
-
*/
|
|
2172
|
-
_generateGraphQLQueryTestSection(query, schema, options) {
|
|
2173
|
-
const scenarios = [];
|
|
2174
|
-
|
|
2175
|
-
// Happy path scenario
|
|
2176
|
-
const happyPathScenario = {
|
|
2177
|
-
title: `${query.name} - Happy Path`,
|
|
2178
|
-
method: 'POST',
|
|
2179
|
-
endpoint: '/graphql',
|
|
2180
|
-
description: query.description || `Test successful execution of ${query.name} query`,
|
|
2181
|
-
data: {
|
|
2182
|
-
query: this._buildGraphQLQueryString(query, query.args, schema, 'query'),
|
|
2183
|
-
variables: this._generateGraphQLVariables(query.args, schema)
|
|
2184
|
-
},
|
|
2185
|
-
headers: {
|
|
2186
|
-
'Content-Type': 'application/json'
|
|
2187
|
-
},
|
|
2188
|
-
expect: {
|
|
2189
|
-
status: 200,
|
|
2190
|
-
body: {
|
|
2191
|
-
data: {
|
|
2192
|
-
[query.name]: this._generateExpectedGraphQLResponse(query.returnType, schema)
|
|
2193
|
-
}
|
|
2194
|
-
}
|
|
2195
|
-
}
|
|
2196
|
-
};
|
|
2197
|
-
|
|
2198
|
-
if (options.includeAuth) {
|
|
2199
|
-
happyPathScenario.headers['Authorization'] = 'Bearer {{auth_token}}';
|
|
2200
|
-
}
|
|
2201
|
-
|
|
2202
|
-
scenarios.push(happyPathScenario);
|
|
2203
|
-
|
|
2204
|
-
// Error scenarios
|
|
2205
|
-
if (options.includeErrorHandling) {
|
|
2206
|
-
// Missing required arguments
|
|
2207
|
-
if (query.args.some(arg => arg.type.kind === 'NON_NULL')) {
|
|
2208
|
-
scenarios.push({
|
|
2209
|
-
title: `${query.name} - Missing Required Arguments`,
|
|
2210
|
-
method: 'POST',
|
|
2211
|
-
endpoint: '/graphql',
|
|
2212
|
-
description: 'Test error handling when required arguments are missing',
|
|
2213
|
-
data: {
|
|
2214
|
-
query: this._buildGraphQLQueryString(query, query.args, schema, 'query'),
|
|
2215
|
-
variables: {}
|
|
2216
|
-
},
|
|
2217
|
-
headers: {
|
|
2218
|
-
'Content-Type': 'application/json'
|
|
2219
|
-
},
|
|
2220
|
-
expect: {
|
|
2221
|
-
status: 200,
|
|
2222
|
-
body: {
|
|
2223
|
-
errors: [
|
|
2224
|
-
{
|
|
2225
|
-
message: 'string'
|
|
2226
|
-
}
|
|
2227
|
-
]
|
|
2228
|
-
}
|
|
2229
|
-
}
|
|
2230
|
-
});
|
|
2231
|
-
}
|
|
2232
|
-
|
|
2233
|
-
// Invalid field selection
|
|
2234
|
-
scenarios.push({
|
|
2235
|
-
title: `${query.name} - Invalid Field Selection`,
|
|
2236
|
-
method: 'POST',
|
|
2237
|
-
endpoint: '/graphql',
|
|
2238
|
-
description: 'Test error handling for invalid field in query',
|
|
2239
|
-
data: {
|
|
2240
|
-
query: `query { ${query.name} { invalidField } }`,
|
|
2241
|
-
variables: this._generateGraphQLVariables(query.args, schema)
|
|
2242
|
-
},
|
|
2243
|
-
headers: {
|
|
2244
|
-
'Content-Type': 'application/json'
|
|
2245
|
-
},
|
|
2246
|
-
expect: {
|
|
2247
|
-
status: 200,
|
|
2248
|
-
body: {
|
|
2249
|
-
errors: [
|
|
2250
|
-
{
|
|
2251
|
-
message: 'string'
|
|
2252
|
-
}
|
|
2253
|
-
]
|
|
2254
|
-
}
|
|
2255
|
-
}
|
|
2256
|
-
});
|
|
2257
|
-
}
|
|
2258
|
-
|
|
2259
|
-
// Authentication error
|
|
2260
|
-
if (options.includeAuth) {
|
|
2261
|
-
scenarios.push({
|
|
2262
|
-
title: `${query.name} - Unauthorized Access`,
|
|
2263
|
-
method: 'POST',
|
|
2264
|
-
endpoint: '/graphql',
|
|
2265
|
-
description: 'Test access without authentication',
|
|
2266
|
-
data: {
|
|
2267
|
-
query: this._buildGraphQLQueryString(query, query.args, schema, 'query'),
|
|
2268
|
-
variables: this._generateGraphQLVariables(query.args, schema)
|
|
2269
|
-
},
|
|
2270
|
-
headers: {
|
|
2271
|
-
'Content-Type': 'application/json'
|
|
2272
|
-
},
|
|
2273
|
-
expect: {
|
|
2274
|
-
status: [200, 401],
|
|
2275
|
-
body: {
|
|
2276
|
-
errors: [
|
|
2277
|
-
{
|
|
2278
|
-
message: 'string'
|
|
2279
|
-
}
|
|
2280
|
-
]
|
|
2281
|
-
}
|
|
2282
|
-
}
|
|
2283
|
-
});
|
|
2284
|
-
}
|
|
2285
|
-
|
|
2286
|
-
return {
|
|
2287
|
-
title: `Query: ${query.name}`,
|
|
2288
|
-
description: query.description || `Test cases for ${query.name} query`,
|
|
2289
|
-
scenarios: scenarios
|
|
2290
|
-
};
|
|
2291
|
-
}
|
|
2292
|
-
|
|
2293
|
-
/**
|
|
2294
|
-
* Generate test section for GraphQL mutation
|
|
2295
|
-
* GENERIC - Creates test scenarios for any GraphQL mutation operation
|
|
2296
|
-
*/
|
|
2297
|
-
_generateGraphQLMutationTestSection(mutation, schema, options) {
|
|
2298
|
-
const scenarios = [];
|
|
2299
|
-
|
|
2300
|
-
// Happy path scenario
|
|
2301
|
-
const happyPathScenario = {
|
|
2302
|
-
title: `${mutation.name} - Happy Path`,
|
|
2303
|
-
method: 'POST',
|
|
2304
|
-
endpoint: '/graphql',
|
|
2305
|
-
description: mutation.description || `Test successful execution of ${mutation.name} mutation`,
|
|
2306
|
-
data: {
|
|
2307
|
-
query: this._buildGraphQLQueryString(mutation, mutation.args, schema, 'mutation'),
|
|
2308
|
-
variables: this._generateGraphQLVariables(mutation.args, schema)
|
|
2309
|
-
},
|
|
2310
|
-
headers: {
|
|
2311
|
-
'Content-Type': 'application/json'
|
|
2312
|
-
},
|
|
2313
|
-
expect: {
|
|
2314
|
-
status: 200,
|
|
2315
|
-
body: {
|
|
2316
|
-
data: {
|
|
2317
|
-
[mutation.name]: this._generateExpectedGraphQLResponse(mutation.returnType, schema)
|
|
2318
|
-
}
|
|
2319
|
-
}
|
|
2320
|
-
}
|
|
2321
|
-
};
|
|
2322
|
-
|
|
2323
|
-
if (options.includeAuth) {
|
|
2324
|
-
happyPathScenario.headers['Authorization'] = 'Bearer {{auth_token}}';
|
|
2325
|
-
}
|
|
2326
|
-
|
|
2327
|
-
scenarios.push(happyPathScenario);
|
|
2328
|
-
|
|
2329
|
-
// Error scenarios
|
|
2330
|
-
if (options.includeErrorHandling) {
|
|
2331
|
-
// Invalid input data
|
|
2332
|
-
scenarios.push({
|
|
2333
|
-
title: `${mutation.name} - Invalid Input Data`,
|
|
2334
|
-
method: 'POST',
|
|
2335
|
-
endpoint: '/graphql',
|
|
2336
|
-
description: 'Test error handling with invalid input data',
|
|
2337
|
-
data: {
|
|
2338
|
-
query: this._buildGraphQLQueryString(mutation, mutation.args, schema, 'mutation'),
|
|
2339
|
-
variables: this._generateInvalidGraphQLVariables(mutation.args, schema)
|
|
2340
|
-
},
|
|
2341
|
-
headers: {
|
|
2342
|
-
'Content-Type': 'application/json'
|
|
2343
|
-
},
|
|
2344
|
-
expect: {
|
|
2345
|
-
status: 200,
|
|
2346
|
-
body: {
|
|
2347
|
-
errors: [
|
|
2348
|
-
{
|
|
2349
|
-
message: 'string'
|
|
2350
|
-
}
|
|
2351
|
-
]
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
});
|
|
2355
|
-
}
|
|
2356
|
-
|
|
2357
|
-
// Authorization error for mutations
|
|
2358
|
-
if (options.includeAuth) {
|
|
2359
|
-
scenarios.push({
|
|
2360
|
-
title: `${mutation.name} - Unauthorized Mutation`,
|
|
2361
|
-
method: 'POST',
|
|
2362
|
-
endpoint: '/graphql',
|
|
2363
|
-
description: 'Test mutation without proper authorization',
|
|
2364
|
-
data: {
|
|
2365
|
-
query: this._buildGraphQLQueryString(mutation, mutation.args, schema, 'mutation'),
|
|
2366
|
-
variables: this._generateGraphQLVariables(mutation.args, schema)
|
|
2367
|
-
},
|
|
2368
|
-
headers: {
|
|
2369
|
-
'Content-Type': 'application/json'
|
|
2370
|
-
},
|
|
2371
|
-
expect: {
|
|
2372
|
-
status: [200, 401, 403],
|
|
2373
|
-
body: {
|
|
2374
|
-
errors: [
|
|
2375
|
-
{
|
|
2376
|
-
message: 'string'
|
|
2377
|
-
}
|
|
2378
|
-
]
|
|
2379
|
-
}
|
|
2380
|
-
}
|
|
2381
|
-
});
|
|
2382
|
-
}
|
|
2383
|
-
|
|
2384
|
-
return {
|
|
2385
|
-
title: `Mutation: ${mutation.name}`,
|
|
2386
|
-
description: mutation.description || `Test cases for ${mutation.name} mutation`,
|
|
2387
|
-
scenarios: scenarios
|
|
2388
|
-
};
|
|
2389
|
-
}
|
|
2390
|
-
|
|
2391
|
-
/**
|
|
2392
|
-
* Generate test section for GraphQL subscription
|
|
2393
|
-
* GENERIC - Creates test scenarios for any GraphQL subscription operation
|
|
2394
|
-
*/
|
|
2395
|
-
_generateGraphQLSubscriptionTestSection(subscription, schema, options) {
|
|
2396
|
-
const scenarios = [];
|
|
2397
|
-
|
|
2398
|
-
// Happy path scenario
|
|
2399
|
-
const happyPathScenario = {
|
|
2400
|
-
title: `${subscription.name} - Happy Path`,
|
|
2401
|
-
method: 'POST',
|
|
2402
|
-
endpoint: '/graphql',
|
|
2403
|
-
description: subscription.description || `Test successful subscription to ${subscription.name}`,
|
|
2404
|
-
data: {
|
|
2405
|
-
query: this._buildGraphQLQueryString(subscription, subscription.args, schema, 'subscription'),
|
|
2406
|
-
variables: this._generateGraphQLVariables(subscription.args, schema)
|
|
2407
|
-
},
|
|
2408
|
-
headers: {
|
|
2409
|
-
'Content-Type': 'application/json'
|
|
2410
|
-
},
|
|
2411
|
-
expect: {
|
|
2412
|
-
status: 200,
|
|
2413
|
-
body: {
|
|
2414
|
-
data: {
|
|
2415
|
-
[subscription.name]: this._generateExpectedGraphQLResponse(subscription.returnType, schema)
|
|
2416
|
-
}
|
|
2417
|
-
}
|
|
2418
|
-
}
|
|
2419
|
-
};
|
|
2420
|
-
|
|
2421
|
-
if (options.includeAuth) {
|
|
2422
|
-
happyPathScenario.headers['Authorization'] = 'Bearer {{auth_token}}';
|
|
2423
|
-
}
|
|
2424
|
-
|
|
2425
|
-
scenarios.push(happyPathScenario);
|
|
2426
|
-
|
|
2427
|
-
return {
|
|
2428
|
-
title: `Subscription: ${subscription.name}`,
|
|
2429
|
-
description: subscription.description || `Test cases for ${subscription.name} subscription`,
|
|
2430
|
-
scenarios: scenarios
|
|
2431
|
-
};
|
|
2432
|
-
}
|
|
2433
|
-
|
|
2434
|
-
/**
|
|
2435
|
-
* Generate variables object for GraphQL operation
|
|
2436
|
-
* GENERIC - Creates realistic variable values based on argument types
|
|
2437
|
-
*/
|
|
2438
|
-
_generateGraphQLVariables(args, schema) {
|
|
2439
|
-
const variables = {};
|
|
2440
|
-
|
|
2441
|
-
for (const arg of args) {
|
|
2442
|
-
const argType = this._unwrapType(arg.type);
|
|
2443
|
-
variables[arg.name] = this._generateSampleFromGraphQLType(argType, schema, arg.name, new Set(), 0);
|
|
2444
|
-
}
|
|
2445
|
-
|
|
2446
|
-
return variables;
|
|
2447
|
-
}
|
|
2448
|
-
|
|
2449
|
-
/**
|
|
2450
|
-
* Generate invalid variables for error testing
|
|
2451
|
-
* GENERIC - Creates intentionally invalid data for negative testing
|
|
2452
|
-
*/
|
|
2453
|
-
_generateInvalidGraphQLVariables(args, schema) {
|
|
2454
|
-
const variables = {};
|
|
2455
|
-
|
|
2456
|
-
for (const arg of args) {
|
|
2457
|
-
const argType = this._unwrapType(arg.type);
|
|
2458
|
-
|
|
2459
|
-
// Generate wrong type of data based on expected type
|
|
2460
|
-
if (argType.kind === 'SCALAR') {
|
|
2461
|
-
switch (argType.name) {
|
|
2462
|
-
case 'String':
|
|
2463
|
-
variables[arg.name] = 12345; // Wrong type
|
|
2464
|
-
break;
|
|
2465
|
-
case 'Int':
|
|
2466
|
-
case 'Float':
|
|
2467
|
-
variables[arg.name] = 'not_a_number'; // Wrong type
|
|
2468
|
-
break;
|
|
2469
|
-
case 'Boolean':
|
|
2470
|
-
variables[arg.name] = 'not_a_boolean'; // Wrong type
|
|
2471
|
-
break;
|
|
2472
|
-
case 'ID':
|
|
2473
|
-
variables[arg.name] = { invalid: 'object' }; // Wrong type
|
|
2474
|
-
break;
|
|
2475
|
-
default:
|
|
2476
|
-
variables[arg.name] = null;
|
|
2477
|
-
}
|
|
2478
|
-
} else {
|
|
2479
|
-
variables[arg.name] = null;
|
|
2480
|
-
}
|
|
2481
|
-
}
|
|
2482
|
-
|
|
2483
|
-
return variables;
|
|
2484
|
-
}
|
|
2485
|
-
|
|
2486
|
-
/**
|
|
2487
|
-
* Generate expected response structure for GraphQL operation
|
|
2488
|
-
* GENERIC - Creates expected response based on return type
|
|
2489
|
-
*/
|
|
2490
|
-
_generateExpectedGraphQLResponse(returnType, schema) {
|
|
2491
|
-
const unwrappedType = this._unwrapType(returnType);
|
|
2492
|
-
return this._generateSampleFromGraphQLType(unwrappedType, schema, '', new Set(), 0);
|
|
2493
|
-
}
|
|
2494
|
-
|
|
2495
|
-
_generateMarkdownContent(testPlan) {
|
|
2496
|
-
let markdown = `# ${testPlan.summary.title}\n\n`;
|
|
2497
|
-
|
|
2498
|
-
markdown += `## API Overview\n\n`;
|
|
2499
|
-
markdown += `${testPlan.summary.description}\n\n`;
|
|
2500
|
-
markdown += `- **Base URL**: \`${testPlan.summary.baseUrl}\`\n`;
|
|
2501
|
-
markdown += `- **Version**: ${testPlan.summary.version}\n`;
|
|
2502
|
-
markdown += `- **Total Endpoints**: ${testPlan.summary.totalEndpoints}\n`;
|
|
2503
|
-
markdown += `- **Total Test Scenarios**: ${testPlan.summary.totalScenarios}\n`;
|
|
2504
|
-
|
|
2505
|
-
// Add validation summary if available
|
|
2506
|
-
if (testPlan.validationSummary) {
|
|
2507
|
-
const vs = testPlan.validationSummary;
|
|
2508
|
-
markdown += `\n### 🔍 Validation Summary\n\n`;
|
|
2509
|
-
markdown += `- **Endpoints Validated**: ${vs.totalValidated}\n`;
|
|
2510
|
-
markdown += `- **✅ Successful**: ${vs.successful}\n`;
|
|
2511
|
-
markdown += `- **❌ Failed**: ${vs.failed}\n`;
|
|
2512
|
-
markdown += `- **Success Rate**: ${vs.totalValidated > 0 ? Math.round((vs.successful / vs.totalValidated) * 100) : 0}%\n`;
|
|
2513
|
-
}
|
|
2514
|
-
|
|
2515
|
-
markdown += `\n`;
|
|
2516
|
-
|
|
2517
|
-
testPlan.sections.forEach((section, index) => {
|
|
2518
|
-
markdown += `## ${index + 1}. ${section.title}\n\n`;
|
|
2519
|
-
if (section.description) {
|
|
2520
|
-
markdown += `${section.description}\n\n`;
|
|
2521
|
-
}
|
|
2522
|
-
|
|
2523
|
-
section.scenarios.forEach((scenario, scenarioIndex) => {
|
|
2524
|
-
markdown += `### ${index + 1}.${scenarioIndex + 1} ${scenario.title}\n`;
|
|
2525
|
-
markdown += `**Endpoint:** \`${scenario.method} ${scenario.endpoint}\`\n\n`;
|
|
2526
|
-
|
|
2527
|
-
if (scenario.description) {
|
|
2528
|
-
markdown += `**Description:** ${scenario.description}\n\n`;
|
|
2529
|
-
}
|
|
2530
|
-
|
|
2531
|
-
if (scenario.headers) {
|
|
2532
|
-
markdown += `**Headers:**\n\`\`\`json\n${JSON.stringify(scenario.headers, null, 2)}\n\`\`\`\n\n`;
|
|
2533
|
-
}
|
|
2534
|
-
|
|
2535
|
-
if (scenario.query) {
|
|
2536
|
-
markdown += `**Query Parameters:**\n\`\`\`json\n${JSON.stringify(scenario.query, null, 2)}\n\`\`\`\n\n`;
|
|
2537
|
-
}
|
|
2538
|
-
|
|
2539
|
-
if (scenario.data) {
|
|
2540
|
-
markdown += `**Request Body:**\n\`\`\`json\n${JSON.stringify(scenario.data, null, 2)}\n\`\`\`\n\n`;
|
|
2541
|
-
}
|
|
2542
|
-
|
|
2543
|
-
if (scenario.expect) {
|
|
2544
|
-
markdown += `**Expected Response:**\n`;
|
|
2545
|
-
markdown += `- Status Code: ${Array.isArray(scenario.expect.status) ?
|
|
2546
|
-
scenario.expect.status.join(' or ') : scenario.expect.status}\n`;
|
|
2547
|
-
if (scenario.expect.body) {
|
|
2548
|
-
markdown += `- Response Body:\n\`\`\`json\n${JSON.stringify(scenario.expect.body, null, 2)}\n\`\`\`\n`;
|
|
2549
|
-
}
|
|
2550
|
-
markdown += `\n`;
|
|
2551
|
-
}
|
|
2552
|
-
|
|
2553
|
-
// Add validation results if available
|
|
2554
|
-
if (scenario.validation) {
|
|
2555
|
-
const val = scenario.validation;
|
|
2556
|
-
markdown += `**${val.success ? '✅' : '❌'} Validation Result:**\n`;
|
|
2557
|
-
if (val.success) {
|
|
2558
|
-
markdown += `- Status: SUCCESS\n`;
|
|
2559
|
-
markdown += `- Status Code: ${val.statusCode}\n`;
|
|
2560
|
-
markdown += `- Response Time: ${val.responseTime}ms\n`;
|
|
2561
|
-
if (val.responseBody) {
|
|
2562
|
-
markdown += `- Actual Response Body:\n\`\`\`json\n${JSON.stringify(val.responseBody, null, 2)}\n\`\`\`\n`;
|
|
2563
|
-
}
|
|
2564
|
-
} else {
|
|
2565
|
-
markdown += `- Status: FAILED\n`;
|
|
2566
|
-
markdown += `- Error: ${val.error}\n`;
|
|
2567
|
-
}
|
|
2568
|
-
markdown += `\n`;
|
|
2569
|
-
}
|
|
2570
|
-
|
|
2571
|
-
if (scenario.chain) {
|
|
2572
|
-
markdown += `**Request Chain:**\n`;
|
|
2573
|
-
scenario.chain.forEach((step, stepIndex) => {
|
|
2574
|
-
markdown += `${stepIndex + 1}. **${step.name}**: \`${step.method} ${step.endpoint}\`\n`;
|
|
2575
|
-
if (step.extract) {
|
|
2576
|
-
markdown += ` - Extract: ${Object.entries(step.extract).map(([k,v]) => `${k} from ${v}`).join(', ')}\n`;
|
|
2577
|
-
}
|
|
2578
|
-
});
|
|
2579
|
-
markdown += `\n`;
|
|
2580
|
-
}
|
|
2581
|
-
|
|
2582
|
-
markdown += `---\n\n`;
|
|
2583
|
-
});
|
|
2584
|
-
});
|
|
2585
|
-
|
|
2586
|
-
return markdown;
|
|
2587
|
-
}
|
|
2588
|
-
|
|
2589
|
-
async _saveTestPlan(testPlan, outputPath) {
|
|
2590
|
-
const dir = this.path.dirname(outputPath);
|
|
2591
|
-
if (!this.fs.existsSync(dir)) {
|
|
2592
|
-
this.fs.mkdirSync(dir, { recursive: true });
|
|
2593
|
-
}
|
|
2594
|
-
this.fs.writeFileSync(outputPath, testPlan.content, 'utf8');
|
|
2595
|
-
}
|
|
2596
|
-
}
|
|
2597
|
-
|
|
2598
|
-
module.exports = ApiPlannerTool;
|