diff --git a/.existdb.json.tmpl b/.existdb.json.tmpl
index 63cbcebe..511a7a6c 100644
--- a/.existdb.json.tmpl
+++ b/.existdb.json.tmpl
@@ -20,6 +20,7 @@
"Dockerfile",
"devel/**",
"test/**",
+ "schemathesis-results.xml",
"*.xar"
]
}
diff --git a/.github/workflows/test_api.yml b/.github/workflows/schemathesis.yml
similarity index 63%
rename from .github/workflows/test_api.yml
rename to .github/workflows/schemathesis.yml
index cbabd97d..56b1f879 100644
--- a/.github/workflows/test_api.yml
+++ b/.github/workflows/schemathesis.yml
@@ -1,4 +1,4 @@
-name: Test API
+name: Schemathesis Tests
on:
push:
@@ -8,6 +8,7 @@ on:
permissions:
contents: read
+ pull-requests: write
env:
COMPOSE_FILE: compose.yml:compose.t.yml
@@ -15,10 +16,11 @@ env:
FORCE_COLOR: 1
jobs:
- test_api:
+ schemathesis:
+ name: Schemathesis
runs-on: ubuntu-22.04
steps:
- - uses: actions/checkout@v7
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- run: docker --version
- name: Start the docker-compose stack
run: docker compose up --detach --wait
@@ -31,7 +33,7 @@ jobs:
--url http://localhost:8081/exist/restxq/v1/corpora \
--user admin: \
--header 'Content-Type: application/json' \
- --data @./corpora/test.json
+ --data @./test/test.json
- name: Load data for test corpus
run: |
curl \
@@ -46,21 +48,13 @@ jobs:
run: curl --verbose http://localhost:8081/exist/restxq/v1/corpora/test
- name: Test API with schemathesis
- # Refs:
- # https://github.com/schemathesis/action#configuration
- # https://schemathesis.readthedocs.io/en/stable/cli.html
- uses: schemathesis/action@95849c1d1e806909cca98b7bf031bf47d552cd5b # v1.1.1
+ uses: schemathesis/action@806cace2053cbbac93188e1281ff7da415643160 # v3.0.0
with:
schema: http://localhost:8081/exist/restxq/v1/openapi.yaml
base-url: http://localhost:8081/exist/restxq/v1/
- # OPTIONAL. List of Schemathesis checks to run. Defaults to `all`
- # if multiple checks should be used, use a comma seperated string,
- # e.g. "not_a_server_error,status_code_conformance"
- checks: not_a_server_error,content_type_conformance,response_headers_conformance,response_schema_conformance
- # OPTIONAL. Maximum number of generated examples for each endpoint
+ authorization: 'Basic YWRtaW46'
max-examples: 100
- # OPTIONAL. Extra arguments to pass to Schemathesis
- args: "--auth=admin: --workers=2 --include-method=GET"
+ args: "--include-method GET --mode positive --request-timeout 30"
- name: Stop the docker-compose stack
run: docker compose down
diff --git a/.gitignore b/.gitignore
index 667279ec..d4125278 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,5 +5,6 @@ devel/
expath-pkg.xml
local.build.properties
*.xar
+schemathesis-results.*
.hypothesis
.claude
\ No newline at end of file
diff --git a/api.yaml b/api.yaml
index 99d66543..95290812 100644
--- a/api.yaml
+++ b/api.yaml
@@ -980,6 +980,7 @@ paths:
required: true
schema:
type: string
+ pattern: '^Q[0-9]+$'
example: Q131412
responses:
'200':
@@ -1031,7 +1032,7 @@ paths:
- name: id
in: query
required: false
- description: Identifier for a collection or document. Preferably the full URI of a corpus or a play, but shorter DraCor IDs are also supported.
+ description: Identifier for a collection. Preferably the full URI of a corpus, but shorter DraCor corpus IDs are also supported.
schema:
type: string
examples:
@@ -1113,6 +1114,7 @@ paths:
description: The unique identifier for the Resource whose tree or subtree must be returned. Should be the full URI of a play (recommended) or the DraCor ID.
schema:
type: string
+ minLength: 1
example: https://dracor.org/id/ger000088
- name: ref
in: query
@@ -1181,6 +1183,7 @@ paths:
description: The unique identifier for the Resource whose tree or subtree must be returned. Should be the full URI of a play (recommended) or the DraCor ID.
schema:
type: string
+ minLength: 1
example: https://dracor.org/id/ger000088
- name: ref
in: query
diff --git a/modules/api.xqm b/modules/api.xqm
index 82bd8fba..58ad5b0c 100644
--- a/modules/api.xqm
+++ b/modules/api.xqm
@@ -257,6 +257,12 @@ declare
%output:media-type("application/json")
%output:method("json")
function api:corpora($include) {
+ if ($include[1] and not($include[1] = "metrics")) then
+ (
+ ,
+ map {"error": "invalid value for parameter 'include'"}
+ )
+ else
array {
(: DEPRECATED: remove teiCorpus support in v2 :)
for $corpus in collection($config:corpora-root)/(tei:dracorCorpus|tei:teiCorpus)
@@ -266,7 +272,7 @@ function api:corpora($include) {
return map:merge ((
$info,
map:entry("uri", $config:api-base || '/corpora/' || $name),
- if ($include = "metrics") then (
+ if ($include[1] = "metrics") then (
map:entry("metrics", local:get-corpus-metrics($name))
) else ()
@@ -1270,6 +1276,11 @@ function api:spoken-text(
$corpusname, $playname, $sex, $role, $relation, $relation-active,
$relation-passive
) {
+ let $sex := $sex[1]
+ let $role := $role[1]
+ let $relation := $relation[1]
+ let $relation-active := $relation-active[1]
+ let $relation-passive := $relation-passive[1]
let $doc := dutil:get-doc($corpusname, $playname)
let $sexes := tokenize($sex, ',')
return
@@ -1500,6 +1511,8 @@ declare
%output:media-type("application/json")
%output:method("json")
function api:authorInfo($id, $lang) {
+ let $lang := $lang[1]
+ return
if (not(matches($id, '^Q[0-9]+$'))) then
(
diff --git a/modules/dts.xqm b/modules/dts.xqm
index c80e2744..b00fe098 100644
--- a/modules/dts.xqm
+++ b/modules/dts.xqm
@@ -191,6 +191,16 @@ declare
%output:method("json")
function ddts:collections($id as xs:string*, $page as xs:string*, $nav as xs:string*)
as item()+ {
+ let $id := $id[1]
+ let $page := $page[1]
+ let $nav := $nav[1]
+ return
+ if ($nav and $nav != "parents") then
+ (
+ ,
+ "The value '" || $nav || "' of the parameter 'nav' is not allowed. Use the single allowed value 'parents' if you want to request the parent collection."
+ )
+ else
(: check, if param $id is set -- request a certain collection :)
if ( $id ) then
@@ -349,9 +359,9 @@ as item()+ {
(: test: http://localhost:8088/api/v1/dts/collection?id=http://localhost:8088/id/ger12345 :)
(
-
+
,
- "The value '" || $id || "' of the parameter 'id' is not in a valid format: Either provide the id or URI of a corpus or play."
+ "The value '" || $id || "' of the parameter 'id' does not identify a known collection. Provide the name or URI of a corpus."
)
else
@@ -652,28 +662,35 @@ as item()+
:)
declare function local:child-readable-collection-with-parent-by-id($id as xs:string)
-as map()
+as item()+
{
- let $self := local:child-readable-collection-by-id($id)
-
- (: get parent collection and remove the members :)
-
- (: TODO: maybe use a dutil:function instead; this is very custom; not sure how this will
- work when something is changed in the general API code
- :)
let $file-db-path := util:collection-name(collection($config:corpora-root)/tei:TEI[@xml:id eq $id])
let $corpusname := tokenize(replace($file-db-path, "/db/dracor/corpora/",""),"/")[1]
+ return
+ if (not($corpusname)) then
+ (
+ ,
+ "Resource '" || $id || "' does not exist!"
+ )
+ else
+ let $self := local:child-readable-collection-by-id($id)
- (: get the parent by the function to generate a collection :)
- let $parent := local:corpus-to-collection($corpusname)
+ (: get parent collection and remove the members :)
- (: remove "members" and "@context" :)
- let $parent-without-members := map:remove($parent,"member")
- let $parent-without-context := map:remove($parent-without-members, "@context")
- let $members := map {"member" : array { $parent-without-context }}
+ (: TODO: maybe use a dutil:function instead; this is very custom; not sure how this will
+ work when something is changed in the general API code
+ :)
- return
- map:merge(($self, $members))
+ (: get the parent by the function to generate a collection :)
+ let $parent := local:corpus-to-collection($corpusname)
+
+ (: remove "members" and "@context" :)
+ let $parent-without-members := map:remove($parent,"member")
+ let $parent-without-context := map:remove($parent-without-members, "@context")
+ let $members := map {"member" : array { $parent-without-context }}
+
+ return
+ map:merge(($self, $members))
};
@@ -894,7 +911,7 @@ as item() {
: $tree
: $mediaType
:
- : Params used in POST, PUT, DELETE requests are not availiable
+ : Params used in POST, PUT, DELETE requests are not available
:)
@@ -925,23 +942,22 @@ declare
%output:media-type("application/xml")
%output:method("xml")
function ddts:document($resource, $ref, $start, $end, $tree, $media-type) {
+ let $resource := $resource[1]
+ let $ref := $ref[1]
+ (: ref takes precedence: if ref is set, start and end are ignored :)
+ let $start := if ($ref) then () else $start[1]
+ let $end := if ($ref) then () else $end[1]
+ (: start and end must both be present for a range; if only one is given, ignore both :)
+ let $start := if ($start and $end) then $start else ()
+ let $end := if ($start and $end) then $end else ()
+ let $media-type := $media-type[1]
+ return
(: TODO: to properly implement a possibility to request a document or fragment in a different media type
we would need to set the serialization parameters rest:produces, output: ... dynamically :)
(: check, if valid request :)
- (: In GET requests one may either provide a ref parameter or a pair of start and end parameters. A request cannot combine ref with the other two. If, say, a ref and a start are both provided this should cause the request to fail. :)
- if ( $ref and ( $start or $end ) ) then
- (
-
-
- ,
-
- Bad Request
- GET requests may either have a 'ref' parameter or a pair of 'start' and 'end' parameters. A request cannot combine 'ref' with the other two.
-
- )
- else if ( ($start and not($end) ) or ( $end and not($start) ) ) then
+ if ( ($start and not($end) ) or ( $end and not($start) ) ) then
(: requesting a range, should check, if start and end is present :)
(
@@ -1405,7 +1421,7 @@ declare function local:collections-self-describe() {
,
Bad Request
- Should at least use the required parameter 'id'. Automatic self description is not availiable.
+ Should at least use the required parameter 'id'. Automatic self description is not available.
)
};
@@ -1788,6 +1804,17 @@ declare function local:link-header-of-fragment($tei as element(tei:TEI), $ref as
%output:media-type("application/ld+json")
%output:method("json")
function ddts:navigation($resource, $ref, $start, $end, $level, $down) {
+ let $resource := $resource[1]
+ let $ref := $ref[1]
+ (: ref takes precedence: if ref is set, start and end are ignored :)
+ let $start := if ($ref) then () else $start[1]
+ let $end := if ($ref) then () else $end[1]
+ (: start and end must both be present for a range; if only one is given, ignore both :)
+ let $start := if ($start and $end) then $start else ()
+ let $end := if ($start and $end) then $end else ()
+ (: default to top-level navigation when no navigation parameter is specified :)
+ let $down := if ($down[1]) then $down[1] else if (not($ref) and not($start)) then "1" else ()
+ return
(: parameter $id is mandatory :)
if ( not($resource) ) then
(
@@ -1796,14 +1823,6 @@ declare function local:link-header-of-fragment($tei as element(tei:TEI), $ref as
,
"Mandatory parameter 'resource' is missing."
)
- (: both ref and either start or end is specified should return an error :)
- else if ( $ref and ($start or $end) ) then
- (
-
-
- ,
- "Bad Request: Use of both parameters 'ref' and 'start' or 'end' is not allowed."
- )
(: should not use start without end or vice versa :)
else if ( ($start and not($end)) or ($end and not($start)) ) then
(
@@ -2521,7 +2540,7 @@ declare function local:snippet($tei-fragment as element()) {
(: end of level2 :)
else
- (: this level is not implemented/not availiable :)
+ (: this level is not implemented/not available :)
(
diff --git a/test/schemathesis.sh b/test/schemathesis.sh
index ec4e1e11..43af593e 100755
--- a/test/schemathesis.sh
+++ b/test/schemathesis.sh
@@ -1,8 +1,10 @@
# This should reproduce the test in the GitHub workflow.
schemathesis run http://localhost:8081/exist/restxq/v1/openapi.yaml \
- --exclude-checks status_code_conformance \
- --report \
- --hypothesis-max-examples 50 \
+ --include-method GET \
+ --mode positive \
+ --max-examples 50 \
+ --request-timeout 30 \
--auth admin: \
- --method GET
+ --report junit \
+ --report-junit-path schemathesis-results.xml
diff --git a/test/test.json b/test/test.json
new file mode 100644
index 00000000..55e943b6
--- /dev/null
+++ b/test/test.json
@@ -0,0 +1,5 @@
+{
+ "name": "test",
+ "title": "Test Corpus",
+ "repository": "https://github.com/dracor-org/testdracor"
+}
\ No newline at end of file