mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 18:36:15 -08:00
fix(web): address self-hosted review feedback
This commit is contained in:
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>');
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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()}`;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user