Skip to content

Commit ba0fcdf

Browse files
authored
chore: add a few sdk tests (#44)
1 parent b9afdc3 commit ba0fcdf

File tree

6 files changed

+482
-123
lines changed

6 files changed

+482
-123
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ jobs:
6363
- name: Deploy Demo-App contracts
6464
run: pnpm nx deploy-contracts demo-app
6565

66+
- name: Run tests
67+
run: pnpm test
68+
working-directory: packages/sdk
69+
6670
# Run E2E tests
6771
- name: Install Playwright Chromium Browser
6872
run: pnpm exec playwright install chromium

packages/sdk/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
"build:types": "tsc --project ./tsconfig.build.json --module esnext --declarationDir ./dist/_types --emitDeclarationOnly --declaration --declarationMap",
2121
"clean": "rm -rf *.tsbuildinfo dist",
2222
"typecheck": "tsc --noEmit",
23-
"publish:local": "pnpm publish --no-git-checks --force"
23+
"publish:local": "pnpm publish --no-git-checks --force",
24+
"test": "vitest"
2425
},
2526
"peerDependencies": {
2627
"@simplewebauthn/browser": "10.x",
@@ -37,7 +38,8 @@
3738
"@types/ms": "^0.7.34",
3839
"@types/node": "^22.1.0",
3940
"eventemitter3": "^5.0.1",
40-
"viem": "2.21.14"
41+
"viem": "2.21.14",
42+
"vitest": "^2.1.8"
4143
},
4244
"files": [
4345
"*",
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import { type Address, type Hash, type TransactionReceipt } from "viem";
2+
import { waitForTransactionReceipt, writeContract } from "viem/actions";
3+
import { describe, expect, test, vi } from "vitest";
4+
5+
import { deployAccount } from "./account.js";
6+
7+
// Mock the passkey utils
8+
vi.mock("../../../utils/passkey.js", () => ({
9+
getPublicKeyBytesFromPasskeySignature: vi.fn().mockReturnValue([
10+
Buffer.from("0000000000000000000000000000000000000000000000000000000000000001", "hex"),
11+
Buffer.from("0000000000000000000000000000000000000000000000000000000000000002", "hex"),
12+
]),
13+
}));
14+
15+
// Mock viem actions
16+
vi.mock("viem/actions", () => ({
17+
writeContract: vi.fn(),
18+
waitForTransactionReceipt: vi.fn(),
19+
}));
20+
21+
// Add FactoryAbi mock at the top with other mocks
22+
vi.mock("../../../abi/Factory.js", () => ({
23+
FactoryAbi: [
24+
{
25+
inputs: [
26+
{ type: "bytes32", name: "_salt" },
27+
{ type: "string", name: "_uniqueAccountId" },
28+
{ type: "bytes[]", name: "_initialValidators" },
29+
{ type: "address[]", name: "_initialK1Owners" },
30+
],
31+
name: "deployProxySsoAccount",
32+
outputs: [{ type: "address", name: "accountAddress" }],
33+
stateMutability: "nonpayable",
34+
type: "function",
35+
},
36+
],
37+
}));
38+
39+
describe("deployAccount", () => {
40+
// Setup common test data
41+
const mockSalt = new Uint8Array([
42+
213, 36, 52, 69, 251, 82, 199, 45, 113, 6, 20, 213, 78, 47, 165,
43+
164, 106, 221, 105, 67, 247, 47, 200, 167, 137, 64, 151, 12, 179,
44+
74, 90, 23,
45+
]);
46+
47+
// CBOR-encoded COSE key with known x,y coordinates
48+
const mockCredentialPublicKey = new Uint8Array([
49+
0xa5, // map of 5 pairs
50+
0x01, // key 1 (kty)
51+
0x02, // value 2 (EC2)
52+
0x03, // key 3 (alg)
53+
0x26, // value -7 (ES256)
54+
0x20, // key -1 (crv)
55+
0x01, // value 1 (P-256)
56+
0x21, // key -2 (x coordinate)
57+
0x58, 0x20, // bytes(32)
58+
...new Uint8Array(32).fill(0x01), // x coordinate filled with 0x01
59+
0x22, // key -3 (y coordinate)
60+
0x58, 0x20, // bytes(32)
61+
...new Uint8Array(32).fill(0x02), // y coordinate filled with 0x02
62+
]);
63+
64+
const mockClient = {
65+
account: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
66+
chain: { id: 1 },
67+
} as any;
68+
const mockContracts = {
69+
accountFactory: "0x1234567890123456789012345678901234567890" as Address,
70+
passkey: "0x2234567890123456789012345678901234567890" as Address,
71+
session: "0x3234567890123456789012345678901234567890" as Address,
72+
};
73+
74+
const mockTransactionHash = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" as Hash;
75+
const mockTransactionReceipt: TransactionReceipt = {
76+
status: "success",
77+
contractAddress: "0x4234567890123456789012345678901234567890",
78+
blockNumber: 1n,
79+
blockHash: "0x5e1d3a76f1b1c3a2b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7" as Hash,
80+
transactionHash: mockTransactionHash,
81+
logs: [],
82+
logsBloom: "0x",
83+
cumulativeGasUsed: 0n,
84+
effectiveGasPrice: 0n,
85+
gasUsed: 0n,
86+
type: "eip1559",
87+
from: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
88+
to: "0x1234567890123456789012345678901234567890",
89+
transactionIndex: 0,
90+
};
91+
92+
test("deploys account successfully", async () => {
93+
// Setup mocks
94+
vi.mocked(writeContract).mockResolvedValue(mockTransactionHash);
95+
vi.mocked(waitForTransactionReceipt).mockResolvedValue(mockTransactionReceipt);
96+
97+
const result = await deployAccount(mockClient, {
98+
credentialPublicKey: mockCredentialPublicKey,
99+
contracts: mockContracts,
100+
expectedOrigin: "https://example.com",
101+
salt: mockSalt,
102+
});
103+
104+
// Verify the result
105+
expect(result).toEqual({
106+
address: "0x4234567890123456789012345678901234567890",
107+
transactionReceipt: mockTransactionReceipt,
108+
});
109+
110+
// Verify writeContract was called with correct parameters
111+
expect(writeContract).toHaveBeenCalledWith(
112+
mockClient,
113+
expect.objectContaining({
114+
address: mockContracts.accountFactory,
115+
functionName: "deployProxySsoAccount",
116+
}),
117+
);
118+
});
119+
120+
test("handles transaction failure", async () => {
121+
// Setup mock for failed transaction
122+
vi.mocked(writeContract).mockResolvedValue(mockTransactionHash);
123+
vi.mocked(waitForTransactionReceipt).mockResolvedValue({
124+
...mockTransactionReceipt,
125+
status: "reverted",
126+
});
127+
128+
await expect(
129+
deployAccount(mockClient, {
130+
credentialPublicKey: mockCredentialPublicKey,
131+
contracts: mockContracts,
132+
expectedOrigin: "https://example.com",
133+
salt: mockSalt,
134+
}),
135+
).rejects.toThrow("Account deployment transaction reverted");
136+
});
137+
138+
test("handles missing contract address in receipt", async () => {
139+
// Setup mock for missing contract address
140+
vi.mocked(writeContract).mockResolvedValue(mockTransactionHash);
141+
vi.mocked(waitForTransactionReceipt).mockResolvedValue({
142+
...mockTransactionReceipt,
143+
contractAddress: null,
144+
});
145+
146+
await expect(
147+
deployAccount(mockClient, {
148+
credentialPublicKey: mockCredentialPublicKey,
149+
contracts: mockContracts,
150+
expectedOrigin: "https://example.com",
151+
salt: mockSalt,
152+
}),
153+
).rejects.toThrow("No contract address in transaction receipt");
154+
});
155+
156+
test("calls onTransactionSent callback when provided", async () => {
157+
const onTransactionSent = vi.fn();
158+
vi.mocked(writeContract).mockResolvedValue(mockTransactionHash);
159+
vi.mocked(waitForTransactionReceipt).mockResolvedValue(mockTransactionReceipt);
160+
161+
await deployAccount(mockClient, {
162+
credentialPublicKey: mockCredentialPublicKey,
163+
contracts: mockContracts,
164+
expectedOrigin: "https://example.com",
165+
salt: mockSalt,
166+
onTransactionSent,
167+
});
168+
169+
expect(onTransactionSent).toHaveBeenCalledWith(mockTransactionHash);
170+
});
171+
172+
test("uses window.location.origin when expectedOrigin is not provided", async () => {
173+
// Mock window.location
174+
const originalWindow = global.window;
175+
global.window = {
176+
...originalWindow,
177+
location: {
178+
...originalWindow?.location,
179+
origin: "https://example.com",
180+
},
181+
} as any;
182+
183+
vi.mocked(writeContract).mockResolvedValue(mockTransactionHash);
184+
vi.mocked(waitForTransactionReceipt).mockResolvedValue(mockTransactionReceipt);
185+
186+
const writeContractSpy = vi.mocked(writeContract);
187+
await deployAccount(mockClient, {
188+
credentialPublicKey: mockCredentialPublicKey,
189+
contracts: mockContracts,
190+
salt: mockSalt,
191+
});
192+
193+
// Simpler assertion that just checks the key parts
194+
const lastCall = writeContractSpy.mock.lastCall;
195+
expect(lastCall?.[0]).toBe(mockClient);
196+
expect(lastCall?.[1]).toMatchObject({
197+
address: mockContracts.accountFactory,
198+
functionName: "deployProxySsoAccount",
199+
});
200+
201+
// Restore window
202+
global.window = originalWindow;
203+
});
204+
205+
test("handles paymaster configuration", async () => {
206+
vi.mocked(writeContract).mockResolvedValue(mockTransactionHash);
207+
vi.mocked(waitForTransactionReceipt).mockResolvedValue(mockTransactionReceipt);
208+
209+
const paymasterAddress = "0x5234567890123456789012345678901234567890" as Address;
210+
const paymasterInput = "0x1234" as const;
211+
212+
await deployAccount(mockClient, {
213+
credentialPublicKey: mockCredentialPublicKey,
214+
contracts: mockContracts,
215+
expectedOrigin: "https://example.com",
216+
paymasterAddress,
217+
paymasterInput,
218+
});
219+
220+
expect(writeContract).toHaveBeenCalledWith(
221+
mockClient,
222+
expect.objectContaining({
223+
paymaster: paymasterAddress,
224+
paymasterInput,
225+
}),
226+
);
227+
});
228+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { describe, expect, test } from "vitest";
2+
3+
import { encodeModuleData, encodePasskeyModuleParameters } from "./encoding";
4+
5+
describe("encoding utils", () => {
6+
describe("encodePasskeyModuleParameters", () => {
7+
test("correctly encodes passkey parameters", () => {
8+
const passkey = {
9+
passkeyPublicKey: [
10+
Buffer.from("1234567890123456789012345678901234567890123456789012345678901234", "hex"),
11+
Buffer.from("abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd", "hex"),
12+
],
13+
expectedOrigin: "https://example.com",
14+
};
15+
16+
const encoded = encodePasskeyModuleParameters(passkey);
17+
18+
// The encoding should be a hex string
19+
expect(encoded).toMatch(/^0x[0-9a-f]+$/i);
20+
21+
// Should contain both public key components and the origin
22+
expect(encoded).toContain("1234567890123456789012345678901234567890123456789012345678901234");
23+
expect(encoded).toContain("abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd");
24+
expect(encoded).toContain(Buffer.from("https://example.com").toString("hex"));
25+
expect(encoded).toEqual("0x1234567890123456789012345678901234567890123456789012345678901234abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000001368747470733a2f2f6578616d706c652e636f6d00000000000000000000000000");
26+
});
27+
});
28+
29+
describe("encodeModuleData", () => {
30+
test("correctly encodes module data", () => {
31+
const moduleData = {
32+
address: "0x1234567890123456789012345678901234567890" as const,
33+
parameters: "0xabcdef" as const,
34+
};
35+
36+
const encoded = encodeModuleData(moduleData);
37+
38+
// The encoding should be a hex string
39+
expect(encoded).toMatch(/^0x[0-9a-f]+$/i);
40+
41+
// Should contain both the address and parameters
42+
expect(encoded.toLowerCase()).toContain(moduleData.address.slice(2).toLowerCase());
43+
expect(encoded.toLowerCase()).toContain(moduleData.parameters.slice(2).toLowerCase());
44+
expect(encoded).toEqual("0x000000000000000000000000123456789012345678901234567890123456789000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000003abcdef0000000000000000000000000000000000000000000000000000000000");
45+
});
46+
});
47+
});
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { describe, expect, test } from "vitest";
2+
3+
import {
4+
getPasskeySignatureFromPublicKeyBytes,
5+
getPublicKeyBytesFromPasskeySignature,
6+
} from "./passkey";
7+
8+
describe("passkey utils", () => {
9+
describe("getPublicKeyBytesFromPasskeySignature", () => {
10+
test("correctly decodes CBOR-encoded COSE key", () => {
11+
// This is a sample CBOR-encoded COSE key with known x,y coordinates
12+
// Format: map with 5 entries:
13+
// 1: 2 (kty: EC2)
14+
// 3: -7 (alg: ES256)
15+
// -1: 1 (crv: P-256)
16+
// -2: x coordinate (32 bytes)
17+
// -3: y coordinate (32 bytes)
18+
const samplePublicKey = new Uint8Array([
19+
0xa5, // map of 5 pairs
20+
0x01, // key 1 (kty)
21+
0x02, // value 2 (EC2)
22+
0x03, // key 3 (alg)
23+
0x26, // value -7 (ES256)
24+
0x20, // key -1 (crv)
25+
0x01, // value 1 (P-256)
26+
0x21, // key -2 (x coordinate)
27+
0x58,
28+
0x20, // bytes(32)
29+
...new Uint8Array(32).fill(0x01), // x coordinate filled with 0x01
30+
0x22, // key -3 (y coordinate)
31+
0x58,
32+
0x20, // bytes(32)
33+
...new Uint8Array(32).fill(0x02), // y coordinate filled with 0x02
34+
]);
35+
36+
const [x, y] = getPublicKeyBytesFromPasskeySignature(samplePublicKey);
37+
38+
// Check that x coordinate is all 0x01
39+
expect(Buffer.from(x).every((byte) => byte === 0x01)).toBe(true);
40+
// Check that y coordinate is all 0x02
41+
expect(Buffer.from(y).every((byte) => byte === 0x02)).toBe(true);
42+
// Check lengths
43+
expect(x.length).toBe(32);
44+
expect(y.length).toBe(32);
45+
});
46+
47+
test("roundtrip conversion works", () => {
48+
// Create sample x,y coordinates as hex strings
49+
const xHex = "0x" + "01".repeat(32);
50+
const yHex = "0x" + "02".repeat(32);
51+
52+
// Convert to COSE format
53+
const coseKey = getPasskeySignatureFromPublicKeyBytes([xHex, yHex]);
54+
55+
// Convert back to coordinates
56+
const [x, y] = getPublicKeyBytesFromPasskeySignature(coseKey);
57+
58+
// Check that we got back our original values
59+
expect(Buffer.from(x).toString("hex")).toBe(xHex.slice(2));
60+
expect(Buffer.from(y).toString("hex")).toBe(yHex.slice(2));
61+
});
62+
63+
test("throws on invalid CBOR data", () => {
64+
const invalidCBOR = new Uint8Array([0xff, 0xff, 0xff]); // Invalid CBOR bytes
65+
66+
expect(() => {
67+
getPublicKeyBytesFromPasskeySignature(invalidCBOR);
68+
}).toThrow();
69+
});
70+
71+
test("throws if x or y coordinates are missing", () => {
72+
// CBOR map with only kty, alg, and crv (missing x,y)
73+
const incompleteCOSE = new Uint8Array([
74+
0xa3, // map of 3 pairs
75+
0x01, // key 1 (kty)
76+
0x02, // value 2 (EC2)
77+
0x03, // key 3 (alg)
78+
0x26, // value -7 (ES256)
79+
0x20, // key -1 (crv)
80+
0x01, // value 1 (P-256)
81+
]);
82+
83+
expect(() => {
84+
getPublicKeyBytesFromPasskeySignature(incompleteCOSE);
85+
}).toThrow();
86+
});
87+
});
88+
});

0 commit comments

Comments
 (0)