Real APIs require credentials. Real responses must match a contract. This lecture covers
Basic Auth, OAuth2 client_credentials against Spotify, dynamic token refresh on 401,
and AJV-based schema validation with a custom expect matcher.
01The auth landscapeBasic, Bearer, OAuth2
An API call without credentials gets you anonymous access at best, a 401 at worst.
Real services use one of three patterns: HTTP Basic Auth, opaque Bearer tokens, or
OAuth2 access tokens. Playwright's request fixture supports all three
equally well.
Scheme
Header
Where credentials live
Renewable?
Basic
Authorization: Basic base64(user:pass)
your .env or a secret manager
no, you keep using the same creds
Bearer (opaque)
Authorization: Bearer xyz
issued by an auth endpoint
via a refresh endpoint or a second login
OAuth2 client_credentials
Authorization: Bearer xyz
client_id + client_secret -> token endpoint
yes, with TTL in the response
OAuth2 authorization_code
Authorization: Bearer xyz
user browser flow + redirect
refresh_token
sequenceDiagram
participant T as test
participant API as protected API
Note over T,API: Basic Auth
T->>API: GET /me + Authorization: Basic base64(user:pass)
API-->>T: 200 OR 401
Note over T,API: OAuth2 client_credentials
T->>API: POST /token + client_id + client_secret
API-->>T: 200 { access_token, expires_in }
T->>API: GET /me + Authorization: Bearer access_token
API-->>T: 200
Basic Auth is one request, OAuth2 is two
02Basic Auth in PlaywrighthttpCredentials
Basic Auth is the simplest scheme - the username and password go on every request,
base64-encoded. Playwright handles the encoding for you when you set
httpCredentials on the context.
Basic Auth over HTTPS only. The header is base64, NOT encrypted. Anyone watching plain HTTP can decode demo:pass in milliseconds. Use TLS or pick OAuth2.
The header is one line - everything sits in the value
03OAuth2 client_credentials against Spotifytwo-step
Spotify's public Web API ships with a free client credentials grant.
You register a small app on the developer dashboard, get a client_id and
a client_secret, exchange them for an access token, then use the token
to read public data such as new releases.
sequenceDiagram
participant T as test
participant A as accounts.spotify.com
participant API as api.spotify.com
T->>A: POST /api/token (grant_type=client_credentials)
Note over T,A: Authorization: Basic base64(id:secret)
A-->>T: 200 { access_token, expires_in: 3600 }
T->>API: GET /v1/browse/new-releases?limit=20
Note over T,API: Authorization: Bearer access_token
API-->>T: 200 { albums: { items: [...] } }
OAuth2 client_credentials - one POST for a token, then Bearer on every call
04Dynamic token refresh on 401resilience
Tokens expire. A long-running suite that grabbed a token at globalSetup
may see it expire halfway through. The classic recovery is: on 401, fetch a fresh
token and retry the request once.
flowchart TB
S[start] --> C[get token if missing]
C --> R[GET /v1/browse/new-releases]
R -->|200| OK[return body]
R -->|401| REF[fetch fresh token]
REF --> R2[retry GET]
R2 -->|200| OK
R2 -->|401| FAIL[fail loudly]
Refresh once. If still 401, the credentials themselves are wrong
Why retry only once? If the credentials are wrong, retrying forever just hammers the auth server and locks the client. A single retry covers the "expired between request and now" race; everything else is a config bug.
05AJV - install and instantiateajv + ajv-formats
AJV is the fastest JSON Schema validator in the Node.js ecosystem. It compiles a
schema once into a JavaScript function, then validates instances against it in
microseconds.
AJV compiles each schema once into a tight JS function
06Custom matcher - expect.extendtoMatchSchema
Calling ajv.validate(schema, body) directly works but yields ugly error
messages. A custom expect matcher integrates with Playwright's reporter
and gives you a clean failure with the full validation path.
tsconfig.json must allow importing JSON: set
"resolveJsonModule": true under compilerOptions.
09Drift detectioncontract vs prod
Schema drift means a field changed in production without the schema being updated.
Catch it by running the schema test against production daily and surfacing failures.
One extra field in prod, no matching schema entry - AJV blocks the merge
Tighten or loosen.additionalProperties: false is strict - useful for contract drift. additionalProperties: true is loose - good for forward compatibility on consumer-side tests. Pick one per direction.
DDrills - eight auth + schema exercises
1Basic Auth happy path
Write a test that uses httpCredentials to call httpbin.org/basic-auth/demo/pass and asserts authenticated: true.
Hint
Set httpCredentials: { username, password } on request.newContext().
2Basic Auth bad password
Send the wrong password to the same endpoint. Assert status 401 and that the body is empty.
Hint
Use the same path but a different password literal.
3Spotify token
Register a free app on the Spotify developer dashboard, put the id and secret in .env, and write a test that fetches a token and asserts expires_in: 3600.
Hint
Use process.env through dotenv-cli or Playwright's --env flag.
4Twenty new releases
Get a Spotify token and GET /v1/browse/new-releases?limit=20. Assert exactly twenty items and that each item has a non-empty name.
Hint
Map and assert with expect.arrayContaining or a small loop.
5Refresh on 401
Build the getWithRefresh() helper from section 4. Force a 401 by passing a junk token first, then confirm the retry succeeds with a fresh one.
Hint
Override currentToken to 'expired-fake' for the first call.
6Author the email schema
Write email.schema.json with a single email field of format: email. Validate { email: '[email protected]' } passes and { email: 'not-an-email' } fails.
Hint
Don't forget $schema and required.
7Schema for booking
Wire the booking schema into the booking spec. Run it against three different booking ids and assert all three match.
Hint
Loop over [1, 2, 3] with test.describe.parallel or generated tests.
8Drift report
Set additionalProperties: false. Manually add extra: 'x' to the response in a stub. Confirm AJV flags it. Then revert and confirm the real response still passes.
Hint
Use route.fulfill from lecture 3 - or stub via a wrapper.