import assert from 'node:assert/strict' import fs from 'node:fs/promises' import http from 'node:http' import os from 'node:os' import path from 'node:path' import test from 'node:test' import { generateApiFiles } from '../src/core/generate.js' const createTempDir = async () => { return fs.mkdtemp(path.join(os.tmpdir(), 'yx-generate-api-')) } const writeJson = async (filePath, value) => { await fs.writeFile(filePath, JSON.stringify(value, null, 2), 'utf8') } const readFile = async (filePath) => { return fs.readFile(filePath, 'utf8') } test('full generation cleans stale auto-generated files and keeps manual files', async () => { const tempDir = await createTempDir() try { const initialSwaggerPath = path.join(tempDir, 'swagger-initial.json') const updatedSwaggerPath = path.join(tempDir, 'swagger-updated.json') const outputDir = path.join(tempDir, 'generated') await writeJson(initialSwaggerPath, { openapi: '3.0.0', paths: { '/api/v1/Alpha/GetThing': { get: { tags: ['Alpha'], summary: 'Get alpha thing', responses: { 200: { description: 'OK' }, }, }, }, '/api/v1/Beta/GetList': { get: { tags: ['Beta'], summary: 'Get beta list', responses: { 200: { description: 'OK' }, }, }, }, }, }) await writeJson(updatedSwaggerPath, { openapi: '3.0.0', paths: { '/api/v1/Alpha/GetThing': { get: { tags: ['Alpha'], summary: 'Get alpha thing', responses: { 200: { description: 'OK' }, }, }, }, }, }) await generateApiFiles({ projectRoot: tempDir, swaggerUrl: initialSwaggerPath, swaggerTimeoutMs: 1000, outputDir, requestImport: '../request', paramStyle: 'object', modules: [], cleanOutput: true, }) await fs.writeFile(path.join(outputDir, 'manual-helper.js'), 'export const keepMe = true\n', 'utf8') await generateApiFiles({ projectRoot: tempDir, swaggerUrl: updatedSwaggerPath, swaggerTimeoutMs: 1000, outputDir, requestImport: '../request', paramStyle: 'object', modules: [], cleanOutput: true, }) await assert.doesNotReject(fs.access(path.join(outputDir, 'alpha.js'))) await assert.rejects(fs.access(path.join(outputDir, 'beta.js'))) await assert.doesNotReject(fs.access(path.join(outputDir, 'manual-helper.js'))) const sharedFile = await readFile(path.join(outputDir, 'shared.js')) assert.doesNotMatch(sharedFile, /from 'qs'/) assert.match(sharedFile, /appendQueryEntries/) } finally { await fs.rm(tempDir, { recursive: true, force: true }) } }) test('partial generation keeps other generated modules even when cleanOutput is enabled', async () => { const tempDir = await createTempDir() try { const swaggerPath = path.join(tempDir, 'swagger.json') const outputDir = path.join(tempDir, 'generated') await writeJson(swaggerPath, { openapi: '3.0.0', paths: { '/api/v1/Alpha/GetThing': { get: { tags: ['Alpha'], responses: { 200: { description: 'OK' }, }, }, }, '/api/v1/Beta/GetList': { get: { tags: ['Beta'], responses: { 200: { description: 'OK' }, }, }, }, }, }) await generateApiFiles({ projectRoot: tempDir, swaggerUrl: swaggerPath, swaggerTimeoutMs: 1000, outputDir, requestImport: '../request', paramStyle: 'object', modules: [], cleanOutput: true, }) await generateApiFiles({ projectRoot: tempDir, swaggerUrl: swaggerPath, swaggerTimeoutMs: 1000, outputDir, requestImport: '../request', paramStyle: 'object', modules: ['Alpha'], cleanOutput: true, }) await assert.doesNotReject(fs.access(path.join(outputDir, 'beta.js'))) } finally { await fs.rm(tempDir, { recursive: true, force: true }) } }) test('generated index includes namespace exports and flattened re-exports', async () => { const tempDir = await createTempDir() try { const swaggerPath = path.join(tempDir, 'swagger.json') const outputDir = path.join(tempDir, 'generated') await writeJson(swaggerPath, { openapi: '3.0.0', paths: { '/api/v1/Alpha/GetThing': { get: { tags: ['Alpha'], responses: { 200: { description: 'OK' }, }, }, }, '/api/v1/Beta/GetList': { get: { tags: ['Beta'], responses: { 200: { description: 'OK' }, }, }, }, '/api/v1/Beta/GetDetail': { get: { tags: ['Beta'], responses: { 200: { description: 'OK' }, }, }, }, }, }) await generateApiFiles({ projectRoot: tempDir, swaggerUrl: swaggerPath, swaggerTimeoutMs: 1000, outputDir, requestImport: '../request', paramStyle: 'object', modules: [], cleanOutput: true, }) const indexContent = await readFile(path.join(outputDir, 'index.js')) assert.match(indexContent, /export \* as alphaApi from ["']\.\/alpha["']/) assert.match(indexContent, /export \* as betaApi from ["']\.\/beta["']/) assert.match(indexContent, /export \* from ["']\.\/alpha["']/) assert.match(indexContent, /export \* from ["']\.\/beta["']/) } finally { await fs.rm(tempDir, { recursive: true, force: true }) } }) test('generation can skip generated index.js when generateIndexFile is false', async () => { const tempDir = await createTempDir() try { const swaggerPath = path.join(tempDir, 'swagger.json') const outputDir = path.join(tempDir, 'generated') await fs.mkdir(outputDir, { recursive: true }) await fs.writeFile( path.join(outputDir, 'index.js'), `// Auto-generated. Do not edit manually. // Swagger: old export * from './old' `, 'utf8', ) await writeJson(swaggerPath, { openapi: '3.0.0', paths: { '/api/v1/Alpha/GetThing': { get: { tags: ['Alpha'], responses: { 200: { description: 'OK' }, }, }, }, }, }) await generateApiFiles({ projectRoot: tempDir, swaggerUrl: swaggerPath, swaggerTimeoutMs: 1000, outputDir, generateIndexFile: false, requestImport: '../request', paramStyle: 'object', modules: [], cleanOutput: true, }) await assert.doesNotReject(fs.access(path.join(outputDir, 'alpha.js'))) await assert.rejects(fs.access(path.join(outputDir, 'index.js'))) } finally { await fs.rm(tempDir, { recursive: true, force: true }) } }) test('generation allows duplicate module export names when generateIndexFile is false', async () => { const tempDir = await createTempDir() try { const swaggerPath = path.join(tempDir, 'swagger.json') const outputDir = path.join(tempDir, 'generated') await writeJson(swaggerPath, { openapi: '3.0.0', paths: { '/api/v1/Alpha/GetList': { get: { tags: ['Alpha'], responses: { 200: { description: 'OK' }, }, }, }, '/api/v1/Beta/GetList': { get: { tags: ['Beta'], responses: { 200: { description: 'OK' }, }, }, }, }, }) await assert.doesNotReject( generateApiFiles({ projectRoot: tempDir, swaggerUrl: swaggerPath, swaggerTimeoutMs: 1000, outputDir, generateIndexFile: false, requestImport: '../request', paramStyle: 'object', modules: [], cleanOutput: true, }), ) const alphaContent = await readFile(path.join(outputDir, 'alpha.js')) const betaContent = await readFile(path.join(outputDir, 'beta.js')) assert.match(alphaContent, /const getListApi =/) assert.match(betaContent, /const getListApi =/) await assert.rejects(fs.access(path.join(outputDir, 'index.js'))) } finally { await fs.rm(tempDir, { recursive: true, force: true }) } }) test('generation can rename duplicate exports by appending module names', async () => { const tempDir = await createTempDir() try { const swaggerPath = path.join(tempDir, 'swagger.json') const outputDir = path.join(tempDir, 'generated') await writeJson(swaggerPath, { openapi: '3.0.0', paths: { '/api/v1/Alpha/GetList': { get: { tags: ['Alpha'], responses: { 200: { description: 'OK' }, }, }, }, '/api/v1/Beta/GetList': { get: { tags: ['Beta'], responses: { 200: { description: 'OK' }, }, }, }, }, }) await generateApiFiles({ projectRoot: tempDir, swaggerUrl: swaggerPath, swaggerTimeoutMs: 1000, outputDir, requestImport: '../request', paramStyle: 'object', modules: [], cleanOutput: true, resolveDuplicateExports: async ({ conflicts }) => { assert.equal(conflicts.length, 1) assert.equal(conflicts[0].exportName, 'getListApi') return { action: 'rename' } }, }) const alphaContent = await readFile(path.join(outputDir, 'alpha.js')) const betaContent = await readFile(path.join(outputDir, 'beta.js')) const indexContent = await readFile(path.join(outputDir, 'index.js')) assert.match(alphaContent, /const getListAlphaApi =/) assert.match(betaContent, /const getListBetaApi =/) assert.match(indexContent, /export \* from ["']\.\/alpha["']/) assert.match(indexContent, /export \* from ["']\.\/beta["']/) } finally { await fs.rm(tempDir, { recursive: true, force: true }) } }) test('generation automatically avoids collisions with module namespace export names', async () => { const tempDir = await createTempDir() try { const swaggerPath = path.join(tempDir, 'swagger.json') const outputDir = path.join(tempDir, 'generated') await writeJson(swaggerPath, { openapi: '3.0.0', paths: { '/api/Values': { get: { tags: ['Values'], responses: { 200: { description: 'OK' }, }, }, }, }, }) await generateApiFiles({ projectRoot: tempDir, swaggerUrl: swaggerPath, swaggerTimeoutMs: 1000, outputDir, requestImport: '../request', paramStyle: 'object', modules: [], cleanOutput: true, }) const valuesContent = await readFile(path.join(outputDir, 'values.js')) const indexContent = await readFile(path.join(outputDir, 'index.js')) assert.match(valuesContent, /const getValuesApi =/) assert.match(indexContent, /export \* as valuesApi from ["']\.\/values["']/) assert.match(indexContent, /export \* from ["']\.\/values["']/) } finally { await fs.rm(tempDir, { recursive: true, force: true }) } }) test('response jsdoc expands nested array object fields', async () => { const tempDir = await createTempDir() try { const swaggerPath = path.join(tempDir, 'swagger.json') const outputDir = path.join(tempDir, 'generated') await writeJson(swaggerPath, { openapi: '3.0.0', paths: { '/api/v1/Alpha/GetRanking': { get: { tags: ['Alpha'], responses: { 200: { description: 'OK', content: { 'application/json': { schema: { $ref: '#/components/schemas/NoteSelectRankingResultPageResponse', }, }, }, }, }, }, }, }, components: { schemas: { NoteSelectRankingResultPageResponse: { type: 'object', properties: { total: { type: 'integer', }, items: { type: 'array', items: { $ref: '#/components/schemas/NoteSelectRankingResultItem', }, }, }, }, NoteSelectRankingResultItem: { type: 'object', properties: { noteId: { type: 'integer', }, author: { $ref: '#/components/schemas/NoteAuthor', }, }, }, NoteAuthor: { type: 'object', properties: { nickName: { type: 'string', }, }, }, }, }, }) await generateApiFiles({ projectRoot: tempDir, swaggerUrl: swaggerPath, swaggerTimeoutMs: 1000, outputDir, requestImport: '../request', paramStyle: 'object', modules: [], cleanOutput: true, }) const alphaContent = await readFile(path.join(outputDir, 'alpha.js')) assert.match( alphaContent, /@returns\s+\{Promise<\{\s*total\?: number,\s*items\?: Array<\{\s*noteId\?: number,\s*author\?: \{\s*nickName\?: string\s*\}\s*\}>\s*\}>\}\s+Returns NoteSelectRankingResultPageResponse/s, ) assert.match( alphaContent, /- items\?: \{Array<\{\s*noteId\?: number,\s*author\?: \{\s*nickName\?: string\s*\}\s*\}>\}/s, ) assert.match(alphaContent, /- items\[\]\.noteId\?: \{number\}/) assert.match(alphaContent, /- items\[\]\.author\?: \{\s*nickName\?: string\s*\}/s) assert.match(alphaContent, /- items\[\]\.author\.nickName\?: \{string\}/) } finally { await fs.rm(tempDir, { recursive: true, force: true }) } }) test('partial generation fails when retained generated files still collide with namespace exports', async () => { const tempDir = await createTempDir() try { const swaggerPath = path.join(tempDir, 'swagger.json') const outputDir = path.join(tempDir, 'generated') await fs.mkdir(outputDir, { recursive: true }) await fs.writeFile( path.join(outputDir, 'values.js'), `// Auto-generated. Do not edit manually. // Module: Values const valuesApi = () => null export { valuesApi } `, 'utf8', ) await writeJson(swaggerPath, { openapi: '3.0.0', paths: { '/api/v1/Alpha/GetThing': { get: { tags: ['Alpha'], responses: { 200: { description: 'OK' }, }, }, }, }, }) await assert.rejects( generateApiFiles({ projectRoot: tempDir, swaggerUrl: swaggerPath, swaggerTimeoutMs: 1000, outputDir, requestImport: '../request', paramStyle: 'object', modules: ['Alpha'], cleanOutput: true, }), /namespace export/, ) } finally { await fs.rm(tempDir, { recursive: true, force: true }) } }) test('generation fails when two modules expose the same API function name', async () => { const tempDir = await createTempDir() try { const swaggerPath = path.join(tempDir, 'swagger.json') await writeJson(swaggerPath, { openapi: '3.0.0', paths: { '/api/v1/Alpha/GetList': { get: { tags: ['Alpha'], responses: { 200: { description: 'OK' }, }, }, }, '/api/v1/Beta/GetList': { get: { tags: ['Beta'], responses: { 200: { description: 'OK' }, }, }, }, }, }) await assert.rejects( generateApiFiles({ projectRoot: tempDir, swaggerUrl: swaggerPath, swaggerTimeoutMs: 1000, outputDir: path.join(tempDir, 'generated'), requestImport: '../request', paramStyle: 'object', modules: [], cleanOutput: true, }), /\/api\/v1\/Alpha\/GetList/, ) } finally { await fs.rm(tempDir, { recursive: true, force: true }) } }) test('generation fails if duplicate exports still conflict after rename', async () => { const tempDir = await createTempDir() try { const swaggerPath = path.join(tempDir, 'swagger.json') await writeJson(swaggerPath, { openapi: '3.0.0', paths: { '/api/v1/Alpha/GetList': { get: { tags: ['Alpha'], responses: { 200: { description: 'OK' }, }, }, }, '/api/v1/Alpha/GetListAlpha': { get: { tags: ['Alpha'], responses: { 200: { description: 'OK' }, }, }, }, '/api/v1/Beta/GetList': { get: { tags: ['Beta'], responses: { 200: { description: 'OK' }, }, }, }, }, }) await assert.rejects( generateApiFiles({ projectRoot: tempDir, swaggerUrl: swaggerPath, swaggerTimeoutMs: 1000, outputDir: path.join(tempDir, 'generated'), requestImport: '../request', paramStyle: 'object', modules: [], cleanOutput: true, resolveDuplicateExports: async () => { return { action: 'rename' } }, }), /Automatic rename still leaves duplicate exports/, ) } finally { await fs.rm(tempDir, { recursive: true, force: true }) } }) test('swagger timeout surfaces a clear error message', async () => { const tempDir = await createTempDir() const server = http.createServer((_, response) => { setTimeout(() => { response.writeHead(200, { 'Content-Type': 'application/json' }) response.end(JSON.stringify({ openapi: '3.0.0', paths: {} })) }, 100) }) try { await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)) const { port } = server.address() await assert.rejects( generateApiFiles({ projectRoot: tempDir, swaggerUrl: `http://127.0.0.1:${port}/swagger.json`, swaggerTimeoutMs: 10, outputDir: path.join(tempDir, 'generated'), requestImport: '../request', paramStyle: 'object', modules: [], cleanOutput: true, }), /timed out/, ) } finally { server.close() await fs.rm(tempDir, { recursive: true, force: true }) } })