Complete Claude Code plugin with: - 9 skills (/pw:init, generate, review, fix, migrate, coverage, testrail, browserstack, report) - 3 specialized agents (test-architect, test-debugger, migration-planner) - 55 test case templates across 11 categories (auth, CRUD, checkout, search, forms, dashboard, settings, onboarding, notifications, API, accessibility) - TestRail MCP server (TypeScript) — 8 tools for bidirectional sync - BrowserStack MCP server (TypeScript) — 7 tools for cross-browser testing - Smart hooks (auto-validate tests, auto-detect Playwright projects) - 6 curated reference docs (golden rules, locators, assertions, fixtures, pitfalls, flaky tests) - Leverages Claude Code built-ins (/batch, /debug, Explore subagent) - Zero-config for core features; TestRail/BrowserStack via env vars - Both TypeScript and JavaScript support throughout Co-authored-by: Leo <leo@openclaw.ai>
149 lines
4.7 KiB
Markdown
149 lines
4.7 KiB
Markdown
# Auth Headers Template
|
|
|
|
Tests token authentication, expired token handling, and token refresh flow.
|
|
|
|
## Prerequisites
|
|
- Valid token: `{{apiToken}}`
|
|
- Expired token: `{{expiredApiToken}}`
|
|
- Refresh token: `{{refreshToken}}`
|
|
- API base: `{{apiBaseUrl}}`
|
|
|
|
---
|
|
|
|
## TypeScript
|
|
|
|
```typescript
|
|
import { test, expect } from '@playwright/test';
|
|
|
|
test.describe('API Auth Headers', () => {
|
|
// Happy path: valid Bearer token accepted
|
|
test('accepts valid Bearer token', async ({ request }) => {
|
|
const res = await request.get('{{apiBaseUrl}}/me', {
|
|
headers: { 'Authorization': `Bearer {{apiToken}}` },
|
|
});
|
|
expect(res.status()).toBe(200);
|
|
const body = await res.json();
|
|
expect(body.id).toBeTruthy();
|
|
});
|
|
|
|
// Happy path: API key in header accepted
|
|
test('accepts API key header', async ({ request }) => {
|
|
const res = await request.get('{{apiBaseUrl}}/{{entityName}}s', {
|
|
headers: { 'X-API-Key': '{{apiKey}}' },
|
|
});
|
|
expect(res.status()).toBe(200);
|
|
});
|
|
|
|
// Error case: no auth header returns 401
|
|
test('returns 401 without auth header', async ({ request }) => {
|
|
const res = await request.get('{{apiBaseUrl}}/me');
|
|
expect(res.status()).toBe(401);
|
|
const body = await res.json();
|
|
expect(body.error ?? body.message).toMatch(/unauthorized|authentication required/i);
|
|
});
|
|
|
|
// Error case: expired token returns 401
|
|
test('returns 401 for expired token', async ({ request }) => {
|
|
const res = await request.get('{{apiBaseUrl}}/me', {
|
|
headers: { 'Authorization': `Bearer {{expiredApiToken}}` },
|
|
});
|
|
expect(res.status()).toBe(401);
|
|
const body = await res.json();
|
|
expect(body.error ?? body.code).toMatch(/token.*expired|expired_token/i);
|
|
});
|
|
|
|
// Happy path: refresh token obtains new access token
|
|
test('refreshes expired token and retries request', async ({ request }) => {
|
|
// Step 1: refresh
|
|
const refresh = await request.post('{{apiBaseUrl}}/auth/refresh', {
|
|
data: { refresh_token: '{{refreshToken}}' },
|
|
});
|
|
expect(refresh.status()).toBe(200);
|
|
const { access_token } = await refresh.json();
|
|
expect(access_token).toBeTruthy();
|
|
|
|
// Step 2: use new token
|
|
const res = await request.get('{{apiBaseUrl}}/me', {
|
|
headers: { 'Authorization': `Bearer ${access_token}` },
|
|
});
|
|
expect(res.status()).toBe(200);
|
|
});
|
|
|
|
// Error case: invalid token format returns 401
|
|
test('returns 401 for malformed token', async ({ request }) => {
|
|
const res = await request.get('{{apiBaseUrl}}/me', {
|
|
headers: { 'Authorization': 'Bearer not.a.jwt' },
|
|
});
|
|
expect(res.status()).toBe(401);
|
|
});
|
|
|
|
// Edge case: token in cookie vs header
|
|
test('accepts session cookie as auth alternative', async ({ request }) => {
|
|
const res = await request.get('{{apiBaseUrl}}/me', {
|
|
headers: { 'Cookie': `{{sessionCookieName}}={{sessionCookieValue}}` },
|
|
});
|
|
expect(res.status()).toBe(200);
|
|
});
|
|
|
|
// Edge case: revoked token returns 401
|
|
test('returns 401 for revoked token', async ({ request }) => {
|
|
const res = await request.get('{{apiBaseUrl}}/me', {
|
|
headers: { 'Authorization': `Bearer {{revokedApiToken}}` },
|
|
});
|
|
expect(res.status()).toBe(401);
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## JavaScript
|
|
|
|
```javascript
|
|
const { test, expect } = require('@playwright/test');
|
|
|
|
test.describe('API Auth Headers', () => {
|
|
test('accepts valid Bearer token', async ({ request }) => {
|
|
const res = await request.get('{{apiBaseUrl}}/me', {
|
|
headers: { 'Authorization': `Bearer {{apiToken}}` },
|
|
});
|
|
expect(res.status()).toBe(200);
|
|
});
|
|
|
|
test('returns 401 without auth header', async ({ request }) => {
|
|
const res = await request.get('{{apiBaseUrl}}/me');
|
|
expect(res.status()).toBe(401);
|
|
});
|
|
|
|
test('returns 401 for expired token', async ({ request }) => {
|
|
const res = await request.get('{{apiBaseUrl}}/me', {
|
|
headers: { 'Authorization': `Bearer {{expiredApiToken}}` },
|
|
});
|
|
expect(res.status()).toBe(401);
|
|
});
|
|
|
|
test('refreshes token and retries', async ({ request }) => {
|
|
const refresh = await request.post('{{apiBaseUrl}}/auth/refresh', {
|
|
data: { refresh_token: '{{refreshToken}}' },
|
|
});
|
|
const { access_token } = await refresh.json();
|
|
const res = await request.get('{{apiBaseUrl}}/me', {
|
|
headers: { 'Authorization': `Bearer ${access_token}` },
|
|
});
|
|
expect(res.status()).toBe(200);
|
|
});
|
|
});
|
|
```
|
|
|
|
## Variants
|
|
| Variant | Description |
|
|
|---------|-------------|
|
|
| Valid Bearer | 200 with user data |
|
|
| API key | X-API-Key header accepted |
|
|
| No auth | 401 + error message |
|
|
| Expired token | 401 + expired error code |
|
|
| Token refresh | New token from refresh endpoint |
|
|
| Malformed token | 401 for non-JWT |
|
|
| Cookie auth | Session cookie accepted |
|
|
| Revoked token | 401 for revoked token |
|