api-tuner 0.5.3 → 0.5.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/README.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  **API** **T**ests **U**sing **n3** **R**ules
4
4
 
5
+ - [Prerequisites](#prerequisites)
6
+ - [Installation](#installation)
7
+ - [Usage](#usage)
8
+ - [Example](#example)
9
+ - [Documentation](#documentation)
10
+ - [Namespaces](#namespaces)
11
+ - [Defining a Test Case](#defining-a-test-case)
12
+ - [HTTP Requests](#http-requests)
13
+ - [Assertions](#assertions)
14
+ - [Utility Rules](#utility-rules)
15
+ - [Debugging](#debugging)
16
+
5
17
  ## Prerequisites
6
18
 
7
19
  - [SWI Prolog](https://www.swi-prolog.org/Download.html)
@@ -77,4 +89,200 @@ api-tuner test.n3
77
89
 
78
90
  ## More examples
79
91
 
80
- TBD
92
+ See the [Documentation](#documentation) section below for more examples and detailed documentation of the N3 rules.
93
+
94
+ ## Documentation
95
+
96
+ This section describes the N3 rules and vocabulary used by `api-tuner` for defining and executing API tests.
97
+
98
+ ### Namespaces
99
+
100
+ Commonly used prefixes in `api-tuner` tests:
101
+
102
+ ```turtle
103
+ PREFIX tuner: <https://api-tuner.described.at/>
104
+ PREFIX resource: <https://api-tuner.described.at/resource#>
105
+ PREFIX earl: <http://www.w3.org/ns/earl#>
106
+ PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
107
+ PREFIX log: <http://www.w3.org/2000/10/swap/log#>
108
+ PREFIX string: <http://www.w3.org/2000/10/swap/string#>
109
+ ```
110
+
111
+ ### Defining a Test Case
112
+
113
+ A test case is defined as an `earl:TestCase`. The core logic of the test resides in `tuner:formula`.
114
+
115
+ ```turtle
116
+ <#myTest>
117
+ a earl:TestCase ;
118
+ rdfs:label "Description of my test" ;
119
+ tuner:formula {
120
+ # Test logic goes here
121
+ } .
122
+ ```
123
+
124
+ If the formula evaluates to true (all statements inside match), the test is considered `earl:passed`.
125
+
126
+ ### HTTP Requests
127
+
128
+ #### Request Object
129
+
130
+ You can define a detailed request using the `tuner:Request` class.
131
+
132
+ ```turtle
133
+ <#test> tuner:request [
134
+ a tuner:Request ;
135
+ tuner:method "POST" ;
136
+ tuner:url <http://example.com/api> ;
137
+ tuner:header ( "Accept" "application/json" ) ;
138
+ tuner:query ( "verbose" "true" ) ;
139
+ tuner:body { <#s> <#p> <#o> }
140
+ ] .
141
+ ```
142
+
143
+ To execute the request and get a response:
144
+ ```turtle
145
+ <#test> tuner:request ?req .
146
+ ?req tuner:response ?res .
147
+ ```
148
+
149
+ #### Simplified Helpers
150
+
151
+ The `resource:` namespace provides shortcuts for common HTTP methods. These helpers automatically assert a `200 OK` response status unless used in a way that captures the response for further assertions.
152
+
153
+ - `( <url> ?res ) resource:getIn []`
154
+ - `( <url> ?body ?res ) resource:postIn []`
155
+ - `( <url> ?res ) resource:postIn []` (no body)
156
+ - `( <url> ?body ?res ) resource:putIn []`
157
+
158
+ Example:
159
+ ```turtle
160
+ ( <http://example.com> ?res ) resource:getIn [] .
161
+ ```
162
+
163
+ #### Request Bodies
164
+
165
+ `api-tuner` supports different types of request bodies:
166
+
167
+ 1. **Inline RDF**: Uses an N3 formula. It is serialized as Turtle and sent with `Content-Type: text/turtle`.
168
+ ```turtle
169
+ tuner:body { <#s> <#p> <#o> }
170
+ ```
171
+ 2. **File Reference**: Sends the contents of a local file.
172
+ ```turtle
173
+ tuner:body <file:data.json>
174
+ ```
175
+ 3. **Multipart Form**:
176
+ ```turtle
177
+ tuner:body [
178
+ tuner:form ( "field1" "value1" ) ;
179
+ tuner:form ( "fileField" <file:photo.jpg> "image/jpeg" ) ;
180
+ ]
181
+ ```
182
+
183
+ #### Query Parameters
184
+
185
+ Query parameters can be added to a request using `tuner:query`.
186
+
187
+ ```turtle
188
+ ?req tuner:query ( "name" "value" ) .
189
+ ```
190
+
191
+ ### Assertions
192
+
193
+ Assertions are performed on the `tuner:Response` object (usually captured in a variable like `?res`).
194
+
195
+ #### Status Code
196
+
197
+ Assert the HTTP status code:
198
+ ```turtle
199
+ ?res tuner:http_code 200 .
200
+ ```
201
+
202
+ #### Headers
203
+
204
+ Assert the presence and value of an HTTP header. Header names are case-insensitive.
205
+
206
+ - **Exact match**:
207
+ ```turtle
208
+ ?res tuner:header ( "Content-Type" "application/json" ) .
209
+ ```
210
+ - **Regex match**:
211
+ ```turtle
212
+ ?res tuner:header ( "Content-Type" "application/.*" string:matches ) .
213
+ ```
214
+
215
+ #### Body
216
+
217
+ - **Raw body string**:
218
+ ```turtle
219
+ ?res tuner:body ?body .
220
+ ?body string:contains "Expected Text" .
221
+ ```
222
+ - **RDF Semantics**: If the response is RDF, you can use `log:includes` to check its content.
223
+ ```turtle
224
+ ?res tuner:body ?body.
225
+ ?body log:includes { <#s> <#p> <#o> } .
226
+ ```
227
+ ⚠️ Be careful when using `?res!log:includes` resource path shorthand which will not work inside `tuner:formula`. Please refer to [this discussion](https://github.com/eyereasoner/eye/issues/148#issuecomment-2810940959).
228
+
229
+ <<<<<<< json-assertions
230
+ - **JSON Path**: If the response is JSON, you can use `tuner:jsonPath` to assert values within the body.
231
+ ```turtle
232
+ ?res tuner:body ?body .
233
+ # Exact match
234
+ ?body tuner:jsonPath ( "$.foo" "bar" ) .
235
+ # Custom assertion (e.g. regex, contains, math)
236
+ ?body tuner:jsonPath ( "$.baz" "42" string:contains ) .
237
+ ```
238
+
239
+ =======
240
+ >>>>>>> master
241
+ #### Generic Assertions
242
+
243
+ Use `tuner:assertThat` to fail a test with a custom message if a condition is not met.
244
+
245
+ ```turtle
246
+ { ?value math:greaterThan 10 }!tuner:assertThat "Value should be greater than 10" .
247
+ ```
248
+
249
+ ### Utility Rules
250
+
251
+ #### Logging
252
+
253
+ Print messages to the console during test execution (depending on log level).
254
+
255
+ - `?message^tuner:info`: Prints an INFO message.
256
+ - `?message^tuner:trace`: Prints a DEBUG message.
257
+
258
+ Example:
259
+ ```turtle
260
+ "Starting request"^tuner:info .
261
+ ```
262
+
263
+ #### File Operations
264
+
265
+ - `() file:temp ?path`: Generates a temporary file path.
266
+ - `?path file:rm ?any`: Deletes a file.
267
+ - `?relative file:libPath ?absolute`: Resolves a path relative to the `api-tuner` library.
268
+
269
+ ### Debugging
270
+
271
+ Setting the `--debug` flag will print verbose response information. The `--raw` flag will print
272
+ the raw triples produced by the n3 rules.
273
+
274
+ Additionally, you can inspect the raw response files, which are written to the system's temp directory. The are prefixed
275
+ with `api-tuner`. Thus, you can list them with `ls -l "${TMPDIR:-/tmp}"/api-tuner*`, or upload to CI artifacts, as shown
276
+ in the GitHub Workflow step example below.
277
+
278
+ ```yaml
279
+ - run: npx api-tuner ...
280
+ env:
281
+ TMPDIR: ${{ runner.temp }}
282
+ - if: failure()
283
+ name: upload api-tuner response data
284
+ uses: actions/upload-artifact@v7
285
+ with:
286
+ name: api-tuner-debug
287
+ path: '${{ runner.temp }}/api-tuner*'
288
+ ```
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env bash
2
+
3
+ SCRIPT_DIR=$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)
4
+
5
+ # find JS entrypoint
6
+ executable="$SCRIPT_DIR/../lib/jsonpath.js"
7
+
8
+ # if tsx exists in path
9
+ if command -v tsx > /dev/null 2>&1
10
+ then
11
+ # use tsx
12
+ node --import tsx --no-warnings "$executable" "$@"
13
+ else
14
+ # use plain node
15
+ node "$executable" "$@"
16
+ fi
@@ -0,0 +1,23 @@
1
+ /* eslint-disable no-console */
2
+ import { JSONPath } from 'jsonpath-plus';
3
+ import getStream from 'get-stream';
4
+ async function main() {
5
+ const path = process.argv[2];
6
+ if (!path) {
7
+ console.error('Usage: node jsonpath.js <path>');
8
+ process.exit(1);
9
+ }
10
+ const jsonString = await getStream(process.stdin);
11
+ try {
12
+ const json = JSON.parse(jsonString);
13
+ const result = JSONPath({ path, json, wrap: false });
14
+ if (result !== undefined && result !== null) {
15
+ process.stdout.write(JSON.stringify(result));
16
+ }
17
+ }
18
+ catch (e) {
19
+ console.error('Error parsing JSON or executing JSONPath:', e.message);
20
+ process.exit(1);
21
+ }
22
+ }
23
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api-tuner",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,7 +28,6 @@
28
28
  "rules"
29
29
  ],
30
30
  "dependencies": {
31
- "@changesets/cli": "^2.29.7",
32
31
  "@jeswr/pretty-turtle": "^1.8.2",
33
32
  "@sindresorhus/merge-streams": "^4.0.0",
34
33
  "@zazuko/env-node": "^3",
@@ -36,11 +35,13 @@
36
35
  "get-stream": "^9.0.1",
37
36
  "is-absolute-url": "^4.0.1",
38
37
  "jsonld": "^9",
38
+ "jsonpath-plus": "^10.2.0",
39
39
  "rdf-validate-shacl": "^0.6.5",
40
40
  "replacestream": "^4.0.3",
41
41
  "tar": "^7.5.7"
42
42
  },
43
43
  "devDependencies": {
44
+ "@changesets/cli": "^2.31.0",
44
45
  "@rdfjs/types": "^2",
45
46
  "@tpluscode/eslint-config": "^0.5.0",
46
47
  "@types/jsonld": "^1.5.15",
@@ -53,7 +54,7 @@
53
54
  "eslint": "^8.57.1",
54
55
  "eslint-import-resolver-typescript": "^4.3.4",
55
56
  "husky": "^9.1.7",
56
- "lint-staged": "^15.4.3",
57
+ "lint-staged": "^16.4.0",
57
58
  "tsx": "^4.19.3",
58
59
  "typescript": "^5.8.3"
59
60
  },
@@ -73,6 +73,31 @@ prefix log: <http://www.w3.org/2000/10/swap/log#>
73
73
  ) log:ifThenElseIn [] .
74
74
  } .
75
75
 
76
+ {
77
+ ?body tuner:jsonPath ( ?path ?expected ) .
78
+ } <= {
79
+ ?body tuner:jsonPath ( ?path ?expected log:equalTo ) .
80
+ } .
81
+
82
+ {
83
+ ?body tuner:jsonPath ( ?path ?expected ?builtIn ) .
84
+ } <= {
85
+ ?body log:rawType log:Literal .
86
+ ( "echo " ?body " | bin/jsonpath.sh '" ?path "' " )!string:concatenation log:shell ?actualRaw .
87
+
88
+ # remove surrounding quotes added by the shell
89
+ ( ( ?actualRaw '^\"' "")!string:replace '\"$' "") string:replace ?actual .
90
+
91
+ (
92
+ { ?actual ?builtIn ?expected }
93
+ true
94
+ {
95
+ ("Expected JSON path '" ?path "' to satisfy '" ?builtIn " " ?expected "' but got '" ?actual "'")!string:concatenation^tuner:info .
96
+ true log:equalTo false .
97
+ }
98
+ ) log:ifThenElseIn ?SCOPE .
99
+ } .
100
+
76
101
  {
77
102
  ?res tuner:header ( ?name ?value ) .
78
103
  } <= {
@@ -30,6 +30,14 @@ prefix math: <http://www.w3.org/2000/10/swap/math#>
30
30
  ( " --data-binary " ?fileUrl!file:curlFileReference ) string:concatenation ?curlArgs .
31
31
  } .
32
32
 
33
+ {
34
+ ?literalBody </#body> ( ?curlArgs [] ) .
35
+ } <= {
36
+ ?literalBody log:rawType log:Literal .
37
+
38
+ ( " -d '" ?literalBody "'" ) string:concatenation ?curlArgs .
39
+ } .
40
+
33
41
  {
34
42
  ?multipartBody </#body> ( ?curlArgs [] ) .
35
43
  } <= {
package/rules/files.n3 CHANGED
@@ -15,7 +15,7 @@ prefix file: <http://www.w3.org/2000/10/swap/file#>
15
15
  } <= {
16
16
  ?uri log:uri ( "urn:rand:" ( 1000 )!e:random )!string:concatenation .
17
17
  # log:shell captures the traling newline
18
- ( "mktemp -d"!log:shell "\n" "" ) string:replace ?tempDir .
18
+ ( "mktemp -t api-tuner -d 2>/dev/null || mktemp -d -t api-tuner.XXXXXXXX"!log:shell "\n" "" ) string:replace ?tempDir .
19
19
 
20
20
  (
21
21
  ?tempDir