mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-28 19:47:25 +00:00
197 lines
9.2 KiB
JavaScript
197 lines
9.2 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
HOST_ACCESS_REQUIRED_STATUS,
|
|
addTabHostAccessRequest,
|
|
describeTabUrl,
|
|
inspectTabHostAccess,
|
|
isHostAccessError,
|
|
normalizeTabId,
|
|
removeTabHostAccessRequest,
|
|
requestOriginPermission
|
|
} from './host-access.js';
|
|
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
|
|
describe('host access helpers', () => {
|
|
afterEach(() => vi.useRealTimers());
|
|
|
|
it('normalizes only positive safe tab IDs', () => {
|
|
expect(HOST_ACCESS_REQUIRED_STATUS).toBe('host_permission_required');
|
|
for (const invalid of [null, undefined, '', 0, true, [42], '42.5', Number.MAX_SAFE_INTEGER + 1]) {
|
|
expect(normalizeTabId(invalid)).toBeNull();
|
|
}
|
|
expect(normalizeTabId('42')).toBe(42);
|
|
expect(normalizeTabId(' 42 ')).toBe(42);
|
|
});
|
|
|
|
it('describes supported origins with Firefox-compatible localhost permissions', () => {
|
|
expect(describeTabUrl('https://emby.example:8443/web/index.html')).toEqual({
|
|
url: 'https://emby.example:8443/web/index.html',
|
|
host: 'emby.example:8443',
|
|
originPattern: 'https://emby.example:8443/*'
|
|
});
|
|
expect(describeTabUrl('http://localhost:8096/web/', { includePort: false })).toEqual({
|
|
url: 'http://localhost:8096/web/',
|
|
host: 'localhost:8096',
|
|
originPattern: 'http://localhost/*'
|
|
});
|
|
expect(describeTabUrl('chrome://extensions/')).toBeNull();
|
|
expect(describeTabUrl('not a url')).toBeNull();
|
|
expect(describeTabUrl('file:///Users/koala/movie.mp4')).toEqual({
|
|
url: 'file:///Users/koala/movie.mp4',
|
|
host: 'local file',
|
|
originPattern: 'file:///*'
|
|
});
|
|
});
|
|
|
|
it('checks the selected tab origin and preserves an unknown callback result', async () => {
|
|
let containsRequest;
|
|
const deniedChrome = {
|
|
tabs: { get: async tabId => ({ id: tabId, url: 'https://video.example/watch' }) },
|
|
permissions: {
|
|
contains: async request => {
|
|
containsRequest = request;
|
|
return false;
|
|
}
|
|
}
|
|
};
|
|
await expect(inspectTabHostAccess(deniedChrome, 42)).resolves.toMatchObject({
|
|
granted: false,
|
|
host: 'video.example',
|
|
originPattern: 'https://video.example/*'
|
|
});
|
|
expect(containsRequest).toEqual({ origins: ['https://video.example/*'] });
|
|
|
|
const unknownChrome = {
|
|
runtime: {},
|
|
tabs: { get: async tabId => ({ id: tabId, url: 'https://video.example/watch' }) },
|
|
permissions: { contains: (_request, callback) => callback(undefined) }
|
|
};
|
|
await expect(inspectTabHostAccess(unknownChrome, 42)).resolves.toMatchObject({ granted: null });
|
|
});
|
|
|
|
it('uses Firefox host patterns without ports', async () => {
|
|
let containsRequest;
|
|
const chromeApi = {
|
|
runtime: { getBrowserInfo: async () => ({ name: 'Firefox' }) },
|
|
tabs: {
|
|
get: async tabId => ({
|
|
id: tabId,
|
|
url: 'http://localhost:8096/web/',
|
|
pendingUrl: 'https://different.example/loading'
|
|
})
|
|
},
|
|
permissions: {
|
|
contains: async request => {
|
|
containsRequest = request;
|
|
return false;
|
|
}
|
|
}
|
|
};
|
|
await expect(inspectTabHostAccess(chromeApi, 42)).resolves.toMatchObject({
|
|
host: 'localhost:8096',
|
|
originPattern: 'http://localhost/*'
|
|
});
|
|
expect(containsRequest).toEqual({ origins: ['http://localhost/*'] });
|
|
});
|
|
|
|
it('adds, removes, and requests permissions through promise and callback APIs', async () => {
|
|
let added;
|
|
expect(await addTabHostAccessRequest({
|
|
permissions: { addHostAccessRequest: async request => { added = request; } }
|
|
}, 42, 'https://video.example/*')).toBe(true);
|
|
expect(added).toEqual({ tabId: 42, pattern: 'https://video.example/*' });
|
|
expect(await addTabHostAccessRequest({ permissions: {} }, 42)).toBe(false);
|
|
|
|
let removed;
|
|
expect(await removeTabHostAccessRequest({
|
|
permissions: { removeHostAccessRequest: async request => { removed = request; } }
|
|
}, 42, 'https://video.example/*')).toBe(true);
|
|
expect(removed).toEqual({ tabId: 42, pattern: 'https://video.example/*' });
|
|
expect(await removeTabHostAccessRequest({ permissions: {} }, 42)).toBe(false);
|
|
|
|
const callbackChrome = {
|
|
runtime: {},
|
|
permissions: { request: (_request, callback) => callback(true) }
|
|
};
|
|
await expect(requestOriginPermission(callbackChrome, 'https://video.example/*')).resolves.toBe(true);
|
|
await expect(requestOriginPermission({ permissions: {} }, 'https://video.example/*')).resolves.toBeNull();
|
|
await expect(requestOriginPermission(callbackChrome, '')).resolves.toBeNull();
|
|
await expect(requestOriginPermission({
|
|
permissions: { request: async () => { throw new Error('denied'); } }
|
|
}, 'https://video.example/*')).resolves.toBe(false);
|
|
await expect(addTabHostAccessRequest({
|
|
permissions: { addHostAccessRequest: async () => { throw new Error('denied'); } }
|
|
}, 42)).resolves.toBe(false);
|
|
await expect(removeTabHostAccessRequest({
|
|
permissions: { removeHostAccessRequest: async () => { throw new Error('denied'); } }
|
|
}, 42)).resolves.toBe(false);
|
|
expect(isHostAccessError(new Error('Missing host permission for the tab'))).toBe(true);
|
|
expect(isHostAccessError(new Error('No tab with id: 42'))).toBe(false);
|
|
});
|
|
|
|
it('treats permission inspection errors and timeouts as advisory unknowns', async () => {
|
|
const base = {
|
|
tabs: { get: async () => ({ url: 'https://video.example/watch' }) }
|
|
};
|
|
await expect(inspectTabHostAccess({
|
|
...base,
|
|
permissions: { contains: async () => { throw new Error('permission API failed'); } }
|
|
}, 42)).resolves.toMatchObject({ granted: null });
|
|
|
|
await expect(inspectTabHostAccess({
|
|
...base,
|
|
runtime: { lastError: { message: 'permission callback failed' } },
|
|
permissions: { contains: (_request, callback) => callback(false) }
|
|
}, 42)).resolves.toMatchObject({ granted: null });
|
|
|
|
vi.useFakeTimers();
|
|
const pending = inspectTabHostAccess({
|
|
...base,
|
|
permissions: { contains: () => undefined }
|
|
}, 42);
|
|
await vi.advanceTimersByTimeAsync(1000);
|
|
await expect(pending).resolves.toMatchObject({ granted: null });
|
|
});
|
|
});
|
|
|
|
describe('host access recovery contracts', () => {
|
|
it('keeps activation, permission recovery, and target identity guarded', () => {
|
|
const background = fs.readFileSync(path.join(repoRoot, 'extension/background.js'), 'utf8');
|
|
const popup = fs.readFileSync(path.join(repoRoot, 'extension/popup.js'), 'utf8');
|
|
const popupHtml = fs.readFileSync(path.join(repoRoot, 'extension/popup.html'), 'utf8');
|
|
const tabManager = fs.readFileSync(path.join(repoRoot, 'extension/modules/tab-manager.js'), 'utf8');
|
|
|
|
expect(background).toMatch(/await activateTargetTab\((?:message\.tabId|selectedTabId), message\.tabTitle\)/);
|
|
expect(background).toMatch(/addTabHostAccessRequest\(chrome, tabId, access\.originPattern\)/);
|
|
expect(background).toMatch(/retryPendingTarget\(\)/);
|
|
expect(background).toMatch(/activationGeneration !== targetActivationGeneration/);
|
|
expect(background).toMatch(/pendingTargetRequestId/);
|
|
expect(background).toMatch(/addedOrigins\.includes\(pending\.originPattern\)/);
|
|
expect(background).toMatch(/isCurrentTargetIdentity\(tabId, targetGeneration\)/);
|
|
expect(background).toMatch(/message\.expectedTabId/);
|
|
expect(background).toMatch(/completeForceSyncBeforeTargetChange\(selectedTabId\)/);
|
|
expect(background).toMatch(/FORCE_SYNC_ACK'[\s\S]*ignored_unselected_tab/);
|
|
expect(background).toMatch(/removeTabHostAccessRequest\([\s\S]*pendingTabId/);
|
|
|
|
const activationBody = background.slice(
|
|
background.indexOf('async function activateTargetTab'),
|
|
background.indexOf('async function retryPendingTarget')
|
|
);
|
|
expect(activationBody.indexOf('await injectContentScript')).toBeLessThan(
|
|
activationBody.indexOf('currentTabId = selectedTabId')
|
|
);
|
|
expect(popup).toMatch(/response\?\.status === 'host_permission_required'/);
|
|
expect(popup).toMatch(/requestOriginPermission\(chrome, requestedOriginPattern\)/);
|
|
expect(popup).toMatch(/expectedCurrentTabId: tabId/);
|
|
expect(popup).toMatch(/expectedTabId: tabId/);
|
|
expect(tabManager).not.toMatch(/injectContentScript/);
|
|
expect((background.match(/tabs\.onRemoved\.addListener/g) || []).length
|
|
+ (tabManager.match(/tabs\.onRemoved\.addListener/g) || []).length).toBe(1);
|
|
expect(popupHtml).toMatch(/id="siteAccessNotice"/);
|
|
});
|
|
});
|