📄 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