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' }), }), ) }) }) })