fix(web): address self-hosted review feedback

This commit is contained in:
4gray committed 2026-05-15 20:01:16 +02:00
1 parent db19d7d9f4
commit b4679f913f
4 files changed
+50 -9

No files matched your search

@@ -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<T>(
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, '<script>alert(1)</script>');
+5 -4
View File
@@ -467,9 +467,10 @@ function createPlaylistObject(options: {
readonly url: string;
}): Record<string, unknown> {
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,
};
}
+4
View File
@@ -129,6 +129,7 @@ export class PwaService extends DataService {
url: string;
macAddress: string;
params: Record<string, string>;
token?: string;
}
) as T;
}
@@ -456,6 +457,7 @@ export class PwaService extends DataService {
url: string;
params: Record<string, string>;
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()}`;
+1 -1
View File
@@ -20,7 +20,7 @@
<link rel="manifest" href="manifest.webmanifest" />
<link rel="apple-touch-icon" href="assets/icons/apple-touch-icon.png" />
<link rel="icon" type="image/x-icon" href="assets/icons/favicon.ico" />
<script src="assets/app-config.js"></script>
<script src="assets/app-config.js" defer></script>
<style>
/* Inline splash — paints immediately while the JS bundle and the
styles.css (~300KB) are still downloading and Angular is