How to Test Google, GitHub, and Social OAuth
Short answer
Social OAuth couples third-party consent screens, popup blockers, account linking, and token exchange—clicking "Continue with Google" in one browser is not proof your API accepts the resulting session. Use sandbox OAuth apps, tiered strategies (API Arrange vs full redirect), and probe Assert on protected routes—not shared personal Google accounts in parallel CI.
Part of Testing Guides by auth and identity.
Who this is for
Product teams shipping Google, GitHub, Apple, Microsoft, or LinkedIn sign-in on web (often alongside email/password or Firebase/Auth0) who need Playwright E2E that survives headless CI, consent denial, and duplicate-account linking—not one-off scripts that log in with a developer's personal Gmail once.
Typical stacks: NextAuth.js, Auth0 social connections, Firebase signInWithPopup, Supabase Auth, Clerk, or custom OAuth2/OIDC handlers.
Why testing social OAuth matters
OAuth bugs look like "login works on my machine" until they do not:
- Revenue loss — Google sign-in succeeds but
email_verifiedis not checked; attacker creates Google account with victim email and accesses billing. - Security incidents —
stateparameter omitted → CSRF login; refresh token stored in localStorage; account linking merges wrong identities when emails differ by case. - Support load — popup blocked in embedded WebViews; Safari ITP drops session cookie after redirect; "Account already exists" with no recovery path when user forgot they used email signup first.
- Compliance exposure — OAuth scopes over-requested (Drive access when only profile needed); audit cannot prove consent screen was shown for enterprise customers.
E2E must assert token exchange, session establishment, and API authorization—not only that the provider button navigates away and back.
Complexity map
| Scenario | Edge case | Why tests break | Approach |
|---|---|---|---|
| Popup vs redirect | signInWithPopup blocked headless | Hangs waiting for popup | Redirect flow in CI; popup in headed nightly |
| Google OAuth | 2FA on test account | Cannot automate consent | Sandbox app + dedicated test user OR API Arrange |
| GitHub OAuth | Org SSO enforcement | 404 on authorize | Test user outside enforced org |
| Denied consent | User clicks Cancel | Unhandled promise rejection | Assert error route + no session |
| Account linking | Email account + Google same email | Duplicate uid or merge failure | Seed both paths; probe single identity |
| Different credential same email | Password user + Google | account-exists errors | Explicit link flow spec |
Missing state / PKCE | Security regression | Test passes without OAuth | Negative unit test + one E2E redirect |
| Scope downgrade | App requests fewer scopes on re-auth | Feature breaks silently | Assert granted scopes in probe |
| Token refresh | Provider refresh token revoked | Mid-suite logout | Refresh spec with stub |
| Apple Sign In | JS SDK + redirect constraints | Web vs native split | Document platform-specific jobs |
| Microsoft / Azure AD | Multi-tenant common vs tenant | Wrong tenant login | Fixture tenant IDs |
| Email not verified (Google) | email_verified: false | Should reject login | Negative probe |
| One Tap / FedCM | Auto prompt blocked | Flaky auto login | Disable in test env |
| Mobile WebView | Custom scheme redirect | Playwright cannot follow | API Arrange for mobile web |
| Rate limits | Too many token exchanges | 429 in parallel CI | Cache session per worker |
| Provider outage simulation | 503 on token endpoint | Untested error UX | AIMock or MSW stub |
Tiered testing strategy
Real OAuth in headless Linux CI is painful. Use tiers:
| Tier | Coverage | When |
|---|---|---|
| Post-login E2E | Seed session as OAuth-linked user | Every PR — features after login |
| Redirect OAuth (sandbox) | Full authorize → callback | Nightly or weekly |
| Popup OAuth | Headed Playwright or manual capture | Pre-release |
| TrueCoverage | Prod oauth_provider share | Prioritize tiers for dominant provider |
See Firebase auth guide for emulator/fake-provider patterns when using Firebase.
Sandbox OAuth applications
Register separate OAuth apps for test/staging—never production client secrets in CI logs.
| Provider | Sandbox setup | Docs |
|---|---|---|
| Cloud Console → OAuth client, test users on consent screen | Google OAuth testing | |
| GitHub | OAuth App or GitHub App with limited repos | GitHub OAuth apps |
| Apple | Services ID + return URLs for staging | Sign in with Apple |
| Microsoft | App registration in dev tenant | Microsoft identity platform |
Add test users to Google OAuth consent screen (External apps in Testing mode) so automated accounts can complete consent without verification delays.
Redirect flow in Playwright (preferred for CI)
Popup flows fail when window.open is blocked. Prefer redirect-based OAuth in test configuration:
// auth.config.ts — test env
export const oauthConfig = {
useRedirect: process.env.CI === 'true',
googleClientId: process.env.GOOGLE_OAUTH_CLIENT_ID,
};
// Playwright — full redirect (nightly tier)
test('Google OAuth redirect establishes session', async ({ page }) => {
await page.goto('/login');
await page.getByRole('button', { name: 'Continue with Google' }).click();
// Lands on Google — use dedicated test account credentials
await page.getByLabel('Email or phone').fill(process.env.GOOGLE_TEST_EMAIL!);
await page.getByRole('button', { name: 'Next' }).click();
await page.getByLabel('Enter your password').fill(process.env.GOOGLE_TEST_PASSWORD!);
await page.getByRole('button', { name: 'Next' }).click();
// Consent screen (first time only)
if (await page.getByRole('button', { name: 'Continue' }).isVisible()) {
await page.getByRole('button', { name: 'Continue' }).click();
}
await page.waitForURL(/\/dashboard/);
const me = await page.request.get('/api/me');
expect(me.status()).toBe(200);
const body = await me.json();
expect(body.authProvider).toBe('google');
expect(body.emailVerified).toBe(true);
});
Store Google test credentials in CI secrets—one account per worker if parallel nightly jobs run consent flows.
Post-login Arrange (preferred for PR CI)
Skip provider UI when testing dashboard, billing, or settings:
// POST /api/test/seed-oauth-user
// Creates user with authProvider: 'google', providerId, email verified
test.beforeEach(async ({ request, page }) => {
const runId = `${test.info().parallelIndex}-${Date.now()}`;
await request.post('/api/test/seed-oauth-user', {
data: { runId, provider: 'google', emailVerified: true },
});
await page.goto(`/test/login-as?runId=${runId}`);
});
Popup flow pitfalls
If production uses popups (signInWithPopup, NextAuth popup mode):
// Headed only — or expect failure in CI
const popupPromise = page.waitForEvent('popup');
await page.getByRole('button', { name: 'Sign in with GitHub' }).click();
const popup = await popupPromise;
await popup.waitForLoadState();
// ... complete OAuth in popup
await popup.waitForEvent('close');
In CI, switch to redirect via env flag rather than fighting popup blockers. Document the flag so prod behavior stays popup if required for UX.
Account linking and duplicate identities
| Case | Expected behavior | Probe |
|---|---|---|
| Email signup exists, Google same email | Offer link or auto-link | Single user id; both providers on profile |
| Email signup exists, Google different email | Separate accounts OR explicit merge UI | Two users OR merged with audit |
| Google first, then set password | Link credential | Login works both ways |
| Unlink provider | Cannot orphan account without backup | Block unlink if only auth method |
test('linking Google to existing email account preserves uid', async ({ page, request }) => {
const runId = Date.now().toString();
await request.post('/api/test/seed-user', { data: { runId, provider: 'email' } });
await page.goto('/login');
// ... login with email/password
const before = await page.request.get('/api/me').then(r => r.json());
// Simulate link via test helper (full Google too heavy for PR)
await page.request.post('/api/test/link-provider', {
data: { uid: before.uid, provider: 'google', providerId: `google-${runId}` },
});
const after = await page.request.get('/api/me').then(r => r.json());
expect(after.uid).toBe(before.uid);
expect(after.providers).toContain('google');
});
Denied consent and error paths
Users click Cancel on Google/GitHub consent more often than tests assume:
test('denied GitHub consent does not create session', async ({ page }) => {
await page.goto('/login?force_oauth_error=access_denied'); // test shim
// Or automate cancel on consent screen in nightly job
await expect(page).toHaveURL(/\/login\?error=/);
const me = await page.request.get('/api/me');
expect(me.status()).toBe(401);
});
Assert no partial session: cookie absent, /api/me 401, no orphan user row in DB probe.
Security assertions (minimum bar)
One E2E or integration test should verify:
statemismatch on callback → error, no session- PKCE
code_verifierrequired (if using PKCE) - Redirect URI allowlist rejects
evil.com email_verified: falsefrom Google rejected if policy requires verification
Use unit tests for exhaustive cases; E2E proves wiring end-to-end.
CI checklist
- Sandbox OAuth apps with staging redirect URIs only
- Google test users on consent screen (External Testing mode)
- Redirect flow default in CI; popup documented for headed jobs
- No personal developer OAuth tokens in repo
- Per-run users for post-login Arrange specs
- Probe
/api/mewithauthProviderandemailVerified - Nightly label for full provider redirect specs
Anti-patterns
| Anti-pattern | Why it fails | Better approach |
|---|---|---|
| Developer's personal Gmail in CI | 2FA, lockout, ToS | Dedicated test account |
| Popup OAuth in headless CI | Blocked popups hang | Redirect or API Arrange |
| Assert provider button label | "Sign in with Google" i18n | Probe session |
| Skip denied consent | Users stuck logged out | Error path spec |
| Ignore account linking | Duplicate billing accounts | Link flow probes |
| OAuth test only, never email | Prod mix differs | TrueCoverage on provider |
waitForTimeout after callback | Race on token exchange | waitForURL + probe |
Example scenario
Situation: User signed up with email/password, then tries Google with the same email.
Expected outcome: Account links to one identity—or clear UX to link—no duplicate subscription.
Why UI-only automation breaks: Google button hidden after email signup but new Google account with same email creates second user with access.
- Arrange: Seed email user runId@test.local with active subscription.
- Act: Complete Google OAuth with same email (nightly) or call link-provider test helper.
- Assert: Probe single uid; subscription count 1; /api/billing shows one customer. Attempt duplicate login returns same uid.
TestChimp workflow: Instrument sign_in with oauth_provider; compare Google vs GitHub vs email share in prod vs test.
Same Arrange/Act/Assert pattern as expired-coupon checkout.
Connect scenarios to your QA workflow
Capture business rules in markdown test plans and enforce them with seed routes and probe Assert. Link SmartTests with // @Scenario: for requirement traceability. Use /testchimp test on PRs; /testchimp explore on SmartTest paths for non-functional gaps (ExploreChimp).
Related scenarios
- Firebase Authentication — Google/Apple via Firebase SDK
- Auth0 and Okta SSO — enterprise + social connections
- Magic links — passwordless alternative
- MFA / 2FA — step-up after OAuth
- RBAC permissions — default role for OAuth users
- Session timeout — OAuth refresh token lifecycle
External references
- Google OAuth 2.0 for web server apps
- Google OAuth testing
- GitHub OAuth apps
- Apple Sign in with Apple JS
- Microsoft OAuth 2.0 auth code flow
- OAuth 2.0 Security BCP (RFC 9700)
Frequently asked questions
Should I use popup or redirect OAuth in Playwright CI?
Redirect. Popups are blocked or flaky in headless browsers. Configure your app or test env to use redirect on CI=true. Reserve popup specs for headed nightly runs if production requires popups.
How do I test Google login without my personal account?
Create a Google Cloud OAuth client in Testing mode, add dedicated test users to the consent screen, and store credentials in CI secrets. For PR speed, use a seed route that creates an OAuth-linked user and skip the Google UI entirely.
How do I test account linking between email and Google?
Seed an email user, then either automate Google with the same email in a nightly job or use a test-only link-provider API that mirrors your production link logic. Probe that uid and subscription rows remain singular.
What happens when a user denies OAuth consent?
Assert callback error params, no session cookie, /api/me returns 401, and no orphan user created. Many apps forget this path—it causes confusing blank screens in prod.
Can I run OAuth tests in parallel Playwright workers?
Post-login Arrange specs parallelize well with unique runId users. Full Google redirect specs should use one test account per worker or run serially to avoid concurrent consent/session conflicts.
We support Google and GitHub—how do we know both are covered?
Compare oauth_provider distribution in prod vs test via TrueCoverage. If GitHub is 40% of sign-ins but untested, add a GitHub sandbox app and tier-2 nightly redirect spec linked in markdown with // @Scenario:.
How does TestChimp help with OAuth test maintenance?
Use /testchimp init for session bootstrap routes, run /testchimp test on auth PRs, and manual session capture for occasional headed OAuth smoke tests. Evolve adds provider-specific scenarios when TrueCoverage shows gaps—without rewriting every post-login spec.
Apply these patterns in your repo
Run `/testchimp init` to connect TestChimp to your repo, then `/testchimp test` on PRs to turn these patterns into maintained SmartTests. Use `/testchimp evolve` when you want to expand coverage as your app grows.