/** * Unit Tests — middleware/api-key-auth.js */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; const { extractApiKey } = require('../../middleware/api-key-auth'); describe('api-key-auth', () => { function mockReq(headers = {}, query = {}) { return { headers, query }; } describe('extractApiKey', () => { it('should extract from Authorization Bearer header', () => { const req = mockReq({ authorization: 'Bearer my-secret-key' }); expect(extractApiKey(req)).toBe('my-secret-key'); }); it('should extract from X-API-Key header', () => { const req = mockReq({ 'x-api-key': 'another-key' }); expect(extractApiKey(req)).toBe('another-key'); }); it('should extract from query param', () => { const req = mockReq({}, { api_key: 'query-key' }); expect(extractApiKey(req)).toBe('query-key'); }); it('should prefer Bearer over X-API-Key', () => { const req = mockReq({ authorization: 'Bearer bearer-key', 'x-api-key': 'x-key', }); expect(extractApiKey(req)).toBe('bearer-key'); }); it('should return null when no key provided', () => { const req = mockReq(); expect(extractApiKey(req)).toBeNull(); }); it('should trim whitespace', () => { const req = mockReq({ authorization: 'Bearer spaced-key ' }); expect(extractApiKey(req)).toBe('spaced-key'); }); it('should ignore non-Bearer auth headers', () => { const req = mockReq({ authorization: 'Basic abc123' }); expect(extractApiKey(req)).toBeNull(); }); }); }); /** * requireApiKey —— **做鉴权决定的那一半**。 * * 上面那组 7 条全在测 extractApiKey(怎么从 header/query 里取出字符串), * 而真正决定放行还是 401 的 requireApiKey 此前**一条用例都没有**。 * 后果不是假设:`OPEN_API_KEYS` 那条分支从来没被执行过,于是 * integration 测试里把变量名写成 `API_KEYS`(生产读的是 `OPEN_API_KEYS`) * 存活了很久也没人发现 —— 没有测试执行过的路径,喂给它什么名字都不会有人知道。 * * 四条分支各一条,覆盖「拦」和「放」两侧: * 无 key → 401 / key 无效 → 401 / 命中系统 key 列表 → 放行 / 命中用户 key → 放行且挂 req.user * * 隔离两处,缺任一条这组用例就会打到真实环境: * - `OPEN_API_KEYS`:config.js 在模块加载时求值,所以要先设 env 再清 require.cache * (config 与本中间件都要清 —— 中间件在顶层 require config) * - `USERS_DB_PATH`:requireApiKey 内部**惰性** require user-db 来查用户 key。 * 这个覆盖口是刚补上的;在它存在之前,这组用例会往**共享的生产用户库**里 * 插一个测试用户(那个库正是这么攒到 356 行夹具的)。 */ describe('requireApiKey — 鉴权判定(此前零覆盖)', () => { const path = require('path'); const fs = require('fs'); const SYSTEM_KEY = 'sk-system-test-key'; const TEST_DB_PATH = `/tmp/test-apikey-users-${process.pid}.sqlite`; const DB_FILES = [TEST_DB_PATH, `${TEST_DB_PATH}-shm`, `${TEST_DB_PATH}-wal`]; const MODULES = ['../../config', '../../middleware/api-key-auth', '../../services/user-db']; let requireApiKey; let userKey; let prevOpenApiKeys; beforeAll(() => { for (const f of DB_FILES) { try { fs.unlinkSync(f); } catch {} } prevOpenApiKeys = process.env.OPEN_API_KEYS; process.env.OPEN_API_KEYS = `${SYSTEM_KEY}, sk-another-one`; // 带空格:config 会 trim process.env.USERS_DB_PATH = TEST_DB_PATH; for (const m of MODULES) delete require.cache[require.resolve(m)]; const userDb = require('../../services/user-db'); const user = userDb.createUser({ email: `apikey-${process.pid}@example.com`, passwordHash: 'x', displayName: 'API Key User', provider: 'local', }); userKey = user.api_key; // createUser 自己生成 sk-user- expect(userKey).toMatch(/^sk-user-/); ({ requireApiKey } = require('../../middleware/api-key-auth')); }); afterAll(() => { for (const f of DB_FILES) { try { fs.unlinkSync(f); } catch {} } if (prevOpenApiKeys === undefined) delete process.env.OPEN_API_KEYS; else process.env.OPEN_API_KEYS = prevOpenApiKeys; delete process.env.USERS_DB_PATH; for (const m of MODULES) delete require.cache[require.resolve(m)]; }); /** 只记录结果,不真起 express */ function run(headers) { const req = { headers, query: {} }; const res = { statusCode: null, body: null, status(code) { this.statusCode = code; return this; }, json(payload) { this.body = payload; return this; }, }; let passed = false; requireApiKey(req, res, () => { passed = true; }); return { req, res, passed }; } it('隔离生效:用的是临时用户库,不是共享的生产库', () => { expect(fs.existsSync(TEST_DB_PATH)).toBe(true); }); it('① 完全不带 key → 401「需要 key」,且不调 next', () => { const { res, passed } = run({}); expect(passed).toBe(false); expect(res.statusCode).toBe(401); expect(res.body.error).toBe('Unauthorized'); // 必须断言到**文案**而不是只断言 401:去掉这道门之后,key=null 会落到 // 下面那道「key 无效」的门,照样回 401 —— 只看状态码的话这个变异测不出来 // (实测过:把 if (!key) 改成 if (false),只断言 401 的版本全绿)。 // 而两者要分得开:一个是「你忘带 key」,一个是「你的 key 不对」,排查时全靠它。 expect(res.body.message).toMatch(/API Key is required/); }); it('② key 既不在系统列表也不属于任何用户 → 401', () => { const { res, passed } = run({ 'x-api-key': 'sk-not-a-real-key' }); expect(passed).toBe(false); expect(res.statusCode).toBe(401); // 与「没带 key」是两种不同的失败,文案要分得开(排查时全靠这个) expect(res.body.message).toMatch(/Invalid API Key/); }); it('③ 命中 OPEN_API_KEYS 系统列表 → 放行,挂 req.apiKey,不挂 req.user', () => { // 这条就是那条从来没被执行过的分支:它存在的话,把变量名写成 API_KEYS 会当场红 const { req, res, passed } = run({ authorization: `Bearer ${SYSTEM_KEY}` }); expect(passed).toBe(true); expect(res.statusCode).toBeNull(); expect(req.apiKey).toBe(SYSTEM_KEY); expect(req.user).toBeUndefined(); // 系统 key 不代表任何用户,不该凭空造一个身份 }); it('④ 命中库里某个用户的 api_key → 放行并把该用户挂上 req.user', () => { const { req, passed } = run({ 'x-api-key': userKey }); expect(passed).toBe(true); expect(req.apiKey).toBe(userKey); expect(req.user).toBeTruthy(); expect(req.user.email).toBe(`apikey-${process.pid}@example.com`); // 走的是 toPublicUser,密码哈希不该跟着挂到请求上 expect(req.user).not.toHaveProperty('password_hash'); }); });