Files
carmanagement/apps/api/src/middleware/requireApiKey.test.ts
T
2026-06-10 00:40:19 -04:00

160 lines
5.1 KiB
TypeScript

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()
})
})