diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 6f03709f1..206aba875 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -8,3 +8,51 @@ jobs:
secrets:
IGNITE_REALTIME_MAVEN_USERNAME: ${{ secrets.IGNITE_REALTIME_MAVEN_USERNAME }}
IGNITE_REALTIME_MAVEN_PASSWORD: ${{ secrets.IGNITE_REALTIME_MAVEN_PASSWORD }}
+ test:
+ runs-on: ubuntu-latest
+ needs: build
+ steps:
+ - name: Checkout tests
+ uses: actions/checkout@v4
+ with:
+ sparse-checkout: |
+ test
+
+ - name: Download the built plugin
+ uses: actions/download-artifact@v4
+ with:
+ name: restAPI
+ path: .
+
+ - name: Rename plugin
+ run: |
+ mv restAPI-openfire-plugin-assembly.jar restAPI.jar
+
+ - name: Run Openfire
+ uses: igniterealtime/launch-openfire-action@v1.1.2
+ with:
+ version: 4.9.0
+ 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
+
+ - name: Expose Openfire logs
+ uses: actions/upload-artifact@v4
+ if: always() # always run even if the previous step fails
+ with:
+ name: Openfire server logs
+ path: openfire/logs/*
+
+ - name: Publish Test Report
+ uses: mikepenz/action-junit-report@v4
+ if: always() # always run even if the previous step fails
+ with:
+ report_paths: 'test-results.xml'
+ suite_regex: '*'
+ include_passed: true
+ detailed_summary: true
diff --git a/test/README.md b/test/README.md
new file mode 100644
index 000000000..cb404e3e0
--- /dev/null
+++ b/test/README.md
@@ -0,0 +1,11 @@
+# Tests
+
+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.
+
+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
diff --git a/test/chatrooms.hurl b/test/chatrooms.hurl
new file mode 100644
index 000000000..1c3027a89
--- /dev/null
+++ b/test/chatrooms.hurl
@@ -0,0 +1,14 @@
+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/chatservice.hurl b/test/chatservice.hurl
new file mode 100644
index 000000000..99f31274b
--- /dev/null
+++ b/test/chatservice.hurl
@@ -0,0 +1,8 @@
+GET http://localhost:9090/plugins/restapi/v1/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"
diff --git a/test/clustering.hurl b/test/clustering.hurl
new file mode 100644
index 000000000..84fd5d471
--- /dev/null
+++ b/test/clustering.hurl
@@ -0,0 +1,11 @@
+GET {{host}}/plugins/restapi/v1/clustering/status
+Authorization: {{authkey}}
+HTTP 200
+[Asserts]
+xpath "string(/clustering/status)" == "Disabled"
+
+GET {{host}}/plugins/restapi/v1/clustering/nodes
+Authorization: {{authkey}}
+HTTP 200
+[Asserts]
+xpath "/clusterNodes[not(child::node())]" exists
diff --git a/test/demoboot-with-additions.xml b/test/demoboot-with-additions.xml
new file mode 100644
index 000000000..f65b5841d
--- /dev/null
+++ b/test/demoboot-with-additions.xml
@@ -0,0 +1,64 @@
+
+
+
+
+ true
+ secret
+ potato
+
+
+
+
+ true
+
+ 9090
+ 9091
+
+
+ org.jivesoftware.database.EmbeddedConnectionProvider
+
+
+ true
+ en
+
+
+ true
+
+ example.org
+ example.org
+
+
+ embedded
+
+
+ admin@example.com
+ admin
+
+
+
+ john
+ secret
+ John Doe
+ john.doe@example.com
+
+
+ jane@example.org
+ Jane
+
+
+
+
+ jane
+ secret
+ Jane Doe
+ jane.doe@example.com
+
+
+ john@example.org
+ John
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/groups.hurl b/test/groups.hurl
new file mode 100644
index 000000000..299ec523a
--- /dev/null
+++ b/test/groups.hurl
@@ -0,0 +1,69 @@
+
+GET http://localhost:9090/plugins/restapi/v1/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
+Authorization: {{authkey}}
+Content-Type: application/xml
+```
+
+
+ group1
+ test-group
+ false
+
+ jane
+
+
+ john
+
+
+```
+HTTP 201
+
+GET http://localhost:9090/plugins/restapi/v1/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
+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
+Authorization: {{authkey}}
+Content-Type: application/xml
+```
+
+
+ group1
+ test-group-updated
+ false
+
+ jane
+
+
+ john
+
+
+```
+HTTP 200
+
+GET http://localhost:9090/plugins/restapi/v1/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
+Authorization: {{authkey}}
+HTTP 200
+
diff --git a/test/messagearchive.hurl b/test/messagearchive.hurl
new file mode 100644
index 000000000..7b06bc4f2
--- /dev/null
+++ b/test/messagearchive.hurl
@@ -0,0 +1,8 @@
+GET http://localhost:9090/plugins/restapi/v1/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
diff --git a/test/messagebroadcast.hurl b/test/messagebroadcast.hurl
new file mode 100644
index 000000000..3bec3dbf5
--- /dev/null
+++ b/test/messagebroadcast.hurl
@@ -0,0 +1,12 @@
+POST http://localhost:9090/plugins/restapi/v1/messages/users
+Authorization: {{authkey}}
+Content-Type: application/xml
+```
+
+
+ test
+
+```
+HTTP 201
+
+# TODO: What could validate behaviour?
\ No newline at end of file
diff --git a/test/securitylog.hurl b/test/securitylog.hurl
new file mode 100644
index 000000000..6a820dcf3
--- /dev/null
+++ b/test/securitylog.hurl
@@ -0,0 +1,11 @@
+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
diff --git a/test/sessions.hurl b/test/sessions.hurl
new file mode 100644
index 000000000..12d28127e
--- /dev/null
+++ b/test/sessions.hurl
@@ -0,0 +1,20 @@
+GET http://localhost:9090/plugins/restapi/v1/sessions
+Authorization: {{authkey}}
+HTTP 200
+[Asserts]
+xpath "/sessions" exists
+
+GET http://localhost:9090/plugins/restapi/v1/sessions/john
+Authorization: {{authkey}}
+HTTP 200
+[Asserts]
+xpath "/sessions" exists
+
+# TODO: This documents behaviour, but is it correct??
+GET http://localhost:9090/plugins/restapi/v1/sessions/nonexistent
+Authorization: {{authkey}}
+HTTP 200
+[Asserts]
+xpath "/sessions" exists
+
+# TODO: create a session (somehow), then read its props, then kick it with DELETE
\ No newline at end of file
diff --git a/test/statistics.hurl b/test/statistics.hurl
new file mode 100644
index 000000000..4df4014d3
--- /dev/null
+++ b/test/statistics.hurl
@@ -0,0 +1,6 @@
+GET http://localhost:9090/plugins/restapi/v1/system/statistics/sessions
+Authorization: {{authkey}}
+HTTP 200
+[Asserts]
+xpath "/sessions/clusterSessions" exists
+xpath "/sessions/localSessions" exists
\ No newline at end of file
diff --git a/test/system.hurl b/test/system.hurl
new file mode 100644
index 000000000..51a8eb62f
--- /dev/null
+++ b/test/system.hurl
@@ -0,0 +1,86 @@
+GET http://localhost:9090/plugins/restapi/v1/system/liveness
+Authorization: {{authkey}}
+HTTP 200
+
+GET http://localhost:9090/plugins/restapi/v1/system/liveness/deadlock
+Authorization: {{authkey}}
+HTTP 200
+
+GET http://localhost:9090/plugins/restapi/v1/system/liveness/properties
+Authorization: {{authkey}}
+HTTP 200
+
+GET http://localhost:9090/plugins/restapi/v1/system/readiness
+Authorization: {{authkey}}
+HTTP 200
+
+GET http://localhost:9090/plugins/restapi/v1/system/readiness/cluster
+Authorization: {{authkey}}
+HTTP 200
+
+GET http://localhost:9090/plugins/restapi/v1/system/readiness/connections
+Authorization: {{authkey}}
+HTTP 200
+
+GET http://localhost:9090/plugins/restapi/v1/system/readiness/plugins
+Authorization: {{authkey}}
+HTTP 200
+
+GET http://localhost:9090/plugins/restapi/v1/system/readiness/server
+Authorization: {{authkey}}
+HTTP 200
+
+GET http://localhost:9090/plugins/restapi/v1/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
+
+POST http://localhost:9090/plugins/restapi/v1/system/properties
+Authorization: {{authkey}}
+Content-Type: application/xml
+```
+
+
+```
+HTTP 201
+
+GET http://localhost:9090/plugins/restapi/v1/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
+
+PUT http://localhost:9090/plugins/restapi/v1/system/properties/test.key
+Authorization: {{authkey}}
+Content-Type: application/xml
+```
+
+
+```
+HTTP 200
+
+PUT http://localhost:9090/plugins/restapi/v1/system/properties/wrong.key
+Authorization: {{authkey}}
+Content-Type: application/xml
+```
+
+
+```
+HTTP 404
+
+PUT http://localhost:9090/plugins/restapi/v1/system/properties/test.key
+Authorization: {{authkey}}
+Content-Type: application/xml
+```
+
+
+```
+HTTP 400
+
+DELETE http://localhost:9090/plugins/restapi/v1/system/properties/test.key
+Authorization: {{authkey}}
+HTTP 200
\ No newline at end of file
diff --git a/test/test.env b/test/test.env
new file mode 100644
index 000000000..6d4bd1e85
--- /dev/null
+++ b/test/test.env
@@ -0,0 +1,2 @@
+host=http://localhost:9090
+authkey=potato
\ No newline at end of file
diff --git a/test/users.hurl b/test/users.hurl
new file mode 100644
index 000000000..8e450aae6
--- /dev/null
+++ b/test/users.hurl
@@ -0,0 +1,110 @@
+GET http://localhost:9090/plugins/restapi/v1/users
+Authorization: {{authkey}}
+HTTP 200
+[Asserts]
+xpath "/users/user" count == 3
+xpath "/users/user[username='admin']" 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
+Authorization: {{authkey}}
+HTTP 200
+[Asserts]
+xpath "/users/user" count == 1
+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
+
+POST http://localhost:9090/plugins/restapi/v1/users
+Authorization: {{authkey}}
+Content-Type: application/xml
+```
+
+
+ jeanluc
+ Jean-Luc
+ jlp@example.com
+ makeitso
+
+
+
+
+```
+HTTP 201
+
+GET http://localhost:9090/plugins/restapi/v1/users?propertyKey=tea&propertyValue=earlgreyhot
+Authorization: {{authkey}}
+HTTP 200
+[Asserts]
+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
+
+PUT http://localhost:9090/plugins/restapi/v1/users/jeanluc
+Authorization: {{authkey}}
+Content-Type: application/xml
+```
+
+
+ jeanluc
+ Jean-Luc
+ jeanluc@example.com
+
+
+
+
+```
+HTTP 200
+
+
+GET http://localhost:9090/plugins/restapi/v1/users/jeanluc
+Authorization: {{authkey}}
+HTTP 200
+[Asserts]
+xpath "/user" count == 1
+xpath "string(/user/username)" == "jeanluc"
+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
+Authorization: {{authkey}}
+HTTP 200
+
+GET http://localhost:9090/plugins/restapi/v1/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