diff --git a/apps/web-backend/src/app/web-backend-app.spec.ts b/apps/web-backend/src/app/web-backend-app.spec.ts index 6f31aadcc..13fdca6b3 100644 --- a/apps/web-backend/src/app/web-backend-app.spec.ts +++ b/apps/web-backend/src/app/web-backend-app.spec.ts @@ -19,6 +19,7 @@ class StubHttpClient implements WebBackendHttpClient { []; private readonly queuedResponses: Array<{ readonly data: unknown; + readonly error?: Error; readonly status?: number; readonly statusText?: string; }> = []; @@ -31,6 +32,10 @@ class StubHttpClient implements WebBackendHttpClient { this.queuedResponses.push({ data: null, status, statusText }); } + queueNetworkFailure(message = 'connect ECONNREFUSED'): void { + this.queuedResponses.push({ data: null, error: new Error(message) }); + } + async get( url: string, options: WebBackendHttpGetOptions = {} @@ -46,6 +51,10 @@ class StubHttpClient implements WebBackendHttpClient { throw new Error(`No queued response for ${url}`); } + if (response.error) { + throw response.error; + } + if (response.status) { const error = new Error(response.statusText) as Error & { response: { status: number; statusText: string }; @@ -128,6 +137,7 @@ describe('web backend app', () => { it('parses remote M3U playlists into the PWA playlist shape', async () => { const httpClient = new StubHttpClient(); + let idCounter = 0; httpClient.queueResponse(`#EXTM3U #EXTINF:-1 tvg-id="news" group-title="News",News Channel https://stream.example/news.m3u8`); @@ -135,7 +145,7 @@ https://stream.example/news.m3u8`); await withServer( createWebBackendApp({ clientOrigins: ['http://localhost:4200'], - guid: () => 'fixed-id', + guid: () => `fixed-id-${++idCounter}`, httpClient, now: () => new Date('2026-05-15T08:00:00.000Z'), resolveHostname: resolvePublicHost, @@ -158,12 +168,12 @@ https://stream.example/news.m3u8`); response.headers.get('access-control-allow-origin') ).toBe('http://localhost:4200'); expect(body).toMatchObject({ - _id: 'fixed-id', + _id: 'fixed-id-1', autoRefresh: false, count: 1, favorites: [], filename: 'list.m3u', - id: 'fixed-id', + id: 'fixed-id-1', importDate: '2026-05-15T08:00:00.000Z', lastUsage: '2026-05-15T08:00:00.000Z', title: 'list.m3u', @@ -171,7 +181,7 @@ https://stream.example/news.m3u8`); }); expect(body.playlist.items).toHaveLength(1); expect(body.playlist.items[0]).toMatchObject({ - id: 'fixed-id', + id: 'fixed-id-2', name: 'News Channel', url: 'https://stream.example/news.m3u8', }); @@ -313,6 +323,32 @@ https://stream.example/news.m3u8`); ); }); + it('normalizes non-HTTP upstream failures as bad gateway', async () => { + const httpClient = new StubHttpClient(); + httpClient.queueNetworkFailure(); + + await withServer( + createWebBackendApp({ + httpClient, + resolveHostname: resolvePublicHost, + }), + async (baseUrl) => { + const targetId = await registerProviderTarget( + baseUrl, + 'http://xtream.example' + ); + const response = await fetch( + `${baseUrl}/xtream?targetId=${targetId}&action=get_account_info` + ); + + await expect(response.json()).resolves.toEqual({ + message: 'Bad Gateway', + status: 502, + }); + } + ); + }); + it('returns provider parse errors as JSON instead of executable text', async () => { const httpClient = new StubHttpClient(); httpClient.queueFailure(502, ''); diff --git a/apps/web-backend/src/app/web-backend-app.ts b/apps/web-backend/src/app/web-backend-app.ts index c868dc31a..8ae5d854f 100644 --- a/apps/web-backend/src/app/web-backend-app.ts +++ b/apps/web-backend/src/app/web-backend-app.ts @@ -467,9 +467,10 @@ function createPlaylistObject(options: { readonly url: string; }): Record { const timestamp = options.now().toISOString(); + const id = options.guid(); return { - id: options.guid(), - _id: options.guid(), + id, + _id: id, filename: options.title, title: options.title, count: options.playlist.items.length, @@ -499,8 +500,8 @@ function normalizeProviderError(error: unknown): { } { const providerError = error as ProviderError; return { - message: providerError.response?.statusText ?? 'Error: not found', - status: providerError.response?.status ?? 404, + message: providerError.response?.statusText ?? 'Bad Gateway', + status: providerError.response?.status ?? 502, }; } diff --git a/apps/web/src/app/services/pwa.service.ts b/apps/web/src/app/services/pwa.service.ts index aa7b916a1..25e2af2d5 100644 --- a/apps/web/src/app/services/pwa.service.ts +++ b/apps/web/src/app/services/pwa.service.ts @@ -129,6 +129,7 @@ export class PwaService extends DataService { url: string; macAddress: string; params: Record; + token?: string; } ) as T; } @@ -456,6 +457,7 @@ export class PwaService extends DataService { url: string; params: Record; macAddress: string; + token?: string; }) { let context = createPortalDebugRequestContext({ provider: 'stalker', @@ -470,10 +472,12 @@ export class PwaService extends DataService { try { const targetId = await this.getProviderTargetId(payload.url); + const token = payload.token ?? payload.params.token; const requestParams = { targetId, ...payload.params, macAddress: payload.macAddress, + ...(token ? { token } : {}), }; const params = new URLSearchParams(requestParams); const requestUrl = `${this.corsProxyUrl}/stalker?${params.toString()}`; diff --git a/apps/web/src/index.html b/apps/web/src/index.html index 21cd9eeea..967aa47ed 100644 --- a/apps/web/src/index.html +++ b/apps/web/src/index.html @@ -20,7 +20,7 @@ - +