add tests and registry

This commit is contained in:
root
2026-05-22 02:58:59 -04:00
parent 99c429d69c
commit b9197bcb5d
10 changed files with 480 additions and 16 deletions
@@ -0,0 +1,98 @@
import { describe, it, expect } from 'vitest'
import { presentBrand } from './company.presenter'
const fullBrand = {
id: 'brand_1',
companyId: 'comp_1',
displayName: 'Test Rentals',
subdomain: 'test-rentals',
logoUrl: 'https://example.com/logo.jpg',
primaryColor: '#1A56DB',
defaultLocale: 'en',
defaultCurrency: 'MAD',
amanpayMerchantId: 'merchant-abc',
amanpaySecretKey: 'super-secret-key',
paypalEmail: 'paypal@example.com',
paypalMerchantId: 'paypal-merchant-xyz',
isListedOnMarketplace: true,
}
describe('presentBrand', () => {
it('strips amanpaySecretKey from the response', () => {
const result = presentBrand(fullBrand)
expect(result).not.toHaveProperty('amanpaySecretKey')
})
it('strips amanpayMerchantId from the response', () => {
const result = presentBrand(fullBrand)
expect(result).not.toHaveProperty('amanpayMerchantId')
})
it('strips paypalEmail from the response', () => {
const result = presentBrand(fullBrand)
expect(result).not.toHaveProperty('paypalEmail')
})
it('strips paypalMerchantId from the response', () => {
const result = presentBrand(fullBrand)
expect(result).not.toHaveProperty('paypalMerchantId')
})
it('sets amanpayConfigured=true when both amanpay credentials are present', () => {
const result = presentBrand(fullBrand)
expect(result.amanpayConfigured).toBe(true)
})
it('sets amanpayConfigured=false when amanpaySecretKey is null', () => {
const result = presentBrand({ ...fullBrand, amanpaySecretKey: null })
expect(result.amanpayConfigured).toBe(false)
})
it('sets amanpayConfigured=false when amanpayMerchantId is null', () => {
const result = presentBrand({ ...fullBrand, amanpayMerchantId: null })
expect(result.amanpayConfigured).toBe(false)
})
it('sets amanpayConfigured=false when both amanpay fields are null', () => {
const result = presentBrand({ ...fullBrand, amanpayMerchantId: null, amanpaySecretKey: null })
expect(result.amanpayConfigured).toBe(false)
})
it('sets paypalConfigured=true when paypalEmail is set', () => {
const result = presentBrand({ ...fullBrand, paypalMerchantId: null })
expect(result.paypalConfigured).toBe(true)
})
it('sets paypalConfigured=true when paypalMerchantId is set', () => {
const result = presentBrand({ ...fullBrand, paypalEmail: null })
expect(result.paypalConfigured).toBe(true)
})
it('sets paypalConfigured=false when both paypal fields are null', () => {
const result = presentBrand({ ...fullBrand, paypalEmail: null, paypalMerchantId: null })
expect(result.paypalConfigured).toBe(false)
})
it('preserves all non-credential fields', () => {
const result = presentBrand(fullBrand)
expect(result).toMatchObject({
id: 'brand_1',
companyId: 'comp_1',
displayName: 'Test Rentals',
subdomain: 'test-rentals',
logoUrl: 'https://example.com/logo.jpg',
primaryColor: '#1A56DB',
defaultLocale: 'en',
defaultCurrency: 'MAD',
isListedOnMarketplace: true,
})
})
it('returns null when brand is null', () => {
expect(presentBrand(null)).toBeNull()
})
it('returns undefined when brand is undefined', () => {
expect(presentBrand(undefined)).toBeUndefined()
})
})
@@ -122,4 +122,27 @@ describe('vehicle.service', () => {
expect(result).toEqual({ id: 'block_1' })
})
})
describe('getMaintenanceLogs', () => {
it('throws NotFoundError when vehicle does not belong to the requesting company', async () => {
vi.mocked(repo.findById).mockResolvedValue(null)
await expect(service.getMaintenanceLogs('veh_1', 'other_company')).rejects.toThrow('Vehicle not found')
})
it('returns maintenance logs when vehicle belongs to the requesting company', async () => {
const mockLogs = [{ id: 'log_1', vehicleId: 'veh_1', description: 'Oil change' }]
vi.mocked(repo.findById).mockResolvedValue(mockVehicle as any)
vi.mocked(repo.findMaintenanceLogs).mockResolvedValue(mockLogs as any)
const result = await service.getMaintenanceLogs('veh_1', 'comp_1')
expect(repo.findById).toHaveBeenCalledWith('veh_1', 'comp_1')
expect(result).toEqual(mockLogs)
})
it('passes companyId to repo.findById for ownership validation', async () => {
vi.mocked(repo.findById).mockResolvedValue(mockVehicle as any)
vi.mocked(repo.findMaintenanceLogs).mockResolvedValue([])
await service.getMaintenanceLogs('veh_1', 'comp_1')
expect(repo.findById).toHaveBeenCalledWith('veh_1', 'comp_1')
})
})
})
@@ -0,0 +1,139 @@
import request from 'supertest'
import { prisma } from '../../lib/prisma'
import { createApp } from '../../app'
import {
authHeader,
createCompanyWithEmployee,
signEmployeeToken,
} from '../helpers/fixtures'
const app = createApp()
describe('Companies API — brand credential exposure (VF-01)', () => {
let companyId: string
let ownerToken: string
let agentToken: string
beforeAll(async () => {
const { company, employee } = await createCompanyWithEmployee({ role: 'OWNER' })
companyId = company.id
ownerToken = signEmployeeToken(employee.id, companyId, 'OWNER')
const agent = await prisma.employee.create({
data: {
companyId,
clerkUserId: `clerk_brand_${Date.now()}`,
firstName: 'Agent',
lastName: 'Brand',
email: `agent-brand-${Date.now()}@test.com`,
role: 'AGENT',
},
})
agentToken = signEmployeeToken(agent.id, companyId, 'AGENT')
// Seed brand with real-looking payment credentials
await prisma.brandSettings.create({
data: {
companyId,
displayName: company.name,
subdomain: `brand-test-${Date.now()}`,
amanpayMerchantId: 'merchant-secret-id',
amanpaySecretKey: 'aman-super-secret-key',
paypalEmail: 'payments@example.com',
paypalMerchantId: 'paypal-merchant-secret',
} as any,
})
})
describe('GET /api/v1/companies/me/brand', () => {
it('returns 401 without auth', async () => {
const res = await request(app).get('/api/v1/companies/me/brand')
expect(res.status).toBe(401)
})
it('does not expose amanpaySecretKey in the response', async () => {
const res = await request(app)
.get('/api/v1/companies/me/brand')
.set(authHeader(ownerToken))
expect(res.status).toBe(200)
expect(res.body.data).not.toHaveProperty('amanpaySecretKey')
})
it('does not expose amanpayMerchantId in the response', async () => {
const res = await request(app)
.get('/api/v1/companies/me/brand')
.set(authHeader(ownerToken))
expect(res.status).toBe(200)
expect(res.body.data).not.toHaveProperty('amanpayMerchantId')
})
it('does not expose paypalEmail in the response', async () => {
const res = await request(app)
.get('/api/v1/companies/me/brand')
.set(authHeader(ownerToken))
expect(res.status).toBe(200)
expect(res.body.data).not.toHaveProperty('paypalEmail')
})
it('does not expose paypalMerchantId in the response', async () => {
const res = await request(app)
.get('/api/v1/companies/me/brand')
.set(authHeader(ownerToken))
expect(res.status).toBe(200)
expect(res.body.data).not.toHaveProperty('paypalMerchantId')
})
it('returns amanpayConfigured=true when credentials are set', async () => {
const res = await request(app)
.get('/api/v1/companies/me/brand')
.set(authHeader(ownerToken))
expect(res.status).toBe(200)
expect(res.body.data.amanpayConfigured).toBe(true)
})
it('returns paypalConfigured=true when credentials are set', async () => {
const res = await request(app)
.get('/api/v1/companies/me/brand')
.set(authHeader(ownerToken))
expect(res.status).toBe(200)
expect(res.body.data.paypalConfigured).toBe(true)
})
it('is accessible to AGENT role (no role gate on this endpoint)', async () => {
const res = await request(app)
.get('/api/v1/companies/me/brand')
.set(authHeader(agentToken))
expect(res.status).toBe(200)
expect(res.body.data).not.toHaveProperty('amanpaySecretKey')
expect(res.body.data).not.toHaveProperty('amanpayMerchantId')
})
it('returns amanpayConfigured=false when credentials are absent', async () => {
const { company: c2, employee: e2 } = await createCompanyWithEmployee({ role: 'OWNER' })
const t2 = signEmployeeToken(e2.id, c2.id, 'OWNER')
await prisma.brandSettings.create({
data: {
companyId: c2.id,
displayName: c2.name,
subdomain: `no-creds-${Date.now()}`,
} as any,
})
const res = await request(app)
.get('/api/v1/companies/me/brand')
.set(authHeader(t2))
expect(res.status).toBe(200)
expect(res.body.data.amanpayConfigured).toBe(false)
expect(res.body.data.paypalConfigured).toBe(false)
})
})
})
@@ -11,12 +11,62 @@ import {
signEmployeeToken,
} from '../helpers/fixtures'
const app = createApp()
function uniqueEmail(prefix: string) {
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}@test.com`
}
describe('Payment webhooks — signature bypass fix (VF-02)', () => {
describe('POST /api/v1/payments/webhooks/amanpay', () => {
it('returns 401 when AmanPay is not configured (env vars absent)', async () => {
// In the test environment AMANPAY_MERCHANT_ID and AMANPAY_SECRET_KEY are not set,
// so isConfigured() returns false. The fix ensures this returns 401, not 200.
const res = await request(app)
.post('/api/v1/payments/webhooks/amanpay')
.send({ transaction_id: 'txn_fake', status: 'PAID' })
expect(res.status).toBe(401)
expect(res.body.error).toBe('invalid_signature')
})
it('does not process the webhook payload when provider is not configured', async () => {
const { company, employee } = await createCompanyWithEmployee({ role: 'OWNER' })
const vehicle = await createVehicle(company.id)
const customer = await createCustomer(company.id)
const reservation = await createReservation(company.id, vehicle.id, customer.id, {
totalAmount: 1000, paidAmount: 0,
})
const payment = await createRentalPayment(company.id, reservation.id, {
status: 'PENDING',
amanpayTransactionId: `txn-webhook-${Date.now()}`,
})
const res = await request(app)
.post('/api/v1/payments/webhooks/amanpay')
.send({ transaction_id: payment.amanpayTransactionId, status: 'PAID' })
expect(res.status).toBe(401)
// Reservation must remain unpaid — the handler was not invoked
const unchanged = await prisma.reservation.findUniqueOrThrow({ where: { id: reservation.id } })
expect(unchanged.paymentStatus).toBe('UNPAID')
})
})
describe('POST /api/v1/payments/webhooks/paypal', () => {
it('returns 401 when PayPal is not configured (env vars absent)', async () => {
const res = await request(app)
.post('/api/v1/payments/webhooks/paypal')
.send({ event_type: 'PAYMENT.CAPTURE.COMPLETED', resource: { id: 'pp-fake' } })
expect(res.status).toBe(401)
expect(res.body.error).toBe('invalid_signature')
})
})
})
describe('Payments API', () => {
let companyId: string
let ownerToken: string
@@ -14,6 +14,51 @@ function uniqueEmail(prefix: string) {
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}@test.com`
}
describe('Subscription webhooks — signature bypass fix (VF-02)', () => {
describe('POST /api/v1/subscriptions/webhooks/amanpay', () => {
it('returns 401 when AmanPay is not configured (env vars absent)', async () => {
// In the test environment AMANPAY_MERCHANT_ID / SECRET_KEY are not set,
// so isConfigured() returns false. The fix ensures this returns 401, not 200.
const res = await request(app)
.post('/api/v1/subscriptions/webhooks/amanpay')
.send({ transaction_id: 'txn_fake_sub', status: 'PAID' })
expect(res.status).toBe(401)
expect(res.body.error).toBe('invalid_signature')
})
it('does not activate a subscription for an unconfigured-provider request', async () => {
const { company } = await createCompanyWithEmployee({ role: 'OWNER' })
const sub = await prisma.subscription.findUniqueOrThrow({ where: { companyId: company.id } })
const invoice = await createSubscriptionInvoice(company.id, sub.id, {
status: 'PENDING',
amanpayTransactionId: `sub-txn-bypass-${Date.now()}`,
})
const res = await request(app)
.post('/api/v1/subscriptions/webhooks/amanpay')
.send({ transaction_id: (invoice as any).amanpayTransactionId, status: 'PAID' })
expect(res.status).toBe(401)
// Invoice must remain PENDING — the handler was not invoked
const unchanged = await prisma.subscriptionInvoice.findUniqueOrThrow({ where: { id: invoice.id } })
expect(unchanged.status).toBe('PENDING')
})
})
describe('POST /api/v1/subscriptions/webhooks/paypal', () => {
it('returns 401 when PayPal is not configured (env vars absent)', async () => {
const res = await request(app)
.post('/api/v1/subscriptions/webhooks/paypal')
.send({ event_type: 'BILLING.SUBSCRIPTION.ACTIVATED', resource: { id: 'sub-fake' } })
expect(res.status).toBe(401)
expect(res.body.error).toBe('invalid_signature')
})
})
})
describe('Subscriptions API', () => {
let companyId: string
let subscriptionId: string
@@ -1,4 +1,5 @@
import request from 'supertest'
import { prisma } from '../../lib/prisma'
import { createApp } from '../../app'
import {
createCompanyWithEmployee,
@@ -143,4 +144,64 @@ describe('Vehicles API', () => {
expect(res.body.data.available).toBe(true)
})
})
describe('GET /api/v1/vehicles/:id/maintenance — tenant isolation (VF-03)', () => {
it('returns 401 without auth', async () => {
const vehicle = await createVehicle(companyId)
const res = await request(app).get(`/api/v1/vehicles/${vehicle.id}/maintenance`)
expect(res.status).toBe(401)
})
it('returns maintenance logs for own company vehicle', async () => {
const vehicle = await createVehicle(companyId)
await prisma.maintenanceLog.create({
data: {
vehicleId: vehicle.id,
type: 'OIL_CHANGE',
description: 'Routine oil change',
cost: 250,
performedAt: new Date(),
} as any,
})
const res = await request(app)
.get(`/api/v1/vehicles/${vehicle.id}/maintenance`)
.set(authHeader(token))
expect(res.status).toBe(200)
expect(Array.isArray(res.body.data)).toBe(true)
expect(res.body.data.length).toBeGreaterThanOrEqual(1)
})
it('returns 404 when accessing maintenance logs for a vehicle belonging to another company', async () => {
const { company: otherCompany } = await createCompanyWithEmployee({ role: 'OWNER' })
const foreignVehicle = await createVehicle(otherCompany.id, { make: 'BMW', model: 'X5' })
const res = await request(app)
.get(`/api/v1/vehicles/${foreignVehicle.id}/maintenance`)
.set(authHeader(token))
expect(res.status).toBe(404)
})
it('does not leak maintenance logs across companies even when vehicle id is known', async () => {
const { company: otherCompany } = await createCompanyWithEmployee({ role: 'OWNER' })
const foreignVehicle = await createVehicle(otherCompany.id)
await prisma.maintenanceLog.create({
data: {
vehicleId: foreignVehicle.id,
type: 'TIRE_ROTATION',
description: 'Other company private data',
cost: 100,
performedAt: new Date(),
} as any,
})
const res = await request(app)
.get(`/api/v1/vehicles/${foreignVehicle.id}/maintenance`)
.set(authHeader(token))
expect(res.status).toBe(404)
})
})
})