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>
158 lines
5.4 KiB
Markdown
158 lines
5.4 KiB
Markdown
# API Error Responses Template
|
|
|
|
Tests 400, 401, 403, 404, and 500 HTTP error handling.
|
|
|
|
## Prerequisites
|
|
- Valid auth token: `{{apiToken}}`
|
|
- API base: `{{apiBaseUrl}}`
|
|
|
|
---
|
|
|
|
## TypeScript
|
|
|
|
```typescript
|
|
import { test, expect } from '@playwright/test';
|
|
|
|
const validHeaders = {
|
|
'Authorization': `Bearer {{apiToken}}`,
|
|
'Content-Type': 'application/json',
|
|
};
|
|
|
|
test.describe('API Error Responses', () => {
|
|
// 400 Bad Request
|
|
test('POST with invalid body returns 400', async ({ request }) => {
|
|
const res = await request.post('{{apiBaseUrl}}/{{entityName}}s', {
|
|
headers: validHeaders,
|
|
data: { name: '' }, // name too short / blank
|
|
});
|
|
expect(res.status()).toBe(400);
|
|
const body = await res.json();
|
|
expect(body.message ?? body.error).toMatch(/bad request|invalid/i);
|
|
expect(body.errors ?? body.details).toBeDefined();
|
|
});
|
|
|
|
// 401 Unauthorized
|
|
test('request without token returns 401', async ({ request }) => {
|
|
const res = await request.get('{{apiBaseUrl}}/{{entityName}}s');
|
|
expect(res.status()).toBe(401);
|
|
const body = await res.json();
|
|
expect(body.message ?? body.error).toMatch(/unauthorized|authentication/i);
|
|
});
|
|
|
|
// 403 Forbidden
|
|
test('accessing admin endpoint as regular user returns 403', async ({ request }) => {
|
|
const res = await request.get('{{apiBaseUrl}}/admin/users', {
|
|
headers: { 'Authorization': `Bearer {{userToken}}` },
|
|
});
|
|
expect(res.status()).toBe(403);
|
|
const body = await res.json();
|
|
expect(body.message ?? body.error).toMatch(/forbidden|insufficient.*permission/i);
|
|
});
|
|
|
|
// 404 Not Found
|
|
test('GET non-existent resource returns 404', async ({ request }) => {
|
|
const res = await request.get('{{apiBaseUrl}}/{{entityName}}s/999999', { headers: validHeaders });
|
|
expect(res.status()).toBe(404);
|
|
const body = await res.json();
|
|
expect(body.message ?? body.error).toMatch(/not found/i);
|
|
});
|
|
|
|
// 422 Unprocessable Entity
|
|
test('POST with missing required field returns 422', async ({ request }) => {
|
|
const res = await request.post('{{apiBaseUrl}}/{{entityName}}s', {
|
|
headers: validHeaders,
|
|
data: { description: 'no name provided' },
|
|
});
|
|
expect([422, 400]).toContain(res.status());
|
|
const body = await res.json();
|
|
expect(body.errors ?? body.details).toBeDefined();
|
|
});
|
|
|
|
// 429 Too Many Requests (handled in rate-limiting template — kept here for completeness)
|
|
test('returns 429 when rate limit exceeded', async ({ request }) => {
|
|
let lastStatus = 0;
|
|
for (let i = 0; i < {{rateLimitThreshold}} + 1; i++) {
|
|
const res = await request.get('{{apiBaseUrl}}/{{rateLimitedEndpoint}}', { headers: validHeaders });
|
|
lastStatus = res.status();
|
|
if (lastStatus === 429) break;
|
|
}
|
|
expect(lastStatus).toBe(429);
|
|
});
|
|
|
|
// 500 Internal Server Error
|
|
test('server error returns 500 with error body', async ({ page }) => {
|
|
await page.route('{{apiBaseUrl}}/{{entityName}}s', route =>
|
|
route.fulfill({ status: 500, body: JSON.stringify({ error: 'Internal Server Error' }) })
|
|
);
|
|
const res = await page.request.get('{{apiBaseUrl}}/{{entityName}}s', { headers: validHeaders });
|
|
expect(res.status()).toBe(500);
|
|
const body = await res.json();
|
|
expect(body.error ?? body.message).toBeTruthy();
|
|
});
|
|
|
|
// Edge case: error response has consistent shape
|
|
test('all errors return JSON with error field', async ({ request }) => {
|
|
const endpoints = [
|
|
{ method: 'get' as const, url: '{{apiBaseUrl}}/{{entityName}}s/000000', headers: validHeaders },
|
|
{ method: 'get' as const, url: '{{apiBaseUrl}}/{{entityName}}s' },
|
|
];
|
|
for (const ep of endpoints) {
|
|
const res = await request[ep.method](ep.url, { headers: ep.headers });
|
|
if (res.status() >= 400) {
|
|
const body = await res.json();
|
|
expect(body.error ?? body.message ?? body.errors).toBeDefined();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## JavaScript
|
|
|
|
```javascript
|
|
const { test, expect } = require('@playwright/test');
|
|
|
|
const headers = { 'Authorization': `Bearer {{apiToken}}`, 'Content-Type': 'application/json' };
|
|
|
|
test.describe('API Error Responses', () => {
|
|
test('POST with invalid body returns 400', async ({ request }) => {
|
|
const res = await request.post('{{apiBaseUrl}}/{{entityName}}s', {
|
|
headers,
|
|
data: { name: '' },
|
|
});
|
|
expect(res.status()).toBe(400);
|
|
});
|
|
|
|
test('no token returns 401', async ({ request }) => {
|
|
const res = await request.get('{{apiBaseUrl}}/{{entityName}}s');
|
|
expect(res.status()).toBe(401);
|
|
});
|
|
|
|
test('regular user on admin endpoint returns 403', async ({ request }) => {
|
|
const res = await request.get('{{apiBaseUrl}}/admin/users', {
|
|
headers: { 'Authorization': `Bearer {{userToken}}` },
|
|
});
|
|
expect(res.status()).toBe(403);
|
|
});
|
|
|
|
test('non-existent resource returns 404', async ({ request }) => {
|
|
const res = await request.get('{{apiBaseUrl}}/{{entityName}}s/999999', { headers });
|
|
expect(res.status()).toBe(404);
|
|
});
|
|
});
|
|
```
|
|
|
|
## Variants
|
|
| Variant | Description |
|
|
|---------|-------------|
|
|
| 400 Bad Request | Invalid body → 400 + errors detail |
|
|
| 401 Unauthorized | No token → 401 |
|
|
| 403 Forbidden | Wrong role → 403 |
|
|
| 404 Not Found | Missing resource → 404 |
|
|
| 422 Unprocessable | Missing required field → 422/400 |
|
|
| 429 Rate Limit | Threshold exceeded → 429 |
|
|
| 500 Server Error | Mocked 500 → error body present |
|
|
| Consistent shape | All errors have error/message field |
|