API Test Suite Generator Skill
You are an expert QA automation engineer specializing in generating comprehensive API test suites from OpenAPI (Swagger) specifications. When the user asks you to generate, review, or enhance API tests from an OpenAPI spec, follow these detailed instructions.
Core Principles
- Spec-driven testing -- The OpenAPI specification is the single source of truth. Every test should trace back to a documented endpoint, schema, or constraint in the spec.
- Complete CRUD coverage -- Generate tests for all Create, Read, Update, and Delete operations for every resource. Never leave an endpoint untested.
- Negative testing first -- Error paths outnumber happy paths. For every successful scenario, generate at least three failure scenarios covering invalid input, missing authentication, and resource conflicts.
- Contract fidelity -- Validate response schemas strictly against the OpenAPI definitions. A 200 response with a missing required field is a test failure.
- Environment independence -- Tests must run against any environment (local, staging, production) by externalizing base URLs, credentials, and test data.
- Idempotent test suites -- Each test run should leave the system in the same state it found it. Create what you need, clean up what you created.
- Deterministic ordering -- Tests should not depend on execution order. Use setup and teardown hooks to establish preconditions explicitly.
Project Structure
Organize your API test suite with clear separation between configuration, test logic, and utilities:
tests/
api/
specs/
openapi.yaml
openapi.json
generated/
users.api.spec.ts
products.api.spec.ts
orders.api.spec.ts
auth.api.spec.ts
helpers/
api-client.ts
schema-validator.ts
auth-helper.ts
pagination-helper.ts
test-data-factory.ts
fixtures/
users.fixture.ts
products.fixture.ts
config/
environments.ts
api.config.ts
postman/
collection.json
environment.json
rest-assured/
src/test/java/api/
UsersApiTest.java
ProductsApiTest.java
BaseApiTest.java
playwright.config.ts
OpenAPI Spec Parsing
The foundation of automated test generation is reliable spec parsing. Extract endpoints, methods, parameters, request bodies, response schemas, and authentication requirements.
Parsing an OpenAPI Specification
import * as fs from 'fs';
import * as yaml from 'js-yaml';
interface OpenApiEndpoint {
path: string;
method: string;
operationId: string;
summary: string;
parameters: OpenApiParameter[];
requestBody?: OpenApiRequestBody;
responses: Record<string, OpenApiResponse>;
security: OpenApiSecurity[];
tags: string[];
}
interface OpenApiParameter {
name: string;
in: 'query' | 'path' | 'header' | 'cookie';
required: boolean;
schema: OpenApiSchema;
description?: string;
}
interface OpenApiSchema {
type: string;
format?: string;
enum?: string[];
minimum?: number;
maximum?: number;
minLength?: number;
maxLength?: number;
pattern?: string;
required?: string[];
properties?: Record<string, OpenApiSchema>;
items?: OpenApiSchema;
}
interface OpenApiRequestBody {
required: boolean;
content: Record<string, { schema: OpenApiSchema }>;
}
interface OpenApiResponse {
description: string;
content?: Record<string, { schema: OpenApiSchema }>;
}
interface OpenApiSecurity {
[scheme: string]: string[];
}
function parseOpenApiSpec(filePath: string): OpenApiEndpoint[] {
const content = fs.readFileSync(filePath, 'utf-8');
const spec = filePath.endsWith('.yaml') || filePath.endsWith('.yml')
? yaml.load(content) as any
: JSON.parse(content);
const endpoints: OpenApiEndpoint[] = [];
for (const [path, methods] of Object.entries(spec.paths || {})) {
for (const [method, operation] of Object.entries(methods as Record<string, any>)) {
if (['get', 'post', 'put', 'patch', 'delete'].includes(method)) {
endpoints.push({
path,
method: method.toUpperCase(),
operationId: operation.operationId || `${method}_${path}`,
summary: operation.summary || '',
parameters: [
...(spec.paths[path].parameters || []),
...(operation.parameters || []),
],
requestBody: operation.requestBody,
responses: operation.responses || {},
security: operation.security || spec.security || [],
tags: operation.tags || [],
});
}
}
}
return endpoints;
}
function resolveRef(spec: any, ref: string): any {
const parts = ref.replace('#/', '').split('/');
let current = spec;
for (const part of parts) {
current = current[part];
}
return current;
}
Schema-Based Test Data Generation
import { faker } from '@faker-js/faker';
function generateTestData(schema: OpenApiSchema): any {
if (!schema) return undefined;
switch (schema.type) {
case 'string':
return generateStringValue(schema);
case 'integer':
case 'number':
return generateNumericValue(schema);
case 'boolean':
return faker.datatype.boolean();
case 'array':
return [generateTestData(schema.items!)];
case 'object':
const obj: Record<string, any> = {};
for (const [key, propSchema] of Object.entries(schema.properties || {})) {
obj[key] = generateTestData(propSchema);
}
return obj;
default:
return null;
}
}
function generateStringValue(schema: OpenApiSchema): string {
if (schema.enum) return schema.enum[0];
switch (schema.format) {
case 'email': return faker.internet.email();
case 'uri':
case 'url': return faker.internet.url();
case 'uuid': return faker.string.uuid();
case 'date': return faker.date.recent().toISOString().split('T')[0];
case 'date-time': return faker.date.recent().toISOString();
case 'password': return faker.internet.password({ length: 16 });
default:
const minLen = schema.minLength || 1;
const maxLen = schema.maxLength || 50;
return faker.string.alpha({ length: { min: minLen, max: maxLen } });
}
}
function generateNumericValue(schema: OpenApiSchema): number {
const min = schema.minimum ?? 0;
const max = schema.maximum ?? 10000;
return schema.type === 'integer'
? faker.number.int({ min, max })
: faker.number.float({ min, max, fractionDigits: 2 });
}
function generateInvalidTestData(schema: OpenApiSchema): any {
switch (schema.type) {
case 'string':
if (schema.minLength) return '';
if (schema.maxLength) return 'x'.repeat(schema.maxLength + 100);
if (schema.format === 'email') return 'not-an-email';
if (schema.enum) return 'INVALID_ENUM_VALUE';
return 12345; // wrong type
case 'integer':
case 'number':
if (schema.minimum !== undefined) return schema.minimum - 1;
if (schema.maximum !== undefined) return schema.maximum + 1;
return 'not-a-number';
case 'boolean':
return 'not-a-boolean';
case 'array':
return 'not-an-array';
default:
return null;
}
}
Automatic CRUD Test Generation with Playwright
API Client Setup
import { test, expect, APIRequestContext, APIResponse } from '@playwright/test';
interface ApiConfig {
baseUrl: string;
authToken?: string;
defaultHeaders?: Record<string, string>;
timeout?: number;
}
class ApiClient {
private request: APIRequestContext;
private config: ApiConfig;
constructor(request: APIRequestContext, config: ApiConfig) {
this.request = request;
this.config = config;
}
private get headers(): Record<string, string> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'Accept': 'application/json',
...this.config.defaultHeaders,
};
if (this.config.authToken) {
headers['Authorization'] = `Bearer ${this.config.authToken}`;
}
return headers;
}
async get(path: string, params?: Record<string, string>): Promise<APIResponse> {
return this.request.get(`${this.config.baseUrl}${path}`, {
headers: this.headers,
params,
timeout: this.config.timeout || 30000,
});
}
async post(path: string, data: any): Promise<APIResponse> {
return this.request.post(`${this.config.baseUrl}${path}`, {
headers: this.headers,
data,
timeout: this.config.timeout || 30000,
});
}
async put(path: string, data: any): Promise<APIResponse> {
return this.request.put(`${this.config.baseUrl}${path}`, {
headers: this.headers,
data,
timeout: this.config.timeout || 30000,
});
}
async patch(path: string, data: any): Promise<APIResponse> {
return this.request.patch(`${this.config.baseUrl}${path}`, {
headers: this.headers,
data,
timeout: this.config.timeout || 30000,
});
}
async delete(path: string): Promise<APIResponse> {
return this.request.delete(`${this.config.baseUrl}${path}`, {
headers: this.headers,
timeout: this.config.timeout || 30000,
});
}
}
Generated CRUD Test Suite
import { test, expect } from '@playwright/test';
const BASE_URL = process.env.API_BASE_URL || 'http://localhost:3000/api';
test.describe('Users API - CRUD Operations', () => {
let authToken: string;
let createdUserId: string;
test.beforeAll(async ({ request }) => {
const loginResponse = await request.post(`${BASE_URL}/auth/login`, {
data: { email: 'admin@test.com', password: 'TestPass123!' },
});
const loginBody = await loginResponse.json();
authToken = loginBody.token;
});
test('POST /users - should create a new user', async ({ request }) => {
const userData = {
name: 'Jane Doe',
email: `jane.doe.${Date.now()}@example.com`,
role: 'editor',
};
const response = await request.post(`${BASE_URL}/users`, {
headers: { Authorization: `Bearer ${authToken}` },
data: userData,
});
expect(response.status()).toBe(201);
const body = await response.json();
expect(body).toHaveProperty('id');
expect(body.name).toBe(userData.name);
expect(body.email).toBe(userData.email);
expect(body.role).toBe(userData.role);
expect(body).toHaveProperty('createdAt');
expect(body).not.toHaveProperty('password');
createdUserId = body.id;
});
test('GET /users/:id - should retrieve the created user', async ({ request }) => {
const response = await request.get(`${BASE_URL}/users/${createdUserId}`, {
headers: { Authorization: `Bearer ${authToken}` },
});
expect(response.status()).toBe(200);
const body = await response.json();
expect(body.id).toBe(createdUserId);
expect(body).toHaveProperty('name');
expect(body).toHaveProperty('email');
});
test('GET /users - should list users with pagination', async ({ request }) => {
const response = await request.get(`${BASE_URL}/users`, {
headers: { Authorization: `Bearer ${authToken}` },
params: { page: '1', limit: '10' },
});
expect(response.status()).toBe(200);
const body = await response.json();
expect(body).toHaveProperty('data');
expect(body).toHaveProperty('meta');
expect(Array.isArray(body.data)).toBe(true);
expect(body.meta).toHaveProperty('total');
expect(body.meta).toHaveProperty('page');
expect(body.meta).toHaveProperty('limit');
expect(body.data.length).toBeLessThanOrEqual(10);
});
test('PUT /users/:id - should update the user', async ({ request }) => {
const updateData = { name: 'Jane Updated' };
const response = await request.put(`${BASE_URL}/users/${createdUserId}`, {
headers: { Authorization: `Bearer ${authToken}` },
data: updateData,
});
expect(response.status()).toBe(200);
const body = await response.json();
expect(body.name).toBe('Jane Updated');
expect(body.id).toBe(createdUserId);
});
test('PATCH /users/:id - should partially update the user', async ({ request }) => {
const patchData = { role: 'admin' };
const response = await request.patch(`${BASE_URL}/users/${createdUserId}`, {
headers: { Authorization: `Bearer ${authToken}` },
data: patchData,
});
expect(response.status()).toBe(200);
const body = await response.json();
expect(body.role).toBe('admin');
});
test('DELETE /users/:id - should delete the user', async ({ request }) => {
const response = await request.delete(`${BASE_URL}/users/${createdUserId}`, {
headers: { Authorization: `Bearer ${authToken}` },
});
expect(response.status()).toBe(204);
});
test('GET /users/:id - should return 404 for deleted user', async ({ request }) => {
const response = await request.get(`${BASE_URL}/users/${createdUserId}`, {
headers: { Authorization: `Bearer ${authToken}` },
});
expect(response.status()).toBe(404);
const body = await response.json();
expect(body).toHaveProperty('error');
});
});
Authentication Flow Testing
Testing Multiple Auth Schemes
test.describe('Authentication Flow Tests', () => {
test('should reject requests without authentication', async ({ request }) => {
const response = await request.get(`${BASE_URL}/users`);
expect(response.status()).toBe(401);
const body = await response.json();
expect(body.error).toContain('authentication');
});
test('should reject requests with expired token', async ({ request }) => {
const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDAwMDAwMDB9.invalid';
const response = await request.get(`${BASE_URL}/users`, {
headers: { Authorization: `Bearer ${expiredToken}` },
});
expect(response.status()).toBe(401);
});
test('should reject requests with malformed token', async ({ request }) => {
const response = await request.get(`${BASE_URL}/users`, {
headers: { Authorization: 'Bearer not.a.valid.jwt' },
});
expect(response.status()).toBe(401);
});
test('should reject requests with wrong auth scheme', async ({ request }) => {
const response = await request.get(`${BASE_URL}/users`, {
headers: { Authorization: 'Basic dXNlcjpwYXNz' },
});
expect(response.status()).toBe(401);
});
test('should enforce role-based access control', async ({ request }) => {
// Login as a regular user
const loginResponse = await request.post(`${BASE_URL}/auth/login`, {
data: { email: 'viewer@test.com', password: 'ViewerPass123!' },
});
const { token } = await loginResponse.json();
// Attempt admin-only operation
const response = await request.delete(`${BASE_URL}/users/some-id`, {
headers: { Authorization: `Bearer ${token}` },
});
expect(response.status()).toBe(403);
const body = await response.json();
expect(body.error).toContain('forbidden');
});
test('should handle OAuth2 token refresh', async ({ request }) => {
const refreshResponse = await request.post(`${BASE_URL}/auth/refresh`, {
data: { refreshToken: process.env.TEST_REFRESH_TOKEN },
});
expect(refreshResponse.status()).toBe(200);
const body = await refreshResponse.json();
expect(body).toHaveProperty('accessToken');
expect(body).toHaveProperty('refreshToken');
expect(body).toHaveProperty('expiresIn');
expect(typeof body.expiresIn).toBe('number');
});
test('should handle API key authentication', async ({ request }) => {
const response = await request.get(`${BASE_URL}/public/data`, {
headers: { 'X-API-Key': process.env.TEST_API_KEY || '' },
});
expect(response.status()).toBe(200);
});
});
Pagination Testing
test.describe('Pagination Tests', () => {
test('should return default page size when no limit specified', async ({ request }) => {
const response = await request.get(`${BASE_URL}/products`, {
headers: { Authorization: `Bearer ${authToken}` },
});
expect(response.status()).toBe(200);
const body = await response.json();
expect(body.data.length).toBeLessThanOrEqual(20); // default limit
expect(body.meta.limit).toBe(20);
});
test('should paginate through all results correctly', async ({ request }) => {
const allItems: any[] = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await request.get(`${BASE_URL}/products`, {
headers: { Authorization: `Bearer ${authToken}` },
params: { page: String(page), limit: '5' },
});
const body = await response.json();
allItems.push(...body.data);
hasMore = body.data.length === 5 && allItems.length < body.meta.total;
page++;
}
// Verify no duplicates across pages
const ids = allItems.map((item) => item.id);
const uniqueIds = new Set(ids);
expect(uniqueIds.size).toBe(ids.length);
});
test('should return empty array for page beyond total', async ({ request }) => {
const response = await request.get(`${BASE_URL}/products`, {
headers: { Authorization: `Bearer ${authToken}` },
params: { page: '99999', limit: '10' },
});
expect(response.status()).toBe(200);
const body = await response.json();
expect(body.data).toEqual([]);
expect(body.meta.page).toBe(99999);
});
test('should reject invalid pagination parameters', async ({ request }) => {
const response = await request.get(`${BASE_URL}/products`, {
headers: { Authorization: `Bearer ${authToken}` },
params: { page: '-1', limit: '0' },
});
expect(response.status()).toBe(400);
});
test('should enforce maximum page size', async ({ request }) => {
const response = await request.get(`${BASE_URL}/products`, {
headers: { Authorization: `Bearer ${authToken}` },
params: { page: '1', limit: '10000' },
});
const body = await response.json();
// API should cap the limit or return 400
expect(body.data.length).toBeLessThanOrEqual(100);
});
test('should support cursor-based pagination', async ({ request }) => {
const firstPage = await request.get(`${BASE_URL}/events`, {
headers: { Authorization: `Bearer ${authToken}` },
params: { limit: '5' },
});
const firstBody = await firstPage.json();
expect(firstBody).toHaveProperty('nextCursor');
if (firstBody.nextCursor) {
const secondPage = await request.get(`${BASE_URL}/events`, {
headers: { Authorization: `Bearer ${authToken}` },
params: { limit: '5', cursor: firstBody.nextCursor },
});
const secondBody = await secondPage.json();
const firstIds = firstBody.data.map((i: any) => i.id);
const secondIds = secondBody.data.map((i: any) => i.id);
const overlap = firstIds.filter((id: string) => secondIds.includes(id));
expect(overlap).toHaveLength(0);
}
});
});
Filtering and Sorting Parameter Testing
test.describe('Filtering and Sorting Tests', () => {
test('should filter by exact field match', async ({ request }) => {
const response = await request.get(`${BASE_URL}/products`, {
headers: { Authorization: `Bearer ${authToken}` },
params: { category: 'electronics' },
});
expect(response.status()).toBe(200);
const body = await response.json();
body.data.forEach((product: any) => {
expect(product.category).toBe('electronics');
});
});
test('should filter by date range', async ({ request }) => {
const response = await request.get(`${BASE_URL}/orders`, {
headers: { Authorization: `Bearer ${authToken}` },
params: {
createdAfter: '2025-01-01',
createdBefore: '2025-12-31',
},
});
expect(response.status()).toBe(200);
const body = await response.json();
body.data.forEach((order: any) => {
const createdAt = new Date(order.createdAt);
expect(createdAt.getFullYear()).toBe(2025);
});
});
test('should sort by field ascending', async ({ request }) => {
const response = await request.get(`${BASE_URL}/products`, {
headers: { Authorization: `Bearer ${authToken}` },
params: { sortBy: 'price', order: 'asc' },
});
expect(response.status()).toBe(200);
const body = await response.json();
for (let i = 1; i < body.data.length; i++) {
expect(body.data[i].price).toBeGreaterThanOrEqual(body.data[i - 1].price);
}
});
test('should sort by field descending', async ({ request }) => {
const response = await request.get(`${BASE_URL}/products`, {
headers: { Authorization: `Bearer ${authToken}` },
params: { sortBy: 'createdAt', order: 'desc' },
});
expect(response.status()).toBe(200);
const body = await response.json();
for (let i = 1; i < body.data.length; i++) {
const current = new Date(body.data[i].createdAt).getTime();
const previous = new Date(body.data[i - 1].createdAt).getTime();
expect(current).toBeLessThanOrEqual(previous);
}
});
test('should handle search/text filter', async ({ request }) => {
const response = await request.get(`${BASE_URL}/products`, {
headers: { Authorization: `Bearer ${authToken}` },
params: { search: 'laptop' },
});
expect(response.status()).toBe(200);
const body = await response.json();
body.data.forEach((product: any) => {
const matchesName = product.name.toLowerCase().includes('laptop');
const matchesDesc = product.description.toLowerCase().includes('laptop');
expect(matchesName || matchesDesc).toBe(true);
});
});
test('should combine multiple filters', async ({ request }) => {
const response = await request.get(`${BASE_URL}/products`, {
headers: { Authorization: `Bearer ${authToken}` },
params: {
category: 'electronics',
minPrice: '100',
maxPrice: '500',
sortBy: 'price',
order: 'asc',
},
});
expect(response.status()).toBe(200);
const body = await response.json();
body.data.forEach((product: any) => {
expect(product.category).toBe('electronics');
expect(product.price).toBeGreaterThanOrEqual(100);
expect(product.price).toBeLessThanOrEqual(500);
});
});
});
Error Response Validation
test.describe('Error Response Validation', () => {
test('400 Bad Request - invalid request body', async ({ request }) => {
const response = await request.post(`${BASE_URL}/users`, {
headers: { Authorization: `Bearer ${authToken}` },
data: { email: 'not-an-email', name: '' },
});
expect(response.status()).toBe(400);
const body = await response.json();
expect(body).toHaveProperty('error');
expect(body).toHaveProperty('details');
expect(Array.isArray(body.details)).toBe(true);
body.details.forEach((detail: any) => {
expect(detail).toHaveProperty('field');
expect(detail).toHaveProperty('message');
});
});
test('401 Unauthorized - missing credentials', async ({ request }) => {
const response = await request.get(`${BASE_URL}/users`);
expect(response.status()).toBe(401);
const body = await response.json();
expect(body.error).toBeDefined();
expect(response.headers()['www-authenticate']).toBeDefined();
});
test('403 Forbidden - insufficient permissions', async ({ request }) => {
const response = await request.delete(`${BASE_URL}/admin/settings`, {
headers: { Authorization: `Bearer ${regularUserToken}` },
});
expect(response.status()).toBe(403);
});
test('404 Not Found - nonexistent resource', async ({ request }) => {
const response = await request.get(`${BASE_URL}/users/nonexistent-id-12345`, {
headers: { Authorization: `Bearer ${authToken}` },
});
expect(response.status()).toBe(404);
const body = await response.json();
expect(body.error).toContain('not found');
});
test('409 Conflict - duplicate resource', async ({ request }) => {
const userData = { name: 'Duplicate', email: 'existing@test.com' };
// Create the first user
await request.post(`${BASE_URL}/users`, {
headers: { Authorization: `Bearer ${authToken}` },
data: userData,
});
// Attempt to create a duplicate
const response = await request.post(`${BASE_URL}/users`, {
headers: { Authorization: `Bearer ${authToken}` },
data: userData,
});
expect(response.status()).toBe(409);
const body = await response.json();
expect(body.error).toContain('already exists');
});
test('422 Unprocessable Entity - valid JSON but invalid semantics', async ({ request }) => {
const response = await request.post(`${BASE_URL}/orders`, {
headers: { Authorization: `Bearer ${authToken}` },
data: { productId: 'valid-id', quantity: -5 },
});
expect(response.status()).toBe(422);
const body = await response.json();
expect(body).toHaveProperty('error');
});
test('500 Internal Server Error - graceful error handling', async ({ request }) => {
// Trigger a known server error path if available
const response = await request.get(`${BASE_URL}/debug/error`, {
headers: { Authorization: `Bearer ${authToken}` },
});
if (response.status() === 500) {
const body = await response.json();
expect(body).toHaveProperty('error');
// Sensitive details should not be exposed
expect(body).not.toHaveProperty('stack');
expect(body).not.toHaveProperty('query');
}
});
test('should return consistent error format across all endpoints', async ({ request }) => {
const errorEndpoints = [
{ method: 'GET', path: '/users/invalid' },
{ method: 'POST', path: '/users', data: {} },
{ method: 'PUT', path: '/users/invalid', data: {} },
];
for (const endpoint of errorEndpoints) {
const response = endpoint.method === 'GET'
? await request.get(`${BASE_URL}${endpoint.path}`, {
headers: { Authorization: `Bearer ${authToken}` },
})
: await request[endpoint.method.toLowerCase() as 'post' | 'put'](
`${BASE_URL}${endpoint.path}`,
{
headers: { Authorization: `Bearer ${authToken}` },
data: endpoint.data,
}
);
if (response.status() >= 400) {
const body = await response.json();
expect(body).toHaveProperty('error');
expect(typeof body.error).toBe('string');
}
}
});
});
Rate Limiting Tests
test.describe('Rate Limiting Tests', () => {
test('should return rate limit headers', async ({ request }) => {
const response = await request.get(`${BASE_URL}/users`, {
headers: { Authorization: `Bearer ${authToken}` },
});
expect(response.headers()['x-ratelimit-limit']).toBeDefined();
expect(response.headers()['x-ratelimit-remaining']).toBeDefined();
expect(response.headers()['x-ratelimit-reset']).toBeDefined();
});
test('should enforce rate limits with 429 status', async ({ request }) => {
const limit = 100;
let lastResponse;
for (let i = 0; i < limit + 10; i++) {
lastResponse = await request.get(`${BASE_URL}/rate-limited-endpoint`, {
headers: { Authorization: `Bearer ${authToken}` },
});
if (lastResponse.status() === 429) break;
}
expect(lastResponse!.status()).toBe(429);
const body = await lastResponse!.json();
expect(body.error).toContain('rate limit');
expect(lastResponse!.headers()['retry-after']).toBeDefined();
});
test('should reset rate limit after window expires', async ({ request }) => {
// This test may need to be adjusted based on the rate limit window
const response = await request.get(`${BASE_URL}/users`, {
headers: { Authorization: `Bearer ${authToken}` },
});
const remaining = parseInt(response.headers()['x-ratelimit-remaining'] || '0');
const resetTime = parseInt(response.headers()['x-ratelimit-reset'] || '0');
expect(remaining).toBeGreaterThanOrEqual(0);
expect(resetTime).toBeGreaterThan(Math.floor(Date.now() / 1000));
});
});
Request and Response Header Validation
test.describe('Header Validation Tests', () => {
test('should return proper content-type headers', async ({ request }) => {
const response = await request.get(`${BASE_URL}/users`, {
headers: { Authorization: `Bearer ${authToken}` },
});
expect(response.headers()['content-type']).toContain('application/json');
});
test('should support content negotiation', async ({ request }) => {
const response = await request.get(`${BASE_URL}/users`, {
headers: {
Authorization: `Bearer ${authToken}`,
Accept: 'application/xml',
},
});
// API should return 406 if XML not supported, or XML content
expect([200, 406]).toContain(response.status());
});
test('should include CORS headers', async ({ request }) => {
const response = await request.get(`${BASE_URL}/users`, {
headers: {
Authorization: `Bearer ${authToken}`,
Origin: 'https://app.example.com',
},
});
expect(response.headers()['access-control-allow-origin']).toBeDefined();
});
test('should include security headers', async ({ request }) => {
const response = await request.get(`${BASE_URL}/users`, {
headers: { Authorization: `Bearer ${authToken}` },
});
expect(response.headers()['x-content-type-options']).toBe('nosniff');
expect(response.headers()['x-frame-options']).toBeDefined();
});
test('should return proper cache headers', async ({ request }) => {
const response = await request.get(`${BASE_URL}/products`, {
headers: { Authorization: `Bearer ${authToken}` },
});
expect(response.headers()['cache-control']).toBeDefined();
});
test('should reject unsupported content types', async ({ request }) => {
const response = await request.post(`${BASE_URL}/users`, {
headers: {
Authorization: `Bearer ${authToken}`,
'Content-Type': 'text/plain',
},
data: 'name=test',
});
expect(response.status()).toBe(415);
});
});
File Upload Endpoint Testing
import * as fs from 'fs';
import * as path from 'path';
test.describe('File Upload Tests', () => {
test('should upload a file successfully', async ({ request }) => {
const filePath = path.resolve(__dirname, '../fixtures/test-image.png');
const fileBuffer = fs.readFileSync(filePath);
const response = await request.post(`${BASE_URL}/uploads`, {
headers: { Authorization: `Bearer ${authToken}` },
multipart: {
file: {
name: 'test-image.png',
mimeType: 'image/png',
buffer: fileBuffer,
},
description: 'Test upload',
},
});
expect(response.status()).toBe(201);
const body = await response.json();
expect(body).toHaveProperty('url');
expect(body).toHaveProperty('fileSize');
expect(body.mimeType).toBe('image/png');
});
test('should reject files exceeding size limit', async ({ request }) => {
const largeBuffer = Buffer.alloc(50 * 1024 * 1024); // 50MB
const response = await request.post(`${BASE_URL}/uploads`, {
headers: { Authorization: `Bearer ${authToken}` },
multipart: {
file: {
name: 'large-file.bin',
mimeType: 'application/octet-stream',
buffer: largeBuffer,
},
},
});
expect(response.status()).toBe(413);
});
test('should reject unsupported file types', async ({ request }) => {
const response = await request.post(`${BASE_URL}/uploads`, {
headers: { Authorization: `Bearer ${authToken}` },
multipart: {
file: {
name: 'malicious.exe',
mimeType: 'application/x-executable',
buffer: Buffer.from('fake executable content'),
},
},
});
expect(response.status()).toBe(400);
const body = await response.json();
expect(body.error).toContain('file type');
});
test('should handle missing file gracefully', async ({ request }) => {
const response = await request.post(`${BASE_URL}/uploads`, {
headers: {
Authorization: `Bearer ${authToken}`,
'Content-Type': 'multipart/form-data',
},
data: {},
});
expect(response.status()).toBe(400);
});
});
Postman Collection Generation
interface PostmanCollection {
info: { name: string; schema: string };
item: PostmanItem[];
variable: PostmanVariable[];
}
interface PostmanItem {
name: string;
request: {
method: string;
header: { key: string; value: string }[];
url: { raw: string; host: string[]; path: string[] };
body?: { mode: string; raw: string };
};
response: any[];
event?: any[];
}
interface PostmanVariable {
key: string;
value: string;
}
function generatePostmanCollection(
endpoints: OpenApiEndpoint[],
baseUrl: string
): PostmanCollection {
const items: PostmanItem[] = endpoints.map((endpoint) => ({
name: `${endpoint.method} ${endpoint.path} - ${endpoint.summary}`,
request: {
method: endpoint.method,
header: [
{ key: 'Content-Type', value: 'application/json' },
{ key: 'Authorization', value: 'Bearer {{authToken}}' },
],
url: {
raw: `{{baseUrl}}${endpoint.path}`,
host: ['{{baseUrl}}'],
path: endpoint.path.split('/').filter(Boolean),
},
body: endpoint.requestBody
? {
mode: 'raw',
raw: JSON.stringify(
generateTestData(
endpoint.requestBody.content['application/json']?.schema
),
null,
2
),
}
: undefined,
},
response: [],
event: [
{
listen: 'test',
script: {
exec: generatePostmanTests(endpoint),
},
},
],
}));
return {
info: {
name: 'Generated API Tests',
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',
},
item: items,
variable: [
{ key: 'baseUrl', value: baseUrl },
{ key: 'authToken', value: '' },
],
};
}
function generatePostmanTests(endpoint: OpenApiEndpoint): string[] {
const tests: string[] = [];
for (const [statusCode, response] of Object.entries(endpoint.responses)) {
if (statusCode.startsWith('2')) {
tests.push(`pm.test("Status code is ${statusCode}", function () {`);
tests.push(` pm.response.to.have.status(${statusCode});`);
tests.push(`});`);
tests.push(`pm.test("Response has correct content type", function () {`);
tests.push(` pm.response.to.have.header("Content-Type", /application\\/json/);`);
tests.push(`});`);
}
}
return tests;
}
REST Assured Example (Java)
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import io.restassured.response.Response;
import io.restassured.specification.RequestSpecification;
import org.junit.jupiter.api.*;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class UsersApiTest {
private static String authToken;
private static String createdUserId;
@BeforeAll
static void setup() {
RestAssured.baseURI = System.getenv("API_BASE_URL");
if (RestAssured.baseURI == null) {
RestAssured.baseURI = "http://localhost:3000/api";
}
authToken = given()
.contentType(ContentType.JSON)
.body("{\"email\":\"admin@test.com\",\"password\":\"TestPass123!\"}")
.when()
.post("/auth/login")
.then()
.statusCode(200)
.extract()
.path("token");
}
@Test
@Order(1)
void shouldCreateUser() {
String body = """
{
"name": "REST Assured User",
"email": "restassured@test.com",
"role": "editor"
}
""";
createdUserId = given()
.header("Authorization", "Bearer " + authToken)
.contentType(ContentType.JSON)
.body(body)
.when()
.post("/users")
.then()
.statusCode(201)
.body("id", notNullValue())
.body("name", equalTo("REST Assured User"))
.body("email", equalTo("restassured@test.com"))
.body("$", not(hasKey("password")))
.extract()
.path("id");
}
@Test
@Order(2)
void shouldGetUser() {
given()
.header("Authorization", "Bearer " + authToken)
.when()
.get("/users/{id}", createdUserId)
.then()
.statusCode(200)
.body("id", equalTo(createdUserId))
.body("name", notNullValue());
}
@Test
@Order(3)
void shouldListUsersWithPagination() {
given()
.header("Authorization", "Bearer " + authToken)
.queryParam("page", 1)
.queryParam("limit", 10)
.when()
.get("/users")
.then()
.statusCode(200)
.body("data", hasSize(lessThanOrEqualTo(10)))
.body("meta.total", greaterThanOrEqualTo(0))
.body("meta.page", equalTo(1));
}
@Test
@Order(4)
void shouldReturn404ForNonExistentUser() {
given()
.header("Authorization", "Bearer " + authToken)
.when()
.get("/users/{id}", "nonexistent-id")
.then()
.statusCode(404)
.body("error", containsStringIgnoringCase("not found"));
}
@Test
@Order(5)
void shouldReturn400ForInvalidInput() {
given()
.header("Authorization", "Bearer " + authToken)
.contentType(ContentType.JSON)
.body("{\"email\": \"invalid\"}")
.when()
.post("/users")
.then()
.statusCode(400)
.body("details", not(empty()));
}
}
Configuration
Playwright API Configuration
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests/api',
timeout: 30000,
retries: process.env.CI ? 2 : 0,
reporter: [
['html', { outputFolder: 'test-results/api-report' }],
['json', { outputFile: 'test-results/api-results.json' }],
['junit', { outputFile: 'test-results/api-junit.xml' }],
],
use: {
baseURL: process.env.API_BASE_URL || 'http://localhost:3000/api',
extraHTTPHeaders: {
'Accept': 'application/json',
'X-Request-ID': 'playwright-test',
},
trace: 'on-first-retry',
},
projects: [
{
name: 'api-smoke',
testMatch: /.*\.smoke\.spec\.ts/,
},
{
name: 'api-full',
testMatch: /.*\.api\.spec\.ts/,
},
{
name: 'api-contract',
testMatch: /.*\.contract\.spec\.ts/,
},
],
});
Environment Configuration
// tests/api/config/environments.ts
interface Environment {
name: string;
baseUrl: string;
auth: {
adminEmail: string;
adminPassword: string;
apiKey?: string;
};
timeouts: {
request: number;
suite: number;
};
features: {
rateLimiting: boolean;
fileUploads: boolean;
};
}
const environments: Record<string, Environment> = {
local: {
name: 'local',
baseUrl: 'http://localhost:3000/api',
auth: {
adminEmail: 'admin@test.com',
adminPassword: 'TestPass123!',
},
timeouts: { request: 10000, suite: 120000 },
features: { rateLimiting: false, fileUploads: true },
},
staging: {
…(truncated)