@testsmith/api-spector 0.6.0 → 0.6.2
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/docs/ai/api-spector-scripting-reference.md +149 -0
- package/docs/ai/collection-file-format.md +110 -0
- package/docs/ai/functional-testing-guide.md +145 -0
- package/docs/ai/security-testing-guide.md +253 -0
- package/out/main/agents.js +2 -1
- package/out/main/chunks/{snapshots-Y33M1SEy.js → broker-client-WEymdqAK.js} +119 -10
- package/out/main/chunks/{cli-common-BKYgsbGB.js → cli-common-vASoDfQH.js} +3 -2
- package/out/main/chunks/data-files-Br2h3Dt2.js +85 -0
- package/out/main/chunks/escape-BwPM4Tf6.js +9 -0
- package/out/main/chunks/{handle-BFaeFA0A.js → handle-CL-nWtYA.js} +11 -2
- package/out/main/chunks/{import-D9LH0isB.js → import-DdXhO56X.js} +2 -2
- package/out/main/chunks/{request-collection-BQIo1765.js → request-collection-Hqh3I3rs.js} +75 -15
- package/out/main/chunks/{request-exec-5_3279w1.js → request-exec-DAFA6Fap.js} +19 -1
- package/out/main/chunks/{soap-handler-CWn3o6Py.js → soap-handler-BAPCALkN.js} +2 -2
- package/out/main/compare.js +2 -1
- package/out/main/contract.js +50 -147
- package/out/main/coverage.js +2 -1
- package/out/main/generate-tests.js +2 -1
- package/out/main/index.js +131 -36
- package/out/main/lib.js +2 -2
- package/out/main/mock.js +2 -1
- package/out/main/record.js +2 -1
- package/out/main/runner.js +95 -13
- package/out/main/wsdl.js +5 -4
- package/out/preload/index.js +21 -3
- package/out/renderer/assets/{GraphQLEditor-DITvPhtG.js → GraphQLEditor-DtppmZJO.js} +3 -1
- package/out/renderer/assets/{SoapEditor-Bf-oE-rS.js → SoapEditor-BwSm12wH.js} +4 -2
- package/out/renderer/assets/ajv-2MAg6kGF.js +6153 -0
- package/out/renderer/assets/codemirror-F6m3Hm1M.js +26552 -0
- package/out/renderer/assets/index-B3mtLdU3.css +2 -0
- package/out/renderer/assets/{index-MMIHKSKx.js → index-D6-zmXen.js} +17493 -49301
- package/out/renderer/index.html +4 -2
- package/package.json +4 -2
- package/src/cli/agent-templates/aider/conventions.md +10 -0
- package/src/cli/agent-templates/claude/skills/api-spector-api-audit.md +135 -0
- package/src/cli/agent-templates/claude/skills/api-spector-functional-tests.md +73 -0
- package/src/cli/agent-templates/claude/skills/api-spector-generate-mocks.md +182 -0
- package/src/cli/agent-templates/claude/skills/api-spector-security-tests.md +73 -0
- package/src/cli/agent-templates/copilot/copilot-instructions.md +38 -0
- package/src/cli/agent-templates/cursor/rules/api-spector.mdc +21 -0
- package/src/cli/agent-templates/windsurf/windsurfrules +19 -0
- package/out/renderer/assets/index-CkHujZU3.css +0 -2
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# API Spector: Scripting Reference
|
|
2
|
+
|
|
3
|
+
This document describes the scripting API available in API Spector's pre-request and post-request scripts. Use this as a reference when generating test scripts.
|
|
4
|
+
|
|
5
|
+
## Script Context
|
|
6
|
+
|
|
7
|
+
Scripts run in a sandboxed environment. The global `sp` object provides all testing, assertion, and variable functionality.
|
|
8
|
+
|
|
9
|
+
## Assertions
|
|
10
|
+
|
|
11
|
+
### sp.test(name, fn)
|
|
12
|
+
|
|
13
|
+
Wrap assertions in a named test block:
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
sp.test('status is 200', function() {
|
|
17
|
+
sp.expect(sp.response.code).to.equal(200);
|
|
18
|
+
});
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### sp.expect(value)
|
|
22
|
+
|
|
23
|
+
Returns a chainable asserter. Available matchers:
|
|
24
|
+
|
|
25
|
+
| Matcher | Example | Description |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `.to.equal(v)` | `sp.expect(json.name).to.equal("Alice")` | Strict equality |
|
|
28
|
+
| `.to.eql(v)` | `sp.expect(json).to.eql({id: 1})` | Deep equality |
|
|
29
|
+
| `.to.include(v)` | `sp.expect(json.name).to.include("Ali")` | String/array contains |
|
|
30
|
+
| `.to.be.a(type)` | `sp.expect(json.age).to.be.a("number")` | Type check |
|
|
31
|
+
| `.to.be.above(n)` | `sp.expect(json.count).to.be.above(0)` | Greater than |
|
|
32
|
+
| `.to.be.below(n)` | `sp.expect(json.count).to.be.below(100)` | Less than |
|
|
33
|
+
| `.to.be.least(n)` | `sp.expect(json.count).to.be.least(1)` | >= |
|
|
34
|
+
| `.to.be.most(n)` | `sp.expect(json.count).to.be.most(50)` | <= |
|
|
35
|
+
| `.to.have.property(k)` | `sp.expect(json).to.have.property("id")` | Key exists |
|
|
36
|
+
| `.to.have.lengthOf(n)` | `sp.expect(json.items).to.have.lengthOf(3)` | Array/string length |
|
|
37
|
+
| `.to.match(re)` | `sp.expect(json.email).to.match(/^.+@.+$/)` | Regex match |
|
|
38
|
+
| `.to.be.oneOf([...])` | `sp.expect(json.role).to.be.oneOf(["admin","user"])` | Value in list |
|
|
39
|
+
| `.to.not.xxx` | `sp.expect(json.id).to.not.be.oneOf([null, undefined])` | Negation |
|
|
40
|
+
|
|
41
|
+
Chainable words (no-op, for readability): `.to`, `.be`, `.been`, `.have`, `.that`, `.and`, `.is`, `.deep`
|
|
42
|
+
|
|
43
|
+
Flag assertions (property-style): `.ok`, `.true`, `.false`, `.null`, `.undefined`
|
|
44
|
+
|
|
45
|
+
## Response Object
|
|
46
|
+
|
|
47
|
+
Available in post-request scripts when a response has been received:
|
|
48
|
+
|
|
49
|
+
```javascript
|
|
50
|
+
sp.response.code // HTTP status code (number)
|
|
51
|
+
sp.response.status // "200 OK" (string)
|
|
52
|
+
sp.response.statusText // "OK" (string)
|
|
53
|
+
sp.response.responseTime // Duration in ms
|
|
54
|
+
sp.response.responseSize // Body size in bytes
|
|
55
|
+
sp.response.json() // Parse body as JSON (cached)
|
|
56
|
+
sp.response.text() // Raw body as string
|
|
57
|
+
sp.response.xmlText(sel) // Extract text from XML/HTML using CSS selector
|
|
58
|
+
sp.response.headers.get(n) // Get response header by name
|
|
59
|
+
sp.response.headers.toObject() // All headers as {key: value}
|
|
60
|
+
sp.response.to.have.status(code) // Assert status code
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Variables
|
|
64
|
+
|
|
65
|
+
Four variable scopes, from most-local to most-global:
|
|
66
|
+
|
|
67
|
+
```javascript
|
|
68
|
+
// Local variables (per-request, not persisted)
|
|
69
|
+
sp.variables.get("key")
|
|
70
|
+
sp.variables.set("key", "value")
|
|
71
|
+
sp.variables.has("key")
|
|
72
|
+
sp.variables.clear("key")
|
|
73
|
+
|
|
74
|
+
// Environment variables (persisted to the active environment)
|
|
75
|
+
sp.environment.get("key")
|
|
76
|
+
sp.environment.set("key", "value")
|
|
77
|
+
|
|
78
|
+
// Collection variables (persisted to the collection)
|
|
79
|
+
sp.collectionVariables.get("key")
|
|
80
|
+
sp.collectionVariables.set("key", "value")
|
|
81
|
+
|
|
82
|
+
// Global variables (persisted across collections)
|
|
83
|
+
sp.globals.get("key")
|
|
84
|
+
sp.globals.set("key", "value")
|
|
85
|
+
|
|
86
|
+
// Shorthand: read from all scopes (local wins)
|
|
87
|
+
sp.variables_get("key")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Variable interpolation in scripts
|
|
91
|
+
|
|
92
|
+
Scripts support `{{variable}}` interpolation. Before execution, `{{var}}` tokens are replaced with the variable's value:
|
|
93
|
+
|
|
94
|
+
```javascript
|
|
95
|
+
sp.variables.set("otp", sp.totp("{{secret}}"));
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## JSONPath
|
|
99
|
+
|
|
100
|
+
Query JSON data with JSONPath expressions:
|
|
101
|
+
|
|
102
|
+
```javascript
|
|
103
|
+
const matches = sp.jsonPath(sp.response.json(), '$.users[?(@.role=="admin")].name');
|
|
104
|
+
sp.expect(matches.length).to.be.above(0);
|
|
105
|
+
sp.expect(matches[0]).to.equal("Alice");
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## TOTP (Time-based One-Time Password)
|
|
109
|
+
|
|
110
|
+
Generate TOTP codes from a base32 secret:
|
|
111
|
+
|
|
112
|
+
```javascript
|
|
113
|
+
const code = sp.totp("JBSWY3DPEHPK3PXP");
|
|
114
|
+
sp.environment.set("otp", code);
|
|
115
|
+
|
|
116
|
+
// With options:
|
|
117
|
+
const code8 = sp.totp("JBSWY3DPEHPK3PXP", { digits: 8, period: 30, algorithm: "sha1" });
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Available globals
|
|
121
|
+
|
|
122
|
+
Scripts have access to: `JSON`, `Math`, `Date`, `parseInt`, `parseFloat`, `isNaN`, `isFinite`, `encodeURIComponent`, `decodeURIComponent`, `btoa`, `atob`, `console` (log/warn/error/info), `dayjs` (date library), `faker` (test data generation), `tv4` (JSON Schema validation), `DOMParser` (XML parsing).
|
|
123
|
+
|
|
124
|
+
## Hooks
|
|
125
|
+
|
|
126
|
+
Requests can be designated as lifecycle hooks within their folder:
|
|
127
|
+
|
|
128
|
+
| Hook type | When it runs |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `beforeAll` | Once before any request in the folder |
|
|
131
|
+
| `before` | Before each request in the folder |
|
|
132
|
+
| `after` | After each request in the folder |
|
|
133
|
+
| `afterAll` | Once after all requests in the folder |
|
|
134
|
+
|
|
135
|
+
Hooks from parent folders wrap child folders (outer-to-inner for before, inner-to-outer for after).
|
|
136
|
+
|
|
137
|
+
A common pattern is a login hook:
|
|
138
|
+
|
|
139
|
+
```javascript
|
|
140
|
+
// Post-request script on a beforeAll hook that calls POST /login:
|
|
141
|
+
const json = sp.response.json();
|
|
142
|
+
sp.environment.set("access_token", json.access_token);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Then other requests use `{{access_token}}` in their Bearer token auth field.
|
|
146
|
+
|
|
147
|
+
## Schema validation
|
|
148
|
+
|
|
149
|
+
The `request.schema` field (JSON Schema) is automatically validated against every response. Failed validations appear as `[schema]` test results. The schema uses standard JSON Schema draft-07+ syntax.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# API Spector: Collection File Format
|
|
2
|
+
|
|
3
|
+
Collections are stored as `.spector` JSON files. This document describes the format so AI tools can read and modify collections programmatically.
|
|
4
|
+
|
|
5
|
+
## File structure
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"version": "1.0",
|
|
10
|
+
"id": "uuid",
|
|
11
|
+
"name": "My API",
|
|
12
|
+
"description": "Optional description",
|
|
13
|
+
"rootFolder": { ... },
|
|
14
|
+
"requests": { "request-id": { ... }, ... },
|
|
15
|
+
"collectionVariables": { "key": "value" },
|
|
16
|
+
"auth": { "type": "bearer", "token": "{{access_token}}" },
|
|
17
|
+
"headers": [{ "key": "Accept", "value": "application/json", "enabled": true }]
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Request object
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"id": "unique-id",
|
|
26
|
+
"name": "Get Users",
|
|
27
|
+
"method": "GET",
|
|
28
|
+
"url": "{{baseUrl}}/users",
|
|
29
|
+
"headers": [
|
|
30
|
+
{ "key": "Accept", "value": "application/json", "enabled": true }
|
|
31
|
+
],
|
|
32
|
+
"params": [
|
|
33
|
+
{ "key": "page", "value": "1", "enabled": true, "paramType": "query" },
|
|
34
|
+
{ "key": "id", "value": "42", "enabled": true, "paramType": "path" }
|
|
35
|
+
],
|
|
36
|
+
"auth": { "type": "bearer", "token": "{{access_token}}" },
|
|
37
|
+
"body": {
|
|
38
|
+
"mode": "json",
|
|
39
|
+
"json": "{ \"name\": \"Alice\" }"
|
|
40
|
+
},
|
|
41
|
+
"preRequestScript": "sp.environment.set('otp', sp.totp('{{secret}}'));",
|
|
42
|
+
"postRequestScript": "sp.test('status', function() { ... });",
|
|
43
|
+
"schema": "{ \"type\": \"object\", ... }",
|
|
44
|
+
"hookType": "beforeAll",
|
|
45
|
+
"disabled": false
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Auth types
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{ "type": "none" }
|
|
53
|
+
{ "type": "bearer", "token": "{{access_token}}" }
|
|
54
|
+
{ "type": "basic", "username": "admin", "password": "secret" }
|
|
55
|
+
{ "type": "apikey", "apiKeyName": "X-API-Key", "apiKeyValue": "{{key}}", "apiKeyIn": "header" }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Body modes
|
|
59
|
+
|
|
60
|
+
- `"none"`: no body
|
|
61
|
+
- `"json"`: `body.json` contains the JSON string
|
|
62
|
+
- `"form"`: `body.form` is an array of `{ key, value, enabled }` pairs
|
|
63
|
+
- `"raw"`: `body.raw` contains raw text, `body.rawContentType` is the MIME type
|
|
64
|
+
- `"graphql"`: `body.graphql` has `{ query, variables, operationName }`
|
|
65
|
+
|
|
66
|
+
## Folder structure
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"id": "folder-id",
|
|
71
|
+
"name": "Users",
|
|
72
|
+
"description": "",
|
|
73
|
+
"folders": [ ... ],
|
|
74
|
+
"requestIds": ["req-1", "req-2"],
|
|
75
|
+
"auth": { "type": "bearer", "token": "{{admin_token}}" },
|
|
76
|
+
"headers": [{ "key": "X-Tenant", "value": "{{tenant}}", "enabled": true }],
|
|
77
|
+
"tags": ["smoke"]
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Folders can nest. Each folder can define auth and headers that are inherited by all requests within it (overridable at the request level). The root folder is synthetic (name "root"); its subfolders are the visible top-level groups.
|
|
82
|
+
|
|
83
|
+
## Environment files
|
|
84
|
+
|
|
85
|
+
Stored as `.env.json`:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"version": "1.0",
|
|
90
|
+
"id": "uuid",
|
|
91
|
+
"name": "Staging",
|
|
92
|
+
"variables": [
|
|
93
|
+
{ "key": "baseUrl", "value": "https://staging.api.com", "enabled": true },
|
|
94
|
+
{ "key": "admin_password", "value": "", "enabled": true, "secret": true }
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Workspace file
|
|
100
|
+
|
|
101
|
+
`workspace.json` ties collections and environments together:
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"version": "1.0",
|
|
106
|
+
"collections": ["collections/users.spector", "collections/orders.spector"],
|
|
107
|
+
"environments": ["environments/staging.env.json"],
|
|
108
|
+
"activeEnvironmentId": "env-uuid"
|
|
109
|
+
}
|
|
110
|
+
```
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# API Spector: Functional API Testing Guide
|
|
2
|
+
|
|
3
|
+
Use this guide to generate functional test cases for REST APIs. Each endpoint should be tested across multiple categories.
|
|
4
|
+
|
|
5
|
+
## Test categories per endpoint
|
|
6
|
+
|
|
7
|
+
### 1. Happy path (positive)
|
|
8
|
+
- Send valid data, assert correct status code and response structure
|
|
9
|
+
- Verify all required response fields are present and correctly typed
|
|
10
|
+
|
|
11
|
+
```javascript
|
|
12
|
+
sp.test('returns 200 with user data', function() {
|
|
13
|
+
const json = sp.response.json();
|
|
14
|
+
sp.expect(sp.response.code).to.equal(200);
|
|
15
|
+
sp.expect(json.id).to.not.be.oneOf([null, undefined]);
|
|
16
|
+
sp.expect(json.name).to.be.a("string");
|
|
17
|
+
sp.expect(json.email).to.match(/^.+@.+\..+$/);
|
|
18
|
+
});
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### 2. Missing required fields (negative)
|
|
22
|
+
- Omit each required field one at a time
|
|
23
|
+
- Expect 422 or 400 with a descriptive error
|
|
24
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
sp.test('returns 422 when email is missing', function() {
|
|
27
|
+
sp.expect(sp.response.code).to.equal(422);
|
|
28
|
+
const json = sp.response.json();
|
|
29
|
+
sp.expect(JSON.stringify(json)).to.include("email");
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 3. Wrong types (negative)
|
|
34
|
+
- Send a string where a number is expected, an object where a string is expected, etc.
|
|
35
|
+
- Expect 422 or 400
|
|
36
|
+
|
|
37
|
+
### 4. Boundary values
|
|
38
|
+
- Empty strings for string fields
|
|
39
|
+
- 0, -1, MAX_INT for numeric fields
|
|
40
|
+
- Very long strings (1000+ chars)
|
|
41
|
+
- Special characters: `<script>alert(1)</script>`, `'; DROP TABLE--`, unicode
|
|
42
|
+
|
|
43
|
+
### 5. Authentication
|
|
44
|
+
- Missing auth header → 401
|
|
45
|
+
- Invalid/expired token → 401
|
|
46
|
+
- Valid token, wrong role → 403
|
|
47
|
+
|
|
48
|
+
### 6. Resource not found
|
|
49
|
+
- Request a non-existent ID → 404
|
|
50
|
+
- Request with a malformed ID format → 400 or 404
|
|
51
|
+
|
|
52
|
+
### 7. List/pagination endpoints
|
|
53
|
+
- Default pagination returns reasonable count
|
|
54
|
+
- `page` and `limit` params work correctly
|
|
55
|
+
- Out-of-range page returns empty list (not error)
|
|
56
|
+
- Sorting works if supported
|
|
57
|
+
|
|
58
|
+
### 8. Idempotency
|
|
59
|
+
- PUT with same data twice → same result
|
|
60
|
+
- DELETE then DELETE again → 404 on second call
|
|
61
|
+
|
|
62
|
+
## Test naming convention
|
|
63
|
+
|
|
64
|
+
Use descriptive names that include the endpoint and expected behavior:
|
|
65
|
+
|
|
66
|
+
```javascript
|
|
67
|
+
sp.test('POST /users - valid data returns 201', function() { ... });
|
|
68
|
+
sp.test('POST /users - missing email returns 422', function() { ... });
|
|
69
|
+
sp.test('GET /users/:id - non-existent ID returns 404', function() { ... });
|
|
70
|
+
sp.test('GET /users - unauthorized returns 401', function() { ... });
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Variable extraction pattern
|
|
74
|
+
|
|
75
|
+
When a test creates a resource, extract its ID for subsequent tests:
|
|
76
|
+
|
|
77
|
+
```javascript
|
|
78
|
+
// Post-request script on POST /users
|
|
79
|
+
const json = sp.response.json();
|
|
80
|
+
sp.environment.set("userId", String(json.id));
|
|
81
|
+
|
|
82
|
+
// Then GET /users/{{userId}} in a subsequent request uses the created ID
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Hook pattern for auth
|
|
86
|
+
|
|
87
|
+
Create a `beforeAll` hook that logs in and stores the token:
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
// Post-request script on the login hook (POST /auth/login)
|
|
91
|
+
const json = sp.response.json();
|
|
92
|
+
sp.environment.set("access_token", json.access_token);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Then configure the folder/collection auth as Bearer with token `{{access_token}}`.
|
|
96
|
+
|
|
97
|
+
## Test plan structure
|
|
98
|
+
|
|
99
|
+
When generating a test plan, organize by endpoint and include:
|
|
100
|
+
|
|
101
|
+
1. **Endpoint**: method + path
|
|
102
|
+
2. **Test cases**: list of scenarios with:
|
|
103
|
+
- Name (descriptive)
|
|
104
|
+
- Category (positive/negative/boundary/auth/edge)
|
|
105
|
+
- Request body (if applicable)
|
|
106
|
+
- Expected status code
|
|
107
|
+
- Assertions to make
|
|
108
|
+
3. **Dependencies**: which tests depend on data from earlier tests
|
|
109
|
+
4. **Hooks needed**: login, setup data, cleanup
|
|
110
|
+
|
|
111
|
+
## Example test plan output
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
## POST /users
|
|
115
|
+
|
|
116
|
+
### Positive
|
|
117
|
+
- [x] Valid user creation → 201, response has id, name, email
|
|
118
|
+
- [x] Create user with all optional fields → 201
|
|
119
|
+
|
|
120
|
+
### Negative
|
|
121
|
+
- [x] Missing name → 422, error mentions "name"
|
|
122
|
+
- [x] Missing email → 422, error mentions "email"
|
|
123
|
+
- [x] Invalid email format → 422
|
|
124
|
+
- [x] Duplicate email → 409
|
|
125
|
+
|
|
126
|
+
### Boundary
|
|
127
|
+
- [x] Name with 255 characters → 201 (or 422 if max exceeded)
|
|
128
|
+
- [x] Empty name → 422
|
|
129
|
+
- [x] Price = 0 → 201 or 422 depending on business rule
|
|
130
|
+
- [x] Price = -1 → 422
|
|
131
|
+
|
|
132
|
+
### Auth
|
|
133
|
+
- [x] No token → 401
|
|
134
|
+
- [x] Expired token → 401
|
|
135
|
+
|
|
136
|
+
## GET /users
|
|
137
|
+
|
|
138
|
+
### Positive
|
|
139
|
+
- [x] List users → 200, returns array
|
|
140
|
+
- [x] Filter by name → 200, filtered results
|
|
141
|
+
|
|
142
|
+
### Pagination
|
|
143
|
+
- [x] ?page=1&limit=10 → 200, max 10 results
|
|
144
|
+
- [x] ?page=9999 → 200, empty array
|
|
145
|
+
```
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# API Spector: Security API Testing Guide (OWASP API Top 10)
|
|
2
|
+
|
|
3
|
+
Use this guide to generate security test cases based on the OWASP API Security Top 10 (2023). Each test case should use the API Spector `sp.*` scripting API.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
Security tests typically require multiple user identities:
|
|
8
|
+
- **User A** (attacker): a regular user trying to access resources they shouldn't
|
|
9
|
+
- **User B** (victim): the owner of the resources being targeted
|
|
10
|
+
- **Admin**: for testing admin-only endpoints with regular credentials
|
|
11
|
+
|
|
12
|
+
Store tokens in environment variables: `{{user_a_token}}`, `{{user_b_token}}`, `{{admin_token}}`
|
|
13
|
+
|
|
14
|
+
## API1:2023 - Broken Object-Level Authorization (BOLA)
|
|
15
|
+
|
|
16
|
+
**What to test**: Can User A access User B's resources by changing the ID?
|
|
17
|
+
|
|
18
|
+
```javascript
|
|
19
|
+
// Request: GET /users/{{user_b_id}} with User A's token
|
|
20
|
+
sp.test('[BOLA] cannot access other user profile', function() {
|
|
21
|
+
sp.expect(sp.response.code).to.be.oneOf([403, 404]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Request: PUT /users/{{user_b_id}} with User A's token
|
|
25
|
+
sp.test('[BOLA] cannot modify other user profile', function() {
|
|
26
|
+
sp.expect(sp.response.code).to.be.oneOf([403, 404]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Request: DELETE /orders/{{user_b_order_id}} with User A's token
|
|
30
|
+
sp.test('[BOLA] cannot delete other user order', function() {
|
|
31
|
+
sp.expect(sp.response.code).to.be.oneOf([403, 404]);
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**For each endpoint with an ID parameter**: create a request where User A tries to access User B's resource.
|
|
36
|
+
|
|
37
|
+
## API2:2023 - Broken Authentication
|
|
38
|
+
|
|
39
|
+
**What to test**: Authentication bypass, weak tokens, missing validation.
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
// Request with no Authorization header
|
|
43
|
+
sp.test('[AUTH] missing token returns 401', function() {
|
|
44
|
+
sp.expect(sp.response.code).to.equal(401);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Request with an invalid/garbage token
|
|
48
|
+
sp.test('[AUTH] invalid token returns 401', function() {
|
|
49
|
+
sp.expect(sp.response.code).to.equal(401);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Request with expired token
|
|
53
|
+
sp.test('[AUTH] expired token returns 401', function() {
|
|
54
|
+
sp.expect(sp.response.code).to.equal(401);
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## API3:2023 - Broken Object Property-Level Authorization
|
|
59
|
+
|
|
60
|
+
**What to test**: Mass assignment. Can a user set fields they shouldn't?
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
// Request: PUT /users/{{user_a_id}} with body including role: "admin"
|
|
64
|
+
sp.test('[MASS-ASSIGN] cannot escalate own role', function() {
|
|
65
|
+
sp.expect(sp.response.code).to.be.oneOf([200, 422]);
|
|
66
|
+
// If 200, verify the role wasn't actually changed
|
|
67
|
+
const json = sp.response.json();
|
|
68
|
+
if (sp.response.code === 200) {
|
|
69
|
+
sp.expect(json.role).to.not.equal("admin");
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Request: POST /users with is_admin: true
|
|
74
|
+
sp.test('[MASS-ASSIGN] cannot create admin user', function() {
|
|
75
|
+
const json = sp.response.json();
|
|
76
|
+
if (json.is_admin !== undefined) {
|
|
77
|
+
sp.expect(json.is_admin).to.equal(false);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**For each write endpoint**: include fields that shouldn't be user-settable (role, is_admin, balance, permissions).
|
|
83
|
+
|
|
84
|
+
## API4:2023 - Unrestricted Resource Consumption
|
|
85
|
+
|
|
86
|
+
**What to test**: Rate limits, pagination abuse, large payloads.
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
// Request: GET /users?limit=999999
|
|
90
|
+
sp.test('[RESOURCE] excessive limit is capped or rejected', function() {
|
|
91
|
+
sp.expect(sp.response.code).to.be.oneOf([200, 400, 422]);
|
|
92
|
+
if (sp.response.code === 200) {
|
|
93
|
+
const json = sp.response.json();
|
|
94
|
+
const items = Array.isArray(json) ? json : json.data ?? [];
|
|
95
|
+
sp.expect(items.length).to.be.most(100);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Request: POST /upload with an extremely large body
|
|
100
|
+
sp.test('[RESOURCE] rejects oversized payload', function() {
|
|
101
|
+
sp.expect(sp.response.code).to.be.oneOf([413, 400, 422]);
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## API5:2023 - Broken Function-Level Authorization
|
|
106
|
+
|
|
107
|
+
**What to test**: Admin endpoints accessible by regular users.
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
// Request: GET /admin/users with regular user token
|
|
111
|
+
sp.test('[FUNC-AUTH] admin endpoint rejects regular user', function() {
|
|
112
|
+
sp.expect(sp.response.code).to.be.oneOf([403, 404]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Request: DELETE /users/{{user_b_id}} with regular user token
|
|
116
|
+
sp.test('[FUNC-AUTH] user cannot delete other users', function() {
|
|
117
|
+
sp.expect(sp.response.code).to.be.oneOf([403, 404]);
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**For each endpoint**: determine the expected minimum role and test with a lower-privilege token.
|
|
122
|
+
|
|
123
|
+
## API6:2023 - Unrestricted Access to Sensitive Business Flows
|
|
124
|
+
|
|
125
|
+
**What to test**: Business logic abuse.
|
|
126
|
+
|
|
127
|
+
```javascript
|
|
128
|
+
// Request: POST /orders (repeat the same order)
|
|
129
|
+
sp.test('[BIZ-FLOW] duplicate order is rejected', function() {
|
|
130
|
+
// Second call with same data should not create a duplicate
|
|
131
|
+
sp.expect(sp.response.code).to.be.oneOf([409, 422, 429]);
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## API7:2023 - Server-Side Request Forgery (SSRF)
|
|
136
|
+
|
|
137
|
+
**What to test**: URL parameters that the server fetches.
|
|
138
|
+
|
|
139
|
+
```javascript
|
|
140
|
+
// Request with URL field pointing to internal metadata
|
|
141
|
+
// e.g. POST /import with { "url": "http://169.254.169.254/latest/meta-data/" }
|
|
142
|
+
sp.test('[SSRF] internal URL is blocked', function() {
|
|
143
|
+
sp.expect(sp.response.code).to.be.oneOf([400, 403, 422]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Same with http://localhost, http://127.0.0.1, http://[::1]
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**For each endpoint that accepts a URL parameter**: test with internal addresses.
|
|
150
|
+
|
|
151
|
+
## API8:2023 - Security Misconfiguration
|
|
152
|
+
|
|
153
|
+
**What to test**: Headers, error verbosity, CORS.
|
|
154
|
+
|
|
155
|
+
```javascript
|
|
156
|
+
// Any endpoint: check security headers
|
|
157
|
+
sp.test('[CONFIG] has security headers', function() {
|
|
158
|
+
sp.expect(sp.response.headers.get('x-content-type-options')).to.equal('nosniff');
|
|
159
|
+
// Optional: check for other headers
|
|
160
|
+
// sp.expect(sp.response.headers.get('x-frame-options')).to.not.equal(null);
|
|
161
|
+
// sp.expect(sp.response.headers.get('strict-transport-security')).to.not.equal(null);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Invalid endpoint: check that error doesn't leak stack traces
|
|
165
|
+
sp.test('[CONFIG] error response does not expose stack trace', function() {
|
|
166
|
+
const body = sp.response.text();
|
|
167
|
+
sp.expect(body).to.not.include("at Object.");
|
|
168
|
+
sp.expect(body).to.not.include("node_modules");
|
|
169
|
+
sp.expect(body).to.not.include("Exception in thread");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// OPTIONS request: check CORS
|
|
173
|
+
sp.test('[CONFIG] CORS does not allow *', function() {
|
|
174
|
+
const allow = sp.response.headers.get('access-control-allow-origin');
|
|
175
|
+
if (allow) {
|
|
176
|
+
sp.expect(allow).to.not.equal('*');
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## API9:2023 - Improper Inventory Management
|
|
182
|
+
|
|
183
|
+
**What to test**: Deprecated or undocumented endpoints still accessible.
|
|
184
|
+
|
|
185
|
+
This is best tested by comparing the spec against actual behavior:
|
|
186
|
+
- Hit known deprecated endpoints → should return 404 or 410
|
|
187
|
+
- Hit versioned paths (v1 when v2 is current) → should be blocked or redirected
|
|
188
|
+
|
|
189
|
+
## API10:2023 - Unsafe Consumption of APIs
|
|
190
|
+
|
|
191
|
+
**What to test**: Injection through fields that the API passes downstream.
|
|
192
|
+
|
|
193
|
+
```javascript
|
|
194
|
+
// SQL injection in search/filter parameters
|
|
195
|
+
// Request: GET /users?search=' OR '1'='1
|
|
196
|
+
sp.test('[INJECTION] SQL injection in search returns 400 or safe results', function() {
|
|
197
|
+
sp.expect(sp.response.code).to.be.oneOf([200, 400, 422]);
|
|
198
|
+
if (sp.response.code === 200) {
|
|
199
|
+
const json = sp.response.json();
|
|
200
|
+
const items = Array.isArray(json) ? json : json.data ?? [];
|
|
201
|
+
sp.expect(items.length).to.be.below(50); // should not dump entire table
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// XSS payload in input fields
|
|
206
|
+
// Request: POST /users with name: "<script>alert(1)</script>"
|
|
207
|
+
sp.test('[INJECTION] XSS payload is sanitized or rejected', function() {
|
|
208
|
+
sp.expect(sp.response.code).to.be.oneOf([201, 200, 400, 422]);
|
|
209
|
+
if (sp.response.code <= 201) {
|
|
210
|
+
const json = sp.response.json();
|
|
211
|
+
sp.expect(json.name).to.not.include("<script>");
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// NoSQL injection
|
|
216
|
+
// Request: POST /login with { "email": {"$gt": ""}, "password": {"$gt": ""} }
|
|
217
|
+
sp.test('[INJECTION] NoSQL injection is rejected', function() {
|
|
218
|
+
sp.expect(sp.response.code).to.be.oneOf([400, 401, 422]);
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Test plan structure
|
|
223
|
+
|
|
224
|
+
When generating security tests, organize by OWASP category:
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
Security/
|
|
228
|
+
├── BOLA/
|
|
229
|
+
│ ├── [BOLA] GET /users/:id - access other user
|
|
230
|
+
│ ├── [BOLA] PUT /orders/:id - modify other user order
|
|
231
|
+
│ └── ...
|
|
232
|
+
├── Auth/
|
|
233
|
+
│ ├── [AUTH] No token
|
|
234
|
+
│ ├── [AUTH] Invalid token
|
|
235
|
+
│ └── [AUTH] Expired token
|
|
236
|
+
├── Mass Assignment/
|
|
237
|
+
│ ├── [MASS-ASSIGN] Escalate role via PUT /users
|
|
238
|
+
│ └── ...
|
|
239
|
+
├── Injection/
|
|
240
|
+
│ ├── [INJECTION] SQL in search param
|
|
241
|
+
│ ├── [INJECTION] XSS in name field
|
|
242
|
+
│ └── ...
|
|
243
|
+
└── Config/
|
|
244
|
+
├── [CONFIG] Security headers
|
|
245
|
+
├── [CONFIG] Error verbosity
|
|
246
|
+
└── [CONFIG] CORS policy
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Each request should:
|
|
250
|
+
1. Have a descriptive name prefixed with the OWASP category
|
|
251
|
+
2. Be pre-filled with the attack payload in the body/params/headers
|
|
252
|
+
3. Have a post-request script that asserts the expected safe behavior
|
|
253
|
+
4. Use the attacker's token (User A) unless testing missing-auth scenarios
|
package/out/main/agents.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
const promises = require("fs/promises");
|
|
4
4
|
const path = require("path");
|
|
5
|
-
const cliCommon = require("./chunks/cli-common-
|
|
5
|
+
const cliCommon = require("./chunks/cli-common-vASoDfQH.js");
|
|
6
|
+
require("./chunks/data-files-Br2h3Dt2.js");
|
|
6
7
|
require("crypto");
|
|
7
8
|
const AGENTS = {
|
|
8
9
|
claude: {
|