mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-13 20:12:54 -07:00
[codex] Fix Jellyfin setup and discovery toggle (#59)
This commit is contained in:
@@ -2,6 +2,7 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
buildJellyfinSetupFormHtml,
|
||||
buildJellyfinSetupViewState,
|
||||
createHandleJellyfinSetupWindowClosedHandler,
|
||||
createHandleJellyfinSetupNavigationHandler,
|
||||
createHandleJellyfinSetupSubmissionHandler,
|
||||
@@ -12,10 +13,50 @@ import {
|
||||
} from './jellyfin-setup-window';
|
||||
|
||||
test('buildJellyfinSetupFormHtml escapes default values', () => {
|
||||
const html = buildJellyfinSetupFormHtml('http://host/"x"', 'user"name');
|
||||
const html = buildJellyfinSetupFormHtml({
|
||||
servers: [
|
||||
{
|
||||
serverUrl: 'http://host/"x"',
|
||||
label: 'Configured "Server"',
|
||||
source: 'config',
|
||||
},
|
||||
],
|
||||
selectedServerUrl: 'http://host/"x"',
|
||||
username: 'user"name',
|
||||
hasStoredSession: true,
|
||||
statusMessage: 'Ready "now"',
|
||||
statusKind: 'success',
|
||||
});
|
||||
assert.ok(html.includes('http://host/"x"'));
|
||||
assert.ok(html.includes('user"name'));
|
||||
assert.ok(html.includes('Ready "now"'));
|
||||
assert.ok(html.includes('Logout'));
|
||||
assert.ok(html.includes('subminer://jellyfin-setup?'));
|
||||
assert.equal(html.includes('params.set("password"'), false);
|
||||
});
|
||||
|
||||
test('buildJellyfinSetupViewState composes config, recent, and default servers', () => {
|
||||
const state = buildJellyfinSetupViewState({
|
||||
config: {
|
||||
serverUrl: ' http://configured:8096/ ',
|
||||
username: 'alice',
|
||||
recentServers: ['http://recent:8096', 'http://configured:8096', ''],
|
||||
},
|
||||
defaultServerUrl: 'http://127.0.0.1:8096',
|
||||
hasStoredSession: false,
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
state.servers.map((server) => [server.serverUrl, server.source]),
|
||||
[
|
||||
['http://configured:8096', 'config'],
|
||||
['http://recent:8096', 'recent'],
|
||||
['http://127.0.0.1:8096', 'default'],
|
||||
],
|
||||
);
|
||||
assert.equal(state.selectedServerUrl, 'http://configured:8096');
|
||||
assert.equal(state.username, 'alice');
|
||||
assert.equal(state.statusKind, 'idle');
|
||||
});
|
||||
|
||||
test('maybe focus jellyfin setup window no-ops without window', () => {
|
||||
@@ -28,13 +69,26 @@ test('maybe focus jellyfin setup window no-ops without window', () => {
|
||||
|
||||
test('parseJellyfinSetupSubmissionUrl parses setup url parameters', () => {
|
||||
const parsed = parseJellyfinSetupSubmissionUrl(
|
||||
'subminer://jellyfin-setup?server=http%3A%2F%2Flocalhost&username=a&password=b',
|
||||
'subminer://jellyfin-setup?action=login&server=http%3A%2F%2Flocalhost&username=a&password=b',
|
||||
);
|
||||
assert.deepEqual(parsed, {
|
||||
action: 'login',
|
||||
server: 'http://localhost',
|
||||
username: 'a',
|
||||
password: 'b',
|
||||
});
|
||||
assert.deepEqual(parseJellyfinSetupSubmissionUrl('subminer://jellyfin-setup?action=logout'), {
|
||||
action: 'logout',
|
||||
server: '',
|
||||
username: '',
|
||||
password: '',
|
||||
});
|
||||
assert.deepEqual(parseJellyfinSetupSubmissionUrl('subminer://jellyfin-setup?action=done'), {
|
||||
action: 'done',
|
||||
server: '',
|
||||
username: '',
|
||||
password: '',
|
||||
});
|
||||
assert.equal(parseJellyfinSetupSubmissionUrl('https://example.com'), null);
|
||||
});
|
||||
|
||||
@@ -42,14 +96,18 @@ test('createHandleJellyfinSetupSubmissionHandler applies successful login', asyn
|
||||
const calls: string[] = [];
|
||||
let patchPayload: unknown = null;
|
||||
let savedSession: unknown = null;
|
||||
let authPassword = '';
|
||||
const handler = createHandleJellyfinSetupSubmissionHandler({
|
||||
parseSubmissionUrl: (rawUrl) => parseJellyfinSetupSubmissionUrl(rawUrl),
|
||||
authenticateWithPassword: async () => ({
|
||||
serverUrl: 'http://localhost',
|
||||
username: 'user',
|
||||
accessToken: 'token',
|
||||
userId: 'uid',
|
||||
}),
|
||||
authenticateWithPassword: async (_server, _username, password) => {
|
||||
authPassword = password;
|
||||
return {
|
||||
serverUrl: 'http://localhost',
|
||||
username: 'user',
|
||||
accessToken: 'token',
|
||||
userId: 'uid',
|
||||
};
|
||||
},
|
||||
getJellyfinClientInfo: () => ({
|
||||
clientName: 'SubMiner',
|
||||
clientVersion: '1.0',
|
||||
@@ -59,6 +117,7 @@ test('createHandleJellyfinSetupSubmissionHandler applies successful login', asyn
|
||||
savedSession = session;
|
||||
calls.push('save');
|
||||
},
|
||||
clearStoredSession: () => calls.push('clear'),
|
||||
patchJellyfinConfig: (session) => {
|
||||
patchPayload = session;
|
||||
calls.push('patch');
|
||||
@@ -67,13 +126,16 @@ test('createHandleJellyfinSetupSubmissionHandler applies successful login', asyn
|
||||
logError: () => calls.push('error'),
|
||||
showMpvOsd: (message) => calls.push(`osd:${message}`),
|
||||
closeSetupWindow: () => calls.push('close'),
|
||||
reloadSetupWindow: () => calls.push('reload'),
|
||||
});
|
||||
|
||||
const handled = await handler(
|
||||
'subminer://jellyfin-setup?server=http%3A%2F%2Flocalhost&username=a&password=b',
|
||||
'subminer://jellyfin-setup?action=login&server=http%3A%2F%2Flocalhost&username=a',
|
||||
'b',
|
||||
);
|
||||
assert.equal(handled, true);
|
||||
assert.deepEqual(calls, ['save', 'patch', 'info', 'osd:Jellyfin login success', 'close']);
|
||||
assert.deepEqual(calls, ['save', 'patch', 'info', 'osd:Jellyfin login success', 'reload']);
|
||||
assert.equal(authPassword, 'b');
|
||||
assert.deepEqual(savedSession, { accessToken: 'token', userId: 'uid' });
|
||||
assert.deepEqual(patchPayload, {
|
||||
serverUrl: 'http://localhost',
|
||||
@@ -96,18 +158,155 @@ test('createHandleJellyfinSetupSubmissionHandler reports failure to OSD', async
|
||||
deviceId: 'did',
|
||||
}),
|
||||
saveStoredSession: () => calls.push('save'),
|
||||
clearStoredSession: () => calls.push('clear'),
|
||||
patchJellyfinConfig: () => calls.push('patch'),
|
||||
logInfo: () => calls.push('info'),
|
||||
logError: () => calls.push('error'),
|
||||
showMpvOsd: (message) => calls.push(`osd:${message}`),
|
||||
closeSetupWindow: () => calls.push('close'),
|
||||
reloadSetupWindow: (_state) => calls.push('reload'),
|
||||
});
|
||||
|
||||
const handled = await handler(
|
||||
'subminer://jellyfin-setup?server=http%3A%2F%2Flocalhost&username=a&password=b',
|
||||
'subminer://jellyfin-setup?action=login&server=http%3A%2F%2Flocalhost&username=a&password=b',
|
||||
);
|
||||
assert.equal(handled, true);
|
||||
assert.deepEqual(calls, ['error', 'osd:Jellyfin login failed: bad credentials']);
|
||||
assert.deepEqual(calls, ['error', 'osd:Jellyfin login failed: bad credentials', 'reload']);
|
||||
});
|
||||
|
||||
test('createHandleJellyfinSetupSubmissionHandler reports logout failure inline', async () => {
|
||||
const calls: string[] = [];
|
||||
let reloadState: unknown = null;
|
||||
const handler = createHandleJellyfinSetupSubmissionHandler({
|
||||
parseSubmissionUrl: (rawUrl) => parseJellyfinSetupSubmissionUrl(rawUrl),
|
||||
authenticateWithPassword: async () => {
|
||||
throw new Error('should not authenticate');
|
||||
},
|
||||
getJellyfinClientInfo: () => ({
|
||||
clientName: 'SubMiner',
|
||||
clientVersion: '1.0',
|
||||
deviceId: 'did',
|
||||
}),
|
||||
saveStoredSession: () => calls.push('save'),
|
||||
clearStoredSession: () => {
|
||||
throw new Error('logout failed');
|
||||
},
|
||||
patchJellyfinConfig: () => calls.push('patch'),
|
||||
logInfo: () => calls.push('info'),
|
||||
logError: (message) => calls.push(`error:${message}`),
|
||||
showMpvOsd: (message) => calls.push(`osd:${message}`),
|
||||
closeSetupWindow: () => calls.push('close'),
|
||||
reloadSetupWindow: (state) => {
|
||||
reloadState = state;
|
||||
calls.push('reload');
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(await handler('subminer://jellyfin-setup?action=logout'), true);
|
||||
assert.deepEqual(calls, [
|
||||
'error:Jellyfin logout failed',
|
||||
'osd:Jellyfin logout failed: logout failed',
|
||||
'reload',
|
||||
]);
|
||||
assert.deepEqual(reloadState, {
|
||||
statusMessage: 'logout failed',
|
||||
statusKind: 'error',
|
||||
});
|
||||
});
|
||||
|
||||
test('createHandleJellyfinSetupSubmissionHandler ignores concurrent login submissions', async () => {
|
||||
const calls: string[] = [];
|
||||
type TestSession = {
|
||||
serverUrl: string;
|
||||
username: string;
|
||||
accessToken: string;
|
||||
userId: string;
|
||||
};
|
||||
let finishAuth: ((session: TestSession) => void) | undefined;
|
||||
const handler = createHandleJellyfinSetupSubmissionHandler({
|
||||
parseSubmissionUrl: (rawUrl) => parseJellyfinSetupSubmissionUrl(rawUrl),
|
||||
authenticateWithPassword: async () =>
|
||||
new Promise<TestSession>((resolve) => {
|
||||
finishAuth = resolve;
|
||||
}),
|
||||
getJellyfinClientInfo: () => ({
|
||||
clientName: 'SubMiner',
|
||||
clientVersion: '1.0',
|
||||
deviceId: 'did',
|
||||
}),
|
||||
saveStoredSession: () => calls.push('save'),
|
||||
clearStoredSession: () => calls.push('clear'),
|
||||
patchJellyfinConfig: () => calls.push('patch'),
|
||||
logInfo: () => calls.push('info'),
|
||||
logError: () => calls.push('error'),
|
||||
showMpvOsd: (message) => calls.push(`osd:${message}`),
|
||||
closeSetupWindow: () => calls.push('close'),
|
||||
reloadSetupWindow: (state) => calls.push(`reload:${state?.statusKind || 'none'}`),
|
||||
});
|
||||
|
||||
const first = handler(
|
||||
'subminer://jellyfin-setup?action=login&server=http%3A%2F%2Flocalhost&username=a',
|
||||
'first',
|
||||
);
|
||||
const second = await handler(
|
||||
'subminer://jellyfin-setup?action=login&server=http%3A%2F%2Flocalhost&username=a',
|
||||
'second',
|
||||
);
|
||||
|
||||
assert.equal(second, true);
|
||||
const resolveAuth = finishAuth;
|
||||
if (!resolveAuth) {
|
||||
throw new Error('missing auth resolver');
|
||||
}
|
||||
resolveAuth({
|
||||
serverUrl: 'http://localhost',
|
||||
username: 'a',
|
||||
accessToken: 'token',
|
||||
userId: 'uid',
|
||||
});
|
||||
assert.equal(await first, true);
|
||||
assert.deepEqual(calls, [
|
||||
'osd:Jellyfin login already in progress',
|
||||
'reload:loading',
|
||||
'save',
|
||||
'patch',
|
||||
'info',
|
||||
'osd:Jellyfin login success',
|
||||
'reload:success',
|
||||
]);
|
||||
});
|
||||
|
||||
test('createHandleJellyfinSetupSubmissionHandler handles logout and done', async () => {
|
||||
const calls: string[] = [];
|
||||
const handler = createHandleJellyfinSetupSubmissionHandler({
|
||||
parseSubmissionUrl: (rawUrl) => parseJellyfinSetupSubmissionUrl(rawUrl),
|
||||
authenticateWithPassword: async () => {
|
||||
throw new Error('should not authenticate');
|
||||
},
|
||||
getJellyfinClientInfo: () => ({
|
||||
clientName: 'SubMiner',
|
||||
clientVersion: '1.0',
|
||||
deviceId: 'did',
|
||||
}),
|
||||
saveStoredSession: () => calls.push('save'),
|
||||
clearStoredSession: () => calls.push('clear'),
|
||||
patchJellyfinConfig: () => calls.push('patch'),
|
||||
logInfo: (message) => calls.push(message),
|
||||
logError: () => calls.push('error'),
|
||||
showMpvOsd: (message) => calls.push(`osd:${message}`),
|
||||
closeSetupWindow: () => calls.push('close'),
|
||||
reloadSetupWindow: () => calls.push('reload'),
|
||||
});
|
||||
|
||||
assert.equal(await handler('subminer://jellyfin-setup?action=logout'), true);
|
||||
assert.equal(await handler('subminer://jellyfin-setup?action=done'), true);
|
||||
assert.deepEqual(calls, [
|
||||
'clear',
|
||||
'Cleared stored Jellyfin auth session.',
|
||||
'osd:Jellyfin logged out',
|
||||
'reload',
|
||||
'close',
|
||||
]);
|
||||
});
|
||||
|
||||
test('createHandleJellyfinSetupNavigationHandler ignores unrelated urls', () => {
|
||||
@@ -200,7 +399,10 @@ test('createOpenJellyfinSetupWindowHandler no-ops when existing setup window is
|
||||
showMpvOsd: () => {},
|
||||
clearSetupWindow: () => {},
|
||||
setSetupWindow: () => {},
|
||||
clearStoredSession: () => {},
|
||||
encodeURIComponent: (value) => value,
|
||||
defaultServerUrl: 'http://127.0.0.1:8096',
|
||||
hasStoredSession: () => false,
|
||||
});
|
||||
|
||||
handler();
|
||||
@@ -224,6 +426,7 @@ test('createOpenJellyfinSetupWindowHandler wires navigation, load, and window li
|
||||
willNavigateHandler = handler;
|
||||
}
|
||||
},
|
||||
executeJavaScript: async () => 'pass',
|
||||
},
|
||||
loadURL: (url: string) => {
|
||||
calls.push(`load:${url.startsWith('data:text/html;charset=utf-8,') ? 'data-url' : 'other'}`);
|
||||
@@ -240,21 +443,29 @@ test('createOpenJellyfinSetupWindowHandler wires navigation, load, and window li
|
||||
const handler = createOpenJellyfinSetupWindowHandler({
|
||||
maybeFocusExistingSetupWindow: () => false,
|
||||
createSetupWindow: () => fakeWindow,
|
||||
getResolvedJellyfinConfig: () => ({ serverUrl: 'http://localhost:8096', username: 'alice' }),
|
||||
buildSetupFormHtml: (server, username) => `<html>${server}|${username}</html>`,
|
||||
parseSubmissionUrl: (rawUrl) => parseJellyfinSetupSubmissionUrl(rawUrl),
|
||||
authenticateWithPassword: async () => ({
|
||||
getResolvedJellyfinConfig: () => ({
|
||||
serverUrl: 'http://localhost:8096',
|
||||
username: 'alice',
|
||||
accessToken: 'token',
|
||||
userId: 'uid',
|
||||
recentServers: [],
|
||||
}),
|
||||
buildSetupFormHtml: (state) => `<html>${state.selectedServerUrl}|${state.username}</html>`,
|
||||
parseSubmissionUrl: (rawUrl) => parseJellyfinSetupSubmissionUrl(rawUrl),
|
||||
authenticateWithPassword: async (_server, _username, password) => {
|
||||
calls.push(`password:${password}`);
|
||||
return {
|
||||
serverUrl: 'http://localhost:8096',
|
||||
username: 'alice',
|
||||
accessToken: 'token',
|
||||
userId: 'uid',
|
||||
};
|
||||
},
|
||||
getJellyfinClientInfo: () => ({
|
||||
clientName: 'SubMiner',
|
||||
clientVersion: '1.0',
|
||||
deviceId: 'did',
|
||||
}),
|
||||
saveStoredSession: () => calls.push('save'),
|
||||
clearStoredSession: () => calls.push('clear'),
|
||||
patchJellyfinConfig: () => calls.push('patch'),
|
||||
logInfo: () => calls.push('info'),
|
||||
logError: () => calls.push('error'),
|
||||
@@ -262,6 +473,8 @@ test('createOpenJellyfinSetupWindowHandler wires navigation, load, and window li
|
||||
clearSetupWindow: () => calls.push('clear-window'),
|
||||
setSetupWindow: () => calls.push('set-window'),
|
||||
encodeURIComponent: (value) => encodeURIComponent(value),
|
||||
defaultServerUrl: 'http://127.0.0.1:8096',
|
||||
hasStoredSession: () => true,
|
||||
});
|
||||
|
||||
handler();
|
||||
@@ -281,15 +494,16 @@ test('createOpenJellyfinSetupWindowHandler wires navigation, load, and window li
|
||||
prevented = true;
|
||||
},
|
||||
},
|
||||
'subminer://jellyfin-setup?server=http%3A%2F%2Flocalhost&username=alice&password=pass',
|
||||
'subminer://jellyfin-setup?action=login&server=http%3A%2F%2Flocalhost&username=alice',
|
||||
);
|
||||
await Promise.resolve();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
assert.equal(prevented, true);
|
||||
assert.ok(calls.includes('password:pass'));
|
||||
assert.ok(calls.includes('save'));
|
||||
assert.ok(calls.includes('patch'));
|
||||
assert.ok(calls.includes('osd:Jellyfin login success'));
|
||||
assert.ok(calls.includes('close'));
|
||||
assert.ok(calls.includes('load:data-url'));
|
||||
|
||||
const onClosed = closedHandler as (() => void) | null;
|
||||
if (!onClosed) {
|
||||
|
||||
Reference in New Issue
Block a user