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 diff --git a/test/README.md b/test/README.md index cb404e3e0..a6ad6a969 100644 --- a/test/README.md +++ b/test/README.md @@ -2,10 +2,31 @@ 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: * 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 + +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`) 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 +sure that port 7070 is published (or override the variable, e.g. `--variable bosh_url=http://:7070`). diff --git a/test/chatroomaffiliations.hurl b/test/chatroomaffiliations.hurl new file mode 100644 index 000000000..42495bd07 --- /dev/null +++ b/test/chatroomaffiliations.hurl @@ -0,0 +1,297 @@ +# 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. See https://github.com/igniterealtime/openfire-restAPI-plugin/issues/267 +GET {{restapi_url}}/chatrooms/ops/visitors +Authorization: {{authkey}} +HTTP 404 + +# 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 + +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 +Authorization: {{authkey}} +HTTP 200 + +DELETE {{restapi_url}}/groups/crew +Authorization: {{authkey}} +HTTP 200 diff --git a/test/chatrooms.hurl b/test/chatrooms.hurl index 1c3027a89..aad477332 100644 --- a/test/chatrooms.hurl +++ b/test/chatrooms.hurl @@ -1,14 +1,505 @@ -GET http://localhost:9090/plugins/restapi/v1/chatrooms +# TEST: List the chat rooms +GET {{restapi_url}}/chatrooms Authorization: {{authkey}} HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/sessions +# 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 + +# 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 +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 + +# 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 +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" + +# 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 +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/participants/participant" count == 0 + +GET {{restapi_url}}/chatrooms/lobby/occupants Authorization: {{authkey}} HTTP 200 [Asserts] -xpath "/sessions[not(child::node())]" count == 1 # sessions at the root, with no child nodes +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 http://localhost:9090/plugins/restapi/v1/system/readiness/server +GET {{restapi_url}}/chatrooms/nonexistent/occupants Authorization: {{authkey}} -HTTP 200 \ No newline at end of file +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" + +# 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: 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}} +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 diff --git a/test/chatservice.hurl b/test/chatservice.hurl index 99f31274b..96889cb0b 100644 --- a/test/chatservice.hurl +++ b/test/chatservice.hurl @@ -1,8 +1,106 @@ -GET http://localhost:9090/plugins/restapi/v1/chatservices +# TEST: The default 'conference' chat service is listed +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. + +# 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}}" + +# 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/clustering.hurl b/test/clustering.hurl index 84fd5d471..83dbbd7fa 100644 --- a/test/clustering.hurl +++ b/test/clustering.hurl @@ -1,10 +1,12 @@ -GET {{host}}/plugins/restapi/v1/clustering/status +# 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" -GET {{host}}/plugins/restapi/v1/clustering/nodes +# TEST: A standalone server has no cluster nodes +GET {{restapi_url}}/clustering/nodes Authorization: {{authkey}} HTTP 200 [Asserts] diff --git a/test/groups.hurl b/test/groups.hurl index 299ec523a..365457202 100644 --- a/test/groups.hurl +++ b/test/groups.hurl @@ -1,11 +1,13 @@ -GET http://localhost:9090/plugins/restapi/v1/groups +# 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 -POST http://localhost:9090/plugins/restapi/v1/groups +# TEST: A created group is listed, and can be retrieved +POST {{restapi_url}}/groups Authorization: {{authkey}} Content-Type: application/xml ``` @@ -24,20 +26,21 @@ Content-Type: application/xml ``` HTTP 201 -GET http://localhost:9090/plugins/restapi/v1/groups # check if the group was created +GET {{restapi_url}}/groups 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 +# TEST: Updating a group changes its description +PUT {{restapi_url}}/groups/group1 Authorization: {{authkey}} Content-Type: application/xml ``` @@ -56,14 +59,49 @@ 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 +# TEST: Delete a group +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/messagearchive.hurl b/test/messagearchive.hurl index 7b06bc4f2..1b7551c47 100644 --- a/test/messagearchive.hurl +++ b/test/messagearchive.hurl @@ -1,8 +1,13 @@ -GET http://localhost:9090/plugins/restapi/v1/archive/messages/unread/john@example.org +# TEST: Get the unread message count for a user +GET {{restapi_url}}/archive/messages/unread/john@example.org Authorization: {{authkey}} HTTP 200 [Asserts] 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. diff --git a/test/messagebroadcast.hurl b/test/messagebroadcast.hurl index 3bec3dbf5..5ec55e215 100644 --- a/test/messagebroadcast.hurl +++ b/test/messagebroadcast.hurl @@ -1,4 +1,5 @@ -POST http://localhost:9090/plugins/restapi/v1/messages/users +# TEST: Broadcast a message while there are no sessions to deliver it to +POST {{restapi_url}}/messages/users Authorization: {{authkey}} Content-Type: application/xml ``` @@ -9,4 +10,103 @@ Content-Type: application/xml ``` HTTP 201 -# TODO: What could validate behaviour? \ No newline at end of file +# TEST: Broadcasting a message with an empty body is rejected +POST {{restapi_url}}/messages/users +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + + +``` +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 +``` + + +``` +HTTP 400 +[Asserts] +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 +``` + +``` +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 {{restapi_url}}/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" + +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` + +``` +HTTP 200 +[Asserts] +xpath "string(/_:body/@type)" == "terminate" diff --git a/test/securitylog.hurl b/test/securitylog.hurl index 6a820dcf3..9a67730ed 100644 --- a/test/securitylog.hurl +++ b/test/securitylog.hurl @@ -1,11 +1,93 @@ -GET http://localhost:9090/plugins/restapi/v1/logs/security +# 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 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 +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. (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] +csrf: cookie "csrf" + +POST {{adminconsole_url}}/login.jsp +[Form] +login: true +csrf: {{csrf}} +username: {{admin_username}} +password: {{admin_password}} +HTTP 302 +[Asserts] +header "Location" == "/index.jsp" + + +GET {{restapi_url}}/logs/security +Authorization: {{authkey}} +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_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_username}} +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/logs/log[logId > {{last_log_id}}]" count == 1 +xpath "/logs/log[username!='{{admin_username}}']" 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 +[Asserts] +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}} +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 +[Asserts] +xpath "/logs/log[logId > {{last_log_id}}]" count == 1 diff --git a/test/sessions.hurl b/test/sessions.hurl index 12d28127e..523e7dec6 100644 --- a/test/sessions.hurl +++ b/test/sessions.hurl @@ -1,20 +1,149 @@ -GET http://localhost:9090/plugins/restapi/v1/sessions +# 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}} 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 +# TEST: A user without sessions has an empty list of sessions +GET {{restapi_url}}/sessions/riker Authorization: {{authkey}} HTTP 200 [Asserts] xpath "/sessions" exists +xpath "/sessions/session" count == 0 -# TODO: This documents behaviour, but is it correct?? -GET http://localhost:9090/plugins/restapi/v1/sessions/nonexistent +# 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 Authorization: {{authkey}} HTTP 200 [Asserts] 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 the user, 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("\0riker\0number1") +POST {{bosh_url}}/http-bind/ +Content-Type: text/xml; charset=utf-8 +``` +AHJpa2VyAG51bWJlcjE= +``` +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'])" == "riker@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 {{restapi_url}}/sessions +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/sessions/session[sessionId='riker@example.org/hurl']" exists + +GET {{restapi_url}}/sessions/riker +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/sessions/session" count == 1 +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" +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 -# TODO: create a session (somehow), then read its props, then kick it with DELETE \ No newline at end of file +GET {{restapi_url}}/sessions/jane +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/sessions/session" count == 0 + +# TEST: Kicking a user's sessions closes them +DELETE {{restapi_url}}/sessions/riker +Authorization: {{authkey}} +HTTP 200 + +# Sessions are closed asynchronously, so allow for some delay. +GET {{restapi_url}}/sessions/riker +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 + +# TEST: Kicking the sessions of a user that has no sessions is fine +DELETE {{restapi_url}}/sessions/riker +Authorization: {{authkey}} +HTTP 200 + + +DELETE {{restapi_url}}/users/riker +Authorization: {{authkey}} +HTTP 200 diff --git a/test/statistics.hurl b/test/statistics.hurl index 4df4014d3..1862e5506 100644 --- a/test/statistics.hurl +++ b/test/statistics.hurl @@ -1,4 +1,5 @@ -GET http://localhost:9090/plugins/restapi/v1/system/statistics/sessions +# TEST: Get the session statistics +GET {{restapi_url}}/system/statistics/sessions Authorization: {{authkey}} HTTP 200 [Asserts] diff --git a/test/system.hurl b/test/system.hurl index 375d876fd..6cd6621a0 100644 --- a/test/system.hurl +++ b/test/system.hurl @@ -1,63 +1,78 @@ -GET http://localhost:9090/plugins/restapi/v1/system/liveness +# TEST: Liveness check +GET {{restapi_url}}/system/liveness Authorization: {{authkey}} HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/system/liveness/deadlock +# TEST: Liveness check for deadlocked threads +GET {{restapi_url}}/system/liveness/deadlock Authorization: {{authkey}} HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/system/liveness/properties +# TEST: Liveness check for system properties +GET {{restapi_url}}/system/liveness/properties Authorization: {{authkey}} HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/system/readiness +# TEST: Readiness check +GET {{restapi_url}}/system/readiness Authorization: {{authkey}} HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/system/readiness/cluster +# TEST: Readiness check for the cluster +GET {{restapi_url}}/system/readiness/cluster Authorization: {{authkey}} HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/system/readiness/connections +# TEST: Readiness check for connections +GET {{restapi_url}}/system/readiness/connections Authorization: {{authkey}} HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/system/readiness/plugins +# TEST: Readiness check for plugins +GET {{restapi_url}}/system/readiness/plugins Authorization: {{authkey}} HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/system/readiness/server +# TEST: Readiness check for the server +GET {{restapi_url}}/system/readiness/server Authorization: {{authkey}} HTTP 200 -GET http://localhost:9090/plugins/restapi/v1/system/properties +# TEST: List the system properties, which excludes the plugin's own properties +GET {{restapi_url}}/system/properties 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 +# 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. -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] 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. -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 +# TEST: Create a property +POST {{restapi_url}}/system/properties Authorization: {{authkey}} Content-Type: application/xml ``` @@ -66,14 +81,16 @@ 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] 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 +# TEST: Update a property +PUT {{restapi_url}}/system/properties/test.key Authorization: {{authkey}} Content-Type: application/xml ``` @@ -82,7 +99,14 @@ Content-Type: application/xml ``` HTTP 200 -PUT http://localhost:9090/plugins/restapi/v1/system/properties/wrong.key +GET {{restapi_url}}/system/properties/test.key +Authorization: {{authkey}} +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 ``` @@ -91,7 +115,8 @@ Content-Type: application/xml ``` HTTP 404 -PUT http://localhost:9090/plugins/restapi/v1/system/properties/test.key +# 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 ``` @@ -100,19 +125,26 @@ Content-Type: application/xml ``` HTTP 400 -DELETE http://localhost:9090/plugins/restapi/v1/system/properties/test.key +# TEST: Delete a property +DELETE {{restapi_url}}/system/properties/test.key Authorization: {{authkey}} HTTP 200 +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 http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.enabled +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 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 ``` @@ -121,22 +153,25 @@ 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 http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.enabled +DELETE {{restapi_url}}/system/properties/plugin.restapi.enabled 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 http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.httpAuth +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 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 ``` @@ -145,22 +180,25 @@ 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 http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.httpAuth +DELETE {{restapi_url}}/system/properties/plugin.restapi.httpAuth 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 http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.customAuthFilter +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 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 ``` @@ -169,22 +207,25 @@ 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 http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.customAuthFilter +DELETE {{restapi_url}}/system/properties/plugin.restapi.customAuthFilter 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 http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.secret +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 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 ``` @@ -193,22 +234,25 @@ 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 http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.secret +DELETE {{restapi_url}}/system/properties/plugin.restapi.secret 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 http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.serviceLoggingEnabled +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 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 ``` @@ -217,22 +261,25 @@ 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 http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.serviceLoggingEnabled +DELETE {{restapi_url}}/system/properties/plugin.restapi.serviceLoggingEnabled 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 http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.allowedIPs +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 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 ``` @@ -241,23 +288,25 @@ 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 http://localhost:9090/plugins/restapi/v1/system/properties/plugin.restapi.allowedIPs +DELETE {{restapi_url}}/system/properties/plugin.restapi.allowedIPs 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. -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 +# TEST: Deleting a property also deletes its child properties +POST {{restapi_url}}/system/properties Authorization: {{authkey}} Content-Type: application/xml ``` @@ -266,7 +315,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 ``` @@ -275,19 +324,20 @@ 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 +# 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. -POST http://localhost:9090/plugins/restapi/v1/system/properties +POST {{restapi_url}}/system/properties Authorization: {{authkey}} Content-Type: application/xml ``` @@ -296,7 +346,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 ``` @@ -305,33 +355,35 @@ 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 +# 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. -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 +# 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 ``` @@ -340,25 +392,26 @@ 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 +# 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. -POST http://localhost:9090/plugins/restapi/v1/system/properties +POST {{restapi_url}}/system/properties Authorization: {{authkey}} Content-Type: application/xml ``` @@ -367,7 +420,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 ``` @@ -376,7 +429,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 ``` @@ -385,7 +438,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 ``` @@ -394,25 +447,26 @@ 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 +# 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 http://localhost:9090/plugins/restapi/v1/system/properties +POST {{restapi_url}}/system/properties Authorization: {{authkey}} Content-Type: application/xml ``` @@ -421,8 +475,9 @@ 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 http://localhost:9090/plugins/restapi/v1/system/properties +POST {{restapi_url}}/system/properties Authorization: {{authkey}} Content-Type: application/xml ``` @@ -431,12 +486,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 6d4bd1e85..6f9191b94 100644 --- a/test/test.env +++ b/test/test.env @@ -1,2 +1,6 @@ -host=http://localhost:9090 -authkey=potato \ No newline at end of file +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/usergroups.hurl b/test/usergroups.hurl new file mode 100644 index 000000000..69e91fc1d --- /dev/null +++ b/test/usergroups.hurl @@ -0,0 +1,237 @@ +# Group membership is tested on a dedicated user, which is removed at the end. +POST {{restapi_url}}/users +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + geordi + visor + +``` +HTTP 201 + +POST {{restapi_url}}/groups +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + security + Security + false + + + +``` +HTTP 201 + +POST {{restapi_url}}/groups +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + klingons + Klingons + false + + + +``` +HTTP 201 + +POST {{restapi_url}}/groups +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + bridge + Bridge crew + false + + + +``` +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 + +POST {{restapi_url}}/users/geordi/groups +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + klingons + bridge + +``` +HTTP 201 + +GET {{restapi_url}}/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 {{restapi_url}}/groups/klingons +Authorization: {{authkey}} +HTTP 200 +[Asserts] +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 + +GET {{restapi_url}}/users/geordi/groups +Authorization: {{authkey}} +HTTP 200 +[Asserts] +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 +``` + + + klingons + bridge + +``` +HTTP 200 + +GET {{restapi_url}}/users/geordi/groups +Authorization: {{authkey}} +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 +[Asserts] +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 +[Asserts] +xpath "string(/error/exception)" == "UserNotFoundException" + +# 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 + +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}} +HTTP 200 + +DELETE {{restapi_url}}/groups/klingons +Authorization: {{authkey}} +HTTP 200 + +DELETE {{restapi_url}}/groups/bridge +Authorization: {{authkey}} +HTTP 200 + +DELETE {{restapi_url}}/users/geordi +Authorization: {{authkey}} +HTTP 200 diff --git a/test/userlockouts.hurl b/test/userlockouts.hurl new file mode 100644 index 000000000..a27a4a5a3 --- /dev/null +++ b/test/userlockouts.hurl @@ -0,0 +1,196 @@ +# Lockouts are tested on a dedicated user, which is removed at the end. +POST {{restapi_url}}/users +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + worf + qapla + +``` +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 +``` + +``` +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 {{restapi_url}}/sessions/worf +Authorization: {{authkey}} +HTTP 200 +[Asserts] +xpath "/sessions/session" count == 1 + +POST {{restapi_url}}/lockouts/worf +Authorization: {{authkey}} +HTTP 201 + +# Sessions are closed asynchronously, so allow for some delay. +GET {{restapi_url}}/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 + +# TEST: Locking out a user that is already locked out is fine +POST {{restapi_url}}/lockouts/worf +Authorization: {{authkey}} +HTTP 201 + +# TEST: 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 + +# TEST: Unlock a user +DELETE {{restapi_url}}/lockouts/worf +Authorization: {{authkey}} +HTTP 200 + +# TEST: Unlocking a user that is not locked out is fine +DELETE {{restapi_url}}/lockouts/worf +Authorization: {{authkey}} +HTTP 200 + +# 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 +``` + +``` +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 + +# TEST: Locking out a user that does not exist fails +POST {{restapi_url}}/lockouts/nonexistent +Authorization: {{authkey}} +HTTP 404 +[Asserts] +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 +[Asserts] +xpath "string(/error/exception)" == "UserNotFoundException" +xpath "string(/error/resource)" == "nonexistent" + + +DELETE {{restapi_url}}/users/worf +Authorization: {{authkey}} +HTTP 200 diff --git a/test/userroster.hurl b/test/userroster.hurl new file mode 100644 index 000000000..7a2f201e0 --- /dev/null +++ b/test/userroster.hurl @@ -0,0 +1,268 @@ +# TEST: Get the roster of a user from the demoboot configuration +GET {{restapi_url}}/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 + + +# Roster changes are tested on a dedicated user, which is removed at the end. +POST {{restapi_url}}/users +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + data + spot + +``` +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 +``` + + + john@example.org + John + 3 + + Friends + + +``` +HTTP 201 + +GET {{restapi_url}}/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" + +# 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 +``` + + + john@example.org + +``` +HTTP 409 +[Asserts] +xpath "string(/error/exception)" == "UserAlreadyExistsException" + +# TEST: Adding a roster entry without a JID is rejected +POST {{restapi_url}}/users/data/roster +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + Nobody + +``` +HTTP 400 +[Asserts] +xpath "string(/error/exception)" == "IllegalArgumentException" + +# 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 +Authorization: {{authkey}} +Content-Type: application/xml +``` + + + john@example.org + Johnny + 1 + + Crew + + +``` +HTTP 200 + +GET {{restapi_url}}/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" + +# 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 +``` + + + jane@example.org + Jane + +``` +HTTP 404 +[Asserts] +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 + +GET {{restapi_url}}/users/data/roster +Authorization: {{authkey}} +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 +[Asserts] +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 +[Asserts] +xpath "string(/error/exception)" == "UserNotFoundException" +xpath "string(/error/resource)" == "nonexistent" + +DELETE {{restapi_url}}/users/data +Authorization: {{authkey}} +HTTP 200 diff --git a/test/users.hurl b/test/users.hurl index 8e450aae6..3f496008c 100644 --- a/test/users.hurl +++ b/test/users.hurl @@ -1,15 +1,17 @@ -GET http://localhost:9090/plugins/restapi/v1/users +# TEST: List all users +GET {{restapi_url}}/users 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 xpath "/users/user[email='john.doe@example.com']" exists -GET http://localhost:9090/plugins/restapi/v1/users?search=john +# TEST: Search for users by (part of) their username +GET {{restapi_url}}/users?search=john Authorization: {{authkey}} HTTP 200 [Asserts] @@ -18,15 +20,15 @@ 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 +# 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 -POST http://localhost:9090/plugins/restapi/v1/users +# TEST: Create a user with a property +POST {{restapi_url}}/users Authorization: {{authkey}} Content-Type: application/xml ``` @@ -43,7 +45,8 @@ Content-Type: application/xml ``` HTTP 201 -GET http://localhost:9090/plugins/restapi/v1/users?propertyKey=tea&propertyValue=earlgreyhot +# TEST: Search for users by property key and value +GET {{restapi_url}}/users?propertyKey=tea&propertyValue=earlgreyhot Authorization: {{authkey}} HTTP 200 [Asserts] @@ -51,10 +54,27 @@ 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" + +# TEST: Search for users by property key only +GET {{restapi_url}}/users?propertyKey=tea +Authorization: {{authkey}} +HTTP 200 +[Asserts] +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 -PUT http://localhost:9090/plugins/restapi/v1/users/jeanluc +# TEST: Update a user +PUT {{restapi_url}}/users/jeanluc Authorization: {{authkey}} Content-Type: application/xml ``` @@ -71,7 +91,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] @@ -81,30 +101,15 @@ 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 +# TEST: Delete a user +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] 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 diff --git a/test/uservcard.hurl b/test/uservcard.hurl new file mode 100644 index 000000000..e019b8f61 --- /dev/null +++ b/test/uservcard.hurl @@ -0,0 +1,121 @@ +# 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 + +# 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 +Authorization: {{authkey}} +HTTP 200