📄 src/web3/wallet/credentials.test.ts
D-OPEN SOVEREIGN
1import { describe, it, expect, beforeEach } from 'vitest';
2import {
3 getCachedEmail,
4 setCachedEmail,
5 storePrivateKey,
6 getPrivateKey,
7 hasPrivateKey,
8 hasAnyPrivateKey,
9 storeEncryptedPrivateKey,
10 getDecryptedPrivateKey,
11 hasEncryptedPrivateKey,
12} from './credentials';
13
14class FakeLocalStorage {
15 private map = new Map<string, string>();
16 get length() { return this.map.size; }
17 key(i: number): string | null { return Array.from(this.map.keys())[i] ?? null; }
18 getItem(k: string): string | null { return this.map.has(k) ? this.map.get(k)! : null; }
19 setItem(k: string, v: string): void { this.map.set(k, v); }
20 removeItem(k: string): void { this.map.delete(k); }
21 clear(): void { this.map.clear(); }
22}
23
24beforeEach(() => {
25 (globalThis as any).window = globalThis;
26 (globalThis as any).localStorage = new FakeLocalStorage();
27 setCachedEmail(null);
28});
29
30describe('cached email', () => {
31 it('defaults to null', () => {
32 expect(getCachedEmail()).toBeNull();
33 });
34
35 it('round-trips through setCachedEmail/getCachedEmail', () => {
36 setCachedEmail('a@b.com');
37 expect(getCachedEmail()).toBe('a@b.com');
38 });
39
40 it('can be cleared back to null', () => {
41 setCachedEmail('a@b.com');
42 setCachedEmail(null);
43 expect(getCachedEmail()).toBeNull();
44 });
45});
46
47describe('storePrivateKey / getPrivateKey / hasPrivateKey', () => {
48 it('round-trips a private key for a given chainId', () => {
49 storePrivateKey(1116, '0xabc');
50 expect(getPrivateKey(1116)).toBe('0xabc');
51 expect(hasPrivateKey(1116)).toBe(true);
52 });
53
54 it('keeps keys for different chainIds independent', () => {
55 storePrivateKey(1116, '0xmainnet');
56 storePrivateKey(1114, '0xtestnet');
57
58 expect(getPrivateKey(1116)).toBe('0xmainnet');
59 expect(getPrivateKey(1114)).toBe('0xtestnet');
60 });
61
62 it('returns null and false for a chainId with no stored key', () => {
63 expect(getPrivateKey(9999)).toBeNull();
64 expect(hasPrivateKey(9999)).toBe(false);
65 });
66
67 it('throws when storing an empty private key', () => {
68 expect(() => storePrivateKey(1116, '')).toThrow('Private key is required');
69 });
70
71 it('overwrites an existing key for the same chainId', () => {
72 storePrivateKey(1116, '0xold');
73 storePrivateKey(1116, '0xnew');
74 expect(getPrivateKey(1116)).toBe('0xnew');
75 });
76
77 it('returns null instead of throwing when localStorage contains malformed JSON for the key', () => {
78 (globalThis as any).localStorage.setItem('@the_library/web3-session:wallet_pk_1116', '{not json');
79 expect(getPrivateKey(1116)).toBeNull();
80 });
81
82 it('treats string and numeric chainId the same way', () => {
83 storePrivateKey(1116, '0xabc');
84 expect(getPrivateKey('1116')).toBe('0xabc');
85 });
86});
87
88describe('hasAnyPrivateKey', () => {
89 it('is false when no keys are stored', () => {
90 expect(hasAnyPrivateKey()).toBe(false);
91 });
92
93 it('detects a stored key against a spec-correct localStorage implementation (fixed)', () => {
94 // Regression test: hasAnyPrivateKey() must enumerate via
95 // `localStorage.length` + `localStorage.key(i)` (the documented Web
96 // Storage API), not `Object.keys(localStorage)` — the latter only
97 // happens to work against a real browser's non-standard indexed-property
98 // Storage object, not this (or any spec-correct) implementation.
99 storePrivateKey(1116, '0xabc');
100 expect(hasAnyPrivateKey()).toBe(true);
101 });
102
103 it('is false when localStorage holds unrelated keys only', () => {
104 (globalThis as any).localStorage.setItem('some-other-app:unrelated', 'x');
105 expect(hasAnyPrivateKey()).toBe(false);
106 });
107
108 it('is false when localStorage is unavailable', () => {
109 delete (globalThis as any).window;
110 expect(hasAnyPrivateKey()).toBe(false);
111 });
112});
113
114describe('storage-unavailable guard (no window/localStorage)', () => {
115 beforeEach(() => {
116 delete (globalThis as any).window;
117 });
118
119 it('getPrivateKey returns null instead of throwing', () => {
120 expect(getPrivateKey(1116)).toBeNull();
121 });
122
123 it('hasPrivateKey returns false instead of throwing', () => {
124 expect(hasPrivateKey(1116)).toBe(false);
125 });
126
127 it('storePrivateKey does not throw even though it cannot persist', () => {
128 expect(() => storePrivateKey(1116, '0xabc')).not.toThrow();
129 });
130});
131
132describe('legacy aliases', () => {
133 it('storeEncryptedPrivateKey/getDecryptedPrivateKey/hasEncryptedPrivateKey delegate to the plain equivalents', () => {
134 storeEncryptedPrivateKey(1116, '0xabc', 'ignored@email.com');
135 expect(getDecryptedPrivateKey(1116, 'ignored@email.com')).toBe('0xabc');
136 expect(hasEncryptedPrivateKey(1116)).toBe(true);
137 });
138});
139