dánwòdánwò

Identity

Google

Sign in with Google on accounts.google.com, with seeded accounts, OAuth clients, tokens that refresh and access a user can revoke mid-test.

danwo's Google emulator answers Google's real hosts: the sign-in page, tokens, OpenID Connect, and the Gmail, Calendar and Drive APIs. googleapis, google-auth-library and any OpenID Connect library work against it unchanged.

This page covers signing in. Gmail, Calendar and Drive have their own page, Gmail, Calendar, Drive.

Set up

Add googleService with your OAuth client and the accounts that may sign in. Hand the app the same client:

danwo.config.ts
import { appEnvironment, defineConfig, googleService } from "@danwo/sdk"

const url = "http://localhost:3000"
const client = {
  client_id: "danwo-client.apps.googleusercontent.com",
  client_secret: "danwo-client-secret",
  name: "My app",
  redirect_uris: [`${url}/auth/google/callback`],
}

export default defineConfig({
  testDir: "tests",
  environment: () =>
    appEnvironment({
      app: {
        command: "npm run dev",
        url,
        env: { GOOGLE_CLIENT_ID: client.client_id, GOOGLE_CLIENT_SECRET: client.client_secret },
      },
      services: [
        googleService({
          seed: {
            oauth_clients: [client],
            users: [{ email: "[email protected]", name: "Ada Lovelace" }],
          },
        }),
      ],
    }),
})

An unknown client gets invalid_client, and a redirect URI that isn't listed gets redirect_uri_mismatch, as on Google.

Seed it

FieldWhat it seeds
usersGoogle accounts by email, with name, picture, locale and Workspace domain.
oauth_clientsYour OAuth clients, with their redirect URIs.
Mail, events, filesEach account's data; see Gmail, Calendar, Drive.

Use it in a test

The sign-in page lists the seeded accounts as buttons named "Continue as" and the account's name:

tests/sign-in.danwo.ts
import { type GoogleFixtures, googleSignIns, test as base } from "@danwo/sdk"

const test = base.extend<GoogleFixtures>()

test("signs in with Google", async ({ app, screen, expect, google }) => {
  await app.open("/")
  await screen.getByRole("button", { name: "Sign in with Google" }).click()
  await screen.getByRole("button", { name: "Continue as Ada Lovelace" }).click()

  await expect(screen.getByText("Hello, Ada")).toBeVisible()

  const signIn = googleSignIns(google).latest()

  expect(signIn?.email).toBe("[email protected]")
  expect(signIn?.redeemed).toBe(true)
})

googleSignIns(google) lists who signed in to which client, with the scope granted, whether the app exchanged the code and how many access tokens it was given. To test what happens when a user removes your app from their Google Account, call revokeGoogleAccess(google, email): their tokens stop working and refreshes fail with invalid_grant.

What's covered

AreaCovered
Sign-inThe account chooser, consent, cancelling, offline access and incremental scopes.
TokensCode exchange, refresh, revoke, and ID tokens signed with the published keys.
OpenID ConnectDiscovery, signing keys and user info.
ScopesEvery Gmail, Calendar and Drive call checks the token's scopes.

Faults

CallFaults the matrix tries
POST oauth2.googleapis.com/token500, 503, 400, a 10-second delay, connection reset, answer lost after Google issued the token
Gmail, Calendar and Drive callssee Gmail, Calendar, Drive

Fidelity

Public endpoints only. danwo fidelity google compares the discovery document and the signing keys with Google's. Both match. Calls that need a signed-in account aren't checked live.

Not covered yet

  • One Tap and the Google Identity Services script
  • Service accounts and domain-wide delegation
  • Google's people, contacts and admin APIs

Next

On this page

Run it on every pull request

A GitHub check and one comment per pull request, with the flight records attached.

Set up CI