refractor code,
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
import bcrypt from 'bcryptjs'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { prisma } from '../../lib/prisma'
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET ?? 'test-secret'
|
||||
|
||||
let counter = 0
|
||||
function uid() {
|
||||
return `${Date.now()}-${++counter}-${Math.random().toString(36).slice(2, 7)}`
|
||||
}
|
||||
|
||||
export async function createCompanyWithEmployee(overrides: {
|
||||
role?: 'OWNER' | 'MANAGER' | 'AGENT'
|
||||
companyStatus?: 'ACTIVE' | 'TRIALING' | 'PAST_DUE' | 'SUSPENDED' | 'PENDING'
|
||||
} = {}) {
|
||||
const slug = `test-${uid()}`
|
||||
const company = await prisma.company.create({
|
||||
data: {
|
||||
name: `Test Company ${slug}`,
|
||||
slug,
|
||||
email: `company-${slug}@test.com`,
|
||||
status: (overrides.companyStatus ?? 'ACTIVE') as any,
|
||||
subscription: {
|
||||
create: { status: 'TRIALING', plan: 'STARTER' },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const employee = await prisma.employee.create({
|
||||
data: {
|
||||
companyId: company.id,
|
||||
clerkUserId: `clerk_${uid()}`,
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
email: `employee-${uid()}@test.com`,
|
||||
role: overrides.role ?? 'OWNER',
|
||||
},
|
||||
})
|
||||
|
||||
return { company, employee }
|
||||
}
|
||||
|
||||
export async function createVehicle(companyId: string, overrides: Record<string, unknown> = {}) {
|
||||
return prisma.vehicle.create({
|
||||
data: {
|
||||
companyId,
|
||||
make: 'Toyota',
|
||||
model: 'Corolla',
|
||||
year: 2022,
|
||||
color: 'White',
|
||||
licensePlate: `PL-${uid()}`,
|
||||
dailyRate: 500,
|
||||
status: 'AVAILABLE',
|
||||
isPublished: true,
|
||||
...overrides,
|
||||
} as any,
|
||||
})
|
||||
}
|
||||
|
||||
export async function createCustomer(companyId: string, overrides: Record<string, unknown> = {}) {
|
||||
return prisma.customer.create({
|
||||
data: {
|
||||
companyId,
|
||||
firstName: 'Jane',
|
||||
lastName: 'Doe',
|
||||
email: `customer-${uid()}@test.com`,
|
||||
...overrides,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function createRenter(overrides: {
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
email?: string
|
||||
password?: string
|
||||
isActive?: boolean
|
||||
} = {}) {
|
||||
return prisma.renter.create({
|
||||
data: {
|
||||
firstName: overrides.firstName ?? 'Renter',
|
||||
lastName: overrides.lastName ?? 'User',
|
||||
email: overrides.email ?? `renter-${uid()}@test.com`,
|
||||
passwordHash: await bcrypt.hash(overrides.password ?? 'RenterPass123!', 10),
|
||||
isActive: overrides.isActive ?? true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function createAdminUser(overrides: {
|
||||
email?: string
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
password?: string
|
||||
role?: 'SUPER_ADMIN' | 'ADMIN' | 'SUPPORT' | 'FINANCE' | 'VIEWER'
|
||||
isActive?: boolean
|
||||
totpEnabled?: boolean
|
||||
totpSecret?: string | null
|
||||
} = {}) {
|
||||
return prisma.adminUser.create({
|
||||
data: {
|
||||
email: overrides.email ?? `admin-${uid()}@test.com`,
|
||||
firstName: overrides.firstName ?? 'Admin',
|
||||
lastName: overrides.lastName ?? 'User',
|
||||
passwordHash: await bcrypt.hash(overrides.password ?? 'AdminPass123!', 10),
|
||||
role: (overrides.role ?? 'ADMIN') as any,
|
||||
isActive: overrides.isActive ?? true,
|
||||
totpEnabled: overrides.totpEnabled ?? false,
|
||||
totpSecret: overrides.totpSecret ?? null,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function createReservation(
|
||||
companyId: string,
|
||||
vehicleId: string,
|
||||
customerId: string,
|
||||
overrides: Record<string, unknown> = {},
|
||||
) {
|
||||
return prisma.reservation.create({
|
||||
data: {
|
||||
companyId,
|
||||
vehicleId,
|
||||
customerId,
|
||||
startDate: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
||||
endDate: new Date(Date.now() + 4 * 24 * 60 * 60 * 1000),
|
||||
dailyRate: 500,
|
||||
totalDays: 3,
|
||||
totalAmount: 1500,
|
||||
depositAmount: 300,
|
||||
status: 'CONFIRMED',
|
||||
paymentStatus: 'UNPAID',
|
||||
paidAmount: 0,
|
||||
...overrides,
|
||||
} as any,
|
||||
})
|
||||
}
|
||||
|
||||
export async function createRentalPayment(
|
||||
companyId: string,
|
||||
reservationId: string,
|
||||
overrides: Record<string, unknown> = {},
|
||||
) {
|
||||
return prisma.rentalPayment.create({
|
||||
data: {
|
||||
companyId,
|
||||
reservationId,
|
||||
amount: 500,
|
||||
currency: 'MAD',
|
||||
status: 'SUCCEEDED',
|
||||
type: 'CHARGE',
|
||||
paymentProvider: 'AMANPAY',
|
||||
amanpayTransactionId: `aman-${uid()}`,
|
||||
paidAt: new Date(),
|
||||
...overrides,
|
||||
} as any,
|
||||
})
|
||||
}
|
||||
|
||||
export async function createSubscriptionInvoice(
|
||||
companyId: string,
|
||||
subscriptionId: string,
|
||||
overrides: Record<string, unknown> = {},
|
||||
) {
|
||||
return prisma.subscriptionInvoice.create({
|
||||
data: {
|
||||
companyId,
|
||||
subscriptionId,
|
||||
amount: 29900,
|
||||
currency: 'MAD',
|
||||
status: 'PENDING',
|
||||
paymentProvider: 'AMANPAY',
|
||||
amanpayTransactionId: `sub-${uid()}`,
|
||||
...overrides,
|
||||
} as any,
|
||||
})
|
||||
}
|
||||
|
||||
export async function createCompanyNotification(
|
||||
companyId: string,
|
||||
employeeId?: string,
|
||||
overrides: Record<string, unknown> = {},
|
||||
) {
|
||||
return prisma.notification.create({
|
||||
data: {
|
||||
companyId,
|
||||
employeeId,
|
||||
type: 'PAYMENT_RECEIVED',
|
||||
title: 'Payment received',
|
||||
body: 'A payment was recorded.',
|
||||
channel: 'IN_APP',
|
||||
status: 'PENDING',
|
||||
...overrides,
|
||||
} as any,
|
||||
})
|
||||
}
|
||||
|
||||
export async function createRenterNotification(
|
||||
renterId: string,
|
||||
overrides: Record<string, unknown> = {},
|
||||
) {
|
||||
return prisma.notification.create({
|
||||
data: {
|
||||
renterId,
|
||||
type: 'BOOKING_CONFIRMED',
|
||||
title: 'Booking confirmed',
|
||||
body: 'Your booking is confirmed.',
|
||||
channel: 'IN_APP',
|
||||
status: 'PENDING',
|
||||
...overrides,
|
||||
} as any,
|
||||
})
|
||||
}
|
||||
|
||||
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 signAdminToken(adminId: string) {
|
||||
return jwt.sign(
|
||||
{ sub: adminId, type: 'admin' },
|
||||
JWT_SECRET,
|
||||
{ expiresIn: '1h' },
|
||||
)
|
||||
}
|
||||
|
||||
export function signRenterToken(renterId: string) {
|
||||
return jwt.sign(
|
||||
{ sub: renterId, type: 'renter' },
|
||||
JWT_SECRET,
|
||||
{ expiresIn: '1h' },
|
||||
)
|
||||
}
|
||||
|
||||
export function authHeader(token: string) {
|
||||
return { Authorization: `Bearer ${token}` }
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import request from 'supertest'
|
||||
import { createApp } from '../../app'
|
||||
import {
|
||||
authHeader,
|
||||
createAdminUser,
|
||||
createCompanyWithEmployee,
|
||||
createRenter,
|
||||
signAdminToken,
|
||||
} from '../helpers/fixtures'
|
||||
|
||||
const app = createApp()
|
||||
|
||||
describe('Admin API', () => {
|
||||
let adminToken: string
|
||||
let financeToken: string
|
||||
let viewerToken: string
|
||||
let adminId: string
|
||||
let companyId: string
|
||||
|
||||
beforeAll(async () => {
|
||||
const admin = await createAdminUser({
|
||||
email: 'admin-login@test.com',
|
||||
firstName: 'Primary',
|
||||
lastName: 'Admin',
|
||||
role: 'ADMIN',
|
||||
password: 'AdminPass123!',
|
||||
})
|
||||
adminId = admin.id
|
||||
adminToken = signAdminToken(admin.id)
|
||||
|
||||
const financeAdmin = await createAdminUser({
|
||||
email: 'finance-admin@test.com',
|
||||
role: 'FINANCE',
|
||||
password: 'FinancePass123!',
|
||||
})
|
||||
financeToken = signAdminToken(financeAdmin.id)
|
||||
|
||||
const viewerAdmin = await createAdminUser({
|
||||
email: 'viewer-admin@test.com',
|
||||
role: 'VIEWER',
|
||||
password: 'ViewerPass123!',
|
||||
})
|
||||
viewerToken = signAdminToken(viewerAdmin.id)
|
||||
|
||||
const { company } = await createCompanyWithEmployee()
|
||||
companyId = company.id
|
||||
await createRenter()
|
||||
})
|
||||
|
||||
describe('POST /api/v1/admin/auth/login', () => {
|
||||
it('returns 401 for invalid credentials', async () => {
|
||||
const res = await request(app)
|
||||
.post('/api/v1/admin/auth/login')
|
||||
.send({
|
||||
email: 'admin-login@test.com',
|
||||
password: 'wrong-password',
|
||||
})
|
||||
|
||||
expect(res.status).toBe(401)
|
||||
expect(res.body.error).toBe('invalid_credentials')
|
||||
})
|
||||
|
||||
it('returns a token for valid credentials', async () => {
|
||||
const res = await request(app)
|
||||
.post('/api/v1/admin/auth/login')
|
||||
.send({
|
||||
email: 'admin-login@test.com',
|
||||
password: 'AdminPass123!',
|
||||
})
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(typeof res.body.data.data.token).toBe('string')
|
||||
expect(res.body.data.data.admin.id).toBe(adminId)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /api/v1/admin/auth/me', () => {
|
||||
it('returns the authenticated admin profile', async () => {
|
||||
const res = await request(app)
|
||||
.get('/api/v1/admin/auth/me')
|
||||
.set(authHeader(adminToken))
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body.data.data.id).toBe(adminId)
|
||||
expect(res.body.data.data).not.toHaveProperty('passwordHash')
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /api/v1/admin/companies', () => {
|
||||
it('returns paginated companies', async () => {
|
||||
const res = await request(app)
|
||||
.get('/api/v1/admin/companies')
|
||||
.set(authHeader(adminToken))
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(Array.isArray(res.body.data.data)).toBe(true)
|
||||
expect(typeof res.body.data.total).toBe('number')
|
||||
expect(res.body.data.data.some((item: any) => item.id === companyId)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /api/v1/admin/renters', () => {
|
||||
it('returns paginated renters', async () => {
|
||||
const res = await request(app)
|
||||
.get('/api/v1/admin/renters')
|
||||
.set(authHeader(adminToken))
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(Array.isArray(res.body.data.data)).toBe(true)
|
||||
expect(typeof res.body.data.total).toBe('number')
|
||||
expect(res.body.data.total).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /api/v1/admin/metrics', () => {
|
||||
it('returns 403 for VIEWER role', async () => {
|
||||
const res = await request(app)
|
||||
.get('/api/v1/admin/metrics')
|
||||
.set(authHeader(viewerToken))
|
||||
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('returns platform metrics for FINANCE role', async () => {
|
||||
const res = await request(app)
|
||||
.get('/api/v1/admin/metrics')
|
||||
.set(authHeader(financeToken))
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body.data.data).toEqual(
|
||||
expect.objectContaining({
|
||||
totalCompanies: expect.any(Number),
|
||||
totalRenters: expect.any(Number),
|
||||
totalReservations: expect.any(Number),
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,131 @@
|
||||
import request from 'supertest'
|
||||
import { createApp } from '../../app'
|
||||
import {
|
||||
createCompanyWithEmployee,
|
||||
createCustomer,
|
||||
signEmployeeToken,
|
||||
authHeader,
|
||||
} from '../helpers/fixtures'
|
||||
|
||||
const app = createApp()
|
||||
|
||||
describe('Customers API', () => {
|
||||
let companyId: string
|
||||
let token: string
|
||||
let agentToken: string
|
||||
|
||||
beforeAll(async () => {
|
||||
const { company, employee } = await createCompanyWithEmployee({ role: 'OWNER' })
|
||||
companyId = company.id
|
||||
token = signEmployeeToken(employee.id, companyId, 'OWNER')
|
||||
|
||||
const { employee: agent } = await createCompanyWithEmployee({ role: 'AGENT' })
|
||||
agentToken = signEmployeeToken(agent.id, agent.companyId, 'AGENT')
|
||||
})
|
||||
|
||||
describe('GET /api/v1/customers', () => {
|
||||
it('returns 401 without auth', async () => {
|
||||
const res = await request(app).get('/api/v1/customers')
|
||||
expect(res.status).toBe(401)
|
||||
})
|
||||
|
||||
it('returns paginated customer list', async () => {
|
||||
await createCustomer(companyId)
|
||||
await createCustomer(companyId)
|
||||
|
||||
const res = await request(app)
|
||||
.get('/api/v1/customers')
|
||||
.set(authHeader(token))
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(typeof res.body.total).toBe('number')
|
||||
expect(res.body.total).toBeGreaterThanOrEqual(2)
|
||||
expect(Array.isArray(res.body.data)).toBe(true)
|
||||
})
|
||||
|
||||
it('filters by search query q', async () => {
|
||||
await createCustomer(companyId, { firstName: 'Unique', lastName: 'Xyzzy' })
|
||||
|
||||
const res = await request(app)
|
||||
.get('/api/v1/customers?q=Xyzzy')
|
||||
.set(authHeader(token))
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body.total).toBeGreaterThanOrEqual(1)
|
||||
expect(res.body.data[0].lastName).toBe('Xyzzy')
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /api/v1/customers', () => {
|
||||
it('creates a new customer', async () => {
|
||||
const res = await request(app)
|
||||
.post('/api/v1/customers')
|
||||
.set(authHeader(token))
|
||||
.send({
|
||||
firstName: 'Alice',
|
||||
lastName: 'Smith',
|
||||
email: `alice-${Date.now()}@example.com`,
|
||||
})
|
||||
|
||||
expect(res.status).toBe(201)
|
||||
expect(res.body.data.firstName).toBe('Alice')
|
||||
expect(res.body.data.lastName).toBe('Smith')
|
||||
})
|
||||
|
||||
it('returns 400 for missing required fields', async () => {
|
||||
const res = await request(app)
|
||||
.post('/api/v1/customers')
|
||||
.set(authHeader(token))
|
||||
.send({ firstName: 'Noemail' })
|
||||
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /api/v1/customers/:id', () => {
|
||||
it('returns a single customer', async () => {
|
||||
const customer = await createCustomer(companyId, { firstName: 'Bob', lastName: 'Test' })
|
||||
|
||||
const res = await request(app)
|
||||
.get(`/api/v1/customers/${customer.id}`)
|
||||
.set(authHeader(token))
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body.data.id).toBe(customer.id)
|
||||
expect(res.body.data.firstName).toBe('Bob')
|
||||
})
|
||||
|
||||
it('returns 404 for unknown id', async () => {
|
||||
const res = await request(app)
|
||||
.get('/api/v1/customers/clzunknownidzzzzzzzzzzz')
|
||||
.set(authHeader(token))
|
||||
|
||||
expect(res.status).toBe(404)
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /api/v1/customers/:id/flag', () => {
|
||||
it('flags a customer (MANAGER+)', async () => {
|
||||
const customer = await createCustomer(companyId)
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/api/v1/customers/${customer.id}/flag`)
|
||||
.set(authHeader(token))
|
||||
.send({ reason: 'Suspicious activity' })
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body.data.success).toBe(true)
|
||||
})
|
||||
|
||||
it('returns 403 for AGENT role', async () => {
|
||||
const customer = await createCustomer(companyId)
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/api/v1/customers/${customer.id}/flag`)
|
||||
.set(authHeader(agentToken))
|
||||
.send({ reason: 'Test' })
|
||||
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
import request from 'supertest'
|
||||
import { createApp } from '../../app'
|
||||
|
||||
const app = createApp()
|
||||
|
||||
describe('GET /health', () => {
|
||||
it('returns 200 with status ok', async () => {
|
||||
const res = await request(app).get('/health')
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body.status).toBe('ok')
|
||||
expect(res.body.version).toBe('1.0.0')
|
||||
expect(typeof res.body.timestamp).toBe('string')
|
||||
})
|
||||
|
||||
it('GET /api/v1/docs returns route index', async () => {
|
||||
const res = await request(app).get('/api/v1/docs')
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body.name).toBe('rentaldrivego-api')
|
||||
expect(Array.isArray(res.body.routes)).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,146 @@
|
||||
import request from 'supertest'
|
||||
import { createApp } from '../../app'
|
||||
import {
|
||||
authHeader,
|
||||
createCompanyNotification,
|
||||
createCompanyWithEmployee,
|
||||
createRenter,
|
||||
createRenterNotification,
|
||||
signEmployeeToken,
|
||||
signRenterToken,
|
||||
} from '../helpers/fixtures'
|
||||
|
||||
const app = createApp()
|
||||
|
||||
describe('Notifications API', () => {
|
||||
let companyId: string
|
||||
let employeeId: string
|
||||
let ownerToken: string
|
||||
let renterId: string
|
||||
let renterToken: string
|
||||
|
||||
beforeAll(async () => {
|
||||
const { company, employee } = await createCompanyWithEmployee({ role: 'OWNER' })
|
||||
companyId = company.id
|
||||
employeeId = employee.id
|
||||
ownerToken = signEmployeeToken(employee.id, companyId, 'OWNER')
|
||||
|
||||
const renter = await createRenter()
|
||||
renterId = renter.id
|
||||
renterToken = signRenterToken(renter.id)
|
||||
})
|
||||
|
||||
describe('Company notifications', () => {
|
||||
it('returns unread count', async () => {
|
||||
const res = await request(app)
|
||||
.get('/api/v1/notifications/unread-count')
|
||||
.set(authHeader(ownerToken))
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(typeof res.body.data.data.unread).toBe('number')
|
||||
})
|
||||
|
||||
it('lists company notifications and supports unread filtering', async () => {
|
||||
const unread = await createCompanyNotification(companyId, employeeId, { title: 'Unread company notice' })
|
||||
await createCompanyNotification(companyId, employeeId, {
|
||||
title: 'Read company notice',
|
||||
readAt: new Date(),
|
||||
status: 'READ',
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
.get('/api/v1/notifications/company?unread=true')
|
||||
.set(authHeader(ownerToken))
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(Array.isArray(res.body.data.data)).toBe(true)
|
||||
expect(res.body.data.data.some((item: any) => item.id === unread.id)).toBe(true)
|
||||
expect(res.body.data.data.every((item: any) => item.readAt === null)).toBe(true)
|
||||
})
|
||||
|
||||
it('marks a company notification as read', async () => {
|
||||
const notification = await createCompanyNotification(companyId, employeeId)
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/api/v1/notifications/company/${notification.id}/read`)
|
||||
.set(authHeader(ownerToken))
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body.data.data.success).toBe(true)
|
||||
})
|
||||
|
||||
it('updates company notification preferences', async () => {
|
||||
const payload = [
|
||||
{ notificationType: 'PAYMENT_RECEIVED', channel: 'EMAIL', enabled: false },
|
||||
]
|
||||
|
||||
const patchRes = await request(app)
|
||||
.patch('/api/v1/notifications/company/preferences')
|
||||
.set(authHeader(ownerToken))
|
||||
.send(payload)
|
||||
|
||||
expect(patchRes.status).toBe(200)
|
||||
expect(patchRes.body.data.data.success).toBe(true)
|
||||
|
||||
const getRes = await request(app)
|
||||
.get('/api/v1/notifications/company/preferences')
|
||||
.set(authHeader(ownerToken))
|
||||
|
||||
expect(getRes.status).toBe(200)
|
||||
expect(getRes.body.data.data).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
employeeId,
|
||||
notificationType: 'PAYMENT_RECEIVED',
|
||||
channel: 'EMAIL',
|
||||
enabled: false,
|
||||
}),
|
||||
]),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Renter notifications', () => {
|
||||
it('lists renter notifications', async () => {
|
||||
const notification = await createRenterNotification(renterId)
|
||||
|
||||
const res = await request(app)
|
||||
.get('/api/v1/notifications/renter')
|
||||
.set(authHeader(renterToken))
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(Array.isArray(res.body.data.data)).toBe(true)
|
||||
expect(res.body.data.data.some((item: any) => item.id === notification.id)).toBe(true)
|
||||
})
|
||||
|
||||
it('updates renter notification preferences', async () => {
|
||||
const payload = [
|
||||
{ notificationType: 'BOOKING_CONFIRMED', channel: 'PUSH', enabled: true },
|
||||
]
|
||||
|
||||
const patchRes = await request(app)
|
||||
.patch('/api/v1/notifications/renter/preferences')
|
||||
.set(authHeader(renterToken))
|
||||
.send(payload)
|
||||
|
||||
expect(patchRes.status).toBe(200)
|
||||
expect(patchRes.body.data.data.success).toBe(true)
|
||||
|
||||
const getRes = await request(app)
|
||||
.get('/api/v1/notifications/renter/preferences')
|
||||
.set(authHeader(renterToken))
|
||||
|
||||
expect(getRes.status).toBe(200)
|
||||
expect(getRes.body.data.data).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
renterId,
|
||||
notificationType: 'BOOKING_CONFIRMED',
|
||||
channel: 'PUSH',
|
||||
enabled: true,
|
||||
}),
|
||||
]),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,159 @@
|
||||
import request from 'supertest'
|
||||
import { prisma } from '../../lib/prisma'
|
||||
import { createApp } from '../../app'
|
||||
import {
|
||||
authHeader,
|
||||
createCompanyWithEmployee,
|
||||
createCustomer,
|
||||
createRentalPayment,
|
||||
createReservation,
|
||||
createVehicle,
|
||||
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('Payments API', () => {
|
||||
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_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
||||
firstName: 'Agent',
|
||||
lastName: 'User',
|
||||
email: uniqueEmail('agent'),
|
||||
role: 'AGENT',
|
||||
},
|
||||
})
|
||||
agentToken = signEmployeeToken(agent.id, companyId, 'AGENT')
|
||||
})
|
||||
|
||||
describe('GET /api/v1/payments/company', () => {
|
||||
it('returns 401 without auth', async () => {
|
||||
const res = await request(app).get('/api/v1/payments/company')
|
||||
expect(res.status).toBe(401)
|
||||
})
|
||||
|
||||
it('returns company payments', async () => {
|
||||
const vehicle = await createVehicle(companyId)
|
||||
const customer = await createCustomer(companyId)
|
||||
const reservation = await createReservation(companyId, vehicle.id, customer.id)
|
||||
const payment = await createRentalPayment(companyId, reservation.id)
|
||||
|
||||
const res = await request(app)
|
||||
.get('/api/v1/payments/company')
|
||||
.set(authHeader(ownerToken))
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(Array.isArray(res.body.data.data)).toBe(true)
|
||||
expect(res.body.data.data.some((item: any) => item.id === payment.id)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /api/v1/payments/reservations/:id/manual', () => {
|
||||
it('records a manual payment and updates the reservation balance', async () => {
|
||||
const vehicle = await createVehicle(companyId)
|
||||
const customer = await createCustomer(companyId)
|
||||
const reservation = await createReservation(companyId, vehicle.id, customer.id, {
|
||||
totalAmount: 1200,
|
||||
paidAmount: 0,
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/api/v1/payments/reservations/${reservation.id}/manual`)
|
||||
.set(authHeader(ownerToken))
|
||||
.send({
|
||||
amount: 400,
|
||||
currency: 'MAD',
|
||||
type: 'CHARGE',
|
||||
paymentMethod: 'CASH',
|
||||
})
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body.data.data.amount).toBe(400)
|
||||
expect(res.body.data.data.status).toBe('SUCCEEDED')
|
||||
|
||||
const updated = await prisma.reservation.findUniqueOrThrow({ where: { id: reservation.id } })
|
||||
expect(updated.paidAmount).toBe(400)
|
||||
expect(updated.paymentStatus).toBe('PARTIAL')
|
||||
})
|
||||
|
||||
it('returns 403 for AGENT role', async () => {
|
||||
const vehicle = await createVehicle(companyId)
|
||||
const customer = await createCustomer(companyId)
|
||||
const reservation = await createReservation(companyId, vehicle.id, customer.id)
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/api/v1/payments/reservations/${reservation.id}/manual`)
|
||||
.set(authHeader(agentToken))
|
||||
.send({
|
||||
amount: 300,
|
||||
currency: 'MAD',
|
||||
type: 'CHARGE',
|
||||
paymentMethod: 'CASH',
|
||||
})
|
||||
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /api/v1/payments/reservations/:id/charge', () => {
|
||||
it('returns 409 when the reservation is already fully paid', async () => {
|
||||
const vehicle = await createVehicle(companyId)
|
||||
const customer = await createCustomer(companyId)
|
||||
const reservation = await createReservation(companyId, vehicle.id, customer.id, {
|
||||
totalAmount: 900,
|
||||
paidAmount: 900,
|
||||
paymentStatus: 'PAID',
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/api/v1/payments/reservations/${reservation.id}/charge`)
|
||||
.set(authHeader(ownerToken))
|
||||
.send({
|
||||
provider: 'PAYPAL',
|
||||
type: 'CHARGE',
|
||||
currency: 'MAD',
|
||||
successUrl: 'https://example.com/success',
|
||||
failureUrl: 'https://example.com/failure',
|
||||
})
|
||||
|
||||
expect(res.status).toBe(409)
|
||||
expect(res.body.error).toBe('conflict')
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /api/v1/payments/reservations/:reservationId/payments/:paymentId/refund', () => {
|
||||
it('rejects refunding a manual payment through the gateway flow', async () => {
|
||||
const vehicle = await createVehicle(companyId)
|
||||
const customer = await createCustomer(companyId)
|
||||
const reservation = await createReservation(companyId, vehicle.id, customer.id)
|
||||
const payment = await createRentalPayment(companyId, reservation.id, {
|
||||
paymentMethod: 'CASH',
|
||||
paymentProvider: 'AMANPAY',
|
||||
amanpayTransactionId: null,
|
||||
paypalCaptureId: null,
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/api/v1/payments/reservations/${reservation.id}/payments/${payment.id}/refund`)
|
||||
.set(authHeader(ownerToken))
|
||||
.send({ amount: 100, reason: 'Test refund' })
|
||||
|
||||
expect(res.status).toBe(400)
|
||||
expect(res.body.message).toContain('Manual payments')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,140 @@
|
||||
import request from 'supertest'
|
||||
import { createApp } from '../../app'
|
||||
import {
|
||||
createCompanyWithEmployee,
|
||||
createVehicle,
|
||||
createCustomer,
|
||||
signEmployeeToken,
|
||||
authHeader,
|
||||
} from '../helpers/fixtures'
|
||||
|
||||
const app = createApp()
|
||||
|
||||
const future = (days: number) =>
|
||||
new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString()
|
||||
|
||||
describe('Reservations API', () => {
|
||||
let companyId: string
|
||||
let token: string
|
||||
let vehicleId: string
|
||||
let customerId: string
|
||||
|
||||
beforeAll(async () => {
|
||||
const { company, employee } = await createCompanyWithEmployee({ role: 'OWNER' })
|
||||
companyId = company.id
|
||||
token = signEmployeeToken(employee.id, companyId, 'OWNER')
|
||||
const v = await createVehicle(companyId)
|
||||
vehicleId = v.id
|
||||
const c = await createCustomer(companyId)
|
||||
customerId = c.id
|
||||
})
|
||||
|
||||
describe('GET /api/v1/reservations', () => {
|
||||
it('returns 401 without auth', async () => {
|
||||
const res = await request(app).get('/api/v1/reservations')
|
||||
expect(res.status).toBe(401)
|
||||
})
|
||||
|
||||
it('returns paginated list', async () => {
|
||||
const res = await request(app)
|
||||
.get('/api/v1/reservations')
|
||||
.set(authHeader(token))
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(typeof res.body.total).toBe('number')
|
||||
expect(Array.isArray(res.body.data)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /api/v1/reservations', () => {
|
||||
it('creates a reservation with valid payload', async () => {
|
||||
const res = await request(app)
|
||||
.post('/api/v1/reservations')
|
||||
.set(authHeader(token))
|
||||
.send({
|
||||
vehicleId,
|
||||
customerId,
|
||||
startDate: future(10),
|
||||
endDate: future(15),
|
||||
})
|
||||
|
||||
expect(res.status).toBe(201)
|
||||
expect(res.body.data.vehicleId).toBe(vehicleId)
|
||||
expect(res.body.data.customerId).toBe(customerId)
|
||||
expect(res.body.data.status).toBe('DRAFT')
|
||||
})
|
||||
|
||||
it('returns 400 for missing vehicleId', async () => {
|
||||
const res = await request(app)
|
||||
.post('/api/v1/reservations')
|
||||
.set(authHeader(token))
|
||||
.send({
|
||||
customerId,
|
||||
startDate: future(10),
|
||||
endDate: future(15),
|
||||
})
|
||||
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
|
||||
it('returns 409 when vehicle is not available for requested dates', async () => {
|
||||
// Create a reservation blocking the vehicle
|
||||
const createRes = await request(app)
|
||||
.post('/api/v1/reservations')
|
||||
.set(authHeader(token))
|
||||
.send({ vehicleId, customerId, startDate: future(20), endDate: future(25) })
|
||||
.expect(201)
|
||||
|
||||
await request(app)
|
||||
.post(`/api/v1/reservations/${createRes.body.data.id}/confirm`)
|
||||
.set(authHeader(token))
|
||||
.expect(200)
|
||||
|
||||
const res = await request(app)
|
||||
.post('/api/v1/reservations')
|
||||
.set(authHeader(token))
|
||||
.send({ vehicleId, customerId, startDate: future(21), endDate: future(24) })
|
||||
|
||||
// Overlapping dates should conflict
|
||||
expect([409, 422]).toContain(res.status)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Reservation state machine', () => {
|
||||
let reservationId: string
|
||||
|
||||
beforeEach(async () => {
|
||||
const v = await createVehicle(companyId)
|
||||
const res = await request(app)
|
||||
.post('/api/v1/reservations')
|
||||
.set(authHeader(token))
|
||||
.send({ vehicleId: v.id, customerId, startDate: future(30), endDate: future(35) })
|
||||
expect(res.status).toBe(201)
|
||||
reservationId = res.body.data.id
|
||||
})
|
||||
|
||||
it('DRAFT → CONFIRMED via POST /:id/confirm', async () => {
|
||||
const res = await request(app)
|
||||
.post(`/api/v1/reservations/${reservationId}/confirm`)
|
||||
.set(authHeader(token))
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body.data.status).toBe('CONFIRMED')
|
||||
})
|
||||
|
||||
it('CONFIRMED → ACTIVE via POST /:id/checkin', async () => {
|
||||
await request(app)
|
||||
.post(`/api/v1/reservations/${reservationId}/confirm`)
|
||||
.set(authHeader(token))
|
||||
.expect(200)
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/api/v1/reservations/${reservationId}/checkin`)
|
||||
.set(authHeader(token))
|
||||
.send({ fuelLevel: 'FULL' })
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body.data.status).toBe('ACTIVE')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,130 @@
|
||||
import request from 'supertest'
|
||||
import { prisma } from '../../lib/prisma'
|
||||
import { createApp } from '../../app'
|
||||
import {
|
||||
authHeader,
|
||||
createCompanyWithEmployee,
|
||||
createSubscriptionInvoice,
|
||||
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('Subscriptions API', () => {
|
||||
let companyId: string
|
||||
let subscriptionId: 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 subscription = await prisma.subscription.findUniqueOrThrow({ where: { companyId } })
|
||||
subscriptionId = subscription.id
|
||||
|
||||
const agent = await prisma.employee.create({
|
||||
data: {
|
||||
companyId,
|
||||
clerkUserId: `clerk_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
||||
firstName: 'Agent',
|
||||
lastName: 'User',
|
||||
email: uniqueEmail('sub-agent'),
|
||||
role: 'AGENT',
|
||||
},
|
||||
})
|
||||
agentToken = signEmployeeToken(agent.id, companyId, 'AGENT')
|
||||
})
|
||||
|
||||
describe('Public endpoints', () => {
|
||||
it('returns subscription plans', async () => {
|
||||
const res = await request(app).get('/api/v1/subscriptions/plans')
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body.data.data).toHaveProperty('STARTER')
|
||||
expect(res.body.data.data).toHaveProperty('GROWTH')
|
||||
})
|
||||
|
||||
it('returns provider availability', async () => {
|
||||
const res = await request(app).get('/api/v1/subscriptions/providers')
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(typeof res.body.data.data.amanpay).toBe('boolean')
|
||||
expect(typeof res.body.data.data.paypal).toBe('boolean')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Authenticated endpoints', () => {
|
||||
it('returns the current subscription', async () => {
|
||||
const res = await request(app)
|
||||
.get('/api/v1/subscriptions/me')
|
||||
.set(authHeader(ownerToken))
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body.data.data.companyId).toBe(companyId)
|
||||
expect(res.body.data.data.id).toBe(subscriptionId)
|
||||
})
|
||||
|
||||
it('returns company invoices', async () => {
|
||||
const invoice = await createSubscriptionInvoice(companyId, subscriptionId)
|
||||
|
||||
const res = await request(app)
|
||||
.get('/api/v1/subscriptions/invoices')
|
||||
.set(authHeader(ownerToken))
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(Array.isArray(res.body.data.data)).toBe(true)
|
||||
expect(res.body.data.data.some((item: any) => item.id === invoice.id)).toBe(true)
|
||||
})
|
||||
|
||||
it('changes the subscription plan for OWNER', async () => {
|
||||
const res = await request(app)
|
||||
.post('/api/v1/subscriptions/change-plan')
|
||||
.set(authHeader(ownerToken))
|
||||
.send({
|
||||
plan: 'GROWTH',
|
||||
billingPeriod: 'ANNUAL',
|
||||
currency: 'EUR',
|
||||
})
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body.data.data.plan).toBe('GROWTH')
|
||||
expect(res.body.data.data.billingPeriod).toBe('ANNUAL')
|
||||
expect(res.body.data.data.currency).toBe('EUR')
|
||||
})
|
||||
|
||||
it('returns 403 for AGENT on plan changes', async () => {
|
||||
const res = await request(app)
|
||||
.post('/api/v1/subscriptions/change-plan')
|
||||
.set(authHeader(agentToken))
|
||||
.send({
|
||||
plan: 'PRO',
|
||||
billingPeriod: 'MONTHLY',
|
||||
currency: 'MAD',
|
||||
})
|
||||
|
||||
expect(res.status).toBe(403)
|
||||
})
|
||||
|
||||
it('cancels and resumes at period end', async () => {
|
||||
const cancelRes = await request(app)
|
||||
.post('/api/v1/subscriptions/cancel')
|
||||
.set(authHeader(ownerToken))
|
||||
|
||||
expect(cancelRes.status).toBe(200)
|
||||
expect(cancelRes.body.data.data.cancelAtPeriodEnd).toBe(true)
|
||||
|
||||
const resumeRes = await request(app)
|
||||
.post('/api/v1/subscriptions/resume')
|
||||
.set(authHeader(ownerToken))
|
||||
|
||||
expect(resumeRes.status).toBe(200)
|
||||
expect(resumeRes.body.data.data.cancelAtPeriodEnd).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,146 @@
|
||||
import request from 'supertest'
|
||||
import { createApp } from '../../app'
|
||||
import {
|
||||
createCompanyWithEmployee,
|
||||
createVehicle,
|
||||
signEmployeeToken,
|
||||
authHeader,
|
||||
} from '../helpers/fixtures'
|
||||
|
||||
const app = createApp()
|
||||
|
||||
describe('Vehicles API', () => {
|
||||
let companyId: string
|
||||
let token: string
|
||||
|
||||
beforeAll(async () => {
|
||||
const { company, employee } = await createCompanyWithEmployee({ role: 'OWNER' })
|
||||
companyId = company.id
|
||||
token = signEmployeeToken(employee.id, companyId, 'OWNER')
|
||||
})
|
||||
|
||||
describe('GET /api/v1/vehicles', () => {
|
||||
it('returns 401 without auth', async () => {
|
||||
const res = await request(app).get('/api/v1/vehicles')
|
||||
expect(res.status).toBe(401)
|
||||
})
|
||||
|
||||
it('returns paginated vehicle list', async () => {
|
||||
await createVehicle(companyId)
|
||||
await createVehicle(companyId)
|
||||
|
||||
const res = await request(app)
|
||||
.get('/api/v1/vehicles')
|
||||
.set(authHeader(token))
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(typeof res.body.total).toBe('number')
|
||||
expect(res.body.total).toBeGreaterThanOrEqual(2)
|
||||
expect(Array.isArray(res.body.data)).toBe(true)
|
||||
})
|
||||
|
||||
it('filters by status', async () => {
|
||||
await createVehicle(companyId, { status: 'MAINTENANCE' })
|
||||
|
||||
const res = await request(app)
|
||||
.get('/api/v1/vehicles?status=MAINTENANCE')
|
||||
.set(authHeader(token))
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
res.body.data.forEach((v: any) => expect(v.status).toBe('MAINTENANCE'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /api/v1/vehicles', () => {
|
||||
it('creates a vehicle with valid payload', async () => {
|
||||
const res = await request(app)
|
||||
.post('/api/v1/vehicles')
|
||||
.set(authHeader(token))
|
||||
.send({
|
||||
make: 'Honda',
|
||||
model: 'Civic',
|
||||
year: 2023,
|
||||
licensePlate: `TEST-${Date.now()}`,
|
||||
category: 'COMPACT',
|
||||
dailyRate: 400,
|
||||
})
|
||||
|
||||
expect(res.status).toBe(201)
|
||||
expect(res.body.data.make).toBe('Honda')
|
||||
expect(res.body.data.model).toBe('Civic')
|
||||
})
|
||||
|
||||
it('returns 400 for missing required fields', async () => {
|
||||
const res = await request(app)
|
||||
.post('/api/v1/vehicles')
|
||||
.set(authHeader(token))
|
||||
.send({ make: 'Honda' })
|
||||
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /api/v1/vehicles/:id', () => {
|
||||
it('returns a single vehicle', async () => {
|
||||
const vehicle = await createVehicle(companyId, { make: 'BMW', model: 'X5' })
|
||||
|
||||
const res = await request(app)
|
||||
.get(`/api/v1/vehicles/${vehicle.id}`)
|
||||
.set(authHeader(token))
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body.data.id).toBe(vehicle.id)
|
||||
expect(res.body.data.make).toBe('BMW')
|
||||
})
|
||||
|
||||
it('returns 404 for unknown vehicle', async () => {
|
||||
const res = await request(app)
|
||||
.get('/api/v1/vehicles/clzunknownidzzzzzzzzzzz')
|
||||
.set(authHeader(token))
|
||||
|
||||
expect(res.status).toBe(404)
|
||||
})
|
||||
})
|
||||
|
||||
describe('PATCH /api/v1/vehicles/:id', () => {
|
||||
it('updates vehicle notes', async () => {
|
||||
const vehicle = await createVehicle(companyId)
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/api/v1/vehicles/${vehicle.id}`)
|
||||
.set(authHeader(token))
|
||||
.send({ notes: 'Updated via integration test' })
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body.data.notes).toBe('Updated via integration test')
|
||||
})
|
||||
|
||||
it('sets isPublished=false when status=MAINTENANCE', async () => {
|
||||
const vehicle = await createVehicle(companyId)
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/api/v1/vehicles/${vehicle.id}`)
|
||||
.set(authHeader(token))
|
||||
.send({ status: 'MAINTENANCE' })
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body.data.status).toBe('MAINTENANCE')
|
||||
expect(res.body.data.isPublished).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /api/v1/vehicles/:id/availability', () => {
|
||||
it('reports vehicle available for future dates', async () => {
|
||||
const vehicle = await createVehicle(companyId)
|
||||
const start = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]
|
||||
const end = new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]
|
||||
|
||||
const res = await request(app)
|
||||
.get(`/api/v1/vehicles/${vehicle.id}/availability?startDate=${start}&endDate=${end}`)
|
||||
.set(authHeader(token))
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body.data.available).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
import { beforeAll, afterAll } from 'vitest'
|
||||
import { prisma } from '../lib/prisma'
|
||||
|
||||
const delegates = [
|
||||
'auditLog',
|
||||
'adminPermission',
|
||||
'adminUser',
|
||||
'damagePoint',
|
||||
'damageInspection',
|
||||
'damageReport',
|
||||
'additionalDriver',
|
||||
'reservationInsurance',
|
||||
'notificationPreference',
|
||||
'notification',
|
||||
'review',
|
||||
'rentalPayment',
|
||||
'reservation',
|
||||
'customer',
|
||||
'renterSavedCompany',
|
||||
'renter',
|
||||
'offerVehicle',
|
||||
'offer',
|
||||
'vehicleCalendarBlock',
|
||||
'maintenanceLog',
|
||||
'vehicle',
|
||||
'employee',
|
||||
'pricingRule',
|
||||
'insurancePolicy',
|
||||
'accountingSettings',
|
||||
'contractSettings',
|
||||
'brandSettings',
|
||||
'subscriptionInvoice',
|
||||
'subscription',
|
||||
'company',
|
||||
] as const
|
||||
|
||||
// Wipe all data between test files in a safe order that respects FK constraints.
|
||||
async function cleanDatabase() {
|
||||
for (const delegate of delegates) {
|
||||
await (prisma as any)[delegate].deleteMany({})
|
||||
}
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDatabase()
|
||||
await prisma.$disconnect()
|
||||
})
|
||||
Reference in New Issue
Block a user