import 'zone.js/node'; import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { DOCUMENT } from '@angular/common'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; import { SubscriptionsService, type SubscriptionItem } from './subscriptions.service'; import type { ChannelMeta, ProviderId } from '../app/shared/models/channel.model'; const mockDocument = { cookie: '', createElement: () => ({ setAttribute() {}, }), querySelector: () => null, body: { appendChild() {}, removeChild() {}, }, } as unknown as Document; TestBed.resetTestEnvironment(); TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { teardown: { destroyAfterEach: true, rethrowErrors: true }, }); function setup() { TestBed.resetTestingModule(); TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [ SubscriptionsService, { provide: DOCUMENT, useValue: mockDocument }, ], }); const service = TestBed.inject(SubscriptionsService); const httpMock = TestBed.inject(HttpTestingController); return { service, httpMock }; } function assertEqual(actual: unknown, expected: unknown, message: string): void { const a = JSON.stringify(actual); const b = JSON.stringify(expected); if (a !== b) { throw new Error(`${message} (expected ${b}, got ${a})`); } } function assert(condition: unknown, message: string): void { if (!condition) { throw new Error(message); } } function makeChannel(provider: ProviderId, externalId: string, overrides: Partial = {}): ChannelMeta { return { provider, externalId, id: overrides.id ?? 1, title: overrides.title ?? 'Channel Title', handle: overrides.handle ?? '@channel', avatarUrl: overrides.avatarUrl ?? 'https://example.com/avatar.jpg', url: overrides.url ?? 'https://example.com', subsCount: overrides.subsCount ?? 1234, verified: overrides.verified ?? true, lastRefreshedAt: overrides.lastRefreshedAt ?? Date.now(), }; } function testListCachesResults(): void { const { service, httpMock } = setup(); const channel = makeChannel('yt', 'abc'); const subscription: SubscriptionItem = { subscriptionId: 101, createdAt: Date.now(), channel, }; let received: SubscriptionItem[] | undefined; service.list().subscribe(items => (received = items)); const req = httpMock.expectOne('/api/subscriptions'); assert(req.request.method === 'GET', 'list() should perform GET request'); req.flush({ items: [subscription], ttl: 600000 }); assertEqual(received, [subscription], 'list() should emit fetched subscriptions'); assertEqual(service.items(), [subscription], 'items signal should reflect fetched subscriptions'); httpMock.verify(); } function testSubscribeAppendsToCache(): void { const { service, httpMock } = setup(); const baseChannel = makeChannel('yt', 'abc'); const base: SubscriptionItem = { subscriptionId: 1, createdAt: Date.now() - 1000, channel: baseChannel, }; service.list().subscribe(); httpMock.expectOne('/api/subscriptions').flush({ items: [base] }); const newChannel = makeChannel('dm', 'xyz'); const newSubscription: SubscriptionItem = { subscriptionId: 2, createdAt: Date.now(), channel: newChannel, }; let subscribed: SubscriptionItem | null = null; service.subscribe('dm', 'xyz').subscribe(result => (subscribed = result)); const req = httpMock.expectOne('/api/subscriptions'); assert(req.request.method === 'POST', 'subscribe() should perform POST request'); assertEqual(req.request.body, { provider: 'dm', externalId: 'xyz' }, 'subscribe() should send provider/externalId payload'); req.flush(newSubscription); assertEqual(subscribed, newSubscription, 'subscribe() should emit created subscription'); assertEqual( service.items().map(item => item.subscriptionId), [newSubscription.subscriptionId, base.subscriptionId], 'subscribe() should prepend new subscription into cache' ); httpMock.verify(); } function testUnsubscribeRemovesFromCache(): void { const { service, httpMock } = setup(); const channel = makeChannel('pt', 'foo'); const item: SubscriptionItem = { subscriptionId: 44, createdAt: Date.now(), channel, }; service.list().subscribe(); httpMock.expectOne('/api/subscriptions').flush({ items: [item] }); let result: boolean | null = null; service.unsubscribe(item.subscriptionId).subscribe(ok => (result = ok)); const req = httpMock.expectOne(`/api/subscriptions/${item.subscriptionId}`); assert(req.request.method === 'DELETE', 'unsubscribe() should perform DELETE request'); req.flush({}, { status: 204, statusText: 'No Content' }); assert(result === true, 'unsubscribe() should emit true when deletion succeeds'); assert(service.items().length === 0, 'unsubscribe() should remove subscription from cache'); httpMock.verify(); } const tests: Array<[string, () => void]> = [ ['list() caches fetched subscriptions', testListCachesResults], ['subscribe() appends to cache', testSubscribeAppendsToCache], ['unsubscribe() removes from cache', testUnsubscribeRemovesFromCache], ]; let failures = 0; for (const [name, fn] of tests) { try { fn(); console.log(`✓ ${name}`); } catch (error) { failures += 1; console.error(`✗ ${name}`); console.error(error); } } if (failures > 0) { process.exitCode = 1; }