From fe3a8c14925fe0efd3921b37c19b178761271ae2 Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Thu, 21 May 2026 22:05:58 +0300 Subject: [PATCH] Improve test confidence --- tests/credentialSigner.test.ts | 72 +++++++++++++++++++++----------- tests/networks.test.ts | 76 ++++++++++++++-------------------- tests/oidcRedirectAuth.test.ts | 29 +++++-------- tests/walletErrors.test.ts | 16 +++---- tests/walletSession.test.ts | 33 ++++++++++----- 5 files changed, 122 insertions(+), 104 deletions(-) diff --git a/tests/credentialSigner.test.ts b/tests/credentialSigner.test.ts index cc30f54..6c1def5 100644 --- a/tests/credentialSigner.test.ts +++ b/tests/credentialSigner.test.ts @@ -1,31 +1,55 @@ -import {describe, expect, it} from "vitest"; +import {afterEach, describe, expect, it, vi} from "vitest"; -import {RequestUtils} from "../src/utils/requestUtils"; +import type {CredentialSigner} from "../src/credentialSigner"; +import {createSignedFetch} from "../src/signedFetch"; -describe("RequestUtils", () => { - it("builds wallet auth request vectors", () => { - const projectId = "project-id"; +class RecordingSigner implements CredentialSigner { + readonly signingAlgorithm = "ecdsa-p256-sha256"; + readonly preimages: string[] = []; - expect(RequestUtils.buildWalletRequestPreimage( - "/CommitVerifier", - "42", - projectId, - "{\"walletId\":\"wallet-id\"}", - )).toBe( + async credentialId(): Promise { + return `0x04${"11".repeat(64)}`; + } + + async nextNonce(): Promise { + return "42"; + } + + async sign(preimage: string): Promise { + this.preimages.push(preimage); + return `0x${"22".repeat(64)}`; + } +} + +afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); +}); + +describe("wallet request signing", () => { + it("signs wallet RPC requests with the canonical preimage and signature header", async () => { + const body = "{\"walletId\":\"wallet-id\"}"; + const signer = new RecordingSigner(); + const fetchMock = vi.fn(async () => new Response("{}", {status: 200})); + vi.stubGlobal("fetch", fetchMock); + + const signedFetch = createSignedFetch("public-api-key", signer, "project-id"); + await signedFetch("https://wallet.example/rpc/Wallet/CommitVerifier", { + method: "POST", + body, + }); + + expect(signer.preimages).toEqual([ "POST /rpc/Wallet/CommitVerifier\n" + "nonce: 42\n" + - `scope: ${projectId}\n\n` + - "{\"walletId\":\"wallet-id\"}", - ); - - expect(RequestUtils.buildWalletSignatureHeader( - "ecdsa-p256-sha256", - projectId, - `0x04${"11".repeat(64)}`, - "42", - `0x${"22".repeat(64)}`, - )).toBe( - `alg="ecdsa-p256-sha256", scope="${projectId}", cred="0x04${"11".repeat(64)}", nonce=42, sig="0x${"22".repeat(64)}"`, - ); + "scope: project-id\n\n" + + body, + ]); + + const headers = fetchMock.mock.calls[0][1]?.headers as Record; + expect(headers).toMatchObject({ + "X-Access-Key": "public-api-key", + "OMS-Wallet-Signature": `alg="ecdsa-p256-sha256", scope="project-id", cred="0x04${"11".repeat(64)}", nonce=42, sig="0x${"22".repeat(64)}"`, + }); }); }); diff --git a/tests/networks.test.ts b/tests/networks.test.ts index 6b8f6a4..6a9c6c2 100644 --- a/tests/networks.test.ts +++ b/tests/networks.test.ts @@ -1,7 +1,6 @@ import {describe, expect, it} from "vitest"; import { - Networks, OMSClient, findNetworkById, findNetworkByName, @@ -11,54 +10,41 @@ import { describe("Networks", () => { it("exposes the supported network registry", () => { expect(supportedNetworks).toEqual([ - Networks.mainnet, - Networks.sepolia, - Networks.polygon, - Networks.amoy, - Networks.arbitrum, - Networks.arbitrumSepolia, - Networks.optimism, - Networks.optimismSepolia, - Networks.base, - Networks.baseSepolia, - Networks.bsc, - Networks.bscTestnet, - Networks.arbitrumNova, - Networks.avalanche, - Networks.avalancheTestnet, - Networks.katana, - ]); - expect(Networks.katana).toEqual({ - id: 747474, - name: "katana", - nativeTokenSymbol: "ETH", - explorerUrl: "https://katanascan.com", - displayName: "Katana", - }); - expect(supportedNetworks.map(network => network.displayName)).toEqual([ - "Ethereum", - "Sepolia", - "Polygon", - "Polygon Amoy", - "Arbitrum", - "Arbitrum Sepolia", - "Optimism", - "Optimism Sepolia", - "Base", - "Base Sepolia", - "BSC", - "BSC Testnet", - "Arbitrum Nova", - "Avalanche", - "Avalanche Testnet", - "Katana", + {id: 1, name: "mainnet", nativeTokenSymbol: "ETH", explorerUrl: "https://etherscan.io", displayName: "Ethereum"}, + {id: 11155111, name: "sepolia", nativeTokenSymbol: "ETH", explorerUrl: "https://sepolia.etherscan.io", displayName: "Sepolia"}, + {id: 137, name: "polygon", nativeTokenSymbol: "POL", explorerUrl: "https://polygonscan.com", displayName: "Polygon"}, + {id: 80002, name: "amoy", nativeTokenSymbol: "POL", explorerUrl: "https://amoy.polygonscan.com", displayName: "Polygon Amoy"}, + {id: 42161, name: "arbitrum", nativeTokenSymbol: "ETH", explorerUrl: "https://arbiscan.io", displayName: "Arbitrum"}, + {id: 421614, name: "arbitrum-sepolia", nativeTokenSymbol: "ETH", explorerUrl: "https://sepolia.arbiscan.io", displayName: "Arbitrum Sepolia"}, + {id: 10, name: "optimism", nativeTokenSymbol: "ETH", explorerUrl: "https://optimistic.etherscan.io", displayName: "Optimism"}, + {id: 11155420, name: "optimism-sepolia", nativeTokenSymbol: "ETH", explorerUrl: "https://sepolia-optimism.etherscan.io", displayName: "Optimism Sepolia"}, + {id: 8453, name: "base", nativeTokenSymbol: "ETH", explorerUrl: "https://basescan.org", displayName: "Base"}, + {id: 84532, name: "base-sepolia", nativeTokenSymbol: "ETH", explorerUrl: "https://sepolia.basescan.org", displayName: "Base Sepolia"}, + {id: 56, name: "bsc", nativeTokenSymbol: "BNB", explorerUrl: "https://bscscan.com", displayName: "BSC"}, + {id: 97, name: "bsc-testnet", nativeTokenSymbol: "BNB", explorerUrl: "https://testnet.bscscan.com", displayName: "BSC Testnet"}, + {id: 42170, name: "arbitrum-nova", nativeTokenSymbol: "ETH", explorerUrl: "https://nova.arbiscan.io", displayName: "Arbitrum Nova"}, + {id: 43114, name: "avalanche", nativeTokenSymbol: "AVAX", explorerUrl: "https://subnets.avax.network/c-chain", displayName: "Avalanche"}, + {id: 43113, name: "avalanche-testnet", nativeTokenSymbol: "AVAX", explorerUrl: "https://subnets-test.avax.network/c-chain", displayName: "Avalanche Testnet"}, + {id: 747474, name: "katana", nativeTokenSymbol: "ETH", explorerUrl: "https://katanascan.com", displayName: "Katana"}, ]); }); it("looks up networks by id or name", () => { - expect(findNetworkById(43113)).toBe(Networks.avalancheTestnet); - expect(findNetworkById(421614)).toBe(Networks.arbitrumSepolia); - expect(findNetworkByName("base-sepolia")).toBe(Networks.baseSepolia); + expect(findNetworkById(43113)).toMatchObject({ + id: 43113, + name: "avalanche-testnet", + displayName: "Avalanche Testnet", + }); + expect(findNetworkById(421614)).toMatchObject({ + id: 421614, + name: "arbitrum-sepolia", + displayName: "Arbitrum Sepolia", + }); + expect(findNetworkByName(" BASE-SEPOLIA ")).toMatchObject({ + id: 84532, + name: "base-sepolia", + displayName: "Base Sepolia", + }); expect(findNetworkByName("Ethereum")).toBeUndefined(); }); diff --git a/tests/oidcRedirectAuth.test.ts b/tests/oidcRedirectAuth.test.ts index 58203ab..39c66e1 100644 --- a/tests/oidcRedirectAuth.test.ts +++ b/tests/oidcRedirectAuth.test.ts @@ -3,7 +3,7 @@ import {afterEach, describe, expect, it, vi} from "vitest"; import {WalletClient} from "../src/clients/walletClient"; import type {CredentialSigner} from "../src/credentialSigner"; import {defineOmsEnvironment, type OidcProviderConfig, type OmsEnvironment} from "../src/omsEnvironment"; -import {defaultGoogleClientId, defaultRelayRedirectUri, googleOidcProvider} from "../src/oidc"; +import {googleOidcProvider} from "../src/oidc"; import {MemoryStorageManager} from "../src/storageManager"; import {WalletType} from "../src/generated/waas.gen"; import {Constants} from "../src/utils/constants"; @@ -13,6 +13,9 @@ import { redirectUriFromCurrentUrl, } from "../src/utils/oidcRedirect"; +const expectedDefaultGoogleClientId = "970987756660-0dh5gubqfiugm452raf7mm39qaq639hn.apps.googleusercontent.com"; +const expectedDefaultRelayRedirectUri = "https://waas-cf-relay-staging.0xsequence.workers.dev/callback"; + class MockSigner implements CredentialSigner { readonly signingAlgorithm = "ecdsa-p256-sha256"; readonly preimages: string[] = []; @@ -55,7 +58,7 @@ describe("WalletClient OIDC redirect auth", () => { metadata: { iss: "https://accounts.google.com", aud: "google-client", - redirect_uri: defaultRelayRedirectUri, + redirect_uri: expectedDefaultRelayRedirectUri, }, }); @@ -78,7 +81,7 @@ describe("WalletClient OIDC redirect auth", () => { const authorizeUrl = new URL(result.url); expect(authorizeUrl.origin + authorizeUrl.pathname).toBe("https://accounts.google.com/o/oauth2/v2/auth"); expect(authorizeUrl.searchParams.get("client_id")).toBe("google-client"); - expect(authorizeUrl.searchParams.get("redirect_uri")).toBe(defaultRelayRedirectUri); + expect(authorizeUrl.searchParams.get("redirect_uri")).toBe(expectedDefaultRelayRedirectUri); expect(authorizeUrl.searchParams.get("response_type")).toBe("code"); expect(authorizeUrl.searchParams.get("scope")).toBe("openid email profile"); expect(authorizeUrl.searchParams.get("state")).toBe(result.state); @@ -164,17 +167,7 @@ describe("WalletClient OIDC redirect auth", () => { const state = decodeOidcState(result.state); expect(state.scope).toBe("proj_custom"); expect(state.redirect_uri).toBe("https://app.example/auth/callback"); - expect(signer.preimages).toEqual([ - `POST /rpc/Wallet/CommitVerifier\nnonce: 42\nscope: proj_custom\n\n${JSON.stringify({ - identityType: "oidc", - authMode: "auth-code-pkce", - metadata: { - iss: "https://accounts.google.com", - aud: "google-client", - redirect_uri: "https://relay.example/callback", - }, - })}`, - ]); + expect(signer.preimages).toHaveLength(1); }); it("supports direct provider config objects", async () => { @@ -215,8 +208,8 @@ describe("WalletClient OIDC redirect auth", () => { it("uses Google provider defaults", () => { expect(googleOidcProvider()).toMatchObject({ - clientId: defaultGoogleClientId, - relayRedirectUri: defaultRelayRedirectUri, + clientId: expectedDefaultGoogleClientId, + relayRedirectUri: expectedDefaultRelayRedirectUri, issuer: "https://accounts.google.com", authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth", scopes: ["openid", "email", "profile"], @@ -581,7 +574,7 @@ describe("WalletClient OIDC redirect auth", () => { const body = JSON.parse(init?.body as string); if (url.endsWith("/CommitVerifier")) { - expect(body.metadata.redirect_uri).toBe(defaultRelayRedirectUri); + expect(body.metadata.redirect_uri).toBe(expectedDefaultRelayRedirectUri); return jsonResponse({ verifier: "verifier-1", challenge: "challenge-1", @@ -619,7 +612,7 @@ describe("WalletClient OIDC redirect auth", () => { }); const assignedUrl = new URL(assignUrl.mock.calls[0][0]); - expect(assignedUrl.searchParams.get("redirect_uri")).toBe(defaultRelayRedirectUri); + expect(assignedUrl.searchParams.get("redirect_uri")).toBe(expectedDefaultRelayRedirectUri); expect(redirectUriFromCurrentUrl("https://app.example/login?from=home#section")).toBe("https://app.example/login"); const replaceUrl = vi.fn(); diff --git a/tests/walletErrors.test.ts b/tests/walletErrors.test.ts index 0c51e3e..f645d4a 100644 --- a/tests/walletErrors.test.ts +++ b/tests/walletErrors.test.ts @@ -69,7 +69,7 @@ describe("WalletClient errors", () => { }); it("maps consumed auth commitments to a specific SDK error code", async () => { - vi.stubGlobal("fetch", vi.fn(async (input: RequestInfo | URL) => { + const fetchMock = vi.fn(async (input: RequestInfo | URL) => { const url = input.toString(); if (url.endsWith("/CompleteAuth")) { return jsonResponse({ @@ -81,7 +81,8 @@ describe("WalletClient errors", () => { } throw new Error(`Unexpected request: ${url}`); - })); + }); + vi.stubGlobal("fetch", fetchMock); const wallet = new WalletClient({ publicApiKey: "public-api-key", @@ -98,7 +99,12 @@ describe("WalletClient errors", () => { status: 400, retryable: false, }); - expect(activeEmailAuthAttempt(wallet)).toBeUndefined(); + await expect(wallet.completeEmailAuth({code: "123456"})).rejects.toMatchObject({ + code: "OMS_SESSION_MISSING", + operation: "wallet.completeEmailAuth", + message: "No pending email auth attempt", + }); + expect(fetchMock).toHaveBeenCalledOnce(); }); }); @@ -109,10 +115,6 @@ function seedEmailAuthAttempt(wallet: WalletClient): void { }; } -function activeEmailAuthAttempt(wallet: WalletClient): unknown { - return (wallet as any).activeEmailAuthAttempt; -} - function jsonResponse(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { status, diff --git a/tests/walletSession.test.ts b/tests/walletSession.test.ts index 53a0f93..a49b3f7 100644 --- a/tests/walletSession.test.ts +++ b/tests/walletSession.test.ts @@ -45,10 +45,6 @@ function seedEmailAuthAttempt( (wallet as any).activeEmailAuthAttempt = {verifier, challenge}; } -function activeEmailAuthAttempt(wallet: WalletClient): unknown { - return (wallet as any).activeEmailAuthAttempt; -} - describe("WalletClient session storage", () => { it("falls back to memory storage when localStorage is unavailable", () => { vi.stubGlobal("localStorage", undefined); @@ -114,7 +110,11 @@ describe("WalletClient session storage", () => { await wallet.signOut(); expect(wallet.walletAddress).toBeUndefined(); - expect(activeEmailAuthAttempt(wallet)).toBeUndefined(); + await expect(wallet.completeEmailAuth({code: "123456"})).rejects.toMatchObject({ + code: "OMS_SESSION_MISSING", + operation: "wallet.completeEmailAuth", + message: "No pending email auth attempt", + }); expect(wallet.session).toEqual({ walletAddress: undefined, expiresAt: undefined, @@ -323,10 +323,19 @@ describe("WalletClient session storage", () => { it("does not persist stale automatic email auth after a newer email auth starts", async () => { const completeAuth = deferred(); - const fetchMock = vi.fn(async (input: RequestInfo | URL) => { + const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString(); + const body = init?.body ? JSON.parse(init.body as string) : undefined; if (url.endsWith("/CompleteAuth")) { + if (body?.verifier === "verifier-2") { + return jsonResponse({ + identity: {type: "email", sub: "user-2"}, + email: "new@example.com", + wallets: [testWallet("wallet-new", WalletType.Ethereum, "22")], + credential: testCredential(), + }); + } return completeAuth.promise; } @@ -338,6 +347,9 @@ describe("WalletClient session storage", () => { } if (url.endsWith("/UseWallet")) { + if (body?.walletId === "wallet-new") { + return jsonResponse({wallet: testWallet("wallet-new", WalletType.Ethereum, "22")}); + } throw new Error("UseWallet should not be called for stale auth"); } @@ -371,11 +383,12 @@ describe("WalletClient session storage", () => { message: "Email auth attempt is no longer active", }); expect(wallet.walletAddress).toBeUndefined(); - expect(activeEmailAuthAttempt(wallet)).toMatchObject({ - verifier: "verifier-2", - challenge: "challenge-2", - }); expect(requestCount(fetchMock, "/UseWallet")).toBe(0); + await expect(wallet.completeEmailAuth({code: "222222"})).resolves.toMatchObject({ + wallet: {id: "wallet-new"}, + }); + expect(wallet.walletAddress).toBe("0x2222222222222222222222222222222222222222"); + expect(requestCount(fetchMock, "/UseWallet")).toBe(1); }); it("allows email auth completion retry after a failed completion request", async () => {