@tzurp/openapi-skills 1.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +230 -0
- package/bin/openapi-skills +4 -0
- package/dist/cli.d.ts +37 -0
- package/dist/cli.js +1646 -0
- package/dist/cli.js.map +1 -0
- package/dist/client-schema-builder.d.ts +35 -0
- package/dist/client-schema-builder.js +268 -0
- package/dist/client-schema-builder.js.map +1 -0
- package/dist/helper/base64url-json.d.ts +3 -0
- package/dist/helper/base64url-json.js +28 -0
- package/dist/helper/base64url-json.js.map +1 -0
- package/dist/helper/dotNotation.d.ts +3 -0
- package/dist/helper/dotNotation.js +34 -0
- package/dist/helper/dotNotation.js.map +1 -0
- package/dist/helper/endpoint-filter.d.ts +16 -0
- package/dist/helper/endpoint-filter.js +165 -0
- package/dist/helper/endpoint-filter.js.map +1 -0
- package/dist/helper/endpoint-utils.d.ts +2 -0
- package/dist/helper/endpoint-utils.js +10 -0
- package/dist/helper/endpoint-utils.js.map +1 -0
- package/dist/helper/error-codes.d.ts +27 -0
- package/dist/helper/error-codes.js +116 -0
- package/dist/helper/error-codes.js.map +1 -0
- package/dist/helper/error-formatter.d.ts +46 -0
- package/dist/helper/error-formatter.js +72 -0
- package/dist/helper/error-formatter.js.map +1 -0
- package/dist/helper/get-operation-filter.d.ts +17 -0
- package/dist/helper/get-operation-filter.js +26 -0
- package/dist/helper/get-operation-filter.js.map +1 -0
- package/dist/helper/graphql.d.ts +40 -0
- package/dist/helper/graphql.js +967 -0
- package/dist/helper/graphql.js.map +1 -0
- package/dist/helper/http-client.d.ts +28 -0
- package/dist/helper/http-client.js +90 -0
- package/dist/helper/http-client.js.map +1 -0
- package/dist/helper/json-updater.d.ts +17 -0
- package/dist/helper/json-updater.js +195 -0
- package/dist/helper/json-updater.js.map +1 -0
- package/dist/helper/logger.d.ts +13 -0
- package/dist/helper/logger.js +56 -0
- package/dist/helper/logger.js.map +1 -0
- package/dist/helper/output-size.d.ts +5 -0
- package/dist/helper/output-size.js +7 -0
- package/dist/helper/output-size.js.map +1 -0
- package/dist/helper/parameter-schema.d.ts +14 -0
- package/dist/helper/parameter-schema.js +36 -0
- package/dist/helper/parameter-schema.js.map +1 -0
- package/dist/helper/paths.d.ts +11 -0
- package/dist/helper/paths.js +47 -0
- package/dist/helper/paths.js.map +1 -0
- package/dist/helper/prompt-delete.d.ts +1 -0
- package/dist/helper/prompt-delete.js +13 -0
- package/dist/helper/prompt-delete.js.map +1 -0
- package/dist/helper/request-artifacts.d.ts +14 -0
- package/dist/helper/request-artifacts.js +60 -0
- package/dist/helper/request-artifacts.js.map +1 -0
- package/dist/helper/request-preparation.d.ts +23 -0
- package/dist/helper/request-preparation.js +85 -0
- package/dist/helper/request-preparation.js.map +1 -0
- package/dist/helper/sanitizer.d.ts +1 -0
- package/dist/helper/sanitizer.js +17 -0
- package/dist/helper/sanitizer.js.map +1 -0
- package/dist/helper/update-check.d.ts +1 -0
- package/dist/helper/update-check.js +36 -0
- package/dist/helper/update-check.js.map +1 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +163 -0
- package/dist/index.js.map +1 -0
- package/dist/install-skill.d.ts +5 -0
- package/dist/install-skill.js +139 -0
- package/dist/install-skill.js.map +1 -0
- package/dist/parser.d.ts +12 -0
- package/dist/parser.js +393 -0
- package/dist/parser.js.map +1 -0
- package/dist/validate-response.d.ts +47 -0
- package/dist/validate-response.js +1017 -0
- package/dist/validate-response.js.map +1 -0
- package/package.json +63 -0
- package/skill-templates/SKILL.md +204 -0
- package/skill-templates/create-endpoint-test.md +413 -0
- package/skill-templates/http-client.md +141 -0
- package/skill-templates/write-client-code.md +219 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
# API Operation Test Generation Skill
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
Generate live integration tests for schema operations that exercise and validate the generated client code against a real API.
|
|
5
|
+
|
|
6
|
+
## Core Principle
|
|
7
|
+
**All generated tests MUST call the operation exclusively through the generated client class. NO direct HTTP calls (fetch, axios, supertest, etc.). NO mocks or test doubles.**
|
|
8
|
+
|
|
9
|
+
This ensures:
|
|
10
|
+
- Client code is exercised in realistic scenarios
|
|
11
|
+
- Tests validate actual API behavior, not mocked behavior
|
|
12
|
+
- Tests are maintainable and match client structure
|
|
13
|
+
|
|
14
|
+
## References (Required Dependencies)
|
|
15
|
+
- `/references/write-client-code.md` — Instructions and examples for generating the required client code class
|
|
16
|
+
- `/references/http-client.md` — for FetchClient interface specification
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## File Header Requirement
|
|
21
|
+
|
|
22
|
+
Every generated test file must start with:
|
|
23
|
+
```typescript
|
|
24
|
+
// Generated from <apiName> API schema by openapi-skills.
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Agent Workflow (Decision Tree)
|
|
30
|
+
|
|
31
|
+
**FOLLOW THIS SEQUENCE IN ORDER:**
|
|
32
|
+
|
|
33
|
+
### Step 1: Gather Prerequisites
|
|
34
|
+
1. **Ask for testing framework** (if not specified):
|
|
35
|
+
- Options: Jest, Vitest, Mocha, Node test runner, Playwright
|
|
36
|
+
- Required before proceeding
|
|
37
|
+
2. **Check for client code**:
|
|
38
|
+
- It is mandatory to verify whether the generated client file exists in the workspace
|
|
39
|
+
- If the client code file is missing or incomplete, it is mandatory to generate the client code by following `/references/write-client-code.md` before creating any test
|
|
40
|
+
- Do NOT proceed without client code
|
|
41
|
+
3. **Verify API is parsed** (`openapi-skills get-api-names`):
|
|
42
|
+
- Extract `apiName` from output
|
|
43
|
+
- If not found, offer to parse it
|
|
44
|
+
|
|
45
|
+
### Step 2: Retrieve Operation Metadata
|
|
46
|
+
1. Run `openapi-skills list --api <apiName>` with at least one filter. Valid filters include `--method`, `--path`, `--tag`, `--root-type`, `--filter`, and `--index`. Use the schema-appropriate filters; `--filter`, `--tag`, and `--index` can be combined with either schema.
|
|
47
|
+
2. Extract:
|
|
48
|
+
- OpenAPI: `operationId`, `method`, `path`
|
|
49
|
+
- GraphQL: `name`, `rootType`
|
|
50
|
+
3. Note: If the operation returns multiple response codes or outcomes, plan error tests for each relevant branch
|
|
51
|
+
|
|
52
|
+
### Step 3: Optional Live Data Check
|
|
53
|
+
If a base URL is configured, try `openapi-skills request <operationName> --api <apiName>` once to capture real request/response examples. If the call succeeds, use those artifacts as reference when writing tests. If it fails repeatedly with 400/500 errors, ask the user whether to skip this step and continue with schema-only data, known from the client code in Step 1.
|
|
54
|
+
For multipart or file-upload operations, inspect the request template and make sure the test uses a real file path or file descriptor for the binary field instead of a plain string body. If you know the uploaded file, derive `fileName` and `mimeType` from that file context instead of leaving them blank.
|
|
55
|
+
|
|
56
|
+
### Step 4: Generate Test File
|
|
57
|
+
1. Ensure `/tests` directory exists (create if needed)
|
|
58
|
+
2. Generate tests using rules in next sections
|
|
59
|
+
3. Filename: `<operationName>.test.ts`
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Setup: FetchClient Helper (User Responsibility)
|
|
64
|
+
|
|
65
|
+
**One-time setup per project:**
|
|
66
|
+
1. Copy the `FetchClient` interface from `/references/http-client.md`
|
|
67
|
+
2. Save as a separate file `tests/fetch-client.ts` in your project root
|
|
68
|
+
3. Agent assumes this file exists when generating tests
|
|
69
|
+
|
|
70
|
+
**Config File Reference:**
|
|
71
|
+
- Location: `.openapi-skills/config.json` (relative to project root)
|
|
72
|
+
- Contains API base URL at: `apis.<apiName>.baseUrl`
|
|
73
|
+
- Example:
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"apis": {
|
|
77
|
+
"petstore": {
|
|
78
|
+
"baseUrl": "https://api.petstore.com",
|
|
79
|
+
"auth": { "Authorization": "Bearer token..." }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Test Types (Definitions & Requirements)
|
|
88
|
+
|
|
89
|
+
### 1. Success Test (Happy Path)
|
|
90
|
+
**Definition:** Operation is called with valid input; API returns a successful response.
|
|
91
|
+
|
|
92
|
+
**Generates when:** Always (every operation has at least one success scenario)
|
|
93
|
+
|
|
94
|
+
**What to assert:**
|
|
95
|
+
- Response structure matches client return type (check keys/types exist)
|
|
96
|
+
- Response status is 2xx
|
|
97
|
+
- Key fields have expected values (use client types or API schema constraints)
|
|
98
|
+
- For upload endpoints, assert the request succeeds with a real file fixture and a 2xx response, even if the API does not echo the filename back.
|
|
99
|
+
|
|
100
|
+
**Example triggers:**
|
|
101
|
+
- GET operation with path `{id}` → provide valid ID, expect pet details
|
|
102
|
+
- POST operation → provide valid request body, expect 201 + resource ID
|
|
103
|
+
|
|
104
|
+
**Minimum assertions:**
|
|
105
|
+
```typescript
|
|
106
|
+
const result = await petClient.getPetById(1);
|
|
107
|
+
expect(result).toBeDefined();
|
|
108
|
+
expect(result.id).toBe(1); // or typeof check if value varies
|
|
109
|
+
expect(result.name).toBeDefined();
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 2. Error Test (Failure Scenarios)
|
|
113
|
+
**Definition:** Operation is called with invalid input or encounters an error; API returns an error response.
|
|
114
|
+
|
|
115
|
+
**Generates when:** Operation defines non-2xx responses (400, 404, 500, etc.)
|
|
116
|
+
|
|
117
|
+
**How many:** One test per distinct error response type (one 404 test, one 400 test, etc.)
|
|
118
|
+
|
|
119
|
+
**What to assert:**
|
|
120
|
+
- Response structure matches error type from client (e.g., `FindPetError`)
|
|
121
|
+
- Response status code matches expected error (404, 400, etc.)
|
|
122
|
+
- Error message/details are present
|
|
123
|
+
|
|
124
|
+
**Example triggers:**
|
|
125
|
+
- GET `/pet/{id}` with invalid ID → expect 404 error response
|
|
126
|
+
- POST with malformed body → expect 400 error response
|
|
127
|
+
- API rate limit exceeded → expect 429 error response
|
|
128
|
+
|
|
129
|
+
**Minimum assertions:**
|
|
130
|
+
```typescript
|
|
131
|
+
const result = await petClient.getPetById(999); // invalid ID
|
|
132
|
+
expect(result.error).toBeDefined();
|
|
133
|
+
expect(result.status).toBe(404);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 3. Validation Test (Input Constraints)
|
|
137
|
+
**Definition:** Operation requires specific input validation per API schema (e.g., required fields, min/max values, enum constraints).
|
|
138
|
+
|
|
139
|
+
**Generates when:** API schema includes validation rules (required properties, minLength, minimum value, enum, pattern, etc.)
|
|
140
|
+
|
|
141
|
+
**What to assert:**
|
|
142
|
+
- Omitting required field → API returns 400 error
|
|
143
|
+
- Providing value outside min/max range → API returns 400 error
|
|
144
|
+
- Providing invalid enum value → API returns 400 error
|
|
145
|
+
|
|
146
|
+
**Example triggers:**
|
|
147
|
+
- POST body missing required `name` field → expect 400
|
|
148
|
+
- Query param `age` with value `-5` (minimum is 0) → expect 400
|
|
149
|
+
- Path param with non-numeric value when integer required → expect 400
|
|
150
|
+
|
|
151
|
+
**Minimum assertions:**
|
|
152
|
+
```typescript
|
|
153
|
+
// Test: missing required field
|
|
154
|
+
const result = await petClient.createPet({}); // missing name
|
|
155
|
+
expect(result.error).toBeDefined();
|
|
156
|
+
expect(result.error).toContain('required'); // or similar
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Test Generation Rules
|
|
162
|
+
|
|
163
|
+
### File Structure
|
|
164
|
+
- **Location:** `/tests` directory
|
|
165
|
+
- **Filename:** `<operationName>.test.ts`
|
|
166
|
+
- **First line:** `// Generated from <apiName> API schema by openapi-skills.`
|
|
167
|
+
|
|
168
|
+
### Client Usage Requirement
|
|
169
|
+
- All tests MUST invoke the client's method for the operation under test
|
|
170
|
+
- Import the client class: `import { PetstorePetClient } from '../client/PetstorePetClient'`
|
|
171
|
+
- Instantiate with `FetchClient`: `const client = new PetstorePetClient(httpClient)`
|
|
172
|
+
- Call method: `const result = await client.findPetById(1)`
|
|
173
|
+
- No direct HTTP calls, mocks, or test doubles
|
|
174
|
+
|
|
175
|
+
### FetchClient Setup in Tests
|
|
176
|
+
```typescript
|
|
177
|
+
import fs from 'fs-extra';
|
|
178
|
+
import path from 'path';
|
|
179
|
+
import { FetchClient } from './fetch-client';
|
|
180
|
+
import { PetstorePetClient } from '../client/PetstorePetClient';
|
|
181
|
+
|
|
182
|
+
const config = await fs.readJson(path.resolve('.openapi-skills/config.json'));
|
|
183
|
+
const apiBaseUrl = config.apis.petstore.baseUrl;
|
|
184
|
+
|
|
185
|
+
const httpClient = new FetchClient(apiBaseUrl);
|
|
186
|
+
const petstorePetClient = new PetstorePetClient(httpClient);
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Test Count per Operation
|
|
190
|
+
- **Success test:** Always (1)
|
|
191
|
+
- **Error tests:** One per non-2xx response code defined for the operation (0 to N)
|
|
192
|
+
- **Validation tests:** One per input validation rule if applicable (0 to N)
|
|
193
|
+
- **Minimum:** 1 (success); 2-3 typical
|
|
194
|
+
|
|
195
|
+
### Assertions Strategy
|
|
196
|
+
1. **Structure assertions:** Verify response keys/properties exist
|
|
197
|
+
2. **Type assertions:** Verify values match expected types (number, string, etc.)
|
|
198
|
+
3. **Value assertions:** Use realistic values from API constraints or live request data
|
|
199
|
+
4. **Status assertions:** Explicitly check response status code (2xx for success, specific codes for errors)
|
|
200
|
+
|
|
201
|
+
Example:
|
|
202
|
+
```typescript
|
|
203
|
+
const result = await petClient.getPetById(1);
|
|
204
|
+
expect(result).toBeDefined();
|
|
205
|
+
expect(typeof result.id).toBe('number');
|
|
206
|
+
expect(result.id).toBe(1);
|
|
207
|
+
expect(result.name).toBeDefined();
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Error Handling
|
|
211
|
+
- Derive error response types from client return union (e.g., `FindPetByIdError`)
|
|
212
|
+
- Assert error structure matches client interface
|
|
213
|
+
- Include status code and error message in assertions
|
|
214
|
+
- Do NOT use try/catch for error tests; assert on response object
|
|
215
|
+
|
|
216
|
+
Example:
|
|
217
|
+
```typescript
|
|
218
|
+
const result = await petClient.getPetById(999);
|
|
219
|
+
expect(result.error).toBeDefined();
|
|
220
|
+
expect(result.status).toBe(404);
|
|
221
|
+
expect(result.error.message).toContain('not found');
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Client Variable Naming
|
|
225
|
+
- Use descriptive, operation-specific names
|
|
226
|
+
- Format: `<ApiName><Operation>Client`
|
|
227
|
+
- Examples: `petstorePetClient`, `githubRepoClient`, `stripePaymentClient`
|
|
228
|
+
- NOT: `client`, `c`, `httpClient`, `apiClient`
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Framework Support
|
|
233
|
+
|
|
234
|
+
### Supported Frameworks
|
|
235
|
+
- **Jest** — use `test()`, `expect()`
|
|
236
|
+
- **Vitest** — use `test()`, `expect()`
|
|
237
|
+
- **Mocha** — use `it()`, `expect()` with assertion library
|
|
238
|
+
- **Node test runner** — use `test()`, `assert()`
|
|
239
|
+
- **Playwright** — use `test()`, `expect()` from `@playwright/test`
|
|
240
|
+
|
|
241
|
+
### Framework Agnostic Pattern
|
|
242
|
+
|
|
243
|
+
All frameworks must follow this structure:
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
import fs from 'fs-extra';
|
|
247
|
+
import path from 'path';
|
|
248
|
+
import { FetchClient } from './fetch-client';
|
|
249
|
+
import { PetstorePetClient } from '../client/PetstorePetClient';
|
|
250
|
+
|
|
251
|
+
const config = await fs.readJson(path.resolve('.openapi-skills/config.json'));
|
|
252
|
+
const apiBaseUrl = config.apis.petstore.baseUrl;
|
|
253
|
+
const httpClient = new FetchClient(apiBaseUrl);
|
|
254
|
+
const petstorePetClient = new PetstorePetClient(httpClient);
|
|
255
|
+
|
|
256
|
+
test('getPetById returns pet', async () => {
|
|
257
|
+
const result = await petstorePetClient.getPetById(1);
|
|
258
|
+
expect(result.id).toBe(1);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('getPetById returns 404 for invalid id', async () => {
|
|
262
|
+
const result = await petstorePetClient.getPetById(999);
|
|
263
|
+
expect(result.error).toBeDefined();
|
|
264
|
+
});
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Playwright API Testing
|
|
268
|
+
When user selects Playwright:
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
import { test, expect } from '@playwright/test';
|
|
272
|
+
import fs from 'fs-extra';
|
|
273
|
+
import path from 'path';
|
|
274
|
+
import { FetchClient } from './fetch-client';
|
|
275
|
+
import { PetstorePetClient } from '../client/PetstorePetClient';
|
|
276
|
+
|
|
277
|
+
test('getPetById returns pet', async ({ request }) => {
|
|
278
|
+
const config = await fs.readJson(path.resolve('.openapi-skills/config.json'));
|
|
279
|
+
const apiBaseUrl = config.apis.petstore.baseUrl;
|
|
280
|
+
// Passing Playwright's request:APIRequestContext enables FetchClient.fetchWithContext()
|
|
281
|
+
const httpClient = new FetchClient(apiBaseUrl, request);
|
|
282
|
+
const petstorePetClient = new PetstorePetClient(httpClient);
|
|
283
|
+
const result = await petstorePetClient.getPetById(1);
|
|
284
|
+
expect(result.id).toBe(1);
|
|
285
|
+
});
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Important:** Playwright tests are API-only (not browser/UI tests). Use same client-driven pattern as other frameworks.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## Error Handling & Troubleshooting
|
|
293
|
+
|
|
294
|
+
### Common Issues
|
|
295
|
+
|
|
296
|
+
| Issue | Solution |
|
|
297
|
+
|-------|----------|
|
|
298
|
+
| **FetchClient import fails** | Verify `tests/fetch-client.ts` exists and copied from `/references/http-client.md` |
|
|
299
|
+
| **Config file not found** | Verify `.openapi-skills/config.json` exists in project root |
|
|
300
|
+
| **API base URL wrong** | Check `apis.<apiName>.baseUrl` in config.json matches live API |
|
|
301
|
+
| **Client class import fails** | Verify client code generated first using `/references/write-client-code.md` |
|
|
302
|
+
| **Tests fail at runtime** | Ensure live API is accessible; tests perform real HTTP requests |
|
|
303
|
+
| **Response doesn't match type** | Verify API schema is accurate; report schema issues to API maintainer |
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Examples
|
|
308
|
+
|
|
309
|
+
### Example 1: Jest Test (GET operation)
|
|
310
|
+
```typescript
|
|
311
|
+
// Generated from petstore API schema by openapi-skills.
|
|
312
|
+
import fs from 'fs-extra';
|
|
313
|
+
import path from 'path';
|
|
314
|
+
import { FetchClient } from './fetch-client';
|
|
315
|
+
import { PetstorePetClient } from '../client/PetstorePetClient';
|
|
316
|
+
|
|
317
|
+
const config = await fs.readJson(path.resolve('.openapi-skills/config.json'));
|
|
318
|
+
const apiBaseUrl = config.apis.petstore.baseUrl;
|
|
319
|
+
|
|
320
|
+
describe('PetstorePetClient', () => {
|
|
321
|
+
let httpClient: FetchClient;
|
|
322
|
+
let petstorePetClient: PetstorePetClient;
|
|
323
|
+
|
|
324
|
+
beforeEach(() => {
|
|
325
|
+
httpClient = new FetchClient(apiBaseUrl);
|
|
326
|
+
petstorePetClient = new PetstorePetClient(httpClient);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test('getPetById returns pet for valid id', async () => {
|
|
330
|
+
const result = await petstorePetClient.getPetById(1);
|
|
331
|
+
expect(result).toBeDefined();
|
|
332
|
+
expect(result.id).toBe(1);
|
|
333
|
+
expect(typeof result.name).toBe('string');
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test('getPetById returns 404 for invalid id', async () => {
|
|
337
|
+
const result = await petstorePetClient.getPetById(999);
|
|
338
|
+
expect(result.error).toBeDefined();
|
|
339
|
+
expect(result.status).toBe(404);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Example 2: Vitest Test (POST operation)
|
|
345
|
+
```typescript
|
|
346
|
+
// Generated from petstore API schema by openapi-skills.
|
|
347
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
348
|
+
import fs from 'fs-extra';
|
|
349
|
+
import path from 'path';
|
|
350
|
+
import { FetchClient } from './fetch-client';
|
|
351
|
+
import { PetstorePetClient } from '../client/PetstorePetClient';
|
|
352
|
+
|
|
353
|
+
const config = await fs.readJson(path.resolve('.openapi-skills/config.json'));
|
|
354
|
+
const apiBaseUrl = config.apis.petstore.baseUrl;
|
|
355
|
+
|
|
356
|
+
describe('PetstorePetClient', () => {
|
|
357
|
+
let httpClient: FetchClient;
|
|
358
|
+
let petstorePetClient: PetstorePetClient;
|
|
359
|
+
|
|
360
|
+
beforeEach(() => {
|
|
361
|
+
httpClient = new FetchClient(apiBaseUrl);
|
|
362
|
+
petstorePetClient = new PetstorePetClient(httpClient);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('createPet creates and returns pet', async () => {
|
|
366
|
+
const body = { name: 'Fluffy', status: 'available' };
|
|
367
|
+
const result = await petstorePetClient.createPet(body);
|
|
368
|
+
expect(result.id).toBeDefined();
|
|
369
|
+
expect(result.name).toBe('Fluffy');
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('createPet returns 400 for missing required name', async () => {
|
|
373
|
+
const body = { status: 'available' };
|
|
374
|
+
const result = await petstorePetClient.createPet(body);
|
|
375
|
+
expect(result.error).toBeDefined();
|
|
376
|
+
expect(result.status).toBe(400);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Example 3: Playwright Test (API testing)
|
|
382
|
+
```typescript
|
|
383
|
+
// Generated from petstore API schema by openapi-skills.
|
|
384
|
+
import { test, expect } from '@playwright/test';
|
|
385
|
+
import fs from 'fs-extra';
|
|
386
|
+
import path from 'path';
|
|
387
|
+
import { FetchClient } from './fetch-client';
|
|
388
|
+
import { PetstorePetClient } from '../client/PetstorePetClient';
|
|
389
|
+
|
|
390
|
+
const config = await fs.readJson(path.resolve('.openapi-skills/config.json'));
|
|
391
|
+
const apiBaseUrl = config.apis.petstore.baseUrl;
|
|
392
|
+
|
|
393
|
+
test.describe('PetstorePetClient', () => {
|
|
394
|
+
let httpClient: FetchClient;
|
|
395
|
+
let petstorePetClient: PetstorePetClient;
|
|
396
|
+
|
|
397
|
+
test.beforeEach(() => {
|
|
398
|
+
httpClient = new FetchClient(apiBaseUrl);
|
|
399
|
+
petstorePetClient = new PetstorePetClient(httpClient);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test('getPetById returns pet', async () => {
|
|
403
|
+
const result = await petstorePetClient.getPetById(1);
|
|
404
|
+
expect(result.id).toBe(1);
|
|
405
|
+
expect(result.name).toBeDefined();
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test('getPetById returns 404 for invalid id', async () => {
|
|
409
|
+
const result = await petstorePetClient.getPetById(999);
|
|
410
|
+
expect(result.error).toBeDefined();
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
```
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Shared Fetch Client Helper
|
|
2
|
+
|
|
3
|
+
Copy this file to `tests/fetch-client.ts` and instantiate `FetchClient` in your generated tests.
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
export interface HttpRequestOptionsBase {
|
|
7
|
+
headers?: Record<string, string>;
|
|
8
|
+
query?: Record<string, string | number | boolean | undefined>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface HttpRequestWithBodyOptions extends HttpRequestOptionsBase {
|
|
12
|
+
body?: any;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface HttpClient {
|
|
16
|
+
// Methods WITHOUT body
|
|
17
|
+
get<T = unknown>(url: string, options?: HttpRequestOptionsBase): Promise<T>;
|
|
18
|
+
head<T = unknown>(url: string, options?: HttpRequestOptionsBase): Promise<T>;
|
|
19
|
+
options<T = unknown>(url: string, options?: HttpRequestOptionsBase): Promise<T>;
|
|
20
|
+
// Methods WITH body
|
|
21
|
+
post<T = unknown>(url: string, options?: HttpRequestWithBodyOptions): Promise<T>;
|
|
22
|
+
put<T = unknown>(url: string, options?: HttpRequestWithBodyOptions): Promise<T>;
|
|
23
|
+
patch<T = unknown>(url: string, options?: HttpRequestWithBodyOptions): Promise<T>;
|
|
24
|
+
delete<T = unknown>(url: string, options?: HttpRequestWithBodyOptions): Promise<T>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* PLAYWRIGHT ADAPTATION NOTE:
|
|
29
|
+
*
|
|
30
|
+
* This FetchClient is written for Node's global `fetch()`, but it can be
|
|
31
|
+
* switched to Playwright's APIRequestContext by replacing the fetch call.
|
|
32
|
+
*
|
|
33
|
+
* To enable Playwright mode:
|
|
34
|
+
* 1. Pass a Playwright `APIRequestContext` into the constructor:
|
|
35
|
+
* new FetchClient(baseUrl, requestContext)
|
|
36
|
+
*
|
|
37
|
+
* 2. Replace:
|
|
38
|
+
* fetch(finalUrl, { method, headers, body })
|
|
39
|
+
* With:
|
|
40
|
+
* requestContext.fetch(finalUrl, {
|
|
41
|
+
* method,
|
|
42
|
+
* headers,
|
|
43
|
+
* data: body, // Playwright uses `data` instead of `body`
|
|
44
|
+
* })
|
|
45
|
+
*
|
|
46
|
+
* 3. Playwright response fields are functions:
|
|
47
|
+
* response.status() instead of response.status
|
|
48
|
+
* response.statusText() instead of response.statusText
|
|
49
|
+
*
|
|
50
|
+
* 4. The rest of the client stays the same — only the fetch call changes.
|
|
51
|
+
*
|
|
52
|
+
* This allows the same client to work in both environments:
|
|
53
|
+
* - Node/Vitest → uses global fetch()
|
|
54
|
+
* - Playwright → uses requestContext.fetch()
|
|
55
|
+
*
|
|
56
|
+
* Multipart and binary uploads should be passed through as real request bodies:
|
|
57
|
+
* - JSON bodies can still be plain objects
|
|
58
|
+
* - multipart bodies should be FormData
|
|
59
|
+
* - file parts should be represented as Blob/File/Buffer-backed values, not strings
|
|
60
|
+
* - do not force `Content-Type: application/json` when the request is multipart
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
export class FetchClient implements HttpClient {
|
|
64
|
+
constructor(private readonly baseUrl: string) {}
|
|
65
|
+
|
|
66
|
+
private async request<T>(
|
|
67
|
+
method: string,
|
|
68
|
+
path: string,
|
|
69
|
+
options: HttpRequestOptionsBase | HttpRequestWithBodyOptions = {}
|
|
70
|
+
): Promise<T> {
|
|
71
|
+
const finalUrl = this.baseUrl.replace(/\/$/, "") + "/" + path.replace(/^\//, "");
|
|
72
|
+
|
|
73
|
+
const hasBody =
|
|
74
|
+
"body" in options &&
|
|
75
|
+
options.body !== undefined &&
|
|
76
|
+
method !== "GET" &&
|
|
77
|
+
method !== "HEAD";
|
|
78
|
+
|
|
79
|
+
const response = await fetch(finalUrl, {
|
|
80
|
+
method,
|
|
81
|
+
headers: {
|
|
82
|
+
...(hasBody ? { "Content-Type": "application/json" } : {}),
|
|
83
|
+
...(options.headers ?? {})
|
|
84
|
+
},
|
|
85
|
+
body: hasBody ? JSON.stringify((options as HttpRequestWithBodyOptions).body) : null
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const text = await response.text();
|
|
89
|
+
let body: unknown = {};
|
|
90
|
+
|
|
91
|
+
if (text.trim().length > 0) {
|
|
92
|
+
try {
|
|
93
|
+
body = JSON.parse(text);
|
|
94
|
+
} catch {
|
|
95
|
+
body = text;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (body !== null && typeof body === "object" && !Array.isArray(body)) {
|
|
100
|
+
return {
|
|
101
|
+
...body,
|
|
102
|
+
status: response.status,
|
|
103
|
+
statusText: response.statusText,
|
|
104
|
+
} as T;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
body,
|
|
109
|
+
status: response.status,
|
|
110
|
+
statusText: response.statusText,
|
|
111
|
+
} as T;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
get<T>(path: string, options?: HttpRequestOptionsBase): Promise<T> {
|
|
115
|
+
return this.request<T>("GET", path, options);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
head<T>(path: string, options?: HttpRequestOptionsBase): Promise<T> {
|
|
119
|
+
return this.request<T>("HEAD", path, options);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
options<T>(path: string, options?: HttpRequestOptionsBase): Promise<T> {
|
|
123
|
+
return this.request<T>("OPTIONS", path, options);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
post<T>(path: string, options?: HttpRequestWithBodyOptions): Promise<T> {
|
|
127
|
+
return this.request<T>("POST", path, options);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
put<T>(path: string, options?: HttpRequestWithBodyOptions): Promise<T> {
|
|
131
|
+
return this.request<T>("PUT", path, options);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
patch<T>(path: string, options?: HttpRequestWithBodyOptions): Promise<T> {
|
|
135
|
+
return this.request<T>("PATCH", path, options);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
delete<T>(path: string, options?: HttpRequestWithBodyOptions): Promise<T> {
|
|
139
|
+
return this.request<T>("DELETE", path, options);
|
|
140
|
+
}
|
|
141
|
+
}
|