Release 0.3.4: Jellyfin 12 stable support and audit fixes
Harden proxy permissions and cache behavior, streamline metadata fetching, remove obsolete rules, and validate the stable SDK with expanded regression coverage. Document backup format changes.
This commit is contained in:
@@ -101,9 +101,15 @@ test('pre-caches the fixture movie library after user activity', async ({ reques
|
||||
const entries = await apiGet(request, '/Multilang/CacheEntries');
|
||||
return entries.some(entry => entry.Url.includes(`ParentId=${fixture.libraryId}`));
|
||||
}, { timeout: 60000 }).toBeTruthy();
|
||||
await getFixtureItems(request);
|
||||
const recent = await apiGet(request, '/Multilang/ItemsProxyRequests');
|
||||
expect(recent[0].CacheHit).toBeTruthy();
|
||||
});
|
||||
|
||||
test('shows configured translations and uses the injected ItemsProxy route', async ({ page, request }) => {
|
||||
const rules = await apiGet(request, '/Multilang/UserRules/self');
|
||||
rules.FallbackFieldActions.poster = ['Language:fi', 'Jellyfin'];
|
||||
await apiPost(request, '/Multilang/UserRules/self', rules);
|
||||
const proxiedRequests = [];
|
||||
page.on('request', request => {
|
||||
if (new URL(request.url()).pathname === '/Multilang/ItemsProxy') {
|
||||
@@ -112,7 +118,10 @@ test('shows configured translations and uses the injected ItemsProxy route', asy
|
||||
});
|
||||
|
||||
await signIn(page);
|
||||
const artworkResponse = page.waitForResponse(response =>
|
||||
new URL(response.url()).pathname === `/Multilang/Assets/${fixture.orderedItemIds[0]}/fixture.png`);
|
||||
await openFixtureLibrary(page, fixture.translatedTitles[0]);
|
||||
expect((await artworkResponse).status()).toBe(200);
|
||||
|
||||
for (const title of fixture.translatedTitles) {
|
||||
await expect(page.getByText(title, { exact: true })).toBeVisible();
|
||||
@@ -151,7 +160,6 @@ test('applies a structured category rule before fallback translation actions', a
|
||||
Id: 'browser-fixture-swedish',
|
||||
Label: 'Swedish originals',
|
||||
Requirements: [{
|
||||
Field: 'original_language',
|
||||
Fields: ['original_language'],
|
||||
Relation: 'is',
|
||||
Values: ['sv'],
|
||||
@@ -161,7 +169,6 @@ test('applies a structured category rule before fallback translation actions', a
|
||||
MatchAllConditions: true,
|
||||
Scopes: ['M'],
|
||||
FieldActionLists: categoryActions,
|
||||
FieldActions: {}
|
||||
}];
|
||||
await apiPost(request, '/Multilang/UserRules/self', rules);
|
||||
|
||||
@@ -257,3 +264,81 @@ test('saves a structured classification rule through the user settings page', as
|
||||
Values: ['sv']
|
||||
})]);
|
||||
});
|
||||
|
||||
test('rejects unsafe proxy targets and enforces restricted-user item access', async ({ request }) => {
|
||||
for (const url of ['//outside.invalid/Items', '/System/Info', '/Multilang/ItemsProxy']) {
|
||||
const response = await request.get('/Multilang/ItemsProxy', { headers: authHeaders(), params: { url } });
|
||||
expect(response.status()).toBe(400);
|
||||
}
|
||||
const name = 'multilang-access-test-' + Date.now();
|
||||
const user = await apiPost(request, '/Users/New', { Name: name });
|
||||
const policy = { ...user.Policy, IsAdministrator: false, EnableAllFolders: true };
|
||||
let response = await request.post(`/Users/${user.Id}/Policy`, { headers: authHeaders(), data: policy });
|
||||
expect(response.ok()).toBeTruthy();
|
||||
response = await request.post('/Users/AuthenticateByName', {
|
||||
headers: { Authorization: 'MediaBrowser Client="Multilang tests", Device="test", DeviceId="multilang-access-test", Version="1"' },
|
||||
data: { Username: name, Pw: '' }
|
||||
});
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const login = await response.json();
|
||||
const headers = { Authorization: `MediaBrowser Token="${login.AccessToken}"` };
|
||||
const asset = `/Multilang/Assets/${fixture.orderedItemIds[0]}/fixture.png`;
|
||||
expect((await request.get(asset)).status()).toBe(401);
|
||||
expect((await request.get(asset, { headers })).status()).toBe(200);
|
||||
response = await request.post('/Multilang/UserRules/self', { headers, data: { Enabled: true, FallbackFieldActions: jellyfinActions() } });
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const params = { url: `/Items?ParentId=${fixture.libraryId}&IncludeItemTypes=Movie&Recursive=true&SortBy=SortName` };
|
||||
response = await request.get('/Multilang/ItemsProxy', { headers, params });
|
||||
expect(response.ok()).toBeTruthy();
|
||||
expect((await response.json()).Items).toHaveLength(3);
|
||||
let entries = await apiGet(request, '/Multilang/CacheEntries');
|
||||
expect(entries.some(entry => entry.UserId === user.Id)).toBeTruthy();
|
||||
|
||||
policy.EnableAllFolders = false;
|
||||
policy.EnabledFolders = [];
|
||||
response = await request.post(`/Users/${user.Id}/Policy`, { headers: authHeaders(), data: policy });
|
||||
expect(response.ok()).toBeTruthy();
|
||||
response = await request.get('/Multilang/ItemsProxy', { headers, params });
|
||||
const native = await request.get(`/Users/${user.Id}/Items?ParentId=${fixture.libraryId}`, { headers });
|
||||
expect(native.status()).toBe(401);
|
||||
expect(response.status()).toBe(native.status());
|
||||
const recent = await apiGet(request, '/Multilang/ItemsProxyRequests');
|
||||
expect(recent[0].CacheHit).toBeFalsy();
|
||||
expect((await request.get(asset, { headers })).status()).toBe(404);
|
||||
entries = await apiGet(request, '/Multilang/CacheEntries');
|
||||
expect(entries.some(entry => entry.UserId === user.Id && entry.ItemCount > 0)).toBeFalsy();
|
||||
for (const path of [`Debug/${fixture.orderedItemIds[0]}`, `Debug/${fixture.orderedItemIds[0]}/self`]) {
|
||||
expect((await request.get(`/Multilang/${path}`, { headers })).status()).toBe(404);
|
||||
}
|
||||
expect((await request.post(`/Multilang/RefreshItem/${fixture.orderedItemIds[0]}?includeChildren=true`, { headers })).status()).toBe(404);
|
||||
response = await request.get('/Multilang/ItemsProxy', { headers, params });
|
||||
expect(response.status()).toBe(401);
|
||||
});
|
||||
|
||||
test('reuses a library fetch across different pages and sorting controls', async ({ request }) => {
|
||||
const base = `/Items?ParentId=${fixture.libraryId}&IncludeItemTypes=Movie&Recursive=true`;
|
||||
let response = await request.get('/Multilang/ItemsProxy', { headers: authHeaders(), params: { url: base + '&SortBy=SortName&Limit=1', mlLocale: 'fi-FI' } });
|
||||
expect((await response.json()).Items.map(item => item.Name)).toEqual(['Ankkuri']);
|
||||
response = await request.get('/Multilang/ItemsProxy', { headers: authHeaders(), params: { url: base + '&SortBy=SortName&StartIndex=1&Limit=1', mlLocale: 'fi-FI' } });
|
||||
expect((await response.json()).Items.map(item => item.Name)).toEqual(['The Clock']);
|
||||
const recent = await apiGet(request, '/Multilang/ItemsProxyRequests');
|
||||
expect(recent[0].CacheHit).toBeTruthy();
|
||||
});
|
||||
|
||||
test('reopens user settings without duplicate loads or a native replaceChildren dependency', async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
Object.defineProperty(Element.prototype, 'replaceChildren', { configurable: true, value: undefined });
|
||||
});
|
||||
await signIn(page);
|
||||
await page.goto(`/web/#/mypreferencesmenu?userId=${fixture.userId}`);
|
||||
let loads = 0;
|
||||
page.on('request', request => { if (new URL(request.url()).pathname === '/Multilang/UserRules/self') loads++; });
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await expect(page.locator('#mlUserRulesMenuItem')).toBeVisible();
|
||||
await page.locator('#mlUserRulesMenuItem').click();
|
||||
await expect(page.locator('#ml-sort-locale option[value="en"]')).toHaveCount(1);
|
||||
expect(loads).toBe(i + 1);
|
||||
await page.evaluate(userId => { location.hash = '#/mypreferencesmenu?userId=' + userId; }, fixture.userId);
|
||||
await expect(page.locator('#ml-user-shell')).toHaveCount(0);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user