Skip to main content

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_verified is not checked; attacker creates Google account with victim email and accesses billing.
  • Security incidentsstate parameter 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

ScenarioEdge caseWhy tests breakApproach
Popup vs redirectsignInWithPopup blocked headlessHangs waiting for popupRedirect flow in CI; popup in headed nightly
Google OAuth2FA on test accountCannot automate consentSandbox app + dedicated test user OR API Arrange
GitHub OAuthOrg SSO enforcement404 on authorizeTest user outside enforced org
Denied consentUser clicks CancelUnhandled promise rejectionAssert error route + no session
Account linkingEmail account + Google same emailDuplicate uid or merge failureSeed both paths; probe single identity
Different credential same emailPassword user + Googleaccount-exists errorsExplicit link flow spec
Missing state / PKCESecurity regressionTest passes without OAuthNegative unit test + one E2E redirect
Scope downgradeApp requests fewer scopes on re-authFeature breaks silentlyAssert granted scopes in probe
Token refreshProvider refresh token revokedMid-suite logoutRefresh spec with stub
Apple Sign InJS SDK + redirect constraintsWeb vs native splitDocument platform-specific jobs
Microsoft / Azure ADMulti-tenant common vs tenantWrong tenant loginFixture tenant IDs
Email not verified (Google)email_verified: falseShould reject loginNegative probe
One Tap / FedCMAuto prompt blockedFlaky auto loginDisable in test env
Mobile WebViewCustom scheme redirectPlaywright cannot followAPI Arrange for mobile web
Rate limitsToo many token exchanges429 in parallel CICache session per worker
Provider outage simulation503 on token endpointUntested error UXAIMock or MSW stub

Tiered testing strategy

Real OAuth in headless Linux CI is painful. Use tiers:

TierCoverageWhen
Post-login E2ESeed session as OAuth-linked userEvery PR — features after login
Redirect OAuth (sandbox)Full authorize → callbackNightly or weekly
Popup OAuthHeaded Playwright or manual capturePre-release
TrueCoverageProd oauth_provider sharePrioritize 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.

ProviderSandbox setupDocs
GoogleCloud Console → OAuth client, test users on consent screenGoogle OAuth testing
GitHubOAuth App or GitHub App with limited reposGitHub OAuth apps
AppleServices ID + return URLs for stagingSign in with Apple
MicrosoftApp registration in dev tenantMicrosoft 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}`);
});

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

CaseExpected behaviorProbe
Email signup exists, Google same emailOffer link or auto-linkSingle user id; both providers on profile
Email signup exists, Google different emailSeparate accounts OR explicit merge UITwo users OR merged with audit
Google first, then set passwordLink credentialLogin works both ways
Unlink providerCannot orphan account without backupBlock 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');
});

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:

  • state mismatch on callback → error, no session
  • PKCE code_verifier required (if using PKCE)
  • Redirect URI allowlist rejects evil.com
  • email_verified: false from Google rejected if policy requires verification

Use unit tests for exhaustive cases; E2E proves wiring end-to-end.

CI checklist

  1. Sandbox OAuth apps with staging redirect URIs only
  2. Google test users on consent screen (External Testing mode)
  3. Redirect flow default in CI; popup documented for headed jobs
  4. No personal developer OAuth tokens in repo
  5. Per-run users for post-login Arrange specs
  6. Probe /api/me with authProvider and emailVerified
  7. Nightly label for full provider redirect specs

Anti-patterns

Anti-patternWhy it failsBetter approach
Developer's personal Gmail in CI2FA, lockout, ToSDedicated test account
Popup OAuth in headless CIBlocked popups hangRedirect or API Arrange
Assert provider button label"Sign in with Google" i18nProbe session
Skip denied consentUsers stuck logged outError path spec
Ignore account linkingDuplicate billing accountsLink flow probes
OAuth test only, never emailProd mix differsTrueCoverage on provider
waitForTimeout after callbackRace on token exchangewaitForURL + 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.

  1. Arrange: Seed email user runId@test.local with active subscription.
  2. Act: Complete Google OAuth with same email (nightly) or call link-provider test helper.
  3. 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).

External references

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.

Start free on TestChimp · Book a demo