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
+81
View File
@@ -0,0 +1,81 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
function installBrowser(token?: string) {
Object.defineProperty(globalThis, 'window', {
configurable: true,
value: {
localStorage: {
getItem: vi.fn(() => token ?? null),
},
},
})
}
afterEach(() => {
vi.restoreAllMocks()
Reflect.deleteProperty(globalThis, 'window')
Reflect.deleteProperty(globalThis, 'fetch')
vi.resetModules()
})
describe('dashboard apiFetch', () => {
it('adds JSON headers and sends cookies for browser requests', 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 expect(apiFetch('/team')).resolves.toEqual({ ok: true })
expect(fetchMock).toHaveBeenCalledWith('/dashboard/api/v1/team', expect.objectContaining({
credentials: 'include',
headers: expect.objectContaining({
'Content-Type': 'application/json',
}),
}))
})
it('does not force JSON content type for FormData payloads', async () => {
installBrowser()
const fetchMock = vi.fn(async () => ({ ok: true, json: async () => ({ data: { uploaded: true } }) }))
Object.defineProperty(globalThis, 'fetch', { configurable: true, value: fetchMock })
const { apiFetch } = await import('./api')
await apiFetch('/uploads', { method: 'POST', body: new FormData() })
expect((fetchMock as any).mock.calls[0]?.[1].headers).not.toHaveProperty('Content-Type')
expect((fetchMock as any).mock.calls[0]?.[1]).toEqual(expect.objectContaining({ credentials: 'include' }))
})
it('surfaces API error details on failed responses', async () => {
installBrowser()
Object.defineProperty(globalThis, 'fetch', {
configurable: true,
value: vi.fn(async () => ({
ok: false,
status: 403,
json: async () => ({ message: 'Forbidden', error: 'FORBIDDEN' }),
})),
})
const { apiFetch } = await import('./api')
await expect(apiFetch('/team')).rejects.toMatchObject({ message: 'Forbidden', code: 'FORBIDDEN', statusCode: 403 })
})
it('uses explicit server token and disables caching for server requests', async () => {
Reflect.deleteProperty(globalThis, 'window')
const fetchMock = vi.fn(async () => ({ ok: true, json: async () => ({ data: { ok: true } }) }))
Object.defineProperty(globalThis, 'fetch', { configurable: true, value: fetchMock })
const { apiFetchServer } = await import('./api')
await apiFetchServer('/fleet', 'server-token')
expect(fetchMock).toHaveBeenCalledWith('http://localhost:4000/api/v1/fleet', expect.objectContaining({
cache: 'no-store',
headers: expect.objectContaining({ Authorization: 'Bearer server-token' }),
}))
})
})
-15
View File
@@ -3,20 +3,9 @@ export const API_BASE =
? (process.env.API_INTERNAL_URL || 'http://localhost:4000/api/v1')
: (process.env.NEXT_PUBLIC_API_URL || '/dashboard/api/v1')
export const EMPLOYEE_TOKEN_KEY = 'employee_token'
export const EMPLOYEE_PROFILE_KEY = 'employee_profile'
async function getAuthToken(): Promise<string | null> {
if (typeof window === 'undefined') return null
try {
return localStorage.getItem(EMPLOYEE_TOKEN_KEY)
} catch {
return null
}
}
export async function apiFetch<T>(path: string, options?: RequestInit): Promise<T> {
const token = await getAuthToken()
const isFormData = typeof FormData !== 'undefined' && options?.body instanceof FormData
const headers: Record<string, string> = {
@@ -27,10 +16,6 @@ export async function apiFetch<T>(path: string, options?: RequestInit): Promise<
headers['Content-Type'] = 'application/json'
}
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
const res = await fetch(`${API_BASE}${path}`, {
...options,
headers,
+34
View File
@@ -0,0 +1,34 @@
import { afterEach, describe, expect, it } from 'vitest'
import { resolveBrowserAppUrl } from './appUrls'
function installWindow(hostname: string, protocol = 'https:') {
Object.defineProperty(globalThis, 'window', {
configurable: true,
value: { location: { hostname, protocol } },
})
}
afterEach(() => {
Reflect.deleteProperty(globalThis, 'window')
})
describe('dashboard resolveBrowserAppUrl', () => {
it('returns the fallback untouched on the server', () => {
expect(resolveBrowserAppUrl('http://localhost:3001/dashboard')).toBe('http://localhost:3001/dashboard')
})
it('keeps local browser fallbacks local and removes trailing slash', () => {
installWindow('localhost')
expect(resolveBrowserAppUrl('http://localhost:3001/dashboard/')).toBe('http://localhost:3001/dashboard')
})
it('rewrites fallback origin to the current production host', () => {
installWindow('tenant.example.com', 'https:')
expect(resolveBrowserAppUrl('http://localhost:3001/dashboard')).toBe('https://tenant.example.com/dashboard')
})
it('falls back safely when the configured value is not a URL', () => {
installWindow('tenant.example.com')
expect(resolveBrowserAppUrl('/dashboard/')).toBe('/dashboard/')
})
})
@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest'
import { toDashboardAppPath, toPublicDashboardPath } from './dashboardPaths'
describe('dashboard path normalization', () => {
it('collapses empty and root values to the app root', () => {
expect(toDashboardAppPath()).toBe('/')
expect(toDashboardAppPath('')).toBe('/')
expect(toDashboardAppPath('/')).toBe('/')
})
it('strips repeated dashboard base prefixes', () => {
expect(toDashboardAppPath('/dashboard')).toBe('/')
expect(toDashboardAppPath('/dashboard/fleet')).toBe('/fleet')
expect(toDashboardAppPath('/dashboard/dashboard/reservations')).toBe('/reservations')
})
it('adds the public deployment prefix exactly once', () => {
expect(toPublicDashboardPath('/')).toBe('/dashboard')
expect(toPublicDashboardPath('/dashboard/fleet')).toBe('/dashboard/fleet')
expect(toPublicDashboardPath('customers')).toBe('/dashboard/customers')
})
})
@@ -0,0 +1,96 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import {
getScopedPreferenceCookieName,
getScopedPreferenceKey,
readCurrentUserScopedPreference,
readScopedPreference,
writeScopedPreference,
} from './preferences'
function installBrowser(cookie = '') {
const store = new Map<string, string>()
let cookieValue = cookie
Object.defineProperty(globalThis, 'window', {
configurable: true,
value: {
localStorage: {
getItem: (key: string) => store.get(key) ?? null,
setItem: (key: string, value: string) => store.set(key, value),
removeItem: (key: string) => store.delete(key),
},
},
})
Object.defineProperty(globalThis, 'document', {
configurable: true,
value: {
get cookie() {
return cookieValue
},
set cookie(value: string) {
const [pair] = value.split(';')
const [name, val] = pair.split('=')
const existing = cookieValue
.split(';')
.map((chunk) => chunk.trim())
.filter(Boolean)
.filter((chunk) => !chunk.startsWith(`${name}=`))
existing.push(`${name}=${val}`)
cookieValue = existing.join('; ')
},
},
})
Object.defineProperty(globalThis, 'atob', {
configurable: true,
value: (value: string) => Buffer.from(value, 'base64').toString('binary'),
})
return { store, get cookie() { return cookieValue } }
}
beforeEach(() => {
installBrowser()
})
afterEach(() => {
Reflect.deleteProperty(globalThis, 'window')
Reflect.deleteProperty(globalThis, 'document')
Reflect.deleteProperty(globalThis, 'atob')
})
describe('dashboard scoped preferences', () => {
it('uses base keys when no employee token is available', () => {
expect(getScopedPreferenceKey('theme')).toBe('theme')
expect(getScopedPreferenceCookieName('theme')).toBe('theme')
})
it('prefers scoped cookie/current user values before local storage', () => {
Reflect.deleteProperty(globalThis, 'window')
Reflect.deleteProperty(globalThis, 'document')
const browser = installBrowser('theme=dark')
browser.store.set('theme', 'local-dark')
expect(readCurrentUserScopedPreference('theme')).toBe('dark')
expect(readScopedPreference('theme')).toBe('dark')
})
it('falls back through shared cookie, scoped local storage, base key, and legacy keys', () => {
const browser = installBrowser('theme=dark')
expect(readScopedPreference('theme', ['legacy-theme'])).toBe('dark')
Reflect.deleteProperty(globalThis, 'window')
Reflect.deleteProperty(globalThis, 'document')
const second = installBrowser()
second.store.set('theme', 'base')
second.store.set('legacy-theme', 'legacy')
expect(readScopedPreference('theme', ['legacy-theme'])).toBe('base')
})
it('writes shared and legacy preference values without auth-token scoping', () => {
const browser = installBrowser()
writeScopedPreference('language', 'fr', ['old-language'])
expect(browser.store.get('language')).toBe('fr')
expect(browser.store.get('old-language')).toBe('fr')
expect(browser.cookie).toContain('language=fr')
})
})
+1 -10
View File
@@ -3,16 +3,7 @@ export const SHARED_LANGUAGE_KEY = 'rentaldrivego-language'
export const SHARED_THEME_KEY = 'rentaldrivego-theme'
function readEmployeeToken() {
if (typeof window === 'undefined') return null
const localToken = window.localStorage.getItem('employee_token')
if (localToken) return localToken
return document.cookie
.split(';')
.map((chunk) => chunk.trim())
.find((chunk) => chunk.startsWith('employee_token='))
?.split('=')[1] ?? null
return null
}
function decodeEmployeeId(token: string | null) {
+64
View File
@@ -0,0 +1,64 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
function installWindow(hostname: string, protocol = 'https:') {
Object.defineProperty(globalThis, 'window', {
configurable: true,
value: { location: { hostname, protocol } },
})
}
async function loadUrls(env: Record<string, string | undefined> = {}) {
vi.resetModules()
for (const key of ['NEXT_PUBLIC_MARKETPLACE_URL', 'NEXT_PUBLIC_ADMIN_URL']) {
if (env[key] === undefined) {
delete process.env[key]
} else {
process.env[key] = env[key]
}
}
return import('./urls')
}
afterEach(() => {
Reflect.deleteProperty(globalThis, 'window')
delete process.env.NEXT_PUBLIC_MARKETPLACE_URL
delete process.env.NEXT_PUBLIC_ADMIN_URL
vi.resetModules()
})
describe('dashboard cross-app URLs', () => {
it('uses default marketplace and admin bases on the server', async () => {
const { marketplaceUrl, adminUrl } = await loadUrls()
expect(marketplaceUrl).toBe('http://localhost:3000')
expect(adminUrl).toBe('http://localhost:3000/admin')
})
it('strips trailing slashes from configured absolute URLs', async () => {
const { marketplaceUrl, adminUrl } = await loadUrls({
NEXT_PUBLIC_MARKETPLACE_URL: 'https://market.example.com/',
NEXT_PUBLIC_ADMIN_URL: 'https://ops.example.com/admin/',
})
expect(marketplaceUrl).toBe('https://market.example.com')
expect(adminUrl).toBe('https://ops.example.com/admin')
})
it('rewrites local browser fallbacks onto the current production host', async () => {
installWindow('tenant.rentaldrivego.com', 'https:')
const { marketplaceUrl, adminUrl } = await loadUrls()
expect(marketplaceUrl).toBe('https://tenant.rentaldrivego.com')
expect(adminUrl).toBe('https://tenant.rentaldrivego.com/admin')
})
it('preserves non-URL values after normalizing their trailing slash', async () => {
const { marketplaceUrl, adminUrl } = await loadUrls({
NEXT_PUBLIC_MARKETPLACE_URL: '/marketplace/',
NEXT_PUBLIC_ADMIN_URL: '/admin/',
})
expect(marketplaceUrl).toBe('/marketplace')
expect(adminUrl).toBe('/admin')
})
})