yx_generate_api_js/test/generate.test.js

611 lines
16 KiB
JavaScript

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('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 })
}
})