import * as fs from 'fs'; import * as path from 'path'; import * as process from 'process'; import { INTROSPECTION_QUERY } from './introspection-query'; import { Field, InputValue, IntrospectionResponse, TypeRef, } from './introspection.interface'; const GRAPHQL_URL = 'http://localhost:3000/graphql'; const BEARER_TOKEN = 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwiaWF0IjoxNzI2NDkyNTAyLCJleHAiOjEzMjQ1MDE2NTAyfQ.zM6TbfeOqYVH5Sgryc2zf02hd9uqUOSL1-iJlMgwzsI'; const TEST_OUTPUT_DIR = './test'; const fetchGraphQLSchema = async (): Promise => { const headers = { Authorization: BEARER_TOKEN, 'Content-Type': 'application/json', }; const response = await fetch(GRAPHQL_URL, { method: 'POST', headers, body: JSON.stringify({ query: INTROSPECTION_QUERY }), }); if (!response.ok) { throw new Error(`Failed to fetch schema: ${response.statusText}`); } return response.json(); }; const toKebabCase = (name: string): string => { return name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); }; const unwrapType = (typeInfo: TypeRef): any => { while (typeInfo.ofType) { typeInfo = typeInfo.ofType; } return typeInfo; }; const hasRequiredArgs = (args: InputValue[]): boolean => { return args.some((arg) => unwrapType(arg.type).kind === 'NON_NULL'); }; const generateTestContent = ( queryName: string, fields: Field[], ): string | null => { const fieldNames = fields .filter((f) => ['SCALAR', 'ENUM'].includes(unwrapType(f.type).kind)) .map((f) => f.name); if (fieldNames.length === 0) { console.log(`Skipping ${queryName}: No usable fields found.`); return null; } const fieldSelection = fieldNames.join('\n '); const expectSelection = fieldNames .map((f) => `expect(${queryName}).toHaveProperty('${f}');`) .join('\n '); return `import request from 'supertest'; const client = request(\`http://localhost:\${APP_PORT}\`); describe('${queryName}Resolver (e2e)', () => { it('should find many ${queryName}', () => { const queryData = { query: \` query ${queryName} { ${queryName} { edges { node { ${fieldSelection} } } } } \`, }; return client .post('/graphql') .set('Authorization', \`Bearer \${ACCESS_TOKEN}\`) .send(queryData) .expect(200) .expect((res) => { expect(res.body.data).toBeDefined(); expect(res.body.errors).toBeUndefined(); }) .expect((res) => { const data = res.body.data.${queryName}; expect(data).toBeDefined(); expect(Array.isArray(data.edges)).toBe(true); const edges = data.edges; if (edges.length > 0) { const ${queryName} = edges[0].node; ${expectSelection} } }); }); }); `; }; const writeTestFile = ( queryName: string, content: string | null, force = false, ): string => { if (!content) return 'skipped'; const fileName = `${toKebabCase(queryName)}.integration-spec.ts`; const filePath = path.join(TEST_OUTPUT_DIR, fileName); if (fs.existsSync(filePath) && !force) { return 'skipped'; } fs.writeFileSync(filePath, content); return force ? 'updated' : 'created'; }; const generateTests = async (force = false) => { fs.mkdirSync(TEST_OUTPUT_DIR, { recursive: true }); const schemaData = await fetchGraphQLSchema(); const types = schemaData.data.__schema.types; const queryTypeName = schemaData.data.__schema.queryType.name; const queryType = types.find((t: any) => t.name === queryTypeName); let createdCount = 0; let updatedCount = 0; let totalCount = 0; if (!queryType?.fields) { console.log('No query fields found.'); return; } for (const query of queryType.fields) { const queryName = query.name; if (hasRequiredArgs(query.args)) continue; if (queryName.includes('Duplicates')) continue; const queryReturnType = unwrapType(query.type); if ( queryReturnType.kind === 'OBJECT' && queryReturnType.name.includes('Connection') ) { totalCount++; const connectionTypeInfo = types.find( (f: any) => f.name === queryReturnType.name, ); const edgeTypeInfo = connectionTypeInfo?.fields?.find( (f: any) => f.name === 'edges', ); if (edgeTypeInfo) { const returnType = unwrapType(edgeTypeInfo.type); const returnTypeInfo = types.find( (t: any) => t.name === returnType.name, ); const returnNodeTypeInfo = returnTypeInfo?.fields?.find( (f: any) => f.name === 'node', ); if (returnNodeTypeInfo) { const nodeType = unwrapType(returnNodeTypeInfo.type); const nodeTypeInfo = types.find((t: any) => t.name === nodeType.name); if (!nodeTypeInfo?.fields) { continue; } const content = generateTestContent(queryName, nodeTypeInfo?.fields); const result = writeTestFile(queryName, content, force); if (result === 'created') createdCount++; if (result === 'updated') updatedCount++; } } } } console.log(`Number of tests created: ${createdCount}/${totalCount}`); if (force) { console.log(`Number of tests updated: ${updatedCount}/${totalCount}`); } }; // Basic command-line argument parsing const forceArg = process.argv.includes('--force'); // Call the function with the parsed argument generateTests(forceArg);