fix tests isues
This commit is contained in:
@@ -0,0 +1,342 @@
|
||||
diff -ru '--exclude=node_modules' car_project_original/apps/api/src/index.ts car_project_work/apps/api/src/index.ts
|
||||
--- car_project_original/apps/api/src/index.ts 2026-06-09 19:40:36.000000000 +0000
|
||||
+++ car_project_work/apps/api/src/index.ts 2026-06-09 23:18:30.521017321 +0000
|
||||
@@ -1,11 +1,11 @@
|
||||
import http from 'http'
|
||||
import { Server as SocketIOServer } from 'socket.io'
|
||||
import cron from 'node-cron'
|
||||
-import jwt from 'jsonwebtoken'
|
||||
import { redis } from './lib/redis'
|
||||
import { prisma } from './lib/prisma'
|
||||
import { assertStorageConfiguration } from './lib/storage'
|
||||
import { createApp, corsOrigins } from './app'
|
||||
+import { verifyAnyActorToken } from './security/tokens'
|
||||
import { sendNotification } from './services/notificationService'
|
||||
import {
|
||||
runTrialExpirationJob,
|
||||
@@ -29,7 +29,7 @@
|
||||
const token = socket.handshake.auth?.token as string | undefined
|
||||
if (!token) return next() // unauthenticated connections allowed; they just don't join rooms
|
||||
try {
|
||||
- const payload = jwt.verify(token, process.env.JWT_SECRET!) as { sub: string; type: string }
|
||||
+ const payload = verifyAnyActorToken(token)
|
||||
;(socket as any).authenticatedUserId = payload.sub
|
||||
next()
|
||||
} catch {
|
||||
diff -ru '--exclude=node_modules' car_project_original/apps/api/src/middleware/requireApiKey.test.ts car_project_work/apps/api/src/middleware/requireApiKey.test.ts
|
||||
--- car_project_original/apps/api/src/middleware/requireApiKey.test.ts 2026-06-09 19:40:36.000000000 +0000
|
||||
+++ car_project_work/apps/api/src/middleware/requireApiKey.test.ts 2026-06-09 23:18:42.795559504 +0000
|
||||
@@ -1,10 +1,12 @@
|
||||
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: {
|
||||
- company: {
|
||||
+ companyApiKey: {
|
||||
findUnique: vi.fn(),
|
||||
+ update: vi.fn(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
@@ -42,19 +44,18 @@
|
||||
message: 'API key required in x-api-key header',
|
||||
statusCode: 401,
|
||||
})
|
||||
- expect(prisma.company.findUnique).not.toHaveBeenCalled()
|
||||
+ expect((prisma as any).companyApiKey.findUnique).not.toHaveBeenCalled()
|
||||
expect(next).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
- it('rejects unknown API keys', async () => {
|
||||
- vi.mocked(prisma.company.findUnique).mockResolvedValue(null)
|
||||
+ 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.company.findUnique).toHaveBeenCalledWith({ where: { apiKey: 'bad-key' } })
|
||||
+ expect((prisma as any).companyApiKey.findUnique).not.toHaveBeenCalled()
|
||||
expect(res.status).toHaveBeenCalledWith(401)
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'invalid_api_key',
|
||||
@@ -64,15 +65,91 @@
|
||||
expect(next).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
- it('attaches company context and calls next for valid API keys', async () => {
|
||||
- const company = { id: 'company_1', apiKey: 'valid-key', name: 'Atlas Cars' }
|
||||
- vi.mocked(prisma.company.findUnique).mockResolvedValue(company as any)
|
||||
- const req = { headers: { 'x-api-key': 'valid-key' } } as unknown as Request
|
||||
+ 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)
|
||||
diff -ru '--exclude=node_modules' car_project_original/apps/api/src/middleware/requireApiKey.ts car_project_work/apps/api/src/middleware/requireApiKey.ts
|
||||
--- car_project_original/apps/api/src/middleware/requireApiKey.ts 2026-06-09 19:44:15.000000000 +0000
|
||||
+++ car_project_work/apps/api/src/middleware/requireApiKey.ts 2026-06-09 23:18:30.518795557 +0000
|
||||
@@ -21,14 +21,6 @@
|
||||
|
||||
const hashed = hashApiKey(apiKey)
|
||||
if (!keyRecord || keyRecord.revokedAt || !timingSafeEqualHex(hashed, keyRecord.keyHash)) {
|
||||
- if (process.env.ALLOW_LEGACY_COMPANY_API_KEYS === 'true') {
|
||||
- const company = await prisma.company.findUnique({ where: { apiKey } })
|
||||
- if (company) {
|
||||
- req.company = company
|
||||
- req.companyId = company.id
|
||||
- return next()
|
||||
- }
|
||||
- }
|
||||
return res.status(401).json({ error: 'invalid_api_key', message: 'Invalid API key', statusCode: 401 })
|
||||
}
|
||||
|
||||
diff -ru '--exclude=node_modules' car_project_original/apps/api/src/middleware/requireCompanyAuth.test.ts car_project_work/apps/api/src/middleware/requireCompanyAuth.test.ts
|
||||
--- car_project_original/apps/api/src/middleware/requireCompanyAuth.test.ts 2026-06-09 20:03:48.000000000 +0000
|
||||
+++ car_project_work/apps/api/src/middleware/requireCompanyAuth.test.ts 2026-06-09 23:18:50.351586123 +0000
|
||||
@@ -63,7 +63,7 @@
|
||||
await requireCompanyAuth(req, res, next)
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(401)
|
||||
- expect(res.json).toHaveBeenCalledWith({ error: 'invalid_token', message: 'Invalid token type for this endpoint', statusCode: 401 })
|
||||
+ expect(res.json).toHaveBeenCalledWith({ error: 'invalid_token', message: 'Invalid or expired session token', statusCode: 401 })
|
||||
expect(prisma.employee.findUnique).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -108,7 +108,11 @@
|
||||
|
||||
await requireCompanyDocumentAuth(req, res, next)
|
||||
|
||||
- expect(jwt.verify).toHaveBeenCalledWith('cookie-token', 'test-secret')
|
||||
+ expect(jwt.verify).toHaveBeenCalledWith('cookie-token', 'test-secret', {
|
||||
+ algorithms: ['HS256'],
|
||||
+ issuer: 'rentaldrivego-api',
|
||||
+ audience: 'employee',
|
||||
+ })
|
||||
expect(req.companyId).toBe('company_2')
|
||||
expect(next).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
diff -ru '--exclude=node_modules' car_project_original/apps/api/src/middleware/requireRenterAuth.test.ts car_project_work/apps/api/src/middleware/requireRenterAuth.test.ts
|
||||
--- car_project_original/apps/api/src/middleware/requireRenterAuth.test.ts 2026-06-09 19:40:36.000000000 +0000
|
||||
+++ car_project_work/apps/api/src/middleware/requireRenterAuth.test.ts 2026-06-09 23:18:50.352660214 +0000
|
||||
@@ -49,7 +49,7 @@
|
||||
await requireRenterAuth(req, res, next)
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(401)
|
||||
- expect(res.json).toHaveBeenCalledWith({ error: 'invalid_token', message: 'Invalid token type', statusCode: 401 })
|
||||
+ expect(res.json).toHaveBeenCalledWith({ error: 'invalid_token', message: 'Invalid or expired token', statusCode: 401 })
|
||||
expect(prisma.renter.findUnique).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
diff -ru '--exclude=node_modules' car_project_original/apps/api/src/modules/auth/auth.employee.service.ts car_project_work/apps/api/src/modules/auth/auth.employee.service.ts
|
||||
--- car_project_original/apps/api/src/modules/auth/auth.employee.service.ts 2026-06-09 19:42:41.000000000 +0000
|
||||
+++ car_project_work/apps/api/src/modules/auth/auth.employee.service.ts 2026-06-09 23:19:29.323222951 +0000
|
||||
@@ -41,13 +41,22 @@
|
||||
pwdv: getEmployeePasswordResetVersion(passwordHash),
|
||||
},
|
||||
process.env.JWT_SECRET!,
|
||||
- { expiresIn: `${RESET_TOKEN_TTL_MINUTES}m` },
|
||||
+ {
|
||||
+ algorithm: 'HS256',
|
||||
+ issuer: 'rentaldrivego-api',
|
||||
+ audience: 'employee_password_reset',
|
||||
+ expiresIn: `${RESET_TOKEN_TTL_MINUTES}m`,
|
||||
+ },
|
||||
)
|
||||
}
|
||||
|
||||
function verifyEmployeePasswordResetToken(token: string): EmployeePasswordResetPayload | null {
|
||||
try {
|
||||
- const payload = jwt.verify(token, process.env.JWT_SECRET!) as jwt.JwtPayload
|
||||
+ const payload = jwt.verify(token, process.env.JWT_SECRET!, {
|
||||
+ algorithms: ['HS256'],
|
||||
+ issuer: 'rentaldrivego-api',
|
||||
+ audience: 'employee_password_reset',
|
||||
+ }) as jwt.JwtPayload
|
||||
|
||||
if (
|
||||
typeof payload.sub !== 'string' ||
|
||||
diff -ru '--exclude=node_modules' car_project_original/apps/api/src/security/tokens.ts car_project_work/apps/api/src/security/tokens.ts
|
||||
--- car_project_original/apps/api/src/security/tokens.ts 2026-06-09 20:20:10.000000000 +0000
|
||||
+++ car_project_work/apps/api/src/security/tokens.ts 2026-06-09 23:18:30.519590128 +0000
|
||||
@@ -54,3 +54,14 @@
|
||||
|
||||
return payload as ActorTokenPayload
|
||||
}
|
||||
+
|
||||
+export function verifyAnyActorToken(token: string): ActorTokenPayload {
|
||||
+ const decoded = jwt.decode(token) as jwt.JwtPayload | null
|
||||
+ const actorType = decoded?.type
|
||||
+
|
||||
+ if (actorType !== 'admin' && actorType !== 'employee' && actorType !== 'renter') {
|
||||
+ throw new Error('Invalid actor token')
|
||||
+ }
|
||||
+
|
||||
+ return verifyActorToken(token, actorType)
|
||||
+}
|
||||
diff -ru '--exclude=node_modules' car_project_original/apps/api/src/tests/helpers/fixtures.ts car_project_work/apps/api/src/tests/helpers/fixtures.ts
|
||||
--- car_project_original/apps/api/src/tests/helpers/fixtures.ts 2026-06-09 19:40:36.000000000 +0000
|
||||
+++ car_project_work/apps/api/src/tests/helpers/fixtures.ts 2026-06-09 23:19:11.289635569 +0000
|
||||
@@ -1,8 +1,6 @@
|
||||
import bcrypt from 'bcryptjs'
|
||||
-import jwt from 'jsonwebtoken'
|
||||
import { prisma } from '../../lib/prisma'
|
||||
-
|
||||
-const JWT_SECRET = process.env.JWT_SECRET ?? 'test-secret'
|
||||
+import { signActorToken } from '../../security/tokens'
|
||||
|
||||
let counter = 0
|
||||
function uid() {
|
||||
@@ -215,28 +213,16 @@
|
||||
})
|
||||
}
|
||||
|
||||
-export function signEmployeeToken(employeeId: string, companyId: string, role: string = 'OWNER') {
|
||||
- return jwt.sign(
|
||||
- { sub: employeeId, companyId, role, type: 'employee' },
|
||||
- JWT_SECRET,
|
||||
- { expiresIn: '1h' },
|
||||
- )
|
||||
+export function signEmployeeToken(employeeId: string, _companyId: string, _role: string = 'OWNER') {
|
||||
+ return signActorToken(employeeId, 'employee', { expiresIn: '1h' })
|
||||
}
|
||||
|
||||
export function signAdminToken(adminId: string) {
|
||||
- return jwt.sign(
|
||||
- { sub: adminId, type: 'admin' },
|
||||
- JWT_SECRET,
|
||||
- { expiresIn: '1h' },
|
||||
- )
|
||||
+ return signActorToken(adminId, 'admin', { expiresIn: '1h', last2faAt: Date.now() })
|
||||
}
|
||||
|
||||
export function signRenterToken(renterId: string) {
|
||||
- return jwt.sign(
|
||||
- { sub: renterId, type: 'renter' },
|
||||
- JWT_SECRET,
|
||||
- { expiresIn: '1h' },
|
||||
- )
|
||||
+ return signActorToken(renterId, 'renter', { expiresIn: '1h' })
|
||||
}
|
||||
|
||||
export function authHeader(token: string) {
|
||||
Only in car_project_work/packages/database/prisma/migrations: 20260609233000_drop_legacy_company_api_key
|
||||
diff -ru '--exclude=node_modules' car_project_original/packages/database/prisma/schema.prisma car_project_work/packages/database/prisma/schema.prisma
|
||||
--- car_project_original/packages/database/prisma/schema.prisma 2026-06-09 20:18:52.000000000 +0000
|
||||
+++ car_project_work/packages/database/prisma/schema.prisma 2026-06-09 23:18:30.516362264 +0000
|
||||
@@ -502,7 +502,6 @@
|
||||
address Json?
|
||||
status CompanyStatus @default(PENDING)
|
||||
subscriptionPaymentRef String?
|
||||
- apiKey String @unique @default(cuid())
|
||||
|
||||
subscription Subscription?
|
||||
billingAccounts BillingAccount[]
|
||||
diff -ru '--exclude=node_modules' car_project_original/packages/database/src/index.d.ts car_project_work/packages/database/src/index.d.ts
|
||||
--- car_project_original/packages/database/src/index.d.ts 2026-06-09 19:40:36.000000000 +0000
|
||||
+++ car_project_work/packages/database/src/index.d.ts 2026-06-09 23:18:30.517981475 +0000
|
||||
@@ -33,7 +33,6 @@
|
||||
email: string
|
||||
phone: string | null
|
||||
status: string
|
||||
- apiKey: string
|
||||
}
|
||||
|
||||
export interface Employee {
|
||||
diff -ru '--exclude=node_modules' car_project_original/packages/database/src/index.ts car_project_work/packages/database/src/index.ts
|
||||
--- car_project_original/packages/database/src/index.ts 2026-06-09 19:40:36.000000000 +0000
|
||||
+++ car_project_work/packages/database/src/index.ts 2026-06-09 23:18:30.517228009 +0000
|
||||
@@ -33,7 +33,6 @@
|
||||
email: string
|
||||
phone: string | null
|
||||
status: string
|
||||
- apiKey: string
|
||||
}
|
||||
|
||||
export interface Employee {
|
||||
Reference in New Issue
Block a user