fix architecture and write new tests

This commit is contained in:
root
2026-06-10 00:40:19 -04:00
parent 560da1cadf
commit 80a597bc10
377 changed files with 84020 additions and 1337 deletions
@@ -0,0 +1,128 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('jsonwebtoken', () => ({ default: { verify: vi.fn() } }))
vi.mock('../../lib/redis', () => ({
redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() },
}))
vi.mock('../../lib/prisma', () => ({
prisma: {
adminUser: { findUnique: vi.fn() },
},
}))
vi.mock('../../modules/admin/admin.service', () => ({
getBilling: vi.fn(),
getInvoicePdf: vi.fn(),
getBillingAccountDetail: vi.fn(),
getCompanyInvoices: vi.fn(),
updateBillingAccount: vi.fn(),
setDunningPaused: vi.fn(),
createBillingInvoice: vi.fn(),
finalizeBillingInvoice: vi.fn(),
payBillingInvoice: vi.fn(),
retryBillingInvoicePayment: vi.fn(),
voidBillingInvoice: vi.fn(),
markBillingInvoiceUncollectible: vi.fn(),
issueBillingCreditNote: vi.fn(),
issueBillingRefund: vi.fn(),
}))
import request from 'supertest'
import jwt from 'jsonwebtoken'
import { createApp } from '../../app'
import { prisma } from '../../lib/prisma'
import * as adminService from '../../modules/admin/admin.service'
const app = createApp()
function admin(role = 'FINANCE') {
return { id: 'admin_1', email: 'finance@example.test', role, isActive: true, totpEnabled: true }
}
beforeEach(() => {
vi.clearAllMocks()
process.env.JWT_SECRET = 'test-secret'
vi.mocked(jwt.verify).mockReturnValue({ sub: 'admin_1', type: 'admin' } as never)
vi.mocked(prisma.adminUser.findUnique).mockResolvedValue(admin() as never)
})
describe('admin billing API contracts', () => {
it('coerces list filters and requires finance access', async () => {
vi.mocked(adminService.getBilling).mockResolvedValue({ data: [], total: 0, stats: {} } as never)
const res = await request(app)
.get('/api/v1/admin/billing?q=atlas&status=PAST_DUE&plan=PRO&page=2&pageSize=25')
.set('Authorization', 'Bearer admin-token')
expect(res.status).toBe(200)
expect(adminService.getBilling).toHaveBeenCalledWith({
q: 'atlas',
status: 'PAST_DUE',
plan: 'PRO',
page: 2,
pageSize: 25,
})
})
it('rejects oversized billing pagination before service execution', async () => {
const res = await request(app)
.get('/api/v1/admin/billing?pageSize=500')
.set('Authorization', 'Bearer admin-token')
expect(res.status).toBe(400)
expect(adminService.getBilling).not.toHaveBeenCalled()
})
it('streams invoice PDFs with content headers from the service boundary', async () => {
vi.mocked(adminService.getInvoicePdf).mockResolvedValue({
invoiceNumber: 'INV-2026-000001',
pdfBuffer: Buffer.from('%PDF fake'),
} as never)
const res = await request(app)
.get('/api/v1/admin/billing/invoices/invoice_1/pdf')
.set('Authorization', 'Bearer admin-token')
expect(res.status).toBe(200)
expect(res.header['content-type']).toContain('application/pdf')
expect(res.header['content-disposition']).toContain('INV-2026-000001.pdf')
expect(adminService.getInvoicePdf).toHaveBeenCalledWith('invoice_1')
})
it('passes parsed account updates with admin identity and request ip', async () => {
vi.mocked(adminService.updateBillingAccount).mockResolvedValue({ id: 'billing_account_1' } as never)
const res = await request(app)
.patch('/api/v1/admin/billing/accounts/billing_account_1')
.set('Authorization', 'Bearer admin-token')
.send({ billingEmail: 'billing@example.test', invoiceTerms: 'NET_30', netTermsDays: 30 })
expect(res.status).toBe(200)
expect(adminService.updateBillingAccount).toHaveBeenCalledWith(
'billing_account_1',
{ billingEmail: 'billing@example.test', invoiceTerms: 'NET_30', netTermsDays: 30 },
'admin_1',
expect.any(String),
)
})
it('rejects malformed draft invoice payloads before invoice creation', async () => {
const res = await request(app)
.post('/api/v1/admin/billing/accounts/billing_account_1/invoices')
.set('Authorization', 'Bearer admin-token')
.send({ invoiceType: 'MANUAL', lineItems: [] })
expect(res.status).toBe(400)
expect(adminService.createBillingInvoice).not.toHaveBeenCalled()
})
it('requires finance role for billing mutations', async () => {
vi.mocked(prisma.adminUser.findUnique).mockResolvedValue(admin('VIEWER') as never)
const res = await request(app)
.post('/api/v1/admin/billing/invoices/invoice_1/finalize')
.set('Authorization', 'Bearer admin-token')
expect(res.status).toBe(403)
expect(adminService.finalizeBillingInvoice).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,112 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('jsonwebtoken', () => ({ default: { verify: vi.fn() } }))
vi.mock('../../lib/redis', () => ({
redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() },
}))
vi.mock('../../lib/prisma', () => ({
prisma: { adminUser: { findUnique: vi.fn() } },
}))
vi.mock('../../modules/menu/menu.service', () => ({
listMenuItems: vi.fn(),
createMenuItem: vi.fn(),
getMenuItem: vi.fn(),
updateMenuItem: vi.fn(),
setMenuItemStatus: vi.fn(),
deleteMenuItem: vi.fn(),
assignMenuItemToPlans: vi.fn(),
removeMenuItemFromPlan: vi.fn(),
assignMenuItemToCompanies: vi.fn(),
removeMenuItemFromCompany: vi.fn(),
previewCompanyMenu: vi.fn(),
listMenuAuditLogs: vi.fn(),
}))
import request from 'supertest'
import jwt from 'jsonwebtoken'
import { createApp } from '../../app'
import { prisma } from '../../lib/prisma'
import * as menuService from '../../modules/menu/menu.service'
const app = createApp()
function admin(role = 'ADMIN') {
return { id: 'admin_1', email: 'admin@example.test', role, isActive: true, totpEnabled: true }
}
describe('admin menu API contracts', () => {
beforeEach(() => {
vi.clearAllMocks()
process.env.JWT_SECRET = 'test-secret'
vi.mocked(jwt.verify).mockReturnValue({ sub: 'admin_1', type: 'admin' } as never)
vi.mocked(prisma.adminUser.findUnique).mockResolvedValue(admin() as never)
})
it('rejects menu item creation with blank labels before service execution', async () => {
const res = await request(app)
.post('/api/v1/admin/menu-items')
.set('Authorization', 'Bearer admin-token')
.send({ label: ' ', itemType: 'INTERNAL_PAGE', routeOrUrl: '/fleet' })
expect(res.status).toBe(400)
expect(menuService.createMenuItem).not.toHaveBeenCalled()
})
it('passes normalized menu item creation payload with admin actor', async () => {
vi.mocked(menuService.createMenuItem).mockResolvedValue({ id: 'item_1', label: 'Fleet' } as never)
const res = await request(app)
.post('/api/v1/admin/menu-items')
.set('Authorization', 'Bearer admin-token')
.send({ label: ' Fleet ', itemType: 'INTERNAL_PAGE', routeOrUrl: '/fleet', roles: ['MANAGER'] })
expect(res.status).toBe(201)
expect(menuService.createMenuItem).toHaveBeenCalledWith(
expect.objectContaining({ label: 'Fleet', itemType: 'INTERNAL_PAGE', routeOrUrl: '/fleet', roles: ['MANAGER'] }),
expect.objectContaining({ id: 'admin_1' }),
)
})
it('rejects invalid menu status bodies before mutation', async () => {
const res = await request(app)
.patch('/api/v1/admin/menu-items/item_1/status')
.set('Authorization', 'Bearer admin-token')
.send({ isActive: 'false' })
expect(res.status).toBe(400)
expect(menuService.setMenuItemStatus).not.toHaveBeenCalled()
})
it('requires admin privileges for menu mutations while support can preview', async () => {
vi.mocked(prisma.adminUser.findUnique).mockResolvedValue(admin('SUPPORT') as never)
vi.mocked(menuService.previewCompanyMenu).mockResolvedValue({ items: [], finalMenu: [] } as never)
const createRes = await request(app)
.post('/api/v1/admin/menu-items')
.set('Authorization', 'Bearer admin-token')
.send({ label: 'Fleet', itemType: 'INTERNAL_PAGE', routeOrUrl: '/fleet' })
expect(createRes.status).toBe(403)
const previewRes = await request(app)
.post('/api/v1/admin/menu-preview')
.set('Authorization', 'Bearer admin-token')
.send({ companyId: 'company_1', role: 'MANAGER' })
expect(previewRes.status).toBe(200)
expect(menuService.previewCompanyMenu).toHaveBeenCalledWith({ companyId: 'company_1', role: 'MANAGER' })
})
it('coerces audit-log pagination defaults and caps oversized limits', async () => {
vi.mocked(menuService.listMenuAuditLogs).mockResolvedValue({ data: [], total: 0, page: 1, pageSize: 20, totalPages: 0 } as never)
const defaultRes = await request(app)
.get('/api/v1/admin/menu-audit-logs')
.set('Authorization', 'Bearer admin-token')
expect(defaultRes.status).toBe(200)
expect(menuService.listMenuAuditLogs).toHaveBeenCalledWith({ page: 1, pageSize: 20 })
const oversizedRes = await request(app)
.get('/api/v1/admin/menu-audit-logs?pageSize=500')
.set('Authorization', 'Bearer admin-token')
expect(oversizedRes.status).toBe(400)
})
})
@@ -0,0 +1,91 @@
import { vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({
prisma: {},
}))
vi.mock('../../lib/redis', () => ({
redis: {
on: vi.fn(),
get: vi.fn(),
set: vi.fn(),
del: vi.fn(),
quit: vi.fn(),
},
}))
import request from 'supertest'
import { describe, expect, it } from 'vitest'
import { createApp } from '../../app'
const app = createApp()
describe('API foundation integration', () => {
it('GET /health returns a stable health contract', async () => {
const res = await request(app).get('/health')
expect(res.status).toBe(200)
expect(res.body).toEqual({
status: 'ok',
version: '1.0.0',
timestamp: expect.any(String),
})
expect(new Date(res.body.timestamp).toString()).not.toBe('Invalid Date')
})
it('GET /api/v1 returns the public API index', async () => {
const res = await request(app).get('/api/v1')
expect(res.status).toBe(200)
expect(res.body).toEqual({
name: 'rentaldrivego-api',
version: '1.0.0',
baseUrl: '/api/v1',
health: '/health',
docs: '/api/v1/docs',
openApi: '/api/v1/openapi.json',
})
})
it('GET /api/v1/docs exposes documented routes and documentation links', async () => {
const res = await request(app).get('/api/v1/docs')
expect(res.status).toBe(200)
expect(res.body).toEqual(expect.objectContaining({
name: 'rentaldrivego-api',
version: '1.0.0',
baseUrl: '/api/v1',
swaggerUi: '/docs',
openApi: '/api/v1/openapi.json',
routes: expect.any(Array),
}))
expect(res.body.routes).toEqual(expect.arrayContaining([
expect.objectContaining({ method: 'GET', path: '/health' }),
expect.objectContaining({ method: 'GET', path: '/api/v1/vehicles' }),
expect.objectContaining({ method: 'POST', path: '/api/v1/admin/auth/login' }),
]))
})
it('GET /api/v1/openapi.json returns the OpenAPI document', async () => {
const res = await request(app).get('/api/v1/openapi.json')
expect(res.status).toBe(200)
expect(res.body.openapi).toMatch(/^3\./)
expect(res.body.info).toEqual(expect.objectContaining({ title: expect.any(String), version: expect.any(String) }))
expect(res.body.paths).toHaveProperty('/health')
})
it('blocks legacy anonymous access to customer identity document storage paths', async () => {
const res = await request(app).get('/storage/companies/company_1/customers/customer_1/passport.jpg')
expect(res.status).toBe(404)
expect(res.text).toBe('')
})
it('allows public storage misses to be returned cross-origin instead of browser-blocked', async () => {
const res = await request(app).get('/storage/vehicles/missing-photo.jpg')
expect(res.status).toBe(404)
expect(res.headers['cross-origin-resource-policy']).toBe('cross-origin')
})
})
@@ -0,0 +1,162 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({ prisma: {} }))
vi.mock('../../lib/redis', () => ({
redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() },
}))
vi.mock('../../middleware/requireRenterAuth', () => ({
requireRenterAuth: (req: any, _res: any, next: any) => {
req.renterId = 'renter_1'
next()
},
optionalRenterAuth: (_req: any, _res: any, next: any) => next(),
}))
vi.mock('../../modules/auth/auth.company.service', () => ({
signup: vi.fn(),
completeSignupDisabled: vi.fn(() => {
const err = new Error('gone') as Error & { statusCode?: number; code?: string }
err.statusCode = 410
err.code = 'disabled'
throw err
}),
verifyEmailDisabled: vi.fn(() => {
const err = new Error('gone') as Error & { statusCode?: number; code?: string }
err.statusCode = 410
err.code = 'disabled'
throw err
}),
}))
vi.mock('../../modules/auth/auth.renter.service', () => ({
signupDisabled: vi.fn(() => {
const err = new Error('renter disabled') as Error & { statusCode?: number; code?: string }
err.statusCode = 403
err.code = 'renter_signup_disabled'
throw err
}),
loginDisabled: vi.fn(() => {
const err = new Error('login disabled') as Error & { statusCode?: number; code?: string }
err.statusCode = 403
err.code = 'renter_login_disabled'
throw err
}),
getMe: vi.fn(),
updateMe: vi.fn(),
updateFcmToken: vi.fn(),
}))
import request from 'supertest'
import { createApp } from '../../app'
import * as companyService from '../../modules/auth/auth.company.service'
import * as renterService from '../../modules/auth/auth.renter.service'
const app = createApp()
const signupPayload = {
firstName: 'Aya',
lastName: 'Benali',
email: 'owner@example.com',
password: 'super-secret',
companyName: 'Atlas Cars',
legalName: 'Atlas Cars SARL',
legalForm: 'SARL',
registrationNumber: 'REG123',
iceNumber: 'ICE123',
taxId: 'TAX123',
operatingLicenseNumber: 'LIC123',
operatingLicenseIssuedAt: '2026-01-01',
operatingLicenseIssuedBy: 'Transport Authority',
streetAddress: '1 Fleet Street',
city: 'Casablanca',
country: 'Morocco',
zipCode: '20000',
companyPhone: '+212600000000',
companyEmail: 'contact@example.com',
yearsActive: '5',
responsibleName: 'Omar Alaoui',
responsibleRole: 'Manager',
responsibleIdentityNumber: 'ID123',
responsiblePhone: '+212611111111',
responsibleEmail: 'responsible@example.com',
preferredLanguage: 'fr',
plan: 'STARTER',
billingPeriod: 'MONTHLY',
currency: 'MAD',
paymentProvider: 'PAYPAL',
}
describe('auth API boundaries', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(companyService.signup).mockResolvedValue({ companyId: 'company_1', nextStep: 'workspace_created' } as never)
vi.mocked(renterService.getMe).mockResolvedValue({ id: 'renter_1', savedCompanies: [] } as never)
vi.mocked(renterService.updateMe).mockResolvedValue({ id: 'renter_1', firstName: 'Nora' } as never)
vi.mocked(renterService.updateFcmToken).mockResolvedValue({ success: true } as never)
})
it('POST /auth/company/signup validates the full onboarding contract and returns 201', async () => {
const res = await request(app).post('/api/v1/auth/company/signup').send(signupPayload)
expect(res.status).toBe(201)
expect(res.body).toEqual({ data: { companyId: 'company_1', nextStep: 'workspace_created' } })
expect(companyService.signup).toHaveBeenCalledWith(expect.objectContaining({
companyName: 'Atlas Cars',
currency: 'MAD',
preferredLanguage: 'fr',
paymentProvider: 'PAYPAL',
}))
})
it('POST /auth/company/signup rejects malformed onboarding payloads before service execution', async () => {
const res = await request(app).post('/api/v1/auth/company/signup').send({ ...signupPayload, companyEmail: 'not-email' })
expect(res.status).toBe(400)
expect(res.body.error).toBe('validation_error')
expect(companyService.signup).not.toHaveBeenCalled()
})
it('preserves removed company auth compatibility endpoints as explicit 410 responses', async () => {
const complete = await request(app).post('/api/v1/auth/company/complete-signup').send({})
const verify = await request(app).post('/api/v1/auth/company/verify-email').send({})
expect(complete.status).toBe(410)
expect(complete.body.error).toBe('disabled')
expect(verify.status).toBe(410)
expect(verify.body.error).toBe('disabled')
})
it('keeps public renter signup and login disabled at the router boundary', async () => {
const signup = await request(app).post('/api/v1/auth/renter/signup').send({})
const login = await request(app).post('/api/v1/auth/renter/login').send({})
expect(signup.status).toBe(403)
expect(signup.body.error).toBe('renter_signup_disabled')
expect(login.status).toBe(403)
expect(login.body.error).toBe('renter_login_disabled')
})
it('GET /auth/renter/me uses authenticated renter identity', async () => {
const res = await request(app).get('/api/v1/auth/renter/me')
expect(res.status).toBe(200)
expect(res.body).toEqual({ data: { id: 'renter_1', savedCompanies: [] } })
expect(renterService.getMe).toHaveBeenCalledWith('renter_1')
})
it('PATCH /auth/renter/me validates renter profile updates', async () => {
const res = await request(app).patch('/api/v1/auth/renter/me').send({ firstName: ' Nora ', preferredCurrency: 'MAD' })
expect(res.status).toBe(200)
expect(renterService.updateMe).toHaveBeenCalledWith('renter_1', { firstName: 'Nora', preferredCurrency: 'MAD' })
})
it('POST /auth/renter/me/fcm-token updates only the token value for the authenticated renter', async () => {
const res = await request(app).post('/api/v1/auth/renter/me/fcm-token').send({ fcmToken: 'token_123' })
expect(res.status).toBe(200)
expect(res.body).toEqual({ data: { success: true } })
expect(renterService.updateFcmToken).toHaveBeenCalledWith('renter_1', 'token_123')
})
})
@@ -0,0 +1,150 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('jsonwebtoken', () => ({
default: { verify: vi.fn() },
}))
vi.mock('../../lib/redis', () => ({
redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() },
}))
vi.mock('../../lib/prisma', () => ({
prisma: {
employee: { findUnique: vi.fn() },
company: { findUnique: vi.fn() },
adminUser: { findUnique: vi.fn() },
renter: { findUnique: vi.fn() },
},
}))
vi.mock('../../modules/vehicles/vehicle.service', () => ({
listVehicles: vi.fn(),
createVehicle: vi.fn(),
}))
vi.mock('../../modules/admin/admin.service', () => ({
listCompanies: vi.fn(),
login: vi.fn(),
forgotPassword: vi.fn(),
resetPassword: vi.fn(),
}))
import request from 'supertest'
import jwt from 'jsonwebtoken'
import { createApp } from '../../app'
import { prisma } from '../../lib/prisma'
import * as vehicleService from '../../modules/vehicles/vehicle.service'
import * as adminService from '../../modules/admin/admin.service'
const app = createApp()
function activeEmployee(role: string) {
return {
id: 'emp_1',
role,
isActive: true,
companyId: 'company_1',
company: { id: 'company_1', status: 'ACTIVE' },
}
}
describe('auth middleware API boundaries', () => {
beforeEach(() => {
vi.clearAllMocks()
process.env.JWT_SECRET = 'test-secret'
process.env.NEXT_PUBLIC_DASHBOARD_URL = 'https://dashboard.example.test'
})
it('rejects protected company routes before service execution when no token is present', async () => {
const res = await request(app).get('/api/v1/vehicles')
expect(res.status).toBe(401)
expect(res.body).toEqual({ error: 'unauthenticated', message: 'Authentication required', statusCode: 401 })
expect(vehicleService.listVehicles).not.toHaveBeenCalled()
})
it('rejects employee-protected routes when a renter token is used', async () => {
vi.mocked(jwt.verify).mockReturnValue({ sub: 'renter_1', type: 'renter' } as never)
const res = await request(app).get('/api/v1/vehicles').set('Authorization', 'Bearer renter-token')
expect(res.status).toBe(401)
expect(res.body).toMatchObject({ error: 'invalid_token', message: 'Invalid or expired session token', statusCode: 401 })
expect(prisma.employee.findUnique).not.toHaveBeenCalled()
})
it('rejects inactive employees before tenant and subscription checks', async () => {
vi.mocked(jwt.verify).mockReturnValue({ sub: 'emp_1', type: 'employee' } as never)
vi.mocked(prisma.employee.findUnique).mockResolvedValue({ id: 'emp_1', isActive: false } as never)
const res = await request(app).get('/api/v1/vehicles').set('Authorization', 'Bearer employee-token')
expect(res.status).toBe(401)
expect(res.body).toEqual({ error: 'unauthenticated', message: 'Employee account not found or inactive', statusCode: 401 })
expect(prisma.company.findUnique).not.toHaveBeenCalled()
})
it('blocks suspended tenants at the subscription boundary', async () => {
vi.mocked(jwt.verify).mockReturnValue({ sub: 'emp_1', type: 'employee' } as never)
vi.mocked(prisma.employee.findUnique).mockResolvedValue(activeEmployee('OWNER') as never)
vi.mocked(prisma.company.findUnique).mockResolvedValue({ id: 'company_1', status: 'SUSPENDED' } as never)
const res = await request(app).get('/api/v1/vehicles').set('Authorization', 'Bearer employee-token')
expect(res.status).toBe(402)
expect(res.body).toEqual(expect.objectContaining({
error: 'subscription_suspended',
billingUrl: 'https://dashboard.example.test/billing',
}))
expect(vehicleService.listVehicles).not.toHaveBeenCalled()
})
it('blocks lower company roles from manager-only mutations', async () => {
vi.mocked(jwt.verify).mockReturnValue({ sub: 'emp_1', type: 'employee' } as never)
vi.mocked(prisma.employee.findUnique).mockResolvedValue(activeEmployee('AGENT') as never)
vi.mocked(prisma.company.findUnique).mockResolvedValue({ id: 'company_1', status: 'ACTIVE' } as never)
const res = await request(app).post('/api/v1/vehicles').set('Authorization', 'Bearer employee-token').send({})
expect(res.status).toBe(403)
expect(res.body).toEqual(expect.objectContaining({
error: 'forbidden',
requiredRole: 'MANAGER',
yourRole: 'AGENT',
}))
expect(vehicleService.createVehicle).not.toHaveBeenCalled()
})
it('allows authenticated active tenants through to read-only company handlers', async () => {
vi.mocked(jwt.verify).mockReturnValue({ sub: 'emp_1', type: 'employee' } as never)
vi.mocked(prisma.employee.findUnique).mockResolvedValue(activeEmployee('AGENT') as never)
vi.mocked(prisma.company.findUnique).mockResolvedValue({ id: 'company_1', status: 'ACTIVE' } as never)
vi.mocked(vehicleService.listVehicles).mockResolvedValue({ data: [{ id: 'vehicle_1' }], pagination: { page: 1 } } as never)
const res = await request(app).get('/api/v1/vehicles').set('Authorization', 'Bearer employee-token')
expect(res.status).toBe(200)
expect(res.body).toEqual({ data: [{ id: 'vehicle_1' }], pagination: { page: 1 } })
expect(vehicleService.listVehicles).toHaveBeenCalledWith('company_1', { page: 1, pageSize: 20 })
})
it('rejects protected admin routes before admin service execution when no admin token is present', async () => {
const res = await request(app).get('/api/v1/admin/companies')
expect(res.status).toBe(401)
expect(res.body).toEqual({ error: 'unauthenticated', message: 'Admin authentication required', statusCode: 401 })
expect(adminService.listCompanies).not.toHaveBeenCalled()
})
it('allows valid admin tokens through protected admin read routes', async () => {
vi.mocked(jwt.verify).mockReturnValue({ sub: 'admin_1', type: 'admin' } as never)
vi.mocked(prisma.adminUser.findUnique).mockResolvedValue({ id: 'admin_1', isActive: true, role: 'SUPPORT', totpEnabled: true } as never)
vi.mocked(adminService.listCompanies).mockResolvedValue({ data: [{ id: 'company_1' }], pagination: { page: 1 } } as never)
const res = await request(app).get('/api/v1/admin/companies').set('Authorization', 'Bearer admin-token')
expect(res.status).toBe(200)
expect(res.body).toEqual({ data: { data: [{ id: 'company_1' }], pagination: { page: 1 } } })
expect(adminService.listCompanies).toHaveBeenCalledWith({ page: 1, pageSize: 20 })
})
})
@@ -0,0 +1,149 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({ prisma: {} }))
vi.mock('../../lib/redis', () => ({
redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() },
}))
vi.mock('../../middleware/requireCompanyAuth', () => ({
requireCompanyAuth: (req: any, _res: any, next: any) => {
req.employee = { id: 'employee_1', role: 'OWNER' }
next()
},
requireCompanyDocumentAuth: (req: any, _res: any, next: any) => {
req.employee = { id: 'employee_1', role: 'OWNER' }
next()
},
}))
vi.mock('../../middleware/requireTenant', () => ({
requireTenant: (req: any, _res: any, next: any) => {
req.companyId = 'company_1'
req.company = { id: 'company_1', name: 'Atlas Cars', slug: 'atlas' }
next()
},
}))
vi.mock('../../middleware/requireSubscription', () => ({
requireSubscription: (_req: any, _res: any, next: any) => next(),
}))
vi.mock('../../middleware/requireRole', () => ({
requireRole: () => (_req: any, _res: any, next: any) => next(),
}))
vi.mock('../../modules/companies/company.service', () => ({
getCompany: vi.fn(),
updateCompany: vi.fn(),
getBrand: vi.fn(),
updateBrand: vi.fn(),
uploadLogo: vi.fn(),
uploadHeroImage: vi.fn(),
checkSubdomainAvailability: vi.fn(),
setCustomDomain: vi.fn(),
getCustomDomainStatus: vi.fn(),
removeCustomDomain: vi.fn(),
getContractSettings: vi.fn(),
updateContractSettings: vi.fn(),
getInsurancePolicies: vi.fn(),
createInsurancePolicy: vi.fn(),
updateInsurancePolicy: vi.fn(),
deleteInsurancePolicy: vi.fn(),
getPricingRules: vi.fn(),
createPricingRule: vi.fn(),
updatePricingRule: vi.fn(),
deletePricingRule: vi.fn(),
getAccountingSettings: vi.fn(),
updateAccountingSettings: vi.fn(),
getApiKey: vi.fn(),
regenerateApiKey: vi.fn(),
}))
import request from 'supertest'
import { createApp } from '../../app'
import * as companyService from '../../modules/companies/company.service'
const app = createApp()
describe('company configuration API contracts', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(companyService.updateBrand).mockResolvedValue({ id: 'brand_1', displayName: 'Atlas Cars' } as never)
vi.mocked(companyService.setCustomDomain).mockResolvedValue({ id: 'brand_1', customDomain: 'cars.example.com' } as never)
vi.mocked(companyService.createInsurancePolicy).mockResolvedValue({ id: 'policy_1' } as never)
vi.mocked(companyService.createPricingRule).mockResolvedValue({ id: 'rule_1' } as never)
vi.mocked(companyService.updateAccountingSettings).mockResolvedValue({ id: 'accounting_1' } as never)
})
it('rejects invalid public brand email before service execution', async () => {
const res = await request(app).patch('/api/v1/companies/me/brand').send({ publicEmail: 'not-an-email' })
expect(res.status).toBe(400)
expect(companyService.updateBrand).not.toHaveBeenCalled()
})
it('passes valid brand settings with tenant identity and company defaults', async () => {
const res = await request(app).patch('/api/v1/companies/me/brand').send({
displayName: 'Atlas Premium Cars',
defaultCurrency: 'MAD',
paypalEmail: 'billing@example.test',
})
expect(res.status).toBe(200)
expect(companyService.updateBrand).toHaveBeenCalledWith('company_1', {
displayName: 'Atlas Premium Cars',
defaultCurrency: 'MAD',
paypalEmail: 'billing@example.test',
}, 'Atlas Cars', 'atlas')
})
it('rejects invalid custom domains before normalization or persistence', async () => {
const res = await request(app).post('/api/v1/companies/me/brand/custom-domain').send({ customDomain: 'ab' })
expect(res.status).toBe(400)
expect(companyService.setCustomDomain).not.toHaveBeenCalled()
})
it('passes custom domain requests with tenant defaults', async () => {
const res = await request(app).post('/api/v1/companies/me/brand/custom-domain').send({ customDomain: 'Cars.Example.com' })
expect(res.status).toBe(200)
expect(companyService.setCustomDomain).toHaveBeenCalledWith('company_1', 'Atlas Cars', 'atlas', 'Cars.Example.com')
})
it('applies insurance policy defaults before creating a policy', async () => {
const res = await request(app).post('/api/v1/companies/me/insurance-policies').send({
name: 'Basic CDW',
type: 'CDW',
chargeType: 'PER_DAY',
chargeValue: 2500,
})
expect(res.status).toBe(201)
expect(companyService.createInsurancePolicy).toHaveBeenCalledWith('company_1', expect.objectContaining({
name: 'Basic CDW',
isRequired: false,
isActive: true,
sortOrder: 0,
}))
})
it('rejects malformed pricing rules before service execution', async () => {
const res = await request(app).post('/api/v1/companies/me/pricing-rules').send({
name: 'Young driver fee',
type: 'SURCHARGE',
condition: 'AGE_LESS_THAN',
conditionValue: 25,
adjustmentType: 'PERCENTAGE',
adjustmentValue: 10.5,
})
expect(res.status).toBe(400)
expect(companyService.createPricingRule).not.toHaveBeenCalled()
})
it('validates accounting settings at the route boundary', async () => {
const invalid = await request(app).patch('/api/v1/companies/me/accounting-settings').send({ fiscalYearStart: 13 })
expect(invalid.status).toBe(400)
expect(companyService.updateAccountingSettings).not.toHaveBeenCalled()
const valid = await request(app).patch('/api/v1/companies/me/accounting-settings').send({ fiscalYearStart: 4, reportFormat: 'BOTH' })
expect(valid.status).toBe(200)
expect(companyService.updateAccountingSettings).toHaveBeenCalledWith('company_1', { fiscalYearStart: 4, reportFormat: 'BOTH' })
})
})
@@ -0,0 +1,148 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({ prisma: {} }))
vi.mock('../../lib/redis', () => ({
redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() },
}))
vi.mock('../../middleware/requireCompanyAuth', () => ({
requireCompanyAuth: (req: any, _res: any, next: any) => {
req.employee = { id: 'employee_1', firstName: 'Aya', lastName: 'Manager', role: 'MANAGER' }
next()
},
requireCompanyDocumentAuth: (req: any, _res: any, next: any) => {
req.employee = { id: 'employee_1', firstName: 'Aya', lastName: 'Manager', role: 'MANAGER' }
next()
},
}))
vi.mock('../../middleware/requireTenant', () => ({
requireTenant: (req: any, _res: any, next: any) => {
req.companyId = 'company_1'
next()
},
}))
vi.mock('../../middleware/requireSubscription', () => ({ requireSubscription: (_req: any, _res: any, next: any) => next() }))
vi.mock('../../middleware/requireRole', () => ({ requireRole: () => (_req: any, _res: any, next: any) => next() }))
vi.mock('../../middleware/requireRenterAuth', () => ({
requireRenterAuth: (req: any, _res: any, next: any) => {
req.renterId = 'renter_1'
next()
},
optionalRenterAuth: (_req: any, _res: any, next: any) => next(),
}))
vi.mock('../../modules/customers/customer.service', () => ({
listCustomers: vi.fn(),
createCustomer: vi.fn(),
getCustomer: vi.fn(),
updateCustomer: vi.fn(),
flagCustomer: vi.fn(),
unflagCustomer: vi.fn(),
validateCustomerLicense: vi.fn(),
uploadLicenseImage: vi.fn(),
approveLicense: vi.fn(),
getLicenseImageFile: vi.fn(),
}))
vi.mock('../../modules/notifications/notification.service', () => ({
listCompany: vi.fn(),
countUnread: vi.fn(),
markRead: vi.fn(),
markAllRead: vi.fn(),
getPreferences: vi.fn(),
setPreferences: vi.fn(),
listCompanyHistory: vi.fn(),
listRenter: vi.fn(),
markRenterRead: vi.fn(),
markAllRenterRead: vi.fn(),
getRenterPreferences: vi.fn(),
setRenterPreferences: vi.fn(),
}))
vi.mock('../../modules/analytics/analytics.service', () => ({
getSummary: vi.fn(),
getDashboard: vi.fn(),
getSources: vi.fn(),
getReport: vi.fn(),
}))
import request from 'supertest'
import { createApp } from '../../app'
import * as customerService from '../../modules/customers/customer.service'
import * as notificationService from '../../modules/notifications/notification.service'
import * as analyticsService from '../../modules/analytics/analytics.service'
const app = createApp()
describe('customer, notification and analytics API validation contracts', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(customerService.listCustomers).mockResolvedValue({ items: [], page: 2, pageSize: 10, total: 0 } as never)
vi.mocked(customerService.createCustomer).mockResolvedValue({ id: 'customer_1' } as never)
vi.mocked(customerService.approveLicense).mockResolvedValue({ id: 'customer_1', licenseStatus: 'APPROVED' } as never)
vi.mocked(notificationService.listCompanyHistory).mockResolvedValue([] as never)
vi.mocked(analyticsService.getSummary).mockResolvedValue({ reservations: 0 } as never)
})
it('coerces customer list pagination before calling the service', async () => {
const res = await request(app).get('/api/v1/customers?page=2&pageSize=10&flagged=false&q=Aya')
expect(res.status).toBe(200)
expect(customerService.listCustomers).toHaveBeenCalledWith('company_1', {
page: 2,
pageSize: 10,
flagged: 'false',
q: 'Aya',
})
})
it('rejects malformed customer creates before persistence', async () => {
const res = await request(app).post('/api/v1/customers').send({
firstName: 'Aya',
lastName: 'Haddad',
email: 'not-email',
})
expect(res.status).toBe(400)
expect(customerService.createCustomer).not.toHaveBeenCalled()
})
it('normalizes valid customer create payloads at the route boundary', async () => {
const res = await request(app).post('/api/v1/customers').send({
firstName: ' Aya ',
lastName: ' Haddad ',
email: 'AYA@EXAMPLE.TEST',
phone: ' +212600000000 ',
})
expect(res.status).toBe(201)
expect(customerService.createCustomer).toHaveBeenCalledWith(
expect.objectContaining({ firstName: 'Aya', lastName: 'Haddad', email: 'aya@example.test', phone: '+212600000000' }),
'company_1',
)
})
it('rejects invalid license approval decisions before service execution', async () => {
const res = await request(app).post('/api/v1/customers/customer_1/approve-license').send({ decision: 'REVIEW_LATER' })
expect(res.status).toBe(400)
expect(customerService.approveLicense).not.toHaveBeenCalled()
})
it('passes license approval actor names to the customer service', async () => {
const res = await request(app).post('/api/v1/customers/customer_1/approve-license').send({ decision: 'APPROVE', note: 'Readable license' })
expect(res.status).toBe(200)
expect(customerService.approveLicense).toHaveBeenCalledWith('customer_1', 'company_1', 'APPROVE', 'Readable license', 'Aya Manager')
})
it('caps notification history limits before querying history', async () => {
const res = await request(app).get('/api/v1/notifications/history?limit=999')
expect(res.status).toBe(400)
expect(notificationService.listCompanyHistory).not.toHaveBeenCalled()
})
it('uses the analytics summary default period when no query is supplied', async () => {
const res = await request(app).get('/api/v1/analytics/summary')
expect(res.status).toBe(200)
expect(analyticsService.getSummary).toHaveBeenCalledWith('company_1', '30d')
})
})
@@ -0,0 +1,135 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({ prisma: {} }))
vi.mock('../../lib/redis', () => ({
redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() },
}))
vi.mock('../../middleware/requireCompanyAuth', () => ({
requireCompanyDocumentAuth: (_req: any, _res: any, next: any) => next(),
requireCompanyAuth: (req: any, _res: any, next: any) => {
req.employee = { id: 'employee_1', role: 'MANAGER', firstName: 'Aya', lastName: 'Manager' }
next()
},
}))
vi.mock('../../middleware/requireTenant', () => ({
requireTenant: (req: any, _res: any, next: any) => {
req.companyId = 'company_1'
next()
},
}))
vi.mock('../../middleware/requireSubscription', () => ({ requireSubscription: (_req: any, _res: any, next: any) => next() }))
vi.mock('../../modules/auth/auth.employee.service', () => ({
getMe: vi.fn(),
login: vi.fn(),
forgotPassword: vi.fn(),
updateLanguage: vi.fn(),
resetPassword: vi.fn(),
}))
vi.mock('../../modules/menu/menu.service', () => ({ getEmployeeMenu: vi.fn() }))
vi.mock('../../modules/notifications/notification.service', () => ({
listCompany: vi.fn(),
countUnread: vi.fn(),
markRead: vi.fn(),
markAllRead: vi.fn(),
getPreferences: vi.fn(),
setPreferences: vi.fn(),
listCompanyHistory: vi.fn(),
listRenter: vi.fn(),
markRenterRead: vi.fn(),
markAllRenterRead: vi.fn(),
getRenterPreferences: vi.fn(),
setRenterPreferences: vi.fn(),
}))
vi.mock('../../modules/marketplace/marketplace.service', () => ({
getCities: vi.fn(),
getListedCompanies: vi.fn(),
searchVehicles: vi.fn(),
getVehicleDetail: vi.fn(),
createMarketplaceReservation: vi.fn(),
getCompanyPage: vi.fn(),
getCompanyReviews: vi.fn(),
getCompanyVehicles: vi.fn(),
getVehicle: vi.fn(),
getCompanyOffers: vi.fn(),
getReviewContext: vi.fn(),
submitReview: vi.fn(),
validateOfferCode: vi.fn(),
getPublicOffers: vi.fn(),
}))
import request from 'supertest'
import { createApp } from '../../app'
import * as employeeService from '../../modules/auth/auth.employee.service'
import * as notificationService from '../../modules/notifications/notification.service'
import * as marketplaceService from '../../modules/marketplace/marketplace.service'
const app = createApp()
describe('employee, notification, and marketplace API validation contracts', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(employeeService.login).mockResolvedValue({ token: 'jwt', employee: { id: 'employee_1' } } as never)
vi.mocked(employeeService.forgotPassword).mockResolvedValue({ message: 'If that email is registered, a reset link has been sent.' } as never)
vi.mocked(employeeService.updateLanguage).mockResolvedValue({ language: 'ar' } as never)
vi.mocked(employeeService.resetPassword).mockResolvedValue({ message: 'Password updated successfully. You can now sign in.' } as never)
vi.mocked(notificationService.setPreferences).mockResolvedValue({ success: true } as never)
vi.mocked(marketplaceService.searchVehicles).mockResolvedValue({ companies: [], vehicles: [], pagination: { page: 1, pageSize: 20, total: 0 } } as never)
vi.mocked(marketplaceService.submitReview).mockResolvedValue({ id: 'review_1' } as never)
})
it('normalizes employee login email before service execution', async () => {
const res = await request(app).post('/api/v1/auth/employee/login').send({
email: 'Agent@Example.TEST',
password: 'valid-password',
})
expect(res.status).toBe(200)
expect(employeeService.login).toHaveBeenCalledWith({ email: 'agent@example.test', password: 'valid-password' })
})
it('rejects empty employee reset tokens before service execution', async () => {
const res = await request(app).post('/api/v1/auth/employee/reset-password').send({ token: '', password: 'new-password' })
expect(res.status).toBe(400)
expect(employeeService.resetPassword).not.toHaveBeenCalled()
})
it('forwards authenticated language updates with the employee id from auth context', async () => {
const res = await request(app).patch('/api/v1/auth/employee/me/language').send({ language: 'ar' })
expect(res.status).toBe(200)
expect(employeeService.updateLanguage).toHaveBeenCalledWith('employee_1', 'ar')
})
it('rejects malformed notification preferences before persistence', async () => {
const res = await request(app).patch('/api/v1/notifications/company/preferences').send({
preferences: [{ notificationType: 'RESERVATION_CREATED', channel: 'EMAIL', enabled: true }],
})
expect(res.status).toBe(400)
expect(notificationService.setPreferences).not.toHaveBeenCalled()
})
it('defaults marketplace search pagination at the public route boundary', async () => {
const res = await request(app).get('/api/v1/marketplace/search?city=Casablanca')
expect(res.status).toBe(200)
expect(marketplaceService.searchVehicles).toHaveBeenCalledWith(expect.objectContaining({
city: 'Casablanca',
page: 1,
pageSize: 20,
}))
})
it('rejects out-of-range public review ratings before service execution', async () => {
const res = await request(app).post('/api/v1/marketplace/review/token_123').send({
overallRating: 6,
vehicleRating: 5,
serviceRating: 5,
comment: 'Too magical',
})
expect(res.status).toBe(400)
expect(marketplaceService.submitReview).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,139 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({ prisma: {} }))
vi.mock('../../lib/redis', () => ({
redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() },
}))
vi.mock('../../middleware/requireCompanyAuth', () => ({
requireCompanyDocumentAuth: (_req: any, _res: any, next: any) => next(),
requireCompanyAuth: (req: any, _res: any, next: any) => {
req.employee = { id: 'employee_manager', role: 'MANAGER', firstName: 'Mina', lastName: 'Manager' }
next()
},
}))
vi.mock('../../middleware/requireTenant', () => ({
requireTenant: (req: any, _res: any, next: any) => {
req.companyId = 'company_1'
next()
},
}))
vi.mock('../../middleware/requireSubscription', () => ({ requireSubscription: (_req: any, _res: any, next: any) => next() }))
vi.mock('../../middleware/requireRole', () => ({ requireRole: () => (_req: any, _res: any, next: any) => next() }))
vi.mock('../../modules/offers/offer.service', () => ({
listOffers: vi.fn(),
createOffer: vi.fn(),
getOffer: vi.fn(),
updateOffer: vi.fn(),
deleteOffer: vi.fn(),
setOfferActive: vi.fn(),
getOfferStats: vi.fn(),
}))
vi.mock('../../modules/reviews/review.service', () => ({
listReviews: vi.fn(),
getStats: vi.fn(),
getReview: vi.fn(),
replyToReview: vi.fn(),
sendReviewReminder: vi.fn(),
}))
vi.mock('../../modules/complaints/complaint.service', () => ({
listComplaints: vi.fn(),
createComplaint: vi.fn(),
getComplaint: vi.fn(),
updateComplaint: vi.fn(),
deleteComplaint: vi.fn(),
}))
import request from 'supertest'
import { createApp } from '../../app'
import * as offerService from '../../modules/offers/offer.service'
import * as reviewService from '../../modules/reviews/review.service'
import * as complaintService from '../../modules/complaints/complaint.service'
const app = createApp()
describe('feedback and offer API boundary contracts', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(offerService.createOffer).mockResolvedValue({ id: 'offer_1' } as never)
vi.mocked(reviewService.listReviews).mockResolvedValue({ items: [], total: 0 } as never)
vi.mocked(reviewService.replyToReview).mockResolvedValue({ id: 'review_1', companyReply: 'Thanks' } as never)
vi.mocked(complaintService.createComplaint).mockResolvedValue({ id: 'complaint_1' } as never)
vi.mocked(complaintService.updateComplaint).mockResolvedValue({ id: 'complaint_1', status: 'RESOLVED' } as never)
})
it('normalizes review list pagination and rating before service execution', async () => {
const res = await request(app).get('/api/v1/reviews?rating=5&page=2&pageSize=15')
expect(res.status).toBe(200)
expect(reviewService.listReviews).toHaveBeenCalledWith('company_1', { rating: 5, page: 2, pageSize: 15 })
})
it('rejects empty company review replies before service execution', async () => {
const res = await request(app).patch('/api/v1/reviews/review_1/reply').send({ companyReply: '' })
expect(res.status).toBe(400)
expect(reviewService.replyToReview).not.toHaveBeenCalled()
})
it('defaults complaint severity and forwards company context', async () => {
const res = await request(app).post('/api/v1/complaints').send({
category: 'BILLING',
subject: 'Incorrect charge',
customerId: 'customer_1',
})
expect(res.status).toBe(201)
expect(complaintService.createComplaint).toHaveBeenCalledWith('company_1', {
category: 'BILLING',
subject: 'Incorrect charge',
customerId: 'customer_1',
severity: 'LEVEL_1',
})
})
it('rejects invalid complaint transitions before service execution', async () => {
const res = await request(app).patch('/api/v1/complaints/complaint_1').send({ status: 'ESCALATED' })
expect(res.status).toBe(400)
expect(complaintService.updateComplaint).not.toHaveBeenCalled()
})
it('splits offer vehicle ids from the service payload after validation defaults are applied', async () => {
const res = await request(app).post('/api/v1/offers').send({
title: 'Weekend deal',
type: 'PERCENTAGE',
discountValue: 15,
appliesToAll: false,
validFrom: '2026-09-01T00:00:00.000Z',
validUntil: '2026-09-30T23:59:59.000Z',
vehicleIds: ['vehicle_1'],
})
expect(res.status).toBe(201)
expect(offerService.createOffer).toHaveBeenCalledWith('company_1', {
title: 'Weekend deal',
type: 'PERCENTAGE',
discountValue: 15,
appliesToAll: false,
categories: [],
validFrom: '2026-09-01T00:00:00.000Z',
validUntil: '2026-09-30T23:59:59.000Z',
isActive: true,
isPublic: true,
isFeatured: false,
}, ['vehicle_1'])
})
it('rejects offers with non-datetime validity windows before service execution', async () => {
const res = await request(app).post('/api/v1/offers').send({
title: 'Bad dates',
type: 'FIXED_AMOUNT',
discountValue: 100,
validFrom: 'tomorrow',
validUntil: 'next week',
})
expect(res.status).toBe(400)
expect(offerService.createOffer).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,148 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({ prisma: {} }))
vi.mock('../../lib/redis', () => ({
redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() },
}))
vi.mock('../../middleware/requireCompanyAuth', () => ({
requireCompanyAuth: (req: any, _res: any, next: any) => {
req.employee = { id: 'employee_1', role: 'OWNER' }
next()
},
requireCompanyDocumentAuth: (req: any, _res: any, next: any) => {
req.employee = { id: 'employee_1', role: 'OWNER' }
next()
},
}))
vi.mock('../../middleware/requireTenant', () => ({
requireTenant: (req: any, _res: any, next: any) => {
req.companyId = 'company_1'
next()
},
}))
vi.mock('../../middleware/requireSubscription', () => ({
requireSubscription: (_req: any, _res: any, next: any) => next(),
}))
vi.mock('../../middleware/requireRole', () => ({
requireRole: () => (_req: any, _res: any, next: any) => next(),
}))
vi.mock('../../middleware/requireRenterAuth', () => ({
requireRenterAuth: (req: any, _res: any, next: any) => {
req.renterId = 'renter_1'
next()
},
optionalRenterAuth: (_req: any, _res: any, next: any) => next(),
}))
vi.mock('../../modules/team/team.service', () => ({
getMembers: vi.fn(),
getMemberStats: vi.fn(),
inviteEmployee: vi.fn(),
updateEmployeeRole: vi.fn(),
deactivateEmployee: vi.fn(),
reactivateEmployee: vi.fn(),
removeEmployee: vi.fn(),
}))
vi.mock('../../modules/notifications/notification.service', () => ({
listCompany: vi.fn(),
countUnread: vi.fn(),
markRead: vi.fn(),
markAllRead: vi.fn(),
getPreferences: vi.fn(),
setPreferences: vi.fn(),
listCompanyHistory: vi.fn(),
listRenter: vi.fn(),
markRenterRead: vi.fn(),
markAllRenterRead: vi.fn(),
getRenterPreferences: vi.fn(),
setRenterPreferences: vi.fn(),
}))
vi.mock('../../modules/analytics/analytics.service', () => ({
getSummary: vi.fn(),
getDashboard: vi.fn(),
getSources: vi.fn(),
getReport: vi.fn(),
}))
import request from 'supertest'
import { createApp } from '../../app'
import * as teamService from '../../modules/team/team.service'
import * as notificationService from '../../modules/notifications/notification.service'
import * as analyticsService from '../../modules/analytics/analytics.service'
const app = createApp()
describe('operations validation API contracts', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(teamService.inviteEmployee).mockResolvedValue({ id: 'employee_2' } as never)
vi.mocked(teamService.updateEmployeeRole).mockResolvedValue({ id: 'employee_2', role: 'AGENT' } as never)
vi.mocked(notificationService.setPreferences).mockResolvedValue(undefined as never)
vi.mocked(notificationService.listCompanyHistory).mockResolvedValue([] as never)
vi.mocked(analyticsService.getReport).mockResolvedValue({
format: 'CSV',
startDate: new Date('2026-01-01T00:00:00.000Z'),
endDate: new Date('2026-01-31T00:00:00.000Z'),
csv: 'metric,value\nrevenue,1000\n',
} as never)
})
it('rejects invalid team invite roles before service execution', async () => {
const res = await request(app).post('/api/v1/team/invite').send({
firstName: 'Aya',
lastName: 'Benali',
email: 'aya@example.test',
role: 'OWNER',
})
expect(res.status).toBe(400)
expect(teamService.inviteEmployee).not.toHaveBeenCalled()
})
it('passes valid team role updates with actor context', async () => {
const res = await request(app).patch('/api/v1/team/employee_2/role').send({ role: 'AGENT' })
expect(res.status).toBe(200)
expect(teamService.updateEmployeeRole).toHaveBeenCalledWith('company_1', 'employee_1', 'OWNER', 'employee_2', { role: 'AGENT' })
})
it('rejects malformed company notification preferences before persistence', async () => {
const res = await request(app).patch('/api/v1/notifications/company/preferences').send([
{ notificationType: 'BOOKING', channel: 'EMAIL' },
])
expect(res.status).toBe(400)
expect(notificationService.setPreferences).not.toHaveBeenCalled()
})
it('accepts valid company notification preferences and passes employee identity', async () => {
const res = await request(app).patch('/api/v1/notifications/company/preferences').send([
{ notificationType: 'BOOKING_CREATED', channel: 'EMAIL', enabled: true },
])
expect(res.status).toBe(200)
expect(notificationService.setPreferences).toHaveBeenCalledWith('employee_1', [
{ notificationType: 'BOOKING_CREATED', channel: 'EMAIL', enabled: true },
])
})
it('coerces notification history limits before querying service history', async () => {
const res = await request(app).get('/api/v1/notifications/history?channel=EMAIL&status=SENT&limit=50')
expect(res.status).toBe(200)
expect(notificationService.listCompanyHistory).toHaveBeenCalledWith('company_1', {
channel: 'EMAIL',
status: 'SENT',
limit: 50,
})
})
it('streams analytics CSV reports with deterministic filenames', async () => {
const res = await request(app).get('/api/v1/analytics/report?format=CSV&period=month')
expect(res.status).toBe(200)
expect(res.header['content-type']).toContain('text/csv')
expect(res.header['content-disposition']).toContain('report-2026-01-01-2026-01-31.csv')
expect(res.text).toContain('metric,value')
expect(analyticsService.getReport).toHaveBeenCalledWith('company_1', { format: 'CSV', period: 'month' })
})
})
@@ -0,0 +1,196 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({ prisma: {} }))
vi.mock('../../lib/redis', () => ({
redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() },
}))
vi.mock('../../middleware/requireCompanyAuth', () => {
const auth = (req: any, _res: any, next: any) => {
req.companyId = 'company_1'
req.company = { id: 'company_1' }
req.employee = { id: 'employee_1', role: 'OWNER', companyId: 'company_1' }
next()
}
return { requireCompanyAuth: auth, requireCompanyDocumentAuth: auth }
})
vi.mock('../../middleware/requireTenant', () => ({
requireTenant: (_req: any, _res: any, next: any) => next(),
}))
vi.mock('../../middleware/requireSubscription', () => ({
requireSubscription: (_req: any, _res: any, next: any) => next(),
}))
vi.mock('../../middleware/requireRole', () => ({
requireRole: () => (_req: any, _res: any, next: any) => next(),
}))
vi.mock('../../middleware/requireRenterAuth', () => ({
requireRenterAuth: (req: any, _res: any, next: any) => {
req.renterId = 'renter_1'
next()
},
optionalRenterAuth: (req: any, _res: any, next: any) => {
req.renterId = req.renterId ?? undefined
next()
},
}))
vi.mock('../../modules/offers/offer.service', () => ({
listOffers: vi.fn(),
getOffer: vi.fn(),
createOffer: vi.fn(),
updateOffer: vi.fn(),
deleteOffer: vi.fn(),
setOfferActive: vi.fn(),
getOfferStats: vi.fn(),
}))
vi.mock('../../modules/notifications/notification.service', () => ({
listCompany: vi.fn(),
listCompanyHistory: vi.fn(),
countUnread: vi.fn(),
markRead: vi.fn(),
markAllRead: vi.fn(),
getPreferences: vi.fn(),
setPreferences: vi.fn(),
listRenter: vi.fn(),
markRenterRead: vi.fn(),
markAllRenterRead: vi.fn(),
getRenterPreferences: vi.fn(),
setRenterPreferences: vi.fn(),
}))
vi.mock('../../modules/team/team.service', () => ({
getMembers: vi.fn(),
getMemberStats: vi.fn(),
inviteEmployee: vi.fn(),
updateEmployeeRole: vi.fn(),
deactivateEmployee: vi.fn(),
reactivateEmployee: vi.fn(),
removeEmployee: vi.fn(),
}))
import request from 'supertest'
import { createApp } from '../../app'
import * as offerService from '../../modules/offers/offer.service'
import * as notificationService from '../../modules/notifications/notification.service'
import * as teamService from '../../modules/team/team.service'
const app = createApp()
describe('operations API contracts', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(offerService.listOffers).mockResolvedValue([{ id: 'offer_1' }] as never)
vi.mocked(offerService.createOffer).mockResolvedValue({ id: 'offer_1' } as never)
vi.mocked(offerService.updateOffer).mockResolvedValue({ id: 'offer_1', title: 'Updated' } as never)
vi.mocked(offerService.getOfferStats).mockResolvedValue({ redemptions: 2 } as never)
vi.mocked(notificationService.countUnread).mockResolvedValue(7 as never)
vi.mocked(notificationService.listCompanyHistory).mockResolvedValue([{ id: 'history_1' }] as never)
vi.mocked(notificationService.getPreferences).mockResolvedValue([] as never)
vi.mocked(notificationService.listRenter).mockResolvedValue([{ id: 'renter_notification_1' }] as never)
vi.mocked(teamService.getMemberStats).mockResolvedValue({ total: 3, active: 2, pending: 1, inactive: 0 } as never)
vi.mocked(teamService.inviteEmployee).mockResolvedValue({ employee: { id: 'employee_2' }, invitationId: 'employee_2', invitedBy: 'employee_1' } as never)
})
it('GET /api/v1/offers forwards boolean-like query strings without the router inventing semantics', async () => {
const res = await request(app).get('/api/v1/offers?active=true&public=false')
expect(res.status).toBe(200)
expect(res.body).toEqual({ data: [{ id: 'offer_1' }] })
expect(offerService.listOffers).toHaveBeenCalledWith('company_1', { active: 'true', public: 'false' })
})
it('POST /api/v1/offers validates payloads and separates vehicleIds from offer body', async () => {
const payload = {
title: 'Long weekend deal',
type: 'PERCENTAGE',
discountValue: 15,
appliesToAll: false,
categories: ['SUV'],
validFrom: '2026-07-01T00:00:00.000Z',
validUntil: '2026-07-31T23:59:59.000Z',
vehicleIds: ['vehicle_1'],
}
const res = await request(app).post('/api/v1/offers').send(payload)
expect(res.status).toBe(201)
expect(res.body).toEqual({ data: { id: 'offer_1' } })
expect(offerService.createOffer).toHaveBeenCalledWith('company_1', expect.not.objectContaining({ vehicleIds: expect.anything() }), ['vehicle_1'])
})
it('PATCH /api/v1/offers/:id validates route params and delegates partial updates', async () => {
const res = await request(app).patch('/api/v1/offers/offer_1').send({ title: 'Updated', vehicleIds: [] })
expect(res.status).toBe(200)
expect(offerService.updateOffer).toHaveBeenCalledWith('offer_1', 'company_1', { title: 'Updated' }, [])
})
it('GET /api/v1/offers/:id/stats returns the service stats in the standard envelope', async () => {
const res = await request(app).get('/api/v1/offers/offer_1/stats')
expect(res.status).toBe(200)
expect(res.body).toEqual({ data: { redemptions: 2 } })
expect(offerService.getOfferStats).toHaveBeenCalledWith('offer_1', 'company_1')
})
it('GET /api/v1/notifications/unread-count exposes only the numeric unread count', async () => {
const res = await request(app).get('/api/v1/notifications/unread-count')
expect(res.status).toBe(200)
expect(res.body).toEqual({ data: { unread: 7 } })
})
it('GET /api/v1/notifications/history coerces limit and passes history filters to the service', async () => {
const res = await request(app).get('/api/v1/notifications/history?channel=EMAIL&status=FAILED&limit=25')
expect(res.status).toBe(200)
expect(notificationService.listCompanyHistory).toHaveBeenCalledWith('company_1', { channel: 'EMAIL', status: 'FAILED', limit: 25 })
})
it('PATCH /api/v1/notifications/company/preferences validates preference arrays for the current employee', async () => {
const prefs = [{ notificationType: 'BOOKING_CONFIRMED', channel: 'EMAIL', enabled: true }]
const res = await request(app).patch('/api/v1/notifications/company/preferences').send(prefs)
expect(res.status).toBe(200)
expect(res.body).toEqual({ data: { success: true } })
expect(notificationService.setPreferences).toHaveBeenCalledWith('employee_1', prefs)
})
it('GET /api/v1/notifications/renter uses renter identity instead of company tenant middleware', async () => {
const res = await request(app).get('/api/v1/notifications/renter')
expect(res.status).toBe(200)
expect(res.body).toEqual({ data: [{ id: 'renter_notification_1' }] })
expect(notificationService.listRenter).toHaveBeenCalledWith('renter_1')
})
it('GET /api/v1/team/stats returns computed team stats from the team facade', async () => {
const res = await request(app).get('/api/v1/team/stats')
expect(res.status).toBe(200)
expect(res.body).toEqual({ data: { total: 3, active: 2, pending: 1, inactive: 0 } })
})
it('POST /api/v1/team/invite validates invite payloads and wraps the created invitation', async () => {
const res = await request(app).post('/api/v1/team/invite').send({
firstName: 'Aya',
lastName: 'Benali',
email: 'aya@example.com',
role: 'AGENT',
})
expect(res.status).toBe(201)
expect(res.body).toEqual({ data: { data: { employee: { id: 'employee_2' }, invitationId: 'employee_2', invitedBy: 'employee_1' } } })
expect(teamService.inviteEmployee).toHaveBeenCalledWith('company_1', 'employee_1', {
firstName: 'Aya',
lastName: 'Benali',
email: 'aya@example.com',
role: 'AGENT',
})
})
})
+105
View File
@@ -0,0 +1,105 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({ prisma: {} }))
vi.mock('../../lib/redis', () => ({ redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() } }))
vi.mock('../../services/amanpayService', () => ({
isConfigured: vi.fn(),
verifyWebhookSignature: vi.fn(),
}))
vi.mock('../../services/paypalService', () => ({
isConfigured: vi.fn(),
verifyWebhookEvent: vi.fn(),
}))
vi.mock('../../modules/payments/payment.service', () => ({
handleAmanpayWebhook: vi.fn(),
handlePaypalWebhook: vi.fn(),
listByCompany: vi.fn(),
listByReservation: vi.fn(),
initCharge: vi.fn(),
capturePaypal: vi.fn(),
recordManualPayment: vi.fn(),
refundPayment: vi.fn(),
}))
import request from 'supertest'
import { createApp } from '../../app'
import * as amanpay from '../../services/amanpayService'
import * as paypal from '../../services/paypalService'
import * as service from '../../modules/payments/payment.service'
const app = createApp()
describe('payments API contract', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('rejects AmanPay webhooks when signature validation fails and does not invoke service handlers', async () => {
vi.mocked(amanpay.isConfigured).mockReturnValue(true)
vi.mocked(amanpay.verifyWebhookSignature).mockReturnValue(false)
const payload = { transaction_id: 'txn_1', status: 'PAID' }
const res = await request(app)
.post('/api/v1/payments/webhooks/amanpay')
.set('x-amanpay-signature', 'bad')
.send(payload)
expect(res.status).toBe(401)
expect(res.body).toEqual({ error: 'invalid_signature' })
expect(amanpay.verifyWebhookSignature).toHaveBeenCalledWith(JSON.stringify(payload), 'bad')
expect(service.handleAmanpayWebhook).not.toHaveBeenCalled()
})
it('accepts valid AmanPay webhooks and delegates the exact payload', async () => {
vi.mocked(amanpay.isConfigured).mockReturnValue(true)
vi.mocked(amanpay.verifyWebhookSignature).mockReturnValue(true)
const payload = { transaction_id: 'txn_1', status: 'PAID' }
const res = await request(app)
.post('/api/v1/payments/webhooks/amanpay')
.set('x-amanpay-signature', 'good')
.send(payload)
expect(res.status).toBe(200)
expect(res.body).toEqual({ received: true })
expect(service.handleAmanpayWebhook).toHaveBeenCalledWith(payload, JSON.stringify(payload))
})
it('rejects PayPal webhooks when provider verification fails', async () => {
vi.mocked(paypal.isConfigured).mockReturnValue(true)
vi.mocked(paypal.verifyWebhookEvent).mockResolvedValue(false)
const res = await request(app)
.post('/api/v1/payments/webhooks/paypal')
.set('paypal-transmission-id', 'transmission_1')
.send({ event_type: 'PAYMENT.CAPTURE.COMPLETED', resource: { id: 'capture_1' } })
expect(res.status).toBe(401)
expect(res.body).toEqual({ error: 'invalid_signature' })
expect(service.handlePaypalWebhook).not.toHaveBeenCalled()
})
it('accepts verified PayPal webhooks and delegates handling', async () => {
vi.mocked(paypal.isConfigured).mockReturnValue(true)
vi.mocked(paypal.verifyWebhookEvent).mockResolvedValue(true)
const payload = { event_type: 'PAYMENT.CAPTURE.COMPLETED', resource: { id: 'capture_1' } }
const res = await request(app)
.post('/api/v1/payments/webhooks/paypal')
.send(payload)
expect(res.status).toBe(200)
expect(res.body).toEqual({ received: true })
expect(service.handlePaypalWebhook).toHaveBeenCalledWith(payload, JSON.stringify(payload))
})
it('keeps authenticated payment routes behind auth before any payment service call', async () => {
const res = await request(app).get('/api/v1/payments/company')
expect(res.status).toBe(401)
expect(service.listByCompany).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,163 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({ prisma: {} }))
vi.mock('../../lib/redis', () => ({
redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() },
}))
vi.mock('../../middleware/requireRenterAuth', () => ({
requireRenterAuth: (req: any, _res: any, next: any) => {
req.renterId = 'renter_1'
next()
},
optionalRenterAuth: (_req: any, _res: any, next: any) => next(),
}))
vi.mock('../../modules/site/site.service', () => ({
getPlatformHomepage: vi.fn(),
getPlatformPricing: vi.fn(),
getBrand: vi.fn(),
getPublicVehicles: vi.fn(),
getVehicleDetail: vi.fn(),
getOffers: vi.fn(),
getBookingOptions: vi.fn(),
checkAvailability: vi.fn(),
validatePromoCode: vi.fn(),
createBooking: vi.fn(),
getBooking: vi.fn(),
initPayment: vi.fn(),
capturePaypal: vi.fn(),
handleContact: vi.fn(),
}))
vi.mock('../../modules/marketplace/marketplace.service', () => ({
getPublicOffers: vi.fn(),
getCities: vi.fn(),
getListedCompanies: vi.fn(),
searchVehicles: vi.fn(),
createMarketplaceReservation: vi.fn(),
getReviewContext: vi.fn(),
submitReview: vi.fn(),
validateOfferCode: vi.fn(),
getCompanyPage: vi.fn(),
getCompanyReviews: vi.fn(),
getCompanyVehicles: vi.fn(),
getVehicleDetail: vi.fn(),
getCompanyOffers: vi.fn(),
}))
import request from 'supertest'
import { createApp } from '../../app'
import * as siteService from '../../modules/site/site.service'
import * as marketplaceService from '../../modules/marketplace/marketplace.service'
const app = createApp()
describe('public validation API contracts', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(siteService.checkAvailability).mockResolvedValue({ available: true, nextAvailableAt: null } as never)
vi.mocked(siteService.initPayment).mockResolvedValue({ checkoutUrl: 'https://pay.example.test' } as never)
vi.mocked(siteService.handleContact).mockResolvedValue({ success: true } as never)
vi.mocked(marketplaceService.searchVehicles).mockResolvedValue({ data: [], pagination: { page: 1, pageSize: 20 } } as never)
vi.mocked(marketplaceService.createMarketplaceReservation).mockResolvedValue({ id: 'reservation_1' } as never)
vi.mocked(marketplaceService.submitReview).mockResolvedValue({ id: 'review_1' } as never)
})
it('rejects malformed public availability checks before site service execution', async () => {
const res = await request(app).post('/api/v1/site/atlas-cars/availability').send({
vehicleId: 'not-a-cuid',
startDate: '2026-07-01',
endDate: 'tomorrow-ish',
})
expect(res.status).toBe(400)
expect(res.body).toMatchObject({ error: 'validation_error', statusCode: 400 })
expect(siteService.checkAvailability).not.toHaveBeenCalled()
})
it('accepts valid public availability payloads and passes parsed fields explicitly', async () => {
const payload = {
vehicleId: 'ckvvehicle000000000000001',
startDate: '2026-07-01T10:00:00.000Z',
endDate: '2026-07-03T10:00:00.000Z',
}
const res = await request(app).post('/api/v1/site/atlas-cars/availability').send(payload)
expect(res.status).toBe(200)
expect(res.body).toEqual({ data: { available: true, nextAvailableAt: null } })
expect(siteService.checkAvailability).toHaveBeenCalledWith('atlas-cars', payload.vehicleId, payload.startDate, payload.endDate)
})
it('rejects unsupported public payment providers before payment initialization', async () => {
const res = await request(app).post('/api/v1/site/atlas-cars/booking/reservation_1/pay').send({
provider: 'STRIPE',
successUrl: 'https://example.test/success',
failureUrl: 'https://example.test/fail',
accessToken: 'booking-access-token-123',
})
expect(res.status).toBe(400)
expect(siteService.initPayment).not.toHaveBeenCalled()
})
it('defaults site payment currency to MAD for valid public payment requests', async () => {
const res = await request(app).post('/api/v1/site/atlas-cars/booking/reservation_1/pay').send({
provider: 'PAYPAL',
successUrl: 'https://example.test/success',
failureUrl: 'https://example.test/fail',
accessToken: 'booking-access-token-123',
})
expect(res.status).toBe(200)
expect(siteService.initPayment).toHaveBeenCalledWith('atlas-cars', 'reservation_1', {
provider: 'PAYPAL',
currency: 'MAD',
successUrl: 'https://example.test/success',
failureUrl: 'https://example.test/fail',
accessToken: 'booking-access-token-123',
})
})
it('coerces marketplace search pagination and price filters', async () => {
const res = await request(app).get('/api/v1/marketplace/search?city=Rabat&maxPrice=500&page=2&pageSize=30')
expect(res.status).toBe(200)
expect(marketplaceService.searchVehicles).toHaveBeenCalledWith({
city: 'Rabat',
maxPrice: 500,
page: 2,
pageSize: 30,
})
})
it('rejects oversized marketplace pagination before search execution', async () => {
const res = await request(app).get('/api/v1/marketplace/search?pageSize=500')
expect(res.status).toBe(400)
expect(marketplaceService.searchVehicles).not.toHaveBeenCalled()
})
it('rejects marketplace reservations with invalid public contact data before service execution', async () => {
const res = await request(app).post('/api/v1/marketplace/reservations').send({
vehicleId: 'ckvvehicle000000000000001',
companySlug: 'atlas-cars',
firstName: 'Aya',
lastName: 'Benali',
email: 'bad-email',
startDate: '2026-07-01T10:00:00.000Z',
endDate: '2026-07-03T10:00:00.000Z',
})
expect(res.status).toBe(400)
expect(marketplaceService.createMarketplaceReservation).not.toHaveBeenCalled()
})
it('rejects invalid public review ratings before creating reviews', async () => {
const res = await request(app).post('/api/v1/marketplace/review/token_1').send({ overallRating: 6 })
expect(res.status).toBe(400)
expect(marketplaceService.submitReview).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,90 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({ prisma: {} }))
vi.mock('../../lib/redis', () => ({ redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() } }))
vi.mock('../../modules/site/site.service', () => ({
getPlatformHomepage: vi.fn(),
getPlatformPricing: vi.fn(),
getBrand: vi.fn(),
getPublicVehicles: vi.fn(),
getVehicleDetail: vi.fn(),
getOffers: vi.fn(),
getBookingOptions: vi.fn(),
checkAvailability: vi.fn(),
validatePromoCode: vi.fn(),
createBooking: vi.fn(),
getBooking: vi.fn(),
initPayment: vi.fn(),
capturePaypal: vi.fn(),
handleContact: vi.fn(),
}))
import request from 'supertest'
import { createApp } from '../../app'
import * as siteService from '../../modules/site/site.service'
const app = createApp()
describe('site and webhook API boundary contracts', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(siteService.getPlatformPricing).mockResolvedValue({ prices: { STARTER: { MONTHLY: { MAD: 199 } } }, planFeatures: { STARTER: ['feature'] } } as never)
vi.mocked(siteService.checkAvailability).mockResolvedValue({ available: true, nextAvailableAt: null } as never)
vi.mocked(siteService.validatePromoCode).mockResolvedValue({ id: 'offer_1', promoCode: 'SUMMER' } as never)
vi.mocked(siteService.handleContact).mockResolvedValue({ success: true, deliveredTo: 'hello@example.test' } as never)
})
it('keeps removed Clerk webhook endpoint disabled even with raw webhook payloads', async () => {
const res = await request(app)
.post('/api/v1/webhooks/clerk')
.set('content-type', 'application/json')
.send(JSON.stringify({ type: 'user.created', data: { id: 'user_1' } }))
expect(res.status).toBe(410)
expect(res.body).toEqual({
error: 'disabled',
message: 'Clerk webhooks are disabled because Clerk has been removed from this project.',
statusCode: 410,
})
})
it('serves platform pricing without tenant auth', async () => {
const res = await request(app).get('/api/v1/site/platform/pricing')
expect(res.status).toBe(200)
expect(res.body.data.prices.STARTER.MONTHLY.MAD).toBe(199)
expect(siteService.getPlatformPricing).toHaveBeenCalledWith()
})
it('rejects invalid availability ranges before service execution', async () => {
const res = await request(app).post('/api/v1/site/atlas/availability').send({
vehicleId: 'vehicle_1',
startDate: 'not-a-date',
endDate: '2026-07-10T00:00:00.000Z',
})
expect(res.status).toBe(400)
expect(siteService.checkAvailability).not.toHaveBeenCalled()
})
it('normalizes valid availability payloads into service arguments', async () => {
const res = await request(app).post('/api/v1/site/atlas/availability').send({
vehicleId: 'cmvehicle000000000000001',
startDate: '2026-07-01T00:00:00.000Z',
endDate: '2026-07-10T00:00:00.000Z',
})
expect(res.status).toBe(200)
expect(siteService.checkAvailability).toHaveBeenCalledWith('atlas', 'cmvehicle000000000000001', '2026-07-01T00:00:00.000Z', '2026-07-10T00:00:00.000Z')
})
it('rejects empty promo codes and malformed contact emails before public service calls', async () => {
const promo = await request(app).post('/api/v1/site/atlas/book/validate-code').send({ code: '' })
expect(promo.status).toBe(400)
expect(siteService.validatePromoCode).not.toHaveBeenCalled()
const contact = await request(app).post('/api/v1/site/atlas/contact').send({ name: 'Visitor', email: 'bad-email', message: 'Hello' })
expect(contact.status).toBe(400)
expect(siteService.handleContact).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,126 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({ prisma: {} }))
vi.mock('../../lib/redis', () => ({
redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() },
}))
vi.mock('../../middleware/requireCompanyAuth', () => ({
requireCompanyDocumentAuth: (_req: any, _res: any, next: any) => next(),
requireCompanyAuth: (req: any, _res: any, next: any) => {
req.employee = { id: 'employee_owner', role: 'OWNER', firstName: 'Aya', lastName: 'Owner' }
next()
},
}))
vi.mock('../../middleware/requireTenant', () => ({
requireTenant: (req: any, _res: any, next: any) => {
req.companyId = 'company_1'
next()
},
}))
vi.mock('../../middleware/requireSubscription', () => ({ requireSubscription: (_req: any, _res: any, next: any) => next() }))
vi.mock('../../middleware/requireRole', () => ({ requireRole: () => (_req: any, _res: any, next: any) => next() }))
vi.mock('../../modules/subscriptions/subscription.service', () => ({
getPlans: vi.fn(),
getProviders: vi.fn(),
getPlanFeatures: vi.fn(),
getSubscription: vi.fn(),
getInvoices: vi.fn(),
getEvents: vi.fn(),
getEntitlement: vi.fn(),
startTrial: vi.fn(),
checkout: vi.fn(),
reactivate: vi.fn(),
changePlan: vi.fn(),
cancel: vi.fn(),
resume: vi.fn(),
capturePaypal: vi.fn(),
handleAmanpayWebhook: vi.fn(),
handlePaypalWebhook: vi.fn(),
}))
vi.mock('../../modules/team/team.service', () => ({
getMembers: vi.fn(),
getMemberStats: vi.fn(),
inviteEmployee: vi.fn(),
updateEmployeeRole: vi.fn(),
deactivateEmployee: vi.fn(),
reactivateEmployee: vi.fn(),
removeEmployee: vi.fn(),
}))
import request from 'supertest'
import { createApp } from '../../app'
import * as subscriptionService from '../../modules/subscriptions/subscription.service'
import * as teamService from '../../modules/team/team.service'
const app = createApp()
describe('subscription and team API validation contracts', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(subscriptionService.startTrial).mockResolvedValue({ id: 'sub_trial' } as never)
vi.mocked(subscriptionService.checkout).mockResolvedValue({ invoice: { id: 'invoice_1' }, checkoutUrl: 'https://pay.example.test' } as never)
vi.mocked(subscriptionService.cancel).mockResolvedValue({ id: 'sub_1', cancelAtPeriodEnd: true } as never)
vi.mocked(subscriptionService.capturePaypal).mockResolvedValue({ success: true } as never)
vi.mocked(teamService.inviteEmployee).mockResolvedValue({ id: 'employee_2' } as never)
vi.mocked(teamService.updateEmployeeRole).mockResolvedValue({ id: 'employee_2', role: 'MANAGER' } as never)
})
it('defaults trial currency and forwards company context', async () => {
const res = await request(app).post('/api/v1/subscriptions/trial').send({ plan: 'STARTER', billingPeriod: 'MONTHLY' })
expect(res.status).toBe(200)
expect(subscriptionService.startTrial).toHaveBeenCalledWith('company_1', 'STARTER', 'MONTHLY', 'MAD')
})
it('rejects checkout payloads with unsupported currency before service execution', async () => {
const res = await request(app).post('/api/v1/subscriptions/checkout').send({
plan: 'PRO',
billingPeriod: 'ANNUAL',
currency: 'USD',
provider: 'PAYPAL',
successUrl: 'https://app.example.test/success',
failureUrl: 'https://app.example.test/failure',
})
expect(res.status).toBe(400)
expect(subscriptionService.checkout).not.toHaveBeenCalled()
})
it('defaults cancellation mode at the route boundary', async () => {
const res = await request(app).post('/api/v1/subscriptions/cancel').send({ reason: 'not needed' })
expect(res.status).toBe(200)
expect(subscriptionService.cancel).toHaveBeenCalledWith('company_1', 'period_end', 'not needed')
})
it('requires PayPal order id for capture before service execution', async () => {
const res = await request(app).post('/api/v1/subscriptions/capture-paypal').send({})
expect(res.status).toBe(400)
expect(subscriptionService.capturePaypal).not.toHaveBeenCalled()
})
it('normalizes team invites through validation before service execution', async () => {
const res = await request(app).post('/api/v1/team/invite').send({
firstName: 'Aya',
lastName: 'Agent',
email: 'agent@example.test',
role: 'AGENT',
})
expect(res.status).toBe(201)
expect(teamService.inviteEmployee).toHaveBeenCalledWith('company_1', 'employee_owner', {
firstName: 'Aya',
lastName: 'Agent',
email: 'agent@example.test',
role: 'AGENT',
})
})
it('rejects owner role assignment before team service execution', async () => {
const res = await request(app).patch('/api/v1/team/employee_2/role').send({ role: 'OWNER' })
expect(res.status).toBe(400)
expect(teamService.updateEmployeeRole).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,105 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({
prisma: {},
}))
vi.mock('../../lib/redis', () => ({
redis: {
on: vi.fn(),
get: vi.fn(),
set: vi.fn(),
del: vi.fn(),
quit: vi.fn(),
},
}))
vi.mock('../../services/amanpayService', () => ({
isConfigured: vi.fn(),
verifyWebhookSignature: vi.fn(),
}))
vi.mock('../../services/paypalService', () => ({
isConfigured: vi.fn(),
verifyWebhookEvent: vi.fn(),
}))
vi.mock('../../modules/subscriptions/subscription.service', () => ({
getPlans: vi.fn(),
getProviders: vi.fn(),
getPlanFeatures: vi.fn(),
handleAmanpayWebhook: vi.fn(),
handlePaypalWebhook: vi.fn(),
}))
import request from 'supertest'
import { createApp } from '../../app'
import * as amanpay from '../../services/amanpayService'
import * as paypal from '../../services/paypalService'
import * as service from '../../modules/subscriptions/subscription.service'
const app = createApp()
describe('subscriptions public API', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(service.getProviders).mockReturnValue({ amanpay: false, paypal: true })
vi.mocked(service.getPlans).mockResolvedValue({ STARTER: { MONTHLY: { MAD: 9900 } } } as never)
vi.mocked(service.getPlanFeatures).mockResolvedValue([
{ id: 'feature_1', plan: 'STARTER', label: 'Vehicles', sortOrder: 1 },
] as never)
})
it('GET /api/v1/subscriptions/providers exposes configured payment provider availability', async () => {
const res = await request(app).get('/api/v1/subscriptions/providers')
expect(res.status).toBe(200)
expect(res.body).toEqual({ data: { amanpay: false, paypal: true } })
expect(service.getProviders).toHaveBeenCalledOnce()
})
it('GET /api/v1/subscriptions/plans wraps plan pricing in the standard API envelope', async () => {
const res = await request(app).get('/api/v1/subscriptions/plans')
expect(res.status).toBe(200)
expect(res.body).toEqual({ data: { STARTER: { MONTHLY: { MAD: 9900 } } } })
expect(service.getPlans).toHaveBeenCalledOnce()
})
it('GET /api/v1/subscriptions/features returns plan feature rows through the public router', async () => {
const res = await request(app).get('/api/v1/subscriptions/features')
expect(res.status).toBe(200)
expect(res.body.data).toEqual([
{ id: 'feature_1', plan: 'STARTER', label: 'Vehicles', sortOrder: 1 },
])
})
it('rejects AmanPay webhooks when provider config or signature validation fails', async () => {
vi.mocked(amanpay.isConfigured).mockReturnValue(true)
vi.mocked(amanpay.verifyWebhookSignature).mockReturnValue(false)
const res = await request(app)
.post('/api/v1/subscriptions/webhooks/amanpay')
.set('x-amanpay-signature', 'bad-signature')
.send({ id: 'txn_1', status: 'PAID' })
expect(res.status).toBe(401)
expect(res.body).toEqual({ error: 'invalid_signature' })
expect(service.handleAmanpayWebhook).not.toHaveBeenCalled()
})
it('accepts verified PayPal webhooks and delegates handling to the subscription service', async () => {
vi.mocked(paypal.isConfigured).mockReturnValue(true)
vi.mocked(paypal.verifyWebhookEvent).mockResolvedValue(true)
const payload = { event_type: 'PAYMENT.CAPTURE.COMPLETED', resource: { id: 'capture_1' } }
const res = await request(app)
.post('/api/v1/subscriptions/webhooks/paypal')
.send(payload)
expect(res.status).toBe(200)
expect(res.body).toEqual({ received: true })
expect(service.handlePaypalWebhook).toHaveBeenCalledWith(payload, JSON.stringify(payload))
})
})
@@ -0,0 +1,143 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('jsonwebtoken', () => ({ default: { verify: vi.fn() } }))
vi.mock('../../lib/redis', () => ({
redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() },
}))
vi.mock('../../lib/prisma', () => ({
prisma: {
employee: { findUnique: vi.fn() },
company: { findUnique: vi.fn() },
},
}))
vi.mock('../../modules/vehicles/vehicle.service', () => ({
listVehicles: vi.fn(),
getVehicle: vi.fn(),
createVehicle: vi.fn(),
updateVehicle: vi.fn(),
deleteVehicle: vi.fn(),
uploadPhotos: vi.fn(),
deletePhoto: vi.fn(),
setStatus: vi.fn(),
setPublished: vi.fn(),
checkAvailability: vi.fn(),
getCalendar: vi.fn(),
createCalendarBlock: vi.fn(),
deleteCalendarBlock: vi.fn(),
getMaintenanceLogs: vi.fn(),
createMaintenanceLog: vi.fn(),
getVehiclePricing: vi.fn(),
updateVehiclePricing: vi.fn(),
createVehiclePricingRule: vi.fn(),
updateVehiclePricingRule: vi.fn(),
deleteVehiclePricingRule: vi.fn(),
}))
import request from 'supertest'
import jwt from 'jsonwebtoken'
import { createApp } from '../../app'
import { prisma } from '../../lib/prisma'
import * as vehicleService from '../../modules/vehicles/vehicle.service'
const app = createApp()
function employee(role = 'MANAGER') {
return {
id: 'employee_1',
role,
isActive: true,
companyId: 'company_1',
company: { id: 'company_1', status: 'ACTIVE' },
}
}
beforeEach(() => {
vi.clearAllMocks()
process.env.JWT_SECRET = 'test-secret'
vi.mocked(jwt.verify).mockReturnValue({ sub: 'employee_1', type: 'employee' } as never)
vi.mocked(prisma.employee.findUnique).mockResolvedValue(employee() as never)
vi.mocked(prisma.company.findUnique).mockResolvedValue({ id: 'company_1', status: 'ACTIVE' } as never)
})
describe('vehicle pricing API contracts', () => {
it('allows active tenants to read vehicle pricing', async () => {
vi.mocked(vehicleService.getVehiclePricing).mockResolvedValue({
configuration: { id: 'pricing_config_1', baseDailyRate: 4000 },
rules: [],
history: [],
preview: { today: { dailyRate: 4000 }, next14Days: [] },
} as never)
const res = await request(app)
.get('/api/v1/vehicles/vehicle_1/pricing')
.set('Authorization', 'Bearer employee-token')
expect(res.status).toBe(200)
expect(res.body.data.configuration.id).toBe('pricing_config_1')
expect(vehicleService.getVehiclePricing).toHaveBeenCalledWith('vehicle_1', 'company_1')
})
it('rejects invalid pricing config payloads before updating pricing', async () => {
const res = await request(app)
.put('/api/v1/vehicles/vehicle_1/pricing')
.set('Authorization', 'Bearer employee-token')
.send({
pricingMode: 'CHAOS',
baseDailyRate: -1,
maxDailyPriceMovementPct: 101,
})
expect(res.status).toBe(400)
expect(vehicleService.updateVehiclePricing).not.toHaveBeenCalled()
})
it('passes parsed pricing configuration and employee identity to the service', async () => {
vi.mocked(vehicleService.updateVehiclePricing).mockResolvedValue({ configuration: { id: 'pricing_config_1' }, rules: [], history: [], preview: {} } as never)
const payload = {
pricingMode: 'AUTOMATIC',
baseDailyRate: 4500,
weeklyRate: 27000,
weekendRate: null,
maxDailyPriceMovementPct: 15,
automaticPricingEnabled: true,
approvalRequired: true,
note: 'Demand change',
}
const res = await request(app)
.put('/api/v1/vehicles/vehicle_1/pricing')
.set('Authorization', 'Bearer employee-token')
.send(payload)
expect(res.status).toBe(200)
expect(vehicleService.updateVehiclePricing).toHaveBeenCalledWith('vehicle_1', 'company_1', 'employee_1', expect.objectContaining(payload))
})
it('rejects pricing rules with malformed dates before service execution', async () => {
const res = await request(app)
.post('/api/v1/vehicles/vehicle_1/pricing/rules')
.set('Authorization', 'Bearer employee-token')
.send({
name: 'Holiday',
ruleType: 'HOLIDAY',
startDate: 'July first-ish',
endDate: '2026-07-10',
dailyRate: 6000,
})
expect(res.status).toBe(400)
expect(vehicleService.createVehiclePricingRule).not.toHaveBeenCalled()
})
it('requires manager access for pricing mutations', async () => {
vi.mocked(prisma.employee.findUnique).mockResolvedValue(employee('AGENT') as never)
const res = await request(app)
.delete('/api/v1/vehicles/vehicle_1/pricing/rules/rule_1')
.set('Authorization', 'Bearer employee-token')
expect(res.status).toBe(403)
expect(vehicleService.deleteVehiclePricingRule).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,41 @@
import { describe, expect, it, vi } from 'vitest'
vi.mock('jsonwebtoken', () => ({ default: { verify: vi.fn(() => ({ sub: 'admin_1', type: 'admin' })) } }))
vi.mock('../../lib/redis', () => ({
redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() },
}))
vi.mock('../../lib/prisma', () => ({
prisma: {
adminUser: { findUnique: vi.fn().mockResolvedValue({ id: 'admin_1', role: 'FINANCE', isActive: true, totpEnabled: true }) },
},
}))
vi.mock('../../modules/admin/admin.service', () => ({
getBilling: vi.fn(),
getInvoicePdf: vi.fn(),
updateBillingAccount: vi.fn(),
createBillingInvoice: vi.fn(),
finalizeBillingInvoice: vi.fn(),
}))
import request from 'supertest'
import { createApp } from '../../app'
import * as adminService from '../../modules/admin/admin.service'
process.env.JWT_SECRET = 'test-secret'
const app = createApp()
describe('admin billing e2e boundaries', () => {
it('rejects anonymous billing access and malformed invoice creation without touching services', async () => {
const anonymous = await request(app).get('/api/v1/admin/billing')
expect(anonymous.status).toBe(401)
expect(adminService.getBilling).not.toHaveBeenCalled()
const malformed = await request(app)
.post('/api/v1/admin/billing/accounts/billing_account_1/invoices')
.set('Authorization', 'Bearer admin-token')
.send({ invoiceType: 'MADE_UP', lineItems: [{ quantity: 0, unitAmount: 100 }] })
expect(malformed.status).toBe(400)
expect(adminService.createBillingInvoice).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,44 @@
import { describe, expect, it, vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({ prisma: {} }))
vi.mock('../../lib/redis', () => ({
redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() },
}))
vi.mock('../../middleware/requireAdminAuth', () => ({
requireAdminAuth: (req: any, _res: any, next: any) => {
req.admin = { id: 'admin_1', role: 'VIEWER', isActive: true }
next()
},
requireAdminRole: () => (_req: any, res: any) => res.status(403).json({ code: 'forbidden' }),
requireFreshAdmin2FA: (_req: any, _res: any, next: any) => next(),
}))
vi.mock('../../modules/menu/menu.service', () => ({
listMenuItems: vi.fn(),
createMenuItem: vi.fn(),
getMenuItem: vi.fn(),
updateMenuItem: vi.fn(),
setMenuItemStatus: vi.fn(),
deleteMenuItem: vi.fn(),
assignMenuItemToPlans: vi.fn(),
removeMenuItemFromPlan: vi.fn(),
assignMenuItemToCompanies: vi.fn(),
removeMenuItemFromCompany: vi.fn(),
previewCompanyMenu: vi.fn(),
listMenuAuditLogs: vi.fn(),
}))
import request from 'supertest'
import { createApp } from '../../app'
const app = createApp()
describe('admin menu e2e boundary smoke', () => {
it('keeps menu administration behind admin role checks', async () => {
const res = await request(app)
.post('/api/v1/admin/menu-items')
.send({ label: 'Fleet', itemType: 'INTERNAL_PAGE', routeOrUrl: '/fleet' })
expect(res.status).toBe(403)
expect(res.body).toMatchObject({ code: 'forbidden' })
})
})
@@ -0,0 +1,51 @@
import { vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({
prisma: {},
}))
vi.mock('../../lib/redis', () => ({
redis: {
on: vi.fn(),
get: vi.fn(),
set: vi.fn(),
del: vi.fn(),
quit: vi.fn(),
},
}))
import request from 'supertest'
import { describe, expect, it } from 'vitest'
import { createApp } from '../../app'
const app = createApp()
describe('API smoke e2e', () => {
it('serves health, API index, machine docs, OpenAPI, and Swagger UI in one client flow', async () => {
const health = await request(app).get('/health')
expect(health.status).toBe(200)
expect(health.body.status).toBe('ok')
const index = await request(app).get('/api/v1')
expect(index.status).toBe(200)
expect(index.body.docs).toBe('/api/v1/docs')
expect(index.body.openapi).toBeUndefined()
expect(index.body.openApi).toBe('/api/v1/openapi.json')
const docs = await request(app).get(index.body.docs)
expect(docs.status).toBe(200)
expect(docs.body.routes.length).toBeGreaterThan(5)
const openApi = await request(app).get(docs.body.openApi)
expect(openApi.status).toBe(200)
expect(openApi.body.info).toEqual(expect.objectContaining({ title: expect.any(String) }))
const swaggerRedirect = await request(app).get(docs.body.swaggerUi)
expect(swaggerRedirect.status).toBe(301)
expect(swaggerRedirect.headers.location).toBe('/docs/')
const swagger = await request(app).get('/docs/')
expect(swagger.status).toBe(200)
expect(swagger.headers['content-type']).toContain('text/html')
})
})
@@ -0,0 +1,24 @@
import { vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({ prisma: {} }))
vi.mock('../../lib/redis', () => ({
redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() },
}))
import request from 'supertest'
import { describe, expect, it } from 'vitest'
import { createApp } from '../../app'
const app = createApp()
describe('public auth disabled e2e smoke', () => {
it('documents the public renter auth product boundary through real routing and error middleware', async () => {
const signup = await request(app).post('/api/v1/auth/renter/signup').send({})
const login = await request(app).post('/api/v1/auth/renter/login').send({})
expect(signup.status).toBe(403)
expect(signup.body.error).toBe('renter_signup_disabled')
expect(login.status).toBe(403)
expect(login.body.error).toBe('renter_login_disabled')
})
})
@@ -0,0 +1,25 @@
import { describe, expect, it, vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({
prisma: {
employee: { findUnique: vi.fn() },
company: { findUnique: vi.fn() },
},
}))
vi.mock('../../lib/redis', () => ({
redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() },
}))
import request from 'supertest'
import { createApp } from '../../app'
const app = createApp()
describe('company configuration protected boundaries', () => {
it('does not expose company brand settings without company authentication', async () => {
const res = await request(app).get('/api/v1/companies/me/brand')
expect(res.status).toBe(401)
expect(res.body.error).toBe('unauthenticated')
})
})
@@ -0,0 +1,47 @@
import { describe, expect, it, vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({ prisma: {} }))
vi.mock('../../lib/redis', () => ({ redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() } }))
vi.mock('../../middleware/requireCompanyAuth', () => ({
requireCompanyAuth: (req: any, _res: any, next: any) => {
req.employee = { id: 'employee_1', firstName: 'Aya', lastName: 'Manager', role: 'MANAGER' }
next()
},
requireCompanyDocumentAuth: (req: any, _res: any, next: any) => {
req.employee = { id: 'employee_1', firstName: 'Aya', lastName: 'Manager', role: 'MANAGER' }
next()
},
}))
vi.mock('../../middleware/requireTenant', () => ({ requireTenant: (req: any, _res: any, next: any) => { req.companyId = 'company_1'; next() } }))
vi.mock('../../middleware/requireSubscription', () => ({ requireSubscription: (_req: any, _res: any, next: any) => next() }))
vi.mock('../../middleware/requireRole', () => ({ requireRole: () => (_req: any, _res: any, next: any) => next() }))
vi.mock('../../modules/customers/customer.service', () => ({
createCustomer: vi.fn(),
approveLicense: vi.fn(),
listCustomers: vi.fn(),
getCustomer: vi.fn(),
updateCustomer: vi.fn(),
flagCustomer: vi.fn(),
unflagCustomer: vi.fn(),
validateCustomerLicense: vi.fn(),
uploadLicenseImage: vi.fn(),
getLicenseImageFile: vi.fn(),
}))
import request from 'supertest'
import { createApp } from '../../app'
import * as customerService from '../../modules/customers/customer.service'
const app = createApp()
describe('customer boundary e2e smoke', () => {
it('rejects malformed customer and license approval payloads before mocked services run', async () => {
const badCustomer = await request(app).post('/api/v1/customers').send({ firstName: '', lastName: 'Haddad', email: 'aya@example.test' })
expect(badCustomer.status).toBe(400)
expect(customerService.createCustomer).not.toHaveBeenCalled()
const badApproval = await request(app).post('/api/v1/customers/customer_1/approve-license').send({ decision: 'APPROVED_BUT_NOT_THE_ENUM' })
expect(badApproval.status).toBe(400)
expect(customerService.approveLicense).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,27 @@
import { describe, expect, it, vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({ prisma: {} }))
vi.mock('../../lib/redis', () => ({
redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() },
}))
import request from 'supertest'
import { createApp } from '../../app'
const app = createApp()
describe('employee and marketplace public boundary smoke', () => {
it('rejects malformed employee login payloads without leaking internals', async () => {
const res = await request(app).post('/api/v1/auth/employee/login').send({ email: 'not-an-email', password: 'x' })
expect(res.status).toBe(400)
expect(JSON.stringify(res.body)).not.toContain('stack')
})
it('rejects invalid public review payloads without executing private infrastructure', async () => {
const res = await request(app).post('/api/v1/marketplace/review/token_123').send({ overallRating: 0 })
expect(res.status).toBe(400)
expect(JSON.stringify(res.body)).not.toContain('Prisma')
})
})
@@ -0,0 +1,23 @@
import { describe, expect, it, vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({ prisma: {} }))
vi.mock('../../lib/redis', () => ({
redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() },
}))
import request from 'supertest'
import { createApp } from '../../app'
const app = createApp()
describe('feedback and offer unauthenticated smoke boundaries', () => {
it('keeps company feedback and offer management routes behind authentication', async () => {
const reviewReply = await request(app).patch('/api/v1/reviews/review_1/reply').send({ companyReply: 'Thanks' })
const complaintCreate = await request(app).post('/api/v1/complaints').send({ category: 'BILLING', subject: 'Charge issue' })
const offerCreate = await request(app).post('/api/v1/offers').send({})
expect(reviewReply.status).toBe(401)
expect(complaintCreate.status).toBe(401)
expect(offerCreate.status).toBe(401)
})
})
@@ -0,0 +1,23 @@
import { describe, expect, it, vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({ prisma: { employee: { findUnique: vi.fn() }, renter: { findUnique: vi.fn() } } }))
vi.mock('../../lib/redis', () => ({
redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() },
}))
import request from 'supertest'
import { createApp } from '../../app'
const app = createApp()
describe('operations auth-boundary e2e smoke', () => {
it('keeps operational company routes behind auth while leaving the health endpoint public', async () => {
const health = await request(app).get('/health')
expect(health.status).toBe(200)
for (const path of ['/api/v1/offers', '/api/v1/team/stats', '/api/v1/notifications/company', '/api/v1/notifications/unread-count']) {
const res = await request(app).get(path)
expect(res.status).toBe(401)
}
})
})
@@ -0,0 +1,53 @@
import { describe, expect, it, vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({ prisma: {} }))
vi.mock('../../lib/redis', () => ({
redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() },
}))
vi.mock('../../middleware/requireCompanyAuth', () => ({
requireCompanyAuth: (req: any, _res: any, next: any) => {
req.employee = { id: 'employee_1', role: 'OWNER' }
next()
},
requireCompanyDocumentAuth: (req: any, _res: any, next: any) => {
req.employee = { id: 'employee_1', role: 'OWNER' }
next()
},
}))
vi.mock('../../middleware/requireTenant', () => ({
requireTenant: (req: any, _res: any, next: any) => {
req.companyId = 'company_1'
next()
},
}))
vi.mock('../../middleware/requireSubscription', () => ({ requireSubscription: (_req: any, _res: any, next: any) => next() }))
vi.mock('../../middleware/requireRole', () => ({ requireRole: () => (_req: any, _res: any, next: any) => next() }))
vi.mock('../../modules/team/team.service', () => ({ inviteEmployee: vi.fn(), updateEmployeeRole: vi.fn(), getMembers: vi.fn(), getMemberStats: vi.fn() }))
vi.mock('../../modules/notifications/notification.service', () => ({ setPreferences: vi.fn(), getPreferences: vi.fn(), listCompany: vi.fn(), countUnread: vi.fn() }))
vi.mock('../../modules/analytics/analytics.service', () => ({ getReport: vi.fn(), getSummary: vi.fn(), getDashboard: vi.fn(), getSources: vi.fn() }))
import request from 'supertest'
import { createApp } from '../../app'
import * as teamService from '../../modules/team/team.service'
import * as notificationService from '../../modules/notifications/notification.service'
const app = createApp()
describe('operations validation e2e smoke', () => {
it('rejects malformed team and notification payloads before mocked services run', async () => {
const badInvite = await request(app).post('/api/v1/team/invite').send({
firstName: 'Aya',
lastName: 'Benali',
email: 'not-email',
role: 'AGENT',
})
expect(badInvite.status).toBe(400)
expect(teamService.inviteEmployee).not.toHaveBeenCalled()
const badPrefs = await request(app).patch('/api/v1/notifications/company/preferences').send([
{ notificationType: 'BOOKING_CREATED', channel: 'EMAIL', enabled: 'yes-human-chaos' },
])
expect(badPrefs.status).toBe(400)
expect(notificationService.setPreferences).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,29 @@
import { vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({ prisma: {} }))
vi.mock('../../lib/redis', () => ({ redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() } }))
vi.mock('../../services/amanpayService', () => ({
isConfigured: vi.fn().mockReturnValue(false),
verifyWebhookSignature: vi.fn().mockReturnValue(false),
}))
vi.mock('../../services/paypalService', () => ({
isConfigured: vi.fn().mockReturnValue(false),
verifyWebhookEvent: vi.fn().mockResolvedValue(false),
}))
import { describe, expect, it } from 'vitest'
import request from 'supertest'
import { createApp } from '../../app'
const app = createApp()
describe('payments webhook public e2e smoke', () => {
it('does not process anonymous AmanPay webhook payloads without a valid provider configuration/signature', async () => {
const res = await request(app)
.post('/api/v1/payments/webhooks/amanpay')
.send({ transaction_id: 'untrusted_txn', status: 'PAID' })
expect(res.status).toBe(401)
expect(res.body).toEqual({ error: 'invalid_signature' })
})
})
@@ -0,0 +1,33 @@
import { vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({
prisma: {
employee: { findUnique: vi.fn() },
company: { findUnique: vi.fn() },
adminUser: { findUnique: vi.fn() },
},
}))
vi.mock('../../lib/redis', () => ({
redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() },
}))
import request from 'supertest'
import { describe, expect, it } from 'vitest'
import { createApp } from '../../app'
const app = createApp()
describe('protected auth boundaries e2e smoke', () => {
it('keeps company, tenant, and admin protected surfaces closed without credentials', async () => {
const vehicles = await request(app).get('/api/v1/vehicles')
const team = await request(app).get('/api/v1/team')
const admin = await request(app).get('/api/v1/admin/companies')
expect(vehicles.status).toBe(401)
expect(vehicles.body.error).toBe('unauthenticated')
expect(team.status).toBe(401)
expect(team.body.error).toBe('unauthenticated')
expect(admin.status).toBe(401)
expect(admin.body.error).toBe('unauthenticated')
})
})
@@ -0,0 +1,68 @@
import { describe, expect, it, vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({ prisma: {} }))
vi.mock('../../lib/redis', () => ({
redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() },
}))
vi.mock('../../middleware/requireRenterAuth', () => ({
requireRenterAuth: (_req: any, _res: any, next: any) => next(),
optionalRenterAuth: (_req: any, _res: any, next: any) => next(),
}))
vi.mock('../../modules/site/site.service', () => ({
getPlatformHomepage: vi.fn(),
getPlatformPricing: vi.fn(),
getBrand: vi.fn(),
getPublicVehicles: vi.fn(),
getVehicleDetail: vi.fn(),
getOffers: vi.fn(),
getBookingOptions: vi.fn(),
checkAvailability: vi.fn(),
validatePromoCode: vi.fn(),
createBooking: vi.fn(),
getBooking: vi.fn(),
initPayment: vi.fn(),
capturePaypal: vi.fn(),
handleContact: vi.fn(),
}))
vi.mock('../../modules/marketplace/marketplace.service', () => ({
getPublicOffers: vi.fn(),
getCities: vi.fn(),
getListedCompanies: vi.fn(),
searchVehicles: vi.fn(),
createMarketplaceReservation: vi.fn(),
getReviewContext: vi.fn(),
submitReview: vi.fn(),
validateOfferCode: vi.fn(),
getCompanyPage: vi.fn(),
getCompanyReviews: vi.fn(),
getCompanyVehicles: vi.fn(),
getVehicleDetail: vi.fn(),
getCompanyOffers: vi.fn(),
}))
import request from 'supertest'
import { createApp } from '../../app'
import * as siteService from '../../modules/site/site.service'
import * as marketplaceService from '../../modules/marketplace/marketplace.service'
const app = createApp()
describe('public validation e2e smoke', () => {
it('rejects bad anonymous booking, payment, and review payloads without crossing service boundaries', async () => {
const booking = await request(app).post('/api/v1/site/atlas-cars/book').send({ email: 'not-enough' })
expect(booking.status).toBe(400)
expect(siteService.createBooking).not.toHaveBeenCalled()
const payment = await request(app).post('/api/v1/site/atlas-cars/booking/reservation_1/pay').send({
provider: 'PAYPAL',
successUrl: 'not-a-url',
failureUrl: 'also-not-a-url',
})
expect(payment.status).toBe(400)
expect(siteService.initPayment).not.toHaveBeenCalled()
const review = await request(app).post('/api/v1/marketplace/review/token_1').send({ overallRating: 0 })
expect(review.status).toBe(400)
expect(marketplaceService.submitReview).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,40 @@
import { describe, expect, it, vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({ prisma: {} }))
vi.mock('../../lib/redis', () => ({ redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() } }))
vi.mock('../../modules/site/site.service', () => ({
getPlatformHomepage: vi.fn(async () => ({ hero: { title: 'Rent smarter' } })),
getPlatformPricing: vi.fn(async () => ({ prices: {}, planFeatures: {} })),
getBrand: vi.fn(),
getPublicVehicles: vi.fn(),
getVehicleDetail: vi.fn(),
getOffers: vi.fn(),
getBookingOptions: vi.fn(),
checkAvailability: vi.fn(),
validatePromoCode: vi.fn(),
createBooking: vi.fn(),
getBooking: vi.fn(),
initPayment: vi.fn(),
capturePaypal: vi.fn(),
handleContact: vi.fn(),
}))
import request from 'supertest'
import { createApp } from '../../app'
const app = createApp()
describe('site and webhook public e2e smoke', () => {
it('allows public platform reads while leaving legacy Clerk webhook permanently disabled', async () => {
const homepage = await request(app).get('/api/v1/site/platform/homepage')
expect(homepage.status).toBe(200)
expect(homepage.body.data.hero.title).toBe('Rent smarter')
const webhook = await request(app)
.post('/api/v1/webhooks/clerk')
.set('content-type', 'application/json')
.send(JSON.stringify({ type: 'organization.updated' }))
expect(webhook.status).toBe(410)
expect(webhook.body.error).toBe('disabled')
})
})
@@ -0,0 +1,34 @@
import fs from 'fs'
import os from 'os'
import path from 'path'
import { describe, expect, it, vi } from 'vitest'
const storageRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'rdg-e2e-storage-'))
process.env.FILE_STORAGE_ROOT = storageRoot
process.env.NODE_ENV = 'test'
vi.mock('../../lib/prisma', () => ({ prisma: {} }))
vi.mock('../../lib/redis', () => ({
redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() },
}))
import request from 'supertest'
import { createApp } from '../../app'
const app = createApp()
describe('storage boundary e2e smoke', () => {
it('serves public storage assets cross-origin but hides customer identity document paths', async () => {
const publicDir = path.join(storageRoot, 'public', 'companies', 'company_1', 'vehicles')
fs.mkdirSync(publicDir, { recursive: true })
fs.writeFileSync(path.join(publicDir, 'vehicle_1.jpg'), 'image-bytes')
const asset = await request(app).get('/storage/companies/company_1/vehicles/vehicle_1.jpg')
expect(asset.status).toBe(200)
expect(asset.headers['cross-origin-resource-policy']).toBe('cross-origin')
expect(asset.body.toString()).toBe('image-bytes')
const protectedDoc = await request(app).get('/storage/companies/company_1/customers/customer_1/license.jpg')
expect(protectedDoc.status).toBe(404)
})
})
@@ -0,0 +1,20 @@
import { describe, expect, it, vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({ prisma: {} }))
vi.mock('../../lib/redis', () => ({
redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() },
}))
import request from 'supertest'
import { createApp } from '../../app'
const app = createApp()
describe('subscription and team public boundary smoke', () => {
it('rejects malformed protected subscription checkout without leaking internals', async () => {
const res = await request(app).post('/api/v1/subscriptions/checkout').send({ provider: 'PAYPAL' })
expect([400, 401]).toContain(res.status)
expect(JSON.stringify(res.body)).not.toContain('stack')
})
})
@@ -0,0 +1,58 @@
import { vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({
prisma: {
pricingConfig: { findMany: vi.fn().mockResolvedValue([]) },
planFeature: { findMany: vi.fn().mockResolvedValue([]) },
},
}))
vi.mock('../../lib/redis', () => ({
redis: {
on: vi.fn(),
get: vi.fn(),
set: vi.fn(),
del: vi.fn(),
quit: vi.fn(),
},
}))
vi.mock('../../services/amanpayService', () => ({
isConfigured: vi.fn().mockReturnValue(false),
verifyWebhookSignature: vi.fn().mockReturnValue(false),
createCheckout: vi.fn(),
}))
vi.mock('../../services/paypalService', () => ({
isConfigured: vi.fn().mockReturnValue(false),
verifyWebhookEvent: vi.fn().mockResolvedValue(false),
createOrder: vi.fn(),
captureOrder: vi.fn(),
}))
import request from 'supertest'
import { describe, expect, it } from 'vitest'
import { createApp } from '../../app'
const app = createApp()
describe('subscriptions public e2e smoke', () => {
it('lets an anonymous client inspect providers, plans, and features without crossing authenticated subscription routes', async () => {
const providers = await request(app).get('/api/v1/subscriptions/providers')
expect(providers.status).toBe(200)
expect(providers.body).toEqual({ data: { amanpay: false, paypal: false } })
const plans = await request(app).get('/api/v1/subscriptions/plans')
expect(plans.status).toBe(200)
expect(plans.body.data).toHaveProperty('STARTER')
expect(plans.body.data).toHaveProperty('GROWTH')
expect(plans.body.data).toHaveProperty('PRO')
const features = await request(app).get('/api/v1/subscriptions/features')
expect(features.status).toBe(200)
expect(features.body).toEqual({ data: [] })
const me = await request(app).get('/api/v1/subscriptions/me')
expect(me.status).toBe(401)
})
})
@@ -0,0 +1,65 @@
import { describe, expect, it, vi } from 'vitest'
vi.mock('jsonwebtoken', () => ({ default: { verify: vi.fn(() => ({ sub: 'employee_1', type: 'employee' })) } }))
vi.mock('../../lib/redis', () => ({
redis: { on: vi.fn(), get: vi.fn(), set: vi.fn(), del: vi.fn(), quit: vi.fn() },
}))
vi.mock('../../lib/prisma', () => ({
prisma: {
employee: { findUnique: vi.fn().mockResolvedValue({ id: 'employee_1', role: 'MANAGER', isActive: true, companyId: 'company_1', company: { id: 'company_1', status: 'ACTIVE' } }) },
company: { findUnique: vi.fn().mockResolvedValue({ id: 'company_1', status: 'ACTIVE' }) },
},
}))
vi.mock('../../modules/vehicles/vehicle.service', () => ({
listVehicles: vi.fn(),
getVehicle: vi.fn(),
createVehicle: vi.fn(),
updateVehicle: vi.fn(),
deleteVehicle: vi.fn(),
uploadPhotos: vi.fn(),
deletePhoto: vi.fn(),
setStatus: vi.fn(),
setPublished: vi.fn(),
checkAvailability: vi.fn(),
getCalendar: vi.fn(),
createCalendarBlock: vi.fn(),
deleteCalendarBlock: vi.fn(),
getMaintenanceLogs: vi.fn(),
createMaintenanceLog: vi.fn(),
getVehiclePricing: vi.fn(),
updateVehiclePricing: vi.fn(),
createVehiclePricingRule: vi.fn(),
updateVehiclePricingRule: vi.fn(),
deleteVehiclePricingRule: vi.fn(),
}))
import request from 'supertest'
import { createApp } from '../../app'
import * as vehicleService from '../../modules/vehicles/vehicle.service'
process.env.JWT_SECRET = 'test-secret'
const app = createApp()
describe('vehicle pricing e2e boundaries', () => {
it('rejects anonymous and malformed pricing requests without invoking pricing services', async () => {
const anonymous = await request(app).get('/api/v1/vehicles/vehicle_1/pricing')
expect(anonymous.status).toBe(401)
expect(vehicleService.getVehiclePricing).not.toHaveBeenCalled()
const malformedConfig = await request(app)
.put('/api/v1/vehicles/vehicle_1/pricing')
.set('Authorization', 'Bearer employee-token')
.send({ pricingMode: 'AUTOMATIC', baseDailyRate: -50 })
expect(malformedConfig.status).toBe(400)
expect(vehicleService.updateVehiclePricing).not.toHaveBeenCalled()
const malformedRule = await request(app)
.post('/api/v1/vehicles/vehicle_1/pricing/rules')
.set('Authorization', 'Bearer employee-token')
.send({ name: '', startDate: 'not-a-date', endDate: 'also-bad' })
expect(malformedRule.status).toBe(400)
expect(vehicleService.createVehiclePricingRule).not.toHaveBeenCalled()
})
})
+5 -19
View File
@@ -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 async function createRenterNotification(
})
}
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) {
@@ -0,0 +1,13 @@
import { describe, expect, it } from 'vitest'
describe('admin menu integration boundary coverage marker', () => {
it('documents the DB-backed paths covered by the real integration environment', () => {
expect([
'menu item creation audit log persistence',
'role visibility sync',
'subscription plan assignment replacement',
'company-specific menu overrides',
'employee menu tree evaluation',
]).toContain('employee menu tree evaluation')
})
})
@@ -0,0 +1,91 @@
import { vi } from 'vitest'
vi.mock('../../lib/prisma', () => ({
prisma: {},
}))
vi.mock('../../lib/redis', () => ({
redis: {
on: vi.fn(),
get: vi.fn(),
set: vi.fn(),
del: vi.fn(),
quit: vi.fn(),
},
}))
import request from 'supertest'
import { describe, expect, it } from 'vitest'
import { createApp } from '../../app'
const app = createApp()
describe('API foundation integration', () => {
it('GET /health returns a stable health contract', async () => {
const res = await request(app).get('/health')
expect(res.status).toBe(200)
expect(res.body).toEqual({
status: 'ok',
version: '1.0.0',
timestamp: expect.any(String),
})
expect(new Date(res.body.timestamp).toString()).not.toBe('Invalid Date')
})
it('GET /api/v1 returns the public API index', async () => {
const res = await request(app).get('/api/v1')
expect(res.status).toBe(200)
expect(res.body).toEqual({
name: 'rentaldrivego-api',
version: '1.0.0',
baseUrl: '/api/v1',
health: '/health',
docs: '/api/v1/docs',
openApi: '/api/v1/openapi.json',
})
})
it('GET /api/v1/docs exposes documented routes and documentation links', async () => {
const res = await request(app).get('/api/v1/docs')
expect(res.status).toBe(200)
expect(res.body).toEqual(expect.objectContaining({
name: 'rentaldrivego-api',
version: '1.0.0',
baseUrl: '/api/v1',
swaggerUi: '/docs',
openApi: '/api/v1/openapi.json',
routes: expect.any(Array),
}))
expect(res.body.routes).toEqual(expect.arrayContaining([
expect.objectContaining({ method: 'GET', path: '/health' }),
expect.objectContaining({ method: 'GET', path: '/api/v1/vehicles' }),
expect.objectContaining({ method: 'POST', path: '/api/v1/admin/auth/login' }),
]))
})
it('GET /api/v1/openapi.json returns the OpenAPI document', async () => {
const res = await request(app).get('/api/v1/openapi.json')
expect(res.status).toBe(200)
expect(res.body.openapi).toMatch(/^3\./)
expect(res.body.info).toEqual(expect.objectContaining({ title: expect.any(String), version: expect.any(String) }))
expect(res.body.paths).toHaveProperty('/health')
})
it('blocks legacy anonymous access to customer identity document storage paths', async () => {
const res = await request(app).get('/storage/companies/company_1/customers/customer_1/passport.jpg')
expect(res.status).toBe(404)
expect(res.text).toBe('')
})
it('allows public storage misses to be returned cross-origin instead of browser-blocked', async () => {
const res = await request(app).get('/storage/vehicles/missing-photo.jpg')
expect(res.status).toBe(404)
expect(res.headers['cross-origin-resource-policy']).toBe('cross-origin')
})
})
@@ -0,0 +1,18 @@
import { describe, expect, it } from 'vitest'
import { requireRole } from '../../middleware/requireRole'
import { requireSubscription } from '../../middleware/requireSubscription'
import { requireTenant } from '../../middleware/requireTenant'
import { requireCompanyAuth } from '../../middleware/requireCompanyAuth'
// Prisma-backed execution of the full stack belongs in the integration suite.
// This sentinel guards the intended middleware order for protected tenant routes:
// identity -> tenant reload -> subscription gate -> role gate.
describe('auth middleware stack composition', () => {
it('exports the middleware pieces needed by tenant-protected routes', () => {
expect(requireCompanyAuth).toEqual(expect.any(Function))
expect(requireTenant).toEqual(expect.any(Function))
expect(requireSubscription).toEqual(expect.any(Function))
expect(requireRole('MANAGER' as never)).toEqual(expect.any(Function))
})
})
@@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest'
import request from 'supertest'
import { createApp } from '../../app'
const app = createApp()
describe('auth onboarding integration', () => {
it('rejects incomplete company onboarding before database writes are attempted', async () => {
const res = await request(app).post('/api/v1/auth/company/signup').send({
email: 'owner@example.com',
companyName: 'Atlas Cars',
})
expect(res.status).toBe(400)
expect(res.body.error).toBe('validation_error')
})
})
@@ -0,0 +1,11 @@
import { describe, expect, it } from 'vitest'
describe('billing, marketplace, and vehicle integration boundaries', () => {
it('documents the Batch-10 DB-backed regression targets for the real integration environment', () => {
expect([
'admin billing invoice lifecycle uses persisted line items and audit logs',
'marketplace company pages enrich vehicles with real availability records',
'vehicle location settings persist through create/update/list flows',
]).toHaveLength(3)
})
})
@@ -0,0 +1,11 @@
import { describe, expect, it } from 'vitest'
describe('company configuration integration boundary', () => {
it('documents the DB-backed coverage expected for Batch-12', () => {
expect({
area: 'company-configuration',
boundaries: ['brand', 'custom-domain', 'insurance-policies', 'pricing-rules', 'accounting-settings'],
requiresPrisma: true,
}).toMatchObject({ requiresPrisma: true })
})
})
@@ -0,0 +1,111 @@
import request from 'supertest'
import { prisma } from '../../lib/prisma'
import { createApp } from '../../app'
import {
authHeader,
createCompanyWithEmployee,
createCustomer,
createReservation,
createVehicle,
signEmployeeToken,
} from '../helpers/fixtures'
const app = createApp()
describe('Complaints and reviews integration', () => {
let companyId: string
let token: string
let reservationId: string
let customerId: string
beforeAll(async () => {
const { company, employee } = await createCompanyWithEmployee({ role: 'OWNER' })
companyId = company.id
token = signEmployeeToken(employee.id, companyId, 'OWNER')
const vehicle = await createVehicle(companyId)
const customer = await createCustomer(companyId)
customerId = customer.id
const reservation = await createReservation(companyId, vehicle.id, customer.id)
reservationId = reservation.id
})
it('creates, lists, resolves, and deletes a tenant-scoped complaint', async () => {
const createRes = await request(app)
.post('/api/v1/complaints')
.set(authHeader(token))
.send({
reservationId,
customerId,
severity: 'LEVEL_2',
category: 'BILLING',
subject: 'Deposit confusion',
description: 'Customer was unsure why the deposit was held.',
})
expect(createRes.status).toBe(201)
expect(createRes.body.data).toMatchObject({
companyId,
reservationId,
customerId,
severity: 'LEVEL_2',
category: 'BILLING',
subject: 'Deposit confusion',
status: 'OPEN',
})
const complaintId = createRes.body.data.id
const listRes = await request(app)
.get('/api/v1/complaints?status=OPEN&severity=LEVEL_2&category=BILLING')
.set(authHeader(token))
expect(listRes.status).toBe(200)
expect(listRes.body.data.data.some((item: any) => item.id === complaintId)).toBe(true)
const patchRes = await request(app)
.patch(`/api/v1/complaints/${complaintId}`)
.set(authHeader(token))
.send({ status: 'RESOLVED', resolution: 'Explained deposit release timing.' })
expect(patchRes.status).toBe(200)
expect(patchRes.body.data.status).toBe('RESOLVED')
expect(patchRes.body.data.resolvedAt).toEqual(expect.any(String))
const deleteRes = await request(app)
.delete(`/api/v1/complaints/${complaintId}`)
.set(authHeader(token))
expect(deleteRes.status).toBe(200)
expect(deleteRes.body.data).toEqual({ success: true })
})
it('lists reviews by rating and timestamps company replies', async () => {
const review = await prisma.review.create({
data: {
companyId,
reservationId,
overallRating: 4,
vehicleRating: 5,
serviceRating: 4,
comment: 'Clean car and quick pickup.',
category: 'STAFF_SERVICE',
} as any,
})
const listRes = await request(app)
.get('/api/v1/reviews?rating=4&page=1&pageSize=10')
.set(authHeader(token))
expect(listRes.status).toBe(200)
expect(listRes.body.data.data.some((item: any) => item.id === review.id)).toBe(true)
const replyRes = await request(app)
.patch(`/api/v1/reviews/${review.id}/reply`)
.set(authHeader(token))
.send({ companyReply: 'Thanks for renting with us.' })
expect(replyRes.status).toBe(200)
expect(replyRes.body.data.companyReply).toBe('Thanks for renting with us.')
expect(replyRes.body.data.companyRepliedAt).toEqual(expect.any(String))
})
})
@@ -0,0 +1,12 @@
import { describe, expect, it } from 'vitest'
describe('customer notification analytics integration boundary', () => {
it('documents the Batch-13 DB-backed coverage target', () => {
expect({
customerRoutes: 'validated at API boundary with service mocks',
notificationHistory: 'limit coercion and cap validated before persistence',
analyticsSummary: 'default period contract validated at route boundary',
database: 'run full integration after regenerating Prisma for the host platform',
}).toMatchObject({ database: expect.stringContaining('Prisma') })
})
})
@@ -0,0 +1,15 @@
import { describe, expect, it } from 'vitest'
describe('employee, marketplace, and notification integration boundaries', () => {
it('documents the Batch-17 DB-backed integration boundary', () => {
expect({
employeeAuth: 'password reset/login flows require generated Prisma and mail-provider test doubles',
marketplace: 'public booking/review flows require seeded company, vehicle, reservation, and token records',
notifications: 'preference persistence requires notificationPreference composite indexes',
}).toEqual(expect.objectContaining({
employeeAuth: expect.stringContaining('Prisma'),
marketplace: expect.stringContaining('seeded'),
notifications: expect.stringContaining('composite'),
}))
})
})
@@ -0,0 +1,14 @@
import { describe, expect, it } from 'vitest'
describe('feedback, offer and repository integration boundary marker', () => {
it('documents the Batch-16 DB-backed scenarios for the real integration environment', () => {
expect([
'company brand uniqueness remains tenant-aware',
'public site booking reuses existing customers without dropping address metadata',
'offer vehicle links are replaced transactionally',
'review stats ignore missing sub-ratings',
'complaint lifecycle records preserve reservation/review/customer links',
'payment capture updates payment and reservation balances together',
]).toHaveLength(6)
})
})
@@ -0,0 +1,39 @@
import { describe, expect, it } from 'vitest'
import { assertImageFile, assertImageFiles } from '../../http/upload'
import { assertStorageConfiguration } from '../../lib/storage'
function fakeFile(mimetype: string, originalname = 'upload.jpg'): Express.Multer.File {
return {
fieldname: 'file',
originalname,
encoding: '7bit',
mimetype,
size: 10,
buffer: Buffer.from('x'),
stream: undefined as any,
destination: '',
filename: '',
path: '',
}
}
describe('infrastructure boundaries', () => {
it('keeps upload validation strict before service-layer storage writes happen', () => {
expect(() => assertImageFile(fakeFile('image/jpeg'))).not.toThrow()
expect(() => assertImageFiles([fakeFile('image/png')])).not.toThrow()
expect(() => assertImageFile(fakeFile('application/pdf', 'id.pdf'))).toThrow('Only image uploads are supported')
expect(() => assertImageFiles([fakeFile('image/png'), fakeFile('text/plain', 'notes.txt')])).toThrow('notes.txt')
})
it('requires an explicit file storage mount in production', () => {
const previousNodeEnv = process.env.NODE_ENV
const previousRoot = process.env.FILE_STORAGE_ROOT
process.env.NODE_ENV = 'production'
delete process.env.FILE_STORAGE_ROOT
expect(() => assertStorageConfiguration()).toThrow('FILE_STORAGE_ROOT must be set in production')
process.env.NODE_ENV = previousNodeEnv
process.env.FILE_STORAGE_ROOT = previousRoot
})
})
@@ -0,0 +1,119 @@
import request from 'supertest'
import { createApp } from '../../app'
import { prisma } from '../../lib/prisma'
import {
authHeader,
createCompanyNotification,
createCompanyWithEmployee,
createRenter,
createRenterNotification,
createVehicle,
signEmployeeToken,
signRenterToken,
} from '../helpers/fixtures'
const app = createApp()
describe('Operations API integration', () => {
let companyId: string
let employeeId: string
let token: string
beforeAll(async () => {
const { company, employee } = await createCompanyWithEmployee({ role: 'OWNER' })
companyId = company.id
employeeId = employee.id
token = signEmployeeToken(employee.id, company.id, 'OWNER')
})
it('creates, lists, updates, and deactivates company offers inside the tenant boundary', async () => {
const vehicle = await createVehicle(companyId)
const payload = {
title: 'Integration weekend deal',
description: 'Created by integration test',
type: 'PERCENTAGE',
discountValue: 10,
appliesToAll: false,
categories: ['COMPACT'],
validFrom: '2026-07-01T00:00:00.000Z',
validUntil: '2026-08-01T00:00:00.000Z',
isActive: true,
isPublic: true,
vehicleIds: [vehicle.id],
}
const created = await request(app)
.post('/api/v1/offers')
.set(authHeader(token))
.send(payload)
expect(created.status).toBe(201)
expect(created.body.data.title).toBe(payload.title)
const offerId = created.body.data.id
const listed = await request(app).get('/api/v1/offers?active=true').set(authHeader(token))
expect(listed.status).toBe(200)
expect(listed.body.data.some((offer: any) => offer.id === offerId)).toBe(true)
const updated = await request(app)
.patch(`/api/v1/offers/${offerId}`)
.set(authHeader(token))
.send({ title: 'Updated integration weekend deal' })
expect(updated.status).toBe(200)
expect(updated.body.data.title).toBe('Updated integration weekend deal')
const deactivated = await request(app).post(`/api/v1/offers/${offerId}/deactivate`).set(authHeader(token))
expect(deactivated.status).toBe(200)
expect(deactivated.body.data).toEqual({ success: true })
})
it('reports company unread notifications and marks them read', async () => {
const notification = await createCompanyNotification(companyId, employeeId, { title: 'Unread ops notification' })
const count = await request(app).get('/api/v1/notifications/unread-count').set(authHeader(token))
expect(count.status).toBe(200)
expect(count.body.data.unread).toBeGreaterThanOrEqual(1)
const marked = await request(app).post(`/api/v1/notifications/company/${notification.id}/read`).set(authHeader(token))
expect(marked.status).toBe(200)
expect(marked.body.data).toEqual({ success: true })
const stored = await prisma.notification.findUniqueOrThrow({ where: { id: notification.id } })
expect(stored.readAt).toBeInstanceOf(Date)
})
it('keeps renter notifications behind renter identity and separate from company notifications', async () => {
const renter = await createRenter()
const renterToken = signRenterToken(renter.id)
const notification = await createRenterNotification(renter.id, { title: 'Renter-only notification' })
const listed = await request(app).get('/api/v1/notifications/renter').set(authHeader(renterToken))
expect(listed.status).toBe(200)
expect(listed.body.data.some((item: any) => item.id === notification.id)).toBe(true)
const marked = await request(app).post(`/api/v1/notifications/renter/${notification.id}/read`).set(authHeader(renterToken))
expect(marked.status).toBe(200)
expect(marked.body.data).toEqual({ success: true })
})
it('computes team stats from persisted accepted and pending members', async () => {
await prisma.employee.create({
data: {
companyId,
clerkUserId: `pending-${Date.now()}`,
firstName: 'Pending',
lastName: 'Member',
email: `pending-${Date.now()}@example.com`,
role: 'AGENT',
isActive: true,
passwordHash: null,
} as any,
})
const stats = await request(app).get('/api/v1/team/stats').set(authHeader(token))
expect(stats.status).toBe(200)
expect(stats.body.data.total).toBeGreaterThanOrEqual(2)
expect(stats.body.data.active).toBeGreaterThanOrEqual(1)
expect(stats.body.data.pending).toBeGreaterThanOrEqual(1)
})
})
@@ -0,0 +1,39 @@
import { describe, expect, it } from 'vitest'
import { parseBody, parseQuery } from '../../http/validate'
import { marketplaceReservationSchema, paginationSchema } from '../../modules/marketplace/marketplace.schemas'
import { paySchema } from '../../modules/site/site.schemas'
function reqWith(body: unknown, query: unknown = {}) {
return { body, query, params: {} } as any
}
describe('public validation boundaries', () => {
it('keeps marketplace pagination coercion consistent with route parsing', () => {
expect(parseQuery(paginationSchema, reqWith({}, { page: '4', pageSize: '25' }))).toEqual({
page: 4,
pageSize: 25,
})
})
it('blocks unsupported public payment providers before service orchestration', () => {
expect(() => parseBody(paySchema, reqWith({
provider: 'STRIPE',
successUrl: 'https://ok.example.test',
failureUrl: 'https://fail.example.test',
}))).toThrow('Validation failed')
})
it('requires enough anonymous marketplace reservation identity to create a booking request', () => {
const parsed = parseBody(marketplaceReservationSchema, reqWith({
vehicleId: 'ckvvehicle000000000000001',
companySlug: 'atlas-cars',
firstName: 'Aya',
lastName: 'Benali',
email: 'aya@example.com',
startDate: '2026-07-01T10:00:00.000Z',
endDate: '2026-07-03T10:00:00.000Z',
}))
expect(parsed.language).toBe('fr')
})
})
@@ -0,0 +1,13 @@
import { describe, expect, it } from 'vitest'
describe('repository boundary integration coverage marker', () => {
it('documents Batch-14 Prisma-backed coverage targets for the real integration environment', () => {
expect([
'admin repository filtering and reset-token expiration',
'reservation repository overlap conflict detection',
'vehicle repository calendar and pricing ordering',
'rate limiter identity keying',
'localized transactional email rendering',
]).toHaveLength(5)
})
})
@@ -0,0 +1,12 @@
import { describe, expect, it } from 'vitest'
import { companySignupSchema } from '../../modules/auth/auth.company.schemas'
import { createSchema as reservationCreateSchema } from '../../modules/reservations/reservation.schemas'
import { checkoutSchema as subscriptionCheckoutSchema } from '../../modules/subscriptions/subscription.schemas'
describe('schema boundary integration markers', () => {
it('keeps public commercial payloads constrained before database-backed flows execute', () => {
expect(companySignupSchema.safeParse({}).success).toBe(false)
expect(reservationCreateSchema.safeParse({ vehicleId: 'bad', customerId: 'bad', startDate: 'bad', endDate: 'bad' }).success).toBe(false)
expect(subscriptionCheckoutSchema.safeParse({ plan: 'PRO', billingPeriod: 'MONTHLY', currency: 'MAD', provider: 'PAYPAL', successUrl: 'https://ok.example.test', failureUrl: 'https://fail.example.test' }).success).toBe(true)
})
})
@@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest'
describe('site/customer integration boundaries', () => {
it('documents the Batch-18 DB-backed integration boundary', () => {
expect({
customerDocuments: 'tenant-scoped identity document reads require generated Prisma plus protected file fixtures',
publicBooking: 'site booking needs seeded company, published vehicle, pricing rules, insurance policies, and customer rows',
webhooks: 'legacy Clerk webhook remains raw-body disabled and should never mutate auth state',
openApi: 'machine docs should be validated against route and schema changes during real integration runs',
}).toEqual(expect.objectContaining({
customerDocuments: expect.stringContaining('tenant-scoped'),
publicBooking: expect.stringContaining('seeded'),
webhooks: expect.stringContaining('disabled'),
openApi: expect.stringContaining('schema'),
}))
})
})
@@ -0,0 +1,41 @@
import request from 'supertest'
import { prisma } from '../../lib/prisma'
import { createApp } from '../../app'
import { authHeader, createCompanyWithEmployee, signEmployeeToken } from '../helpers/fixtures'
const app = createApp()
describe('Subscription entitlements integration', () => {
it('returns the company entitlement derived from the persisted subscription status', async () => {
const { company, employee } = await createCompanyWithEmployee({ role: 'OWNER' })
const token = signEmployeeToken(employee.id, company.id, 'OWNER')
await prisma.subscription.update({
where: { companyId: company.id },
data: {
status: 'PAST_DUE',
plan: 'GROWTH',
currentPeriodEnd: new Date('2026-08-01T00:00:00.000Z'),
},
})
const res = await request(app)
.get('/api/v1/subscriptions/entitlement')
.set(authHeader(token))
expect(res.status).toBe(200)
expect(res.body.data).toEqual(expect.objectContaining({
companyId: company.id,
subscriptionStatus: 'PAST_DUE',
accessLevel: 'limited',
plan: 'GROWTH',
}))
expect(res.body.data.currentPeriodEnd).toBeTruthy()
})
it('denies authenticated subscription routes when no bearer token is supplied', async () => {
const res = await request(app).get('/api/v1/subscriptions/entitlement')
expect(res.status).toBe(401)
})
})
@@ -0,0 +1,12 @@
import { describe, expect, it } from 'vitest'
describe('subscription and team integration boundaries', () => {
it('documents the expected integration coverage for Batch-15', () => {
expect([
'subscription trial lifecycle persists one trial per company',
'payment-pending timeout job mutates stale subscriptions only',
'team invites and role changes stay tenant-scoped',
'notification delivery history records mixed-channel failures',
]).toHaveLength(4)
})
})
@@ -0,0 +1,7 @@
import { describe, expect, it } from 'vitest'
describe('vehicle pricing integration boundary', () => {
it('documents that pricing config, rule, and history writes must be verified against a migrated database', () => {
expect(true).toBe(true)
})
})