/** * Tests for js/services/directus/client.js */ import { test, asyncTest, describe, assertEquals, assertTrue, assertFalse, assertThrows, assertDeepEquals, assertNotEquals } from './test-runner.js' import { client, DirectusError, setPersist, getPersist } from '../js/services/directus/client.js' // ── DirectusError ── describe('DirectusError', () => { test('creates error with correct properties', () => { const err = new DirectusError(404, 'Not found', { foo: 'bar' }) assertEquals(err.status, 404) assertEquals(err.message, 'Not found') assertEquals(err.name, 'DirectusError') assertDeepEquals(err.data, { foo: 'bar' }) }) test('extends Error', () => { const err = new DirectusError(500, 'fail') assertTrue(err instanceof Error) assertTrue(err instanceof DirectusError) }) test('defaults data to empty object', () => { const err = new DirectusError(500, 'fail') assertDeepEquals(err.data, {}) }) test('isAuthError returns true for 401', () => { assertTrue(new DirectusError(401, 'Unauthorized').isAuthError()) }) test('isAuthError returns true for 403', () => { assertTrue(new DirectusError(403, 'Forbidden').isAuthError()) }) test('isAuthError returns false for other codes', () => { assertFalse(new DirectusError(404, 'Not found').isAuthError()) assertFalse(new DirectusError(500, 'Server error').isAuthError()) }) test('isNotFound returns true for 404', () => { assertTrue(new DirectusError(404, 'Not found').isNotFound()) }) test('isNotFound returns false for other codes', () => { assertFalse(new DirectusError(401, 'Unauthorized').isNotFound()) }) test('isValidationError returns true for 400', () => { assertTrue(new DirectusError(400, 'Bad request').isValidationError()) }) test('isValidationError returns false for other codes', () => { assertFalse(new DirectusError(404, 'Not found').isValidationError()) }) }) // ── buildQueryString ── describe('DirectusClient.buildQueryString', () => { test('builds simple key-value pairs', () => { const qs = client.buildQueryString({ limit: 10, offset: 5 }) assertTrue(qs.includes('limit=10')) assertTrue(qs.includes('offset=5')) }) test('joins array fields with comma', () => { const qs = client.buildQueryString({ fields: ['id', 'title', 'status'] }) assertTrue(qs.includes('fields=id%2Ctitle%2Cstatus') || qs.includes('fields=id,title,status')) }) test('joins array sort with comma', () => { const qs = client.buildQueryString({ sort: ['-date_created', 'title'] }) assertTrue(qs.includes('sort=')) }) test('stringifies object values as JSON', () => { const filter = { status: { _eq: 'published' } } const qs = client.buildQueryString({ filter }) assertTrue(qs.includes('filter=')) assertTrue(qs.includes('published')) }) test('skips null and undefined values', () => { const qs = client.buildQueryString({ a: null, b: undefined, c: 'value' }) assertFalse(qs.includes('a=')) assertFalse(qs.includes('b=')) assertTrue(qs.includes('c=value')) }) test('returns empty string for empty params', () => { assertEquals(client.buildQueryString({}), '') }) }) // ── Persist toggle ── describe('setPersist / getPersist', () => { test('defaults to false after module load', () => { // Note: may have been set by loadTokens() — just verify type assertEquals(typeof getPersist(), 'boolean') }) test('setPersist changes value', () => { const original = getPersist() setPersist(true) assertEquals(getPersist(), true) setPersist(false) assertEquals(getPersist(), false) setPersist(original) }) }) // ── Token management ── describe('DirectusClient token management', () => { test('clearTokens resets all token state', () => { client.clearTokens() assertEquals(client.accessToken, null) assertEquals(client.refreshToken, null) assertEquals(client.tokenExpiry, null) }) test('isAuthenticated returns false without token', () => { client.clearTokens() assertFalse(client.isAuthenticated()) }) test('saveTokens stores tokens and sets expiry', () => { client.saveTokens('test-access', 'test-refresh', 900000) assertEquals(client.accessToken, 'test-access') assertEquals(client.refreshToken, 'test-refresh') assertTrue(client.tokenExpiry > Date.now()) // Cleanup client.clearTokens() sessionStorage.removeItem('kashilo_auth') localStorage.removeItem('kashilo_auth') }) test('isAuthenticated returns true with valid token', () => { client.saveTokens('valid-token', 'refresh', 900000) assertTrue(client.isAuthenticated()) client.clearTokens() sessionStorage.removeItem('kashilo_auth') localStorage.removeItem('kashilo_auth') }) test('isAuthenticated returns false when token expired', () => { client.accessToken = 'expired' client.tokenExpiry = Date.now() - 10000 assertFalse(client.isAuthenticated()) client.clearTokens() }) test('loadTokens restores from storage', () => { const authData = JSON.stringify({ accessToken: 'stored-access', refreshToken: 'stored-refresh', expiry: Date.now() + 600000 }) sessionStorage.setItem('kashilo_auth', authData) client.loadTokens() assertEquals(client.accessToken, 'stored-access') assertEquals(client.refreshToken, 'stored-refresh') client.clearTokens() sessionStorage.removeItem('kashilo_auth') }) test('loadTokens clears on invalid JSON', () => { sessionStorage.setItem('kashilo_auth', 'not-json') client.loadTokens() assertEquals(client.accessToken, null) sessionStorage.removeItem('kashilo_auth') }) }) // ── HTTP request with mocked fetch ── describe('DirectusClient.request', () => { asyncTest('makes GET request with auth header', async () => { const originalFetch = window.fetch client.accessToken = 'mock-token' let capturedHeaders = null window.fetch = async (url, opts) => { capturedHeaders = opts.headers return { ok: true, status: 200, json: async () => ({ data: { id: 1 } }) } } const result = await client.request('/items/test', { method: 'GET' }) assertEquals(capturedHeaders['Authorization'], 'Bearer mock-token') assertEquals(result.data.id, 1) window.fetch = originalFetch client.clearTokens() }) asyncTest('throws DirectusError on non-ok response', async () => { const originalFetch = window.fetch window.fetch = async () => ({ ok: false, status: 404, json: async () => ({ errors: [{ message: 'Item not found' }] }) }) let caught = null try { await client.request('/items/missing') } catch (e) { caught = e } assertTrue(caught instanceof DirectusError) assertEquals(caught.status, 404) assertTrue(caught.isNotFound()) window.fetch = originalFetch }) asyncTest('throws DirectusError with status 0 on network error', async () => { const originalFetch = window.fetch window.fetch = async () => { throw new TypeError('Failed to fetch') } let caught = null try { await client.request('/items/test') } catch (e) { caught = e } assertTrue(caught instanceof DirectusError) assertEquals(caught.status, 0) assertEquals(caught.message, 'Network error') window.fetch = originalFetch }) asyncTest('returns null for 204 No Content', async () => { const originalFetch = window.fetch window.fetch = async () => ({ ok: true, status: 204 }) const result = await client.request('/items/test', { method: 'DELETE' }) assertEquals(result, null) window.fetch = originalFetch }) asyncTest('retries on 429 rate limit', async () => { const originalFetch = window.fetch let callCount = 0 window.fetch = async () => { callCount++ if (callCount === 1) { return { ok: false, status: 429, headers: { get: () => '0' }, json: async () => ({}) } } return { ok: true, status: 200, json: async () => ({ data: 'ok' }) } } const result = await client.request('/items/test') assertEquals(callCount, 2) assertEquals(result.data, 'ok') window.fetch = originalFetch }) asyncTest('does not retry 401 on auth endpoints', async () => { const originalFetch = window.fetch client.refreshToken = 'some-token' let callCount = 0 window.fetch = async () => { callCount++ return { ok: false, status: 401, json: async () => ({ errors: [{ message: 'Unauthorized' }] }) } } let caught = null try { await client.request('/auth/login') } catch (e) { caught = e } assertTrue(caught instanceof DirectusError) assertEquals(caught.status, 401) assertEquals(callCount, 1) window.fetch = originalFetch client.clearTokens() }) })