test(ext): vitest + router sender-check + origin-bound autofill
This commit is contained in:
162
extension/src/service-worker/router/__tests__/router.test.ts
Normal file
162
extension/src/service-worker/router/__tests__/router.test.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { route, type RouterState } from '../index';
|
||||
import type { Request } from '../../../shared/messages';
|
||||
|
||||
// --- chrome.* shim ---
|
||||
|
||||
// @ts-expect-error test harness
|
||||
globalThis.chrome = {
|
||||
runtime: {
|
||||
id: 'relicario-test-id',
|
||||
getURL: (p: string) => `chrome-extension://relicario-test-id/${p}`,
|
||||
},
|
||||
storage: { local: { get: vi.fn().mockResolvedValue({}), set: vi.fn().mockResolvedValue(undefined) } },
|
||||
tabs: { get: vi.fn(), sendMessage: vi.fn() },
|
||||
};
|
||||
|
||||
function makePopupSender(): chrome.runtime.MessageSender {
|
||||
return { url: `chrome-extension://relicario-test-id/popup.html`, id: 'relicario-test-id' };
|
||||
}
|
||||
|
||||
function makeSetupSender(): chrome.runtime.MessageSender {
|
||||
return { url: `chrome-extension://relicario-test-id/setup.html`, id: 'relicario-test-id' };
|
||||
}
|
||||
|
||||
function makeContentSender(pageUrl = 'https://example.com/'): chrome.runtime.MessageSender {
|
||||
return {
|
||||
tab: { id: 42, url: pageUrl } as chrome.tabs.Tab,
|
||||
frameId: 0,
|
||||
id: 'relicario-test-id',
|
||||
};
|
||||
}
|
||||
|
||||
function makeExternalSender(): chrome.runtime.MessageSender {
|
||||
return { url: 'https://evil.example/', id: 'some-other-extension' };
|
||||
}
|
||||
|
||||
function makeState(): RouterState {
|
||||
return {
|
||||
manifest: { schema_version: 2, items: {} },
|
||||
gitHost: null,
|
||||
wasm: {
|
||||
// Stubs sufficient for the message types exercised by tests:
|
||||
new_item_id: () => 'fakeitemid0000ab',
|
||||
generate_password: () => 'PASSWORD',
|
||||
rate_passphrase: () => ({ score: 4, guesses_log10: 15 }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// --- Sender-check matrix ---
|
||||
|
||||
describe('router sender dispatch', () => {
|
||||
let state: RouterState;
|
||||
beforeEach(() => { state = makeState(); });
|
||||
|
||||
const popupOnlyMsgs: Request[] = [
|
||||
{ type: 'is_unlocked' },
|
||||
{ type: 'lock' },
|
||||
{ type: 'list_items' },
|
||||
{ type: 'generate_password', request: { kind: 'random', length: 20, classes: { lower: true, upper: true, digits: true, symbols: true }, symbol_charset: { kind: 'safe_only' } } },
|
||||
{ type: 'rate_passphrase', passphrase: 'hunter2hunter2hunter2' },
|
||||
{ type: 'get_blacklist' },
|
||||
];
|
||||
|
||||
for (const msg of popupOnlyMsgs) {
|
||||
it(`accepts popup-only "${msg.type}" from popup`, async () => {
|
||||
const res = await route(msg, state, makePopupSender());
|
||||
expect(res).toMatchObject({ ok: true });
|
||||
});
|
||||
it(`rejects popup-only "${msg.type}" from content`, async () => {
|
||||
const res = await route(msg, state, makeContentSender());
|
||||
expect(res).toEqual({ ok: false, error: 'unauthorized_sender' });
|
||||
});
|
||||
it(`rejects popup-only "${msg.type}" from external`, async () => {
|
||||
const res = await route(msg, state, makeExternalSender());
|
||||
expect(res).toEqual({ ok: false, error: 'unauthorized_sender' });
|
||||
});
|
||||
}
|
||||
|
||||
it('accepts save_setup from popup', async () => {
|
||||
const msg: Request = { type: 'save_setup', config: { hostType: 'github', hostUrl: '', repoPath: '', apiToken: '' }, imageBase64: '' };
|
||||
const res = await route(msg, state, makePopupSender());
|
||||
expect(res).toMatchObject({ ok: true });
|
||||
});
|
||||
|
||||
it('accepts save_setup from setup tab', async () => {
|
||||
const msg: Request = { type: 'save_setup', config: { hostType: 'github', hostUrl: '', repoPath: '', apiToken: '' }, imageBase64: '' };
|
||||
const res = await route(msg, state, makeSetupSender());
|
||||
expect(res).toMatchObject({ ok: true });
|
||||
});
|
||||
|
||||
it('rejects save_setup from content', async () => {
|
||||
const msg: Request = { type: 'save_setup', config: { hostType: 'github', hostUrl: '', repoPath: '', apiToken: '' }, imageBase64: '' };
|
||||
const res = await route(msg, state, makeContentSender());
|
||||
expect(res).toEqual({ ok: false, error: 'unauthorized_sender' });
|
||||
});
|
||||
|
||||
const contentMsgs: Request[] = [
|
||||
{ type: 'get_autofill_candidates' },
|
||||
{ type: 'blacklist_site' },
|
||||
];
|
||||
|
||||
for (const msg of contentMsgs) {
|
||||
it(`accepts content "${msg.type}" from top-frame content`, async () => {
|
||||
const res = await route(msg, state, makeContentSender());
|
||||
expect(res.ok).toBe(true);
|
||||
});
|
||||
it(`rejects content "${msg.type}" from popup`, async () => {
|
||||
const res = await route(msg, state, makePopupSender());
|
||||
expect(res).toEqual({ ok: false, error: 'unauthorized_sender' });
|
||||
});
|
||||
it(`rejects content "${msg.type}" from subframe`, async () => {
|
||||
const sender: chrome.runtime.MessageSender = { ...makeContentSender(), frameId: 3 };
|
||||
const res = await route(msg, state, sender);
|
||||
expect(res).toEqual({ ok: false, error: 'unauthorized_sender' });
|
||||
});
|
||||
it(`rejects content "${msg.type}" from external`, async () => {
|
||||
const res = await route(msg, state, makeExternalSender());
|
||||
expect(res).toEqual({ ok: false, error: 'unauthorized_sender' });
|
||||
});
|
||||
}
|
||||
|
||||
it('rejects unknown message type', async () => {
|
||||
// @ts-expect-error intentional invalid type
|
||||
const res = await route({ type: 'nonsense' }, state, makePopupSender());
|
||||
expect(res).toEqual({ ok: false, error: 'unknown_message_type' });
|
||||
});
|
||||
});
|
||||
|
||||
// --- Origin-bound autofill ---
|
||||
|
||||
describe('get_autofill_candidates uses sender.tab.url', () => {
|
||||
it('derives hostname from sender, not message', async () => {
|
||||
const state: RouterState = makeState();
|
||||
state.manifest = {
|
||||
schema_version: 2,
|
||||
items: {
|
||||
'aaaaaaaaaaaaaaaa': {
|
||||
id: 'aaaaaaaaaaaaaaaa', type: 'login', title: 'GitHub',
|
||||
tags: [], favorite: false, icon_hint: 'github.com',
|
||||
modified: 0, attachment_summaries: [],
|
||||
},
|
||||
'bbbbbbbbbbbbbbbb': {
|
||||
id: 'bbbbbbbbbbbbbbbb', type: 'login', title: 'Example',
|
||||
tags: [], favorite: false, icon_hint: 'example.com',
|
||||
modified: 0, attachment_summaries: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
const res = await route(
|
||||
{ type: 'get_autofill_candidates' },
|
||||
state,
|
||||
makeContentSender('https://example.com/login'),
|
||||
);
|
||||
expect(res.ok).toBe(true);
|
||||
if (res.ok) {
|
||||
const data = res.data as { candidates: Array<[string, { title: string }]> };
|
||||
expect(data.candidates).toHaveLength(1);
|
||||
expect(data.candidates[0][1].title).toBe('Example');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-nocheck — transitional: downstream files updated in Slice 6 (item-* rewrites) / Slice 4 (vitest setup) / Slice 5 (content + setup rewires)
|
||||
// extension/src/shared/__tests__/base32.test.ts
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { base32Decode, base32Encode } from '../base32';
|
||||
|
||||
Reference in New Issue
Block a user