diff --git a/src/send.ts b/src/send.ts index 3947298..4d30a86 100644 --- a/src/send.ts +++ b/src/send.ts @@ -79,7 +79,7 @@ export async function send( encodingExt = '.gz'; } - if (extensions && !path.basename(filePath).includes('.')) { + if (extensions && !(await isPathExists(filePath))) { for (let ext of extensions) { if (typeof ext !== 'string') throw new TypeError( diff --git a/src/send.utils.ts b/src/send.utils.ts index 1145996..8fe48eb 100644 --- a/src/send.utils.ts +++ b/src/send.utils.ts @@ -15,8 +15,9 @@ export async function isPathExists(targetPath: string) { try { await asyncFs.access(targetPath); return true; - } catch { - return false; + } catch (err) { + if ((err as {code?: string}).code === 'ENOENT') return false; + throw err; } } diff --git a/test/fixtures/hello.world.txt b/test/fixtures/hello.world.txt new file mode 100644 index 0000000..cc628cc --- /dev/null +++ b/test/fixtures/hello.world.txt @@ -0,0 +1 @@ +world diff --git a/test/index.test.ts b/test/index.test.ts index 60097af..482bc5b 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,3 +1,4 @@ +import asyncFs from 'node:fs/promises'; import path from 'node:path'; import Koa from 'koa'; @@ -5,6 +6,27 @@ import request from 'supertest'; import { decompress } from 'brotli'; import { send } from '../src'; +import { isPathExists } from '../src/send.utils'; + +describe('isPathExists(targetPath)', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('returns false when access reports ENOENT', async () => { + const error = Object.assign(new Error('missing'), { code: 'ENOENT' }); + jest.spyOn(asyncFs, 'access').mockRejectedValueOnce(error); + + await expect(isPathExists('missing')).resolves.toBe(false); + }); + + it('rethrows non-ENOENT access errors', async () => { + const error = Object.assign(new Error('denied'), { code: 'EACCES' }); + jest.spyOn(asyncFs, 'access').mockRejectedValueOnce(error); + + await expect(isPathExists('denied')).rejects.toBe(error); + }); +}); describe('send(ctx, file)', () => { let server: ReturnType['listen']>; @@ -690,6 +712,21 @@ describe('send(ctx, file)', () => { await request(server).get('/').expect(200); }); }); + + describe('when trying to get a file without extension with matching .extensions suffixed with a dot in the basename', () => { + it('should 200', async () => { + const app = new Koa(); + + app.use(async (ctx) => { + await send(ctx, 'test/fixtures/hello.world', { + extensions: ['txt'], + }); + }); + + server = app.listen(); + await request(server).get('/').expect(200).expect('world\n'); + }); + }); }); it('should set the Content-Type', async () => {