by Pramod
Automatically generate comprehensive API test suites from OpenAPI specifications covering CRUD operations, error handling, authentication, pagination, and edge cases
npx @qaskills/cli add api-test-suite-generatorAuto-detects your AI agent and installs the skill. Works with Claude Code, Cursor, Copilot, and more.
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.
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
The foundation of automated test generation is reliable spec parsing. Extract endpoints, methods, parameters, request bodies, response schemas, and authentication requirements.
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;
}
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;
}
}
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,
});
}
}
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');
});
});
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);
});
});
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);
}
});
});
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);
});
});
});
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');
}
}
});
});
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));
});
});
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);
});
});
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);
});
});
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;
}
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()));
}
}
// 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/,
},
],
});
// 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: {
name: 'staging',
baseUrl: 'https://staging-api.example.com',
auth: {
adminEmail: process.env.STAGING_ADMIN_EMAIL || '',
adminPassword: process.env.STAGING_ADMIN_PASSWORD || '',
apiKey: process.env.STAGING_API_KEY,
},
timeouts: { request: 30000, suite: 300000 },
features: { rateLimiting: true, fileUploads: true },
},
production: {
name: 'production',
baseUrl: 'https://api.example.com',
auth: {
adminEmail: process.env.PROD_ADMIN_EMAIL || '',
adminPassword: process.env.PROD_ADMIN_PASSWORD || '',
apiKey: process.env.PROD_API_KEY,
},
timeouts: { request: 15000, suite: 600000 },
features: { rateLimiting: true, fileUploads: true },
},
};
export function getEnvironment(): Environment {
const envName = process.env.TEST_ENV || 'local';
return environments[envName] || environments.local;
}
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
const ajv = new Ajv({ allErrors: true });
addFormats(ajv);
export function validateResponseSchema(data: any, schema: OpenApiSchema): {
valid: boolean;
errors: string[];
} {
const validate = ajv.compile(schema);
const valid = validate(data);
return {
valid: valid as boolean,
errors: valid ? [] : validate.errors!.map(
(e) => `${e.instancePath || '/'}: ${e.message}`
),
};
}
// Usage in tests
test('should match OpenAPI response schema', async ({ request }) => {
const response = await request.get(`${BASE_URL}/users/${userId}`, {
headers: { Authorization: `Bearer ${authToken}` },
});
const body = await response.json();
const userSchema = {
type: 'object',
required: ['id', 'name', 'email', 'createdAt'],
properties: {
id: { type: 'string', format: 'uuid' },
name: { type: 'string', minLength: 1 },
email: { type: 'string', format: 'email' },
role: { type: 'string', enum: ['admin', 'editor', 'viewer'] },
createdAt: { type: 'string', format: 'date-time' },
},
additionalProperties: false,
};
const result = validateResponseSchema(body, userSchema as any);
expect(result.valid).toBe(true);
if (!result.valid) {
console.error('Schema validation errors:', result.errors);
}
});
await sleep(5000) with polling loops that check for the expected condition with a timeout. Hard sleeps waste time and still fail intermittently.DEBUG=pw:api for Playwright or add request interceptors that log every HTTP call. This reveals the exact request that caused a failure.response.text() instead of response.json() when debugging parsing errors. The API might return HTML error pages, empty bodies, or malformed JSON.X-Request-ID header in every test request. When investigating server-side logs, this header links test failures to specific server log entries.- name: Install QA Skills
run: npx @qaskills/cli add api-test-suite-generator12 of 29 agents supported