test: add service tests for DirectusClient, DirectusError, categories, files, listings, and notifications
This commit is contained in:
323
tests/client.test.js
Normal file
323
tests/client.test.js
Normal file
@@ -0,0 +1,323 @@
|
||||
/**
|
||||
* 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('dgray_auth')
|
||||
localStorage.removeItem('dgray_auth')
|
||||
})
|
||||
|
||||
test('isAuthenticated returns true with valid token', () => {
|
||||
client.saveTokens('valid-token', 'refresh', 900000)
|
||||
assertTrue(client.isAuthenticated())
|
||||
|
||||
client.clearTokens()
|
||||
sessionStorage.removeItem('dgray_auth')
|
||||
localStorage.removeItem('dgray_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('dgray_auth', authData)
|
||||
|
||||
client.loadTokens()
|
||||
assertEquals(client.accessToken, 'stored-access')
|
||||
assertEquals(client.refreshToken, 'stored-refresh')
|
||||
|
||||
client.clearTokens()
|
||||
sessionStorage.removeItem('dgray_auth')
|
||||
})
|
||||
|
||||
test('loadTokens clears on invalid JSON', () => {
|
||||
sessionStorage.setItem('dgray_auth', 'not-json')
|
||||
client.loadTokens()
|
||||
assertEquals(client.accessToken, null)
|
||||
|
||||
sessionStorage.removeItem('dgray_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()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user