/** * Tests for XBVR Importer */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { importFromXBVR, XBVRConfig, ImportProgress } from '../xbvrImporter'; // Mock global fetch global.fetch = vi.fn(); describe('xbvrImporter', () => { const mockConfig: XBVRConfig = { url: 'http://localhost:9999', apiKey: 'test-api-key', updateExisting: false }; const mockLogCallback = vi.fn(); const mockProgressCallback = vi.fn(); beforeEach(() => { vi.clearAllMocks(); vi.mocked(fetch).mockClear(); }); describe('importFromXBVR', () => { it('should successfully import videos and actors from XBVR', async () => { // Mock existing media and cast check vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ data: { items: [] } }) } as Response); vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ data: { items: [] } }) } as Response); // Mock scene list fetch vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ scenes: [ { name: 'Recent', list: [ { title: 'Test Video', videoLength: 1800, thumbnailUrl: 'http://example.com/thumb.jpg', video_url: 'http://example.com/api/video/1' } ] }, { name: 'Favorites', list: [] } ] }) } as Response); // Mock video detail fetch vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ id: 1, title: 'Test Video', description: 'A test VR video', date: 1704067200, // 2024-01-01 thumbnailUrl: 'http://example.com/thumb.jpg', rating_avg: 8.5, screenType: '180', stereoMode: 'sbs', videoLength: 1800, paysite: { name: 'Test Studio' }, actors: [ { id: 1, name: 'Actor 1' }, { id: 2, name: 'Actor 2' } ], categories: [ { tag: { name: 'VR' } }, { tag: { name: '180°' } } ] }) } as Response); // Mock actor creation vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ id: 'cast-1' }) } as Response); vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ id: 'cast-2' }) } as Response); // Mock media creation vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ id: 'media-1' }) } as Response); const result = await importFromXBVR( mockConfig, mockLogCallback, mockProgressCallback ); expect(result.stage).toBe('complete'); expect(result.videosImported).toBe(1); expect(result.actorsImported).toBe(2); expect(result.errors).toHaveLength(0); expect(mockLogCallback).toHaveBeenCalledWith('Starting DeoVR import...'); }); it('should handle connection errors', async () => { vi.mocked(fetch).mockRejectedValueOnce(new Error('Connection failed')); const result = await importFromXBVR( mockConfig, mockLogCallback, mockProgressCallback ); expect(result.stage).toBe('error'); expect(result.errors).toContain('Connection failed'); }); it('should handle API response errors', async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ data: { items: [] } }) } as Response); vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ data: { items: [] } }) } as Response); vi.mocked(fetch).mockResolvedValueOnce({ ok: false, statusText: 'Unauthorized' } as Response); const result = await importFromXBVR( mockConfig, mockLogCallback, mockProgressCallback ); expect(result.stage).toBe('error'); expect(result.errors).toContain('Failed to connect to DeoVR API: Unauthorized'); }); it('should skip videos starting with aka:', async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ data: { items: [] } }) } as Response); vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ data: { items: [] } }) } as Response); vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ scenes: [ { name: 'Recent', list: [ { title: 'aka: Test Video', videoLength: 1800, thumbnailUrl: 'http://example.com/thumb.jpg', video_url: 'http://example.com/api/video/1' } ] } ] }) } as Response); vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ id: 1, title: 'aka: Test Video', date: 1704067200, videoLength: 1800, actors: [], categories: [] }) } as Response); const result = await importFromXBVR( mockConfig, mockLogCallback, mockProgressCallback ); expect(result.videosImported).toBe(0); expect(mockLogCallback).toHaveBeenCalledWith('⊘ Skipped \'aka:\' video: aka: Test Video'); }); it('should skip actors containing aka:', async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ data: { items: [] } }) } as Response); vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ data: { items: [] } }) } as Response); vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ scenes: [ { name: 'Recent', list: [ { title: 'Test Video', videoLength: 1800, thumbnailUrl: 'http://example.com/thumb.jpg', video_url: 'http://example.com/api/video/1' } ] } ] }) } as Response); vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ id: 1, title: 'Test Video', date: 1704067200, videoLength: 1800, actors: [ { id: 1, name: 'Actor 1' }, { id: 2, name: 'aka: Actor 2' } ], categories: [] }) } as Response); vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ id: 'cast-1' }) } as Response); vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ id: 'media-1' }) } as Response); const result = await importFromXBVR( mockConfig, mockLogCallback, mockProgressCallback ); expect(result.actorsImported).toBe(1); expect(mockLogCallback).toHaveBeenCalledWith('⊘ Skipped \'aka:\' actor: aka: Actor 2'); }); it('should skip existing videos when updateExisting is false', async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ data: { items: [ { id: 'media-1', title: 'Test Video' } ] } }) } as Response); vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ data: { items: [] } }) } as Response); vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ scenes: [ { name: 'Recent', list: [ { title: 'Test Video', videoLength: 1800, thumbnailUrl: 'http://example.com/thumb.jpg', video_url: 'http://example.com/api/video/1' } ] } ] }) } as Response); vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ id: 1, title: 'Test Video', date: 1704067200, videoLength: 1800, actors: [], categories: [] }) } as Response); const result = await importFromXBVR( mockConfig, mockLogCallback, mockProgressCallback ); expect(result.videosImported).toBe(0); expect(mockLogCallback).toHaveBeenCalledWith('⊘ Skipped duplicate: Test Video (updateExisting is false)'); }); it('should determine aspect ratio based on screenType and stereoMode', async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ data: { items: [] } }) } as Response); vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ data: { items: [] } }) } as Response); vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ scenes: [ { name: 'Recent', list: [ { title: '360 Video', videoLength: 1800, thumbnailUrl: 'http://example.com/thumb.jpg', video_url: 'http://example.com/api/video/1' } ] } ] }) } as Response); vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ id: 1, title: '360 Video', date: 1704067200, videoLength: 1800, screenType: '360', stereoMode: 'sbs', actors: [], categories: [] }) } as Response); vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ id: 'media-1' }) } as Response); const result = await importFromXBVR( mockConfig, mockLogCallback, mockProgressCallback ); expect(result.videosImported).toBe(1); }); it('should convert Unix timestamp to date', async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ data: { items: [] } }) } as Response); vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ data: { items: [] } }) } as Response); vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ scenes: [ { name: 'Recent', list: [ { title: 'Test Video', videoLength: 1800, thumbnailUrl: 'http://example.com/thumb.jpg', video_url: 'http://example.com/api/video/1' } ] } ] }) } as Response); vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ id: 1, title: 'Test Video', date: 1704067200, // 2024-01-01 videoLength: 1800, actors: [], categories: [] }) } as Response); vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ id: 'media-1' }) } as Response); const result = await importFromXBVR( mockConfig, mockLogCallback, mockProgressCallback ); expect(result.videosImported).toBe(1); }); it('should handle missing Recent scene group', async () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ data: { items: [] } }) } as Response); vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ data: { items: [] } }) } as Response); vi.mocked(fetch).mockResolvedValueOnce({ ok: true, json: async () => ({ scenes: [ { name: 'Favorites', list: [] } ] }) } as Response); const result = await importFromXBVR( mockConfig, mockLogCallback, mockProgressCallback ); expect(result.videosImported).toBe(0); expect(result.actorsImported).toBe(0); expect(mockLogCallback).toHaveBeenCalledWith('Found 0 videos in \'Recent\' scene group'); }); }); describe('XBVRConfig', () => { it('should accept valid configuration', () => { const config: XBVRConfig = { url: 'http://localhost:9999', apiKey: 'test-api-key' }; expect(config.url).toBe('http://localhost:9999'); expect(config.apiKey).toBe('test-api-key'); expect(config.updateExisting).toBeUndefined(); }); it('should accept configuration with optional fields', () => { const config: XBVRConfig = { url: 'http://localhost:9999', apiKey: 'test-api-key', updateExisting: true }; expect(config.updateExisting).toBe(true); }); }); describe('ImportProgress', () => { it('should have correct structure', () => { const progress: ImportProgress = { current: 5, total: 10, stage: 'importing', message: 'Importing...', videosImported: 5, actorsImported: 3, errors: [] }; expect(progress.current).toBe(5); expect(progress.total).toBe(10); expect(progress.stage).toBe('importing'); expect(progress.videosImported).toBe(5); expect(progress.actorsImported).toBe(3); expect(progress.errors).toHaveLength(0); }); }); });