api-tuner 0.5.4 → 0.6.0

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,6 +89,180 @@ api-tuner test.n3
77
89
 
78
90
  ## More examples
79
91
 
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 Path**: If the response is JSON, you can use `tuner:jsonPath` to assert values within the body.
230
+ ```turtle
231
+ # Exact match
232
+ ?res tuner:jsonPath ( "$.foo" "bar" ) .
233
+ # Custom assertion (e.g. regex, contains, math)
234
+ ?res tuner:jsonPath ( "$.baz" "42" string:contains ) .
235
+ ```
236
+ ⚠️ Note that unlike RDF assersion, JSON Path assertions are used on the response itself, without using `tuner:body`.
237
+
238
+ #### Generic Assertions
239
+
240
+ Use `tuner:assertThat` to fail a test with a custom message if a condition is not met.
241
+
242
+ ```turtle
243
+ { ?value math:greaterThan 10 }!tuner:assertThat "Value should be greater than 10" .
244
+ ```
245
+
246
+ ### Utility Rules
247
+
248
+ #### Logging
249
+
250
+ Print messages to the console during test execution (depending on log level).
251
+
252
+ - `?message^tuner:info`: Prints an INFO message.
253
+ - `?message^tuner:trace`: Prints a DEBUG message.
254
+
255
+ Example:
256
+ ```turtle
257
+ "Starting request"^tuner:info .
258
+ ```
259
+
260
+ #### File Operations
261
+
262
+ - `() file:temp ?path`: Generates a temporary file path.
263
+ - `?path file:rm ?any`: Deletes a file.
264
+ - `?relative file:libPath ?absolute`: Resolves a path relative to the `api-tuner` library.
265
+
80
266
  ### Debugging
81
267
 
82
268
  Setting the `--debug` flag will print verbose response information. The `--raw` flag will print
@@ -87,6 +273,9 @@ with `api-tuner`. Thus, you can list them with `ls -l "${TMPDIR:-/tmp}"/api-tune
87
273
  in the GitHub Workflow step example below.
88
274
 
89
275
  ```yaml
276
+ - run: npx api-tuner ...
277
+ env:
278
+ TMPDIR: ${{ runner.temp }}
90
279
  - if: failure()
91
280
  name: upload api-tuner response data
92
281
  uses: actions/upload-artifact@v7
@@ -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.4",
3
+ "version": "0.6.0",
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
  },
@@ -3,6 +3,7 @@ PREFIX list: <http://www.w3.org/2000/10/swap/list#>
3
3
  PREFIX tuner: <https://api-tuner.described.at/>
4
4
  prefix string: <http://www.w3.org/2000/10/swap/string#>
5
5
  prefix log: <http://www.w3.org/2000/10/swap/log#>
6
+ prefix file: <http://www.w3.org/2000/10/swap/file#>
6
7
 
7
8
  {
8
9
  ?expr tuner:assertThat ?msg .
@@ -73,6 +74,33 @@ prefix log: <http://www.w3.org/2000/10/swap/log#>
73
74
  ) log:ifThenElseIn [] .
74
75
  } .
75
76
 
77
+ {
78
+ ?res tuner:jsonPath ( ?path ?expected ) .
79
+ } <= {
80
+ ?res tuner:jsonPath ( ?path ?expected log:equalTo ) .
81
+ } .
82
+
83
+ {
84
+ ?res tuner:jsonPath ( ?path ?expected ?builtIn ) .
85
+ } <= {
86
+ ?res log:includes {
87
+ [] a tuner:Response ; tuner:body ?bodyPath .
88
+ } .
89
+ ( "cat " ?bodyPath " | " "bin/jsonpath.sh"!file:libPath " '" ?path "' " )!string:concatenation log:shell ?actualRaw .
90
+
91
+ # remove surrounding quotes added by the shell
92
+ ( ( ?actualRaw '^\"' "")!string:replace '\"$' "") string:replace ?actual .
93
+
94
+ (
95
+ { ?actual ?builtIn ?expected }
96
+ true
97
+ {
98
+ ("Expected JSON path '" ?path "' to satisfy '" ?builtIn " " ?expected "' but got '" ?actual "'")!string:concatenation^tuner:info .
99
+ true log:equalTo false .
100
+ }
101
+ ) log:ifThenElseIn ?SCOPE .
102
+ } .
103
+
76
104
  {
77
105
  ?res tuner:header ( ?name ?value ) .
78
106
  } <= {
@@ -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
  } <= {