What You’ll Learn
- How to test WebAuthn (passkey) authentication in CI environments
- Automating OTP email retrieval with Mailpit API
- Preventing email race conditions in parallel E2E tests
- Locale-specific testing for multilingual UIs
Introduction
In Part 1, I introduced the overall architecture and automation strategy for “Saru,” a multi-tenant SaaS platform. This article dives deeper into the E2E testing implementation that forms the core of that automation.
The most challenging aspect is testing authentication flows. Saru uses two authentication methods:
| Portal | Auth Method | Challenge |
|---|---|---|
| System / Provider | OTP + Passkey | Email retrieval, WebAuthn |
| Reseller / Consumer | Keycloak OAuth | External IdP integration |
This article explains how to automate testing all of these in CI.
1. WebAuthn Virtual Authenticator: Testing Passkeys in CI
The Challenge with Passkey Authentication
WebAuthn (passkeys) typically requires physical security keys or biometric authentication. At first glance, testing this in CI seems impossible.
Solution: Chrome DevTools Protocol (CDP) Virtual Authenticator
Playwright allows you to create virtual authenticators through CDP. This enables testing the full WebAuthn flow without physical devices.
Note: CDP virtual authenticators are Chromium-only. They don’t work with Safari (WebKit) or Firefox. For cross-browser testing, run WebAuthn tests only on Chromium and mock authenticated state for other browsers.
Implementation Code
| |
Alignment Between Transport Settings and Server Configuration
When setting up the WebAuthn virtual authenticator, alignment with server-side settings is crucial.
In Saru’s case, the backend generates WebAuthn registration options with AuthenticatorAttachment: CrossPlatform. This setting “prefers roaming authenticators (USB keys, etc.).”
Initially, I used transport: 'internal' (platform authenticator), which caused registration to fail.
| |
Key Point: The virtual authenticator’s
transportsetting needs to align with the server’sAuthenticatorAttachmentsetting. If registration fails, check the server configuration first. While WebAuthn spec doesn’t require exact 1:1 correspondence, misalignment is a common cause of failures.
2. Automating OTP Email Retrieval: Mailpit API Integration
Problems with Traditional Approaches
Many E2E tests retrieve OTP from a test endpoint:
| |
Problems:
- Adds
TEST_MODEbranches to production code - Doesn’t test actual email sending
- Diverges from real user flows
Solution: Mailpit API
Saru uses Mailpit (development mail server) API to extract OTP from actually sent emails.
| |
Key Points:
- Tests actual email sending flow
- Supports both Japanese/English subject patterns
- Polls for up to 30 seconds (handles SMTP cold start delays)
3. Preventing Email Race Conditions in Parallel Tests
The Problem: OTP Mix-ups in Parallel Execution
When running multiple tests in parallel in CI, tests may accidentally retrieve another test’s OTP.
For example:
- Test A: Sends OTP to
user-a@example.com - Test B: Sends OTP to
user-b@example.com - Test A: Searches Mailpit → Gets Test B’s OTP
Solution: Timestamp + Unique Address Filtering
Saru combines two methods to prevent race conditions:
- Unique email addresses: Each test uses a different email address
- Timestamp filtering: Record time before OTP request, search only emails after that time
| |
| |
Alternative Approaches for Parallel Testing
More robust methods to consider:
| Method | Pros | Cons |
|---|---|---|
| Unique address + timestamp (Saru’s approach) | Simple, no backend changes | Vulnerable to clock skew |
| Embed X-Request-ID in email | Uniquely identifies email | Requires backend changes |
| Mailpit Search API | Direct filtering by conditions | Depends on API features |
Saru’s approach prioritizes “simple and works well enough.”
Deprecated: clearMailpit()
Previously, clearMailpit() deleted all emails before each test, but in parallel execution this deletes other tests’ emails too. Timestamp filtering made this function deprecated.
| |
4. Appendix: Locale-Specific Testing
Not directly related to authentication testing, but a useful technique for E2E testing multilingual apps.
The Challenge with Multilingual E2E
Common approach:
| |
Problem: Regex must be updated every time a language is added.
Locale-Specific Testing Pattern
In Saru, we fix the language at test time and directly verify that language’s text.
| |
| |
Benefits: Text is explicit and readable; impact scope is clear when adding languages.
5. CI Configuration: Parallel Execution on Self-hosted Runners
Matrix Strategy for Parallelization
GitHub Actions uses matrix for parallel execution by portal.
| |
Separating Cross-Portal Tests
Tests spanning multiple portals (e.g., Provider→Reseller integration) run in a separate job.
Reasons:
- Tests logging in as the same user compete
- OTP retrieval timing overlaps
| |
6. Running Cross-Portal Tests Locally
Since it takes 15-20 minutes to reach cross-portal tests in CI, we have scripts for local verification first.
| |
Summary
| Challenge | Solution | Constraints/Notes |
|---|---|---|
| Testing WebAuthn authentication | CDP virtual authenticator | Chromium only |
| OTP email retrieval | Mailpit API integration | Requires polling |
| Email race conditions in parallel tests | Unique address + timestamp | Watch for clock skew |
| Multilingual UI testing | Locale-specific testing | Cookie setup dependent |
| CI execution time | Matrix parallelization + cross-portal separation | Complex job design |
With these mechanisms, Saru’s main authentication flows are automated in CI. Production-specific issues (external IdP outages, browser update behavior changes, etc.) still require manual verification, but manual testing in the daily development cycle has been significantly reduced.
Series Articles
- Part 1: Tackling Unmanageable Complexity with Automation
- Part 2: Testing WebAuthn in CI (this article)