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 { GitHost } from '../git-host';
|
||||
import * as vault from '../vault';
|
||||
import { itemToManifestEntry } from '../vault';
|
||||
import * as session from '../session';
|
||||
import { loadDeviceSettings, loadBlacklist, saveBlacklist } from '../storage';
|
||||
|
||||
export interface ContentState {
|
||||
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 {
|
||||
try { return new URL(url).hostname; } catch { return undefined; }
|
||||
}
|
||||
|
||||
@@ -4,15 +4,16 @@
|
||||
/// via sender.url === popup.html (or setup.html for save_setup).
|
||||
|
||||
import type { PopupMessage, Response } from '../../shared/messages';
|
||||
import type { Item, ItemId, Manifest, VaultConfig, SetupState, DeviceSettings, TotpConfig, AttachmentRef } from '../../shared/types';
|
||||
import { DEFAULT_DEVICE_SETTINGS } from '../../shared/types';
|
||||
import type { Item, ItemId, Manifest, VaultConfig, SetupState, TotpConfig, AttachmentRef } from '../../shared/types';
|
||||
import { base32Decode } from '../../shared/base32';
|
||||
import type { GitHost } from '../git-host';
|
||||
import { createGitHost, base64ToUint8Array } from '../git-host';
|
||||
import * as vault from '../vault';
|
||||
import { itemToManifestEntry } from '../vault';
|
||||
import * as session from '../session';
|
||||
import * as devices from '../devices';
|
||||
import * as sessionTimer from '../session-timer';
|
||||
import { loadDeviceSettings, saveDeviceSettings, loadBlacklist, saveBlacklist } from '../storage';
|
||||
|
||||
// --- 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 };
|
||||
}
|
||||
|
||||
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 {
|
||||
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}`);
|
||||
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