1211 lines
32 KiB
JavaScript
1211 lines
32 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('generated module file includes module description from swagger tags', 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',
|
|
tags: [
|
|
{
|
|
name: 'Ranking',
|
|
description: '排行榜',
|
|
},
|
|
],
|
|
paths: {
|
|
'/api/v1/Ranking/GetList': {
|
|
get: {
|
|
responses: {
|
|
200: { description: 'OK' },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
await generateApiFiles({
|
|
projectRoot: tempDir,
|
|
swaggerUrl: swaggerPath,
|
|
swaggerTimeoutMs: 1000,
|
|
outputDir,
|
|
requestImport: '../request',
|
|
paramStyle: 'object',
|
|
modules: [],
|
|
cleanOutput: true,
|
|
})
|
|
|
|
const rankingContent = await readFile(path.join(outputDir, 'ranking.js'))
|
|
|
|
assert.match(rankingContent, /\/\/ Module: Ranking/)
|
|
assert.match(rankingContent, /\/\/ Module description: 排行榜/)
|
|
} finally {
|
|
await fs.rm(tempDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test('generated module file omits module description when swagger tags have no description', 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',
|
|
tags: [
|
|
{
|
|
name: 'Ranking',
|
|
},
|
|
],
|
|
paths: {
|
|
'/api/v1/Ranking/GetList': {
|
|
get: {
|
|
tags: ['Ranking'],
|
|
responses: {
|
|
200: { description: 'OK' },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
await generateApiFiles({
|
|
projectRoot: tempDir,
|
|
swaggerUrl: swaggerPath,
|
|
swaggerTimeoutMs: 1000,
|
|
outputDir,
|
|
requestImport: '../request',
|
|
paramStyle: 'object',
|
|
modules: [],
|
|
cleanOutput: true,
|
|
})
|
|
|
|
const rankingContent = await readFile(path.join(outputDir, 'ranking.js'))
|
|
|
|
assert.match(rankingContent, /\/\/ Module: Ranking/)
|
|
assert.doesNotMatch(rankingContent, /\/\/ Module description:/)
|
|
} 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('file download responses generate blob request config', 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/File/Export': {
|
|
get: {
|
|
tags: ['File'],
|
|
parameters: [
|
|
{
|
|
name: 'id',
|
|
in: 'query',
|
|
schema: {
|
|
type: 'integer',
|
|
},
|
|
},
|
|
],
|
|
responses: {
|
|
200: {
|
|
description: 'OK',
|
|
content: {
|
|
'application/octet-stream': {
|
|
schema: {
|
|
type: 'string',
|
|
format: 'binary',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
'/api/v1/File/CreateExport': {
|
|
post: {
|
|
tags: ['File'],
|
|
requestBody: {
|
|
content: {
|
|
'application/json': {
|
|
schema: {
|
|
type: 'object',
|
|
properties: {
|
|
name: {
|
|
type: 'string',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
responses: {
|
|
200: {
|
|
description: 'OK',
|
|
content: {
|
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': {},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
await generateApiFiles({
|
|
projectRoot: tempDir,
|
|
swaggerUrl: swaggerPath,
|
|
swaggerTimeoutMs: 1000,
|
|
outputDir,
|
|
requestImport: '../request',
|
|
paramStyle: 'object',
|
|
modules: [],
|
|
cleanOutput: true,
|
|
})
|
|
|
|
const fileContent = await readFile(path.join(outputDir, 'file.js'))
|
|
|
|
assert.match(fileContent, /@returns\s+\{Promise<Blob>\}/)
|
|
assert.match(
|
|
fileContent,
|
|
/const exportApi = \(params = \{\}\) =>\s+request\.get\(buildUrl\(`\/api\/v1\/File\/Export`, params\), \{\s*responseType: ["']blob["'],?\s*\}\)/s,
|
|
)
|
|
assert.match(
|
|
fileContent,
|
|
/const createExportApi = \(data\) =>\s+request\.post\(`\/api\/v1\/File\/CreateExport`, data, \{\s*responseType: ["']blob["'],?\s*\}\)/s,
|
|
)
|
|
} finally {
|
|
await fs.rm(tempDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test('untyped export endpoints infer blob request config from operation metadata', 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/Report/GetUserPointRecordFile': {
|
|
get: {
|
|
tags: ['Report'],
|
|
summary: '导出积分排行榜',
|
|
responses: {
|
|
200: {
|
|
description: 'OK',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
'/api/v1/Report/GetExportJob': {
|
|
get: {
|
|
tags: ['Report'],
|
|
summary: '获取导出任务状态',
|
|
responses: {
|
|
200: {
|
|
description: 'OK',
|
|
content: {
|
|
'application/json': {
|
|
schema: {
|
|
type: 'object',
|
|
properties: {
|
|
status: {
|
|
type: 'string',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
await generateApiFiles({
|
|
projectRoot: tempDir,
|
|
swaggerUrl: swaggerPath,
|
|
swaggerTimeoutMs: 1000,
|
|
outputDir,
|
|
requestImport: '../request',
|
|
paramStyle: 'object',
|
|
modules: [],
|
|
cleanOutput: true,
|
|
})
|
|
|
|
const reportContent = await readFile(path.join(outputDir, 'report.js'))
|
|
|
|
assert.match(
|
|
reportContent,
|
|
/const getUserPointRecordFileApi = \(\) =>\s+request\.get\(`\/api\/v1\/Report\/GetUserPointRecordFile`, \{\s*responseType: ["']blob["'],?\s*\}\)/s,
|
|
)
|
|
assert.match(
|
|
reportContent,
|
|
/const getExportJobApi = \(\) =>\s+request\.get\(`\/api\/v1\/Report\/GetExportJob`\)/s,
|
|
)
|
|
assert.doesNotMatch(
|
|
reportContent,
|
|
/request\.get\(`\/api\/v1\/Report\/GetExportJob`, \{\s*responseType: ["']blob["']/s,
|
|
)
|
|
} 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,
|
|
)
|
|
assert.match(alphaContent, /Response schema: NoteSelectRankingResultPageResponse/)
|
|
assert.match(alphaContent, /\*\s*\n \* Response fields:/)
|
|
assert.match(alphaContent, /- items\?: \{Array<Object>\}/)
|
|
assert.match(alphaContent, / - items\[\]\.noteId\?: \{number\}/)
|
|
assert.match(alphaContent, / - items\[\]\.author\?: \{Object\}/)
|
|
assert.match(alphaContent, / - items\[\]\.author\.nickName\?: \{string\}/)
|
|
} finally {
|
|
await fs.rm(tempDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test('request body 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/Create': {
|
|
post: {
|
|
tags: ['Alpha'],
|
|
requestBody: {
|
|
content: {
|
|
'application/json': {
|
|
schema: {
|
|
$ref: '#/components/schemas/CreatePayload',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
responses: {
|
|
200: { description: 'OK' },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
components: {
|
|
schemas: {
|
|
CreatePayload: {
|
|
type: 'object',
|
|
properties: {
|
|
title: {
|
|
type: 'string',
|
|
},
|
|
items: {
|
|
type: 'array',
|
|
items: {
|
|
$ref: '#/components/schemas/CreateItem',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
CreateItem: {
|
|
type: 'object',
|
|
properties: {
|
|
id: {
|
|
type: 'integer',
|
|
},
|
|
author: {
|
|
$ref: '#/components/schemas/Author',
|
|
},
|
|
},
|
|
},
|
|
Author: {
|
|
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,
|
|
/@param\s+\{\{\s*title\?: string,\s*items\?: Array<\{\s*id\?: number,\s*author\?: \{\s*nickName\?: string\s*\}\s*\}>\s*\}\}\s+data Request body/s,
|
|
)
|
|
assert.match(alphaContent, /Request body schema: CreatePayload/)
|
|
assert.match(alphaContent, /\*\s*\n \* Request body fields:/)
|
|
assert.match(alphaContent, /- data\.title\?: \{string\}/)
|
|
assert.match(alphaContent, /- data\.items\?: \{Array<Object>\}/)
|
|
assert.match(alphaContent, / - data\.items\[\]\.id\?: \{number\}/)
|
|
assert.match(alphaContent, / - data\.items\[\]\.author\?: \{Object\}/)
|
|
assert.match(alphaContent, / - data\.items\[\]\.author\.nickName\?: \{string\}/)
|
|
} finally {
|
|
await fs.rm(tempDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test('response jsdoc handles circular schemas without recursion errors', 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/Tree/GetNode': {
|
|
get: {
|
|
tags: ['Tree'],
|
|
responses: {
|
|
200: {
|
|
description: 'OK',
|
|
content: {
|
|
'application/json': {
|
|
schema: {
|
|
$ref: '#/components/schemas/TreeNode',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
components: {
|
|
schemas: {
|
|
TreeNode: {
|
|
type: 'object',
|
|
properties: {
|
|
id: {
|
|
type: 'integer',
|
|
},
|
|
children: {
|
|
type: 'array',
|
|
items: {
|
|
$ref: '#/components/schemas/TreeNode',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
await assert.doesNotReject(
|
|
generateApiFiles({
|
|
projectRoot: tempDir,
|
|
swaggerUrl: swaggerPath,
|
|
swaggerTimeoutMs: 1000,
|
|
outputDir,
|
|
requestImport: '../request',
|
|
paramStyle: 'object',
|
|
modules: [],
|
|
cleanOutput: true,
|
|
}),
|
|
)
|
|
|
|
const treeContent = await readFile(path.join(outputDir, 'tree.js'))
|
|
|
|
assert.match(treeContent, /@returns\s+\{Promise<\{\s*id\?: number,\s*children\?: Array<Object>\s*\}>\}/s)
|
|
assert.match(treeContent, /- children\?: \{Array<Object>\}/)
|
|
assert.doesNotMatch(treeContent, /children\[\]\./)
|
|
} finally {
|
|
await fs.rm(tempDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test('request body jsdoc handles circular schemas without recursion errors', 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/Tree/CreateNode': {
|
|
post: {
|
|
tags: ['Tree'],
|
|
requestBody: {
|
|
content: {
|
|
'application/json': {
|
|
schema: {
|
|
$ref: '#/components/schemas/TreeNode',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
responses: {
|
|
200: { description: 'OK' },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
components: {
|
|
schemas: {
|
|
TreeNode: {
|
|
type: 'object',
|
|
properties: {
|
|
id: {
|
|
type: 'integer',
|
|
},
|
|
children: {
|
|
type: 'array',
|
|
items: {
|
|
$ref: '#/components/schemas/TreeNode',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
await assert.doesNotReject(
|
|
generateApiFiles({
|
|
projectRoot: tempDir,
|
|
swaggerUrl: swaggerPath,
|
|
swaggerTimeoutMs: 1000,
|
|
outputDir,
|
|
requestImport: '../request',
|
|
paramStyle: 'object',
|
|
modules: [],
|
|
cleanOutput: true,
|
|
}),
|
|
)
|
|
|
|
const treeContent = await readFile(path.join(outputDir, 'tree.js'))
|
|
|
|
assert.match(
|
|
treeContent,
|
|
/@param\s+\{\{\s*id\?: number,\s*children\?: Array<Object>\s*\}\}\s+data Request body/s,
|
|
)
|
|
assert.match(treeContent, /Request body schema: TreeNode/)
|
|
assert.match(treeContent, /- data\.children\?: \{Array<Object>\}/)
|
|
assert.doesNotMatch(treeContent, /data\.children\[\]\./)
|
|
} 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 })
|
|
}
|
|
})
|