fix architecture and write new tests
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user