160 lines
5.1 KiB
TypeScript
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()
|
|
})
|
|
})
|