Spaces:
Paused
Paused
Download web/src/__tests__/lib/api.spec.ts from stremio-db/a: direct link, hf CLI and curl.
- Browser
- Download file 14.6 kB
-
https://huggingface.co/spaces/stremio-db/a/resolve/main/web/src/__tests__/lib/api.spec.ts
- Command line
-
hf download hf://spaces/stremio-db/a/web/src/__tests__/lib/api.spec.ts
-
curl -L -o api.spec.ts https://huggingface.co/spaces/stremio-db/a/resolve/main/web/src/__tests__/lib/api.spec.ts
14.6 kB
| import { describe, it, expect, vi, beforeEach } from 'vitest' | |
| import { createPinia, setActivePinia } from 'pinia' | |
| const mockAuthStore = { | |
| token: 'test-token', | |
| refresh: vi.fn(), | |
| logout: vi.fn(), | |
| } | |
| vi.mock('@/stores/auth', () => ({ | |
| useAuthStore: () => mockAuthStore, | |
| })) | |
| import { get, post, put, del, setOnAuthFailure, adminApi } from '@/lib/api' | |
| const mockOnAuthFailure = vi.fn() | |
| setOnAuthFailure(mockOnAuthFailure) | |
| function makeFetchResponse(status: number, body: unknown = {}) { | |
| return { | |
| ok: status >= 200 && status < 300, | |
| status, | |
| json: () => Promise.resolve(body), | |
| text: () => Promise.resolve(JSON.stringify(body)), | |
| } | |
| } | |
| describe('api', () => { | |
| beforeEach(() => { | |
| setActivePinia(createPinia()) | |
| vi.restoreAllMocks() | |
| mockAuthStore.token = 'test-token' | |
| mockAuthStore.refresh = vi.fn() | |
| mockAuthStore.logout = vi.fn() | |
| mockOnAuthFailure.mockClear() | |
| }) | |
| it('get adds Authorization header when token exists', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200, { data: 'ok' })) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await get('/api/test') | |
| expect(fetchMock).toHaveBeenCalledTimes(1) | |
| const [url, options] = fetchMock.mock.calls[0] | |
| expect(url).toBe('/api/test') | |
| expect(options.headers.get('Authorization')).toBe('Bearer test-token') | |
| }) | |
| it('post sends JSON body', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await post('/api/items', { name: 'test' }) | |
| const [, options] = fetchMock.mock.calls[0] | |
| expect(options.method).toBe('POST') | |
| expect(options.headers.get('Content-Type')).toBe('application/json') | |
| expect(options.body).toBe(JSON.stringify({ name: 'test' })) | |
| }) | |
| it('del uses DELETE method', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await del('/api/items/1') | |
| const [, options] = fetchMock.mock.calls[0] | |
| expect(options.method).toBe('DELETE') | |
| }) | |
| it('401 handling: attempts refresh, retries request on success', async () => { | |
| mockAuthStore.refresh.mockResolvedValue(true) | |
| mockAuthStore.token = 'refreshed-token' | |
| const fetchMock = vi | |
| .fn() | |
| .mockResolvedValueOnce(makeFetchResponse(401)) | |
| .mockResolvedValueOnce(makeFetchResponse(200, { data: 'ok' })) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await get('/api/protected') | |
| expect(mockAuthStore.refresh).toHaveBeenCalled() | |
| expect(fetchMock).toHaveBeenCalledTimes(2) | |
| }) | |
| it('401 + failed refresh: calls logout', async () => { | |
| mockAuthStore.refresh.mockResolvedValue(false) | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(401)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await get('/api/protected') | |
| expect(mockAuthStore.refresh).toHaveBeenCalled() | |
| expect(mockAuthStore.logout).toHaveBeenCalled() | |
| expect(mockOnAuthFailure).toHaveBeenCalled() | |
| }) | |
| it('non-401 error passes through without refresh', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(500, { error: 'server error' })) | |
| vi.stubGlobal('fetch', fetchMock) | |
| const res = await get('/api/broken') | |
| expect(res.status).toBe(500) | |
| expect(mockAuthStore.refresh).not.toHaveBeenCalled() | |
| expect(mockAuthStore.logout).not.toHaveBeenCalled() | |
| }) | |
| it('includes credentials in fetch calls', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await get('/api/test') | |
| const [, options] = fetchMock.mock.calls[0] | |
| expect(options.credentials).toBe('include') | |
| }) | |
| it('does not set Authorization header when token is null', async () => { | |
| mockAuthStore.token = null as unknown as string | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await get('/api/public') | |
| const [, options] = fetchMock.mock.calls[0] | |
| expect(options.headers.has('Authorization')).toBe(false) | |
| }) | |
| it('network error propagates', async () => { | |
| const fetchMock = vi.fn().mockRejectedValue(new TypeError('Failed to fetch')) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await expect(get('/api/test')).rejects.toThrow('Failed to fetch') | |
| }) | |
| it('401 without token does not attempt refresh', async () => { | |
| mockAuthStore.token = null as unknown as string | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(401)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| const res = await get('/api/protected') | |
| expect(res.status).toBe(401) | |
| expect(mockAuthStore.refresh).not.toHaveBeenCalled() | |
| }) | |
| it('put sends JSON body with PUT method', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await put('/api/settings', { image_source: 'f' }) | |
| const [, options] = fetchMock.mock.calls[0] | |
| expect(options.method).toBe('PUT') | |
| expect(options.headers.get('Content-Type')).toBe('application/json') | |
| expect(options.body).toBe(JSON.stringify({ image_source: 'f' })) | |
| }) | |
| it('post without body does not set Content-Type', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await post('/api/action') | |
| const [, options] = fetchMock.mock.calls[0] | |
| expect(options.headers.has('Content-Type')).toBe(false) | |
| expect(options.body).toBeUndefined() | |
| }) | |
| it('adminApi.fetchPoster calls POST with correct URL', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await adminApi.fetchPoster('imdb', 'tt0111161') | |
| const [url, options] = fetchMock.mock.calls[0] | |
| expect(url).toBe('/api/admin/posters/imdb/tt0111161/fetch') | |
| expect(options.method).toBe('POST') | |
| }) | |
| it('adminApi.fetchPoster works with tmdb id type', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await adminApi.fetchPoster('tmdb', '550') | |
| const [url] = fetchMock.mock.calls[0] | |
| expect(url).toBe('/api/admin/posters/tmdb/550/fetch') | |
| }) | |
| it('adminApi.previewPoster calls GET with correct URL and params', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await adminApi.previewPoster(3, 'imdb,rt,tmdb') | |
| const [url] = fetchMock.mock.calls[0] | |
| expect(url).toContain('/api/admin/preview/poster') | |
| expect(url).toContain('ratings_limit=3') | |
| expect(url).toContain('ratings_order=imdb%2Crt%2Ctmdb') | |
| }) | |
| it('adminApi.previewPoster includes position when provided', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await adminApi.previewPoster(3, 'imdb,rt', 'l') | |
| const [url] = fetchMock.mock.calls[0] | |
| expect(url).toContain('position=l') | |
| }) | |
| it('adminApi.previewPoster omits position when not provided', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await adminApi.previewPoster(3, 'imdb,rt') | |
| const [url] = fetchMock.mock.calls[0] | |
| expect(url).not.toContain('position') | |
| }) | |
| it('adminApi.previewPoster includes label_style when provided', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await adminApi.previewPoster(3, 'imdb,rt', 'bc', 'h', 'i') | |
| const [url] = fetchMock.mock.calls[0] | |
| expect(url).toContain('label_style=i') | |
| }) | |
| it('adminApi.previewPoster includes badge_direction when provided', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await adminApi.previewPoster(3, 'imdb,rt', 'bc', 'h', 'i', 'v') | |
| const [url] = fetchMock.mock.calls[0] | |
| expect(url).toContain('badge_direction=v') | |
| }) | |
| it('adminApi.previewPoster omits badge_direction when not provided', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await adminApi.previewPoster(3, 'imdb,rt') | |
| const [url] = fetchMock.mock.calls[0] | |
| expect(url).not.toContain('badge_direction') | |
| }) | |
| it('adminApi.previewLogo includes label_style when provided', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await adminApi.previewLogo(3, 'imdb,rt', 'h', 'i') | |
| const [url] = fetchMock.mock.calls[0] | |
| expect(url).toContain('label_style=i') | |
| }) | |
| it('adminApi.previewBackdrop includes label_style when provided', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await adminApi.previewBackdrop(3, 'imdb,rt', 'v', 'i') | |
| const [url] = fetchMock.mock.calls[0] | |
| expect(url).toContain('label_style=i') | |
| }) | |
| it('adminApi.getLogos calls GET with correct URL', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await adminApi.getLogos(1, 50) | |
| const [url] = fetchMock.mock.calls[0] | |
| expect(url).toBe('/api/admin/logos?page=1&page_size=50') | |
| }) | |
| it('adminApi.getLogoImage calls GET with correct URL', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await adminApi.getLogoImage('imdb/tt0111161') | |
| const [url] = fetchMock.mock.calls[0] | |
| expect(url).toBe('/api/admin/logos/imdb/tt0111161') | |
| }) | |
| it('adminApi.fetchLogo calls POST with correct URL', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await adminApi.fetchLogo('imdb', 'tt0111161') | |
| const [url, options] = fetchMock.mock.calls[0] | |
| expect(url).toBe('/api/admin/logos/imdb/tt0111161/fetch') | |
| expect(options.method).toBe('POST') | |
| }) | |
| it('adminApi.getBackdrops calls GET with correct URL', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await adminApi.getBackdrops(1, 50) | |
| const [url] = fetchMock.mock.calls[0] | |
| expect(url).toBe('/api/admin/backdrops?page=1&page_size=50') | |
| }) | |
| it('adminApi.getBackdropImage calls GET with correct URL', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await adminApi.getBackdropImage('tmdb/550') | |
| const [url] = fetchMock.mock.calls[0] | |
| expect(url).toBe('/api/admin/backdrops/tmdb/550') | |
| }) | |
| it('adminApi.fetchBackdrop calls POST with correct URL', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await adminApi.fetchBackdrop('tmdb', '550') | |
| const [url, options] = fetchMock.mock.calls[0] | |
| expect(url).toBe('/api/admin/backdrops/tmdb/550/fetch') | |
| expect(options.method).toBe('POST') | |
| }) | |
| it('adminApi.getEpisodes calls GET with correct URL', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await adminApi.getEpisodes(1, 50) | |
| const [url] = fetchMock.mock.calls[0] | |
| expect(url).toBe('/api/admin/episodes?page=1&page_size=50') | |
| }) | |
| it('adminApi.getEpisodeImage calls GET with correct URL', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await adminApi.getEpisodeImage('tmdb/episode-1396-S1E1') | |
| const [url] = fetchMock.mock.calls[0] | |
| expect(url).toBe('/api/admin/episodes/tmdb/episode-1396-S1E1/image') | |
| }) | |
| it('adminApi.fetchEpisode calls POST with correct URL', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await adminApi.fetchEpisode('tmdb', 'episode-1396-S1E1') | |
| const [url, options] = fetchMock.mock.calls[0] | |
| expect(url).toBe('/api/admin/episodes/tmdb/episode-1396-S1E1/fetch') | |
| expect(options.method).toBe('POST') | |
| }) | |
| it('adminApi.previewEpisode calls GET with correct URL and params', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await adminApi.previewEpisode(3, 'imdb,rt,tmdb') | |
| const [url] = fetchMock.mock.calls[0] | |
| expect(url).toContain('/api/admin/preview/episode') | |
| expect(url).toContain('ratings_limit=3') | |
| expect(url).toContain('ratings_order=imdb%2Crt%2Ctmdb') | |
| }) | |
| it('adminApi.previewEpisode includes position when provided', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await adminApi.previewEpisode(3, 'imdb,rt', undefined, undefined, undefined, 'tr') | |
| const [url] = fetchMock.mock.calls[0] | |
| expect(url).toContain('position=tr') | |
| }) | |
| it('adminApi.previewEpisode includes badge_direction when provided', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await adminApi.previewEpisode(3, 'imdb,rt', undefined, undefined, undefined, 'tr', 'h') | |
| const [url] = fetchMock.mock.calls[0] | |
| expect(url).toContain('badge_direction=h') | |
| }) | |
| it('adminApi.previewEpisode includes blur=true when enabled', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await adminApi.previewEpisode(3, 'imdb,rt', undefined, undefined, undefined, undefined, undefined, true) | |
| const [url] = fetchMock.mock.calls[0] | |
| expect(url).toContain('blur=true') | |
| }) | |
| it('adminApi.previewEpisode omits blur when false', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await adminApi.previewEpisode(3, 'imdb,rt', undefined, undefined, undefined, undefined, undefined, false) | |
| const [url] = fetchMock.mock.calls[0] | |
| expect(url).not.toContain('blur') | |
| }) | |
| it('adminApi.previewEpisode includes label_style when provided', async () => { | |
| const fetchMock = vi.fn().mockResolvedValue(makeFetchResponse(200)) | |
| vi.stubGlobal('fetch', fetchMock) | |
| await adminApi.previewEpisode(3, 'imdb,rt', 'v', 'i') | |
| const [url] = fetchMock.mock.calls[0] | |
| expect(url).toContain('label_style=i') | |
| }) | |
| }) | |