@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.
Files changed (64) hide show
  1. package/LICENSE +658 -12
  2. package/README.md +11 -6
  3. package/dist/server.d.ts +41 -0
  4. package/dist/server.d.ts.map +1 -0
  5. package/dist/server.js +225 -0
  6. package/dist/server.js.map +1 -0
  7. package/package.json +24 -25
  8. package/browserControl.js +0 -113
  9. package/cli.js +0 -228
  10. package/mcpServer.js +0 -335
  11. package/run-server.js +0 -140
  12. package/src/chatmodes//360/237/214/220 api-generator.chatmode.md" +0 -409
  13. package/src/chatmodes//360/237/214/220 api-healer.chatmode.md" +0 -494
  14. package/src/chatmodes//360/237/214/220 api-planner.chatmode.md" +0 -954
  15. package/src/config/environments/api-only.js +0 -53
  16. package/src/config/environments/development.js +0 -54
  17. package/src/config/environments/production.js +0 -69
  18. package/src/config/index.js +0 -341
  19. package/src/config/server.js +0 -41
  20. package/src/config/tools/api.js +0 -67
  21. package/src/config/tools/browser.js +0 -90
  22. package/src/config/tools/default.js +0 -32
  23. package/src/docs/Agent_README.md +0 -310
  24. package/src/docs/QUICK_REFERENCE.md +0 -111
  25. package/src/services/browserService.js +0 -325
  26. package/src/skills/api-planning/SKILL.md +0 -224
  27. package/src/skills/test-execution/SKILL.md +0 -728
  28. package/src/skills/test-generation/SKILL.md +0 -309
  29. package/src/skills/test-healing/SKILL.md +0 -405
  30. package/src/tools/api/api-generator.js +0 -1865
  31. package/src/tools/api/api-healer.js +0 -617
  32. package/src/tools/api/api-planner.js +0 -2598
  33. package/src/tools/api/api-project-setup.js +0 -313
  34. package/src/tools/api/api-request.js +0 -641
  35. package/src/tools/api/api-session-report.js +0 -1278
  36. package/src/tools/api/api-session-status.js +0 -395
  37. package/src/tools/api/prompts/README.md +0 -293
  38. package/src/tools/api/prompts/generation-prompts.js +0 -703
  39. package/src/tools/api/prompts/healing-prompts.js +0 -195
  40. package/src/tools/api/prompts/index.js +0 -25
  41. package/src/tools/api/prompts/orchestrator.js +0 -334
  42. package/src/tools/api/prompts/validation-rules.js +0 -339
  43. package/src/tools/base/ToolBase.js +0 -230
  44. package/src/tools/base/ToolRegistry.js +0 -269
  45. package/src/tools/browser/advanced/browser-console.js +0 -384
  46. package/src/tools/browser/advanced/browser-dialog.js +0 -319
  47. package/src/tools/browser/advanced/browser-evaluate.js +0 -337
  48. package/src/tools/browser/advanced/browser-file.js +0 -480
  49. package/src/tools/browser/advanced/browser-keyboard.js +0 -343
  50. package/src/tools/browser/advanced/browser-mouse.js +0 -332
  51. package/src/tools/browser/advanced/browser-network.js +0 -421
  52. package/src/tools/browser/advanced/browser-pdf.js +0 -407
  53. package/src/tools/browser/advanced/browser-tabs.js +0 -497
  54. package/src/tools/browser/advanced/browser-wait.js +0 -378
  55. package/src/tools/browser/click.js +0 -168
  56. package/src/tools/browser/close.js +0 -60
  57. package/src/tools/browser/dom.js +0 -70
  58. package/src/tools/browser/launch.js +0 -67
  59. package/src/tools/browser/navigate.js +0 -270
  60. package/src/tools/browser/screenshot.js +0 -351
  61. package/src/tools/browser/type.js +0 -174
  62. package/src/tools/index.js +0 -95
  63. package/src/utils/agentInstaller.js +0 -418
  64. 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;