Files
ChatApp/apps/desktop/src/lib/userIdentity.test.ts
T
byGalax 2c586351fc fix(shared): expose ./crypto/testBackend in package exports
Removes the @shared Vite-alias workaround in userIdentity.test.ts so
tsc can resolve the import without an extra paths entry.
2026-05-15 22:43:24 +02:00

86 lines
3.7 KiB
TypeScript

import { describe, expect, it, beforeEach, vi, beforeAll } from 'vitest';
import { setCryptoBackend } from '@chat-app/shared/crypto';
import { makeWasmTestBackend } from '@chat-app/shared/crypto/testBackend';
beforeAll(async () => { setCryptoBackend(await makeWasmTestBackend()); });
const memStore: Record<string, Uint8Array> = {};
const fakeStore = {
getSecret: async (k: string) => memStore[k] ?? null,
setSecret: async (k: string, v: Uint8Array) => { memStore[k] = v; },
removeSecret: async (k: string) => { delete memStore[k]; },
};
vi.mock('./secretStore', () => ({
devLocalSecretStore: fakeStore,
setSecretStoreUser: vi.fn(),
isEncryptedVaultActive: () => false,
}));
vi.mock('./supabase', () => {
const rpcResponses = new Map<string, { data: unknown; error: unknown }>();
const rpc = vi.fn((name: string, _params: unknown) =>
Promise.resolve(rpcResponses.get(name) ?? { data: null, error: null }),
);
const supabase = {
auth: { getUser: vi.fn().mockResolvedValue({ data: { user: { id: 'me' } }, error: null }) },
rpc,
from: vi.fn(() => ({
select: () => ({ in: () => Promise.resolve({ data: [], error: null }) }),
})),
};
return { supabase, __setRpcResponse: (n: string, r: { data: unknown; error: unknown }) => rpcResponses.set(n, r) };
});
describe('userIdentity', () => {
beforeEach(() => { for (const k of Object.keys(memStore)) delete memStore[k]; });
it('setupNewUserIdentity uploads blob, caches private key in store', async () => {
const supabaseMod = await import('./supabase');
(supabaseMod as unknown as { __setRpcResponse: (n: string, r: { data: unknown; error: unknown }) => void })
.__setRpcResponse('reset_user_key', { data: 0, error: null });
const { setupNewUserIdentity, cachedUserKey } = await import('./userIdentity');
const result = await setupNewUserIdentity({ userId: 'me', pin: '123456', withRecovery: true });
expect(result.publicKey).toHaveLength(32);
expect(result.recoveryCode).toMatch(/^[A-Z2-9]{6}-[A-Z2-9]{6}-[A-Z2-9]{6}-[A-Z2-9]{6}$/);
const cached = await cachedUserKey('me');
expect(cached).toBeInstanceOf(Uint8Array);
expect(cached?.length).toBe(32);
});
it('loadOrUnlockUserKey unlocks with correct PIN, throws on wrong PIN', async () => {
const { setupNewUserIdentity, clearUserKeyCache, loadOrUnlockUserKey } =
await import('./userIdentity');
const supabaseMod = await import('./supabase');
const setRpc = (supabaseMod as unknown as { __setRpcResponse: (n: string, r: { data: unknown; error: unknown }) => void }).__setRpcResponse;
setRpc('reset_user_key', { data: 0, error: null });
await setupNewUserIdentity({ userId: 'me', pin: '123456', withRecovery: false });
const rpc = (supabaseMod.supabase as unknown as { rpc: ReturnType<typeof vi.fn> }).rpc;
const lastCall = rpc.mock.calls[rpc.mock.calls.length - 1]!;
const params = lastCall[1] as Record<string, string>;
setRpc('try_unlock_user_key', {
data: {
exists: true, locked: false,
sealed_private_key: params.p_sealed_private_b64,
salt: params.p_salt_b64,
kdf_params: params.p_kdf_params,
recovery_sealed_private_key: null, recovery_salt: null,
failed_attempts: 0, failed_recovery_attempts: 0,
recovery_locked_until: null, key_version: 1,
},
error: null,
});
setRpc('record_pin_attempt', { data: { failed_attempts: 0, locked_until: null }, error: null });
await clearUserKeyCache('me');
const ok = await loadOrUnlockUserKey({ userId: 'me', pin: '123456' });
expect(ok.kind).toBe('unlocked');
await clearUserKeyCache('me');
await expect(loadOrUnlockUserKey({ userId: 'me', pin: '654321' })).rejects.toThrow();
});
});