mirror of
https://github.com/gramps-project/gramps-web.git
synced 2026-04-17 12:48:04 -04:00
316 lines
7.9 KiB
JavaScript
316 lines
7.9 KiB
JavaScript
import {describe, it, expect, beforeEach, afterEach, vi} from 'vitest'
|
|
import {
|
|
apiGet,
|
|
apiGetTokens,
|
|
apiRegisterUser,
|
|
apiResetPassword,
|
|
apiGetOIDCConfig,
|
|
updateTaskStatus,
|
|
} from '../../src/api.js'
|
|
|
|
describe('apiGet authentication', () => {
|
|
beforeEach(() => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
status: 200,
|
|
ok: true,
|
|
json: () => Promise.resolve({}),
|
|
headers: {get: () => null},
|
|
})
|
|
)
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
it('includes Authorization header when auth is provided', async () => {
|
|
const mockAuth = {
|
|
getValidAccessToken: vi.fn().mockResolvedValue('test-token'),
|
|
}
|
|
|
|
await apiGet(mockAuth, '/api/tasks/test-123')
|
|
|
|
expect(fetch).toHaveBeenCalled()
|
|
const [, options] = fetch.mock.calls[0]
|
|
expect(options.headers).to.have.property(
|
|
'Authorization',
|
|
'Bearer test-token'
|
|
)
|
|
})
|
|
|
|
it('returns an error when auth is undefined and backend returns 401', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
status: 401,
|
|
ok: false,
|
|
statusText: 'Unauthorized',
|
|
json: () => Promise.resolve({error: {message: 'Unauthorized'}}),
|
|
headers: {get: () => null},
|
|
})
|
|
)
|
|
|
|
const result = await apiGet(undefined, '/api/tasks/test-123')
|
|
|
|
expect(result.error).toBeDefined()
|
|
expect(result.error).to.be.a('string')
|
|
})
|
|
})
|
|
|
|
describe('apiGetTokens error handling', () => {
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
it('returns error.message from JSON body on non-200 response', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
status: 422,
|
|
statusText: 'UNPROCESSABLE ENTITY',
|
|
json: () =>
|
|
Promise.resolve({
|
|
error: {
|
|
message: 'username: Missing data for required field.',
|
|
},
|
|
}),
|
|
})
|
|
)
|
|
|
|
const result = await apiGetTokens('', '')
|
|
expect(result.error).toBe('username: Missing data for required field.')
|
|
})
|
|
|
|
it('falls back to statusText when JSON body has no error.message', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
status: 503,
|
|
statusText: 'Service Unavailable',
|
|
json: () => Promise.resolve({}),
|
|
})
|
|
)
|
|
|
|
const result = await apiGetTokens('user', 'pass')
|
|
expect(result.error).toBe('Service Unavailable')
|
|
})
|
|
|
|
it('falls back to Error <status> when JSON parse fails', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
status: 503,
|
|
statusText: '',
|
|
json: () => Promise.reject(new SyntaxError('Unexpected token')),
|
|
})
|
|
)
|
|
|
|
const result = await apiGetTokens('user', 'pass')
|
|
expect(result.error).toBe('Error 503')
|
|
})
|
|
|
|
it('surfaces JSON parse failure on 200 response as an error', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
status: 200,
|
|
statusText: 'OK',
|
|
json: () => Promise.reject(new SyntaxError('Unexpected token')),
|
|
})
|
|
)
|
|
|
|
const result = await apiGetTokens('user', 'pass')
|
|
expect(result.error).toBeDefined()
|
|
expect(result.error).not.toBe('Access token missing in response')
|
|
})
|
|
})
|
|
|
|
describe('apiResetPassword error handling', () => {
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
it('returns error.message from JSON body on unexpected status', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
status: 422,
|
|
statusText: 'UNPROCESSABLE ENTITY',
|
|
json: () =>
|
|
Promise.resolve({error: {message: 'username: Missing field.'}}),
|
|
})
|
|
)
|
|
|
|
const result = await apiResetPassword('user')
|
|
expect(result.error).toBe('username: Missing field.')
|
|
})
|
|
|
|
it('falls back to statusText when response body is not JSON', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
status: 400,
|
|
statusText: 'Bad Request',
|
|
json: () => Promise.reject(new SyntaxError('Unexpected token')),
|
|
})
|
|
)
|
|
|
|
const result = await apiResetPassword('user')
|
|
expect(result.error).toBe('Bad Request')
|
|
})
|
|
})
|
|
|
|
describe('apiRegisterUser error handling', () => {
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
it('returns error.message from JSON body on non-201 response', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
status: 422,
|
|
statusText: 'UNPROCESSABLE ENTITY',
|
|
json: () =>
|
|
Promise.resolve({error: {message: 'password: Missing field.'}}),
|
|
})
|
|
)
|
|
|
|
const result = await apiRegisterUser('user', 'pass', '', '', '')
|
|
expect(result.error).toBe('password: Missing field.')
|
|
})
|
|
|
|
it('falls back to statusText when JSON body has no error.message', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
status: 500,
|
|
statusText: 'Internal Server Error',
|
|
json: () => Promise.resolve({}),
|
|
})
|
|
)
|
|
|
|
const result = await apiRegisterUser('user', 'pass', '', '', '')
|
|
expect(result.error).toBe('Internal Server Error')
|
|
})
|
|
})
|
|
|
|
describe('apiGetOIDCConfig error handling', () => {
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
it('returns error.message from JSON body on error response', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: false,
|
|
status: 500,
|
|
statusText: 'Internal Server Error',
|
|
json: () =>
|
|
Promise.resolve({error: {message: 'OIDC provider misconfigured.'}}),
|
|
})
|
|
)
|
|
|
|
const result = await apiGetOIDCConfig()
|
|
expect(result.error).toBe('OIDC provider misconfigured.')
|
|
})
|
|
|
|
it('falls back to statusText on error response with non-JSON body', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: false,
|
|
status: 502,
|
|
statusText: 'Bad Gateway',
|
|
json: () => Promise.reject(new SyntaxError('Unexpected token')),
|
|
})
|
|
)
|
|
|
|
const result = await apiGetOIDCConfig()
|
|
expect(result.error).toBe('Bad Gateway')
|
|
})
|
|
|
|
it('surfaces JSON parse failure on ok response as an error', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.reject(new SyntaxError('Unexpected token')),
|
|
})
|
|
)
|
|
|
|
const result = await apiGetOIDCConfig()
|
|
expect(result.error).toBeDefined()
|
|
})
|
|
})
|
|
|
|
describe('updateTaskStatus cleanup behavior', () => {
|
|
afterEach(() => {
|
|
vi.useRealTimers()
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
it('stops polling when shouldContinue becomes false', async () => {
|
|
vi.useFakeTimers()
|
|
let keepPolling = true
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
status: 200,
|
|
ok: true,
|
|
json: () =>
|
|
Promise.resolve({state: 'PENDING', result_object: {progress: 0.25}}),
|
|
headers: {get: () => null},
|
|
})
|
|
)
|
|
|
|
const callback = vi.fn(() => {
|
|
keepPolling = false
|
|
})
|
|
|
|
const promise = updateTaskStatus(
|
|
{getValidAccessToken: vi.fn().mockResolvedValue('test-token')},
|
|
'task-1',
|
|
callback,
|
|
1000,
|
|
Infinity,
|
|
() => keepPolling
|
|
)
|
|
|
|
await Promise.resolve()
|
|
await vi.runAllTimersAsync()
|
|
await promise
|
|
|
|
expect(fetch).toHaveBeenCalledTimes(1)
|
|
expect(callback).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('does not schedule another wait after a terminal task state', async () => {
|
|
vi.useFakeTimers()
|
|
const timeoutSpy = vi.spyOn(globalThis, 'setTimeout')
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
status: 200,
|
|
ok: true,
|
|
json: () => Promise.resolve({state: 'SUCCESS'}),
|
|
headers: {get: () => null},
|
|
})
|
|
)
|
|
|
|
await updateTaskStatus(
|
|
{getValidAccessToken: vi.fn().mockResolvedValue('test-token')},
|
|
'task-2',
|
|
vi.fn(),
|
|
1000
|
|
)
|
|
|
|
expect(fetch).toHaveBeenCalledTimes(1)
|
|
expect(timeoutSpy).not.toHaveBeenCalled()
|
|
})
|
|
})
|