- IndexedDB-backed key-value store with non-extractable AES-256-GCM CryptoKey - Key material never leaves browser crypto subsystem - API: set, get, delete, clear, has, keys — all async - Namespace isolation for multi-app usage - Falls back to localStorage when SubtleCrypto unavailable - 16 Vitest tests (fake-indexeddb), all passing - Add IndexedDB globals to root ESLint config
124 lines
4.1 KiB
TypeScript
124 lines
4.1 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import 'fake-indexeddb/auto';
|
|
import { SecureStorage } from './secure-storage.js';
|
|
|
|
describe('SecureStorage', () => {
|
|
let storage: SecureStorage;
|
|
|
|
beforeEach(async () => {
|
|
storage = new SecureStorage('com.test.app');
|
|
await storage.clear();
|
|
});
|
|
|
|
// ── Basic CRUD ──────────────────────────────────
|
|
|
|
it('set and get', async () => {
|
|
await storage.set('token', 'abc123');
|
|
const val = await storage.get('token');
|
|
expect(val).toBe('abc123');
|
|
});
|
|
|
|
it('get returns null for missing key', async () => {
|
|
const val = await storage.get('nonexistent');
|
|
expect(val).toBeNull();
|
|
});
|
|
|
|
it('overwrite existing key', async () => {
|
|
await storage.set('token', 'first');
|
|
await storage.set('token', 'second');
|
|
const val = await storage.get('token');
|
|
expect(val).toBe('second');
|
|
});
|
|
|
|
it('delete removes value', async () => {
|
|
await storage.set('token', 'abc');
|
|
await storage.delete('token');
|
|
const val = await storage.get('token');
|
|
expect(val).toBeNull();
|
|
});
|
|
|
|
it('delete non-existent key does not throw', async () => {
|
|
await expect(storage.delete('nope')).resolves.toBeUndefined();
|
|
});
|
|
|
|
// ── Clear ───────────────────────────────────────
|
|
|
|
it('clear removes all values', async () => {
|
|
await storage.set('a', '1');
|
|
await storage.set('b', '2');
|
|
await storage.clear();
|
|
expect(await storage.get('a')).toBeNull();
|
|
expect(await storage.get('b')).toBeNull();
|
|
});
|
|
|
|
// ── Has ─────────────────────────────────────────
|
|
|
|
it('has returns true for existing key', async () => {
|
|
await storage.set('x', 'val');
|
|
expect(await storage.has('x')).toBe(true);
|
|
});
|
|
|
|
it('has returns false for missing key', async () => {
|
|
expect(await storage.has('missing')).toBe(false);
|
|
});
|
|
|
|
// ── Keys ────────────────────────────────────────
|
|
|
|
it('keys lists stored keys', async () => {
|
|
await storage.set('alpha', '1');
|
|
await storage.set('beta', '2');
|
|
const keys = await storage.keys();
|
|
expect(keys.sort()).toEqual(['alpha', 'beta']);
|
|
});
|
|
|
|
it('keys returns empty for fresh storage', async () => {
|
|
const keys = await storage.keys();
|
|
expect(keys).toEqual([]);
|
|
});
|
|
|
|
// ── Namespace isolation ─────────────────────────
|
|
|
|
it('different namespaces are isolated', async () => {
|
|
const s1 = new SecureStorage('ns1');
|
|
const s2 = new SecureStorage('ns2');
|
|
await s1.set('key', 'from-s1');
|
|
await s2.set('key', 'from-s2');
|
|
expect(await s1.get('key')).toBe('from-s1');
|
|
expect(await s2.get('key')).toBe('from-s2');
|
|
});
|
|
|
|
// ── Data types ──────────────────────────────────
|
|
|
|
it('empty string', async () => {
|
|
await storage.set('empty', '');
|
|
const val = await storage.get('empty');
|
|
expect(val).toBe('');
|
|
});
|
|
|
|
it('unicode values', async () => {
|
|
const text = 'こんにちは世界 🌍 مرحبا Ñoño';
|
|
await storage.set('unicode', text);
|
|
expect(await storage.get('unicode')).toBe(text);
|
|
});
|
|
|
|
it('large value', async () => {
|
|
const big = 'X'.repeat(100_000);
|
|
await storage.set('big', big);
|
|
expect(await storage.get('big')).toBe(big);
|
|
});
|
|
|
|
it('JSON string value', async () => {
|
|
const json = JSON.stringify({ token: 'abc', user: { id: 1 } });
|
|
await storage.set('session', json);
|
|
const parsed = JSON.parse((await storage.get('session'))!);
|
|
expect(parsed.token).toBe('abc');
|
|
expect(parsed.user.id).toBe(1);
|
|
});
|
|
|
|
// ── isSupported ─────────────────────────────────
|
|
|
|
it('isSupported returns true in test environment', () => {
|
|
expect(SecureStorage.isSupported()).toBe(true);
|
|
});
|
|
});
|