Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(sdk tests): add a few sdk tests #44

Merged
merged 1 commit into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ jobs:
- name: Deploy Demo-App contracts
run: pnpm nx deploy-contracts demo-app

- name: Run tests
run: pnpm test
working-directory: packages/sdk

# Run E2E tests
- name: Install Playwright Chromium Browser
run: pnpm exec playwright install chromium
Expand Down
6 changes: 4 additions & 2 deletions packages/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"build:types": "tsc --project ./tsconfig.build.json --module esnext --declarationDir ./dist/_types --emitDeclarationOnly --declaration --declarationMap",
"clean": "rm -rf *.tsbuildinfo dist",
"typecheck": "tsc --noEmit",
"publish:local": "pnpm publish --no-git-checks --force"
"publish:local": "pnpm publish --no-git-checks --force",
"test": "vitest"
},
"peerDependencies": {
"@simplewebauthn/browser": "10.x",
Expand All @@ -37,7 +38,8 @@
"@types/ms": "^0.7.34",
"@types/node": "^22.1.0",
"eventemitter3": "^5.0.1",
"viem": "2.21.14"
"viem": "2.21.14",
"vitest": "^2.1.8"
},
"files": [
"*",
Expand Down
228 changes: 228 additions & 0 deletions packages/sdk/src/client/passkey/actions/account.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import { type Address, type Hash, type TransactionReceipt } from "viem";
import { waitForTransactionReceipt, writeContract } from "viem/actions";
import { describe, expect, test, vi } from "vitest";

import { deployAccount } from "./account.js";

// Mock the passkey utils
vi.mock("../../../utils/passkey.js", () => ({
getPublicKeyBytesFromPasskeySignature: vi.fn().mockReturnValue([
Buffer.from("0000000000000000000000000000000000000000000000000000000000000001", "hex"),
Buffer.from("0000000000000000000000000000000000000000000000000000000000000002", "hex"),
]),
}));

// Mock viem actions
vi.mock("viem/actions", () => ({
writeContract: vi.fn(),
waitForTransactionReceipt: vi.fn(),
}));

// Add FactoryAbi mock at the top with other mocks
vi.mock("../../../abi/Factory.js", () => ({
FactoryAbi: [
{
inputs: [
{ type: "bytes32", name: "_salt" },
{ type: "string", name: "_uniqueAccountId" },
{ type: "bytes[]", name: "_initialValidators" },
{ type: "address[]", name: "_initialK1Owners" },
],
name: "deployProxySsoAccount",
outputs: [{ type: "address", name: "accountAddress" }],
stateMutability: "nonpayable",
type: "function",
},
],
}));

describe("deployAccount", () => {
// Setup common test data
const mockSalt = new Uint8Array([
213, 36, 52, 69, 251, 82, 199, 45, 113, 6, 20, 213, 78, 47, 165,
164, 106, 221, 105, 67, 247, 47, 200, 167, 137, 64, 151, 12, 179,
74, 90, 23,
]);

// CBOR-encoded COSE key with known x,y coordinates
const mockCredentialPublicKey = new Uint8Array([
0xa5, // map of 5 pairs
0x01, // key 1 (kty)
0x02, // value 2 (EC2)
0x03, // key 3 (alg)
0x26, // value -7 (ES256)
0x20, // key -1 (crv)
0x01, // value 1 (P-256)
0x21, // key -2 (x coordinate)
0x58, 0x20, // bytes(32)
...new Uint8Array(32).fill(0x01), // x coordinate filled with 0x01
0x22, // key -3 (y coordinate)
0x58, 0x20, // bytes(32)
...new Uint8Array(32).fill(0x02), // y coordinate filled with 0x02
]);

const mockClient = {
account: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
chain: { id: 1 },
} as any;
const mockContracts = {
accountFactory: "0x1234567890123456789012345678901234567890" as Address,
passkey: "0x2234567890123456789012345678901234567890" as Address,
session: "0x3234567890123456789012345678901234567890" as Address,
};

const mockTransactionHash = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" as Hash;
const mockTransactionReceipt: TransactionReceipt = {
status: "success",
contractAddress: "0x4234567890123456789012345678901234567890",
blockNumber: 1n,
blockHash: "0x5e1d3a76f1b1c3a2b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7" as Hash,
transactionHash: mockTransactionHash,
logs: [],
logsBloom: "0x",
cumulativeGasUsed: 0n,
effectiveGasPrice: 0n,
gasUsed: 0n,
type: "eip1559",
from: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
to: "0x1234567890123456789012345678901234567890",
transactionIndex: 0,
};

test("deploys account successfully", async () => {
// Setup mocks
vi.mocked(writeContract).mockResolvedValue(mockTransactionHash);
vi.mocked(waitForTransactionReceipt).mockResolvedValue(mockTransactionReceipt);

const result = await deployAccount(mockClient, {
credentialPublicKey: mockCredentialPublicKey,
contracts: mockContracts,
expectedOrigin: "https://example.com",
salt: mockSalt,
});

// Verify the result
expect(result).toEqual({
address: "0x4234567890123456789012345678901234567890",
transactionReceipt: mockTransactionReceipt,
});

// Verify writeContract was called with correct parameters
expect(writeContract).toHaveBeenCalledWith(
mockClient,
expect.objectContaining({
address: mockContracts.accountFactory,
functionName: "deployProxySsoAccount",
}),
);
});

test("handles transaction failure", async () => {
// Setup mock for failed transaction
vi.mocked(writeContract).mockResolvedValue(mockTransactionHash);
vi.mocked(waitForTransactionReceipt).mockResolvedValue({
...mockTransactionReceipt,
status: "reverted",
});

await expect(
deployAccount(mockClient, {
credentialPublicKey: mockCredentialPublicKey,
contracts: mockContracts,
expectedOrigin: "https://example.com",
salt: mockSalt,
}),
).rejects.toThrow("Account deployment transaction reverted");
});

test("handles missing contract address in receipt", async () => {
// Setup mock for missing contract address
vi.mocked(writeContract).mockResolvedValue(mockTransactionHash);
vi.mocked(waitForTransactionReceipt).mockResolvedValue({
...mockTransactionReceipt,
contractAddress: null,
});

await expect(
deployAccount(mockClient, {
credentialPublicKey: mockCredentialPublicKey,
contracts: mockContracts,
expectedOrigin: "https://example.com",
salt: mockSalt,
}),
).rejects.toThrow("No contract address in transaction receipt");
});

test("calls onTransactionSent callback when provided", async () => {
const onTransactionSent = vi.fn();
vi.mocked(writeContract).mockResolvedValue(mockTransactionHash);
vi.mocked(waitForTransactionReceipt).mockResolvedValue(mockTransactionReceipt);

await deployAccount(mockClient, {
credentialPublicKey: mockCredentialPublicKey,
contracts: mockContracts,
expectedOrigin: "https://example.com",
salt: mockSalt,
onTransactionSent,
});

expect(onTransactionSent).toHaveBeenCalledWith(mockTransactionHash);
});

test("uses window.location.origin when expectedOrigin is not provided", async () => {
// Mock window.location
const originalWindow = global.window;
global.window = {
...originalWindow,
location: {
...originalWindow?.location,
origin: "https://example.com",
},
} as any;

vi.mocked(writeContract).mockResolvedValue(mockTransactionHash);
vi.mocked(waitForTransactionReceipt).mockResolvedValue(mockTransactionReceipt);

const writeContractSpy = vi.mocked(writeContract);
await deployAccount(mockClient, {
credentialPublicKey: mockCredentialPublicKey,
contracts: mockContracts,
salt: mockSalt,
});

// Simpler assertion that just checks the key parts
const lastCall = writeContractSpy.mock.lastCall;
expect(lastCall?.[0]).toBe(mockClient);
expect(lastCall?.[1]).toMatchObject({
address: mockContracts.accountFactory,
functionName: "deployProxySsoAccount",
});

// Restore window
global.window = originalWindow;
});

test("handles paymaster configuration", async () => {
vi.mocked(writeContract).mockResolvedValue(mockTransactionHash);
vi.mocked(waitForTransactionReceipt).mockResolvedValue(mockTransactionReceipt);

const paymasterAddress = "0x5234567890123456789012345678901234567890" as Address;
const paymasterInput = "0x1234" as const;

await deployAccount(mockClient, {
credentialPublicKey: mockCredentialPublicKey,
contracts: mockContracts,
expectedOrigin: "https://example.com",
paymasterAddress,
paymasterInput,
});

expect(writeContract).toHaveBeenCalledWith(
mockClient,
expect.objectContaining({
paymaster: paymasterAddress,
paymasterInput,
}),
);
});
});
47 changes: 47 additions & 0 deletions packages/sdk/src/utils/encoding.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, expect, test } from "vitest";

import { encodeModuleData, encodePasskeyModuleParameters } from "./encoding";

describe("encoding utils", () => {
describe("encodePasskeyModuleParameters", () => {
test("correctly encodes passkey parameters", () => {
const passkey = {
passkeyPublicKey: [
Buffer.from("1234567890123456789012345678901234567890123456789012345678901234", "hex"),
Buffer.from("abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd", "hex"),
],
expectedOrigin: "https://example.com",
};

const encoded = encodePasskeyModuleParameters(passkey);

// The encoding should be a hex string
expect(encoded).toMatch(/^0x[0-9a-f]+$/i);

// Should contain both public key components and the origin
expect(encoded).toContain("1234567890123456789012345678901234567890123456789012345678901234");
expect(encoded).toContain("abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd");
expect(encoded).toContain(Buffer.from("https://example.com").toString("hex"));
expect(encoded).toEqual("0x1234567890123456789012345678901234567890123456789012345678901234abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000001368747470733a2f2f6578616d706c652e636f6d00000000000000000000000000");
});
});

describe("encodeModuleData", () => {
test("correctly encodes module data", () => {
const moduleData = {
address: "0x1234567890123456789012345678901234567890" as const,
parameters: "0xabcdef" as const,
};

const encoded = encodeModuleData(moduleData);

// The encoding should be a hex string
expect(encoded).toMatch(/^0x[0-9a-f]+$/i);

// Should contain both the address and parameters
expect(encoded.toLowerCase()).toContain(moduleData.address.slice(2).toLowerCase());
expect(encoded.toLowerCase()).toContain(moduleData.parameters.slice(2).toLowerCase());
expect(encoded).toEqual("0x000000000000000000000000123456789012345678901234567890123456789000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000003abcdef0000000000000000000000000000000000000000000000000000000000");
});
});
});
88 changes: 88 additions & 0 deletions packages/sdk/src/utils/passkey.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { describe, expect, test } from "vitest";

import {
getPasskeySignatureFromPublicKeyBytes,
getPublicKeyBytesFromPasskeySignature,
} from "./passkey";

describe("passkey utils", () => {
describe("getPublicKeyBytesFromPasskeySignature", () => {
test("correctly decodes CBOR-encoded COSE key", () => {
// This is a sample CBOR-encoded COSE key with known x,y coordinates
// Format: map with 5 entries:
// 1: 2 (kty: EC2)
// 3: -7 (alg: ES256)
// -1: 1 (crv: P-256)
// -2: x coordinate (32 bytes)
// -3: y coordinate (32 bytes)
const samplePublicKey = new Uint8Array([
0xa5, // map of 5 pairs
0x01, // key 1 (kty)
0x02, // value 2 (EC2)
0x03, // key 3 (alg)
0x26, // value -7 (ES256)
0x20, // key -1 (crv)
0x01, // value 1 (P-256)
0x21, // key -2 (x coordinate)
0x58,
0x20, // bytes(32)
...new Uint8Array(32).fill(0x01), // x coordinate filled with 0x01
0x22, // key -3 (y coordinate)
0x58,
0x20, // bytes(32)
...new Uint8Array(32).fill(0x02), // y coordinate filled with 0x02
]);

const [x, y] = getPublicKeyBytesFromPasskeySignature(samplePublicKey);

// Check that x coordinate is all 0x01
expect(Buffer.from(x).every((byte) => byte === 0x01)).toBe(true);
// Check that y coordinate is all 0x02
expect(Buffer.from(y).every((byte) => byte === 0x02)).toBe(true);
// Check lengths
expect(x.length).toBe(32);
expect(y.length).toBe(32);
});

test("roundtrip conversion works", () => {
// Create sample x,y coordinates as hex strings
const xHex = "0x" + "01".repeat(32);
const yHex = "0x" + "02".repeat(32);

// Convert to COSE format
const coseKey = getPasskeySignatureFromPublicKeyBytes([xHex, yHex]);

// Convert back to coordinates
const [x, y] = getPublicKeyBytesFromPasskeySignature(coseKey);

// Check that we got back our original values
expect(Buffer.from(x).toString("hex")).toBe(xHex.slice(2));
expect(Buffer.from(y).toString("hex")).toBe(yHex.slice(2));
});

test("throws on invalid CBOR data", () => {
const invalidCBOR = new Uint8Array([0xff, 0xff, 0xff]); // Invalid CBOR bytes

expect(() => {
getPublicKeyBytesFromPasskeySignature(invalidCBOR);
}).toThrow();
});

test("throws if x or y coordinates are missing", () => {
// CBOR map with only kty, alg, and crv (missing x,y)
const incompleteCOSE = new Uint8Array([
0xa3, // map of 3 pairs
0x01, // key 1 (kty)
0x02, // value 2 (EC2)
0x03, // key 3 (alg)
0x26, // value -7 (ES256)
0x20, // key -1 (crv)
0x01, // value 1 (P-256)
]);

expect(() => {
getPublicKeyBytesFromPasskeySignature(incompleteCOSE);
}).toThrow();
});
});
});
Loading
Loading