Skip to content

Commit

Permalink
chore(sdk tests): add a few sdk tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jackpooleyml committed Dec 20, 2024
1 parent dda9ebe commit b9ec43e
Show file tree
Hide file tree
Showing 6 changed files with 471 additions and 78 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ jobs:
run: pnpm run deploy
working-directory: packages/contracts

- 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 { describe, expect, test, vi } from 'vitest'
import { type Address, type Hash, type TransactionReceipt } from 'viem'
import { writeContract, waitForTransactionReceipt } from 'viem/actions'
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 = '0xhash' as Hash
const mockTransactionReceipt: TransactionReceipt = {
status: 'success',
contractAddress: '0x4234567890123456789012345678901234567890',
blockNumber: 1n,
blockHash: '0xblockhash' 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,
})
)
})
})
46 changes: 46 additions & 0 deletions packages/sdk/src/utils/encoding.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { describe, expect, test } from 'vitest';
import { encodePasskeyModuleParameters, encodeModuleData, encodeSession, encodeSessionTx } 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);

console.log("XDBG - encoding.test.ts - encoded: ", encoded);

// 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'));
});
});

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());
});
});
});
87 changes: 87 additions & 0 deletions packages/sdk/src/utils/passkey.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { describe, expect, test } from "vitest";
import {
getPublicKeyBytesFromPasskeySignature,
getPasskeySignatureFromPublicKeyBytes,
} 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

0 comments on commit b9ec43e

Please sign in to comment.