fix test issues
Build & Deploy / Build & Push Docker Image (push) Successful in 3m1s
Test / Type Check (all packages) (push) Successful in 55s
Build & Deploy / Deploy to VPS (push) Successful in 5s
Test / API Unit Tests (push) Successful in 47s
Test / Homepage Unit Tests (push) Successful in 42s
Test / Carplace Unit Tests (push) Successful in 42s
Test / Admin Unit Tests (push) Successful in 40s
Test / Dashboard Unit Tests (push) Successful in 41s
Test / API Integration Tests (push) Successful in 1m0s
Build & Deploy / Build & Push Docker Image (push) Successful in 3m1s
Test / Type Check (all packages) (push) Successful in 55s
Build & Deploy / Deploy to VPS (push) Successful in 5s
Test / API Unit Tests (push) Successful in 47s
Test / Homepage Unit Tests (push) Successful in 42s
Test / Carplace Unit Tests (push) Successful in 42s
Test / Admin Unit Tests (push) Successful in 40s
Test / Dashboard Unit Tests (push) Successful in 41s
Test / API Integration Tests (push) Successful in 1m0s
This commit is contained in:
@@ -133,8 +133,8 @@ jobs:
|
||||
- run: npm run build --workspace @rentaldrivego/types
|
||||
- run: npm run test:homepage
|
||||
|
||||
storefront-tests:
|
||||
name: Storefront Unit Tests
|
||||
carplace-tests:
|
||||
name: Carplace Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: type-check
|
||||
steps:
|
||||
@@ -169,7 +169,7 @@ jobs:
|
||||
node -e "require('@rollup/rollup-linux-arm64-gnu')"
|
||||
fi
|
||||
- run: npm run build --workspace @rentaldrivego/types
|
||||
- run: npm run test:storefront
|
||||
- run: npm run test:carplace
|
||||
|
||||
admin-tests:
|
||||
name: Admin Unit Tests
|
||||
|
||||
@@ -52,7 +52,10 @@ describe('carplace.repo public query and write boundaries', () => {
|
||||
where: {
|
||||
id: 'vehicle_1',
|
||||
isPublished: true,
|
||||
company: { slug: 'atlas', status: { in: ['ACTIVE', 'TRIALING'] } },
|
||||
company: {
|
||||
OR: [{ slug: 'atlas' }, { brand: { subdomain: 'atlas' } }],
|
||||
status: { in: ['ACTIVE', 'TRIALING'] },
|
||||
},
|
||||
},
|
||||
include: { company: { include: { brand: true } } },
|
||||
})
|
||||
|
||||
@@ -44,6 +44,7 @@ vi.mock('../../modules/carplace/carplace.service', () => ({
|
||||
getCities: vi.fn(),
|
||||
getListedCompanies: vi.fn(),
|
||||
searchVehicles: vi.fn(),
|
||||
searchVehiclesPage: vi.fn(),
|
||||
getVehicleDetail: vi.fn(),
|
||||
createCarplaceReservation: vi.fn(),
|
||||
getCompanyPage: vi.fn(),
|
||||
@@ -73,7 +74,11 @@ describe('employee, notification, and carplace API validation contracts', () =>
|
||||
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(carplaceService.searchVehicles).mockResolvedValue({ companies: [], vehicles: [], pagination: { page: 1, pageSize: 20, total: 0 } } as never)
|
||||
vi.mocked(carplaceService.searchVehiclesPage).mockResolvedValue({
|
||||
items: [],
|
||||
pagination: { page: 1, pageSize: 20, totalItems: 0, totalPages: 0 },
|
||||
facets: { categories: [], transmissions: [], makes: [], price: { min: null, max: null } },
|
||||
} as never)
|
||||
vi.mocked(carplaceService.submitReview).mockResolvedValue({ id: 'review_1' } as never)
|
||||
})
|
||||
|
||||
@@ -114,11 +119,12 @@ describe('employee, notification, and carplace API validation contracts', () =>
|
||||
const res = await request(app).get('/api/v1/carplace/search?city=Casablanca')
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(carplaceService.searchVehicles).toHaveBeenCalledWith(expect.objectContaining({
|
||||
expect(carplaceService.searchVehiclesPage).toHaveBeenCalledWith(expect.objectContaining({
|
||||
city: 'Casablanca',
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
}))
|
||||
expect(res.body.data.pagination).toEqual({ page: 1, pageSize: 20, totalItems: 0, totalPages: 0 })
|
||||
})
|
||||
|
||||
it('rejects out-of-range public review ratings before service execution', async () => {
|
||||
|
||||
@@ -35,6 +35,7 @@ vi.mock('../../modules/carplace/carplace.service', () => ({
|
||||
getCities: vi.fn(),
|
||||
getListedCompanies: vi.fn(),
|
||||
searchVehicles: vi.fn(),
|
||||
searchVehiclesPage: vi.fn(),
|
||||
createCarplaceReservation: vi.fn(),
|
||||
getReviewContext: vi.fn(),
|
||||
submitReview: vi.fn(),
|
||||
@@ -59,7 +60,11 @@ describe('public validation API contracts', () => {
|
||||
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(carplaceService.searchVehicles).mockResolvedValue({ data: [], pagination: { page: 1, pageSize: 20 } } as never)
|
||||
vi.mocked(carplaceService.searchVehiclesPage).mockResolvedValue({
|
||||
items: [],
|
||||
pagination: { page: 2, pageSize: 30, totalItems: 0, totalPages: 0 },
|
||||
facets: { categories: [], transmissions: [], makes: [], price: { min: null, max: null } },
|
||||
} as never)
|
||||
vi.mocked(carplaceService.createCarplaceReservation).mockResolvedValue({ id: 'reservation_1' } as never)
|
||||
vi.mocked(carplaceService.submitReview).mockResolvedValue({ id: 'review_1' } as never)
|
||||
})
|
||||
@@ -124,19 +129,20 @@ describe('public validation API contracts', () => {
|
||||
const res = await request(app).get('/api/v1/carplace/search?city=Rabat&maxPrice=500&page=2&pageSize=30')
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(carplaceService.searchVehicles).toHaveBeenCalledWith({
|
||||
expect(carplaceService.searchVehiclesPage).toHaveBeenCalledWith({
|
||||
city: 'Rabat',
|
||||
maxPrice: 500,
|
||||
page: 2,
|
||||
pageSize: 30,
|
||||
})
|
||||
expect(res.body.data.pagination).toEqual({ page: 2, pageSize: 30, totalItems: 0, totalPages: 0 })
|
||||
})
|
||||
|
||||
it('rejects oversized carplace pagination before search execution', async () => {
|
||||
const res = await request(app).get('/api/v1/carplace/search?pageSize=500')
|
||||
|
||||
expect(res.status).toBe(400)
|
||||
expect(carplaceService.searchVehicles).not.toHaveBeenCalled()
|
||||
expect(carplaceService.searchVehiclesPage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects carplace reservations with invalid public contact data before service execution', async () => {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { CarplaceApiError, carplaceFetch, carplaceFetchOrDefault, carplacePost } from './api'
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.NEXT_PUBLIC_API_URL
|
||||
delete process.env.NEXT_PUBLIC_CARPLACE_DIRECT_API
|
||||
delete process.env.API_INTERNAL_URL
|
||||
vi.restoreAllMocks()
|
||||
Reflect.deleteProperty(globalThis, 'window')
|
||||
Reflect.deleteProperty(globalThis, 'fetch')
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
describe('carplace API helpers', () => {
|
||||
@@ -13,6 +15,7 @@ describe('carplace API helpers', () => {
|
||||
const fetchMock = vi.fn(async () => ({ ok: true, json: async () => ({ data: [{ id: 'vehicle_1' }] }) }))
|
||||
Object.defineProperty(globalThis, 'fetch', { configurable: true, value: fetchMock })
|
||||
|
||||
const { carplaceFetch } = await import('./api')
|
||||
await expect(carplaceFetch('/carplace/vehicles')).resolves.toEqual([{ id: 'vehicle_1' }])
|
||||
expect(fetchMock).toHaveBeenCalledWith(expect.stringMatching(/\/api\/v1\/carplace\/vehicles$/), {
|
||||
cache: 'no-store',
|
||||
@@ -30,6 +33,7 @@ describe('carplace API helpers', () => {
|
||||
})),
|
||||
})
|
||||
|
||||
const { carplaceFetch } = await import('./api')
|
||||
await expect(carplaceFetch('/carplace/book')).rejects.toMatchObject({
|
||||
name: 'CarplaceApiError',
|
||||
message: 'Vehicle unavailable',
|
||||
@@ -45,14 +49,18 @@ describe('carplace API helpers', () => {
|
||||
value: vi.fn(async () => ({ ok: false, status: 500, json: async () => ({ message: 'Down' }) })),
|
||||
})
|
||||
|
||||
const { carplaceFetchOrDefault } = await import('./api')
|
||||
await expect(carplaceFetchOrDefault('/carplace/homepage', { hero: null })).resolves.toEqual({ hero: null })
|
||||
})
|
||||
|
||||
it('posts JSON payloads and unwraps successful responses', async () => {
|
||||
process.env.NEXT_PUBLIC_API_URL = 'https://api.example.com/api/v1'
|
||||
process.env.NEXT_PUBLIC_CARPLACE_DIRECT_API = 'true'
|
||||
Object.defineProperty(globalThis, 'window', { configurable: true, value: {} })
|
||||
const fetchMock = vi.fn(async () => ({ ok: true, json: async () => ({ data: { id: 'reservation_1' } }) }))
|
||||
Object.defineProperty(globalThis, 'fetch', { configurable: true, value: fetchMock })
|
||||
|
||||
const { carplacePost } = await import('./api')
|
||||
await expect(carplacePost('/site/reservations', { vehicleId: 'vehicle_1' })).resolves.toEqual({ id: 'reservation_1' })
|
||||
expect(fetchMock).toHaveBeenCalledWith('https://api.example.com/api/v1/site/reservations', expect.objectContaining({
|
||||
method: 'POST',
|
||||
@@ -68,6 +76,7 @@ describe('carplace API helpers', () => {
|
||||
value: vi.fn(async () => ({ ok: false, status: 502, json: async () => { throw new Error('bad json') } })),
|
||||
})
|
||||
|
||||
const { carplaceFetch } = await import('./api')
|
||||
await expect(carplaceFetch('/site/homepage')).rejects.toMatchObject({
|
||||
name: 'CarplaceApiError',
|
||||
message: 'Request failed',
|
||||
|
||||
@@ -16,6 +16,7 @@ afterEach(() => {
|
||||
Reflect.deleteProperty(globalThis, 'window')
|
||||
Reflect.deleteProperty(globalThis, 'fetch')
|
||||
delete process.env.NEXT_PUBLIC_API_URL
|
||||
delete process.env.API_INTERNAL_URL
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
@@ -54,6 +55,59 @@ describe('dashboard apiFetch', () => {
|
||||
expect(fetchMock).toHaveBeenCalledWith('https://api.rentaldrivego.ma/api/v1/auth/account/start', expect.any(Object))
|
||||
})
|
||||
|
||||
it.each([
|
||||
['https://api.rentaldrivego.ma', 'https://api.rentaldrivego.ma/api/v1/auth/account/start'],
|
||||
['https://api.rentaldrivego.ma/', 'https://api.rentaldrivego.ma/api/v1/auth/account/start'],
|
||||
['https://api.rentaldrivego.ma/api', 'https://api.rentaldrivego.ma/api/v1/auth/account/start'],
|
||||
['https://api.rentaldrivego.ma/api/v1', 'https://api.rentaldrivego.ma/api/v1/auth/account/start'],
|
||||
['/dashboard/api/v1', '/dashboard/api/v1/auth/account/start'],
|
||||
])('normalizes API base %s', async (configuredBase, expectedUrl) => {
|
||||
installBrowser()
|
||||
process.env.NEXT_PUBLIC_API_URL = configuredBase
|
||||
const fetchMock = vi.fn(async () => ({
|
||||
ok: true,
|
||||
json: async () => ({ data: { ok: true } }),
|
||||
}))
|
||||
Object.defineProperty(globalThis, 'fetch', { configurable: true, value: fetchMock })
|
||||
|
||||
const { apiFetch } = await import('./api')
|
||||
await apiFetch('/auth/account/start', { method: 'POST', body: JSON.stringify({}) })
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(expectedUrl, expect.any(Object))
|
||||
})
|
||||
|
||||
it('falls back to the dashboard proxy when no public API URL is configured', async () => {
|
||||
installBrowser()
|
||||
const fetchMock = vi.fn(async () => ({
|
||||
ok: true,
|
||||
json: async () => ({ data: { ok: true } }),
|
||||
}))
|
||||
Object.defineProperty(globalThis, 'fetch', { configurable: true, value: fetchMock })
|
||||
|
||||
const { apiFetch } = await import('./api')
|
||||
await apiFetch('/auth/account/start')
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith('/dashboard/api/v1/auth/account/start', expect.any(Object))
|
||||
})
|
||||
|
||||
it('does not add a dashboard prefix when an absolute API origin is configured', async () => {
|
||||
installBrowser()
|
||||
process.env.NEXT_PUBLIC_API_URL = 'https://api.rentaldrivego.ma/api/v1'
|
||||
const fetchMock = vi.fn(async () => ({
|
||||
ok: true,
|
||||
json: async () => ({ data: { ok: true } }),
|
||||
}))
|
||||
Object.defineProperty(globalThis, 'fetch', { configurable: true, value: fetchMock })
|
||||
|
||||
const { apiFetch } = await import('./api')
|
||||
await apiFetch('/auth/account/start')
|
||||
|
||||
const calledUrl = (fetchMock as any).mock.calls[0]?.[0]
|
||||
expect(calledUrl).toBe('https://api.rentaldrivego.ma/api/v1/auth/account/start')
|
||||
expect(calledUrl).not.toContain('/dashboard')
|
||||
expect(calledUrl).not.toContain('/api/v1/api/v1')
|
||||
})
|
||||
|
||||
it('does not force JSON content type for FormData payloads', async () => {
|
||||
installBrowser()
|
||||
const fetchMock = vi.fn(async () => ({ ok: true, json: async () => ({ data: { uploaded: true } }) }))
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
function normalizeApiBase(value: string): string {
|
||||
export function normalizeApiBase(value: string): string {
|
||||
const base = value.replace(/\/+$/, '')
|
||||
if (/\/api$/.test(base)) return `${base}/v1`
|
||||
return /\/api\/v1$/.test(base) ? base : `${base}/api/v1`
|
||||
}
|
||||
|
||||
export const API_BASE = normalizeApiBase(
|
||||
typeof window === 'undefined'
|
||||
export function resolveApiBase(): string {
|
||||
const value = typeof window === 'undefined'
|
||||
? (process.env.API_INTERNAL_URL || 'http://localhost:4000/api/v1')
|
||||
: (process.env.NEXT_PUBLIC_DASHBOARD_DIRECT_API === 'true' && process.env.NEXT_PUBLIC_API_URL
|
||||
? process.env.NEXT_PUBLIC_API_URL
|
||||
: '/dashboard/api/v1'),
|
||||
)
|
||||
: (process.env.NEXT_PUBLIC_API_URL || '/dashboard/api/v1')
|
||||
|
||||
return normalizeApiBase(value)
|
||||
}
|
||||
|
||||
export const API_BASE = resolveApiBase()
|
||||
|
||||
export const EMPLOYEE_PROFILE_KEY = 'employee_profile'
|
||||
|
||||
@@ -24,7 +27,7 @@ export async function apiFetch<T>(path: string, options?: RequestInit): Promise<
|
||||
headers['Content-Type'] = 'application/json'
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_BASE}${path}`, {
|
||||
const res = await fetch(`${resolveApiBase()}${path}`, {
|
||||
...options,
|
||||
headers,
|
||||
credentials: 'include',
|
||||
@@ -49,7 +52,7 @@ export async function apiFetch<T>(path: string, options?: RequestInit): Promise<
|
||||
|
||||
export async function apiFetchServer<T>(path: string, token: string, options?: RequestInit): Promise<T> {
|
||||
const isFormData = typeof FormData !== 'undefined' && options?.body instanceof FormData
|
||||
const res = await fetch(`${API_BASE}${path}`, {
|
||||
const res = await fetch(`${resolveApiBase()}${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
...(isFormData ? {} : { 'Content-Type': 'application/json' }),
|
||||
|
||||
@@ -38,12 +38,12 @@ export const demoLeadSchema = z
|
||||
.regex(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, 'invalid_email'),
|
||||
),
|
||||
company: compactText(160),
|
||||
fleetSize: z.enum(fleetSizeValues, { error: 'invalid_fleet_size' }),
|
||||
fleetSize: z.enum(fleetSizeValues, { message: 'invalid_fleet_size' }),
|
||||
market: optionalCompactText(100),
|
||||
preferredLanguage: z.enum(['en', 'fr', 'ar']).optional(),
|
||||
message: optionalCompactText(1_000),
|
||||
idempotencyKey: z.string().uuid('invalid_idempotency_key'),
|
||||
source: z.enum(demoSourceValues, { error: 'invalid_source' }),
|
||||
source: z.enum(demoSourceValues, { message: 'invalid_source' }),
|
||||
})
|
||||
.strict();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user