import { beforeEach, describe, expect, it, vi } from 'vitest' import type { NextFunction, Request, Response } from 'express' import { generateCompanyApiKey } from '../security/apiKeys' vi.mock('../lib/prisma', () => ({ prisma: { companyApiKey: { findUnique: vi.fn(), update: vi.fn(), }, }, })) import { prisma } from '../lib/prisma' import { requireApiKey } from './requireApiKey' function createResponseStub() { const res = { status: vi.fn(), json: vi.fn(), } res.status.mockReturnValue(res) res.json.mockReturnValue(res) return res as unknown as Response & typeof res } describe('requireApiKey middleware', () => { beforeEach(() => { vi.clearAllMocks() }) it('rejects requests with no x-api-key header', async () => { const req = { headers: {} } as Request const res = createResponseStub() const next = vi.fn() as NextFunction await requireApiKey(req, res, next) expect(res.status).toHaveBeenCalledWith(401) expect(res.json).toHaveBeenCalledWith({ error: 'missing_api_key', message: 'API key required in x-api-key header', statusCode: 401, }) expect((prisma as any).companyApiKey.findUnique).not.toHaveBeenCalled() expect(next).not.toHaveBeenCalled() }) it('rejects malformed API keys before database lookup', async () => { const req = { headers: { 'x-api-key': 'bad-key' } } as unknown as Request const res = createResponseStub() const next = vi.fn() as NextFunction await requireApiKey(req, res, next) expect((prisma as any).companyApiKey.findUnique).not.toHaveBeenCalled() expect(res.status).toHaveBeenCalledWith(401) expect(res.json).toHaveBeenCalledWith({ error: 'invalid_api_key', message: 'Invalid API key', statusCode: 401, }) expect(next).not.toHaveBeenCalled() }) it('rejects unknown API key prefixes', async () => { const generated = generateCompanyApiKey() vi.mocked((prisma as any).companyApiKey.findUnique).mockResolvedValue(null) const req = { headers: { 'x-api-key': generated.rawKey } } as unknown as Request const res = createResponseStub() const next = vi.fn() as NextFunction await requireApiKey(req, res, next) expect((prisma as any).companyApiKey.findUnique).toHaveBeenCalledWith({ where: { prefix: generated.prefix }, include: { company: true }, }) expect(res.status).toHaveBeenCalledWith(401) expect(res.json).toHaveBeenCalledWith({ error: 'invalid_api_key', message: 'Invalid API key', statusCode: 401, }) expect(next).not.toHaveBeenCalled() }) it('rejects revoked API keys', async () => { const generated = generateCompanyApiKey() vi.mocked((prisma as any).companyApiKey.findUnique).mockResolvedValue({ id: 'key_1', companyId: 'company_1', prefix: generated.prefix, keyHash: generated.keyHash, revokedAt: new Date('2026-06-01T00:00:00.000Z'), company: { id: 'company_1', name: 'Atlas Cars' }, }) const req = { headers: { 'x-api-key': generated.rawKey } } as unknown as Request const res = createResponseStub() const next = vi.fn() as NextFunction await requireApiKey(req, res, next) expect(res.status).toHaveBeenCalledWith(401) expect(next).not.toHaveBeenCalled() }) it('rejects API keys whose secret does not match the stored hash', async () => { const generated = generateCompanyApiKey() const other = generateCompanyApiKey() vi.mocked((prisma as any).companyApiKey.findUnique).mockResolvedValue({ id: 'key_1', companyId: 'company_1', prefix: generated.prefix, keyHash: other.keyHash, revokedAt: null, company: { id: 'company_1', name: 'Atlas Cars' }, }) const req = { headers: { 'x-api-key': generated.rawKey } } as unknown as Request const res = createResponseStub() const next = vi.fn() as NextFunction await requireApiKey(req, res, next) expect(res.status).toHaveBeenCalledWith(401) expect(next).not.toHaveBeenCalled() }) it('attaches company context, updates lastUsedAt, and calls next for valid API keys', async () => { const generated = generateCompanyApiKey() const company = { id: 'company_1', name: 'Atlas Cars' } vi.mocked((prisma as any).companyApiKey.findUnique).mockResolvedValue({ id: 'key_1', companyId: company.id, prefix: generated.prefix, keyHash: generated.keyHash, revokedAt: null, company, }) vi.mocked((prisma as any).companyApiKey.update).mockResolvedValue({}) const req = { headers: { 'x-api-key': generated.rawKey } } as unknown as Request const res = createResponseStub() const next = vi.fn() as NextFunction await requireApiKey(req, res, next) expect((prisma as any).companyApiKey.update).toHaveBeenCalledWith({ where: { id: 'key_1' }, data: { lastUsedAt: expect.any(Date) }, }) expect(req.company).toEqual(company) expect(req.companyId).toBe('company_1') expect(next).toHaveBeenCalledTimes(1) expect(res.status).not.toHaveBeenCalled() expect(res.json).not.toHaveBeenCalled() }) })