diff --git a/.changeset/brave-flies-clap.md b/.changeset/brave-flies-clap.md new file mode 100644 index 000000000..41f7889b2 --- /dev/null +++ b/.changeset/brave-flies-clap.md @@ -0,0 +1,5 @@ +--- +"@exactly/mobile": patch +--- + +✅ test card activation flow diff --git a/.changeset/common-drinks-grow.md b/.changeset/common-drinks-grow.md new file mode 100644 index 000000000..040e62081 --- /dev/null +++ b/.changeset/common-drinks-grow.md @@ -0,0 +1,5 @@ +--- +"@exactly/server": patch +--- + +🤡 enhance panda mocks for e2e tests diff --git a/.changeset/hot-buckets-peel.md b/.changeset/hot-buckets-peel.md new file mode 100644 index 000000000..5d7cd09e0 --- /dev/null +++ b/.changeset/hot-buckets-peel.md @@ -0,0 +1,5 @@ +--- +"@exactly/mobile": patch +--- + +♿️ add aria label to card sensitive toggle diff --git a/.changeset/kind-yaks-jump.md b/.changeset/kind-yaks-jump.md new file mode 100644 index 000000000..988aa81f6 --- /dev/null +++ b/.changeset/kind-yaks-jump.md @@ -0,0 +1,5 @@ +--- +"@exactly/mobile": patch +--- + +✅ extract maestro aria subflows diff --git a/.maestro/flows/local.yaml b/.maestro/flows/local.yaml index 6a785596c..e96ac7bd6 100644 --- a/.maestro/flows/local.yaml +++ b/.maestro/flows/local.yaml @@ -33,37 +33,19 @@ tags: [critical] file: ../subflows/sendAsset.yaml env: { asset: USDC, to: "${output.owner}", amount: "69" } - tapOn: Home -- runFlow: # HACK https://github.com/mobile-dev-inc/Maestro/issues/2914 - when: { true: "${maestro.platform != 'web'}" } - commands: - - repeat: - while: { visible: Pending proposals } - commands: [{ tapOn: Home }] -- runFlow: # HACK https://github.com/mobile-dev-inc/Maestro/issues/2914 - when: { platform: web } - commands: - - repeat: - while: { visible: { id: Pending proposals } } - commands: [{ tapOn: Home }] +- runFlow: + file: ../subflows/tapWhileAria.yaml + env: { aria: Pending proposals, tap: Home } - runFlow: ../subflows/readPortfolio.yaml - assertTrue: ${output.portfolioBefore - output.portfolio === 69} - evalScript: ${output.portfolioBefore = output.portfolio} - runFlow: file: ../subflows/borrowAsset.yaml - env: { amount: "10", installments: "4" } + env: { amount: "10", installments: "1" } - tapOn: Home -- runFlow: # HACK https://github.com/mobile-dev-inc/Maestro/issues/2914 - when: { true: "${maestro.platform != 'web'}" } - commands: - - repeat: - while: { visible: Pending proposals } - commands: [{ tapOn: Home }] -- runFlow: # HACK https://github.com/mobile-dev-inc/Maestro/issues/2914 - when: { platform: web } - commands: - - repeat: - while: { visible: { id: Pending proposals } } - commands: [{ tapOn: Home }] +- runFlow: + file: ../subflows/tapWhileAria.yaml + env: { aria: Pending proposals, tap: Home } - runFlow: ../subflows/readPortfolio.yaml - assertTrue: ${output.portfolio - output.portfolioBefore === 10} - runFlow: @@ -72,4 +54,7 @@ tags: [critical] - tapOn: View all steps - runFlow: ../subflows/verifyIdentity.yaml - assertNotVisible: Getting Started +- runFlow: ../subflows/activateCard.yaml +- tapOn: Home +- assertVisible: SPENDING LIMIT - runFlow: ../subflows/storeCoverage.yaml diff --git a/.maestro/subflows/activateCard.yaml b/.maestro/subflows/activateCard.yaml new file mode 100644 index 000000000..8fd041422 --- /dev/null +++ b/.maestro/subflows/activateCard.yaml @@ -0,0 +1,43 @@ +appId: ${APP_ID ?? "app.exactly"} +--- +- tapOn: Card +- runFlow: + when: { true: "${maestro.platform != 'web'}" } + commands: + - tapOn: \$[\s\d,.\xa0]+, AVAILABLE BALANCE +- runFlow: + when: { platform: web } + commands: [{ tapOn: Available balance }] +- runFlow: + when: { visible: Accept and enable card } + commands: [{ tapOn: Accept and enable card }] +- assertVisible: Manually add your card to Apple Pay & Google Pay to make contactless payments. +- tapOn: Close +- tapOn: Freeze card +- tapOn: Unfreeze card +- tapOn: View PIN number +- tapOn: Close +- tapOn: Weekly spending limit +- tapOn: Close +- runFlow: { file: ../subflows/tapAria.yaml, env: { aria: Hide sensitive } } +- runFlow: + when: { true: "${maestro.platform != 'web'}" } + commands: + - assertNotVisible: \$[\s\d,.\xa0]+, AVAILABLE BALANCE +- runFlow: + when: { platform: web } + commands: + - assertNotVisible: + text: \$[\s\d,.\xa0]+ + above: Available balance +- runFlow: { file: ../subflows/tapAria.yaml, env: { aria: Show sensitive } } +- runFlow: + when: { true: "${maestro.platform != 'web'}" } + commands: + - assertVisible: \$[\s\d,.\xa0]+, AVAILABLE BALANCE +- runFlow: + when: { platform: web } + commands: + - assertVisible: + text: \$[\s\d,.\xa0]+ + above: Available balance diff --git a/.maestro/subflows/borrowAsset.yaml b/.maestro/subflows/borrowAsset.yaml index 329777c12..4951c37e5 100644 --- a/.maestro/subflows/borrowAsset.yaml +++ b/.maestro/subflows/borrowAsset.yaml @@ -12,12 +12,7 @@ appId: ${APP_ID ?? "app.exactly"} commands: [{ tapOn: Connect wallet to Exactly Protocol }] - tapOn: Explore funding options - assertVisible: Select amount -- runFlow: # HACK https://github.com/mobile-dev-inc/Maestro/issues/2914 - when: { true: "${maestro.platform != 'web'}" } - commands: [{ tapOn: Amount }] -- runFlow: # HACK https://github.com/mobile-dev-inc/Maestro/issues/2914 - when: { platform: web } - commands: [{ tapOn: { id: Amount } }] +- runFlow: { file: tapAria.yaml, env: { aria: Amount } } - inputText: ${amount} - tapOn: Select Amount # HACK - tapOn: Continue diff --git a/.maestro/subflows/sendAsset.yaml b/.maestro/subflows/sendAsset.yaml index c0899bf3f..91f845c13 100644 --- a/.maestro/subflows/sendAsset.yaml +++ b/.maestro/subflows/sendAsset.yaml @@ -30,9 +30,4 @@ appId: ${APP_ID ?? "app.exactly"} - runScript: file: ../dist/hookProposals.js env: { account: "${output.account}" } -- runFlow: - when: { true: "${maestro.platform != 'web'}" } - commands: [{ tapOn: Close }] -- runFlow: # HACK https://github.com/mobile-dev-inc/Maestro/issues/2914 - when: { platform: web } - commands: [{ tapOn: { id: Close } }] +- runFlow: { file: tapAria.yaml, env: { aria: Close } } diff --git a/.maestro/subflows/storeCoverage.yaml b/.maestro/subflows/storeCoverage.yaml index e8498a1f9..a2dfd88af 100644 --- a/.maestro/subflows/storeCoverage.yaml +++ b/.maestro/subflows/storeCoverage.yaml @@ -1,9 +1,6 @@ appId: ${APP_ID ?? "app.exactly"} --- -- runFlow: - when: { true: "${maestro.platform != 'web'}" } - commands: [{ tapOn: Settings }] -- runFlow: { when: { platform: web }, commands: [{ tapOn: { id: Settings } }] } +- runFlow: { file: tapAria.yaml, env: { aria: Settings } } - tapOn: Submit coverage - runFlow: when: { platform: android } diff --git a/.maestro/subflows/tapAria.yaml b/.maestro/subflows/tapAria.yaml new file mode 100644 index 000000000..7a651be2d --- /dev/null +++ b/.maestro/subflows/tapAria.yaml @@ -0,0 +1,9 @@ +appId: ${APP_ID ?? "app.exactly"} +--- +# HACK https://github.com/mobile-dev-inc/Maestro/issues/2914 +- runFlow: + when: { true: "${maestro.platform != 'web'}" } + commands: [{ tapOn: "${aria}" }] +- runFlow: + when: { platform: web } + commands: [{ tapOn: { id: "${aria}" } }] diff --git a/.maestro/subflows/tapWhileAria.yaml b/.maestro/subflows/tapWhileAria.yaml new file mode 100644 index 000000000..777195a90 --- /dev/null +++ b/.maestro/subflows/tapWhileAria.yaml @@ -0,0 +1,15 @@ +appId: ${APP_ID ?? "app.exactly"} +--- +# HACK https://github.com/mobile-dev-inc/Maestro/issues/2914 +- runFlow: + when: { true: "${maestro.platform != 'web'}" } + commands: + - repeat: + while: { visible: "${aria}" } + commands: [{ tapOn: "${tap ?? aria}" }] +- runFlow: + when: { platform: web } + commands: + - repeat: + while: { visible: { id: "${aria}" } } + commands: [{ tapOn: "${tap ?? aria}" }] diff --git a/.maestro/subflows/verifyIdentity.yaml b/.maestro/subflows/verifyIdentity.yaml index 85ca95faf..789ac589a 100644 --- a/.maestro/subflows/verifyIdentity.yaml +++ b/.maestro/subflows/verifyIdentity.yaml @@ -78,8 +78,5 @@ appId: ${APP_ID ?? "app.exactly"} - runScript: file: ../dist/approveKYC.js env: { credentialId: "${output.owner}" } -- runFlow: - when: { true: "${maestro.platform != 'web'}" } - commands: [{ tapOn: Back }] -- runFlow: { when: { platform: web }, commands: [{ tapOn: { id: Back } }] } +- runFlow: { file: tapAria.yaml, env: { aria: Back } } - tapOn: Home diff --git a/common/pandaCertificate.ts b/common/pandaCertificate.ts index a41e2a2fc..f9b2564d1 100644 --- a/common/pandaCertificate.ts +++ b/common/pandaCertificate.ts @@ -9,11 +9,17 @@ GE+OO1j3fa8HhYlJZZ7CCIAsaCorrU+ZpD5PUTnmME3DJk+JyY1BB3p8XI+C5uno QucrbxFbkM1lgR10ewz/LcuhleG0mrXL/bzUZbeJqI6v3c9bXvLPKlsordPanYBG FZkmBPxc8QEdRgH4awIDAQAB -----END PUBLIC KEY-----`, - }[domain] || - `-----BEGIN PUBLIC KEY----- + "sandbox.exactly.app": `-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCAP192809jZyaw62g/eTzJ3P9H +RmT88sXUYjQ0K8Bx+rJ83f22+9isKx+lo5UuV8tvOlKwvdDS/pVbzpG7D7NO45c 0zkLOXwDHZkou8fuj8xhDO5Tq3GzcrabNLRLVz3dkx0znfzGOhnY4lkOMIdKxlQb LuVM/dGDC9UpulF+UwIDAQAB +-----END PUBLIC KEY-----`, + }[domain] || + `-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCu2YOeObkaYiQmc49t2Cnk8syA +1UBqFBMVhkJXyuSA9f+hGC22fXgQtpfAjQmFRpt5q4f6i0rG2bUi8Km0jZELdD6X +Kz63/hp522fbxNuOOxs37dlH9B3k6W8NQjjDjaFhAwCsevq7uASXwEEK3NpV7DEP +lJe6c8CQ0+QqTTy2ZwIDAQAB -----END PUBLIC KEY-----`; /* eslint-enable @typescript-eslint/prefer-nullish-coalescing */ diff --git a/server/test/e2e.ts b/server/test/e2e.ts index af992844b..c5c73ce05 100644 --- a/server/test/e2e.ts +++ b/server/test/e2e.ts @@ -9,9 +9,11 @@ import "./mocks/sardine"; import "./mocks/sentry"; import { cors } from "hono/cors"; +import crypto from "node:crypto"; import { mkdir, writeFile } from "node:fs/promises"; import { describe, expect, it, vi } from "vitest"; +import type * as panda from "../utils/panda"; import type * as persona from "../utils/persona"; import type * as sentry from "@sentry/node"; @@ -43,12 +45,87 @@ describe("e2e", () => { ); }); -vi.mock("../utils/panda", async (importOriginal) => ({ - ...(await importOriginal()), - createUser: vi - .fn<() => Promise<{ id: string }>>() - .mockImplementation(() => Promise.resolve({ id: String(Math.random()) })), -})); +vi.mock("../utils/panda", async (importOriginal: () => Promise) => { + const original = await importOriginal(); + type User = Awaited>; + type Card = Awaited>; + const users = new Map(); + const cards = new Map(); + return { + ...original, + autoCredit: vi.fn().mockResolvedValue(false), + createCard: vi.fn().mockImplementation((userId: string) => { + const id = `crd_${Math.random().toString(36).slice(2)}`; + const card: Card = { + expirationMonth: "12", + expirationYear: "2030", + id, + last4: String(Math.floor(1000 + Math.random() * 9000)), + limit: { amount: 1_000_000, frequency: "per7DayPeriod" }, + status: "active", + type: "virtual", + userId, + }; + cards.set(id, card); + return Promise.resolve(card); + }), + createUser: vi.fn().mockImplementation(() => { + const id = `usr_${Math.random().toString(36).slice(2)}`; + const user: User = { + applicationReason: "", + applicationStatus: "approved", + email: "test@example.com", + firstName: "Test", + id, + isActive: true, + lastName: "User", + phoneCountryCode: "+1", + phoneNumber: "5551234567", + }; + users.set(id, user); + return Promise.resolve({ id }); + }), + getCard: vi.fn().mockImplementation((cardId: string) => Promise.resolve(cards.get(cardId))), + getPIN: vi.fn().mockResolvedValue({ pin: null }), + getSecrets: vi.fn().mockImplementation((_cardId: string, sessionId: string) => { + const privateKey = process.env.PANDA_E2E_PRIVATE_KEY; + if (!privateKey) throw new Error("PANDA_E2E_PRIVATE_KEY not set"); + const encryptedSecret = Buffer.from(sessionId, "base64"); + const secretKeyBase64 = crypto.privateDecrypt( + { key: privateKey, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha1" }, + encryptedSecret, + ); + const secretKey = Buffer.from(secretKeyBase64.toString("utf8"), "base64"); + function encrypt(plaintext: string) { + const iv = crypto.randomBytes(16); + const cipher = crypto.createCipheriv("aes-128-gcm", secretKey, iv); + const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]); + const authTag = cipher.getAuthTag(); + return { data: Buffer.concat([encrypted, authTag]).toString("base64"), iv: iv.toString("base64") }; + } + return Promise.resolve({ + encryptedCvc: encrypt("123"), + encryptedPan: encrypt("4111111111111234"), + }); + }), + getUser: vi.fn().mockImplementation((userId: string) => Promise.resolve(users.get(userId))), + isPanda: vi.fn().mockResolvedValue(true), + setPIN: vi.fn().mockResolvedValue({}), + signIssuerOp: vi.fn().mockResolvedValue("0x" + "ab".repeat(65)), + updateCard: vi.fn().mockImplementation((update: { id: string }) => { + const card = cards.get(update.id); + if (!card) return Promise.resolve(); + Object.assign(card, update); + return Promise.resolve(card); + }), + updateUser: vi.fn().mockImplementation((update: { id: string }) => { + const user = users.get(update.id); + if (!user) return Promise.resolve(); + Object.assign(user, update); + return Promise.resolve(user); + }), + }; +}); vi.mock("../utils/persona", async (importOriginal: () => Promise) => { const original = await importOriginal(); diff --git a/server/vitest.config.mts b/server/vitest.config.mts index c65e872a5..eaae9e1b4 100644 --- a/server/vitest.config.mts +++ b/server/vitest.config.mts @@ -25,6 +25,22 @@ export default defineConfig({ KEEPER_PRIVATE_KEY: padHex("0x69"), PANDA_API_KEY: "panda", PANDA_API_URL: "https://panda.test", + PANDA_E2E_PRIVATE_KEY: `-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAK7Zg545uRpiJCZz +j23YKeTyzIDVQGoUExWGQlfK5ID1/6EYLbZ9eBC2l8CNCYVGm3mrh/qLSsbZtSLw +qbSNkQt0PpcrPrf+GnnbZ9vE2447Gzft2Uf0HeTpbw1COMONoWEDAKx6+ru4BJfA +QQrc2lXsMQ+Ul7pzwJDT5CpNPLZnAgMBAAECgYA7bxqLPSnLaxLIsz1M5E6RUWrs +XBCyPjKifWmtt/zmTThghPx87LdUTwzUWdyjnfWZbRIiuxhm8Xfd8ZpuEjT79H0j +4GT12UOFlKAvi2lXdqn7IBFIkdVC3kS6wFUFbHKTGwiVDUP/l9z92POPEV+cIpAh +rf1q7VYxoOXU2+RBUQJBAORq7ebGfLgnvNp49o8bSUcNxbhola7jpRCKOt2oexAM +B3SmvX/hPlthst3Lcpa/vYE5VFvLqa1DnObBoDWwvV8CQQDD9qM9W7HmMgByUv9S +3Fs/Qqqe7dhIlcXD3mLjby2vxH5qE1+okNgFcTgJ/G0oacFz3uUhbvYAqFWU3LIh +1Jv5AkBB11zKF87dmn7CjvmrWJcvxxWGSYdUCUSMVvwO5sDKaF1Bz8px8TBzUN8p +NbrLH2v1stvRNgyr6ABzN78BmveLAkEAqMVzA7ZEOghYYB3hLgEASTRmdChN/P2Y +7L9MFaq8A0RMx5jV6vyMP+upotgXPxYN+Xg/iJLjJd/UjTeh5wcQKQJAKkzcVyZw +OEW5okJDZmmfTeh96WBhKGaOczZuuYn88I3A6cKj1p8Yc7UZ1X8vvztY5P7N0YbL +VuNOZKwaXFtqgA== +-----END PRIVATE KEY-----`, PAX_API_KEY: "pax", PAX_API_URL: "https://pax.test", PAX_ASSOCIATE_ID_KEY: "pax", diff --git a/src/components/card/Card.tsx b/src/components/card/Card.tsx index f27236620..632701d4e 100644 --- a/src/components/card/Card.tsx +++ b/src/components/card/Card.tsx @@ -276,6 +276,7 @@ export default function Card() { { queryClient.setQueryData(["settings", "sensitive"], !hidden); }}