@democratize-quality/mcp-server 1.1.2 → 1.1.3
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/README.md +2 -1
- package/package.json +2 -1
- package/src/chatmodes//360/237/214/220 api-planner.chatmode.md" +111 -16
- package/src/tools/api/api-generator.js +170 -5
- package/src/tools/api/api-planner.js +1026 -10
- package/src/tools/api/prompts/generation-prompts.js +131 -27
- package/src/tools/api/prompts/healing-prompts.js +56 -4
- package/src/tools/api/prompts/orchestrator.js +5 -4
- package/src/tools/api/prompts/validation-rules.js +90 -2
- package/src/utils/agentInstaller.js +1 -1
|
@@ -2,6 +2,21 @@ const ToolBase = require('../base/ToolBase');
|
|
|
2
2
|
const https = require('https');
|
|
3
3
|
const http = require('http');
|
|
4
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
|
+
}
|
|
5
20
|
|
|
6
21
|
// Try to load faker.js for enhanced sample data generation
|
|
7
22
|
let faker;
|
|
@@ -42,7 +57,7 @@ try {
|
|
|
42
57
|
// Input schema for the API planner tool
|
|
43
58
|
const apiPlannerInputSchema = z.object({
|
|
44
59
|
schemaUrl: z.string().optional(),
|
|
45
|
-
|
|
60
|
+
schemaPath: z.string().optional(),
|
|
46
61
|
schemaType: z.enum(['openapi', 'swagger', 'graphql', 'auto']).optional(),
|
|
47
62
|
apiBaseUrl: z.string().optional(),
|
|
48
63
|
includeAuth: z.boolean().optional(),
|
|
@@ -69,9 +84,9 @@ class ApiPlannerTool extends ToolBase {
|
|
|
69
84
|
type: "string",
|
|
70
85
|
description: "URL to fetch the API schema/documentation (e.g., OpenAPI spec URL, GraphQL introspection endpoint)"
|
|
71
86
|
},
|
|
72
|
-
|
|
87
|
+
schemaPath: {
|
|
73
88
|
type: "string",
|
|
74
|
-
description: "
|
|
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."
|
|
75
90
|
},
|
|
76
91
|
schemaType: {
|
|
77
92
|
type: "string",
|
|
@@ -173,9 +188,11 @@ class ApiPlannerTool extends ToolBase {
|
|
|
173
188
|
// Fetch or parse schema
|
|
174
189
|
let schemaData;
|
|
175
190
|
if (params.schemaUrl) {
|
|
176
|
-
schemaData = await this._fetchSchema(params.schemaUrl);
|
|
177
|
-
} else if (params.
|
|
178
|
-
schemaData = this.
|
|
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.');
|
|
179
196
|
}
|
|
180
197
|
|
|
181
198
|
if (!schemaData) {
|
|
@@ -233,7 +250,16 @@ class ApiPlannerTool extends ToolBase {
|
|
|
233
250
|
}
|
|
234
251
|
}
|
|
235
252
|
|
|
236
|
-
async _fetchSchema(url) {
|
|
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
|
+
|
|
237
263
|
return new Promise((resolve, reject) => {
|
|
238
264
|
const urlObj = new URL(url);
|
|
239
265
|
const client = urlObj.protocol === 'https:' ? https : http;
|
|
@@ -278,8 +304,160 @@ class ApiPlannerTool extends ToolBase {
|
|
|
278
304
|
req.end();
|
|
279
305
|
});
|
|
280
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
|
+
};
|
|
281
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
|
+
*/
|
|
282
458
|
_parseSchemaContent(content, schemaType) {
|
|
459
|
+
const trimmedContent = content.trim();
|
|
460
|
+
|
|
283
461
|
try {
|
|
284
462
|
// Try JSON first
|
|
285
463
|
return JSON.parse(content);
|
|
@@ -1470,10 +1648,848 @@ class ApiPlannerTool extends ToolBase {
|
|
|
1470
1648
|
};
|
|
1471
1649
|
}
|
|
1472
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
|
+
|
|
1473
1739
|
async _generateGraphQLTestPlan(schema, options) {
|
|
1474
|
-
//
|
|
1475
|
-
|
|
1476
|
-
|
|
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);
|
|
1477
2493
|
}
|
|
1478
2494
|
|
|
1479
2495
|
_generateMarkdownContent(testPlan) {
|