refactor(ext/sw): extract storage.ts + move itemToManifestEntry (Plan C Phase 2)
P1.9: loadDeviceSettings / loadBlacklist / saveBlacklist / saveDeviceSettings + itemToManifestEntry were duplicated across popup-only.ts and content-callable.ts. Lifts the four storage helpers into service-worker/ storage.ts and itemToManifestEntry into service-worker/vault.ts. Both router files now import from one home each. Adds storage.test.ts covering round-trips and defaults. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
56
extension/src/service-worker/__tests__/storage.test.ts
Normal file
56
extension/src/service-worker/__tests__/storage.test.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { loadDeviceSettings, saveDeviceSettings, loadBlacklist, saveBlacklist }
|
||||||
|
from '../storage';
|
||||||
|
|
||||||
|
function mockChromeStorage(initial: Record<string, unknown> = {}) {
|
||||||
|
const store: Record<string, unknown> = { ...initial };
|
||||||
|
(global as { chrome: unknown }).chrome = {
|
||||||
|
storage: {
|
||||||
|
local: {
|
||||||
|
get: vi.fn((keys: string | string[]) => {
|
||||||
|
const arr = Array.isArray(keys) ? keys : [keys];
|
||||||
|
const out: Record<string, unknown> = {};
|
||||||
|
for (const k of arr) if (k in store) out[k] = store[k];
|
||||||
|
return Promise.resolve(out);
|
||||||
|
}),
|
||||||
|
set: vi.fn((kv: Record<string, unknown>) => {
|
||||||
|
Object.assign(store, kv);
|
||||||
|
return Promise.resolve();
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as never;
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('service-worker/storage', () => {
|
||||||
|
beforeEach(() => { mockChromeStorage(); });
|
||||||
|
|
||||||
|
it('loadDeviceSettings returns default when storage is empty', async () => {
|
||||||
|
const s = await loadDeviceSettings();
|
||||||
|
expect(s.captureEnabled).toBe(false);
|
||||||
|
expect(s.captureStyle).toBe('bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loadDeviceSettings returns stored value', async () => {
|
||||||
|
mockChromeStorage({ relicarioSettings: { captureEnabled: true, captureStyle: 'toast' } });
|
||||||
|
const s = await loadDeviceSettings();
|
||||||
|
expect(s.captureEnabled).toBe(true);
|
||||||
|
expect(s.captureStyle).toBe('toast');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('saveDeviceSettings persists', async () => {
|
||||||
|
const store = mockChromeStorage();
|
||||||
|
await saveDeviceSettings({ captureEnabled: true, captureStyle: 'bar' });
|
||||||
|
expect(store.relicarioSettings).toEqual({ captureEnabled: true, captureStyle: 'bar' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loadBlacklist returns empty array by default', async () => {
|
||||||
|
expect(await loadBlacklist()).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('saveBlacklist / loadBlacklist round-trips', async () => {
|
||||||
|
await saveBlacklist(['example.com', 'evil.test']);
|
||||||
|
expect(await loadBlacklist()).toEqual(['example.com', 'evil.test']);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,7 +8,9 @@ import type { ContentMessage, Response } from '../../shared/messages';
|
|||||||
import type { Item, Manifest } from '../../shared/types';
|
import type { Item, Manifest } from '../../shared/types';
|
||||||
import type { GitHost } from '../git-host';
|
import type { GitHost } from '../git-host';
|
||||||
import * as vault from '../vault';
|
import * as vault from '../vault';
|
||||||
|
import { itemToManifestEntry } from '../vault';
|
||||||
import * as session from '../session';
|
import * as session from '../session';
|
||||||
|
import { loadDeviceSettings, loadBlacklist, saveBlacklist } from '../storage';
|
||||||
|
|
||||||
export interface ContentState {
|
export interface ContentState {
|
||||||
manifest: Manifest | null;
|
manifest: Manifest | null;
|
||||||
@@ -164,41 +166,6 @@ export async function handle(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Manifest entry derivation (duplicated from popup-only for self-containment) ---
|
|
||||||
|
|
||||||
function itemToManifestEntry(item: Item) {
|
|
||||||
return {
|
|
||||||
id: item.id,
|
|
||||||
type: item.type,
|
|
||||||
title: item.title,
|
|
||||||
tags: item.tags,
|
|
||||||
favorite: item.favorite,
|
|
||||||
group: item.group,
|
|
||||||
icon_hint: (item.core.type === 'login' && item.core.url)
|
|
||||||
? safeHostname(item.core.url) : undefined,
|
|
||||||
modified: item.modified,
|
|
||||||
trashed_at: item.trashed_at,
|
|
||||||
attachment_summaries: item.attachments.map((a) => ({
|
|
||||||
id: a.id, filename: a.filename, mime_type: a.mime_type, size: a.size,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadDeviceSettings(): Promise<{ captureEnabled: boolean; captureStyle: 'bar' | 'toast' }> {
|
|
||||||
const r = await chrome.storage.local.get('relicarioSettings');
|
|
||||||
return (r.relicarioSettings as { captureEnabled: boolean; captureStyle: 'bar' | 'toast' })
|
|
||||||
?? { captureEnabled: false, captureStyle: 'bar' };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadBlacklist(): Promise<string[]> {
|
|
||||||
const r = await chrome.storage.local.get('captureBlacklist');
|
|
||||||
return (r.captureBlacklist as string[]) ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveBlacklist(list: string[]): Promise<void> {
|
|
||||||
await chrome.storage.local.set({ captureBlacklist: list });
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeHostname(url: string): string | undefined {
|
function safeHostname(url: string): string | undefined {
|
||||||
try { return new URL(url).hostname; } catch { return undefined; }
|
try { return new URL(url).hostname; } catch { return undefined; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,16 @@
|
|||||||
/// via sender.url === popup.html (or setup.html for save_setup).
|
/// via sender.url === popup.html (or setup.html for save_setup).
|
||||||
|
|
||||||
import type { PopupMessage, Response } from '../../shared/messages';
|
import type { PopupMessage, Response } from '../../shared/messages';
|
||||||
import type { Item, ItemId, Manifest, VaultConfig, SetupState, DeviceSettings, TotpConfig, AttachmentRef } from '../../shared/types';
|
import type { Item, ItemId, Manifest, VaultConfig, SetupState, TotpConfig, AttachmentRef } from '../../shared/types';
|
||||||
import { DEFAULT_DEVICE_SETTINGS } from '../../shared/types';
|
|
||||||
import { base32Decode } from '../../shared/base32';
|
import { base32Decode } from '../../shared/base32';
|
||||||
import type { GitHost } from '../git-host';
|
import type { GitHost } from '../git-host';
|
||||||
import { createGitHost, base64ToUint8Array } from '../git-host';
|
import { createGitHost, base64ToUint8Array } from '../git-host';
|
||||||
import * as vault from '../vault';
|
import * as vault from '../vault';
|
||||||
|
import { itemToManifestEntry } from '../vault';
|
||||||
import * as session from '../session';
|
import * as session from '../session';
|
||||||
import * as devices from '../devices';
|
import * as devices from '../devices';
|
||||||
import * as sessionTimer from '../session-timer';
|
import * as sessionTimer from '../session-timer';
|
||||||
|
import { loadDeviceSettings, saveDeviceSettings, loadBlacklist, saveBlacklist } from '../storage';
|
||||||
|
|
||||||
// --- Shared ambient state owned by the SW module ---
|
// --- Shared ambient state owned by the SW module ---
|
||||||
//
|
//
|
||||||
@@ -684,44 +685,6 @@ async function loadSetupState(): Promise<SetupState> {
|
|||||||
return { config, imageBase64, isConfigured: config !== null && imageBase64 !== null };
|
return { config, imageBase64, isConfigured: config !== null && imageBase64 !== null };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadDeviceSettings(): Promise<DeviceSettings> {
|
|
||||||
const r = await chrome.storage.local.get('relicarioSettings');
|
|
||||||
return (r.relicarioSettings as DeviceSettings) ?? { ...DEFAULT_DEVICE_SETTINGS };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveDeviceSettings(s: DeviceSettings): Promise<void> {
|
|
||||||
await chrome.storage.local.set({ relicarioSettings: s });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadBlacklist(): Promise<string[]> {
|
|
||||||
const r = await chrome.storage.local.get('captureBlacklist');
|
|
||||||
return (r.captureBlacklist as string[]) ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveBlacklist(list: string[]): Promise<void> {
|
|
||||||
await chrome.storage.local.set({ captureBlacklist: list });
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Manifest entry derivation (duplicated from index; kept here to keep handler self-contained) ---
|
|
||||||
|
|
||||||
function itemToManifestEntry(item: Item) {
|
|
||||||
return {
|
|
||||||
id: item.id,
|
|
||||||
type: item.type,
|
|
||||||
title: item.title,
|
|
||||||
tags: item.tags,
|
|
||||||
favorite: item.favorite,
|
|
||||||
group: item.group,
|
|
||||||
icon_hint: (item.core.type === 'login' && item.core.url)
|
|
||||||
? safeHostname(item.core.url) : undefined,
|
|
||||||
modified: item.modified,
|
|
||||||
trashed_at: item.trashed_at,
|
|
||||||
attachment_summaries: item.attachments.map((a) => ({
|
|
||||||
id: a.id, filename: a.filename, mime_type: a.mime_type, size: a.size,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeHostname(url: string): string | undefined {
|
function safeHostname(url: string): string | undefined {
|
||||||
try { return new URL(url).hostname; } catch { return undefined; }
|
try { return new URL(url).hostname; } catch { return undefined; }
|
||||||
}
|
}
|
||||||
|
|||||||
25
extension/src/service-worker/storage.ts
Normal file
25
extension/src/service-worker/storage.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/// Single home for chrome.storage.local reads/writes done by the service
|
||||||
|
/// worker. Both router files (popup-only.ts and content-callable.ts) import
|
||||||
|
/// from here — the duplicated definitions in those files were lifted out as
|
||||||
|
/// part of Plan C Phase 2 (P1.9).
|
||||||
|
|
||||||
|
import type { DeviceSettings } from '../shared/types';
|
||||||
|
import { DEFAULT_DEVICE_SETTINGS } from '../shared/types';
|
||||||
|
|
||||||
|
export async function loadDeviceSettings(): Promise<DeviceSettings> {
|
||||||
|
const r = await chrome.storage.local.get('relicarioSettings');
|
||||||
|
return (r.relicarioSettings as DeviceSettings) ?? { ...DEFAULT_DEVICE_SETTINGS };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveDeviceSettings(s: DeviceSettings): Promise<void> {
|
||||||
|
await chrome.storage.local.set({ relicarioSettings: s });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadBlacklist(): Promise<string[]> {
|
||||||
|
const r = await chrome.storage.local.get('captureBlacklist');
|
||||||
|
return (r.captureBlacklist as string[]) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveBlacklist(list: string[]): Promise<void> {
|
||||||
|
await chrome.storage.local.set({ captureBlacklist: list });
|
||||||
|
}
|
||||||
@@ -395,3 +395,33 @@ export async function removeAttachmentsFromItem(
|
|||||||
await syncManifestAttachments(git, handle, itemId, item, `manifest: sync attachments for ${itemId}`);
|
await syncManifestAttachments(git, handle, itemId, item, `manifest: sync attachments for ${itemId}`);
|
||||||
return removed;
|
return removed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Manifest entry derivation ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project a decrypted Item into its ManifestEntry shape for browse-without-
|
||||||
|
* decrypt views. Both router files use this; defined here (the SW's
|
||||||
|
* vault-orchestration home) instead of duplicated in each router. Moved out
|
||||||
|
* of popup-only.ts / content-callable.ts as part of Plan C Phase 2 (P1.9).
|
||||||
|
*/
|
||||||
|
export function itemToManifestEntry(item: Item): ManifestEntry {
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
type: item.type,
|
||||||
|
title: item.title,
|
||||||
|
tags: item.tags,
|
||||||
|
favorite: item.favorite,
|
||||||
|
group: item.group,
|
||||||
|
icon_hint: (item.core.type === 'login' && item.core.url)
|
||||||
|
? safeHostname(item.core.url) : undefined,
|
||||||
|
modified: item.modified,
|
||||||
|
trashed_at: item.trashed_at,
|
||||||
|
attachment_summaries: item.attachments.map((a) => ({
|
||||||
|
id: a.id, filename: a.filename, mime_type: a.mime_type, size: a.size,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeHostname(url: string): string | undefined {
|
||||||
|
try { return new URL(url).hostname; } catch { return undefined; }
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user