From 0aaea7557f8d53f7a9e291e67aada6768d09d06d Mon Sep 17 00:00:00 2001 From: hikali123456789 <271072326@qq.com> Date: Sun, 24 May 2026 22:46:58 +0800 Subject: [PATCH] fix: handle multi-dot filenames in fileExists extension check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, fileExists() used name.split('.')[1] to extract the file extension, which failed for multi-dot filenames like 'my.asyncapi.yaml' — retrieving 'asyncapi' instead of 'yaml'. This caused valid spec files with multiple dots in their names to be rejected as having unsupported extensions. Fix: use split().pop() to get the last segment, correctly identifying the actual file extension for any number of dots. --- src/domains/models/SpecificationFile.ts | 5 +- test/unit/models/spec-file.test.ts | 86 +++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 test/unit/models/spec-file.test.ts diff --git a/src/domains/models/SpecificationFile.ts b/src/domains/models/SpecificationFile.ts index 5370f6e67..2fb3f5fdb 100644 --- a/src/domains/models/SpecificationFile.ts +++ b/src/domains/models/SpecificationFile.ts @@ -237,7 +237,10 @@ export async function fileExists(name: string): Promise { return true; } - const extension = name.split('.')[1]; + // Get the last segment after the last dot to handle multi-dot filenames + // e.g., 'my.asyncapi.yaml' should yield 'yaml', not 'asyncapi' + const splitParts = name.split('.'); + const extension = splitParts.length > 1 ? splitParts.pop() : ''; const allowedExtenstion = ['yml', 'yaml', 'json']; diff --git a/test/unit/models/spec-file.test.ts b/test/unit/models/spec-file.test.ts new file mode 100644 index 000000000..a7591357c --- /dev/null +++ b/test/unit/models/spec-file.test.ts @@ -0,0 +1,86 @@ +import { expect } from 'chai'; +import { promises as fs } from 'fs'; +import path from 'path'; +import os from 'os'; +import { fileExists } from '../../../src/domains/models/SpecificationFile'; + +describe('fileExists()', () => { + const tmpDir = path.join(os.tmpdir(), 'asyncapi-cli-test-' + Date.now()); + const files: string[] = []; + + before(async () => { + await fs.mkdir(tmpDir, { recursive: true }); + }); + + after(async () => { + for (const f of files) { + try { await fs.unlink(f); } catch { /* best effort */ } + } + try { await fs.rmdir(tmpDir); } catch { /* best effort */ } + }); + + function tmpFile(name: string): string { + const p = path.join(tmpDir, name); + files.push(p); + return p; + } + + it('returns true for a valid .yaml file', async () => { + const p = tmpFile('simple.yaml'); + await fs.writeFile(p, 'asyncapi: 3.0.0', 'utf8'); + expect(await fileExists(p)).to.equal(true); + }); + + it('returns true for a valid .yml file', async () => { + const p = tmpFile('simple.yml'); + await fs.writeFile(p, 'asyncapi: 3.0.0', 'utf8'); + expect(await fileExists(p)).to.equal(true); + }); + + it('returns true for a valid .json file', async () => { + const p = tmpFile('simple.json'); + await fs.writeFile(p, '{"asyncapi":"3.0.0"}', 'utf8'); + expect(await fileExists(p)).to.equal(true); + }); + + it('handles multi-dot filenames like my.asyncapi.yaml', async () => { + const p = tmpFile('my.asyncapi.yaml'); + await fs.writeFile(p, 'asyncapi: 3.0.0', 'utf8'); + expect(await fileExists(p)).to.equal(true); + }); + + it('handles multi-dot filenames like my.api.spec.yml', async () => { + const p = tmpFile('my.api.spec.yml'); + await fs.writeFile(p, 'asyncapi: 3.0.0', 'utf8'); + expect(await fileExists(p)).to.equal(true); + }); + + it('handles multi-dot filenames like api.v2.spec.json', async () => { + const p = tmpFile('api.v2.spec.json'); + await fs.writeFile(p, '{"asyncapi":"3.0.0"}', 'utf8'); + expect(await fileExists(p)).to.equal(true); + }); + + it('rejects files with unsupported extensions', async () => { + const p = tmpFile('readme.txt'); + await fs.writeFile(p, 'hello', 'utf8'); + // fileExists should throw an ErrorLoadingSpec for invalid files + try { + await fileExists(p); + expect.fail('expected throw for .txt file'); + } catch (e: unknown) { + expect((e as Error).message).to.match(/invalid|Unable|not a valid/i); + } + }); + + it('rejects files with only one segment (no extension)', async () => { + const p = tmpFile('noextension'); + await fs.writeFile(p, 'asyncapi: 3.0.0', 'utf8'); + try { + await fileExists(p); + expect.fail('expected throw for file with no extension'); + } catch (e: unknown) { + expect((e as Error).message).to.match(/invalid|Unable|not a valid/i); + } + }); +});