import { TestBed } from '@angular/core/testing'; import { provideHttpClient, HttpErrorResponse } from '@angular/common/http'; import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing'; import { Subject } from 'rxjs'; import { DownloadsService, AddDownloadPayload } from './downloads.service'; import { MeTubeSocket } from './metube-socket.service'; import { Download } from '../interfaces'; class MeTubeSocketStub { private subjects: Record> = {}; fromEvent(event: string) { if (!this.subjects[event]) { this.subjects[event] = new Subject(); } return this.subjects[event].asObservable(); } emit(event: string, data: string) { if (!this.subjects[event]) { this.subjects[event] = new Subject(); } this.subjects[event].next(data); } } function basePayload(): AddDownloadPayload { return { url: 'https://example.com/v', downloadType: 'video', codec: 'auto', quality: 'best', format: 'any', folder: '', customNamePrefix: '', playlistItemLimit: 0, autoStart: true, splitByChapters: false, chapterTemplate: '', subtitleLanguage: 'en', subtitleMode: 'prefer_manual', }; } describe('DownloadsService', () => { let socket: MeTubeSocketStub; let httpMock: HttpTestingController; let service: DownloadsService; beforeEach(async () => { socket = new MeTubeSocketStub(); await TestBed.configureTestingModule({ providers: [ DownloadsService, provideHttpClient(), provideHttpClientTesting(), { provide: MeTubeSocket, useValue: socket }, ], }).compileComponents(); service = TestBed.inject(DownloadsService); httpMock = TestBed.inject(HttpTestingController); }); it('add() posts snake_case fields matching backend', () => { service.add(basePayload()).subscribe(); const req = httpMock.expectOne('add'); expect(req.request.method).toBe('POST'); expect(req.request.body).toEqual( expect.objectContaining({ url: 'https://example.com/v', download_type: 'video', codec: 'auto', quality: 'best', format: 'any', playlist_item_limit: 0, auto_start: true, split_by_chapters: false, chapter_template: '', subtitle_language: 'en', subtitle_mode: 'prefer_manual', }), ); req.flush({ status: 'ok' }); }); it('cancelAdd posts to cancel-add', () => { service.cancelAdd().subscribe(); const req = httpMock.expectOne('cancel-add'); expect(req.request.method).toBe('POST'); req.flush({ status: 'ok' }); }); it('startById posts ids', () => { service.startById(['a', 'b']).subscribe(); const req = httpMock.expectOne('start'); expect(req.request.body).toEqual({ ids: ['a', 'b'] }); req.flush({}); }); it('delById marks items deleting and posts delete', () => { const dl: Download = { id: '1', title: 't', url: 'u1', download_type: 'video', quality: 'best', format: 'any', folder: '', custom_name_prefix: '', playlist_item_limit: 0, status: 'finished', msg: '', percent: 0, speed: 0, eta: 0, filename: '', checked: false, deleting: false, }; service.queue.set('u1', dl); service.delById('queue', ['u1']).subscribe(); expect(dl.deleting).toBe(true); const req = httpMock.expectOne('delete'); expect(req.request.body).toEqual({ where: 'queue', ids: ['u1'] }); req.flush({}); }); it('handleHTTPError extracts msg from object body', async () => { const err = new HttpErrorResponse({ error: { msg: 'bad' }, status: 400, }); const res = await new Promise((resolve) => { service.handleHTTPError(err).subscribe(resolve); }); expect((res as { status: string }).status).toBe('error'); expect((res as { msg?: string }).msg).toBe('bad'); }); it('socket all updates queue and done', () => { const row: Download = { id: '1', title: 't', url: 'u1', download_type: 'video', quality: 'best', format: 'any', folder: '', custom_name_prefix: '', playlist_item_limit: 0, status: 'pending', msg: '', percent: 0, speed: 0, eta: 0, filename: '', checked: false, }; const q: [string, Download][] = [['u1', row]]; const d: [string, Download][] = []; socket.emit('all', JSON.stringify([q, d])); expect(service.loading).toBe(false); expect(service.queue.has('u1')).toBe(true); }); it('socket updated preserves checked and deleting', () => { service.queue.set('u1', { id: '1', title: 't', url: 'u1', download_type: 'video', quality: 'best', format: 'any', folder: '', custom_name_prefix: '', playlist_item_limit: 0, status: 'pending', msg: '', percent: 0, speed: 0, eta: 0, filename: '', checked: true, deleting: true, }); socket.emit( 'updated', JSON.stringify({ url: 'u1', title: 't', status: 'downloading' }), ); const updated = service.queue.get('u1'); expect(updated?.checked).toBe(true); expect(updated?.deleting).toBe(true); }); it('socket completed moves entry to done', () => { service.queue.set('u1', { id: '1', title: 't', url: 'u1', download_type: 'video', quality: 'best', format: 'any', folder: '', custom_name_prefix: '', playlist_item_limit: 0, status: 'pending', msg: '', percent: 0, speed: 0, eta: 0, filename: '', checked: false, }); socket.emit('completed', JSON.stringify({ url: 'u1', title: 't', status: 'finished' })); expect(service.queue.has('u1')).toBe(false); expect(service.done.has('u1')).toBe(true); }); it('socket canceled removes from queue', () => { service.queue.set('u1', { id: '1', title: 't', url: 'u1', download_type: 'video', quality: 'best', format: 'any', folder: '', custom_name_prefix: '', playlist_item_limit: 0, status: 'pending', msg: '', percent: 0, speed: 0, eta: 0, filename: '', checked: false, }); socket.emit('canceled', JSON.stringify('u1')); expect(service.queue.has('u1')).toBe(false); }); it('socket cleared removes from done', () => { service.done.set('u1', { id: '1', title: 't', url: 'u1', download_type: 'video', quality: 'best', format: 'any', folder: '', custom_name_prefix: '', playlist_item_limit: 0, status: 'finished', msg: '', percent: 0, speed: 0, eta: 0, filename: '', checked: false, }); socket.emit('cleared', JSON.stringify('u1')); expect(service.done.has('u1')).toBe(false); }); it('socket configuration updates configuration', () => { socket.emit('configuration', JSON.stringify({ CUSTOM_DIRS: true })); expect(service.configuration['CUSTOM_DIRS']).toBe(true); }); it('socket custom_dirs updates customDirs', () => { socket.emit('custom_dirs', JSON.stringify({ download_dir: [''] })); expect(service.customDirs['download_dir']).toEqual(['']); }); afterEach(() => { httpMock.verify(); }); });