Skip to content

Commit 547cf6e

Browse files
committed
Add actor mock helper and tests
1 parent 9aa4faa commit 547cf6e

File tree

5 files changed

+307
-0
lines changed

5 files changed

+307
-0
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { jest } from '@jest/globals';
2+
3+
// Minimal actor stub used to observe send calls and emit state transitions
4+
5+
export const actorMock = {
6+
start: jest.fn(),
7+
stop: jest.fn(),
8+
send: jest.fn(),
9+
subscribe: jest.fn((cb: (state: any) => void) => {
10+
(actorMock as any)._callback = cb;
11+
return { unsubscribe: jest.fn() };
12+
}),
13+
};
14+
15+
export function emitState(stateValue: string) {
16+
const cb = (actorMock as any)._callback;
17+
if (cb) {
18+
cb({ value: stateValue, matches: (v: string) => v === stateValue });
19+
}
20+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { jest } from '@jest/globals';
2+
import { useProvingStore } from '../../../src/utils/proving/provingMachine';
3+
import { useProtocolStore } from '../../../src/stores/protocolStore';
4+
import { useSelfAppStore } from '../../../src/stores/selfAppStore';
5+
6+
jest.mock('xstate', () => {
7+
const actual = jest.requireActual('xstate');
8+
const { actorMock } = require('./actorMock');
9+
return { ...actual, createActor: jest.fn(() => actorMock) };
10+
});
11+
12+
jest.mock('../../../src/utils/analytics', () => () => ({ trackEvent: jest.fn() }));
13+
14+
jest.mock('@selfxyz/common', () => {
15+
const actual = jest.requireActual('@selfxyz/common');
16+
return { ...actual, getSolidityPackedUserContextData: jest.fn(() => '0x1234') };
17+
});
18+
19+
jest.mock('../../../src/utils/proving/provingInputs', () => ({
20+
generateTEEInputsRegister: jest.fn(() => ({
21+
inputs: { r: 1 },
22+
circuitName: 'reg',
23+
endpointType: 'celo',
24+
endpoint: 'https://reg',
25+
})),
26+
generateTEEInputsDSC: jest.fn(() => ({
27+
inputs: { d: 1 },
28+
circuitName: 'dsc',
29+
endpointType: 'celo',
30+
endpoint: 'https://dsc',
31+
})),
32+
generateTEEInputsDisclose: jest.fn(() => ({
33+
inputs: { s: 1 },
34+
circuitName: 'vc_and_disclose',
35+
endpointType: 'https',
36+
endpoint: 'https://dis',
37+
})),
38+
}));
39+
40+
jest.mock('../../../src/utils/proving/provingUtils', () => {
41+
const actual = jest.requireActual('../../../src/utils/proving/provingUtils');
42+
return {
43+
...actual,
44+
getPayload: jest.fn(() => ({ mocked: true })),
45+
encryptAES256GCM: jest.fn(() => ({ nonce: [0], cipher_text: [1], auth_tag: [2] })),
46+
};
47+
});
48+
49+
const { getPayload, encryptAES256GCM } = require('../../../src/utils/proving/provingUtils');
50+
const { generateTEEInputsRegister, generateTEEInputsDSC, generateTEEInputsDisclose } = require('../../../src/utils/proving/provingInputs');
51+
52+
describe('_generatePayload', () => {
53+
beforeEach(() => {
54+
jest.clearAllMocks();
55+
useProvingStore.setState({
56+
circuitType: 'register',
57+
passportData: { documentCategory: 'passport', mock: false },
58+
secret: 'sec',
59+
uuid: '123',
60+
sharedKey: Buffer.alloc(32, 1),
61+
env: 'prod',
62+
});
63+
useSelfAppStore.setState({ selfApp: { chainID: 1, userId: 'u', userDefinedData: '0x0', endpointType: 'https', endpoint: 'https://e', scope: 's', sessionId: '', appName: '', logoBase64: '', header: '', userIdType: 'uuid', devMode: false, disclosures: {}, version: 1 } });
64+
useProtocolStore.setState({ passport: { dsc_tree: 'tree', csca_tree: [['a']], commitment_tree: null, deployed_circuits: null, circuits_dns_mapping: null, alternative_csca: {} }, id_card: { commitment_tree: null, dsc_tree: null, csca_tree: null, deployed_circuits: null, circuits_dns_mapping: null, alternative_csca: {} } });
65+
});
66+
67+
it('register circuit', async () => {
68+
useProvingStore.setState({ circuitType: 'register' });
69+
const payload = await useProvingStore.getState()._generatePayload();
70+
expect(generateTEEInputsRegister).toHaveBeenCalled();
71+
expect(getPayload).toHaveBeenCalled();
72+
expect(encryptAES256GCM).toHaveBeenCalled();
73+
expect(useProvingStore.getState().endpointType).toBe('celo');
74+
expect(payload.params).toEqual({ uuid: '123', nonce: [0], cipher_text: [1], auth_tag: [2] });
75+
});
76+
77+
it('dsc circuit', async () => {
78+
useProvingStore.setState({ circuitType: 'dsc' });
79+
const payload = await useProvingStore.getState()._generatePayload();
80+
expect(generateTEEInputsDSC).toHaveBeenCalled();
81+
expect(useProvingStore.getState().endpointType).toBe('celo');
82+
expect(payload.params.uuid).toBe('123');
83+
});
84+
85+
it('disclose circuit', async () => {
86+
useProvingStore.setState({ circuitType: 'disclose' });
87+
const payload = await useProvingStore.getState()._generatePayload();
88+
expect(generateTEEInputsDisclose).toHaveBeenCalled();
89+
expect(useProvingStore.getState().endpointType).toBe('https');
90+
expect(payload.params.uuid).toBe('123');
91+
});
92+
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { jest } from '@jest/globals';
2+
import { useProvingStore } from '../../../src/utils/proving/provingMachine';
3+
import { emitState } from './actorMock';
4+
5+
jest.mock('xstate', () => {
6+
const actual = jest.requireActual('xstate');
7+
const { actorMock } = require('./actorMock');
8+
return { ...actual, createActor: jest.fn(() => actorMock) };
9+
});
10+
11+
jest.mock('../../../src/providers/passportDataProvider', () => ({
12+
loadSelectedDocument: jest.fn(),
13+
}));
14+
15+
jest.mock('../../../src/providers/authProvider', () => ({
16+
unsafe_getPrivateKey: jest.fn(),
17+
}));
18+
19+
jest.mock('../../../src/utils/analytics', () => () => ({ trackEvent: jest.fn() }));
20+
21+
const { loadSelectedDocument } = require('../../../src/providers/passportDataProvider');
22+
const { unsafe_getPrivateKey } = require('../../../src/providers/authProvider');
23+
const { actorMock } = require('./actorMock');
24+
25+
describe('provingMachine init', () => {
26+
beforeEach(() => {
27+
jest.clearAllMocks();
28+
useProvingStore.setState({});
29+
});
30+
31+
it('handles missing document', async () => {
32+
loadSelectedDocument.mockResolvedValue(null);
33+
await useProvingStore.getState().init('register');
34+
expect(actorMock.send).toHaveBeenCalledWith({ type: 'PASSPORT_DATA_NOT_FOUND' });
35+
emitState('passport_data_not_found');
36+
expect(useProvingStore.getState().currentState).toBe('passport_data_not_found');
37+
});
38+
39+
it('initializes state with document and secret', async () => {
40+
loadSelectedDocument.mockResolvedValue({ data: { documentCategory: 'passport', mock: false } });
41+
unsafe_getPrivateKey.mockResolvedValue('mysecret');
42+
await useProvingStore.getState().init('register');
43+
expect(useProvingStore.getState().passportData).toEqual({ documentCategory: 'passport', mock: false });
44+
expect(useProvingStore.getState().secret).toBe('mysecret');
45+
expect(useProvingStore.getState().env).toBe('prod');
46+
expect(useProvingStore.getState().circuitType).toBe('register');
47+
});
48+
});
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { jest } from '@jest/globals';
2+
import { useProvingStore } from '../../../src/utils/proving/provingMachine';
3+
4+
5+
jest.mock('xstate', () => {
6+
const actual = jest.requireActual('xstate');
7+
const { actorMock } = require('./actorMock');
8+
return { ...actual, createActor: jest.fn(() => actorMock) };
9+
});
10+
11+
jest.mock('../../../src/utils/analytics', () => () => ({ trackEvent: jest.fn() }));
12+
jest.mock('uuid', () => ({ v4: jest.fn(() => 'uuid') }));
13+
14+
jest.mock('../../../src/utils/proving/attest', () => ({
15+
getPublicKey: jest.fn(() => '04' + 'a'.repeat(128)),
16+
verifyAttestation: jest.fn(() => Promise.resolve(true)),
17+
}));
18+
jest.mock('../../../src/utils/proving/provingUtils', () => ({
19+
ec: { keyFromPublic: jest.fn(() => ({ getPublic: jest.fn() })) },
20+
clientKey: { derive: jest.fn(() => ({ toArray: () => Array(32).fill(1) })) },
21+
clientPublicKeyHex: '00',
22+
}));
23+
24+
jest.mock('../../../src/providers/passportDataProvider', () => ({
25+
loadSelectedDocument: jest.fn(() => Promise.resolve({ data: { documentCategory: 'passport', mock: false } })),
26+
}));
27+
28+
jest.mock('../../../src/providers/authProvider', () => ({
29+
unsafe_getPrivateKey: jest.fn(() => Promise.resolve('sec')),
30+
}));
31+
32+
const { actorMock } = require('./actorMock');
33+
34+
const { verifyAttestation } = require('../../../src/utils/proving/attest');
35+
36+
describe('websocket handlers', () => {
37+
beforeEach(async () => {
38+
jest.clearAllMocks();
39+
useProvingStore.setState({ wsConnection: { send: jest.fn(), addEventListener: jest.fn(), removeEventListener: jest.fn(), close: jest.fn() } as any });
40+
await useProvingStore.getState().init('register');
41+
useProvingStore.setState({ wsConnection: { send: jest.fn(), addEventListener: jest.fn(), removeEventListener: jest.fn(), close: jest.fn() } as any });
42+
});
43+
44+
it('_handleWsOpen sends hello', () => {
45+
useProvingStore.getState()._handleWsOpen();
46+
const ws = useProvingStore.getState().wsConnection as any;
47+
expect(ws.send).toHaveBeenCalled();
48+
const sent = JSON.parse(ws.send.mock.calls[0][0]);
49+
expect(sent.params.uuid).toBe('uuid');
50+
expect(useProvingStore.getState().uuid).toBe('uuid');
51+
});
52+
53+
it('_handleWebSocketMessage processes attestation', async () => {
54+
const message = new MessageEvent('message', { data: JSON.stringify({ result: { attestation: 'a' } }) });
55+
await useProvingStore.getState()._handleWebSocketMessage(message);
56+
expect(verifyAttestation).toHaveBeenCalled();
57+
expect(actorMock.send.mock.calls.some(c => c[0].type === 'CONNECT_SUCCESS')).toBe(true);
58+
});
59+
60+
it('_handleWebSocketMessage handles error', async () => {
61+
const message = new MessageEvent('message', { data: JSON.stringify({ error: 'oops' }) });
62+
await useProvingStore.getState()._handleWebSocketMessage(message);
63+
const lastCall = actorMock.send.mock.calls.pop();
64+
expect(lastCall[0]).toEqual({ type: 'PROVE_ERROR' });
65+
});
66+
67+
it('_handleWsClose triggers failure during proving', () => {
68+
useProvingStore.setState({ currentState: 'proving' });
69+
const event: any = { code: 1000, reason: '', type: 'close' };
70+
useProvingStore.getState()._handleWsClose(event);
71+
const last = actorMock.send.mock.calls.pop();
72+
expect(last[0]).toEqual({ type: 'PROVE_ERROR' });
73+
});
74+
});
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import forge from 'node-forge';
2+
import {
3+
encryptAES256GCM,
4+
getPayload,
5+
getWSDbRelayerUrl,
6+
} from '../../../src/utils/proving/provingUtils';
7+
8+
describe('provingUtils', () => {
9+
it('encryptAES256GCM encrypts and decrypts correctly', () => {
10+
const key = forge.random.getBytesSync(32);
11+
const plaintext = 'hello world';
12+
const encrypted = encryptAES256GCM(plaintext, forge.util.createBuffer(key));
13+
14+
const decipher = forge.cipher.createDecipher('AES-GCM', forge.util.createBuffer(key));
15+
decipher.start({
16+
iv: Buffer.from(encrypted.nonce).toString('binary'),
17+
tagLength: 128,
18+
tag: forge.util.createBuffer(Buffer.from(encrypted.auth_tag).toString('binary')),
19+
});
20+
decipher.update(
21+
forge.util.createBuffer(Buffer.from(encrypted.cipher_text).toString('binary')),
22+
);
23+
const success = decipher.finish();
24+
const decrypted = decipher.output.toString();
25+
26+
expect(success).toBe(true);
27+
expect(decrypted).toBe(plaintext);
28+
});
29+
30+
it('getPayload returns disclose payload', () => {
31+
const inputs = { foo: 'bar' };
32+
const payload = getPayload(
33+
inputs,
34+
'disclose',
35+
'vc_and_disclose',
36+
'https',
37+
'https://example.com',
38+
2,
39+
'0xabc',
40+
);
41+
expect(payload).toEqual({
42+
type: 'disclose',
43+
endpointType: 'https',
44+
endpoint: 'https://example.com',
45+
onchain: false,
46+
circuit: { name: 'vc_and_disclose', inputs: JSON.stringify(inputs) },
47+
version: 2,
48+
userDefinedData: '0xabc',
49+
});
50+
});
51+
52+
it('getPayload returns register payload', () => {
53+
const payload = getPayload(
54+
{ a: 1 },
55+
'register',
56+
'register_circuit',
57+
'celo',
58+
'https://self.xyz',
59+
);
60+
expect(payload).toEqual({
61+
type: 'register',
62+
onchain: true,
63+
endpointType: 'celo',
64+
circuit: { name: 'register_circuit', inputs: JSON.stringify({ a: 1 }) },
65+
});
66+
});
67+
68+
it('getWSDbRelayerUrl handles endpoint types', () => {
69+
expect(getWSDbRelayerUrl('celo')).toContain('websocket.self.xyz');
70+
expect(getWSDbRelayerUrl('https')).toContain('websocket.self.xyz');
71+
expect(getWSDbRelayerUrl('staging_celo')).toContain('websocket.staging.self.xyz');
72+
});
73+
});

0 commit comments

Comments
 (0)