diff --git a/content/router.xql b/content/router.xql
index 02133f1..cdc8865 100644
--- a/content/router.xql
+++ b/content/router.xql
@@ -239,59 +239,105 @@ function router:middleware-reducer (
array { apply($next-middleware, $args) }
};
-(:~
- : Try to retrieve and convert the request body if specified
- :)
-declare function router:body ($request as map(*)) {
- if (
- not(map:contains($request?config, "requestBody")) or
- not(map:contains($request?config?requestBody, "content"))
- )
- then () (: this route expects no body, return an empty sequence :)
+declare %private function router:content-type ($request as map(*)) as map(*) {
+ if (not(exists($request?config?requestBody?content)))
+ then (map{}) (: this route expects no body, return an empty map :)
+ else if (not($request?config?requestBody?content instance of map(*)))
+ then error($errors:OPERATION, "requestBody.content is not defined correctly", $request?config)
else (
- let $content-type-header := (: strip charset info from mime-type if present :)
- request:get-header("Content-Type")
- => replace("^([^;]+);?.*$", "$1")
+ let $defined-content-types := map:keys($request?config?requestBody?content)
+
+ let $raw-content-type-header-value := request:get-header("Content-Type")
+ let $media-type :=
+ if (contains($raw-content-type-header-value, ";"))
+ then substring-before($raw-content-type-header-value, ";")
+ else $raw-content-type-header-value
- let $definition := $request?config?requestBody?content
+ let $charset :=
+ if (contains($raw-content-type-header-value, "charset="))
+ then (
+ substring-after($raw-content-type-header-value, "charset=")
+ => lower-case()
+ => replace("^([a-z0-9\-]+).*$", "$1")
+ )
+ else ()
+
+ let $registry := substring-before($media-type, "/")
+ let $format-hint := substring-after($media-type, "+")
return
if (
- map:contains($definition, $content-type-header) or
- map:contains($definition, "*/*")
+ $media-type = $defined-content-types or (
+ $defined-content-types = "*/*" and
+ $registry = (
+ "application", "audio", "example", "font", "image",
+ "message", "model", "multipart", "text", "video"
+ )
+ )
)
- then (
- switch ($content-type-header)
- case "multipart/form-data" return
- () (: TODO: implement form-data handling? :)
- case "application/json" return
- try {
- request:get-data() => util:binary-to-string() => parse-json()
- }
- catch * {
- error($errors:BODY_CONTENT_TYPE, "Invalid JSON", $err:description)
- }
- case "application/xml" return
- try {
- (:
- : workaround for eXist-DB specific behaviour,
- : this way we will get parse errors as early as possible
- : while still having access to the data afterwards
- :)
- let $data := request:get-data()
- let $_test := parse-xml($data)
- (: return root node instead of a document fragment :)
- return $data/node()
- }
- catch * {
- error($errors:BODY_CONTENT_TYPE, "Invalid XML", $err:description)
- }
- default return
- request:get-data()
+ then map {
+ "media-type": $media-type,
+ "charset": $charset,
+ "registry": $registry,
+ "format":
+ if ($media-type = ("application/json", "text/json"))
+ then "json"
+ else if ($media-type = ("application/xml", "text/xml"))
+ then "xml"
+ else if ($media-type = "multipart/form-data")
+ then "form-data"
+ else if ($format-hint)
+ then $format-hint
+ else "binary"
+ }
+ else error(
+ $errors:BODY_CONTENT_TYPE,
+ "Body with media-type '" || $media-type || "' is not allowed",
+ $request
)
- else
- error($errors:BODY_CONTENT_TYPE,
- "Passed in Content-Type " || $content-type-header || " not allowed")
+ )
+};
+
+(:~
+ : Try to retrieve and convert the request body if specified
+ :)
+declare function router:body ($request as map(*)) {
+ if (not(exists($request?media-type)))
+ then () (: this route expects no body, return an empty sequence :)
+ else (
+ try {
+ switch ($request?format)
+ (: Q: do we need to handle form-data? :)
+ case "form-data" return ()
+ (:
+ Parse body contents to XQuery data structure for media types
+ that were identified as being in JSON format.
+ NOTE: The data needs to be serialized again before it can be stored.
+ :)
+ case "json" return
+ request:get-data()
+ => util:binary-to-string()
+ => parse-json()
+ (:
+ Workaround for eXist-DB specific behaviour,
+ this way we will get parse errors as early as possible
+ while still having access to the data afterwards.
+ NOTE: Returns the root node instead of a document (fragment).
+ :)
+ case "xml" return
+ let $data := request:get-data()
+ let $_test := parse-xml($data)
+ return $data/node()
+ (: Treat everything else as binary data :)
+ default return request:get-data()
+ }
+ catch * {
+ error(
+ $errors:BODY_CONTENT_TYPE,
+ "Body with media type '" || $request?media-type || "' could not be parsed (invalid " || upper-case($request?format) || ").",
+ $err:description
+ )
+ }
)
};
@@ -304,7 +350,8 @@ declare %private function router:execute-handler ($base-request as map(*), $use,
error($errors:OPERATION, "Operation does not define an operationId", $base-request?config)
else
try {
- let $request-with-body := map:put($base-request, "body", router:body($base-request))
+ let $request-with-content-type := map:merge(($base-request, router:content-type($base-request)))
+ let $request-with-body := map:put($request-with-content-type, "body", router:body($request-with-content-type))
let $request-response-array :=
fold-left($use, [$request-with-body, map {}], router:middleware-reducer#2)
@@ -523,18 +570,35 @@ declare %private function router:safe-set-header ($header as xs:string, $value a
(:~
: Q: binary types?
+ : XSLT default values: "xml", "xhtml", "html", "text", "json", "adaptive"
+ : "html5" is an eXist-DB provided extension
:)
declare %private function router:method-for-content-type ($type as xs:string) as xs:string {
- switch($type)
- case "application/json" return "json"
- case "text/html" return "html5"
- case "text/text" return "text"
- case "application/octet-stream" return "text"
- case "application/xml" return "xml"
- default return "xml"
+ switch (substring-before($type, "/"))
+ case "application" return
+ if (ends-with($type, "json")) then "json" (: matches application/json and any type ending in +json :)
+ else if (ends-with($type, "xhtml+xml")) then "xhtml"
+ else if (ends-with($type, "xml")) then "xml" (: matches application/xml and any type ending in +xml :)
+ else "text"
+ case "text" return
+ if (ends-with($type, "/xml")) then "xml"
+ else if (ends-with($type, "/html")) then "html5"
+ else "text"
+ case "image" return
+ if (ends-with($type, "/svg+xml")) then "xml"
+ else "text"
+ case "multipart"
+ case "audio"
+ case "font"
+ case "example"
+ case "message"
+ case "model"
+ case "video" return
+ "text" (: assume binary content :)
+ default return
+ error($errors:OPERATION, "Unknown media type '" || $type || '"')
};
-
(: helpers :)
declare %private function router:is-response-map($value as item()*) as xs:boolean {
diff --git a/icon.svg b/icon.svg
index 846f231..4ad5144 100644
--- a/icon.svg
+++ b/icon.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/test/app/api.json b/test/app/api.json
index 23e28c3..f99d62a 100644
--- a/test/app/api.json
+++ b/test/app/api.json
@@ -468,6 +468,24 @@
}
}
},
+ "/api/avatar": {
+ "get": {
+ "summary": "Retrieve avatar as SVG",
+ "operationId": "api:avatar",
+ "responses": {
+ "200": {
+ "description": "Not found",
+ "content": {
+ "image/svg+xml":{
+ "schema": {
+ "type": "object"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"/api/errors/handle": {
"get": {
"summary": "Test error handler",
diff --git a/test/app/modules/api.xql b/test/app/modules/api.xql
index 106d6e1..493a6d5 100644
--- a/test/app/modules/api.xql
+++ b/test/app/modules/api.xql
@@ -2,6 +2,7 @@ xquery version "3.1";
declare namespace api="http://e-editiones.org/roasted/test-api";
declare namespace output="http://www.w3.org/2010/xslt-xquery-serialization";
+declare namespace svg="http://www.w3.org/2000/svg";
import module namespace roaster="http://e-editiones.org/roaster";
@@ -98,6 +99,17 @@ declare function api:get-uploaded-data ($request as map(*)) {
)
};
+declare function api:avatar ($request as map(*)) {
+
+};
(: end of route handlers :)
@@ -110,4 +122,5 @@ declare function api:lookup ($name as xs:string) {
function-lookup(xs:QName($name), 1)
};
+(: util:declare-option("output:indent", "no"), :)
roaster:route($api:definitions, api:lookup#1)
diff --git a/test/auth.test.js b/test/auth.test.js
new file mode 100644
index 0000000..3b689c6
--- /dev/null
+++ b/test/auth.test.js
@@ -0,0 +1,41 @@
+const util = require('./util.js')
+const chai = require('chai')
+const expect = chai.expect
+
+describe('On Login', function () {
+ let response
+
+ before(async function () {
+ await util.login()
+ response = await util.axios.get('api/parameters', {})
+ })
+
+ it('public route can be called', function () {
+ expect(response.status).to.equal(200);
+ })
+
+ it('user property is set on request map', function () {
+ expect(response.data.user).to.be.a('object')
+ expect(response.data.user.name).to.equal("admin")
+ expect(response.data.user.dba).to.equal(true)
+ })
+
+ describe('On logout', function () {
+ let logoutResponse
+ let guestResponse
+ before(async function () {
+ logoutResponse = await util.axios.get('logout')
+ guestResponse = await util.axios.get('api/parameters', {})
+ })
+ it('request returns true', function () {
+ expect(logoutResponse.status).to.equal(200)
+ expect(logoutResponse.data.success).to.equal(true)
+ })
+ it('public route sets guest as user', function () {
+ expect(guestResponse.status).to.equal(200)
+ expect(guestResponse.data.user.name).to.equal("guest")
+ expect(guestResponse.data.user.dba).to.equal(false)
+ })
+
+ })
+})
diff --git a/test/error.test.js b/test/error.test.js
new file mode 100644
index 0000000..0c3f75a
--- /dev/null
+++ b/test/error.test.js
@@ -0,0 +1,41 @@
+const util = require('./util.js')
+const chai = require('chai')
+const expect = chai.expect
+
+describe('Error reporting', function() {
+ it('receives error report', function() {
+ return util.axios.get('api/errors')
+ .catch(function(error) {
+ expect(error.response.status).to.equal(404)
+ expect(error.response.data.description).to.equal('document not found')
+ expect(error.response.data.value).to.equal('error details')
+ })
+ })
+
+ it('receives dynamic XQuery error', function() {
+ return util.axios.post('api/errors')
+ .catch(function(error) {
+ expect(error.response.status).to.equal(500)
+ expect(error.response.data.line).to.match(/\d+/)
+ expect(error.response.data.description).to.contain('$undefined')
+ })
+ })
+
+ it('receives explicit error', function() {
+ return util.axios.delete('api/errors')
+ .catch(function(error) {
+ expect(error.response.status).to.equal(403)
+ expect(error.response.headers['content-type']).to.equal('application/xml')
+ expect(error.response.data).to.equal('')
+ })
+ })
+
+ it('calls error handler', function() {
+ return util.axios.get('api/errors/handle')
+ .catch(function(error) {
+ expect(error.response.status).to.equal(500)
+ expect(error.response.headers['content-type']).to.equal('text/html')
+ expect(error.response.data).to.contain('$undefined')
+ })
+ })
+})
diff --git a/test/mediatype.test.js b/test/mediatype.test.js
new file mode 100644
index 0000000..453c0ee
--- /dev/null
+++ b/test/mediatype.test.js
@@ -0,0 +1,303 @@
+const util = require('./util.js')
+const chai = require('chai')
+const expect = chai.expect
+
+const fs = require('fs')
+const dbUploadCollection = '/db/apps/roasted/uploads/'
+
+describe("Binary up and download", function () {
+ const contents = fs.readFileSync("./roasted.xar")
+
+ describe("using basic authentication", function () {
+ const filename = 'roasted.xar'
+ it('handles post of binary data', async function () {
+ const res = await util.axios.post('api/paths/' + filename, contents, {
+ headers: {
+ 'Content-Type': 'application/octet-stream',
+ 'Authorization': 'Basic YWRtaW46'
+ }
+ })
+ expect(res.status).to.equal(201)
+ expect(res.data).to.equal(dbUploadCollection + filename)
+ })
+ it('retrieves the data', async function () {
+ const res = await util.axios.get('api/paths/' + filename, { responseType: 'arraybuffer' })
+ expect(res.status).to.equal(200)
+ expect(res.data).to.eql(contents)
+ })
+ })
+
+ describe("using cookie authentication", function () {
+ const filename = "roasted2.xar"
+ before(async function () {
+ await util.login()
+ })
+ after(async function () {
+ await util.logout()
+ })
+
+ it('handles post of binary data', async function () {
+ const res = await util.axios.post('api/paths/' + filename, contents, {
+ headers: { 'Content-Type': 'application/octet-stream' }
+ })
+ expect(res.status).to.equal(201)
+ expect(res.data).to.equal(dbUploadCollection + filename)
+ })
+ it('retrieves the data', async function () {
+ const res = await util.axios.get('api/paths/' + filename, { responseType: 'arraybuffer' })
+ expect(res.status).to.equal(200)
+ expect(res.data).to.eql(contents)
+ })
+ })
+})
+
+describe("body with content-type application/xml", function () {
+ before(async function () {
+ await util.login()
+ })
+ after(async function () {
+ await util.logout()
+ })
+
+ describe("with valid content", function () {
+ let uploadResponse
+ const filename = 'valid.xml'
+ const contents = Buffer.from('')
+ before(function () {
+ return util.axios.post('api/paths/' + filename, contents, {
+ headers: { 'Content-Type': 'application/xml' }
+ })
+ .then(r => uploadResponse = r)
+ .catch(e => {
+ console.error(e.response.data)
+ uploadResponse = e.response
+ })
+ })
+ it("is uploaded", function () {
+ expect(uploadResponse.status).to.equal(201)
+ expect(uploadResponse.data).to.equal(dbUploadCollection + filename)
+ })
+ it('can be retrieved', async function () {
+ const res = await util.axios.get('api/paths/' + filename, { responseType: 'arraybuffer' })
+ expect(res.status).to.equal(200)
+ expect(res.data).to.eql(contents)
+ })
+ })
+
+ // this "feature" is quite buggy and is only here to test for other encodings
+ // bottom line - don't use it
+ describe("with valid content encoded in latin1", function () {
+ let uploadResponse
+ const filename = 'latin1.xml'
+ const contents = Buffer.from(`
+<örtchen name="München" />`, 'latin1')
+ const dbNormalizedValue = '<örtchen name="München"/>'
+
+ before(function () {
+ return util.axios.post('api/paths/' + filename, contents, {
+ headers: { 'Content-Type': 'application/xml; charset=iso-8859-1' }
+ })
+ .then(r => uploadResponse = r)
+ .catch(e => {
+ console.error(e.response.data)
+ uploadResponse = e.response
+ })
+ })
+ it("is uploaded", function () {
+ expect(uploadResponse.status).to.equal(201)
+ expect(uploadResponse.data).to.equal(dbUploadCollection + filename)
+ })
+ it('can is stored encoded in UTF-8 and normalized', async function () {
+ const res = await util.axios.get('api/paths/' + filename, { responseType: 'arraybuffer' })
+ expect(res.status).to.equal(200)
+ expect(res.data.toString('utf-8')).to.equal(dbNormalizedValue)
+ })
+ })
+
+ describe("with invalid content", function () {
+ let uploadResponse
+ before(async function () {
+ return util.axios.post('api/paths/invalid.xml', Buffer.from('asdf'), {
+ headers: { 'Content-Type': 'application/xml' }
+ })
+ .then(r => uploadResponse = r)
+ .catch(e => uploadResponse = e.response)
+ })
+ it("is rejected as Bad Request", function () {
+ expect(uploadResponse.status).to.equal(400)
+ })
+ it("with the correct error code", function () {
+ expect(uploadResponse.data.code).to.equal('errors:BODY_CONTENT_TYPE')
+ })
+ it("with a human readable description", function () {
+ expect(uploadResponse.data.description).to.equal('Body with media type \'application/xml\' could not be parsed (invalid XML).')
+ })
+ })
+})
+
+describe("body with content-type application/tei+xml", function () {
+ before(async function () {
+ await util.login()
+ })
+ after(async function () {
+ await util.logout()
+ })
+
+ describe("with valid content", function () {
+ let uploadResponse
+ const filename = 'valid.tei.xml'
+ const contents = Buffer.from('')
+ before(function () {
+ return util.axios.post('api/paths/' + filename, contents, {
+ headers: { 'Content-Type': 'application/tei+xml' }
+ })
+ .then(r => uploadResponse = r)
+ .catch(e => {
+ console.error(e)
+ uploadResponse = e.response
+ })
+ })
+ it("is uploaded", function () {
+ expect(uploadResponse.status).to.equal(201)
+ expect(uploadResponse.data).to.equal(dbUploadCollection + filename)
+ })
+ it('can be retrieved', async function () {
+ const res = await util.axios.get('api/paths/' + filename, { responseType: 'arraybuffer' })
+ expect(res.status).to.equal(200)
+ expect(res.data).to.eql(contents)
+ })
+ })
+
+ describe("with invalid content", function () {
+ let uploadResponse
+ before(async function () {
+ return util.axios.post('api/paths/invalid.tei.xml', Buffer.from('asdf'), {
+ headers: { 'Content-Type': 'application/tei+xml' }
+ })
+ .then(r => uploadResponse = r)
+ .catch(e => uploadResponse = e.response)
+ })
+ it("is rejected as Bad Request", function () {
+ expect(uploadResponse.status).to.equal(400)
+ })
+ it("with the correct error code", function () {
+ expect(uploadResponse.data.code).to.equal('errors:BODY_CONTENT_TYPE')
+ })
+ it("with a human readable description", function () {
+ expect(uploadResponse.data.description).to.equal('Body with media type \'application/tei+xml\' could not be parsed (invalid XML).')
+ })
+ })
+})
+
+describe("body with content-type application/json", function () {
+ before(async function () {
+ await util.login()
+ })
+ after(async function () {
+ await util.logout()
+ })
+ describe("with valid content", function () {
+ let uploadResponse
+ const filename = 'valid.json'
+ const contents = Buffer.from('{"valid":[]}')
+ before(function () {
+ return util.axios.post(
+ 'api/paths/' + filename,
+ contents,
+ { headers: { 'Content-Type': 'application/json'} }
+ )
+ .then(r => uploadResponse = r)
+ .catch(e => uploadResponse = e.response)
+ })
+ it("is uploaded", function () {
+ expect(uploadResponse.status).to.equal(201)
+ expect(uploadResponse.data).to.equal(dbUploadCollection + filename)
+ })
+ it('can be retrieved', async function () {
+ const res = await util.axios.get('api/paths/' + filename, { responseType: 'arraybuffer' })
+ expect(res.status).to.equal(200)
+ expect(res.data).to.eql(contents)
+ })
+ })
+
+ describe("with invalid content", function () {
+ let uploadResponse
+ before(function () {
+ return util.axios.post(
+ 'api/paths/invalid.json',
+ '{"invalid: ()',
+ { headers: { 'Content-Type': 'application/json' } }
+ )
+ .then(r => uploadResponse = r)
+ .catch(e => uploadResponse = e.response)
+ })
+ it("is rejected as Bad Request", function () {
+ expect(uploadResponse.status).to.equal(400)
+ })
+ it("with the correct error code", function () {
+ expect(uploadResponse.data.code).to.equal('errors:BODY_CONTENT_TYPE')
+ })
+ it("with a human readable description", function () {
+ expect(uploadResponse.data.description).to.equal('Body with media type \'application/json\' could not be parsed (invalid JSON).')
+ })
+ })
+})
+
+describe("with invalid content-type header", function () {
+ let uploadResponse
+ before(function () {
+ return util.axios.post(
+ 'api/paths/invalid.stuff',
+ 'asd;lfkjdas;flkja',
+ {
+ headers: {
+ 'Content-Type': 'my/thing',
+ 'Authorization': 'Basic YWRtaW46'
+ }
+ }
+ )
+ .then(r => uploadResponse = r)
+ .catch(e => uploadResponse = e.response)
+ })
+ it("is rejected as Bad Request", function () {
+ expect(uploadResponse.status).to.equal(400)
+ })
+ it("with the correct error code", function () {
+ expect(uploadResponse.data.code).to.equal('errors:BODY_CONTENT_TYPE')
+ })
+ it("with a human readable description", function () {
+ expect(uploadResponse.data.description).to.equal('Body with media-type \'my/thing\' is not allowed')
+ })
+})
+
+describe("Retrieving an SVG image", function () {
+ let response
+ const avatarImage =
+ ''
+
+ before(async function () {
+ response = await util.axios.get('api/avatar')
+ })
+ it("should succeed", function () {
+ expect(response.status).to.equal(200)
+ })
+ it("was sent with the correct Content-Type header", function () {
+ expect(response.headers['content-type']).to.equal('image/svg+xml')
+ })
+ it("is pretty printed", function () {
+ expect(response.data).to.equal(avatarImage)
+ })
+})
diff --git a/test/paths.test.js b/test/paths.test.js
index d5f1e59..57a9040 100644
--- a/test/paths.test.js
+++ b/test/paths.test.js
@@ -1,29 +1,6 @@
-const util = require('./util.js');
-const path = require('path');
-const fs = require('fs');
-const chai = require('chai');
-const expect = chai.expect;
-const chaiResponseValidator = require('chai-openapi-response-validator');
-
-const spec = path.resolve("./test/app/api.json");
-chai.use(chaiResponseValidator(spec));
-
-const dbUploadCollection = '/db/apps/roasted/uploads/'
-
-describe('Requesting a static file from the server', function () {
- it('will download the resource', async function () {
- const res = await util.axios.get('static/roaster-router-logo.png');
- expect(res.status).to.equal(200);
- });
-});
-
-describe('Path parameters', function () {
- it('handles get of path including $', async function () {
- const res = await util.axios.get('api/$op-er+ation*!');
- expect(res.status).to.equal(200);
- // expect(res).to.satisfyApiSpec;
- });
-});
+const util = require('./util.js')
+const chai = require('chai')
+const expect = chai.expect
describe('Request methods on api/$op-er+ation*! route', function (){
const route = 'api/$op-er+ation*!'
@@ -36,31 +13,31 @@ describe('Request methods on api/$op-er+ation*! route', function (){
return util.axios.get(route)
.then(r => expect(r.status).to.equal(200))
.catch(fail)
- });
+ })
it('should handle defined POST request', function () {
return util.axios.post(route, {})
.then(r => expect(r.status).to.equal(200))
.catch(fail)
- });
+ })
it('should reject a HEAD request', function () {
return util.axios.head(route)
.then(fail)
.catch(expectNotAllowed)
- });
+ })
it('should reject a PUT request', function () {
return util.axios.put(route)
.then(fail)
.catch(expectNotAllowed)
- });
+ })
it('should reject a DELETE request', function () {
return util.axios.delete(route)
.then(fail)
.catch(expectNotAllowed)
- });
+ })
// please note that HTTP PATCH is available in
// exist since v5.3.0 and after
@@ -68,7 +45,7 @@ describe('Request methods on api/$op-er+ation*! route', function (){
return util.axios.patch(route)
.then(fail)
.catch(expectNotAllowedOrNotImplented)
- });
+ })
// OPTIONS request is handled by Jetty and will not reach your controller,
// nor roaster API
@@ -76,7 +53,7 @@ describe('Request methods on api/$op-er+ation*! route', function (){
return util.axios.options(route)
.then(r => expect(r.status).to.equal(200))
.catch(fail)
- });
+ })
// exist DB does not handle custom request methods, which is why this will
// return with error 501 instead
@@ -87,7 +64,7 @@ describe('Request methods on api/$op-er+ation*! route', function (){
})
.then(fail)
.catch(expectNotAllowedOrNotImplented)
- });
+ })
})
describe('Prefixed known path', function () {
@@ -100,157 +77,6 @@ describe('Prefixed known path', function () {
});
});
-describe("Binary up and download", function () {
- const contents = fs.readFileSync("./roasted.xar")
-
- describe("using basic authentication", function () {
- const filename = 'roasted.xar'
- it('handles post of binary data', async function () {
- const res = await util.axios.post('api/paths/' + filename, contents, {
- headers: {
- 'Content-Type': 'application/octet-stream',
- 'Authorization': 'Basic YWRtaW46'
- }
- });
- expect(res.status).to.equal(201);
- expect(res.data).to.equal(dbUploadCollection + filename);
- });
- it('retrieves the data', async function () {
- const res = await util.axios.get('api/paths/' + filename, { responseType: 'arraybuffer' });
- expect(res.status).to.equal(200);
- expect(res.data).to.eql(contents);
- });
- })
-
- describe("using cookie authentication", function () {
- const filename = "roasted2.xar"
- before(async function () {
- await util.login()
- })
- after(async function () {
- await util.logout()
- })
-
- it('handles post of binary data', async function () {
- const res = await util.axios.post('api/paths/' + filename, contents, {
- headers: { 'Content-Type': 'application/octet-stream' }
- });
- expect(res.status).to.equal(201);
- expect(res.data).to.equal(dbUploadCollection + filename);
- });
- it('retrieves the data', async function () {
- const res = await util.axios.get('api/paths/' + filename, { responseType: 'arraybuffer' });
- expect(res.status).to.equal(200);
- expect(res.data).to.eql(contents);
- });
- })
-});
-
-describe("body with content-type application/xml", function () {
- before(async function () {
- await util.login()
- })
- after(async function () {
- await util.logout()
- })
-
- describe("with valid content", function () {
- let uploadResponse
- const filename = 'valid.xml'
- const contents = Buffer.from('')
- before(function () {
- return util.axios.post('api/paths/' + filename, contents, {
- headers: { 'Content-Type': 'application/xml' }
- })
- .then(r => uploadResponse = r)
- .catch(e => uploadResponse = e.response)
- })
- it("is uploaded", function () {
- expect(uploadResponse.status).to.equal(201)
- expect(uploadResponse.data).to.equal(dbUploadCollection + filename)
- })
- it('can be retrieved', async function () {
- const res = await util.axios.get('api/paths/' + filename, { responseType: 'arraybuffer' });
- expect(res.status).to.equal(200);
- expect(res.data).to.eql(contents);
- });
- })
-
- describe("with invalid content", function () {
- let uploadResponse
- before(async function () {
- return util.axios.post('api/paths/invalid.xml', Buffer.from('asdf'), {
- headers: { 'Content-Type': 'application/xml' }
- })
- .then(r => uploadResponse = r)
- .catch(e => uploadResponse = e.response)
- })
- it("is rejected as Bad Request", function () {
- expect(uploadResponse.status).to.equal(400)
- })
- it("with the correct error code", function () {
- expect(uploadResponse.data.code).to.equal('errors:BODY_CONTENT_TYPE')
- })
- it("with a human readable description", function () {
- expect(uploadResponse.data.description).to.equal('Invalid XML')
- })
- })
-})
-
-describe("body with content-type application/json", function () {
- before(async function () {
- await util.login()
- })
- after(async function () {
- await util.logout()
- })
- describe("with valid content", function () {
- let uploadResponse
- const filename = 'valid.json'
- const contents = Buffer.from('{"valid":[]}')
- before(function () {
- return util.axios.post(
- 'api/paths/' + filename,
- contents,
- { headers: { 'Content-Type': 'application/json'} }
- )
- .then(r => uploadResponse = r)
- .catch(e => uploadResponse = e.response)
- })
- it("is uploaded", function () {
- expect(uploadResponse.status).to.equal(201)
- expect(uploadResponse.data).to.equal(dbUploadCollection + filename)
- })
- it('can be retrieved', async function () {
- const res = await util.axios.get('api/paths/' + filename, { responseType: 'arraybuffer' });
- expect(res.status).to.equal(200);
- expect(res.data).to.eql(contents);
- });
- })
-
- describe("with invalid content", function () {
- let uploadResponse
- before(function () {
- return util.axios.post(
- 'api/paths/invalid.json',
- '{"invalid: ()',
- { headers: { 'Content-Type': 'application/json' } }
- )
- .then(r => uploadResponse = r)
- .catch(e => uploadResponse = e.response)
- })
- it("is rejected as Bad Request", function () {
- expect(uploadResponse.status).to.equal(400)
- })
- it("with the correct error code", function () {
- expect(uploadResponse.data.code).to.equal('errors:BODY_CONTENT_TYPE')
- })
- it("with a human readable description", function () {
- expect(uploadResponse.data.description).to.equal('Invalid JSON')
- })
- })
-})
-
describe('On Login', function () {
let response
@@ -308,18 +134,18 @@ describe('Query parameters', function () {
headers: {
"X-start": 22
}
- });
- expect(res.status).to.equal(200);
- expect(res.data.parameters.num).to.be.a('number');
- expect(res.data.parameters.num).to.equal(165.75);
- expect(res.data.parameters.bool).to.be.a('boolean');
- expect(res.data.parameters.bool).to.be.true;
- expect(res.data.parameters.int).to.be.a('number');
- expect(res.data.parameters.int).to.equal(776);
- expect(res.data.parameters.string).to.equal('&a=22');
- expect(res.data.parameters.defaultParam).to.equal('abcdefg');
- expect(res.data.parameters['X-start']).to.equal(22);
- });
+ })
+ expect(res.status).to.equal(200)
+ expect(res.data.parameters.num).to.be.a('number')
+ expect(res.data.parameters.num).to.equal(165.75)
+ expect(res.data.parameters.bool).to.be.a('boolean')
+ expect(res.data.parameters.bool).to.be.true
+ expect(res.data.parameters.int).to.be.a('number')
+ expect(res.data.parameters.int).to.equal(776)
+ expect(res.data.parameters.string).to.equal('&a=22')
+ expect(res.data.parameters.defaultParam).to.equal('abcdefg')
+ expect(res.data.parameters['X-start']).to.equal(22)
+ })
it('passes query parameters in POST', async function () {
const res = await util.axios.request({
@@ -334,19 +160,19 @@ describe('Query parameters', function () {
headers: {
"X-start": 22
}
- });
- expect(res.status).to.equal(200);
- expect(res.data.method).to.equal('post');
- expect(res.data.parameters.num).to.be.a('number');
- expect(res.data.parameters.num).to.equal(165.75);
- expect(res.data.parameters.bool).to.be.a('boolean');
- expect(res.data.parameters.bool).to.be.true;
- expect(res.data.parameters.int).to.be.a('number');
- expect(res.data.parameters.int).to.equal(776);
- expect(res.data.parameters.string).to.equal('&a=22');
- expect(res.data.parameters.defaultParam).to.equal('abcdefg');
- expect(res.data.parameters['X-start']).to.equal(22);
- });
+ })
+ expect(res.status).to.equal(200)
+ expect(res.data.method).to.equal('post')
+ expect(res.data.parameters.num).to.be.a('number')
+ expect(res.data.parameters.num).to.equal(165.75)
+ expect(res.data.parameters.bool).to.be.a('boolean')
+ expect(res.data.parameters.bool).to.be.true
+ expect(res.data.parameters.int).to.be.a('number')
+ expect(res.data.parameters.int).to.equal(776)
+ expect(res.data.parameters.string).to.equal('&a=22')
+ expect(res.data.parameters.defaultParam).to.equal('abcdefg')
+ expect(res.data.parameters['X-start']).to.equal(22)
+ })
it('handles date parameters', async function () {
const res = await util.axios.get('api/dates', {
@@ -354,46 +180,8 @@ describe('Query parameters', function () {
date: "2020-11-24Z",
dateTime: "2020-11-24T20:22:41.975Z"
}
- });
- expect(res.status).to.equal(200);
- expect(res.data).to.be.true;
- });
-});
-
-describe('Error reporting', function() {
- it('receives error report', function() {
- return util.axios.get('api/errors')
- .catch(function(error) {
- expect(error.response.status).to.equal(404);
- expect(error.response.data.description).to.equal('document not found');
- expect(error.response.data.value).to.equal('error details');
- });
- });
-
- it('receives dynamic XQuery error', function() {
- return util.axios.post('api/errors')
- .catch(function(error) {
- expect(error.response.status).to.equal(500);
- expect(error.response.data.line).to.match(/\d+/);
- expect(error.response.data.description).to.contain('$undefined');
- });
- });
-
- it('receives explicit error', function() {
- return util.axios.delete('api/errors')
- .catch(function(error) {
- expect(error.response.status).to.equal(403);
- expect(error.response.headers['content-type']).to.equal('application/xml');
- expect(error.response.data).to.equal('');
- });
- });
-
- it('calls error handler', function() {
- return util.axios.get('api/errors/handle')
- .catch(function(error) {
- expect(error.response.status).to.equal(500);
- expect(error.response.headers['content-type']).to.equal('text/html');
- expect(error.response.data).to.contain('$undefined');
- });
- });
-});
+ })
+ expect(res.status).to.equal(200)
+ expect(res.data).to.be.true
+ })
+})
diff --git a/test/static.test.js b/test/static.test.js
new file mode 100644
index 0000000..a859151
--- /dev/null
+++ b/test/static.test.js
@@ -0,0 +1,10 @@
+const util = require('./util.js')
+const chai = require('chai')
+const expect = chai.expect
+
+describe('Requesting a static file from the server', function () {
+ it('will download the resource', async function () {
+ const res = await util.axios.get('static/roaster-router-logo.png')
+ expect(res.status).to.equal(200)
+ })
+})
diff --git a/test/util.js b/test/util.js
index 958c6ed..9e71c3f 100644
--- a/test/util.js
+++ b/test/util.js
@@ -1,24 +1,26 @@
-const chai = require('chai');
-const expect = chai.expect;
-const axios = require('axios');
+const chai = require('chai')
+const axios = require('axios')
+
+const path = require('path')
+const chaiResponseValidator = require('chai-openapi-response-validator')
+const spec = path.resolve("./test/app/api.json")
+chai.use(chaiResponseValidator(spec))
+
// read metadata from .existdb.json
const existJSON = require('../.existdb.json')
const serverInfo = existJSON.servers.localhost
-
const { origin } = new URL(serverInfo.server)
-const app = `${origin}/exist/apps/roasted`;
+const app = `${origin}/exist/apps/roasted`
const axiosInstance = axios.create({
baseURL: app,
- headers: {
- "Origin": origin
- },
+ headers: { "Origin": origin },
withCredentials: true
-});
+})
async function login() {
- // console.log('Logging in ' + serverInfo.user + ' to ' + app);
+ // console.log('Logging in ' + serverInfo.user + ' to ' + app)
const res = await axiosInstance.request({
url: 'login',
method: 'post',
@@ -26,20 +28,22 @@ async function login() {
"user": serverInfo.user,
"password": serverInfo.password
}
- });
+ })
- // expect(res.status).to.equal(200);
- // expect(res.data.user).to.equal(serverInfo.user);
-
- const cookie = res.headers["set-cookie"];
- axiosInstance.defaults.headers.Cookie = cookie[0];
- // console.log('Logged in as %s: %s', res.data.user, res.statusText);
+ const cookie = res.headers["set-cookie"]
+ axiosInstance.defaults.headers.Cookie = cookie[0]
+ // console.log('Logged in as %s: %s', res.data.user, res.statusText)
}
function logout() {
- // console.log('Logging out ...');
- return axiosInstance.request({ url: 'logout', method: 'get'})
- .catch(_ => Promise.resolve())
+ // console.log('Logging out ...')
+ return axiosInstance
+ .request({ url: 'logout', method: 'get'})
+ .catch(_ => Promise.resolve())
}
-module.exports = {axios: axiosInstance, login, logout};
+module.exports = {
+ axios: axiosInstance,
+ login, logout,
+ spec
+}