test(ext): vitest + router sender-check + origin-bound autofill

This commit is contained in:
adlee-was-taken
2026-04-20 20:15:49 -04:00
parent 2d4dcb5f6b
commit 3d2b021cb2
5 changed files with 359 additions and 2 deletions

View 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');
}
});
});