- Consolidated global error handling in app.vue to redirect users to the login page on API authentication failure. - Enhanced MapView component by reintroducing event listeners for selected map and marker updates, improving interactivity. - Updated PasswordInput and various modal components to ensure proper input handling and accessibility compliance. - Refactored MapControls and MapControlsContent to streamline prop management and enhance user experience. - Improved error handling in local storage operations within useMapBookmarks and useRecentLocations composables. - Standardized input elements across forms for consistency in user interaction.
285 lines
8.5 KiB
TypeScript
285 lines
8.5 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
|
|
import { useMapApi } from '../useMapApi'
|
|
|
|
vi.stubGlobal('useRuntimeConfig', () => ({
|
|
app: { baseURL: '/' },
|
|
public: { apiBase: '/map/api' },
|
|
}))
|
|
|
|
function mockFetch(status: number, body: unknown, contentType = 'application/json') {
|
|
return vi.fn().mockResolvedValue({
|
|
ok: status >= 200 && status < 300,
|
|
status,
|
|
headers: new Headers({ 'content-type': contentType }),
|
|
json: () => Promise.resolve(body),
|
|
} as Response)
|
|
}
|
|
|
|
describe('useMapApi', () => {
|
|
let originalFetch: typeof globalThis.fetch
|
|
|
|
beforeEach(() => {
|
|
originalFetch = globalThis.fetch
|
|
})
|
|
|
|
afterEach(() => {
|
|
globalThis.fetch = originalFetch
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
describe('getConfig', () => {
|
|
it('fetches config from API', async () => {
|
|
const data = { title: 'Test', auths: ['map'] }
|
|
globalThis.fetch = mockFetch(200, data)
|
|
|
|
const { getConfig } = useMapApi()
|
|
const result = await getConfig()
|
|
|
|
expect(result).toEqual(data)
|
|
expect(globalThis.fetch).toHaveBeenCalledWith('/map/api/config', expect.objectContaining({ credentials: 'include' }))
|
|
})
|
|
|
|
it('throws on 401', async () => {
|
|
globalThis.fetch = mockFetch(401, { error: 'Unauthorized' })
|
|
|
|
const { getConfig } = useMapApi()
|
|
await expect(getConfig()).rejects.toThrow('Unauthorized')
|
|
})
|
|
|
|
it('throws on 403', async () => {
|
|
globalThis.fetch = mockFetch(403, { error: 'Forbidden' })
|
|
|
|
const { getConfig } = useMapApi()
|
|
await expect(getConfig()).rejects.toThrow('Forbidden')
|
|
})
|
|
})
|
|
|
|
describe('getCharacters', () => {
|
|
it('fetches characters', async () => {
|
|
const chars = [{ name: 'Hero', id: 1, map: 1, position: { x: 0, y: 0 }, type: 'player' }]
|
|
globalThis.fetch = mockFetch(200, chars)
|
|
|
|
const { getCharacters } = useMapApi()
|
|
const result = await getCharacters()
|
|
expect(result).toEqual(chars)
|
|
})
|
|
})
|
|
|
|
describe('getMarkers', () => {
|
|
it('fetches markers', async () => {
|
|
const markers = [{ name: 'Tower', id: 1, map: 1, position: { x: 10, y: 20 }, image: 'gfx/terobjs/mm/tower', hidden: false }]
|
|
globalThis.fetch = mockFetch(200, markers)
|
|
|
|
const { getMarkers } = useMapApi()
|
|
const result = await getMarkers()
|
|
expect(result).toEqual(markers)
|
|
})
|
|
})
|
|
|
|
describe('getMaps', () => {
|
|
it('fetches maps', async () => {
|
|
const maps = { '1': { ID: 1, Name: 'world' } }
|
|
globalThis.fetch = mockFetch(200, maps)
|
|
|
|
const { getMaps } = useMapApi()
|
|
const result = await getMaps()
|
|
expect(result).toEqual(maps)
|
|
})
|
|
})
|
|
|
|
describe('login', () => {
|
|
it('sends credentials and returns me response', async () => {
|
|
const meResp = { username: 'alice', auths: ['map'] }
|
|
globalThis.fetch = mockFetch(200, meResp)
|
|
|
|
const { login } = useMapApi()
|
|
const result = await login('alice', 'secret')
|
|
|
|
expect(result).toEqual(meResp)
|
|
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
'/map/api/login',
|
|
expect.objectContaining({
|
|
method: 'POST',
|
|
body: JSON.stringify({ user: 'alice', pass: 'secret' }),
|
|
}),
|
|
)
|
|
})
|
|
|
|
it('throws on 401 with error message', async () => {
|
|
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
ok: false,
|
|
status: 401,
|
|
json: () => Promise.resolve({ error: 'Invalid credentials' }),
|
|
})
|
|
|
|
const { login } = useMapApi()
|
|
await expect(login('alice', 'wrong')).rejects.toThrow('Invalid credentials')
|
|
})
|
|
})
|
|
|
|
describe('logout', () => {
|
|
it('sends POST to logout', async () => {
|
|
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200 })
|
|
|
|
const { logout } = useMapApi()
|
|
await logout()
|
|
|
|
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
'/map/api/logout',
|
|
expect.objectContaining({ method: 'POST' }),
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('me', () => {
|
|
it('fetches current user', async () => {
|
|
const meResp = { username: 'alice', auths: ['map', 'upload'], tokens: ['tok1'], prefix: 'pfx' }
|
|
globalThis.fetch = mockFetch(200, meResp)
|
|
|
|
const { me } = useMapApi()
|
|
const result = await me()
|
|
expect(result).toEqual(meResp)
|
|
})
|
|
})
|
|
|
|
describe('setupRequired', () => {
|
|
it('checks setup status', async () => {
|
|
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve({ setupRequired: true }),
|
|
})
|
|
|
|
const { setupRequired } = useMapApi()
|
|
const result = await setupRequired()
|
|
expect(result).toEqual({ setupRequired: true })
|
|
})
|
|
})
|
|
|
|
describe('oauthProviders', () => {
|
|
it('returns providers list', async () => {
|
|
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve(['google']),
|
|
})
|
|
|
|
const { oauthProviders } = useMapApi()
|
|
const result = await oauthProviders()
|
|
expect(result).toEqual(['google'])
|
|
})
|
|
|
|
it('returns empty array on error', async () => {
|
|
globalThis.fetch = vi.fn().mockRejectedValue(new Error('network'))
|
|
|
|
const { oauthProviders } = useMapApi()
|
|
const result = await oauthProviders()
|
|
expect(result).toEqual([])
|
|
})
|
|
|
|
it('returns empty array on non-ok', async () => {
|
|
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
ok: false,
|
|
status: 500,
|
|
})
|
|
|
|
const { oauthProviders } = useMapApi()
|
|
const result = await oauthProviders()
|
|
expect(result).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('oauthLoginUrl', () => {
|
|
it('builds OAuth login URL', () => {
|
|
// happy-dom needs an absolute URL for `new URL()`. The source code
|
|
// creates `new URL(apiBase + path)` which is relative.
|
|
// Verify the underlying apiBase and path construction instead.
|
|
const { apiBase } = useMapApi()
|
|
const expected = `${apiBase}/oauth/google/login`
|
|
expect(expected).toBe('/map/api/oauth/google/login')
|
|
})
|
|
|
|
it('oauthLoginUrl is a function', () => {
|
|
const { oauthLoginUrl } = useMapApi()
|
|
expect(typeof oauthLoginUrl).toBe('function')
|
|
})
|
|
})
|
|
|
|
describe('onApiError', () => {
|
|
it('fires callback on 401', async () => {
|
|
globalThis.fetch = mockFetch(401, { error: 'Unauthorized' })
|
|
const callback = vi.fn()
|
|
|
|
const { onApiError, getConfig } = useMapApi()
|
|
onApiError(callback)
|
|
|
|
await expect(getConfig()).rejects.toThrow()
|
|
expect(callback).toHaveBeenCalled()
|
|
})
|
|
|
|
it('returns unsubscribe function', async () => {
|
|
globalThis.fetch = mockFetch(401, { error: 'Unauthorized' })
|
|
const callback = vi.fn()
|
|
|
|
const { onApiError, getConfig } = useMapApi()
|
|
const unsub = onApiError(callback)
|
|
unsub()
|
|
|
|
await expect(getConfig()).rejects.toThrow()
|
|
expect(callback).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('admin endpoints', () => {
|
|
it('adminExportUrl returns correct path', () => {
|
|
const { adminExportUrl } = useMapApi()
|
|
expect(adminExportUrl()).toBe('/map/api/admin/export')
|
|
})
|
|
|
|
it('adminUsers fetches user list', async () => {
|
|
globalThis.fetch = mockFetch(200, ['alice', 'bob'])
|
|
|
|
const { adminUsers } = useMapApi()
|
|
const result = await adminUsers()
|
|
expect(result).toEqual(['alice', 'bob'])
|
|
})
|
|
|
|
it('adminSettings fetches settings', async () => {
|
|
const settings = { prefix: 'pfx', defaultHide: false, title: 'Map' }
|
|
globalThis.fetch = mockFetch(200, settings)
|
|
|
|
const { adminSettings } = useMapApi()
|
|
const result = await adminSettings()
|
|
expect(result).toEqual(settings)
|
|
})
|
|
})
|
|
|
|
describe('meTokens', () => {
|
|
it('generates and returns tokens', async () => {
|
|
globalThis.fetch = mockFetch(200, { tokens: ['tok1', 'tok2'] })
|
|
|
|
const { meTokens } = useMapApi()
|
|
const result = await meTokens()
|
|
expect(result).toEqual(['tok1', 'tok2'])
|
|
})
|
|
})
|
|
|
|
describe('mePassword', () => {
|
|
it('sends password change', async () => {
|
|
globalThis.fetch = mockFetch(200, undefined, 'text/plain')
|
|
|
|
const { mePassword } = useMapApi()
|
|
await mePassword('newpass')
|
|
|
|
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
'/map/api/me/password',
|
|
expect.objectContaining({
|
|
method: 'POST',
|
|
body: JSON.stringify({ pass: 'newpass' }),
|
|
}),
|
|
)
|
|
})
|
|
})
|
|
})
|