Initial commit: Brachnha Insight project setup
- Next.js 14+ with App Router and TypeScript - Tailwind CSS and ShadCN UI styling - Zustand state management - Dexie.js for IndexedDB (local-first data) - Auth.js v5 for authentication - BMAD framework integration Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
442
_bmad/bmm/testarch/knowledge/api-request.md
Normal file
442
_bmad/bmm/testarch/knowledge/api-request.md
Normal file
@@ -0,0 +1,442 @@
|
||||
# API Request Utility
|
||||
|
||||
## Principle
|
||||
|
||||
Use typed HTTP client with built-in schema validation and automatic retry for server errors. The utility handles URL resolution, header management, response parsing, and single-line response validation with proper TypeScript support. **Works without a browser** - ideal for pure API/service testing.
|
||||
|
||||
## Rationale
|
||||
|
||||
Vanilla Playwright's request API requires boilerplate for common patterns:
|
||||
|
||||
- Manual JSON parsing (`await response.json()`)
|
||||
- Repetitive status code checking
|
||||
- No built-in retry logic for transient failures
|
||||
- No schema validation
|
||||
- Complex URL construction
|
||||
|
||||
The `apiRequest` utility provides:
|
||||
|
||||
- **Automatic JSON parsing**: Response body pre-parsed
|
||||
- **Built-in retry**: 5xx errors retry with exponential backoff
|
||||
- **Schema validation**: Single-line validation (JSON Schema, Zod, OpenAPI)
|
||||
- **URL resolution**: Four-tier strategy (explicit > config > Playwright > direct)
|
||||
- **TypeScript generics**: Type-safe response bodies
|
||||
- **No browser required**: Pure API testing without browser overhead
|
||||
|
||||
## Pattern Examples
|
||||
|
||||
### Example 1: Basic API Request
|
||||
|
||||
**Context**: Making authenticated API requests with automatic retry and type safety.
|
||||
|
||||
**Implementation**:
|
||||
|
||||
```typescript
|
||||
import { test } from '@seontechnologies/playwright-utils/api-request/fixtures';
|
||||
|
||||
test('should fetch user data', async ({ apiRequest }) => {
|
||||
const { status, body } = await apiRequest<User>({
|
||||
method: 'GET',
|
||||
path: '/api/users/123',
|
||||
headers: { Authorization: 'Bearer token' },
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.name).toBe('John Doe'); // TypeScript knows body is User
|
||||
});
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
|
||||
- Generic type `<User>` provides TypeScript autocomplete for `body`
|
||||
- Status and body destructured from response
|
||||
- Headers passed as object
|
||||
- Automatic retry for 5xx errors (configurable)
|
||||
|
||||
### Example 2: Schema Validation (Single Line)
|
||||
|
||||
**Context**: Validate API responses match expected schema with single-line syntax.
|
||||
|
||||
**Implementation**:
|
||||
|
||||
```typescript
|
||||
import { test } from '@seontechnologies/playwright-utils/api-request/fixtures';
|
||||
import { z } from 'zod';
|
||||
|
||||
// JSON Schema validation
|
||||
test('should validate response schema (JSON Schema)', async ({ apiRequest }) => {
|
||||
const { status, body } = await apiRequest({
|
||||
method: 'GET',
|
||||
path: '/api/users/123',
|
||||
validateSchema: {
|
||||
type: 'object',
|
||||
required: ['id', 'name', 'email'],
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
name: { type: 'string' },
|
||||
email: { type: 'string', format: 'email' },
|
||||
},
|
||||
},
|
||||
});
|
||||
// Throws if schema validation fails
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
|
||||
// Zod schema validation
|
||||
const UserSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
});
|
||||
|
||||
test('should validate response schema (Zod)', async ({ apiRequest }) => {
|
||||
const { status, body } = await apiRequest({
|
||||
method: 'GET',
|
||||
path: '/api/users/123',
|
||||
validateSchema: UserSchema,
|
||||
});
|
||||
// Response body is type-safe AND validated
|
||||
expect(status).toBe(200);
|
||||
expect(body.email).toContain('@');
|
||||
});
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
|
||||
- Single `validateSchema` parameter
|
||||
- Supports JSON Schema, Zod, YAML files, OpenAPI specs
|
||||
- Throws on validation failure with detailed errors
|
||||
- Zero boilerplate validation code
|
||||
|
||||
### Example 3: POST with Body and Retry Configuration
|
||||
|
||||
**Context**: Creating resources with custom retry behavior for error testing.
|
||||
|
||||
**Implementation**:
|
||||
|
||||
```typescript
|
||||
test('should create user', async ({ apiRequest }) => {
|
||||
const newUser = {
|
||||
name: 'Jane Doe',
|
||||
email: 'jane@example.com',
|
||||
};
|
||||
|
||||
const { status, body } = await apiRequest({
|
||||
method: 'POST',
|
||||
path: '/api/users',
|
||||
body: newUser, // Automatically sent as JSON
|
||||
headers: { Authorization: 'Bearer token' },
|
||||
});
|
||||
|
||||
expect(status).toBe(201);
|
||||
expect(body.id).toBeDefined();
|
||||
});
|
||||
|
||||
// Disable retry for error testing
|
||||
test('should handle 500 errors', async ({ apiRequest }) => {
|
||||
await expect(
|
||||
apiRequest({
|
||||
method: 'GET',
|
||||
path: '/api/error',
|
||||
retryConfig: { maxRetries: 0 }, // Disable retry
|
||||
}),
|
||||
).rejects.toThrow('Request failed with status 500');
|
||||
});
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
|
||||
- `body` parameter auto-serializes to JSON
|
||||
- Default retry: 5xx errors, 3 retries, exponential backoff
|
||||
- Disable retry with `retryConfig: { maxRetries: 0 }`
|
||||
- Only 5xx errors retry (4xx errors fail immediately)
|
||||
|
||||
### Example 4: URL Resolution Strategy
|
||||
|
||||
**Context**: Flexible URL handling for different environments and test contexts.
|
||||
|
||||
**Implementation**:
|
||||
|
||||
```typescript
|
||||
// Strategy 1: Explicit baseUrl (highest priority)
|
||||
await apiRequest({
|
||||
method: 'GET',
|
||||
path: '/users',
|
||||
baseUrl: 'https://api.example.com', // Uses https://api.example.com/users
|
||||
});
|
||||
|
||||
// Strategy 2: Config baseURL (from fixture)
|
||||
import { test } from '@seontechnologies/playwright-utils/api-request/fixtures';
|
||||
|
||||
test.use({ configBaseUrl: 'https://staging-api.example.com' });
|
||||
|
||||
test('uses config baseURL', async ({ apiRequest }) => {
|
||||
await apiRequest({
|
||||
method: 'GET',
|
||||
path: '/users', // Uses https://staging-api.example.com/users
|
||||
});
|
||||
});
|
||||
|
||||
// Strategy 3: Playwright baseURL (from playwright.config.ts)
|
||||
// playwright.config.ts
|
||||
export default defineConfig({
|
||||
use: {
|
||||
baseURL: 'https://api.example.com',
|
||||
},
|
||||
});
|
||||
|
||||
test('uses Playwright baseURL', async ({ apiRequest }) => {
|
||||
await apiRequest({
|
||||
method: 'GET',
|
||||
path: '/users', // Uses https://api.example.com/users
|
||||
});
|
||||
});
|
||||
|
||||
// Strategy 4: Direct path (full URL)
|
||||
await apiRequest({
|
||||
method: 'GET',
|
||||
path: 'https://api.example.com/users', // Full URL works too
|
||||
});
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
|
||||
- Four-tier resolution: explicit > config > Playwright > direct
|
||||
- Trailing slashes normalized automatically
|
||||
- Environment-specific baseUrl easy to configure
|
||||
|
||||
### Example 5: Integration with Recurse (Polling)
|
||||
|
||||
**Context**: Waiting for async operations to complete (background jobs, eventual consistency).
|
||||
|
||||
**Implementation**:
|
||||
|
||||
```typescript
|
||||
import { test } from '@seontechnologies/playwright-utils/fixtures';
|
||||
|
||||
test('should poll until job completes', async ({ apiRequest, recurse }) => {
|
||||
// Create job
|
||||
const { body } = await apiRequest({
|
||||
method: 'POST',
|
||||
path: '/api/jobs',
|
||||
body: { type: 'export' },
|
||||
});
|
||||
|
||||
const jobId = body.id;
|
||||
|
||||
// Poll until ready
|
||||
const completedJob = await recurse(
|
||||
() => apiRequest({ method: 'GET', path: `/api/jobs/${jobId}` }),
|
||||
(response) => response.body.status === 'completed',
|
||||
{ timeout: 60000, interval: 2000 },
|
||||
);
|
||||
|
||||
expect(completedJob.body.result).toBeDefined();
|
||||
});
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
|
||||
- `apiRequest` returns full response object
|
||||
- `recurse` polls until predicate returns true
|
||||
- Composable utilities work together seamlessly
|
||||
|
||||
### Example 6: Microservice Testing (Multiple Services)
|
||||
|
||||
**Context**: Test interactions between microservices without a browser.
|
||||
|
||||
**Implementation**:
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@seontechnologies/playwright-utils/fixtures';
|
||||
|
||||
const USER_SERVICE = process.env.USER_SERVICE_URL || 'http://localhost:3001';
|
||||
const ORDER_SERVICE = process.env.ORDER_SERVICE_URL || 'http://localhost:3002';
|
||||
|
||||
test.describe('Microservice Integration', () => {
|
||||
test('should validate cross-service user lookup', async ({ apiRequest }) => {
|
||||
// Create user in user-service
|
||||
const { body: user } = await apiRequest({
|
||||
method: 'POST',
|
||||
path: '/api/users',
|
||||
baseUrl: USER_SERVICE,
|
||||
body: { name: 'Test User', email: 'test@example.com' },
|
||||
});
|
||||
|
||||
// Create order in order-service (validates user via user-service)
|
||||
const { status, body: order } = await apiRequest({
|
||||
method: 'POST',
|
||||
path: '/api/orders',
|
||||
baseUrl: ORDER_SERVICE,
|
||||
body: {
|
||||
userId: user.id,
|
||||
items: [{ productId: 'prod-1', quantity: 2 }],
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toBe(201);
|
||||
expect(order.userId).toBe(user.id);
|
||||
});
|
||||
|
||||
test('should reject order for invalid user', async ({ apiRequest }) => {
|
||||
const { status, body } = await apiRequest({
|
||||
method: 'POST',
|
||||
path: '/api/orders',
|
||||
baseUrl: ORDER_SERVICE,
|
||||
body: {
|
||||
userId: 'non-existent-user',
|
||||
items: [{ productId: 'prod-1', quantity: 1 }],
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body.code).toBe('INVALID_USER');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
|
||||
- Test multiple services without browser
|
||||
- Use `baseUrl` to target different services
|
||||
- Validate cross-service communication
|
||||
- Pure API testing - fast and reliable
|
||||
|
||||
### Example 7: GraphQL API Testing
|
||||
|
||||
**Context**: Test GraphQL endpoints with queries and mutations.
|
||||
|
||||
**Implementation**:
|
||||
|
||||
```typescript
|
||||
test.describe('GraphQL API', () => {
|
||||
const GRAPHQL_ENDPOINT = '/graphql';
|
||||
|
||||
test('should query users via GraphQL', async ({ apiRequest }) => {
|
||||
const query = `
|
||||
query GetUsers($limit: Int) {
|
||||
users(limit: $limit) {
|
||||
id
|
||||
name
|
||||
email
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const { status, body } = await apiRequest({
|
||||
method: 'POST',
|
||||
path: GRAPHQL_ENDPOINT,
|
||||
body: {
|
||||
query,
|
||||
variables: { limit: 10 },
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.errors).toBeUndefined();
|
||||
expect(body.data.users).toHaveLength(10);
|
||||
});
|
||||
|
||||
test('should create user via mutation', async ({ apiRequest }) => {
|
||||
const mutation = `
|
||||
mutation CreateUser($input: CreateUserInput!) {
|
||||
createUser(input: $input) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const { status, body } = await apiRequest({
|
||||
method: 'POST',
|
||||
path: GRAPHQL_ENDPOINT,
|
||||
body: {
|
||||
query: mutation,
|
||||
variables: {
|
||||
input: { name: 'GraphQL User', email: 'gql@example.com' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.data.createUser.id).toBeDefined();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
|
||||
- GraphQL via POST request
|
||||
- Variables in request body
|
||||
- Check `body.errors` for GraphQL errors (not status code)
|
||||
- Works for queries and mutations
|
||||
|
||||
## Comparison with Vanilla Playwright
|
||||
|
||||
| Vanilla Playwright | playwright-utils apiRequest |
|
||||
| ---------------------------------------------- | ---------------------------------------------------------------------------------- |
|
||||
| `const resp = await request.get('/api/users')` | `const { status, body } = await apiRequest({ method: 'GET', path: '/api/users' })` |
|
||||
| `const body = await resp.json()` | Response already parsed |
|
||||
| `expect(resp.ok()).toBeTruthy()` | Status code directly accessible |
|
||||
| No retry logic | Auto-retry 5xx errors with backoff |
|
||||
| No schema validation | Built-in multi-format validation |
|
||||
| Manual error handling | Descriptive error messages |
|
||||
|
||||
## When to Use
|
||||
|
||||
**Use apiRequest for:**
|
||||
|
||||
- ✅ Pure API/service testing (no browser needed)
|
||||
- ✅ Microservice integration testing
|
||||
- ✅ GraphQL API testing
|
||||
- ✅ Schema validation needs
|
||||
- ✅ Tests requiring retry logic
|
||||
- ✅ Background API calls in UI tests
|
||||
- ✅ Contract testing support
|
||||
|
||||
**Stick with vanilla Playwright for:**
|
||||
|
||||
- Simple one-off requests where utility overhead isn't worth it
|
||||
- Testing Playwright's native features specifically
|
||||
- Legacy tests where migration isn't justified
|
||||
|
||||
## Related Fragments
|
||||
|
||||
- `api-testing-patterns.md` - Comprehensive pure API testing patterns
|
||||
- `overview.md` - Installation and design principles
|
||||
- `auth-session.md` - Authentication token management
|
||||
- `recurse.md` - Polling for async operations
|
||||
- `fixtures-composition.md` - Combining utilities with mergeTests
|
||||
- `log.md` - Logging API requests
|
||||
- `contract-testing.md` - Pact contract testing
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
**❌ Ignoring retry failures:**
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await apiRequest({ method: 'GET', path: '/api/unstable' });
|
||||
} catch {
|
||||
// Silent failure - loses retry information
|
||||
}
|
||||
```
|
||||
|
||||
**✅ Let retries happen, handle final failure:**
|
||||
|
||||
```typescript
|
||||
await expect(apiRequest({ method: 'GET', path: '/api/unstable' })).rejects.toThrow(); // Retries happen automatically, then final error caught
|
||||
```
|
||||
|
||||
**❌ Disabling TypeScript benefits:**
|
||||
|
||||
```typescript
|
||||
const response: any = await apiRequest({ method: 'GET', path: '/users' });
|
||||
```
|
||||
|
||||
**✅ Use generic types:**
|
||||
|
||||
```typescript
|
||||
const { body } = await apiRequest<User[]>({ method: 'GET', path: '/users' });
|
||||
// body is typed as User[]
|
||||
```
|
||||
Reference in New Issue
Block a user