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