From c80ab70d5a0450bbd1d66463d0d752b8979f1c70 Mon Sep 17 00:00:00 2001 From: Dan Caseley Date: Fri, 25 Sep 2026 19:02:28 +0100 Subject: [PATCH 01/21] Hurl: split tests to mirror the REST services Co-Authored-By: Claude Opus 5.5 --- test/chatrooms.hurl | 11 ----------- test/sessions.hurl | 1 + test/usergroups.hurl | 1 + test/userlockouts.hurl | 1 + test/userroster.hurl | 11 +++++++++++ test/users.hurl | 16 ---------------- 6 files changed, 14 insertions(+), 27 deletions(-) create mode 100644 test/usergroups.hurl create mode 100644 test/userlockouts.hurl create mode 100644 test/userroster.hurl diff --git a/test/chatrooms.hurl b/test/chatrooms.hurl index 1c3027a89..cd1995e38 100644 --- a/test/chatrooms.hurl +++ b/test/chatrooms.hurl @@ -1,14 +1,3 @@ GET http://localhost:9090/plugins/restapi/v1/chatrooms Authorization: {{authkey}} HTTP 200 - -GET http://localhost:9090/plugins/restapi/v1/sessions -Authorization: {{authkey}} -HTTP 200 -[Asserts] -xpath "/sessions[not(child::node())]" count == 1 # sessions at the root, with no child nodes - - -GET http://localhost:9090/plugins/restapi/v1/system/readiness/server -Authorization: {{authkey}} -HTTP 200 \ No newline at end of file diff --git a/test/sessions.hurl b/test/sessions.hurl index 12d28127e..62eaaf920 100644 --- a/test/sessions.hurl +++ b/test/sessions.hurl @@ -3,6 +3,7 @@ Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/sessions" exists +xpath "/sessions[not(child::node())]" count == 1 # sessions at the root, with no child nodes GET http://localhost:9090/plugins/restapi/v1/sessions/john Authorization: {{authkey}} diff --git a/test/usergroups.hurl b/test/usergroups.hurl new file mode 100644 index 000000000..2f8ba8ff4 --- /dev/null +++ b/test/usergroups.hurl @@ -0,0 +1 @@ +# TODO: Group membership diff --git a/test/userlockouts.hurl b/test/userlockouts.hurl new file mode 100644 index 000000000..d3a52052d --- /dev/null +++ b/test/userlockouts.hurl @@ -0,0 +1 @@ +# TODO: User lockouts diff --git a/test/userroster.hurl b/test/userroster.hurl new file mode 100644 index 000000000..7ec464486 --- /dev/null +++ b/test/userroster.hurl @@ -0,0 +1,11 @@ +GET http://localhost:9090/plugins/restapi/v1/users/john/roster +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/roster/rosterItem" count == 1 +xpath "string(/roster/rosterItem/jid)" == "jane@example.org" +xpath "string(/roster/rosterItem/nickname)" == "Jane" +xpath "string(/roster/rosterItem/subscriptionType)" == "3" +xpath "/roster/rosterItem/groups" exists + +# TODO: Roster add/edit/delete diff --git a/test/users.hurl b/test/users.hurl index 8e450aae6..9dad8d08f 100644 --- a/test/users.hurl +++ b/test/users.hurl @@ -92,19 +92,3 @@ HTTP 404 xpath "string(/error/exception)" == "UserNotFoundException" xpath "string(/error/message)" == "Could not get user" xpath "string(/error/resource)" == "jeanluc" - -GET http://localhost:9090/plugins/restapi/v1/users/john/roster -Authorization: {{authkey}} -HTTP 200 -[Asserts] -xpath "/roster/rosterItem" count == 1 -xpath "string(/roster/rosterItem/jid)" == "jane@example.org" -xpath "string(/roster/rosterItem/nickname)" == "Jane" -xpath "string(/roster/rosterItem/subscriptionType)" == "3" -xpath "/roster/rosterItem/groups" exists - -# TODO: Roster add/edit/delete - -# TODO: Group membership - -# TODO: User lockouts \ No newline at end of file From 87c48d5f1f5bf56a146668b52b349e484e3ca318 Mon Sep 17 00:00:00 2001 From: Dan Caseley Date: Fri, 25 Sep 2026 16:48:55 +0100 Subject: [PATCH 02/21] Hurl: additional System Properties tests --- test/system.hurl | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/test/system.hurl b/test/system.hurl index 375d876fd..5f805f75d 100644 --- a/test/system.hurl +++ b/test/system.hurl @@ -35,8 +35,11 @@ Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/properties/property" count > 150 -# TODO test that property with attribute key=admin.authorizedJIDs exists -# TODO test that property with attribute key=plugin.restapi.enabled has value=true +xpath "/properties/property[@key='admin.authorizedJIDs']" exists +xpath "string(/properties/property[@key='abstractGroupProvider.shared.recursive']/@value)" == "false" +# https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 +# The plugin's own properties (e.g. plugin.restapi.enabled) must not be exposed in the listing. +xpath "/properties/property[starts-with(@key, 'plugin.restapi.')]" not exists # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/242 # abstractGroupProvider.shared.recursive is registered through Openfire's SystemProperty API with a default value. @@ -71,7 +74,8 @@ Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/property" count == 1 -# TODO test that property with attribute key=test.key has value=test-value +xpath "string(/property/@key)" == "test.key" +xpath "string(/property/@value)" == "test-value" PUT http://localhost:9090/plugins/restapi/v1/system/properties/test.key Authorization: {{authkey}} @@ -82,6 +86,12 @@ Content-Type: application/xml ``` HTTP 200 +GET http://localhost:9090/plugins/restapi/v1/system/properties/test.key +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "string(/property/@value)" == "new-value" + PUT http://localhost:9090/plugins/restapi/v1/system/properties/wrong.key Authorization: {{authkey}} Content-Type: application/xml @@ -104,6 +114,10 @@ DELETE http://localhost:9090/plugins/restapi/v1/system/properties/test.key Authorization: {{authkey}} HTTP 200 +GET http://localhost:9090/plugins/restapi/v1/system/properties/test.key +Authorization: {{authkey}} +HTTP 404 + # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. GET http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.enabled From b2f1c8db35ad3c736f49d148d64e1122c3d0fed5 Mon Sep 17 00:00:00 2001 From: Dan Caseley Date: Fri, 25 Sep 2026 19:02:37 +0100 Subject: [PATCH 03/21] Hurl: additional Users tests Co-Authored-By: Claude Opus 5.5 --- test/users.hurl | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/test/users.hurl b/test/users.hurl index 9dad8d08f..8ec68e414 100644 --- a/test/users.hurl +++ b/test/users.hurl @@ -18,13 +18,11 @@ xpath "/users/user[username='john']" exists xpath "/users/user[name='John Doe']" exists xpath "/users/user[email='john.doe@example.com']" exists -# TODO: Add a user with a property to the demoboot -# How to launch with custom demoboot.xml locally? -#GET http://localhost:9090/plugins/restapi/v1/users?propertyKey=tea&propertyValue=earlgreyhot -#Authorization: {{authkey}} -#HTTP 200 -#[Asserts] -#xpath "/users/user" count == 1 +GET http://localhost:9090/plugins/restapi/v1/users?propertyKey=tea +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/users/user" count == 0 POST http://localhost:9090/plugins/restapi/v1/users Authorization: {{authkey}} @@ -51,8 +49,22 @@ xpath "/users/user" count == 1 xpath "string(/users/user/username)" == "jeanluc" xpath "/users/user[name='Jean-Luc']" exists xpath "/users/user[email='jlp@example.com']" exists -# TODO xpath for attributes -xpath "/users/user/properties/property" exists +xpath "/users/user/properties/property" count == 1 +xpath "string(/users/user/properties/property/@key)" == "tea" +xpath "string(/users/user/properties/property/@value)" == "earlgreyhot" + +GET http://localhost:9090/plugins/restapi/v1/users?propertyKey=tea +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/users/user" count == 1 +xpath "string(/users/user/username)" == "jeanluc" + +GET http://localhost:9090/plugins/restapi/v1/users?propertyKey=tea&propertyValue=coffee +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/users/user" count == 0 PUT http://localhost:9090/plugins/restapi/v1/users/jeanluc Authorization: {{authkey}} From 4fee525ffa3edaf4839ea485b29b8454c98e4f01 Mon Sep 17 00:00:00 2001 From: Dan Caseley Date: Fri, 25 Sep 2026 19:03:05 +0100 Subject: [PATCH 04/21] Hurl: additional User Roster tests Co-Authored-By: Claude Opus 5.5 --- test/userroster.hurl | 159 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 158 insertions(+), 1 deletion(-) diff --git a/test/userroster.hurl b/test/userroster.hurl index 7ec464486..976bd014f 100644 --- a/test/userroster.hurl +++ b/test/userroster.hurl @@ -8,4 +8,161 @@ xpath "string(/roster/rosterItem/nickname)" == "Jane" xpath "string(/roster/rosterItem/subscriptionType)" == "3" xpath "/roster/rosterItem/groups" exists -# TODO: Roster add/edit/delete + +# Roster changes are tested on a dedicated user, which is removed at the end. +POST http://localhost:9090/plugins/restapi/v1/users +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + data + spot + +``` +HTTP 201 + +GET http://localhost:9090/plugins/restapi/v1/users/data/roster +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/roster/rosterItem" count == 0 + +POST http://localhost:9090/plugins/restapi/v1/users/data/roster +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + john@example.org + John + 3 + + Friends + + +``` +HTTP 201 + +GET http://localhost:9090/plugins/restapi/v1/users/data/roster +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/roster/rosterItem" count == 1 +xpath "string(/roster/rosterItem/jid)" == "john@example.org" +xpath "string(/roster/rosterItem/nickname)" == "John" +xpath "string(/roster/rosterItem/subscriptionType)" == "3" +xpath "/roster/rosterItem/groups/group" count == 1 +xpath "string(/roster/rosterItem/groups/group)" == "Friends" + +# Adding an entry for a contact that is already on the roster is a conflict. +POST http://localhost:9090/plugins/restapi/v1/users/data/roster +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + john@example.org + +``` +HTTP 409 +[Asserts] +xpath "string(/error/exception)" == "UserAlreadyExistsException" + +# A roster entry needs a JID. +POST http://localhost:9090/plugins/restapi/v1/users/data/roster +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + Nobody + +``` +HTTP 400 +[Asserts] +xpath "string(/error/exception)" == "IllegalArgumentException" + +# TODO: Adding a roster entry with an invalid subscriptionType (e.g. 9) returns 409 (UserAlreadyExistsException), and +# the entry is created anyway, as UserServiceController#addRosterItem validates the subscription type only after +# creating the roster item. Add a test expecting a 400 (and no new roster entry) once that is fixed. + +# TODO: A JID without an '@' is taken to be a domain JID: adding a roster entry with john stores an entry +# for the domain 'john', rather than for john@example.org (unlike group members, which resolve a username against the +# local domain). An empty or unparsable JID causes a 500 rather than a 400. Add tests once the intended behaviour is +# decided. + +# TODO: Adding a roster entry for a user that does not exist returns 201 (and stores the roster entry), where 404 is +# documented. Getting the roster of a user that does not exist returns 200 (rather than 404) too. Add tests for those +# once UserServiceController#getUserRoster checks that the user exists. + +PUT http://localhost:9090/plugins/restapi/v1/users/data/roster/john@example.org +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + john@example.org + Johnny + 1 + + Crew + + +``` +HTTP 200 + +GET http://localhost:9090/plugins/restapi/v1/users/data/roster +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/roster/rosterItem" count == 1 +xpath "string(/roster/rosterItem/jid)" == "john@example.org" +xpath "string(/roster/rosterItem/nickname)" == "Johnny" +xpath "string(/roster/rosterItem/subscriptionType)" == "1" +xpath "/roster/rosterItem/groups/group" count == 1 +xpath "string(/roster/rosterItem/groups/group)" == "Crew" + +# Updating an entry for a contact that is not on the roster fails. +PUT http://localhost:9090/plugins/restapi/v1/users/data/roster/jane@example.org +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + jane@example.org + Jane + +``` +HTTP 404 +[Asserts] +xpath "string(/error/exception)" == "UserNotFoundException" +xpath "string(/error/resource)" == "jane@example.org" + +DELETE http://localhost:9090/plugins/restapi/v1/users/data/roster/john@example.org +Authorization: {{authkey}} +HTTP 200 + +GET http://localhost:9090/plugins/restapi/v1/users/data/roster +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/roster/rosterItem" count == 0 + +DELETE http://localhost:9090/plugins/restapi/v1/users/data/roster/john@example.org +Authorization: {{authkey}} +HTTP 404 +[Asserts] +xpath "string(/error/exception)" == "RosterItemNotFound" +xpath "string(/error/resource)" == "john@example.org" + +DELETE http://localhost:9090/plugins/restapi/v1/users/nonexistent/roster/john@example.org +Authorization: {{authkey}} +HTTP 404 +[Asserts] +xpath "string(/error/exception)" == "UserNotFoundException" +xpath "string(/error/resource)" == "nonexistent" + +DELETE http://localhost:9090/plugins/restapi/v1/users/data +Authorization: {{authkey}} +HTTP 200 From cba6dce170b2f7e16da5e66d874a761c2492c07f Mon Sep 17 00:00:00 2001 From: Dan Caseley Date: Fri, 25 Sep 2026 19:03:06 +0100 Subject: [PATCH 05/21] Hurl: additional User Groups tests Co-Authored-By: Claude Opus 5.5 --- test/usergroups.hurl | 162 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 161 insertions(+), 1 deletion(-) diff --git a/test/usergroups.hurl b/test/usergroups.hurl index 2f8ba8ff4..625be5a6b 100644 --- a/test/usergroups.hurl +++ b/test/usergroups.hurl @@ -1 +1,161 @@ -# TODO: Group membership +# Group membership is tested on a dedicated user, which is removed at the end. +POST http://localhost:9090/plugins/restapi/v1/users +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + geordi + visor + +``` +HTTP 201 + +POST http://localhost:9090/plugins/restapi/v1/groups +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + security + Security + false + + + +``` +HTTP 201 + +POST http://localhost:9090/plugins/restapi/v1/groups +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + klingons + Klingons + false + + + +``` +HTTP 201 + +POST http://localhost:9090/plugins/restapi/v1/groups +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + bridge + Bridge crew + false + + + +``` +HTTP 201 + +GET http://localhost:9090/plugins/restapi/v1/users/geordi/groups +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/groups/groupname" count == 0 + +POST http://localhost:9090/plugins/restapi/v1/users/geordi/groups/security +Authorization: {{authkey}} +HTTP 201 + +POST http://localhost:9090/plugins/restapi/v1/users/geordi/groups +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + klingons + bridge + +``` +HTTP 201 + +GET http://localhost:9090/plugins/restapi/v1/users/geordi/groups +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/groups/groupname" count == 3 +xpath "/groups[groupname='security']" exists +xpath "/groups[groupname='klingons']" exists +xpath "/groups[groupname='bridge']" exists + +GET http://localhost:9090/plugins/restapi/v1/groups/klingons +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/group/members/member" count == 1 +xpath "string(/group/members/member)" == "geordi@example.org" + +DELETE http://localhost:9090/plugins/restapi/v1/users/geordi/groups/security +Authorization: {{authkey}} +HTTP 200 + +GET http://localhost:9090/plugins/restapi/v1/users/geordi/groups +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/groups/groupname" count == 2 +xpath "/groups[groupname='security']" not exists + +DELETE http://localhost:9090/plugins/restapi/v1/users/geordi/groups +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + klingons + bridge + +``` +HTTP 200 + +GET http://localhost:9090/plugins/restapi/v1/users/geordi/groups +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/groups/groupname" count == 0 + +DELETE http://localhost:9090/plugins/restapi/v1/users/geordi/groups/nonexistent +Authorization: {{authkey}} +HTTP 404 +[Asserts] +xpath "string(/error/exception)" == "GroupNotFoundException" +xpath "string(/error/resource)" == "nonexistent" + +GET http://localhost:9090/plugins/restapi/v1/users/nonexistent/groups +Authorization: {{authkey}} +HTTP 404 +[Asserts] +xpath "string(/error/exception)" == "UserNotFoundException" + +# TODO: Adding a user to a group that does not exist is documented to create that group, but fails with a 500, as +# GroupController#createGroup dereferences the (null) members of the GroupEntity that +# UserServiceController#addUserToGroup(s) creates. (Creating a group through POST /groups without and +# fails in the same way.) Add tests for automatic group creation once that is fixed. + +# TODO: Adding a user that does not exist to an (existing) group returns 201 and adds the JID to the group, where +# 404 might be expected. Add a test once the intended behaviour is decided. + +DELETE http://localhost:9090/plugins/restapi/v1/groups/security +Authorization: {{authkey}} +HTTP 200 + +DELETE http://localhost:9090/plugins/restapi/v1/groups/klingons +Authorization: {{authkey}} +HTTP 200 + +DELETE http://localhost:9090/plugins/restapi/v1/groups/bridge +Authorization: {{authkey}} +HTTP 200 + +DELETE http://localhost:9090/plugins/restapi/v1/users/geordi +Authorization: {{authkey}} +HTTP 200 From e70c79c7ce7c836157c5c719704a5056d9236b0a Mon Sep 17 00:00:00 2001 From: Dan Caseley Date: Fri, 25 Sep 2026 19:03:25 +0100 Subject: [PATCH 06/21] Hurl: additional Sessions tests Co-Authored-By: Claude Opus 5.5 --- test/README.md | 6 ++- test/sessions.hurl | 109 ++++++++++++++++++++++++++++++++++++++++++++- test/test.env | 3 +- 3 files changed, 114 insertions(+), 4 deletions(-) diff --git a/test/README.md b/test/README.md index cb404e3e0..580562bb1 100644 --- a/test/README.md +++ b/test/README.md @@ -8,4 +8,8 @@ Configure the Rest API: * Enable it * Set auth for shared key, and set the value in test.env -* Set `adminConsole.access.allow-wildcards-in-excludes` to true \ No newline at end of file +* Set `adminConsole.access.allow-wildcards-in-excludes` to true + +Some tests create XMPP client sessions by logging in over BOSH (Openfire's HTTP binding, which listens on port 7070 by +default). The BOSH endpoint is configured by the `bosh_url` variable in test.env; when Openfire runs in a container, make +sure that port 7070 is published (or override the variable, e.g. `--variable bosh_url=http://:7070`). diff --git a/test/sessions.hurl b/test/sessions.hurl index 62eaaf920..2d3299d31 100644 --- a/test/sessions.hurl +++ b/test/sessions.hurl @@ -10,12 +10,117 @@ Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/sessions" exists +xpath "/sessions/session" count == 0 -# TODO: This documents behaviour, but is it correct?? +# Openfire does not check whether the user exists: a user that does not exist simply has no sessions. The API documents +# no other response than 200 for this endpoint. GET http://localhost:9090/plugins/restapi/v1/sessions/nonexistent Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/sessions" exists +xpath "/sessions/session" count == 0 -# TODO: create a session (somehow), then read its props, then kick it with DELETE \ No newline at end of file + +# Create a session for john, by logging in over BOSH (XEP-0124/XEP-0206). +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` + +``` +HTTP 200 +[Captures] +sid: xpath "string(/_:body/@sid)" + +# SASL PLAIN, with base64("\0john\0secret") +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` +AGpvaG4Ac2VjcmV0 +``` +HTTP 200 +[Asserts] +xpath "//*[local-name()='success']" exists + +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` + +``` +HTTP 200 + +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` +hurl +``` +HTTP 200 +[Asserts] +xpath "string(//*[local-name()='jid'])" == "john@example.org/hurl" + +# The session is only listed once it has sent initial presence. +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` + +``` +HTTP 200 + +GET http://localhost:9090/plugins/restapi/v1/sessions +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/sessions/session[sessionId='john@example.org/hurl']" exists + +GET http://localhost:9090/plugins/restapi/v1/sessions/john +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/sessions/session" count == 1 +xpath "string(/sessions/session/sessionId)" == "john@example.org/hurl" +xpath "string(/sessions/session/username)" == "john" +xpath "string(/sessions/session/resource)" == "hurl" +xpath "string(/sessions/session/node)" == "Local" +xpath "string(/sessions/session/sessionStatus)" == "Authenticated" +xpath "string(/sessions/session/presenceStatus)" == "Online" +xpath "string(/sessions/session/priority)" == "0" +xpath "/sessions/session/hostAddress" exists +xpath "/sessions/session/creationDate" exists +xpath "/sessions/session/lastActionDate" exists + +GET http://localhost:9090/plugins/restapi/v1/sessions/jane +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/sessions/session" count == 0 + +# Kick the session. +DELETE http://localhost:9090/plugins/restapi/v1/sessions/john +Authorization: {{authkey}} +HTTP 200 + +# Sessions are closed asynchronously, so allow for some delay. +GET http://localhost:9090/plugins/restapi/v1/sessions/john +Authorization: {{authkey}} +[Options] +retry: 10 +retry-interval: 100ms +HTTP 200 +[Asserts] +xpath "/sessions/session" count == 0 + +# The BOSH session no longer exists either. (A poll that is made while the session is being closed is still answered.) +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +[Options] +retry: 10 +retry-interval: 100ms +``` + +``` +HTTP 404 + +# Kicking the sessions of a user without sessions is fine. +DELETE http://localhost:9090/plugins/restapi/v1/sessions/john +Authorization: {{authkey}} +HTTP 200 diff --git a/test/test.env b/test/test.env index 6d4bd1e85..1f0d86b8f 100644 --- a/test/test.env +++ b/test/test.env @@ -1,2 +1,3 @@ host=http://localhost:9090 -authkey=potato \ No newline at end of file +authkey=potato +bosh_url=http://localhost:7070 From 4cfa76d523ad07a83e0c00470be09b8c658f69aa Mon Sep 17 00:00:00 2001 From: Dan Caseley Date: Fri, 25 Sep 2026 19:03:46 +0100 Subject: [PATCH 07/21] Hurl: additional Message Broadcast tests Co-Authored-By: Claude Opus 5.5 --- test/messagebroadcast.hurl | 100 ++++++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/test/messagebroadcast.hurl b/test/messagebroadcast.hurl index 3bec3dbf5..7bb1f79a3 100644 --- a/test/messagebroadcast.hurl +++ b/test/messagebroadcast.hurl @@ -1,3 +1,4 @@ +# Broadcasting without any sessions to deliver to is fine. POST http://localhost:9090/plugins/restapi/v1/messages/users Authorization: {{authkey}} Content-Type: application/xml @@ -9,4 +10,101 @@ Content-Type: application/xml ``` HTTP 201 -# TODO: What could validate behaviour? \ No newline at end of file +POST http://localhost:9090/plugins/restapi/v1/messages/users +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + + +``` +HTTP 400 +[Asserts] +xpath "string(/error/exception)" == "IllegalArgumentException" + +POST http://localhost:9090/plugins/restapi/v1/messages/users +Authorization: {{authkey}} +Content-Type: application/xml +``` + + +``` +HTTP 400 +[Asserts] +xpath "string(/error/exception)" == "IllegalArgumentException" + + +# Log in anonymously over BOSH (XEP-0124/XEP-0206), to be able to receive the broadcast. +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` + +``` +HTTP 200 +[Captures] +sid: xpath "string(/_:body/@sid)" + +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` + +``` +HTTP 200 +[Asserts] +xpath "//*[local-name()='success']" exists + +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` + +``` +HTTP 200 + +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` + +``` +HTTP 200 + +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` + +``` +HTTP 200 + +POST http://localhost:9090/plugins/restapi/v1/messages/users +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + Hello from the REST API + +``` +HTTP 201 + +# Poll for the broadcast, which is delivered as a headline message from the server. +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` + +``` +HTTP 200 +[Asserts] +xpath "//*[local-name()='message']" count == 1 +xpath "string(//*[local-name()='message']/@from)" == "example.org" +xpath "string(//*[local-name()='message']/@type)" == "headline" +xpath "string(//*[local-name()='message']/*[local-name()='body'])" == "Hello from the REST API" + +# Log out. +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` + +``` +HTTP 200 +[Asserts] +xpath "string(/_:body/@type)" == "terminate" From 97c5e5a12d584e43386dd138ebab6f4aed3d8fab Mon Sep 17 00:00:00 2001 From: Dan Caseley Date: Fri, 25 Sep 2026 19:03:46 +0100 Subject: [PATCH 08/21] Hurl: additional User Lockouts tests Co-Authored-By: Claude Opus 5.5 --- test/userlockouts.hurl | 194 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 193 insertions(+), 1 deletion(-) diff --git a/test/userlockouts.hurl b/test/userlockouts.hurl index d3a52052d..0177c669b 100644 --- a/test/userlockouts.hurl +++ b/test/userlockouts.hurl @@ -1 +1,193 @@ -# TODO: User lockouts +# Lockouts are tested on a dedicated user, which is removed at the end. +POST http://localhost:9090/plugins/restapi/v1/users +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + worf + qapla + +``` +HTTP 201 + +# Log in as worf over BOSH (XEP-0124/XEP-0206), so that there is a session for the lockout to close. +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` + +``` +HTTP 200 +[Captures] +sid: xpath "string(/_:body/@sid)" + +# SASL PLAIN, with base64("\0worf\0qapla") +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` +AHdvcmYAcWFwbGE= +``` +HTTP 200 +[Asserts] +xpath "//*[local-name()='success']" exists + +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` + +``` +HTTP 200 + +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` +hurl +``` +HTTP 200 + +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` + +``` +HTTP 200 + +GET http://localhost:9090/plugins/restapi/v1/sessions/worf +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/sessions/session" count == 1 + +POST http://localhost:9090/plugins/restapi/v1/lockouts/worf +Authorization: {{authkey}} +HTTP 201 + +# Sessions are closed asynchronously, so allow for some delay. +# Locking out a user closes its sessions. +GET http://localhost:9090/plugins/restapi/v1/sessions/worf +Authorization: {{authkey}} +[Options] +retry: 10 +retry-interval: 100ms +HTTP 200 +[Asserts] +xpath "/sessions/session" count == 0 + +# The BOSH session no longer exists either. (A poll that is made while the session is being closed is still answered.) +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +[Options] +retry: 10 +retry-interval: 100ms +``` + +``` +HTTP 404 + +# Locking out a user that is already locked out is fine. +POST http://localhost:9090/plugins/restapi/v1/lockouts/worf +Authorization: {{authkey}} +HTTP 201 + +# A user that is locked out cannot log in. +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` + +``` +HTTP 200 +[Captures] +sid: xpath "string(/_:body/@sid)" + +# SASL PLAIN, with base64("\0worf\0qapla") +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` +AHdvcmYAcWFwbGE= +``` +HTTP 200 +[Asserts] +xpath "//*[local-name()='failure']/*[local-name()='not-authorized']" exists + +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` + +``` +HTTP 200 + +DELETE http://localhost:9090/plugins/restapi/v1/lockouts/worf +Authorization: {{authkey}} +HTTP 200 + +# Unlocking a user that is not locked out is fine. +DELETE http://localhost:9090/plugins/restapi/v1/lockouts/worf +Authorization: {{authkey}} +HTTP 200 + +# A user that is no longer locked out can log in again. +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` + +``` +HTTP 200 +[Captures] +sid: xpath "string(/_:body/@sid)" + +# SASL PLAIN, with base64("\0worf\0qapla") +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` +AHdvcmYAcWFwbGE= +``` +HTTP 200 +[Asserts] +xpath "//*[local-name()='success']" exists + +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` + +``` +HTTP 200 + +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` +hurl +``` +HTTP 200 + +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` + +``` +HTTP 200 + +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` + +``` +HTTP 200 + +POST http://localhost:9090/plugins/restapi/v1/lockouts/nonexistent +Authorization: {{authkey}} +HTTP 404 +[Asserts] +xpath "string(/error/exception)" == "UserNotFoundException" +xpath "string(/error/resource)" == "nonexistent" + +DELETE http://localhost:9090/plugins/restapi/v1/lockouts/nonexistent +Authorization: {{authkey}} +HTTP 404 +[Asserts] +xpath "string(/error/exception)" == "UserNotFoundException" +xpath "string(/error/resource)" == "nonexistent" + + +DELETE http://localhost:9090/plugins/restapi/v1/users/worf +Authorization: {{authkey}} +HTTP 200 From d7fb47bbdda9d2465bf752461915a7d302f28ec7 Mon Sep 17 00:00:00 2001 From: Dan Caseley Date: Fri, 25 Sep 2026 19:03:53 +0100 Subject: [PATCH 09/21] Hurl: additional Security Audit Log tests Co-Authored-By: Claude Opus 5.5 --- test/securitylog.hurl | 83 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 76 insertions(+), 7 deletions(-) diff --git a/test/securitylog.hurl b/test/securitylog.hurl index 6a820dcf3..03c37642a 100644 --- a/test/securitylog.hurl +++ b/test/securitylog.hurl @@ -2,10 +2,79 @@ GET http://localhost:9090/plugins/restapi/v1/logs/security Authorization: {{authkey}} HTTP 200 [Asserts] -xpath "/logs/log" count == 0 # TODO: How to make this have 1+ events -#xpath "/logs/log/details" exists -#xpath "/logs/log/logId" exists -#xpath "/logs/log/node" exists -#xpath "/logs/log/summary" exists -#xpath "/logs/log/timestamp" exists -#xpath "/logs/log/username" exists \ No newline at end of file +xpath "/logs" exists + + +# Openfire records a security audit event for every login to the admin console, so log in to create one. +GET http://localhost:9090/login.jsp +HTTP 200 +[Captures] +csrf: cookie "csrf" + +POST http://localhost:9090/login.jsp +[Form] +login: true +csrf: {{csrf}} +username: admin +password: admin +HTTP 302 +[Asserts] +header "Location" == "/index.jsp" + + +GET http://localhost:9090/plugins/restapi/v1/logs/security +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/logs/log" count >= 1 +xpath "/logs/log[summary='Successful admin console login attempt'][username='admin']" exists +xpath "/logs/log/details" exists +xpath "/logs/log/logId" exists +xpath "/logs/log/node" exists +xpath "/logs/log/summary" exists +xpath "/logs/log/timestamp" exists +xpath "/logs/log/username" exists + +GET http://localhost:9090/plugins/restapi/v1/logs/security?username=admin +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/logs/log" count >= 1 +xpath "/logs/log[username!='admin']" not exists + +GET http://localhost:9090/plugins/restapi/v1/logs/security?username=nonexistent +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/logs/log" count == 0 + +GET http://localhost:9090/plugins/restapi/v1/logs/security?limit=1 +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/logs/log" count == 1 + +GET http://localhost:9090/plugins/restapi/v1/logs/security?offset=1000000 +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/logs/log" count == 0 + +# Timestamps are in seconds since the epoch: nothing was logged after 2100-01-01, or before 1970-01-01T00:00:01. +GET http://localhost:9090/plugins/restapi/v1/logs/security?startTime=4102444800 +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/logs/log" count == 0 + +GET http://localhost:9090/plugins/restapi/v1/logs/security?endTime=1 +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/logs/log" count == 0 + +GET http://localhost:9090/plugins/restapi/v1/logs/security?startTime=1&endTime=4102444800 +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/logs/log[summary='Successful admin console login attempt']" exists From f966d0ef8e9a1272e75d61d381c1e63c435ab19c Mon Sep 17 00:00:00 2001 From: Dan Caseley Date: Fri, 25 Sep 2026 19:21:38 +0100 Subject: [PATCH 10/21] Hurl: make Security Audit Log tests independent of existing entries Co-Authored-By: Claude Opus 5.5 --- test/securitylog.hurl | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/test/securitylog.hurl b/test/securitylog.hurl index 03c37642a..8b1bee795 100644 --- a/test/securitylog.hurl +++ b/test/securitylog.hurl @@ -1,8 +1,13 @@ -GET http://localhost:9090/plugins/restapi/v1/logs/security +# The log may already contain events (e.g. from earlier runs). Events are returned newest first, so remember the ID of +# the newest one (or 0 when the log is empty), to be able to tell which events are new. +GET http://localhost:9090/plugins/restapi/v1/logs/security?limit=1 Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/logs" exists +xpath "/logs/log" count <= 1 +[Captures] +last_log_id: xpath "number(concat('0', /logs/log/logId))" # Openfire records a security audit event for every login to the admin console, so log in to create one. @@ -26,20 +31,18 @@ GET http://localhost:9090/plugins/restapi/v1/logs/security Authorization: {{authkey}} HTTP 200 [Asserts] -xpath "/logs/log" count >= 1 -xpath "/logs/log[summary='Successful admin console login attempt'][username='admin']" exists -xpath "/logs/log/details" exists -xpath "/logs/log/logId" exists -xpath "/logs/log/node" exists -xpath "/logs/log/summary" exists -xpath "/logs/log/timestamp" exists -xpath "/logs/log/username" exists +xpath "/logs/log[logId > {{last_log_id}}]" count == 1 +xpath "string(/logs/log[logId > {{last_log_id}}]/summary)" == "Successful admin console login attempt" +xpath "string(/logs/log[logId > {{last_log_id}}]/username)" == "admin" +xpath "string(/logs/log[logId > {{last_log_id}}]/node)" == "example.org" +xpath "string(/logs/log[logId > {{last_log_id}}]/details)" startsWith "The user logged in successfully to the admin console" +xpath "/logs/log[logId > {{last_log_id}}]/timestamp" exists GET http://localhost:9090/plugins/restapi/v1/logs/security?username=admin Authorization: {{authkey}} HTTP 200 [Asserts] -xpath "/logs/log" count >= 1 +xpath "/logs/log[logId > {{last_log_id}}]" count == 1 xpath "/logs/log[username!='admin']" not exists GET http://localhost:9090/plugins/restapi/v1/logs/security?username=nonexistent @@ -53,6 +56,7 @@ Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/logs/log" count == 1 +xpath "/logs/log[logId > {{last_log_id}}]" count == 1 GET http://localhost:9090/plugins/restapi/v1/logs/security?offset=1000000 Authorization: {{authkey}} @@ -77,4 +81,4 @@ GET http://localhost:9090/plugins/restapi/v1/logs/security?startTime=1&endTime=4 Authorization: {{authkey}} HTTP 200 [Asserts] -xpath "/logs/log[summary='Successful admin console login attempt']" exists +xpath "/logs/log[logId > {{last_log_id}}]" count == 1 From a45fb02800a1287b79a9ddc12f743e4eb7ed6b25 Mon Sep 17 00:00:00 2001 From: Dan Caseley Date: Fri, 25 Sep 2026 19:03:53 +0100 Subject: [PATCH 11/21] Hurl: Message Archive test prerequisites Co-Authored-By: Claude Opus 5.5 --- test/messagearchive.hurl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/messagearchive.hurl b/test/messagearchive.hurl index 7b06bc4f2..9f23929b7 100644 --- a/test/messagearchive.hurl +++ b/test/messagearchive.hurl @@ -5,4 +5,8 @@ HTTP 200 xpath "string(/archive/jid)" == "john@example.org" xpath "string(/archive/count)" == "0" -# TODO: How to get this >0 ? \ No newline at end of file +# TODO: The unread message count is read from the ofMessageArchive table, which is created and filled by the Monitoring +# plugin. That plugin is not installed on the test server, so the query fails (Openfire logs a warning) and 0 is +# returned. To test a count above 0: install the Monitoring plugin (with message archiving enabled), log in as john +# over BOSH and log out again (to record an offline date in ofPresence), send john a message from jane over BOSH, and +# then expect a count of 1. From b961c75386a12012f48548b66720bae403c766ae Mon Sep 17 00:00:00 2001 From: Dan Caseley Date: Fri, 25 Sep 2026 19:21:38 +0100 Subject: [PATCH 12/21] Hurl: replace the host variable with more specific URL variables Co-Authored-By: Claude Opus 5.5 --- test/README.md | 3 + test/chatrooms.hurl | 2 +- test/chatservice.hurl | 2 +- test/clustering.hurl | 4 +- test/groups.hurl | 14 ++-- test/messagearchive.hurl | 2 +- test/messagebroadcast.hurl | 8 +-- test/securitylog.hurl | 22 +++---- test/sessions.hurl | 18 +++--- test/statistics.hurl | 2 +- test/system.hurl | 128 ++++++++++++++++++------------------- test/test.env | 3 +- test/usergroups.hurl | 38 +++++------ test/userlockouts.hurl | 20 +++--- test/userroster.hurl | 30 ++++----- test/users.hurl | 22 +++---- 16 files changed, 161 insertions(+), 157 deletions(-) diff --git a/test/README.md b/test/README.md index 580562bb1..429401f39 100644 --- a/test/README.md +++ b/test/README.md @@ -10,6 +10,9 @@ Configure the Rest API: * Set auth for shared key, and set the value in test.env * Set `adminConsole.access.allow-wildcards-in-excludes` to true +test.env defines where the tests find Openfire: `restapi_url` (the base URL of the REST API, including its version), +`adminconsole_url` and `bosh_url`, as well as the shared secret (`authkey`). + Some tests create XMPP client sessions by logging in over BOSH (Openfire's HTTP binding, which listens on port 7070 by default). The BOSH endpoint is configured by the `bosh_url` variable in test.env; when Openfire runs in a container, make sure that port 7070 is published (or override the variable, e.g. `--variable bosh_url=http://:7070`). diff --git a/test/chatrooms.hurl b/test/chatrooms.hurl index cd1995e38..5dbf3953f 100644 --- a/test/chatrooms.hurl +++ b/test/chatrooms.hurl @@ -1,3 +1,3 @@ -GET http://localhost:9090/plugins/restapi/v1/chatrooms +GET {{restapi_url}}/chatrooms Authorization: {{authkey}} HTTP 200 diff --git a/test/chatservice.hurl b/test/chatservice.hurl index 99f31274b..041249176 100644 --- a/test/chatservice.hurl +++ b/test/chatservice.hurl @@ -1,4 +1,4 @@ -GET http://localhost:9090/plugins/restapi/v1/chatservices +GET {{restapi_url}}/chatservices Authorization: {{authkey}} HTTP 200 [Asserts] diff --git a/test/clustering.hurl b/test/clustering.hurl index 84fd5d471..e2496fb8f 100644 --- a/test/clustering.hurl +++ b/test/clustering.hurl @@ -1,10 +1,10 @@ -GET {{host}}/plugins/restapi/v1/clustering/status +GET {{restapi_url}}/clustering/status Authorization: {{authkey}} HTTP 200 [Asserts] xpath "string(/clustering/status)" == "Disabled" -GET {{host}}/plugins/restapi/v1/clustering/nodes +GET {{restapi_url}}/clustering/nodes Authorization: {{authkey}} HTTP 200 [Asserts] diff --git a/test/groups.hurl b/test/groups.hurl index 299ec523a..a06132235 100644 --- a/test/groups.hurl +++ b/test/groups.hurl @@ -1,11 +1,11 @@ -GET http://localhost:9090/plugins/restapi/v1/groups +GET {{restapi_url}}/groups Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/groups[not(child::node())]" exists # groups at the root, with no child nodes -POST http://localhost:9090/plugins/restapi/v1/groups +POST {{restapi_url}}/groups Authorization: {{authkey}} Content-Type: application/xml ``` @@ -24,20 +24,20 @@ Content-Type: application/xml ``` HTTP 201 -GET http://localhost:9090/plugins/restapi/v1/groups # check if the group was created +GET {{restapi_url}}/groups # check if the group was created Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/groups/group[name='group1']" exists -GET http://localhost:9090/plugins/restapi/v1/groups/group1 +GET {{restapi_url}}/groups/group1 Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/group[name='group1']" exists xpath "string(/group/description)" == "test-group" -PUT http://localhost:9090/plugins/restapi/v1/groups/group1 +PUT {{restapi_url}}/groups/group1 Authorization: {{authkey}} Content-Type: application/xml ``` @@ -56,14 +56,14 @@ Content-Type: application/xml ``` HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/groups/group1 +GET {{restapi_url}}/groups/group1 Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/group[name='group1']" exists xpath "string(/group/description)" == "test-group-updated" -DELETE http://localhost:9090/plugins/restapi/v1/groups/group1 +DELETE {{restapi_url}}/groups/group1 Authorization: {{authkey}} HTTP 200 diff --git a/test/messagearchive.hurl b/test/messagearchive.hurl index 9f23929b7..e789a2c32 100644 --- a/test/messagearchive.hurl +++ b/test/messagearchive.hurl @@ -1,4 +1,4 @@ -GET http://localhost:9090/plugins/restapi/v1/archive/messages/unread/john@example.org +GET {{restapi_url}}/archive/messages/unread/john@example.org Authorization: {{authkey}} HTTP 200 [Asserts] diff --git a/test/messagebroadcast.hurl b/test/messagebroadcast.hurl index 7bb1f79a3..3a086b798 100644 --- a/test/messagebroadcast.hurl +++ b/test/messagebroadcast.hurl @@ -1,5 +1,5 @@ # Broadcasting without any sessions to deliver to is fine. -POST http://localhost:9090/plugins/restapi/v1/messages/users +POST {{restapi_url}}/messages/users Authorization: {{authkey}} Content-Type: application/xml ``` @@ -10,7 +10,7 @@ Content-Type: application/xml ``` HTTP 201 -POST http://localhost:9090/plugins/restapi/v1/messages/users +POST {{restapi_url}}/messages/users Authorization: {{authkey}} Content-Type: application/xml ``` @@ -23,7 +23,7 @@ HTTP 400 [Asserts] xpath "string(/error/exception)" == "IllegalArgumentException" -POST http://localhost:9090/plugins/restapi/v1/messages/users +POST {{restapi_url}}/messages/users Authorization: {{authkey}} Content-Type: application/xml ``` @@ -75,7 +75,7 @@ Content-Type: text/xml; charset=utf-8 ``` HTTP 200 -POST http://localhost:9090/plugins/restapi/v1/messages/users +POST {{restapi_url}}/messages/users Authorization: {{authkey}} Content-Type: application/xml ``` diff --git a/test/securitylog.hurl b/test/securitylog.hurl index 8b1bee795..70437ecfb 100644 --- a/test/securitylog.hurl +++ b/test/securitylog.hurl @@ -1,6 +1,6 @@ # The log may already contain events (e.g. from earlier runs). Events are returned newest first, so remember the ID of # the newest one (or 0 when the log is empty), to be able to tell which events are new. -GET http://localhost:9090/plugins/restapi/v1/logs/security?limit=1 +GET {{restapi_url}}/logs/security?limit=1 Authorization: {{authkey}} HTTP 200 [Asserts] @@ -11,12 +11,12 @@ last_log_id: xpath "number(concat('0', /logs/log/logId))" # Openfire records a security audit event for every login to the admin console, so log in to create one. -GET http://localhost:9090/login.jsp +GET {{adminconsole_url}}/login.jsp HTTP 200 [Captures] csrf: cookie "csrf" -POST http://localhost:9090/login.jsp +POST {{adminconsole_url}}/login.jsp [Form] login: true csrf: {{csrf}} @@ -27,7 +27,7 @@ HTTP 302 header "Location" == "/index.jsp" -GET http://localhost:9090/plugins/restapi/v1/logs/security +GET {{restapi_url}}/logs/security Authorization: {{authkey}} HTTP 200 [Asserts] @@ -38,46 +38,46 @@ xpath "string(/logs/log[logId > {{last_log_id}}]/node)" == "example.org" xpath "string(/logs/log[logId > {{last_log_id}}]/details)" startsWith "The user logged in successfully to the admin console" xpath "/logs/log[logId > {{last_log_id}}]/timestamp" exists -GET http://localhost:9090/plugins/restapi/v1/logs/security?username=admin +GET {{restapi_url}}/logs/security?username=admin Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/logs/log[logId > {{last_log_id}}]" count == 1 xpath "/logs/log[username!='admin']" not exists -GET http://localhost:9090/plugins/restapi/v1/logs/security?username=nonexistent +GET {{restapi_url}}/logs/security?username=nonexistent Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/logs/log" count == 0 -GET http://localhost:9090/plugins/restapi/v1/logs/security?limit=1 +GET {{restapi_url}}/logs/security?limit=1 Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/logs/log" count == 1 xpath "/logs/log[logId > {{last_log_id}}]" count == 1 -GET http://localhost:9090/plugins/restapi/v1/logs/security?offset=1000000 +GET {{restapi_url}}/logs/security?offset=1000000 Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/logs/log" count == 0 # Timestamps are in seconds since the epoch: nothing was logged after 2100-01-01, or before 1970-01-01T00:00:01. -GET http://localhost:9090/plugins/restapi/v1/logs/security?startTime=4102444800 +GET {{restapi_url}}/logs/security?startTime=4102444800 Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/logs/log" count == 0 -GET http://localhost:9090/plugins/restapi/v1/logs/security?endTime=1 +GET {{restapi_url}}/logs/security?endTime=1 Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/logs/log" count == 0 -GET http://localhost:9090/plugins/restapi/v1/logs/security?startTime=1&endTime=4102444800 +GET {{restapi_url}}/logs/security?startTime=1&endTime=4102444800 Authorization: {{authkey}} HTTP 200 [Asserts] diff --git a/test/sessions.hurl b/test/sessions.hurl index 2d3299d31..5b820d074 100644 --- a/test/sessions.hurl +++ b/test/sessions.hurl @@ -1,11 +1,11 @@ -GET http://localhost:9090/plugins/restapi/v1/sessions +GET {{restapi_url}}/sessions Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/sessions" exists xpath "/sessions[not(child::node())]" count == 1 # sessions at the root, with no child nodes -GET http://localhost:9090/plugins/restapi/v1/sessions/john +GET {{restapi_url}}/sessions/john Authorization: {{authkey}} HTTP 200 [Asserts] @@ -14,7 +14,7 @@ xpath "/sessions/session" count == 0 # Openfire does not check whether the user exists: a user that does not exist simply has no sessions. The API documents # no other response than 200 for this endpoint. -GET http://localhost:9090/plugins/restapi/v1/sessions/nonexistent +GET {{restapi_url}}/sessions/nonexistent Authorization: {{authkey}} HTTP 200 [Asserts] @@ -66,13 +66,13 @@ Content-Type: text/xml; charset=utf-8 ``` HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/sessions +GET {{restapi_url}}/sessions Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/sessions/session[sessionId='john@example.org/hurl']" exists -GET http://localhost:9090/plugins/restapi/v1/sessions/john +GET {{restapi_url}}/sessions/john Authorization: {{authkey}} HTTP 200 [Asserts] @@ -88,19 +88,19 @@ xpath "/sessions/session/hostAddress" exists xpath "/sessions/session/creationDate" exists xpath "/sessions/session/lastActionDate" exists -GET http://localhost:9090/plugins/restapi/v1/sessions/jane +GET {{restapi_url}}/sessions/jane Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/sessions/session" count == 0 # Kick the session. -DELETE http://localhost:9090/plugins/restapi/v1/sessions/john +DELETE {{restapi_url}}/sessions/john Authorization: {{authkey}} HTTP 200 # Sessions are closed asynchronously, so allow for some delay. -GET http://localhost:9090/plugins/restapi/v1/sessions/john +GET {{restapi_url}}/sessions/john Authorization: {{authkey}} [Options] retry: 10 @@ -121,6 +121,6 @@ retry-interval: 100ms HTTP 404 # Kicking the sessions of a user without sessions is fine. -DELETE http://localhost:9090/plugins/restapi/v1/sessions/john +DELETE {{restapi_url}}/sessions/john Authorization: {{authkey}} HTTP 200 diff --git a/test/statistics.hurl b/test/statistics.hurl index 4df4014d3..5cea3da8d 100644 --- a/test/statistics.hurl +++ b/test/statistics.hurl @@ -1,4 +1,4 @@ -GET http://localhost:9090/plugins/restapi/v1/system/statistics/sessions +GET {{restapi_url}}/system/statistics/sessions Authorization: {{authkey}} HTTP 200 [Asserts] diff --git a/test/system.hurl b/test/system.hurl index 5f805f75d..fb47e3a47 100644 --- a/test/system.hurl +++ b/test/system.hurl @@ -1,36 +1,36 @@ -GET http://localhost:9090/plugins/restapi/v1/system/liveness +GET {{restapi_url}}/system/liveness Authorization: {{authkey}} HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/system/liveness/deadlock +GET {{restapi_url}}/system/liveness/deadlock Authorization: {{authkey}} HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/system/liveness/properties +GET {{restapi_url}}/system/liveness/properties Authorization: {{authkey}} HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/system/readiness +GET {{restapi_url}}/system/readiness Authorization: {{authkey}} HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/system/readiness/cluster +GET {{restapi_url}}/system/readiness/cluster Authorization: {{authkey}} HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/system/readiness/connections +GET {{restapi_url}}/system/readiness/connections Authorization: {{authkey}} HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/system/readiness/plugins +GET {{restapi_url}}/system/readiness/plugins Authorization: {{authkey}} HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/system/readiness/server +GET {{restapi_url}}/system/readiness/server Authorization: {{authkey}} HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/system/properties +GET {{restapi_url}}/system/properties Authorization: {{authkey}} HTTP 200 [Asserts] @@ -44,7 +44,7 @@ xpath "/properties/property[starts-with(@key, 'plugin.restapi.')]" not exists # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/242 # abstractGroupProvider.shared.recursive is registered through Openfire's SystemProperty API with a default value. # It used to 404 when queried individually, as that lookup checked JiveGlobals only. -GET http://localhost:9090/plugins/restapi/v1/system/properties/abstractGroupProvider.shared.recursive +GET {{restapi_url}}/system/properties/abstractGroupProvider.shared.recursive Authorization: {{authkey}} HTTP 200 [Asserts] @@ -54,13 +54,13 @@ xpath "string(/property/@value)" == "false" # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/242 # adminConsole.servlet-request-authenticator is registered through Openfire's SystemProperty API without a default value. # It used to 404 when queried individually, as that lookup checked JiveGlobals only. -GET http://localhost:9090/plugins/restapi/v1/system/properties/adminConsole.servlet-request-authenticator +GET {{restapi_url}}/system/properties/adminConsole.servlet-request-authenticator Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/property" count == 1 -POST http://localhost:9090/plugins/restapi/v1/system/properties +POST {{restapi_url}}/system/properties Authorization: {{authkey}} Content-Type: application/xml ``` @@ -69,7 +69,7 @@ Content-Type: application/xml ``` HTTP 201 -GET http://localhost:9090/plugins/restapi/v1/system/properties/test.key +GET {{restapi_url}}/system/properties/test.key Authorization: {{authkey}} HTTP 200 [Asserts] @@ -77,7 +77,7 @@ xpath "/property" count == 1 xpath "string(/property/@key)" == "test.key" xpath "string(/property/@value)" == "test-value" -PUT http://localhost:9090/plugins/restapi/v1/system/properties/test.key +PUT {{restapi_url}}/system/properties/test.key Authorization: {{authkey}} Content-Type: application/xml ``` @@ -86,13 +86,13 @@ Content-Type: application/xml ``` HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/system/properties/test.key +GET {{restapi_url}}/system/properties/test.key Authorization: {{authkey}} HTTP 200 [Asserts] xpath "string(/property/@value)" == "new-value" -PUT http://localhost:9090/plugins/restapi/v1/system/properties/wrong.key +PUT {{restapi_url}}/system/properties/wrong.key Authorization: {{authkey}} Content-Type: application/xml ``` @@ -101,7 +101,7 @@ Content-Type: application/xml ``` HTTP 404 -PUT http://localhost:9090/plugins/restapi/v1/system/properties/test.key +PUT {{restapi_url}}/system/properties/test.key Authorization: {{authkey}} Content-Type: application/xml ``` @@ -110,23 +110,23 @@ Content-Type: application/xml ``` HTTP 400 -DELETE http://localhost:9090/plugins/restapi/v1/system/properties/test.key +DELETE {{restapi_url}}/system/properties/test.key Authorization: {{authkey}} HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/system/properties/test.key +GET {{restapi_url}}/system/properties/test.key Authorization: {{authkey}} HTTP 404 # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. -GET http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.enabled +GET {{restapi_url}}/system/properties/plugin.restapi.enabled Authorization: {{authkey}} HTTP 403 # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. -PUT http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.enabled +PUT {{restapi_url}}/system/properties/plugin.restapi.enabled Authorization: {{authkey}} Content-Type: application/xml ``` @@ -137,20 +137,20 @@ HTTP 403 # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. -DELETE http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.enabled +DELETE {{restapi_url}}/system/properties/plugin.restapi.enabled Authorization: {{authkey}} HTTP 403 # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. -GET http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.httpAuth +GET {{restapi_url}}/system/properties/plugin.restapi.httpAuth Authorization: {{authkey}} HTTP 403 # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. -PUT http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.httpAuth +PUT {{restapi_url}}/system/properties/plugin.restapi.httpAuth Authorization: {{authkey}} Content-Type: application/xml ``` @@ -161,20 +161,20 @@ HTTP 403 # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. -DELETE http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.httpAuth +DELETE {{restapi_url}}/system/properties/plugin.restapi.httpAuth Authorization: {{authkey}} HTTP 403 # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. -GET http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.customAuthFilter +GET {{restapi_url}}/system/properties/plugin.restapi.customAuthFilter Authorization: {{authkey}} HTTP 403 # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. -PUT http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.customAuthFilter +PUT {{restapi_url}}/system/properties/plugin.restapi.customAuthFilter Authorization: {{authkey}} Content-Type: application/xml ``` @@ -185,20 +185,20 @@ HTTP 403 # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. -DELETE http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.customAuthFilter +DELETE {{restapi_url}}/system/properties/plugin.restapi.customAuthFilter Authorization: {{authkey}} HTTP 403 # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. -GET http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.secret +GET {{restapi_url}}/system/properties/plugin.restapi.secret Authorization: {{authkey}} HTTP 403 # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. -PUT http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.secret +PUT {{restapi_url}}/system/properties/plugin.restapi.secret Authorization: {{authkey}} Content-Type: application/xml ``` @@ -209,20 +209,20 @@ HTTP 403 # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. -DELETE http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.secret +DELETE {{restapi_url}}/system/properties/plugin.restapi.secret Authorization: {{authkey}} HTTP 403 # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. -GET http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.serviceLoggingEnabled +GET {{restapi_url}}/system/properties/plugin.restapi.serviceLoggingEnabled Authorization: {{authkey}} HTTP 403 # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. -PUT http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.serviceLoggingEnabled +PUT {{restapi_url}}/system/properties/plugin.restapi.serviceLoggingEnabled Authorization: {{authkey}} Content-Type: application/xml ``` @@ -233,20 +233,20 @@ HTTP 403 # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. -DELETE http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.serviceLoggingEnabled +DELETE {{restapi_url}}/system/properties/plugin.restapi.serviceLoggingEnabled Authorization: {{authkey}} HTTP 403 # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. -GET http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.allowedIPs +GET {{restapi_url}}/system/properties/plugin.restapi.allowedIPs Authorization: {{authkey}} HTTP 403 # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. -PUT http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.allowedIPs +PUT {{restapi_url}}/system/properties/plugin.restapi.allowedIPs Authorization: {{authkey}} Content-Type: application/xml ``` @@ -257,7 +257,7 @@ HTTP 403 # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. -DELETE http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.allowedIPs +DELETE {{restapi_url}}/system/properties/plugin.restapi.allowedIPs Authorization: {{authkey}} HTTP 403 @@ -265,13 +265,13 @@ HTTP 403 # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # Openfire deletes a property together with all of its child properties. Deleting the parent of the plugin's own # properties (which exist in this setup) should therefore not be allowed either. -DELETE http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi +DELETE {{restapi_url}}/system/properties/plugin.restapi Authorization: {{authkey}} HTTP 403 # Deleting a property also deletes its child properties. -POST http://localhost:9090/plugins/restapi/v1/system/properties +POST {{restapi_url}}/system/properties Authorization: {{authkey}} Content-Type: application/xml ``` @@ -280,7 +280,7 @@ Content-Type: application/xml ``` HTTP 201 -POST http://localhost:9090/plugins/restapi/v1/system/properties +POST {{restapi_url}}/system/properties Authorization: {{authkey}} Content-Type: application/xml ``` @@ -289,11 +289,11 @@ Content-Type: application/xml ``` HTTP 201 -DELETE http://localhost:9090/plugins/restapi/v1/system/properties/test.parent +DELETE {{restapi_url}}/system/properties/test.parent Authorization: {{authkey}} HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/system/properties/test.parent.child +GET {{restapi_url}}/system/properties/test.parent.child Authorization: {{authkey}} HTTP 404 @@ -301,7 +301,7 @@ HTTP 404 # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/259 # Openfire deletes a property using SQL LIKE, in which an underscore matches any character. Deleting a property should # be refused when that would also delete a property other than the property itself and its child properties. -POST http://localhost:9090/plugins/restapi/v1/system/properties +POST {{restapi_url}}/system/properties Authorization: {{authkey}} Content-Type: application/xml ``` @@ -310,7 +310,7 @@ Content-Type: application/xml ``` HTTP 201 -POST http://localhost:9090/plugins/restapi/v1/system/properties +POST {{restapi_url}}/system/properties Authorization: {{authkey}} Content-Type: application/xml ``` @@ -319,19 +319,19 @@ Content-Type: application/xml ``` HTTP 201 -DELETE http://localhost:9090/plugins/restapi/v1/system/properties/test.foo_bar +DELETE {{restapi_url}}/system/properties/test.foo_bar Authorization: {{authkey}} HTTP 409 -GET http://localhost:9090/plugins/restapi/v1/system/properties/test.fooXbar.child +GET {{restapi_url}}/system/properties/test.fooXbar.child Authorization: {{authkey}} HTTP 200 -DELETE http://localhost:9090/plugins/restapi/v1/system/properties/test.fooXbar.child +DELETE {{restapi_url}}/system/properties/test.fooXbar.child Authorization: {{authkey}} HTTP 200 -DELETE http://localhost:9090/plugins/restapi/v1/system/properties/test.foo_bar +DELETE {{restapi_url}}/system/properties/test.foo_bar Authorization: {{authkey}} HTTP 200 @@ -339,13 +339,13 @@ HTTP 200 # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/259 # Openfire itself uses apostrophes in property keys (e.g. for the caches of MUC services), which should be usable in # the URL path. -GET http://localhost:9090/plugins/restapi/v1/system/properties/cache.MUCService'conference'Rooms.size +GET {{restapi_url}}/system/properties/cache.MUCService'conference'Rooms.size Authorization: {{authkey}} HTTP 200 [Asserts] xpath "string(/property/@key)" == "cache.MUCService'conference'Rooms.size" -POST http://localhost:9090/plugins/restapi/v1/system/properties +POST {{restapi_url}}/system/properties Authorization: {{authkey}} Content-Type: application/xml ``` @@ -354,17 +354,17 @@ Content-Type: application/xml ``` HTTP 201 -GET http://localhost:9090/plugins/restapi/v1/system/properties/test.MUCService'room'Rooms.size +GET {{restapi_url}}/system/properties/test.MUCService'room'Rooms.size Authorization: {{authkey}} HTTP 200 [Asserts] xpath "string(/property/@value)" == "42" -DELETE http://localhost:9090/plugins/restapi/v1/system/properties/test.MUCService'room'Rooms.size +DELETE {{restapi_url}}/system/properties/test.MUCService'room'Rooms.size Authorization: {{authkey}} HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/system/properties/test.MUCService'room'Rooms.size +GET {{restapi_url}}/system/properties/test.MUCService'room'Rooms.size Authorization: {{authkey}} HTTP 404 @@ -372,7 +372,7 @@ HTTP 404 # Openfire treats setting a property to a null value as deleting it, together with all of its child properties. # Creating or updating a property without a value should therefore be rejected, as it would otherwise bypass the # checks that prevent (forbidden) child properties from being deleted. -POST http://localhost:9090/plugins/restapi/v1/system/properties +POST {{restapi_url}}/system/properties Authorization: {{authkey}} Content-Type: application/xml ``` @@ -381,7 +381,7 @@ Content-Type: application/xml ``` HTTP 201 -POST http://localhost:9090/plugins/restapi/v1/system/properties +POST {{restapi_url}}/system/properties Authorization: {{authkey}} Content-Type: application/xml ``` @@ -390,7 +390,7 @@ Content-Type: application/xml ``` HTTP 201 -POST http://localhost:9090/plugins/restapi/v1/system/properties +POST {{restapi_url}}/system/properties Authorization: {{authkey}} Content-Type: application/xml ``` @@ -399,7 +399,7 @@ Content-Type: application/xml ``` HTTP 400 -PUT http://localhost:9090/plugins/restapi/v1/system/properties/test.nullparent +PUT {{restapi_url}}/system/properties/test.nullparent Authorization: {{authkey}} Content-Type: application/xml ``` @@ -408,25 +408,25 @@ Content-Type: application/xml ``` HTTP 400 -GET http://localhost:9090/plugins/restapi/v1/system/properties/test.nullparent +GET {{restapi_url}}/system/properties/test.nullparent Authorization: {{authkey}} HTTP 200 [Asserts] xpath "string(/property/@value)" == "parent-value" -GET http://localhost:9090/plugins/restapi/v1/system/properties/test.nullparent.child +GET {{restapi_url}}/system/properties/test.nullparent.child Authorization: {{authkey}} HTTP 200 [Asserts] xpath "string(/property/@value)" == "child-value" -DELETE http://localhost:9090/plugins/restapi/v1/system/properties/test.nullparent +DELETE {{restapi_url}}/system/properties/test.nullparent Authorization: {{authkey}} HTTP 200 # The parent of the plugin's own properties may not be 'created' without a value (as that would delete the plugin's # configuration) either. -POST http://localhost:9090/plugins/restapi/v1/system/properties +POST {{restapi_url}}/system/properties Authorization: {{authkey}} Content-Type: application/xml ``` @@ -436,7 +436,7 @@ Content-Type: application/xml HTTP 400 # An empty value is a value (rather than a deletion), and should be stored as such. -POST http://localhost:9090/plugins/restapi/v1/system/properties +POST {{restapi_url}}/system/properties Authorization: {{authkey}} Content-Type: application/xml ``` @@ -445,12 +445,12 @@ Content-Type: application/xml ``` HTTP 201 -GET http://localhost:9090/plugins/restapi/v1/system/properties/test.emptyvalue +GET {{restapi_url}}/system/properties/test.emptyvalue Authorization: {{authkey}} HTTP 200 [Asserts] xpath "string(/property/@value)" == "" -DELETE http://localhost:9090/plugins/restapi/v1/system/properties/test.emptyvalue +DELETE {{restapi_url}}/system/properties/test.emptyvalue Authorization: {{authkey}} HTTP 200 diff --git a/test/test.env b/test/test.env index 1f0d86b8f..c888f8986 100644 --- a/test/test.env +++ b/test/test.env @@ -1,3 +1,4 @@ -host=http://localhost:9090 authkey=potato +adminconsole_url=http://localhost:9090 +restapi_url=http://localhost:9090/plugins/restapi/v1 bosh_url=http://localhost:7070 diff --git a/test/usergroups.hurl b/test/usergroups.hurl index 625be5a6b..c68bc9eec 100644 --- a/test/usergroups.hurl +++ b/test/usergroups.hurl @@ -1,5 +1,5 @@ # Group membership is tested on a dedicated user, which is removed at the end. -POST http://localhost:9090/plugins/restapi/v1/users +POST {{restapi_url}}/users Authorization: {{authkey}} Content-Type: application/xml ``` @@ -11,7 +11,7 @@ Content-Type: application/xml ``` HTTP 201 -POST http://localhost:9090/plugins/restapi/v1/groups +POST {{restapi_url}}/groups Authorization: {{authkey}} Content-Type: application/xml ``` @@ -26,7 +26,7 @@ Content-Type: application/xml ``` HTTP 201 -POST http://localhost:9090/plugins/restapi/v1/groups +POST {{restapi_url}}/groups Authorization: {{authkey}} Content-Type: application/xml ``` @@ -41,7 +41,7 @@ Content-Type: application/xml ``` HTTP 201 -POST http://localhost:9090/plugins/restapi/v1/groups +POST {{restapi_url}}/groups Authorization: {{authkey}} Content-Type: application/xml ``` @@ -56,17 +56,17 @@ Content-Type: application/xml ``` HTTP 201 -GET http://localhost:9090/plugins/restapi/v1/users/geordi/groups +GET {{restapi_url}}/users/geordi/groups Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/groups/groupname" count == 0 -POST http://localhost:9090/plugins/restapi/v1/users/geordi/groups/security +POST {{restapi_url}}/users/geordi/groups/security Authorization: {{authkey}} HTTP 201 -POST http://localhost:9090/plugins/restapi/v1/users/geordi/groups +POST {{restapi_url}}/users/geordi/groups Authorization: {{authkey}} Content-Type: application/xml ``` @@ -78,7 +78,7 @@ Content-Type: application/xml ``` HTTP 201 -GET http://localhost:9090/plugins/restapi/v1/users/geordi/groups +GET {{restapi_url}}/users/geordi/groups Authorization: {{authkey}} HTTP 200 [Asserts] @@ -87,25 +87,25 @@ xpath "/groups[groupname='security']" exists xpath "/groups[groupname='klingons']" exists xpath "/groups[groupname='bridge']" exists -GET http://localhost:9090/plugins/restapi/v1/groups/klingons +GET {{restapi_url}}/groups/klingons Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/group/members/member" count == 1 xpath "string(/group/members/member)" == "geordi@example.org" -DELETE http://localhost:9090/plugins/restapi/v1/users/geordi/groups/security +DELETE {{restapi_url}}/users/geordi/groups/security Authorization: {{authkey}} HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/users/geordi/groups +GET {{restapi_url}}/users/geordi/groups Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/groups/groupname" count == 2 xpath "/groups[groupname='security']" not exists -DELETE http://localhost:9090/plugins/restapi/v1/users/geordi/groups +DELETE {{restapi_url}}/users/geordi/groups Authorization: {{authkey}} Content-Type: application/xml ``` @@ -117,20 +117,20 @@ Content-Type: application/xml ``` HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/users/geordi/groups +GET {{restapi_url}}/users/geordi/groups Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/groups/groupname" count == 0 -DELETE http://localhost:9090/plugins/restapi/v1/users/geordi/groups/nonexistent +DELETE {{restapi_url}}/users/geordi/groups/nonexistent Authorization: {{authkey}} HTTP 404 [Asserts] xpath "string(/error/exception)" == "GroupNotFoundException" xpath "string(/error/resource)" == "nonexistent" -GET http://localhost:9090/plugins/restapi/v1/users/nonexistent/groups +GET {{restapi_url}}/users/nonexistent/groups Authorization: {{authkey}} HTTP 404 [Asserts] @@ -144,18 +144,18 @@ xpath "string(/error/exception)" == "UserNotFoundException" # TODO: Adding a user that does not exist to an (existing) group returns 201 and adds the JID to the group, where # 404 might be expected. Add a test once the intended behaviour is decided. -DELETE http://localhost:9090/plugins/restapi/v1/groups/security +DELETE {{restapi_url}}/groups/security Authorization: {{authkey}} HTTP 200 -DELETE http://localhost:9090/plugins/restapi/v1/groups/klingons +DELETE {{restapi_url}}/groups/klingons Authorization: {{authkey}} HTTP 200 -DELETE http://localhost:9090/plugins/restapi/v1/groups/bridge +DELETE {{restapi_url}}/groups/bridge Authorization: {{authkey}} HTTP 200 -DELETE http://localhost:9090/plugins/restapi/v1/users/geordi +DELETE {{restapi_url}}/users/geordi Authorization: {{authkey}} HTTP 200 diff --git a/test/userlockouts.hurl b/test/userlockouts.hurl index 0177c669b..f13cebc24 100644 --- a/test/userlockouts.hurl +++ b/test/userlockouts.hurl @@ -1,5 +1,5 @@ # Lockouts are tested on a dedicated user, which is removed at the end. -POST http://localhost:9090/plugins/restapi/v1/users +POST {{restapi_url}}/users Authorization: {{authkey}} Content-Type: application/xml ``` @@ -52,19 +52,19 @@ Content-Type: text/xml; charset=utf-8 ``` HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/sessions/worf +GET {{restapi_url}}/sessions/worf Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/sessions/session" count == 1 -POST http://localhost:9090/plugins/restapi/v1/lockouts/worf +POST {{restapi_url}}/lockouts/worf Authorization: {{authkey}} HTTP 201 # Sessions are closed asynchronously, so allow for some delay. # Locking out a user closes its sessions. -GET http://localhost:9090/plugins/restapi/v1/sessions/worf +GET {{restapi_url}}/sessions/worf Authorization: {{authkey}} [Options] retry: 10 @@ -85,7 +85,7 @@ retry-interval: 100ms HTTP 404 # Locking out a user that is already locked out is fine. -POST http://localhost:9090/plugins/restapi/v1/lockouts/worf +POST {{restapi_url}}/lockouts/worf Authorization: {{authkey}} HTTP 201 @@ -116,12 +116,12 @@ Content-Type: text/xml; charset=utf-8 ``` HTTP 200 -DELETE http://localhost:9090/plugins/restapi/v1/lockouts/worf +DELETE {{restapi_url}}/lockouts/worf Authorization: {{authkey}} HTTP 200 # Unlocking a user that is not locked out is fine. -DELETE http://localhost:9090/plugins/restapi/v1/lockouts/worf +DELETE {{restapi_url}}/lockouts/worf Authorization: {{authkey}} HTTP 200 @@ -173,14 +173,14 @@ Content-Type: text/xml; charset=utf-8 ``` HTTP 200 -POST http://localhost:9090/plugins/restapi/v1/lockouts/nonexistent +POST {{restapi_url}}/lockouts/nonexistent Authorization: {{authkey}} HTTP 404 [Asserts] xpath "string(/error/exception)" == "UserNotFoundException" xpath "string(/error/resource)" == "nonexistent" -DELETE http://localhost:9090/plugins/restapi/v1/lockouts/nonexistent +DELETE {{restapi_url}}/lockouts/nonexistent Authorization: {{authkey}} HTTP 404 [Asserts] @@ -188,6 +188,6 @@ xpath "string(/error/exception)" == "UserNotFoundException" xpath "string(/error/resource)" == "nonexistent" -DELETE http://localhost:9090/plugins/restapi/v1/users/worf +DELETE {{restapi_url}}/users/worf Authorization: {{authkey}} HTTP 200 diff --git a/test/userroster.hurl b/test/userroster.hurl index 976bd014f..471801bf9 100644 --- a/test/userroster.hurl +++ b/test/userroster.hurl @@ -1,4 +1,4 @@ -GET http://localhost:9090/plugins/restapi/v1/users/john/roster +GET {{restapi_url}}/users/john/roster Authorization: {{authkey}} HTTP 200 [Asserts] @@ -10,7 +10,7 @@ xpath "/roster/rosterItem/groups" exists # Roster changes are tested on a dedicated user, which is removed at the end. -POST http://localhost:9090/plugins/restapi/v1/users +POST {{restapi_url}}/users Authorization: {{authkey}} Content-Type: application/xml ``` @@ -22,13 +22,13 @@ Content-Type: application/xml ``` HTTP 201 -GET http://localhost:9090/plugins/restapi/v1/users/data/roster +GET {{restapi_url}}/users/data/roster Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/roster/rosterItem" count == 0 -POST http://localhost:9090/plugins/restapi/v1/users/data/roster +POST {{restapi_url}}/users/data/roster Authorization: {{authkey}} Content-Type: application/xml ``` @@ -44,7 +44,7 @@ Content-Type: application/xml ``` HTTP 201 -GET http://localhost:9090/plugins/restapi/v1/users/data/roster +GET {{restapi_url}}/users/data/roster Authorization: {{authkey}} HTTP 200 [Asserts] @@ -56,7 +56,7 @@ xpath "/roster/rosterItem/groups/group" count == 1 xpath "string(/roster/rosterItem/groups/group)" == "Friends" # Adding an entry for a contact that is already on the roster is a conflict. -POST http://localhost:9090/plugins/restapi/v1/users/data/roster +POST {{restapi_url}}/users/data/roster Authorization: {{authkey}} Content-Type: application/xml ``` @@ -70,7 +70,7 @@ HTTP 409 xpath "string(/error/exception)" == "UserAlreadyExistsException" # A roster entry needs a JID. -POST http://localhost:9090/plugins/restapi/v1/users/data/roster +POST {{restapi_url}}/users/data/roster Authorization: {{authkey}} Content-Type: application/xml ``` @@ -96,7 +96,7 @@ xpath "string(/error/exception)" == "IllegalArgumentException" # documented. Getting the roster of a user that does not exist returns 200 (rather than 404) too. Add tests for those # once UserServiceController#getUserRoster checks that the user exists. -PUT http://localhost:9090/plugins/restapi/v1/users/data/roster/john@example.org +PUT {{restapi_url}}/users/data/roster/john@example.org Authorization: {{authkey}} Content-Type: application/xml ``` @@ -112,7 +112,7 @@ Content-Type: application/xml ``` HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/users/data/roster +GET {{restapi_url}}/users/data/roster Authorization: {{authkey}} HTTP 200 [Asserts] @@ -124,7 +124,7 @@ xpath "/roster/rosterItem/groups/group" count == 1 xpath "string(/roster/rosterItem/groups/group)" == "Crew" # Updating an entry for a contact that is not on the roster fails. -PUT http://localhost:9090/plugins/restapi/v1/users/data/roster/jane@example.org +PUT {{restapi_url}}/users/data/roster/jane@example.org Authorization: {{authkey}} Content-Type: application/xml ``` @@ -139,30 +139,30 @@ HTTP 404 xpath "string(/error/exception)" == "UserNotFoundException" xpath "string(/error/resource)" == "jane@example.org" -DELETE http://localhost:9090/plugins/restapi/v1/users/data/roster/john@example.org +DELETE {{restapi_url}}/users/data/roster/john@example.org Authorization: {{authkey}} HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/users/data/roster +GET {{restapi_url}}/users/data/roster Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/roster/rosterItem" count == 0 -DELETE http://localhost:9090/plugins/restapi/v1/users/data/roster/john@example.org +DELETE {{restapi_url}}/users/data/roster/john@example.org Authorization: {{authkey}} HTTP 404 [Asserts] xpath "string(/error/exception)" == "RosterItemNotFound" xpath "string(/error/resource)" == "john@example.org" -DELETE http://localhost:9090/plugins/restapi/v1/users/nonexistent/roster/john@example.org +DELETE {{restapi_url}}/users/nonexistent/roster/john@example.org Authorization: {{authkey}} HTTP 404 [Asserts] xpath "string(/error/exception)" == "UserNotFoundException" xpath "string(/error/resource)" == "nonexistent" -DELETE http://localhost:9090/plugins/restapi/v1/users/data +DELETE {{restapi_url}}/users/data Authorization: {{authkey}} HTTP 200 diff --git a/test/users.hurl b/test/users.hurl index 8ec68e414..307f01bcd 100644 --- a/test/users.hurl +++ b/test/users.hurl @@ -1,4 +1,4 @@ -GET http://localhost:9090/plugins/restapi/v1/users +GET {{restapi_url}}/users Authorization: {{authkey}} HTTP 200 [Asserts] @@ -9,7 +9,7 @@ xpath "/users/user[username='john']" exists xpath "/users/user[name='John Doe']" exists xpath "/users/user[email='john.doe@example.com']" exists -GET http://localhost:9090/plugins/restapi/v1/users?search=john +GET {{restapi_url}}/users?search=john Authorization: {{authkey}} HTTP 200 [Asserts] @@ -18,13 +18,13 @@ xpath "/users/user[username='john']" exists xpath "/users/user[name='John Doe']" exists xpath "/users/user[email='john.doe@example.com']" exists -GET http://localhost:9090/plugins/restapi/v1/users?propertyKey=tea +GET {{restapi_url}}/users?propertyKey=tea Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/users/user" count == 0 -POST http://localhost:9090/plugins/restapi/v1/users +POST {{restapi_url}}/users Authorization: {{authkey}} Content-Type: application/xml ``` @@ -41,7 +41,7 @@ Content-Type: application/xml ``` HTTP 201 -GET http://localhost:9090/plugins/restapi/v1/users?propertyKey=tea&propertyValue=earlgreyhot +GET {{restapi_url}}/users?propertyKey=tea&propertyValue=earlgreyhot Authorization: {{authkey}} HTTP 200 [Asserts] @@ -53,20 +53,20 @@ xpath "/users/user/properties/property" count == 1 xpath "string(/users/user/properties/property/@key)" == "tea" xpath "string(/users/user/properties/property/@value)" == "earlgreyhot" -GET http://localhost:9090/plugins/restapi/v1/users?propertyKey=tea +GET {{restapi_url}}/users?propertyKey=tea Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/users/user" count == 1 xpath "string(/users/user/username)" == "jeanluc" -GET http://localhost:9090/plugins/restapi/v1/users?propertyKey=tea&propertyValue=coffee +GET {{restapi_url}}/users?propertyKey=tea&propertyValue=coffee Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/users/user" count == 0 -PUT http://localhost:9090/plugins/restapi/v1/users/jeanluc +PUT {{restapi_url}}/users/jeanluc Authorization: {{authkey}} Content-Type: application/xml ``` @@ -83,7 +83,7 @@ Content-Type: application/xml HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/users/jeanluc +GET {{restapi_url}}/users/jeanluc Authorization: {{authkey}} HTTP 200 [Asserts] @@ -93,11 +93,11 @@ xpath "/user[name='Jean-Luc']" exists xpath "/user[email='jlp@example.com']" not exists xpath "/user[email='jeanluc@example.com']" exists -DELETE http://localhost:9090/plugins/restapi/v1/users/jeanluc +DELETE {{restapi_url}}/users/jeanluc Authorization: {{authkey}} HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/users/jeanluc +GET {{restapi_url}}/users/jeanluc Authorization: {{authkey}} HTTP 404 [Asserts] From 7880ce0bdd33442fd9a3d3a15984816dc599a6cd Mon Sep 17 00:00:00 2001 From: Dan Caseley Date: Fri, 25 Sep 2026 19:22:51 +0100 Subject: [PATCH 13/21] Hurl: describe the intent of each test Co-Authored-By: Claude Opus 5.5 --- test/chatrooms.hurl | 1 + test/chatservice.hurl | 1 + test/clustering.hurl | 2 ++ test/groups.hurl | 6 +++++- test/messagearchive.hurl | 1 + test/messagebroadcast.hurl | 6 ++++-- test/securitylog.hurl | 8 +++++++ test/sessions.hurl | 8 +++++-- test/statistics.hurl | 1 + test/system.hurl | 43 +++++++++++++++++++++++++++++++++++++- test/usergroups.hurl | 6 ++++++ test/userlockouts.hurl | 13 +++++++----- test/userroster.hurl | 13 +++++++++--- test/users.hurl | 9 ++++++++ 14 files changed, 104 insertions(+), 14 deletions(-) diff --git a/test/chatrooms.hurl b/test/chatrooms.hurl index 5dbf3953f..4fa565037 100644 --- a/test/chatrooms.hurl +++ b/test/chatrooms.hurl @@ -1,3 +1,4 @@ +# TEST: List the chat rooms GET {{restapi_url}}/chatrooms Authorization: {{authkey}} HTTP 200 diff --git a/test/chatservice.hurl b/test/chatservice.hurl index 041249176..61004072f 100644 --- a/test/chatservice.hurl +++ b/test/chatservice.hurl @@ -1,3 +1,4 @@ +# TEST: The default 'conference' chat service is listed GET {{restapi_url}}/chatservices Authorization: {{authkey}} HTTP 200 diff --git a/test/clustering.hurl b/test/clustering.hurl index e2496fb8f..83dbbd7fa 100644 --- a/test/clustering.hurl +++ b/test/clustering.hurl @@ -1,9 +1,11 @@ +# TEST: Clustering is reported as disabled on a standalone server GET {{restapi_url}}/clustering/status Authorization: {{authkey}} HTTP 200 [Asserts] xpath "string(/clustering/status)" == "Disabled" +# TEST: A standalone server has no cluster nodes GET {{restapi_url}}/clustering/nodes Authorization: {{authkey}} HTTP 200 diff --git a/test/groups.hurl b/test/groups.hurl index a06132235..47d3504b3 100644 --- a/test/groups.hurl +++ b/test/groups.hurl @@ -1,10 +1,12 @@ +# TEST: There are no groups initially GET {{restapi_url}}/groups Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/groups[not(child::node())]" exists # groups at the root, with no child nodes +# TEST: A created group is listed, and can be retrieved POST {{restapi_url}}/groups Authorization: {{authkey}} Content-Type: application/xml @@ -24,7 +26,7 @@ Content-Type: application/xml ``` HTTP 201 -GET {{restapi_url}}/groups # check if the group was created +GET {{restapi_url}}/groups Authorization: {{authkey}} HTTP 200 [Asserts] @@ -37,6 +39,7 @@ HTTP 200 xpath "/group[name='group1']" exists xpath "string(/group/description)" == "test-group" +# TEST: Updating a group changes its description PUT {{restapi_url}}/groups/group1 Authorization: {{authkey}} Content-Type: application/xml @@ -63,6 +66,7 @@ HTTP 200 xpath "/group[name='group1']" exists xpath "string(/group/description)" == "test-group-updated" +# TEST: Delete a group DELETE {{restapi_url}}/groups/group1 Authorization: {{authkey}} HTTP 200 diff --git a/test/messagearchive.hurl b/test/messagearchive.hurl index e789a2c32..1b7551c47 100644 --- a/test/messagearchive.hurl +++ b/test/messagearchive.hurl @@ -1,3 +1,4 @@ +# TEST: Get the unread message count for a user GET {{restapi_url}}/archive/messages/unread/john@example.org Authorization: {{authkey}} HTTP 200 diff --git a/test/messagebroadcast.hurl b/test/messagebroadcast.hurl index 3a086b798..5ec55e215 100644 --- a/test/messagebroadcast.hurl +++ b/test/messagebroadcast.hurl @@ -1,4 +1,4 @@ -# Broadcasting without any sessions to deliver to is fine. +# TEST: Broadcast a message while there are no sessions to deliver it to POST {{restapi_url}}/messages/users Authorization: {{authkey}} Content-Type: application/xml @@ -10,6 +10,7 @@ Content-Type: application/xml ``` HTTP 201 +# TEST: Broadcasting a message with an empty body is rejected POST {{restapi_url}}/messages/users Authorization: {{authkey}} Content-Type: application/xml @@ -23,6 +24,7 @@ HTTP 400 [Asserts] xpath "string(/error/exception)" == "IllegalArgumentException" +# TEST: Broadcasting a message without a body is rejected POST {{restapi_url}}/messages/users Authorization: {{authkey}} Content-Type: application/xml @@ -35,6 +37,7 @@ HTTP 400 xpath "string(/error/exception)" == "IllegalArgumentException" +# TEST: A logged-in client receives a broadcast message # Log in anonymously over BOSH (XEP-0124/XEP-0206), to be able to receive the broadcast. POST {{bosh_url}}/http-bind/ Content-Type: text/xml; charset=utf-8 @@ -99,7 +102,6 @@ xpath "string(//*[local-name()='message']/@from)" == "example.org" xpath "string(//*[local-name()='message']/@type)" == "headline" xpath "string(//*[local-name()='message']/*[local-name()='body'])" == "Hello from the REST API" -# Log out. POST {{bosh_url}}/http-bind/ Content-Type: text/xml; charset=utf-8 ``` diff --git a/test/securitylog.hurl b/test/securitylog.hurl index 70437ecfb..6445babca 100644 --- a/test/securitylog.hurl +++ b/test/securitylog.hurl @@ -1,3 +1,4 @@ +# TEST: An admin console login is recorded as a new event in the security audit log # The log may already contain events (e.g. from earlier runs). Events are returned newest first, so remember the ID of # the newest one (or 0 when the log is empty), to be able to tell which events are new. GET {{restapi_url}}/logs/security?limit=1 @@ -38,6 +39,7 @@ xpath "string(/logs/log[logId > {{last_log_id}}]/node)" == "example.org" xpath "string(/logs/log[logId > {{last_log_id}}]/details)" startsWith "The user logged in successfully to the admin console" xpath "/logs/log[logId > {{last_log_id}}]/timestamp" exists +# TEST: Filtering by username returns only that user's events GET {{restapi_url}}/logs/security?username=admin Authorization: {{authkey}} HTTP 200 @@ -45,12 +47,14 @@ HTTP 200 xpath "/logs/log[logId > {{last_log_id}}]" count == 1 xpath "/logs/log[username!='admin']" not exists +# TEST: Filtering by an unknown username returns no events GET {{restapi_url}}/logs/security?username=nonexistent Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/logs/log" count == 0 +# TEST: The number of events returned can be limited, newest first GET {{restapi_url}}/logs/security?limit=1 Authorization: {{authkey}} HTTP 200 @@ -58,12 +62,14 @@ HTTP 200 xpath "/logs/log" count == 1 xpath "/logs/log[logId > {{last_log_id}}]" count == 1 +# TEST: Skipping more events than exist returns no events GET {{restapi_url}}/logs/security?offset=1000000 Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/logs/log" count == 0 +# TEST: Filtering by a start time in the future returns no events # Timestamps are in seconds since the epoch: nothing was logged after 2100-01-01, or before 1970-01-01T00:00:01. GET {{restapi_url}}/logs/security?startTime=4102444800 Authorization: {{authkey}} @@ -71,12 +77,14 @@ HTTP 200 [Asserts] xpath "/logs/log" count == 0 +# TEST: Filtering by an end time in the distant past returns no events GET {{restapi_url}}/logs/security?endTime=1 Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/logs/log" count == 0 +# TEST: Filtering by a time range includes the new event GET {{restapi_url}}/logs/security?startTime=1&endTime=4102444800 Authorization: {{authkey}} HTTP 200 diff --git a/test/sessions.hurl b/test/sessions.hurl index 5b820d074..ea331e878 100644 --- a/test/sessions.hurl +++ b/test/sessions.hurl @@ -1,3 +1,4 @@ +# TEST: There are no sessions initially GET {{restapi_url}}/sessions Authorization: {{authkey}} HTTP 200 @@ -5,6 +6,7 @@ HTTP 200 xpath "/sessions" exists xpath "/sessions[not(child::node())]" count == 1 # sessions at the root, with no child nodes +# TEST: A user without sessions has an empty list of sessions GET {{restapi_url}}/sessions/john Authorization: {{authkey}} HTTP 200 @@ -12,6 +14,7 @@ HTTP 200 xpath "/sessions" exists xpath "/sessions/session" count == 0 +# TEST: A user that does not exist has an empty list of sessions # Openfire does not check whether the user exists: a user that does not exist simply has no sessions. The API documents # no other response than 200 for this endpoint. GET {{restapi_url}}/sessions/nonexistent @@ -22,6 +25,7 @@ xpath "/sessions" exists xpath "/sessions/session" count == 0 +# TEST: A logged-in client's session is listed with its details, for its user only # Create a session for john, by logging in over BOSH (XEP-0124/XEP-0206). POST {{bosh_url}}/http-bind/ Content-Type: text/xml; charset=utf-8 @@ -94,7 +98,7 @@ HTTP 200 [Asserts] xpath "/sessions/session" count == 0 -# Kick the session. +# TEST: Kicking a user's sessions closes them DELETE {{restapi_url}}/sessions/john Authorization: {{authkey}} HTTP 200 @@ -120,7 +124,7 @@ retry-interval: 100ms ``` HTTP 404 -# Kicking the sessions of a user without sessions is fine. +# TEST: Kicking the sessions of a user that has no sessions is fine DELETE {{restapi_url}}/sessions/john Authorization: {{authkey}} HTTP 200 diff --git a/test/statistics.hurl b/test/statistics.hurl index 5cea3da8d..1862e5506 100644 --- a/test/statistics.hurl +++ b/test/statistics.hurl @@ -1,3 +1,4 @@ +# TEST: Get the session statistics GET {{restapi_url}}/system/statistics/sessions Authorization: {{authkey}} HTTP 200 diff --git a/test/system.hurl b/test/system.hurl index fb47e3a47..6cd6621a0 100644 --- a/test/system.hurl +++ b/test/system.hurl @@ -1,35 +1,44 @@ +# TEST: Liveness check GET {{restapi_url}}/system/liveness Authorization: {{authkey}} HTTP 200 +# TEST: Liveness check for deadlocked threads GET {{restapi_url}}/system/liveness/deadlock Authorization: {{authkey}} HTTP 200 +# TEST: Liveness check for system properties GET {{restapi_url}}/system/liveness/properties Authorization: {{authkey}} HTTP 200 +# TEST: Readiness check GET {{restapi_url}}/system/readiness Authorization: {{authkey}} HTTP 200 +# TEST: Readiness check for the cluster GET {{restapi_url}}/system/readiness/cluster Authorization: {{authkey}} HTTP 200 +# TEST: Readiness check for connections GET {{restapi_url}}/system/readiness/connections Authorization: {{authkey}} HTTP 200 +# TEST: Readiness check for plugins GET {{restapi_url}}/system/readiness/plugins Authorization: {{authkey}} HTTP 200 +# TEST: Readiness check for the server GET {{restapi_url}}/system/readiness/server Authorization: {{authkey}} HTTP 200 +# TEST: List the system properties, which excludes the plugin's own properties GET {{restapi_url}}/system/properties Authorization: {{authkey}} HTTP 200 @@ -41,6 +50,7 @@ xpath "string(/properties/property[@key='abstractGroupProvider.shared.recursive' # The plugin's own properties (e.g. plugin.restapi.enabled) must not be exposed in the listing. xpath "/properties/property[starts-with(@key, 'plugin.restapi.')]" not exists +# TEST: Get a property that has a default value, but was never set # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/242 # abstractGroupProvider.shared.recursive is registered through Openfire's SystemProperty API with a default value. # It used to 404 when queried individually, as that lookup checked JiveGlobals only. @@ -51,6 +61,7 @@ HTTP 200 xpath "/property" count == 1 xpath "string(/property/@value)" == "false" +# TEST: Get a property that has no default value, and was never set # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/242 # adminConsole.servlet-request-authenticator is registered through Openfire's SystemProperty API without a default value. # It used to 404 when queried individually, as that lookup checked JiveGlobals only. @@ -60,6 +71,7 @@ HTTP 200 [Asserts] xpath "/property" count == 1 +# TEST: Create a property POST {{restapi_url}}/system/properties Authorization: {{authkey}} Content-Type: application/xml @@ -77,6 +89,7 @@ xpath "/property" count == 1 xpath "string(/property/@key)" == "test.key" xpath "string(/property/@value)" == "test-value" +# TEST: Update a property PUT {{restapi_url}}/system/properties/test.key Authorization: {{authkey}} Content-Type: application/xml @@ -92,6 +105,7 @@ HTTP 200 [Asserts] xpath "string(/property/@value)" == "new-value" +# TEST: Updating a property that does not exist fails PUT {{restapi_url}}/system/properties/wrong.key Authorization: {{authkey}} Content-Type: application/xml @@ -101,6 +115,7 @@ Content-Type: application/xml ``` HTTP 404 +# TEST: Updating a property with a key that does not match the URL is rejected PUT {{restapi_url}}/system/properties/test.key Authorization: {{authkey}} Content-Type: application/xml @@ -110,6 +125,7 @@ Content-Type: application/xml ``` HTTP 400 +# TEST: Delete a property DELETE {{restapi_url}}/system/properties/test.key Authorization: {{authkey}} HTTP 200 @@ -118,12 +134,14 @@ GET {{restapi_url}}/system/properties/test.key Authorization: {{authkey}} HTTP 404 +# TEST: Getting plugin.restapi.enabled is forbidden # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. GET {{restapi_url}}/system/properties/plugin.restapi.enabled Authorization: {{authkey}} HTTP 403 +# TEST: Updating plugin.restapi.enabled is forbidden # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. PUT {{restapi_url}}/system/properties/plugin.restapi.enabled @@ -135,6 +153,7 @@ Content-Type: application/xml ``` HTTP 403 +# TEST: Deleting plugin.restapi.enabled is forbidden # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. DELETE {{restapi_url}}/system/properties/plugin.restapi.enabled @@ -142,12 +161,14 @@ Authorization: {{authkey}} HTTP 403 +# TEST: Getting plugin.restapi.httpAuth is forbidden # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. GET {{restapi_url}}/system/properties/plugin.restapi.httpAuth Authorization: {{authkey}} HTTP 403 +# TEST: Updating plugin.restapi.httpAuth is forbidden # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. PUT {{restapi_url}}/system/properties/plugin.restapi.httpAuth @@ -159,6 +180,7 @@ Content-Type: application/xml ``` HTTP 403 +# TEST: Deleting plugin.restapi.httpAuth is forbidden # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. DELETE {{restapi_url}}/system/properties/plugin.restapi.httpAuth @@ -166,12 +188,14 @@ Authorization: {{authkey}} HTTP 403 +# TEST: Getting plugin.restapi.customAuthFilter is forbidden # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. GET {{restapi_url}}/system/properties/plugin.restapi.customAuthFilter Authorization: {{authkey}} HTTP 403 +# TEST: Updating plugin.restapi.customAuthFilter is forbidden # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. PUT {{restapi_url}}/system/properties/plugin.restapi.customAuthFilter @@ -183,6 +207,7 @@ Content-Type: application/xml ``` HTTP 403 +# TEST: Deleting plugin.restapi.customAuthFilter is forbidden # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. DELETE {{restapi_url}}/system/properties/plugin.restapi.customAuthFilter @@ -190,12 +215,14 @@ Authorization: {{authkey}} HTTP 403 +# TEST: Getting plugin.restapi.secret is forbidden # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. GET {{restapi_url}}/system/properties/plugin.restapi.secret Authorization: {{authkey}} HTTP 403 +# TEST: Updating plugin.restapi.secret is forbidden # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. PUT {{restapi_url}}/system/properties/plugin.restapi.secret @@ -207,6 +234,7 @@ Content-Type: application/xml ``` HTTP 403 +# TEST: Deleting plugin.restapi.secret is forbidden # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. DELETE {{restapi_url}}/system/properties/plugin.restapi.secret @@ -214,12 +242,14 @@ Authorization: {{authkey}} HTTP 403 +# TEST: Getting plugin.restapi.serviceLoggingEnabled is forbidden # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. GET {{restapi_url}}/system/properties/plugin.restapi.serviceLoggingEnabled Authorization: {{authkey}} HTTP 403 +# TEST: Updating plugin.restapi.serviceLoggingEnabled is forbidden # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. PUT {{restapi_url}}/system/properties/plugin.restapi.serviceLoggingEnabled @@ -231,6 +261,7 @@ Content-Type: application/xml ``` HTTP 403 +# TEST: Deleting plugin.restapi.serviceLoggingEnabled is forbidden # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. DELETE {{restapi_url}}/system/properties/plugin.restapi.serviceLoggingEnabled @@ -238,12 +269,14 @@ Authorization: {{authkey}} HTTP 403 +# TEST: Getting plugin.restapi.allowedIPs is forbidden # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. GET {{restapi_url}}/system/properties/plugin.restapi.allowedIPs Authorization: {{authkey}} HTTP 403 +# TEST: Updating plugin.restapi.allowedIPs is forbidden # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. PUT {{restapi_url}}/system/properties/plugin.restapi.allowedIPs @@ -255,6 +288,7 @@ Content-Type: application/xml ``` HTTP 403 +# TEST: Deleting plugin.restapi.allowedIPs is forbidden # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # The REST API plugin should not allow users to access properties used to control the plugin's functionality. DELETE {{restapi_url}}/system/properties/plugin.restapi.allowedIPs @@ -262,6 +296,7 @@ Authorization: {{authkey}} HTTP 403 +# TEST: Deleting the parent of the plugin's own properties is forbidden # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/244 # Openfire deletes a property together with all of its child properties. Deleting the parent of the plugin's own # properties (which exist in this setup) should therefore not be allowed either. @@ -270,7 +305,7 @@ Authorization: {{authkey}} HTTP 403 -# Deleting a property also deletes its child properties. +# TEST: Deleting a property also deletes its child properties POST {{restapi_url}}/system/properties Authorization: {{authkey}} Content-Type: application/xml @@ -298,6 +333,7 @@ Authorization: {{authkey}} HTTP 404 +# TEST: Deleting a property is refused when the underscore in its key would match other properties # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/259 # Openfire deletes a property using SQL LIKE, in which an underscore matches any character. Deleting a property should # be refused when that would also delete a property other than the property itself and its child properties. @@ -336,6 +372,7 @@ Authorization: {{authkey}} HTTP 200 +# TEST: Get one of Openfire's own properties with apostrophes in its key # https://github.com/igniterealtime/openfire-restAPI-plugin/issues/259 # Openfire itself uses apostrophes in property keys (e.g. for the caches of MUC services), which should be usable in # the URL path. @@ -345,6 +382,7 @@ HTTP 200 [Asserts] xpath "string(/property/@key)" == "cache.MUCService'conference'Rooms.size" +# TEST: A property with apostrophes in its key can be created, retrieved and deleted POST {{restapi_url}}/system/properties Authorization: {{authkey}} Content-Type: application/xml @@ -369,6 +407,7 @@ Authorization: {{authkey}} HTTP 404 +# TEST: Creating or updating a property without a value is rejected, and deletes nothing # Openfire treats setting a property to a null value as deleting it, together with all of its child properties. # Creating or updating a property without a value should therefore be rejected, as it would otherwise bypass the # checks that prevent (forbidden) child properties from being deleted. @@ -424,6 +463,7 @@ DELETE {{restapi_url}}/system/properties/test.nullparent Authorization: {{authkey}} HTTP 200 +# TEST: Creating the parent of the plugin's own properties without a value is rejected # The parent of the plugin's own properties may not be 'created' without a value (as that would delete the plugin's # configuration) either. POST {{restapi_url}}/system/properties @@ -435,6 +475,7 @@ Content-Type: application/xml ``` HTTP 400 +# TEST: A property can have an empty value # An empty value is a value (rather than a deletion), and should be stored as such. POST {{restapi_url}}/system/properties Authorization: {{authkey}} diff --git a/test/usergroups.hurl b/test/usergroups.hurl index c68bc9eec..f8c5b3d9e 100644 --- a/test/usergroups.hurl +++ b/test/usergroups.hurl @@ -56,12 +56,14 @@ Content-Type: application/xml ``` HTTP 201 +# TEST: A new user is in no groups GET {{restapi_url}}/users/geordi/groups Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/groups/groupname" count == 0 +# TEST: Add a user to a single group, and to multiple groups POST {{restapi_url}}/users/geordi/groups/security Authorization: {{authkey}} HTTP 201 @@ -94,6 +96,7 @@ HTTP 200 xpath "/group/members/member" count == 1 xpath "string(/group/members/member)" == "geordi@example.org" +# TEST: Remove a user from a single group DELETE {{restapi_url}}/users/geordi/groups/security Authorization: {{authkey}} HTTP 200 @@ -105,6 +108,7 @@ HTTP 200 xpath "/groups/groupname" count == 2 xpath "/groups[groupname='security']" not exists +# TEST: Remove a user from multiple groups DELETE {{restapi_url}}/users/geordi/groups Authorization: {{authkey}} Content-Type: application/xml @@ -123,6 +127,7 @@ HTTP 200 [Asserts] xpath "/groups/groupname" count == 0 +# TEST: Removing a user from a group that does not exist fails DELETE {{restapi_url}}/users/geordi/groups/nonexistent Authorization: {{authkey}} HTTP 404 @@ -130,6 +135,7 @@ HTTP 404 xpath "string(/error/exception)" == "GroupNotFoundException" xpath "string(/error/resource)" == "nonexistent" +# TEST: Getting the groups of a user that does not exist fails GET {{restapi_url}}/users/nonexistent/groups Authorization: {{authkey}} HTTP 404 diff --git a/test/userlockouts.hurl b/test/userlockouts.hurl index f13cebc24..a27a4a5a3 100644 --- a/test/userlockouts.hurl +++ b/test/userlockouts.hurl @@ -11,6 +11,7 @@ Content-Type: application/xml ``` HTTP 201 +# TEST: Locking out a user closes its sessions # Log in as worf over BOSH (XEP-0124/XEP-0206), so that there is a session for the lockout to close. POST {{bosh_url}}/http-bind/ Content-Type: text/xml; charset=utf-8 @@ -63,7 +64,6 @@ Authorization: {{authkey}} HTTP 201 # Sessions are closed asynchronously, so allow for some delay. -# Locking out a user closes its sessions. GET {{restapi_url}}/sessions/worf Authorization: {{authkey}} [Options] @@ -84,12 +84,12 @@ retry-interval: 100ms ``` HTTP 404 -# Locking out a user that is already locked out is fine. +# TEST: Locking out a user that is already locked out is fine POST {{restapi_url}}/lockouts/worf Authorization: {{authkey}} HTTP 201 -# A user that is locked out cannot log in. +# TEST: A user that is locked out cannot log in POST {{bosh_url}}/http-bind/ Content-Type: text/xml; charset=utf-8 ``` @@ -116,16 +116,17 @@ Content-Type: text/xml; charset=utf-8 ``` HTTP 200 +# TEST: Unlock a user DELETE {{restapi_url}}/lockouts/worf Authorization: {{authkey}} HTTP 200 -# Unlocking a user that is not locked out is fine. +# TEST: Unlocking a user that is not locked out is fine DELETE {{restapi_url}}/lockouts/worf Authorization: {{authkey}} HTTP 200 -# A user that is no longer locked out can log in again. +# TEST: A user that is no longer locked out can log in again POST {{bosh_url}}/http-bind/ Content-Type: text/xml; charset=utf-8 ``` @@ -173,6 +174,7 @@ Content-Type: text/xml; charset=utf-8 ``` HTTP 200 +# TEST: Locking out a user that does not exist fails POST {{restapi_url}}/lockouts/nonexistent Authorization: {{authkey}} HTTP 404 @@ -180,6 +182,7 @@ HTTP 404 xpath "string(/error/exception)" == "UserNotFoundException" xpath "string(/error/resource)" == "nonexistent" +# TEST: Unlocking a user that does not exist fails DELETE {{restapi_url}}/lockouts/nonexistent Authorization: {{authkey}} HTTP 404 diff --git a/test/userroster.hurl b/test/userroster.hurl index 471801bf9..fbfe6d77c 100644 --- a/test/userroster.hurl +++ b/test/userroster.hurl @@ -1,3 +1,4 @@ +# TEST: Get the roster of a user from the demoboot configuration GET {{restapi_url}}/users/john/roster Authorization: {{authkey}} HTTP 200 @@ -22,12 +23,14 @@ Content-Type: application/xml ``` HTTP 201 +# TEST: A new user has an empty roster GET {{restapi_url}}/users/data/roster Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/roster/rosterItem" count == 0 +# TEST: Add a roster entry POST {{restapi_url}}/users/data/roster Authorization: {{authkey}} Content-Type: application/xml @@ -55,7 +58,7 @@ xpath "string(/roster/rosterItem/subscriptionType)" == "3" xpath "/roster/rosterItem/groups/group" count == 1 xpath "string(/roster/rosterItem/groups/group)" == "Friends" -# Adding an entry for a contact that is already on the roster is a conflict. +# TEST: Adding an entry for a contact that is already on the roster is a conflict POST {{restapi_url}}/users/data/roster Authorization: {{authkey}} Content-Type: application/xml @@ -69,7 +72,7 @@ HTTP 409 [Asserts] xpath "string(/error/exception)" == "UserAlreadyExistsException" -# A roster entry needs a JID. +# TEST: Adding a roster entry without a JID is rejected POST {{restapi_url}}/users/data/roster Authorization: {{authkey}} Content-Type: application/xml @@ -96,6 +99,7 @@ xpath "string(/error/exception)" == "IllegalArgumentException" # documented. Getting the roster of a user that does not exist returns 200 (rather than 404) too. Add tests for those # once UserServiceController#getUserRoster checks that the user exists. +# TEST: Update a roster entry PUT {{restapi_url}}/users/data/roster/john@example.org Authorization: {{authkey}} Content-Type: application/xml @@ -123,7 +127,7 @@ xpath "string(/roster/rosterItem/subscriptionType)" == "1" xpath "/roster/rosterItem/groups/group" count == 1 xpath "string(/roster/rosterItem/groups/group)" == "Crew" -# Updating an entry for a contact that is not on the roster fails. +# TEST: Updating an entry for a contact that is not on the roster fails PUT {{restapi_url}}/users/data/roster/jane@example.org Authorization: {{authkey}} Content-Type: application/xml @@ -139,6 +143,7 @@ HTTP 404 xpath "string(/error/exception)" == "UserNotFoundException" xpath "string(/error/resource)" == "jane@example.org" +# TEST: Delete a roster entry DELETE {{restapi_url}}/users/data/roster/john@example.org Authorization: {{authkey}} HTTP 200 @@ -149,6 +154,7 @@ HTTP 200 [Asserts] xpath "/roster/rosterItem" count == 0 +# TEST: Deleting an entry that is not on the roster fails DELETE {{restapi_url}}/users/data/roster/john@example.org Authorization: {{authkey}} HTTP 404 @@ -156,6 +162,7 @@ HTTP 404 xpath "string(/error/exception)" == "RosterItemNotFound" xpath "string(/error/resource)" == "john@example.org" +# TEST: Deleting a roster entry of a user that does not exist fails DELETE {{restapi_url}}/users/nonexistent/roster/john@example.org Authorization: {{authkey}} HTTP 404 diff --git a/test/users.hurl b/test/users.hurl index 307f01bcd..7d2d27f61 100644 --- a/test/users.hurl +++ b/test/users.hurl @@ -1,3 +1,4 @@ +# TEST: List all users GET {{restapi_url}}/users Authorization: {{authkey}} HTTP 200 @@ -9,6 +10,7 @@ xpath "/users/user[username='john']" exists xpath "/users/user[name='John Doe']" exists xpath "/users/user[email='john.doe@example.com']" exists +# TEST: Search for users by (part of) their username GET {{restapi_url}}/users?search=john Authorization: {{authkey}} HTTP 200 @@ -18,12 +20,14 @@ xpath "/users/user[username='john']" exists xpath "/users/user[name='John Doe']" exists xpath "/users/user[email='john.doe@example.com']" exists +# TEST: Search for users by a property that no user has GET {{restapi_url}}/users?propertyKey=tea Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/users/user" count == 0 +# TEST: Create a user with a property POST {{restapi_url}}/users Authorization: {{authkey}} Content-Type: application/xml @@ -41,6 +45,7 @@ Content-Type: application/xml ``` HTTP 201 +# TEST: Search for users by property key and value GET {{restapi_url}}/users?propertyKey=tea&propertyValue=earlgreyhot Authorization: {{authkey}} HTTP 200 @@ -53,6 +58,7 @@ xpath "/users/user/properties/property" count == 1 xpath "string(/users/user/properties/property/@key)" == "tea" xpath "string(/users/user/properties/property/@value)" == "earlgreyhot" +# TEST: Search for users by property key only GET {{restapi_url}}/users?propertyKey=tea Authorization: {{authkey}} HTTP 200 @@ -60,12 +66,14 @@ HTTP 200 xpath "/users/user" count == 1 xpath "string(/users/user/username)" == "jeanluc" +# TEST: Search for users by property key and a non-matching value GET {{restapi_url}}/users?propertyKey=tea&propertyValue=coffee Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/users/user" count == 0 +# TEST: Update a user PUT {{restapi_url}}/users/jeanluc Authorization: {{authkey}} Content-Type: application/xml @@ -93,6 +101,7 @@ xpath "/user[name='Jean-Luc']" exists xpath "/user[email='jlp@example.com']" not exists xpath "/user[email='jeanluc@example.com']" exists +# TEST: Delete a user DELETE {{restapi_url}}/users/jeanluc Authorization: {{authkey}} HTTP 200 From ccbf3b42e6433767e0ec0f4854ff75bef3120982 Mon Sep 17 00:00:00 2001 From: Dan Caseley Date: Fri, 25 Sep 2026 19:23:02 +0100 Subject: [PATCH 14/21] CI: run Hurl from its container image, replacing the archived action Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3605b4346..2be50493b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,11 +38,11 @@ jobs: config: ./test/demoboot-with-additions.xml plugin: ./restAPI.jar - - uses: gacts/install-hurl@v1 - - name: Test the plugin run: | - hurl --test --report-junit test-results.xml --variables-file test/test.env --jobs 1 test/*.hurl + docker run --rm --network host --user "$(id -u):$(id -g)" --volume "$PWD:$PWD" --workdir "$PWD" \ + ghcr.io/orange-opensource/hurl:8.0.1 \ + --test --report-junit test-results.xml --variables-file test/test.env --jobs 1 test/*.hurl - name: Expose Openfire logs uses: actions/upload-artifact@v7 From bc742dad7ee8cd0661e5de01b1420da9ac8fcd5f Mon Sep 17 00:00:00 2001 From: Dan Caseley Date: Fri, 25 Sep 2026 20:24:43 +0100 Subject: [PATCH 15/21] Hurl: additional Chat Rooms tests Co-Authored-By: Claude Opus 5.5 --- test/chatrooms.hurl | 419 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 419 insertions(+) diff --git a/test/chatrooms.hurl b/test/chatrooms.hurl index 4fa565037..3166a5e87 100644 --- a/test/chatrooms.hurl +++ b/test/chatrooms.hurl @@ -2,3 +2,422 @@ GET {{restapi_url}}/chatrooms Authorization: {{authkey}} HTTP 200 + +# TEST: A created chat room can be retrieved +POST {{restapi_url}}/chatrooms +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + lobby + The Lobby + Where everyone meets + Welcome + 30 + true + true + + john@example.org + + + jane@example.org + + +``` +HTTP 201 + +GET {{restapi_url}}/chatrooms/lobby +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "string(/chatRoom/roomName)" == "lobby" +xpath "string(/chatRoom/naturalName)" == "The Lobby" +xpath "string(/chatRoom/description)" == "Where everyone meets" +xpath "string(/chatRoom/subject)" == "Welcome" +xpath "string(/chatRoom/maxUsers)" == "30" +xpath "string(/chatRoom/persistent)" == "true" +xpath "string(/chatRoom/publicRoom)" == "true" +xpath "/chatRoom/owners/owner" count == 1 +xpath "string(/chatRoom/owners/owner)" == "john@example.org" +xpath "/chatRoom/members/member" count == 1 +xpath "string(/chatRoom/members/member)" == "jane@example.org" +xpath "/chatRoom/creationDate" exists + +# TODO: Creating a chat room that already exists returns 201 (the bulk endpoint reports 'Success'), and replaces the +# existing room's configuration and affiliations, where 409 is documented. Add a test once that is fixed. + +# TEST: Only public chat rooms are listed, unless all rooms are asked for +POST {{restapi_url}}/chatrooms +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + backroom + The Back Room + Not listed + false + +``` +HTTP 201 + +GET {{restapi_url}}/chatrooms +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/chatRooms/chatRoom[roomName='lobby']" exists +xpath "/chatRooms/chatRoom[roomName='backroom']" not exists + +GET {{restapi_url}}/chatrooms?type=public +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/chatRooms/chatRoom[roomName='lobby']" exists +xpath "/chatRooms/chatRoom[roomName='backroom']" not exists + +GET {{restapi_url}}/chatrooms?type=all +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/chatRooms/chatRoom[roomName='lobby']" exists +xpath "/chatRooms/chatRoom[roomName='backroom']" exists + +# TEST: Search for chat rooms by (part of) their name or natural name +GET {{restapi_url}}/chatrooms?search=lob +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/chatRooms/chatRoom" count == 1 +xpath "string(/chatRooms/chatRoom/roomName)" == "lobby" + +GET {{restapi_url}}/chatrooms?type=all&search=Back +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/chatRooms/chatRoom" count == 1 +xpath "string(/chatRooms/chatRoom/roomName)" == "backroom" + +GET {{restapi_url}}/chatrooms?type=all&search=nothing-matches-this +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/chatRooms/chatRoom" count == 0 + +# TODO: Searching for a term that is not a valid JID node (e.g. 'Back Room', with a space) fails with a 500, as the +# term is nodeprep'ed to match room names, even though natural names can contain such characters. Add a test once +# that is fixed. + +# TEST: Listing the chat rooms of a service that does not exist fails +GET {{restapi_url}}/chatrooms?servicename=nonexistent +Authorization: {{authkey}} +HTTP 404 +[Asserts] +xpath "string(/error/exception)" == "MUCServiceNotFoundException" +xpath "string(/error/resource)" == "nonexistent" + +# TEST: Getting a chat room that does not exist fails +GET {{restapi_url}}/chatrooms/nonexistent +Authorization: {{authkey}} +HTTP 404 +[Asserts] +xpath "string(/error/exception)" == "RoomNotFoundException" +xpath "string(/error/resource)" == "nonexistent" + +# TEST: Getting a chat room of a service that does not exist fails +GET {{restapi_url}}/chatrooms/lobby?servicename=nonexistent +Authorization: {{authkey}} +HTTP 404 +[Asserts] +xpath "string(/error/exception)" == "MUCServiceNotFoundException" + +# TEST: Update a chat room +PUT {{restapi_url}}/chatrooms/lobby +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + lobby + The Grand Lobby + Where everyone meets, now bigger + Welcome + 50 + true + true + + john@example.org + + + jane@example.org + + +``` +HTTP 200 + +GET {{restapi_url}}/chatrooms/lobby +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "string(/chatRoom/naturalName)" == "The Grand Lobby" +xpath "string(/chatRoom/description)" == "Where everyone meets, now bigger" +xpath "string(/chatRoom/maxUsers)" == "50" + +# TODO: Updating a chat room that does not exist returns 200 and creates the room. Add a test once it is decided +# whether that should be a 404 instead. + +# TEST: A new chat room has no participants, occupants or history +GET {{restapi_url}}/chatrooms/lobby/participants +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/participants/participant" count == 0 + +GET {{restapi_url}}/chatrooms/lobby/occupants +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/occupants/occupant" count == 0 + +GET {{restapi_url}}/chatrooms/lobby/chathistory +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/messages/message" count == 0 + +# TEST: Getting the participants, occupants or history of a chat room that does not exist fails +GET {{restapi_url}}/chatrooms/nonexistent/participants +Authorization: {{authkey}} +HTTP 404 +[Asserts] +xpath "string(/error/exception)" == "RoomNotFoundException" + +GET {{restapi_url}}/chatrooms/nonexistent/occupants +Authorization: {{authkey}} +HTTP 404 +[Asserts] +xpath "string(/error/exception)" == "RoomNotFoundException" + +GET {{restapi_url}}/chatrooms/nonexistent/chathistory +Authorization: {{authkey}} +HTTP 404 +[Asserts] +xpath "string(/error/exception)" == "RoomNotFoundException" + + +# TEST: A client that joins a chat room is listed as a participant and occupant, and what it says is in the history +# Log in anonymously over BOSH (XEP-0124/XEP-0206). The server may send stanzas (like pings) at any time, so responses +# are only checked for the presence of the stanzas that are expected. +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +[Options] +variable: nick=visitor-{{newUuid}} +``` + +``` +HTTP 200 +[Captures] +sid: xpath "string(/_:body/@sid)" + +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` + +``` +HTTP 200 +[Asserts] +xpath "//*[local-name()='success']" exists + +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` + +``` +HTTP 200 + +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` + +``` +HTTP 200 +[Captures] +bare_jid: xpath "substring-before(//*[local-name()='jid'], '/')" + +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` + +``` +HTTP 200 + +# Join the room. +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` + +``` +HTTP 200 +[Asserts] +xpath "//*[local-name()='presence'][@from='lobby@conference.example.org/{{nick}}']" exists + +GET {{restapi_url}}/chatrooms/lobby/participants +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/participants/participant" count == 1 +xpath "string(/participants/participant/jid)" == "lobby@conference.example.org/{{nick}}" +xpath "string(/participants/participant/role)" == "participant" +xpath "string(/participants/participant/affiliation)" == "none" + +GET {{restapi_url}}/chatrooms/lobby/occupants +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/occupants/occupant" count == 1 +xpath "string(/occupants/occupant/jid)" == "lobby@conference.example.org/{{nick}}" +xpath "string(/occupants/occupant/userAddress)" startsWith "{{bare_jid}}/" + +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` +Hello, room +``` +HTTP 200 + +GET {{restapi_url}}/chatrooms/lobby/chathistory +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/messages/message" count == 1 +xpath "string(/messages/message/from)" == "lobby@conference.example.org/{{nick}}" +xpath "string(/messages/message/type)" == "groupchat" +xpath "string(/messages/message/body)" == "Hello, room" + + +# TEST: Invite a user to a chat room, with a reason +POST {{restapi_url}}/chatrooms/lobby/invite/{{bare_jid}} +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + Please join us + +``` +HTTP 200 + +# The poll returns everything that is pending for the client, which includes the invitation. +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` + +``` +HTTP 200 +[Asserts] +xpath "//*[local-name()='invite']" count == 1 +xpath "string(//*[local-name()='message'][*[local-name()='x']/*[local-name()='invite']]/@from)" == "lobby@conference.example.org" +xpath "string(//*[local-name()='invite']/*[local-name()='reason'])" == "Please join us" + +# TODO: Inviting a user without a request body (the reason) fails with a 500 (a NullPointerException), rather than +# sending an invitation without a reason, or returning a 400. Add a test once that is fixed. + +# TEST: Invite a collection of users to a chat room +POST {{restapi_url}}/chatrooms/lobby/invite +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + Please join us, again + + {{bare_jid}} + + +``` +HTTP 200 + +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` + +``` +HTTP 200 +[Asserts] +xpath "//*[local-name()='invite']" count == 1 +xpath "string(//*[local-name()='invite']/*[local-name()='reason'])" == "Please join us, again" + +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` + +``` +HTTP 200 + + +# TEST: Create multiple chat rooms at once +POST {{restapi_url}}/chatrooms/bulk +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + + bulk1 + Bulk room 1 + Created in bulk + + + bulk2 + Bulk room 2 + Created in bulk + + +``` +HTTP 200 +[Asserts] +xpath "/results/success/result" count == 2 +xpath "/results/success/result[roomName='bulk1'][resultType='Success']" exists +xpath "/results/success/result[roomName='bulk2'][resultType='Success']" exists +xpath "/results/failure/result" count == 0 + +GET {{restapi_url}}/chatrooms/bulk1 +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "string(/chatRoom/naturalName)" == "Bulk room 1" + +GET {{restapi_url}}/chatrooms/bulk2 +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "string(/chatRoom/naturalName)" == "Bulk room 2" + +# TEST: Delete a chat room +DELETE {{restapi_url}}/chatrooms/lobby +Authorization: {{authkey}} +HTTP 200 + +GET {{restapi_url}}/chatrooms/lobby +Authorization: {{authkey}} +HTTP 404 + +# TEST: Deleting a chat room that does not exist fails +DELETE {{restapi_url}}/chatrooms/nonexistent +Authorization: {{authkey}} +HTTP 404 +[Asserts] +xpath "string(/error/exception)" == "RoomNotFoundException" + + +DELETE {{restapi_url}}/chatrooms/backroom +Authorization: {{authkey}} +HTTP 200 + +DELETE {{restapi_url}}/chatrooms/bulk1 +Authorization: {{authkey}} +HTTP 200 + +DELETE {{restapi_url}}/chatrooms/bulk2 +Authorization: {{authkey}} +HTTP 200 From 4ca901f1899aecdbe645c65078f5606bc1bec05a Mon Sep 17 00:00:00 2001 From: Dan Caseley Date: Fri, 25 Sep 2026 20:25:35 +0100 Subject: [PATCH 16/21] Hurl: additional Chat Room Affiliations tests Co-Authored-By: Claude Opus 5.5 --- test/chatroomaffiliations.hurl | 200 +++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 test/chatroomaffiliations.hurl diff --git a/test/chatroomaffiliations.hurl b/test/chatroomaffiliations.hurl new file mode 100644 index 000000000..8be1b2c29 --- /dev/null +++ b/test/chatroomaffiliations.hurl @@ -0,0 +1,200 @@ +# Affiliations are tested on a dedicated chat room, and a dedicated group, which are removed at the end. +POST {{restapi_url}}/chatrooms +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + ops + Operations + Affiliations test room + + john@example.org + + +``` +HTTP 201 + +POST {{restapi_url}}/groups +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + crew + Crew + false + + + john + jane + + +``` +HTTP 201 + + +# TEST: The owners that a chat room was created with are listed +GET {{restapi_url}}/chatrooms/ops/owners +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/owners/owner" count == 1 +xpath "string(/owners/owner)" == "john@example.org" + +# TEST: A new chat room has no admins, members or outcasts +GET {{restapi_url}}/chatrooms/ops/admins +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/admins/admin" count == 0 + +GET {{restapi_url}}/chatrooms/ops/members +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/members/member" count == 0 + +GET {{restapi_url}}/chatrooms/ops/outcasts +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/outcasts/outcast" count == 0 + +# TEST: Add an affiliation by JID +POST {{restapi_url}}/chatrooms/ops/members/jane@example.org +Authorization: {{authkey}} +HTTP 201 + +GET {{restapi_url}}/chatrooms/ops/members +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/members/member" count == 1 +xpath "string(/members/member)" == "jane@example.org" + +# TEST: A user has only one affiliation, so adding another replaces the previous one +POST {{restapi_url}}/chatrooms/ops/admins/jane@example.org +Authorization: {{authkey}} +HTTP 201 + +GET {{restapi_url}}/chatrooms/ops/admins +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/admins/admin" count == 1 +xpath "string(/admins/admin)" == "jane@example.org" + +GET {{restapi_url}}/chatrooms/ops/members +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/members/member" count == 0 + +# TEST: Add an affiliation by username, which is resolved against the local domain +POST {{restapi_url}}/chatrooms/ops/outcasts/mallory +Authorization: {{authkey}} +HTTP 201 + +GET {{restapi_url}}/chatrooms/ops/outcasts +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/outcasts/outcast" count == 1 +xpath "string(/outcasts/outcast)" == "mallory@example.org" + +# TEST: Remove an affiliation +DELETE {{restapi_url}}/chatrooms/ops/admins/jane@example.org +Authorization: {{authkey}} +HTTP 200 + +GET {{restapi_url}}/chatrooms/ops/admins +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/admins/admin" count == 0 + +DELETE {{restapi_url}}/chatrooms/ops/outcasts/mallory@example.org +Authorization: {{authkey}} +HTTP 200 + +GET {{restapi_url}}/chatrooms/ops/outcasts +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/outcasts/outcast" count == 0 + +# TEST: Removing the last owner of a chat room is a conflict +DELETE {{restapi_url}}/chatrooms/ops/owners/john@example.org +Authorization: {{authkey}} +HTTP 409 +[Asserts] +xpath "string(/error/exception)" == "NotAllowedException" + +GET {{restapi_url}}/chatrooms/ops/owners +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "string(/owners/owner)" == "john@example.org" + +# TEST: Affiliate the members of a group, which the room lists as a group (or its members, when expanded) +POST {{restapi_url}}/chatrooms/ops/members/group/crew +Authorization: {{authkey}} +HTTP 201 + +GET {{restapi_url}}/chatrooms/ops +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/chatRoom/memberGroups/memberGroup" count == 1 +xpath "string(/chatRoom/memberGroups/memberGroup)" == "crew" +xpath "/chatRoom/members/member" count == 0 + +GET {{restapi_url}}/chatrooms/ops?expandGroups=true +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/chatRoom/members[member='john@example.org']" exists +xpath "/chatRoom/members[member='jane@example.org']" exists + +# TEST: Remove the affiliation of the members of a group +DELETE {{restapi_url}}/chatrooms/ops/members/group/crew +Authorization: {{authkey}} +HTTP 200 + +GET {{restapi_url}}/chatrooms/ops +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/chatRoom/memberGroups/memberGroup" count == 0 + +# TEST: Getting the affiliations of a chat room that does not exist fails +GET {{restapi_url}}/chatrooms/nonexistent/members +Authorization: {{authkey}} +HTTP 404 +[Asserts] +xpath "string(/error/exception)" == "RoomNotFoundException" +xpath "string(/error/resource)" == "nonexistent" + +# TEST: An affiliation type that does not exist is not found +# The API documents a 400 for this, but the URL does not match any endpoint, as the affiliation type is part of the +# path. +GET {{restapi_url}}/chatrooms/ops/visitors +Authorization: {{authkey}} +HTTP 404 + +# TODO: Replacing (PUT) or adding (POST) a collection of affiliations cannot be done, as the request body is +# deserialized to the abstract AffiliatedEntities class: XML requests are rejected with a 415, and JSON requests with a +# 400. Add tests once that is fixed. + +# TODO: Affiliating the members of a group that does not exist returns 201, and affiliates a user with the group's name +# (e.g. nonexistent@example.org) instead. Add a test expecting a 404 once that is fixed. + + +DELETE {{restapi_url}}/chatrooms/ops +Authorization: {{authkey}} +HTTP 200 + +DELETE {{restapi_url}}/groups/crew +Authorization: {{authkey}} +HTTP 200 From 16f995b93cea8589405bed17a2bc82c56ee7e299 Mon Sep 17 00:00:00 2001 From: Dan Caseley Date: Fri, 25 Sep 2026 20:25:44 +0100 Subject: [PATCH 17/21] Hurl: additional User vCard tests Co-Authored-By: Claude Opus 5.5 --- test/uservcard.hurl | 79 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 test/uservcard.hurl diff --git a/test/uservcard.hurl b/test/uservcard.hurl new file mode 100644 index 000000000..2b62234be --- /dev/null +++ b/test/uservcard.hurl @@ -0,0 +1,79 @@ +# vCards are tested on a dedicated user, which is removed at the end. +POST {{restapi_url}}/users +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + troi + imzadi + +``` +HTTP 201 + + +# TEST: A user without a vCard has no content +GET {{restapi_url}}/users/troi/vcard +Authorization: {{authkey}} +HTTP 204 + +# TEST: A stored vCard can be retrieved +PUT {{restapi_url}}/users/troi/vcard +Authorization: {{authkey}} +Content-Type: application/xml +``` +Deanna TroiCounselortroi@example.com +``` +HTTP 200 + +GET {{restapi_url}}/users/troi/vcard +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "string(/_:vCard/_:FN)" == "Deanna Troi" +xpath "string(/_:vCard/_:NICKNAME)" == "Counselor" +xpath "string(/_:vCard/_:EMAIL/_:USERID)" == "troi@example.com" + +# TEST: Storing a vCard replaces the previous one +PUT {{restapi_url}}/users/troi/vcard +Authorization: {{authkey}} +Content-Type: application/xml +``` +Deanna TroiShip's Counselor +``` +HTTP 200 + +GET {{restapi_url}}/users/troi/vcard +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "string(/_:vCard/_:TITLE)" == "Ship's Counselor" +xpath "/_:vCard/_:NICKNAME" not exists + +# TEST: Delete a vCard +DELETE {{restapi_url}}/users/troi/vcard +Authorization: {{authkey}} +HTTP 200 + +GET {{restapi_url}}/users/troi/vcard +Authorization: {{authkey}} +HTTP 204 + +# TEST: Deleting a vCard that does not exist is fine +DELETE {{restapi_url}}/users/troi/vcard +Authorization: {{authkey}} +HTTP 200 + +# TEST: A user that does not exist has no vCard +GET {{restapi_url}}/users/nonexistent/vcard +Authorization: {{authkey}} +HTTP 204 + +# TODO: Storing a vCard for a user that does not exist returns 200 (and stores the vCard), and any XML document (e.g. +# ) is accepted and stored as a vCard. Add tests expecting a 404 and a 400 respectively, once it is decided +# that the API should validate these. + + +DELETE {{restapi_url}}/users/troi +Authorization: {{authkey}} +HTTP 200 From b6dbbf5ee1d6dc4c4eace7351bb9e2287136ba0e Mon Sep 17 00:00:00 2001 From: Dan Caseley Date: Fri, 25 Sep 2026 20:26:05 +0100 Subject: [PATCH 18/21] Hurl: additional Chat Services tests Co-Authored-By: Claude Opus 5.5 --- test/chatservice.hurl | 77 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 4 deletions(-) diff --git a/test/chatservice.hurl b/test/chatservice.hurl index 61004072f..29d8e7d83 100644 --- a/test/chatservice.hurl +++ b/test/chatservice.hurl @@ -3,7 +3,76 @@ GET {{restapi_url}}/chatservices Authorization: {{authkey}} HTTP 200 [Asserts] -xpath "/chatServices/chatService" count == 1 -xpath "string(/chatServices/chatService/serviceName)" == "conference" -xpath "string(/chatServices/chatService/description)" == "Public Chatrooms" -xpath "string(/chatServices/chatService/hidden)" == "false" +xpath "/chatServices/chatService[serviceName='conference']" count == 1 +xpath "string(/chatServices/chatService[serviceName='conference']/description)" == "Public Chatrooms" +xpath "string(/chatServices/chatService[serviceName='conference']/hidden)" == "false" + +# The API cannot delete a chat service, so each run creates a new one, with a unique name. +# TODO: Once the API can delete chat services, use a fixed name, and delete the service at the end. + +# TEST: A created chat service is listed, and can have chat rooms +POST {{restapi_url}}/chatservices +Authorization: {{authkey}} +Content-Type: application/xml +[Options] +variable: service=test-{{newUuid}} +``` + + + {{service}} + Test service + true + +``` +HTTP 201 + +GET {{restapi_url}}/chatservices +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/chatServices/chatService[serviceName='{{service}}']" count == 1 +xpath "string(/chatServices/chatService[serviceName='{{service}}']/description)" == "Test service" +xpath "string(/chatServices/chatService[serviceName='{{service}}']/hidden)" == "true" + +POST {{restapi_url}}/chatrooms?servicename={{service}} +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + room + A room + A room in the test service + +``` +HTTP 201 + +GET {{restapi_url}}/chatrooms?servicename={{service}}&type=all +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/chatRooms/chatRoom" count == 1 +xpath "string(/chatRooms/chatRoom/roomName)" == "room" + +DELETE {{restapi_url}}/chatrooms/room?servicename={{service}} +Authorization: {{authkey}} +HTTP 200 + +# TEST: Creating a chat service that already exists is a conflict +POST {{restapi_url}}/chatservices +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + {{service}} + Another test service + false + +``` +HTTP 409 +[Asserts] +xpath "string(/error/exception)" == "AlreadyExistsException" +xpath "string(/error/resource)" == "{{service}}" + +# TODO: Creating a chat service without a name fails with a 500, rather than a 400. Add a test once that is fixed. From c5e372b2a811ea2fecbd1a913ae8e209af22c7c1 Mon Sep 17 00:00:00 2001 From: Dan Caseley Date: Fri, 25 Sep 2026 22:41:19 +0100 Subject: [PATCH 19/21] Hurl: disabled tests for known issues Replaces the TODOs for known issues with tests that expect the fixed behaviour, skipped until the respective issue is fixed: #140, #163, #274, #275, #276, #277, #278, #279, #280, #281, #282, #283, #284, #285, #286, #287, #288. Also refers to #267 (a documented 400 for an invalid affiliation type that cannot occur) and #115 (the REST API not recording security audit events of its own), and drops the TODO about deleting chat services. Co-Authored-By: Claude Opus 5.5 --- test/chatroomaffiliations.hurl | 109 ++++++++++++++++++++++++++++-- test/chatrooms.hurl | 102 +++++++++++++++++++++++++--- test/chatservice.hurl | 32 ++++++++- test/groups.hurl | 34 ++++++++++ test/securitylog.hurl | 3 +- test/usergroups.hurl | 82 +++++++++++++++++++++-- test/userroster.hurl | 117 +++++++++++++++++++++++++++++---- test/uservcard.hurl | 48 +++++++++++++- 8 files changed, 487 insertions(+), 40 deletions(-) diff --git a/test/chatroomaffiliations.hurl b/test/chatroomaffiliations.hurl index 8be1b2c29..42495bd07 100644 --- a/test/chatroomaffiliations.hurl +++ b/test/chatroomaffiliations.hurl @@ -178,17 +178,114 @@ xpath "string(/error/resource)" == "nonexistent" # TEST: An affiliation type that does not exist is not found # The API documents a 400 for this, but the URL does not match any endpoint, as the affiliation type is part of the -# path. +# path. See https://github.com/igniterealtime/openfire-restAPI-plugin/issues/267 GET {{restapi_url}}/chatrooms/ops/visitors Authorization: {{authkey}} HTTP 404 -# TODO: Replacing (PUT) or adding (POST) a collection of affiliations cannot be done, as the request body is -# deserialized to the abstract AffiliatedEntities class: XML requests are rejected with a 415, and JSON requests with a -# 400. Add tests once that is fixed. +# TEST: Replace the affiliations of a type with a collection +# Disabled until https://github.com/igniterealtime/openfire-restAPI-plugin/issues/163 is fixed. +PUT {{restapi_url}}/chatrooms/ops/members +Authorization: {{authkey}} +Content-Type: application/xml +[Options] +skip: true +``` + + + jane@example.org + mallory@example.org + +``` +HTTP 201 + +GET {{restapi_url}}/chatrooms/ops/members +Authorization: {{authkey}} +[Options] +skip: true +HTTP 200 +[Asserts] +xpath "/members/member" count == 2 + +PUT {{restapi_url}}/chatrooms/ops/members +Authorization: {{authkey}} +Content-Type: application/xml +[Options] +skip: true +``` + + + jane@example.org + +``` +HTTP 201 + +GET {{restapi_url}}/chatrooms/ops/members +Authorization: {{authkey}} +[Options] +skip: true +HTTP 200 +[Asserts] +xpath "/members/member" count == 1 +xpath "string(/members/member)" == "jane@example.org" + +DELETE {{restapi_url}}/chatrooms/ops/members/jane@example.org +Authorization: {{authkey}} +[Options] +skip: true +HTTP 200 + +# TEST: Add a collection of affiliations +# Disabled until https://github.com/igniterealtime/openfire-restAPI-plugin/issues/163 is fixed. +POST {{restapi_url}}/chatrooms/ops/outcasts +Authorization: {{authkey}} +Content-Type: application/xml +[Options] +skip: true +``` + + + eve@example.org + mallory@example.org + +``` +HTTP 201 -# TODO: Affiliating the members of a group that does not exist returns 201, and affiliates a user with the group's name -# (e.g. nonexistent@example.org) instead. Add a test expecting a 404 once that is fixed. +GET {{restapi_url}}/chatrooms/ops/outcasts +Authorization: {{authkey}} +[Options] +skip: true +HTTP 200 +[Asserts] +xpath "/outcasts/outcast" count == 2 + +DELETE {{restapi_url}}/chatrooms/ops/outcasts/eve@example.org +Authorization: {{authkey}} +[Options] +skip: true +HTTP 200 + +DELETE {{restapi_url}}/chatrooms/ops/outcasts/mallory@example.org +Authorization: {{authkey}} +[Options] +skip: true +HTTP 200 + +# TEST: Affiliating the members of a group that does not exist fails +# Disabled until https://github.com/igniterealtime/openfire-restAPI-plugin/issues/286 is fixed. +POST {{restapi_url}}/chatrooms/ops/members/group/nonexistent +Authorization: {{authkey}} +[Options] +skip: true +HTTP 404 + +GET {{restapi_url}}/chatrooms/ops/members +Authorization: {{authkey}} +[Options] +skip: true +HTTP 200 +[Asserts] +xpath "/members[member='nonexistent@example.org']" not exists DELETE {{restapi_url}}/chatrooms/ops diff --git a/test/chatrooms.hurl b/test/chatrooms.hurl index 3166a5e87..aad477332 100644 --- a/test/chatrooms.hurl +++ b/test/chatrooms.hurl @@ -44,8 +44,51 @@ xpath "/chatRoom/members/member" count == 1 xpath "string(/chatRoom/members/member)" == "jane@example.org" xpath "/chatRoom/creationDate" exists -# TODO: Creating a chat room that already exists returns 201 (the bulk endpoint reports 'Success'), and replaces the -# existing room's configuration and affiliations, where 409 is documented. Add a test once that is fixed. +# TEST: Creating a chat room that already exists is a conflict, and leaves the room unchanged +# Disabled until https://github.com/igniterealtime/openfire-restAPI-plugin/issues/140 is fixed. +POST {{restapi_url}}/chatrooms +Authorization: {{authkey}} +Content-Type: application/xml +[Options] +skip: true +``` + + + lobby + Replaced + Replaced + +``` +HTTP 409 + +POST {{restapi_url}}/chatrooms/bulk +Authorization: {{authkey}} +Content-Type: application/xml +[Options] +skip: true +``` + + + + lobby + Replaced + Replaced + + +``` +HTTP 200 +[Asserts] +xpath "/results/failure/result[roomName='lobby']" exists + +GET {{restapi_url}}/chatrooms/lobby +Authorization: {{authkey}} +[Options] +skip: true +HTTP 200 +[Asserts] +xpath "string(/chatRoom/naturalName)" == "The Lobby" +xpath "string(/chatRoom/owners/owner)" == "john@example.org" +xpath "string(/chatRoom/members/member)" == "jane@example.org" # TEST: Only public chat rooms are listed, unless all rooms are asked for POST {{restapi_url}}/chatrooms @@ -104,9 +147,16 @@ HTTP 200 [Asserts] xpath "/chatRooms/chatRoom" count == 0 -# TODO: Searching for a term that is not a valid JID node (e.g. 'Back Room', with a space) fails with a 500, as the -# term is nodeprep'ed to match room names, even though natural names can contain such characters. Add a test once -# that is fixed. +# TEST: Search for chat rooms by a natural name that is not a valid JID node +# Disabled until https://github.com/igniterealtime/openfire-restAPI-plugin/issues/284 is fixed. +GET {{restapi_url}}/chatrooms?type=all&search=Back%20Room +Authorization: {{authkey}} +[Options] +skip: true +HTTP 200 +[Asserts] +xpath "/chatRooms/chatRoom" count == 1 +xpath "string(/chatRooms/chatRoom/roomName)" == "backroom" # TEST: Listing the chat rooms of a service that does not exist fails GET {{restapi_url}}/chatrooms?servicename=nonexistent @@ -163,8 +213,29 @@ xpath "string(/chatRoom/naturalName)" == "The Grand Lobby" xpath "string(/chatRoom/description)" == "Where everyone meets, now bigger" xpath "string(/chatRoom/maxUsers)" == "50" -# TODO: Updating a chat room that does not exist returns 200 and creates the room. Add a test once it is decided -# whether that should be a 404 instead. +# TEST: Updating a chat room that does not exist fails +# Disabled until https://github.com/igniterealtime/openfire-restAPI-plugin/issues/283 is fixed. +# (The issue also allows for documenting that this creates the room, in which case this test needs to change.) +PUT {{restapi_url}}/chatrooms/nonexistent +Authorization: {{authkey}} +Content-Type: application/xml +[Options] +skip: true +``` + + + nonexistent + Created by an update + A room that did not exist + +``` +HTTP 404 + +GET {{restapi_url}}/chatrooms/nonexistent +Authorization: {{authkey}} +[Options] +skip: true +HTTP 404 # TEST: A new chat room has no participants, occupants or history GET {{restapi_url}}/chatrooms/lobby/participants @@ -319,9 +390,6 @@ xpath "//*[local-name()='invite']" count == 1 xpath "string(//*[local-name()='message'][*[local-name()='x']/*[local-name()='invite']]/@from)" == "lobby@conference.example.org" xpath "string(//*[local-name()='invite']/*[local-name()='reason'])" == "Please join us" -# TODO: Inviting a user without a request body (the reason) fails with a 500 (a NullPointerException), rather than -# sending an invitation without a reason, or returning a 400. Add a test once that is fixed. - # TEST: Invite a collection of users to a chat room POST {{restapi_url}}/chatrooms/lobby/invite Authorization: {{authkey}} @@ -355,6 +423,20 @@ Content-Type: text/xml; charset=utf-8 HTTP 200 +# TEST: Inviting to a chat room without a request body is rejected +# Disabled until https://github.com/igniterealtime/openfire-restAPI-plugin/issues/285 is fixed. +POST {{restapi_url}}/chatrooms/lobby/invite/jane@example.org +Authorization: {{authkey}} +[Options] +skip: true +HTTP 400 + +POST {{restapi_url}}/chatrooms/lobby/invite +Authorization: {{authkey}} +[Options] +skip: true +HTTP 400 + # TEST: Create multiple chat rooms at once POST {{restapi_url}}/chatrooms/bulk Authorization: {{authkey}} diff --git a/test/chatservice.hurl b/test/chatservice.hurl index 29d8e7d83..96889cb0b 100644 --- a/test/chatservice.hurl +++ b/test/chatservice.hurl @@ -8,7 +8,6 @@ xpath "string(/chatServices/chatService[serviceName='conference']/description)" xpath "string(/chatServices/chatService[serviceName='conference']/hidden)" == "false" # The API cannot delete a chat service, so each run creates a new one, with a unique name. -# TODO: Once the API can delete chat services, use a fixed name, and delete the service at the end. # TEST: A created chat service is listed, and can have chat rooms POST {{restapi_url}}/chatservices @@ -75,4 +74,33 @@ HTTP 409 xpath "string(/error/exception)" == "AlreadyExistsException" xpath "string(/error/resource)" == "{{service}}" -# TODO: Creating a chat service without a name fails with a 500, rather than a 400. Add a test once that is fixed. +# TEST: Creating a chat service without a name is rejected +# Disabled until https://github.com/igniterealtime/openfire-restAPI-plugin/issues/287 is fixed. +POST {{restapi_url}}/chatservices +Authorization: {{authkey}} +Content-Type: application/xml +[Options] +skip: true +``` + + + No name + false + +``` +HTTP 400 + +POST {{restapi_url}}/chatservices +Authorization: {{authkey}} +Content-Type: application/xml +[Options] +skip: true +``` + + + + Empty name + false + +``` +HTTP 400 diff --git a/test/groups.hurl b/test/groups.hurl index 47d3504b3..365457202 100644 --- a/test/groups.hurl +++ b/test/groups.hurl @@ -71,3 +71,37 @@ DELETE {{restapi_url}}/groups/group1 Authorization: {{authkey}} HTTP 200 +# TEST: A group can be created without an admins element +# Disabled until https://github.com/igniterealtime/openfire-restAPI-plugin/issues/288 is fixed. +POST {{restapi_url}}/groups +Authorization: {{authkey}} +Content-Type: application/xml +[Options] +skip: true +``` + + + group2 + A group without an admins element + false + + john + + +``` +HTTP 201 + +GET {{restapi_url}}/groups/group2 +Authorization: {{authkey}} +[Options] +skip: true +HTTP 200 +[Asserts] +xpath "/group/members/member" count == 1 +xpath "/group/admins/admin" count == 0 + +DELETE {{restapi_url}}/groups/group2 +Authorization: {{authkey}} +[Options] +skip: true +HTTP 200 diff --git a/test/securitylog.hurl b/test/securitylog.hurl index 6445babca..bc221f3e4 100644 --- a/test/securitylog.hurl +++ b/test/securitylog.hurl @@ -11,7 +11,8 @@ xpath "/logs/log" count <= 1 last_log_id: xpath "number(concat('0', /logs/log/logId))" -# Openfire records a security audit event for every login to the admin console, so log in to create one. +# Openfire records a security audit event for every login to the admin console, so log in to create one. (The REST API +# does not record audit events of its own: https://github.com/igniterealtime/openfire-restAPI-plugin/issues/115) GET {{adminconsole_url}}/login.jsp HTTP 200 [Captures] diff --git a/test/usergroups.hurl b/test/usergroups.hurl index f8c5b3d9e..69e91fc1d 100644 --- a/test/usergroups.hurl +++ b/test/usergroups.hurl @@ -142,13 +142,83 @@ HTTP 404 [Asserts] xpath "string(/error/exception)" == "UserNotFoundException" -# TODO: Adding a user to a group that does not exist is documented to create that group, but fails with a 500, as -# GroupController#createGroup dereferences the (null) members of the GroupEntity that -# UserServiceController#addUserToGroup(s) creates. (Creating a group through POST /groups without and -# fails in the same way.) Add tests for automatic group creation once that is fixed. +# TEST: Adding a user to a group that does not exist creates that group +# Disabled until https://github.com/igniterealtime/openfire-restAPI-plugin/issues/279 is fixed. +POST {{restapi_url}}/users/geordi/groups/engineering +Authorization: {{authkey}} +[Options] +skip: true +HTTP 201 + +GET {{restapi_url}}/groups/engineering +Authorization: {{authkey}} +[Options] +skip: true +HTTP 200 +[Asserts] +xpath "/group/members[member='geordi@example.org']" exists -# TODO: Adding a user that does not exist to an (existing) group returns 201 and adds the JID to the group, where -# 404 might be expected. Add a test once the intended behaviour is decided. +POST {{restapi_url}}/users/geordi/groups +Authorization: {{authkey}} +Content-Type: application/xml +[Options] +skip: true +``` + + + sickbay + +``` +HTTP 201 + +GET {{restapi_url}}/groups/sickbay +Authorization: {{authkey}} +[Options] +skip: true +HTTP 200 +[Asserts] +xpath "/group/members[member='geordi@example.org']" exists + +DELETE {{restapi_url}}/groups/engineering +Authorization: {{authkey}} +[Options] +skip: true +HTTP 200 + +DELETE {{restapi_url}}/groups/sickbay +Authorization: {{authkey}} +[Options] +skip: true +HTTP 200 + +# TEST: Adding a user that does not exist to a group fails +# Disabled until https://github.com/igniterealtime/openfire-restAPI-plugin/issues/280 is fixed. +POST {{restapi_url}}/users/nonexistent/groups/security +Authorization: {{authkey}} +[Options] +skip: true +HTTP 404 + +POST {{restapi_url}}/users/nonexistent/groups +Authorization: {{authkey}} +Content-Type: application/xml +[Options] +skip: true +``` + + + security + +``` +HTTP 404 + +GET {{restapi_url}}/groups/security +Authorization: {{authkey}} +[Options] +skip: true +HTTP 200 +[Asserts] +xpath "/group/members[member='nonexistent@example.org']" not exists DELETE {{restapi_url}}/groups/security Authorization: {{authkey}} diff --git a/test/userroster.hurl b/test/userroster.hurl index fbfe6d77c..7a2f201e0 100644 --- a/test/userroster.hurl +++ b/test/userroster.hurl @@ -86,18 +86,111 @@ HTTP 400 [Asserts] xpath "string(/error/exception)" == "IllegalArgumentException" -# TODO: Adding a roster entry with an invalid subscriptionType (e.g. 9) returns 409 (UserAlreadyExistsException), and -# the entry is created anyway, as UserServiceController#addRosterItem validates the subscription type only after -# creating the roster item. Add a test expecting a 400 (and no new roster entry) once that is fixed. - -# TODO: A JID without an '@' is taken to be a domain JID: adding a roster entry with john stores an entry -# for the domain 'john', rather than for john@example.org (unlike group members, which resolve a username against the -# local domain). An empty or unparsable JID causes a 500 rather than a 400. Add tests once the intended behaviour is -# decided. - -# TODO: Adding a roster entry for a user that does not exist returns 201 (and stores the roster entry), where 404 is -# documented. Getting the roster of a user that does not exist returns 200 (rather than 404) too. Add tests for those -# once UserServiceController#getUserRoster checks that the user exists. +# TEST: Adding a roster entry with an invalid subscription type is rejected, and adds no entry +# Disabled until https://github.com/igniterealtime/openfire-restAPI-plugin/issues/276 is fixed. +POST {{restapi_url}}/users/data/roster +Authorization: {{authkey}} +Content-Type: application/xml +[Options] +skip: true +``` + + + jane@example.org + 9 + +``` +HTTP 400 + +GET {{restapi_url}}/users/data/roster +Authorization: {{authkey}} +[Options] +skip: true +HTTP 200 +[Asserts] +xpath "/roster/rosterItem[jid='jane@example.org']" not exists + +# TEST: A roster entry JID without a domain is resolved against the local domain +# Disabled until https://github.com/igniterealtime/openfire-restAPI-plugin/issues/277 is fixed. +# (The issue also allows for rejecting such a JID with a 400 instead, in which case this test needs to change.) +POST {{restapi_url}}/users/data/roster +Authorization: {{authkey}} +Content-Type: application/xml +[Options] +skip: true +``` + + + jane + +``` +HTTP 201 + +GET {{restapi_url}}/users/data/roster +Authorization: {{authkey}} +[Options] +skip: true +HTTP 200 +[Asserts] +xpath "/roster/rosterItem[jid='jane@example.org']" exists +xpath "/roster/rosterItem[jid='jane']" not exists + +DELETE {{restapi_url}}/users/data/roster/jane@example.org +Authorization: {{authkey}} +[Options] +skip: true +HTTP 200 + +# TEST: Adding a roster entry with an empty or invalid JID is rejected +# Disabled until https://github.com/igniterealtime/openfire-restAPI-plugin/issues/278 is fixed. +POST {{restapi_url}}/users/data/roster +Authorization: {{authkey}} +Content-Type: application/xml +[Options] +skip: true +``` + + + + +``` +HTTP 400 + +POST {{restapi_url}}/users/data/roster +Authorization: {{authkey}} +Content-Type: application/xml +[Options] +skip: true +``` + + + not a@valid@jid + +``` +HTTP 400 + +# TEST: Adding a roster entry for a user that does not exist fails +# Disabled until https://github.com/igniterealtime/openfire-restAPI-plugin/issues/274 is fixed. +POST {{restapi_url}}/users/nonexistent/roster +Authorization: {{authkey}} +Content-Type: application/xml +[Options] +skip: true +``` + + + john@example.org + +``` +HTTP 404 + +# TEST: Getting the roster of a user that does not exist fails +# Disabled until https://github.com/igniterealtime/openfire-restAPI-plugin/issues/275 is fixed. +GET {{restapi_url}}/users/nonexistent/roster +Authorization: {{authkey}} +[Options] +skip: true +HTTP 404 # TEST: Update a roster entry PUT {{restapi_url}}/users/data/roster/john@example.org diff --git a/test/uservcard.hurl b/test/uservcard.hurl index 2b62234be..e019b8f61 100644 --- a/test/uservcard.hurl +++ b/test/uservcard.hurl @@ -69,9 +69,51 @@ GET {{restapi_url}}/users/nonexistent/vcard Authorization: {{authkey}} HTTP 204 -# TODO: Storing a vCard for a user that does not exist returns 200 (and stores the vCard), and any XML document (e.g. -# ) is accepted and stored as a vCard. Add tests expecting a 404 and a 400 respectively, once it is decided -# that the API should validate these. +# TEST: Storing a vCard for a user that does not exist fails +# Disabled until https://github.com/igniterealtime/openfire-restAPI-plugin/issues/281 is fixed. +PUT {{restapi_url}}/users/nonexistent/vcard +Authorization: {{authkey}} +Content-Type: application/xml +[Options] +skip: true +``` +Nobody +``` +HTTP 404 + +GET {{restapi_url}}/users/nonexistent/vcard +Authorization: {{authkey}} +[Options] +skip: true +HTTP 204 + +# TEST: Storing a document that is not a vCard is rejected +# Disabled until https://github.com/igniterealtime/openfire-restAPI-plugin/issues/282 is fixed. +PUT {{restapi_url}}/users/troi/vcard +Authorization: {{authkey}} +Content-Type: application/xml +[Options] +skip: true +``` + +``` +HTTP 400 + +PUT {{restapi_url}}/users/troi/vcard +Authorization: {{authkey}} +Content-Type: application/xml +[Options] +skip: true +``` +Deanna Troi +``` +HTTP 400 + +GET {{restapi_url}}/users/troi/vcard +Authorization: {{authkey}} +[Options] +skip: true +HTTP 204 DELETE {{restapi_url}}/users/troi From 49ff070f706f61061f787b8ad3bc748e4b07831c Mon Sep 17 00:00:00 2001 From: Dan Caseley Date: Fri, 25 Sep 2026 23:01:33 +0100 Subject: [PATCH 20/21] Hurl: document running the tests from the Hurl container image Co-Authored-By: Claude Opus 5.5 --- test/README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/test/README.md b/test/README.md index 429401f39..e27c2bc94 100644 --- a/test/README.md +++ b/test/README.md @@ -2,7 +2,20 @@ The tests contained in this folder are written in Hurl (see [docs](https://hurl.dev/docs/manual.html)). -Install Hurl with instructions as per the documentation. +Install Hurl with instructions as per the documentation, and run the tests from the root of the repository: + +```bash +hurl --test --variables-file test/test.env --jobs 1 test/*.hurl +``` + +Alternatively, run Hurl from its container image, as CI does (`--network host` lets the container reach Openfire on +`localhost`): + +```bash +docker run --rm --network host --user "$(id -u):$(id -g)" --volume "$PWD:$PWD" --workdir "$PWD" \ + ghcr.io/orange-opensource/hurl:8.0.1 \ + --test --variables-file test/test.env --jobs 1 test/*.hurl +``` Configure the Rest API: From 430d2ff8ee86b26bf37056d87a043ed6636e18b5 Mon Sep 17 00:00:00 2001 From: Dan Caseley Date: Fri, 25 Sep 2026 23:02:25 +0100 Subject: [PATCH 21/21] Hurl: take the admin credentials from test.env The sessions tests now use a dedicated user, rather than a user whose password comes from the server configuration, as a SASL PLAIN request cannot be built from variables. Co-Authored-By: Claude Opus 5.5 --- test/README.md | 3 ++- test/securitylog.hurl | 10 +++++----- test/sessions.hurl | 43 +++++++++++++++++++++++++++++++------------ test/test.env | 2 ++ test/users.hurl | 2 +- 5 files changed, 41 insertions(+), 19 deletions(-) diff --git a/test/README.md b/test/README.md index e27c2bc94..a6ad6a969 100644 --- a/test/README.md +++ b/test/README.md @@ -24,7 +24,8 @@ Configure the Rest API: * Set `adminConsole.access.allow-wildcards-in-excludes` to true test.env defines where the tests find Openfire: `restapi_url` (the base URL of the REST API, including its version), -`adminconsole_url` and `bosh_url`, as well as the shared secret (`authkey`). +`adminconsole_url` and `bosh_url`, as well as the shared secret (`authkey`) and the credentials of an admin user +(`admin_username` and `admin_password`), which are used to log in to the admin console. Some tests create XMPP client sessions by logging in over BOSH (Openfire's HTTP binding, which listens on port 7070 by default). The BOSH endpoint is configured by the `bosh_url` variable in test.env; when Openfire runs in a container, make diff --git a/test/securitylog.hurl b/test/securitylog.hurl index bc221f3e4..9a67730ed 100644 --- a/test/securitylog.hurl +++ b/test/securitylog.hurl @@ -22,8 +22,8 @@ POST {{adminconsole_url}}/login.jsp [Form] login: true csrf: {{csrf}} -username: admin -password: admin +username: {{admin_username}} +password: {{admin_password}} HTTP 302 [Asserts] header "Location" == "/index.jsp" @@ -35,18 +35,18 @@ HTTP 200 [Asserts] xpath "/logs/log[logId > {{last_log_id}}]" count == 1 xpath "string(/logs/log[logId > {{last_log_id}}]/summary)" == "Successful admin console login attempt" -xpath "string(/logs/log[logId > {{last_log_id}}]/username)" == "admin" +xpath "string(/logs/log[logId > {{last_log_id}}]/username)" == "{{admin_username}}" xpath "string(/logs/log[logId > {{last_log_id}}]/node)" == "example.org" xpath "string(/logs/log[logId > {{last_log_id}}]/details)" startsWith "The user logged in successfully to the admin console" xpath "/logs/log[logId > {{last_log_id}}]/timestamp" exists # TEST: Filtering by username returns only that user's events -GET {{restapi_url}}/logs/security?username=admin +GET {{restapi_url}}/logs/security?username={{admin_username}} Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/logs/log[logId > {{last_log_id}}]" count == 1 -xpath "/logs/log[username!='admin']" not exists +xpath "/logs/log[username!='{{admin_username}}']" not exists # TEST: Filtering by an unknown username returns no events GET {{restapi_url}}/logs/security?username=nonexistent diff --git a/test/sessions.hurl b/test/sessions.hurl index ea331e878..523e7dec6 100644 --- a/test/sessions.hurl +++ b/test/sessions.hurl @@ -1,3 +1,17 @@ +# Sessions are tested on a dedicated user, which is removed at the end. +POST {{restapi_url}}/users +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + riker + number1 + +``` +HTTP 201 + + # TEST: There are no sessions initially GET {{restapi_url}}/sessions Authorization: {{authkey}} @@ -7,7 +21,7 @@ xpath "/sessions" exists xpath "/sessions[not(child::node())]" count == 1 # sessions at the root, with no child nodes # TEST: A user without sessions has an empty list of sessions -GET {{restapi_url}}/sessions/john +GET {{restapi_url}}/sessions/riker Authorization: {{authkey}} HTTP 200 [Asserts] @@ -26,7 +40,7 @@ xpath "/sessions/session" count == 0 # TEST: A logged-in client's session is listed with its details, for its user only -# Create a session for john, by logging in over BOSH (XEP-0124/XEP-0206). +# Create a session for the user, by logging in over BOSH (XEP-0124/XEP-0206). POST {{bosh_url}}/http-bind/ Content-Type: text/xml; charset=utf-8 ``` @@ -36,11 +50,11 @@ HTTP 200 [Captures] sid: xpath "string(/_:body/@sid)" -# SASL PLAIN, with base64("\0john\0secret") +# SASL PLAIN, with base64("\0riker\0number1") POST {{bosh_url}}/http-bind/ Content-Type: text/xml; charset=utf-8 ``` -AGpvaG4Ac2VjcmV0 +AHJpa2VyAG51bWJlcjE= ``` HTTP 200 [Asserts] @@ -60,7 +74,7 @@ Content-Type: text/xml; charset=utf-8 ``` HTTP 200 [Asserts] -xpath "string(//*[local-name()='jid'])" == "john@example.org/hurl" +xpath "string(//*[local-name()='jid'])" == "riker@example.org/hurl" # The session is only listed once it has sent initial presence. POST {{bosh_url}}/http-bind/ @@ -74,15 +88,15 @@ GET {{restapi_url}}/sessions Authorization: {{authkey}} HTTP 200 [Asserts] -xpath "/sessions/session[sessionId='john@example.org/hurl']" exists +xpath "/sessions/session[sessionId='riker@example.org/hurl']" exists -GET {{restapi_url}}/sessions/john +GET {{restapi_url}}/sessions/riker Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/sessions/session" count == 1 -xpath "string(/sessions/session/sessionId)" == "john@example.org/hurl" -xpath "string(/sessions/session/username)" == "john" +xpath "string(/sessions/session/sessionId)" == "riker@example.org/hurl" +xpath "string(/sessions/session/username)" == "riker" xpath "string(/sessions/session/resource)" == "hurl" xpath "string(/sessions/session/node)" == "Local" xpath "string(/sessions/session/sessionStatus)" == "Authenticated" @@ -99,12 +113,12 @@ HTTP 200 xpath "/sessions/session" count == 0 # TEST: Kicking a user's sessions closes them -DELETE {{restapi_url}}/sessions/john +DELETE {{restapi_url}}/sessions/riker Authorization: {{authkey}} HTTP 200 # Sessions are closed asynchronously, so allow for some delay. -GET {{restapi_url}}/sessions/john +GET {{restapi_url}}/sessions/riker Authorization: {{authkey}} [Options] retry: 10 @@ -125,6 +139,11 @@ retry-interval: 100ms HTTP 404 # TEST: Kicking the sessions of a user that has no sessions is fine -DELETE {{restapi_url}}/sessions/john +DELETE {{restapi_url}}/sessions/riker +Authorization: {{authkey}} +HTTP 200 + + +DELETE {{restapi_url}}/users/riker Authorization: {{authkey}} HTTP 200 diff --git a/test/test.env b/test/test.env index c888f8986..6f9191b94 100644 --- a/test/test.env +++ b/test/test.env @@ -1,4 +1,6 @@ authkey=potato +admin_username=admin +admin_password=admin adminconsole_url=http://localhost:9090 restapi_url=http://localhost:9090/plugins/restapi/v1 bosh_url=http://localhost:7070 diff --git a/test/users.hurl b/test/users.hurl index 7d2d27f61..3f496008c 100644 --- a/test/users.hurl +++ b/test/users.hurl @@ -4,7 +4,7 @@ Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/users/user" count == 3 -xpath "/users/user[username='admin']" exists +xpath "/users/user[username='{{admin_username}}']" exists xpath "/users/user[username='jane']" exists xpath "/users/user[username='john']" exists xpath "/users/user[name='John Doe']" exists