Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
aa0b71d
test(avoid-temp-email) use a different test structure
christopherferreira9 Jul 7, 2026
2558372
test(avoid-temp-email) update connector and WalletCore to use bypass
christopherferreira9 Jul 7, 2026
dbd558a
test(avoid-temp-email): adds test OTP generation logic
christopherferreira9 Jul 7, 2026
20bc675
test(avoid-temp-email): updates the demo app to use bypass
christopherferreira9 Jul 7, 2026
62398bc
test(avoid-temp-email): adds mocking
christopherferreira9 Jul 7, 2026
0f0e24c
test(avoid-temp-email): updates tests to use fixtures
christopherferreira9 Jul 7, 2026
50383a7
test(avoid-temp-email): add signing mocks and adds authenticatedSessi…
christopherferreira9 Jul 7, 2026
0525d0a
test(avoid-temp-email): fix mock gaps and add CI observability; adds …
christopherferreira9 Jul 8, 2026
19a916a
test(avoid-temp-email): adapt reuseExistingServer depending on USE_RE…
christopherferreira9 Jul 8, 2026
b19dd8f
test(avoid-temp-email): adds unit tests to cover internal option to u…
christopherferreira9 Jul 8, 2026
6bb546c
test(avoid-temp-email): annotates email mode in workflows and renames…
christopherferreira9 Jul 8, 2026
a1c0644
test(avoid-temp-email): decouples the main build step from the tests
christopherferreira9 Jul 8, 2026
2d40b8a
test(avoid-temp-email): adds single required check
christopherferreira9 Jul 9, 2026
9d5e9f1
test(avoid-temp-email): adds a shared backend mocking approach
christopherferreira9 Jul 9, 2026
b184e13
test(avoid-temp-email): updates fixture and adds intgration tests to …
christopherferreira9 Jul 9, 2026
eb5b385
Merge branch 'main' into cferreira/avoid-temp-email-dependency
christopherferreira9 Jul 9, 2026
031b45d
chore: trigger CI
christopherferreira9 Jul 9, 2026
26afb76
Merge branch 'main' into cferreira/avoid-temp-email-dependency
christopherferreira9 Jul 9, 2026
6b5ce21
Merge branch 'main' into cferreira/avoid-temp-email-dependency
christopherferreira9 Jul 14, 2026
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
111 changes: 24 additions & 87 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ jobs:
done

test:
name: Test
name: Unit Tests

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed for a clear read on the pipeline since it hasn't been extracted to the dedicated test workflow.

runs-on: ubuntu-latest
steps:
- name: Checkout
Expand All @@ -113,93 +113,30 @@ jobs:
- name: Run tests
run: pnpm test

integration-test:
name: Integration Tests
runs-on: ubuntu-latest
call-tests:
name: Tests
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
needs: [build]
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Setup pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0

- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Run integration tests
run: pnpm test:integration
env:
ZD_PROJECT_ID: ${{ secrets.ZD_PROJECT_ID }}

e2e-test:
name: Browser E2E Tests
uses: ./.github/workflows/test.yml
with:
use_real_email: false
secrets: inherit

all-checks-pass:

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

single job to have a mandatory success status. Easier to maintain through code.

name: All Checks Pass
if: always()
needs:
- lint-and-format
- typecheck
- build
- test
- call-tests
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
needs: [build]
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Setup pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0

- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build SDK
run: pnpm build

- name: Install Playwright browsers
run: pnpm exec playwright install --with-deps chromium

- name: Build and start demo app
working-directory: apps/zerodev-signer-demo
- name: Verify required checks
run: |
pnpm build
pnpm start &
env:
NEXT_PUBLIC_ZERODEV_PROJECT_ID: ${{ secrets.ZD_PROJECT_ID }}
NEXT_PUBLIC_KMS_PROXY_BASE_URL: https://kms.staging.zerodev.app/api/v1
# Pin the bundler/paymaster to staging so the AA stack matches the
# staging KMS above (the SDK default host is prod).
NEXT_PUBLIC_ZERODEV_AA_HOST: https://staging-meta-aa-provider.onrender.com

- name: Wait for demo app
run: |
for i in $(seq 1 15); do
if curl -sf http://localhost:3000 > /dev/null 2>&1; then
echo "Demo app is ready"
exit 0
fi
echo "Waiting for demo app... ($i/15)"
sleep 2
done
echo "Demo app failed to start"
exit 1

- name: Run E2E tests
run: pnpm test:e2e
env:
DEMO_APP_URL: http://localhost:3000

- name: Upload Playwright report
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 14
if [[ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "One or more required checks failed or were cancelled."
echo "Results: ${{ toJSON(needs.*.result) }}"
exit 1
fi
echo "All required checks passed."
20 changes: 20 additions & 0 deletions .github/workflows/scheduled-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Scheduled Tests

on:
schedule:
- cron: '0 8 * * *'

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently scheduled to run daily using using real email keeping the original behavior and reducing the amount of flakiness due to rate limiting from the email service probider.

workflow_dispatch:
inputs:
use_real_email:
description: 'Use real email service (mail.tm) for OTP tests'
required: false
default: true
type: boolean

jobs:
scheduled-tests:
name: Scheduled Tests (Real Email)
uses: ./.github/workflows/test.yml
with:
use_real_email: ${{ github.event_name == 'workflow_dispatch' && inputs.use_real_email || true }}
secrets: inherit
130 changes: 130 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
name: Tests

on:
workflow_call:
inputs:
use_real_email:
description: 'Use real email service (mail.tm) for OTP tests'
required: false
default: true
type: boolean
secrets:
ZD_PROJECT_ID:
required: true
workflow_dispatch:
inputs:
use_real_email:
description: 'Use real email service (mail.tm) for OTP tests'
required: false
default: true
type: boolean

jobs:
integration-test:
name: Integration Tests
runs-on: ubuntu-latest
steps:
- name: Annotate email mode
run: |
if [ "${{ inputs.use_real_email }}" = "true" ]; then
echo "::notice::Email mode: real (Guerrilla Mail + Turnkey)"
else
echo "::notice::Email mode: mock (no external dependencies)"
fi

- name: Checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Run integration tests
run: pnpm test:integration
env:
ZD_PROJECT_ID: ${{ secrets.ZD_PROJECT_ID }}
USE_REAL_EMAIL: ${{ inputs.use_real_email }}

e2e-test:
name: Browser E2E Tests
runs-on: ubuntu-latest
steps:
- name: Annotate email mode
run: |
if [ "${{ inputs.use_real_email }}" = "true" ]; then
echo "::notice::Email mode: real (Guerrilla Mail + Turnkey)"
else
echo "::notice::Email mode: mock (no external dependencies)"
fi

- name: Checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build SDK
run: pnpm build

- name: Install Playwright browsers
run: pnpm exec playwright install --with-deps chromium

- name: Build and start demo app
working-directory: apps/zerodev-signer-demo
run: |
if [ "$USE_REAL_EMAIL" = "false" ]; then
export NEXT_PUBLIC_DANGEROUS_OTP_SIGNER_KEY=$(cat ../../e2e/fixtures/test-signer-public-key.txt)
fi
pnpm build
pnpm start &
env:
NEXT_PUBLIC_ZERODEV_PROJECT_ID: ${{ secrets.ZD_PROJECT_ID }}
NEXT_PUBLIC_KMS_PROXY_BASE_URL: https://kms.staging.zerodev.app/api/v1
# Pin the bundler/paymaster to staging so the AA stack matches the
# staging KMS above (the SDK default host is prod).
NEXT_PUBLIC_ZERODEV_AA_HOST: https://staging-meta-aa-provider.onrender.com
USE_REAL_EMAIL: ${{ inputs.use_real_email }}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Demo is built with this env variable which is then passed downstream to the wagmi options.


- name: Wait for demo app
run: |
for i in $(seq 1 15); do
if curl -sf http://localhost:3000 > /dev/null 2>&1; then
echo "Demo app is ready"
exit 0
fi
echo "Waiting for demo app... ($i/15)"
sleep 2
done
echo "Demo app failed to start"
exit 1

- name: Run E2E tests
run: pnpm test:e2e
env:
DEMO_APP_URL: http://localhost:3000
USE_REAL_EMAIL: ${{ inputs.use_real_email }}

- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 14
3 changes: 3 additions & 0 deletions apps/zerodev-signer-demo/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ NEXT_PUBLIC_SEPOLIA_RPC_URL=
NEXT_PUBLIC_BASE_SEPOLIA_RPC_URL=
NEXT_PUBLIC_ARB_SEPOLIA_RPC_URL=
NEXT_PUBLIC_ZERODEV_PROJECT_ID=

# For E2E test mocking only — never set in production
# NEXT_PUBLIC_DANGEROUS_OTP_SIGNER_KEY=
3 changes: 3 additions & 0 deletions apps/zerodev-signer-demo/src/app/wagmi-config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export const config = createConfig({
organizationId: process.env.NEXT_PUBLIC_ORG_ID,
}),
...(mode && { mode }),
...(process.env.NEXT_PUBLIC_DANGEROUS_OTP_SIGNER_KEY && {
dangerouslyOverrideOtpSignerPublicKey: process.env.NEXT_PUBLIC_DANGEROUS_OTP_SIGNER_KEY,
}),
config: {
logo: <ZeroDevLogo variant="mark" tone="color" className="zd:h-8 zd:w-auto" />,
auth: {
Expand Down
77 changes: 77 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# E2E Tests

## Two-mode testing strategy

Browser tests run in two modes controlled by the `USE_REAL_EMAIL` environment variable.

| | Mock mode (`USE_REAL_EMAIL=false`) | Real-email mode (`USE_REAL_EMAIL=true`) |
|---|---|---|
| **When** | PR CI, local development | Nightly scheduled run |
| **Email** | Intercepted via `page.route()` | mail.tm temporary inbox |
| **Auth session** | Fake JWT (never hits backend) | Real session from ZeroDev KMS |
| **Signing** | Fake signature stub | Real ZeroDev KMS signing |
| **Wallet address** | Indeterminate (no real account) | Real counterfactual address |
| **Project config** | Not enforced | Enforced (chains, sponsorship) |
| **Speed** | Fast, no network waits | Slower, dependent on external services |

### What mock mode does NOT cover

Mock mode verifies UI flow and component wiring. It deliberately bypasses:

- **Session validation** — the fake JWT is never checked against the database; user/org do not exist.
- **Project configuration** — allowed chains, permitted operations, and gas sponsorship rules are never evaluated.
- **Cryptographic signing** — the returned signature is a deterministic stub; it is not valid for any message and is not tied to any key or wallet address.
- **Real wallet address** — no account is created; wallet address is meaningless in mock mode.

These gaps exist because mocking auth cascades: once auth is mocked the backend rejects all subsequent authenticated requests, so signing endpoints must also be mocked. This is an accepted tradeoff — the nightly real-email run covers correctness end-to-end.

### What mock mode does cover

- OTP entry form renders and accepts input
- Magic-link entry form renders and accepts input
- Error states (wrong code, expired session) — by adjusting fixture responses
- Post-auth UI (wallet address display, sign message, logout flow)
- Component composition in `@zerodev/wallet-react-kit`

## Directory structure

```
e2e/
├── browser/ # Playwright tests (UI + integration)
│ ├── otp.spec.ts
│ ├── magic-link.spec.ts
│ └── post-auth.spec.ts
├── integration/ # Vitest tests against real backend (no UI)
├── fixtures/
│ ├── auth.ts # Playwright fixture extensions (OTP, magic-link, authenticated sessions)
│ ├── test-otp-bundle.json # Pre-signed EncryptionTargetEnvelope for mock mode
│ ├── test-signer-public-key.txt # enclaveQuorumPublic hex for the test key pair
│ └── README.md # Fixture regeneration instructions
├── helpers/
│ ├── mock-backend.ts # page.route() intercepts for auth + signing
│ ├── mock-session.ts # Fake session JWT builder
│ └── env-utils.ts # isRealEmail() helper
└── scripts/
└── generate-test-otp-bundle.ts # One-time fixture generator
```

## Running tests locally

```bash
# Mock mode (default — no email service needed)
pnpm test:e2e:headed

# Real-email mode
USE_REAL_EMAIL=true pnpm test:e2e:headed
```

`USE_REAL_EMAIL` unset is treated as `false` — mock mode is the default so tests work out of the box without any configuration.

## CI workflows

- **`ci.yml`** calls `test.yml` with `use_real_email: false` on every PR and push to `main`/`develop`.
- **`scheduled-tests.yml`** calls `test.yml` with `use_real_email: true` daily at 08:00 UTC.

## Fixture maintenance

The pre-signed OTP bundle in `e2e/fixtures/` must be regenerated when `encryptOtpAttempt` validation logic or the `EncryptionTargetEnvelope` shape changes. See [`e2e/fixtures/README.md`](fixtures/README.md) for instructions.
Loading
Loading