import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const originalWindow = globalThis.window const originalDocument = globalThis.document function setMockWindow(runtimeConfig?: Window['__SKILLHUB_RUNTIME_CONFIG__']) { Object.defineProperty(globalThis, 'window', { configurable: true, writable: true, value: { __SKILLHUB_RUNTIME_CONFIG__: runtimeConfig, } satisfies Pick, }) } // Mock i18n before importing client vi.mock('@/i18n/config', () => ({ default: { resolvedLanguage: 'en' }, })) // Mock api-error before importing client vi.mock('@/shared/lib/api-error', () => ({ ApiError: class ApiError extends Error { status: number serverMessage?: string serverMessageKey?: string constructor(message: string, status: number, serverMessage?: string, serverMessageKey?: string) { super(message) this.status = status this.serverMessage = serverMessage this.serverMessageKey = serverMessageKey } }, handleApiError: vi.fn(), })) import { WEB_API_PREFIX, buildApiUrl, fetchText, getDirectAuthRuntimeConfig, getSessionBootstrapRuntimeConfig, namespaceApi, } from './client' beforeEach(() => { setMockWindow() }) afterEach(() => { vi.unstubAllGlobals() if (originalDocument) { Object.defineProperty(globalThis, 'document', { configurable: true, writable: true, value: originalDocument, }) } else { Reflect.deleteProperty(globalThis, 'document') } if (originalWindow) { Object.defineProperty(globalThis, 'window', { configurable: true, writable: true, value: originalWindow, }) return } Reflect.deleteProperty(globalThis, 'window') }) describe('WEB_API_PREFIX', () => { it('uses the /api/web prefix for web-facing endpoints', () => { expect(WEB_API_PREFIX).toBe('/api/web') }) }) describe('buildApiUrl', () => { it('returns the path as-is when no runtime base URL is configured', () => { expect(buildApiUrl('/api/v1/auth/me')).toBe('/api/v1/auth/me') }) it('prepends the runtime base URL when one is set', () => { window.__SKILLHUB_RUNTIME_CONFIG__ = { apiBaseUrl: 'https://api.example.com' } const url = buildApiUrl('/api/v1/auth/me') expect(url).toBe('https://api.example.com/api/v1/auth/me') }) it('handles a trailing slash on the base URL', () => { window.__SKILLHUB_RUNTIME_CONFIG__ = { apiBaseUrl: 'https://api.example.com/' } const url = buildApiUrl('/api/v1/auth/me') expect(url).toBe('https://api.example.com/api/v1/auth/me') }) it('preserves base URL path prefixes', () => { window.__SKILLHUB_RUNTIME_CONFIG__ = { apiBaseUrl: 'https://api.example.com/skill_hub' } const url = buildApiUrl('/api/v1/auth/me') expect(url).toBe('https://api.example.com/skill_hub/api/v1/auth/me') }) it('supports relative base URL path prefixes', () => { window.__SKILLHUB_RUNTIME_CONFIG__ = { apiBaseUrl: '/skill_hub' } const url = buildApiUrl('/api/v1/auth/me') expect(url).toBe('/skill_hub/api/v1/auth/me') }) }) describe('fetchText', () => { it('applies base URL path prefixes for fetch requests', async () => { window.__SKILLHUB_RUNTIME_CONFIG__ = { apiBaseUrl: 'https://api.example.com/skill_hub' } const fetchMock = vi.fn().mockResolvedValue({ ok: true, text: async () => 'ok', }) vi.stubGlobal('fetch', fetchMock) await fetchText('/api/v1/auth/me') expect(fetchMock).toHaveBeenCalledWith( 'https://api.example.com/skill_hub/api/v1/auth/me', expect.objectContaining({ headers: expect.any(Headers), }), ) }) }) describe('namespaceApi.delete', () => { it('sends a DELETE request to the normalized namespace endpoint', async () => { window.__SKILLHUB_RUNTIME_CONFIG__ = { apiBaseUrl: 'https://api.example.com' } Object.defineProperty(globalThis, 'document', { configurable: true, writable: true, value: { cookie: 'XSRF-TOKEN=test-token', }, }) const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ code: 0, msg: 'ok', data: null, timestamp: '2026-05-07T00:00:00Z', requestId: 'req-test', }), }) vi.stubGlobal('fetch', fetchMock) await namespaceApi.delete('@team-delete') expect(fetchMock).toHaveBeenCalledWith( 'https://api.example.com/api/web/namespaces/team-delete', expect.objectContaining({ method: 'DELETE', headers: expect.any(Headers), }), ) }) }) describe('getDirectAuthRuntimeConfig', () => { it('returns disabled when no runtime config is present', () => { const config = getDirectAuthRuntimeConfig() expect(config.enabled).toBe(false) expect(config.provider).toBeUndefined() }) it('returns enabled with provider when both flag and provider are set', () => { window.__SKILLHUB_RUNTIME_CONFIG__ = { authDirectEnabled: 'true', authDirectProvider: 'ldap', } const config = getDirectAuthRuntimeConfig() expect(config.enabled).toBe(true) expect(config.provider).toBe('ldap') }) it('returns disabled when the flag is true but the provider is missing', () => { window.__SKILLHUB_RUNTIME_CONFIG__ = { authDirectEnabled: 'true', } const config = getDirectAuthRuntimeConfig() expect(config.enabled).toBe(false) }) it('returns disabled when the flag is false', () => { window.__SKILLHUB_RUNTIME_CONFIG__ = { authDirectEnabled: 'false', authDirectProvider: 'ldap', } const config = getDirectAuthRuntimeConfig() expect(config.enabled).toBe(false) }) it('treats various truthy flag values correctly', () => { for (const flag of ['1', 'yes', 'on', 'TRUE', ' True ']) { window.__SKILLHUB_RUNTIME_CONFIG__ = { authDirectEnabled: flag, authDirectProvider: 'ldap', } expect(getDirectAuthRuntimeConfig().enabled).toBe(true) } }) }) describe('getSessionBootstrapRuntimeConfig', () => { it('returns disabled when no runtime config is present', () => { const config = getSessionBootstrapRuntimeConfig() expect(config.enabled).toBe(false) expect(config.auto).toBe(false) expect(config.provider).toBeUndefined() }) it('returns fully enabled config when all flags and provider are set', () => { window.__SKILLHUB_RUNTIME_CONFIG__ = { authSessionBootstrapEnabled: '1', authSessionBootstrapProvider: 'sso', authSessionBootstrapAuto: 'true', } const config = getSessionBootstrapRuntimeConfig() expect(config.enabled).toBe(true) expect(config.provider).toBe('sso') expect(config.auto).toBe(true) }) it('returns disabled when the provider is blank', () => { window.__SKILLHUB_RUNTIME_CONFIG__ = { authSessionBootstrapEnabled: 'true', authSessionBootstrapProvider: ' ', } const config = getSessionBootstrapRuntimeConfig() expect(config.enabled).toBe(false) }) })