aboutsummaryrefslogtreecommitdiffstats
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/core_configmanager_spec.lua4
-rw-r--r--spec/core_storagemanager_spec.lua195
-rw-r--r--spec/inputs/http/httpstream-chunked-test.txt15
-rw-r--r--spec/muc_util_spec.lua22
-rw-r--r--spec/net_http_parser_spec.lua127
-rw-r--r--spec/net_websocket_frames_spec.lua37
-rw-r--r--spec/scansion/basic_message.scs6
-rw-r--r--spec/scansion/blocking.scs8
-rw-r--r--spec/scansion/extdisco.scs57
-rw-r--r--spec/scansion/keep_full_sub_req.scs58
-rw-r--r--spec/scansion/lastactivity.scs45
-rw-r--r--spec/scansion/muc_create_destroy.scs316
-rw-r--r--spec/scansion/muc_members_only_change.scs2
-rw-r--r--spec/scansion/muc_nickname_change.scs127
-rw-r--r--spec/scansion/muc_password.scs2
-rw-r--r--spec/scansion/muc_register.scs14
-rw-r--r--spec/scansion/muc_show_offline.scs544
-rw-r--r--spec/scansion/muc_subject_issue_667.scs129
-rw-r--r--spec/scansion/presence_preapproval.scs74
-rw-r--r--spec/scansion/prosody.cfg.lua66
-rw-r--r--spec/scansion/pubsub_basic.scs2
-rw-r--r--spec/scansion/pubsub_preconditions.scs234
-rw-r--r--spec/scansion/server_contact_info.scs56
-rw-r--r--spec/scansion/uptime.scs21
-rw-r--r--spec/scansion/version.scs27
-rw-r--r--spec/util_array_spec.lua155
-rw-r--r--spec/util_async_spec.lua2
-rw-r--r--spec/util_dataforms_spec.lua62
-rw-r--r--spec/util_error_spec.lua146
-rw-r--r--spec/util_events_spec.lua38
-rw-r--r--spec/util_format_spec.lua5
-rw-r--r--spec/util_hashes_spec.lua55
-rw-r--r--spec/util_hashring_spec.lua85
-rw-r--r--spec/util_hmac_spec.lua106
-rw-r--r--spec/util_http_spec.lua24
-rw-r--r--spec/util_human_io_spec.lua29
-rw-r--r--spec/util_human_units_spec.lua15
-rw-r--r--spec/util_indexedbheap_spec.lua15
-rw-r--r--spec/util_interpolation_spec.lua61
-rw-r--r--spec/util_jid_spec.lua50
-rw-r--r--spec/util_json_spec.lua10
-rw-r--r--spec/util_jwt_spec.lua20
-rw-r--r--spec/util_promise_spec.lua78
-rw-r--r--spec/util_pubsub_spec.lua34
-rw-r--r--spec/util_queue_spec.lua37
-rw-r--r--spec/util_ringbuffer_spec.lua103
-rw-r--r--spec/util_rsm_spec.lua89
-rw-r--r--spec/util_sasl_spec.lua43
-rw-r--r--spec/util_stanza_spec.lua146
-rw-r--r--spec/util_table_spec.lua17
-rw-r--r--spec/util_throttle_spec.lua2
51 files changed, 3545 insertions, 70 deletions
diff --git a/spec/core_configmanager_spec.lua b/spec/core_configmanager_spec.lua
index afb7d492..7958ec6b 100644
--- a/spec/core_configmanager_spec.lua
+++ b/spec/core_configmanager_spec.lua
@@ -9,7 +9,9 @@ describe("core.configmanager", function()
configmanager.set("*", "testkey1", 321);
assert.are.equal(321, configmanager.get("*", "testkey1"), "Retrieving a set global key");
- assert.are.equal(321, configmanager.get("example.com", "testkey1"), "Retrieving a set key of undefined host, of which only a globally set one exists");
+ assert.are.equal(321, configmanager.get("example.com", "testkey1"),
+ "Retrieving a set key of undefined host, of which only a globally set one exists"
+ );
configmanager.set("example.com", ""); -- Creates example.com host in config
assert.are.equal(321, configmanager.get("example.com", "testkey1"), "Retrieving a set key, of which only a globally set one exists");
diff --git a/spec/core_storagemanager_spec.lua b/spec/core_storagemanager_spec.lua
index a0a8b5ef..d07dc4ec 100644
--- a/spec/core_storagemanager_spec.lua
+++ b/spec/core_storagemanager_spec.lua
@@ -1,4 +1,4 @@
-local unpack = table.unpack or unpack;
+local unpack = table.unpack or unpack; -- luacheck: ignore 113
local server = require "net.server_select";
package.loaded["net.server"] = server;
@@ -90,6 +90,112 @@ describe("storagemanager", function ()
end);
end);
+ describe("map stores", function ()
+ -- These tests rely on being executed in order, disable any order
+ -- randomization for this block
+ randomize(false);
+
+ local store, kv_store;
+ it("may be opened", function ()
+ store = assert(sm.open(test_host, "test-map", "map"));
+ end);
+
+ it("may be opened as a keyval store", function ()
+ kv_store = assert(sm.open(test_host, "test-map", "keyval"));
+ end);
+
+ it("may set a specific key for a user", function ()
+ assert(store:set("user9999", "foo", "bar"));
+ assert.same(kv_store:get("user9999"), { foo = "bar" });
+ end);
+
+ it("may get a specific key for a user", function ()
+ assert.equal("bar", store:get("user9999", "foo"));
+ end);
+
+ it("may find all users with a specific key", function ()
+ assert.is_function(store.get_all);
+ assert(store:set("user9999b", "bar", "bar"));
+ assert(store:set("user9999c", "foo", "blah"));
+ local ret, err = store:get_all("foo");
+ assert.is_nil(err);
+ assert.same({ user9999 = "bar", user9999c = "blah" }, ret);
+ end);
+
+ it("rejects empty or non-string keys to get_all", function ()
+ assert.is_function(store.get_all);
+ do
+ local ret, err = store:get_all("");
+ assert.is_nil(ret);
+ assert.is_not_nil(err);
+ end
+ do
+ local ret, err = store:get_all(true);
+ assert.is_nil(ret);
+ assert.is_not_nil(err);
+ end
+ end);
+
+ it("rejects empty or non-string keys to delete_all", function ()
+ assert.is_function(store.delete_all);
+ do
+ local ret, err = store:delete_all("");
+ assert.is_nil(ret);
+ assert.is_not_nil(err);
+ end
+ do
+ local ret, err = store:delete_all(true);
+ assert.is_nil(ret);
+ assert.is_not_nil(err);
+ end
+ end);
+
+ it("may delete all instances of a specific key", function ()
+ assert.is_function(store.delete_all);
+ assert(store:set("user9999b", "foo", "hello"));
+
+ assert(store:delete_all("bar"));
+ -- Ensure key was deleted
+ do
+ local ret, err = store:get("user9999b", "bar");
+ assert.is_nil(ret);
+ assert.is_nil(err);
+ end
+ -- Ensure other users/keys are intact
+ do
+ local ret, err = store:get("user9999", "foo");
+ assert.equal("bar", ret);
+ assert.is_nil(err);
+ end
+ do
+ local ret, err = store:get("user9999b", "foo");
+ assert.equal("hello", ret);
+ assert.is_nil(err);
+ end
+ do
+ local ret, err = store:get("user9999c", "foo");
+ assert.equal("blah", ret);
+ assert.is_nil(err);
+ end
+ end);
+
+ it("may remove data for a specific key for a user", function ()
+ assert(store:set("user9999", "foo", nil));
+ do
+ local ret, err = store:get("user9999", "foo");
+ assert.is_nil(ret);
+ assert.is_nil(err);
+ end
+
+ assert(store:set("user9999b", "foo", nil));
+ do
+ local ret, err = store:get("user9999b", "foo");
+ assert.is_nil(ret);
+ assert.is_nil(err);
+ end
+ end);
+ end);
+
describe("archive stores", function ()
randomize(false);
@@ -100,7 +206,8 @@ describe("storagemanager", function ()
local test_stanza = st.stanza("test", { xmlns = "urn:example:foo" })
:tag("foo"):up()
- :tag("foo"):up();
+ :tag("foo"):up()
+ :reset();
local test_time = 1539204123;
local test_data = {
@@ -108,17 +215,22 @@ describe("storagemanager", function ()
{ nil, test_stanza, test_time+1, "contact2@example.com" };
{ nil, test_stanza, test_time+2, "contact2@example.com" };
{ nil, test_stanza, test_time-1, "contact2@example.com" };
+ { nil, test_stanza, test_time-1, "contact3@example.com" };
+ { nil, test_stanza, test_time+0, "contact3@example.com" };
+ { nil, test_stanza, test_time+1, "contact3@example.com" };
};
it("can be added to", function ()
for _, data_item in ipairs(test_data) do
- local ok = archive:append("user", unpack(data_item, 1, 4));
- assert.truthy(ok);
+ local id = archive:append("user", unpack(data_item, 1, 4));
+ assert.truthy(id);
+ data_item[1] = id;
end
end);
describe("can be queried", function ()
it("for all items", function ()
+ -- luacheck: ignore 211/err
local data, err = archive:find("user", {});
assert.truthy(data);
local count = 0;
@@ -135,6 +247,7 @@ describe("storagemanager", function ()
end);
it("by JID", function ()
+ -- luacheck: ignore 211/err
local data, err = archive:find("user", {
with = "contact@example.com";
});
@@ -153,6 +266,7 @@ describe("storagemanager", function ()
end);
it("by time (end)", function ()
+ -- luacheck: ignore 211/err
local data, err = archive:find("user", {
["end"] = test_time;
});
@@ -167,10 +281,11 @@ describe("storagemanager", function ()
assert.equal(2, #item.tags);
assert(test_time >= when);
end
- assert.equal(2, count);
+ assert.equal(4, count);
end);
it("by time (start)", function ()
+ -- luacheck: ignore 211/err
local data, err = archive:find("user", {
["start"] = test_time;
});
@@ -185,10 +300,11 @@ describe("storagemanager", function ()
assert.equal(2, #item.tags);
assert(test_time <= when);
end
- assert.equal(#test_data -1, count);
+ assert.equal(#test_data - 2, count);
end);
it("by time (start+end)", function ()
+ -- luacheck: ignore 211/err
local data, err = archive:find("user", {
["start"] = test_time;
["end"] = test_time+1;
@@ -205,7 +321,45 @@ describe("storagemanager", function ()
assert(when >= test_time, ("%d >= %d"):format(when, test_time));
assert(when <= test_time+1, ("%d <= %d"):format(when, test_time+1));
end
- assert.equal(2, count);
+ assert.equal(4, count);
+ end);
+
+ it("by id (after)", function ()
+ -- luacheck: ignore 211/err
+ local data, err = archive:find("user", {
+ ["after"] = test_data[2][1];
+ });
+ assert.truthy(data);
+ local count = 0;
+ for id, item in data do
+ count = count + 1;
+ assert.truthy(id);
+ assert.equal(test_data[2+count][1], id);
+ assert(st.is_stanza(item));
+ assert.equal("test", item.name);
+ assert.equal("urn:example:foo", item.attr.xmlns);
+ assert.equal(2, #item.tags);
+ end
+ assert.equal(5, count);
+ end);
+
+ it("by id (before)", function ()
+ -- luacheck: ignore 211/err
+ local data, err = archive:find("user", {
+ ["before"] = test_data[4][1];
+ });
+ assert.truthy(data);
+ local count = 0;
+ for id, item in data do
+ count = count + 1;
+ assert.truthy(id);
+ assert.equal(test_data[count][1], id);
+ assert(st.is_stanza(item));
+ assert.equal("test", item.name);
+ assert.equal("urn:example:foo", item.attr.xmlns);
+ assert.equal(2, #item.tags);
+ end
+ assert.equal(3, count);
end);
end);
@@ -239,6 +393,7 @@ describe("storagemanager", function ()
end);
it("can be purged", function ()
+ -- luacheck: ignore 211/err
local ok, err = archive:delete("user");
assert.truthy(ok);
local data, err = archive:find("user", {
@@ -326,6 +481,32 @@ describe("storagemanager", function ()
assert.equal(2, count);
assert(archive:delete("user-issue1073"));
end);
+
+ it("can be treated as a map store", function ()
+ assert.falsy(archive:get("mapuser", "no-such-id"));
+ assert.falsy(archive:set("mapuser", "no-such-id", test_stanza));
+
+ local id = archive:append("mapuser", nil, test_stanza, test_time, "contact@example.com");
+ do
+ local stanza_roundtrip, when, with = archive:get("mapuser", id);
+ assert.same(test_stanza, stanza_roundtrip, "same stanza is returned");
+ assert.equal(test_time, when, "same 'when' is returned");
+ assert.equal("contact@example.com", with, "same 'with' is returned");
+ end
+
+ local replacement_stanza = st.stanza("test", { xmlns = "urn:example:foo" })
+ :tag("bar"):up()
+ :reset();
+ assert(archive:set("mapuser", id, replacement_stanza, test_time+1));
+
+ do
+ local replaced, when, with = archive:get("mapuser", id);
+ assert.same(replacement_stanza, replaced, "replaced stanza is returned");
+ assert.equal(test_time+1, when, "modified 'when' is returned");
+ assert.equal("contact@example.com", with, "original 'with' is returned");
+ end
+ end);
+
end);
end);
end
diff --git a/spec/inputs/http/httpstream-chunked-test.txt b/spec/inputs/http/httpstream-chunked-test.txt
new file mode 100644
index 00000000..56efa067
--- /dev/null
+++ b/spec/inputs/http/httpstream-chunked-test.txt
@@ -0,0 +1,15 @@
+HTTP/1.1 200 OK
+Cache-Control: max-age=0, must-revalidate, private
+Content-Type: application/json
+Date: Fri, 21 Aug 2020 12:18:51 GMT
+Expires: Fri, 21 Aug 2020 12:18:51 GMT
+Server: Apache/2.4.38 (Debian)
+Set-Cookie: PHPSESSID=00000000000000000000000000; path=/; HttpOnly
+Strict-Transport-Security: max-age=29030400
+X-Powered-By: PHP/7.4.7
+Transfer-Encoding: chunked
+
+2b4d
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+0
+
diff --git a/spec/muc_util_spec.lua b/spec/muc_util_spec.lua
index cef68e80..3b2da4d0 100644
--- a/spec/muc_util_spec.lua
+++ b/spec/muc_util_spec.lua
@@ -3,11 +3,23 @@ local muc_util;
local st = require "util.stanza";
do
- local old_pp = package.path;
- package.path = "./?.lib.lua;"..package.path;
- muc_util = require "plugins.muc.util";
- package.path = old_pp;
-end
+ -- XXX Hack for lack of a mock moduleapi
+ local env = setmetatable({
+ module = {
+ _shared = {};
+ -- Close enough to the real module:shared() for our purposes here
+ shared = function (self, name)
+ local t = self._shared[name];
+ if t == nil then
+ t = {};
+ self._shared[name] = t;
+ end
+ return t;
+ end;
+ }
+ }, { __index = _ENV or _G });
+ muc_util = require "util.envload".envloadfile("plugins/muc/util.lib.lua", env)();
+ end
describe("muc/util", function ()
describe("filter_muc_x()", function ()
diff --git a/spec/net_http_parser_spec.lua b/spec/net_http_parser_spec.lua
index 6bba087c..f71cad20 100644
--- a/spec/net_http_parser_spec.lua
+++ b/spec/net_http_parser_spec.lua
@@ -1,16 +1,76 @@
-local httpstreams = { [[
+local http_parser = require "net.http.parser";
+local sha1 = require "util.hashes".sha1;
+
+local parser_input_bytes = 3;
+
+local function CRLF(s)
+ return (s:gsub("\n", "\r\n"));
+end
+
+local function test_stream(stream, expect)
+ local success_cb = spy.new(function (packet)
+ assert.is_table(packet);
+ if packet.body ~= false then
+ assert.is_equal(expect.body, packet.body);
+ end
+ end);
+
+ local parser = http_parser.new(success_cb, error, stream:sub(1,4) == "HTTP" and "client" or "server")
+ for chunk in stream:gmatch("."..string.rep(".?", parser_input_bytes-1)) do
+ parser:feed(chunk);
+ end
+
+ assert.spy(success_cb).was_called(expect.count or 1);
+end
+
+
+describe("net.http.parser", function()
+ describe("parser", function()
+ it("should handle requests with no content-length or body", function ()
+ test_stream(
+CRLF[[
GET / HTTP/1.1
Host: example.com
-]], [[
+]],
+ {
+ body = "";
+ }
+ );
+ end);
+
+ it("should handle responses with empty body", function ()
+ test_stream(
+CRLF[[
HTTP/1.1 200 OK
Content-Length: 0
-]], [[
+]],
+ {
+ body = "";
+ }
+ );
+ end);
+
+ it("should handle simple responses", function ()
+ test_stream(
+
+CRLF[[
HTTP/1.1 200 OK
Content-Length: 7
Hello
+]],
+ {
+ body = "Hello\r\n", count = 1;
+ }
+ );
+ end);
+
+ it("should handle chunked encoding in responses", function ()
+ test_stream(
+
+CRLF[[
HTTP/1.1 200 OK
Transfer-Encoding: chunked
@@ -25,28 +85,51 @@ o
0
-]]
-}
+]],
+ {
+ body = "Hello", count = 2;
+ }
+ );
+ end);
+ it("should handle a stream of responses", function ()
+ test_stream(
-local http_parser = require "net.http.parser";
+CRLF[[
+HTTP/1.1 200 OK
+Content-Length: 5
-describe("net.http.parser", function()
- describe("#new()", function()
- it("should work", function()
- for _, stream in ipairs(httpstreams) do
- local success;
- local function success_cb(packet)
- success = true;
- end
- stream = stream:gsub("\n", "\r\n");
- local parser = http_parser.new(success_cb, error, stream:sub(1,4) == "HTTP" and "client" or "server")
- for chunk in stream:gmatch("..?.?") do
- parser:feed(chunk);
- end
-
- assert.is_true(success);
- end
+Hello
+HTTP/1.1 200 OK
+Transfer-Encoding: chunked
+
+1
+H
+1
+e
+2
+ll
+1
+o
+0
+
+
+]],
+ {
+ body = "Hello", count = 3;
+ }
+ );
end);
end);
+
+ it("should handle large chunked responses", function ()
+ local data = io.open("spec/inputs/http/httpstream-chunked-test.txt", "rb"):read("*a");
+
+ -- Just a sanity check... text editors and things may mess with line endings, etc.
+ assert.equal("25930f021785ae14053a322c2dbc1897c3769720", sha1(data, true), "test data malformed");
+
+ test_stream(data, {
+ body = string.rep("~", 11085), count = 2;
+ });
+ end);
end);
diff --git a/spec/net_websocket_frames_spec.lua b/spec/net_websocket_frames_spec.lua
index d4df3a54..7eed73c9 100644
--- a/spec/net_websocket_frames_spec.lua
+++ b/spec/net_websocket_frames_spec.lua
@@ -32,6 +32,37 @@ describe("net.websocket.frames", function ()
["RSV2"] = false;
["RSV3"] = false;
};
+ masked_data = {
+ ["opcode"] = 0;
+ ["length"] = 5;
+ ["data"] = "hello";
+ ["FIN"] = true;
+ ["MASK"] = true;
+ ["RSV1"] = false;
+ ["RSV2"] = false;
+ ["RSV3"] = false;
+ ["key"] = { 0x20, 0x20, 0x20, 0x20, };
+ };
+ ping = {
+ ["opcode"] = 0x9;
+ ["length"] = 4;
+ ["data"] = "ping";
+ ["FIN"] = true;
+ ["MASK"] = false;
+ ["RSV1"] = false;
+ ["RSV2"] = false;
+ ["RSV3"] = false;
+ };
+ pong = {
+ ["opcode"] = 0xa;
+ ["length"] = 4;
+ ["data"] = "pong";
+ ["FIN"] = true;
+ ["MASK"] = false;
+ ["RSV1"] = false;
+ ["RSV2"] = false;
+ ["RSV3"] = false;
+ };
}
describe("build", function ()
@@ -40,6 +71,9 @@ describe("net.websocket.frames", function ()
assert.equal("\0\0", build(test_frames.simple_empty));
assert.equal("\0\5hello", build(test_frames.simple_data));
assert.equal("\128\0", build(test_frames.simple_fin));
+ assert.equal("\128\133 HELLO", build(test_frames.masked_data));
+ assert.equal("\137\4ping", build(test_frames.ping));
+ assert.equal("\138\4pong", build(test_frames.pong));
end);
end);
@@ -49,6 +83,9 @@ describe("net.websocket.frames", function ()
assert.same(test_frames.simple_empty, parse("\0\0"));
assert.same(test_frames.simple_data, parse("\0\5hello"));
assert.same(test_frames.simple_fin, parse("\128\0"));
+ assert.same(test_frames.masked_data, parse("\128\133 HELLO"));
+ assert.same(test_frames.ping, parse("\137\4ping"));
+ assert.same(test_frames.pong, parse("\138\4pong"));
end);
end);
diff --git a/spec/scansion/basic_message.scs b/spec/scansion/basic_message.scs
index fb21c465..1258dbf5 100644
--- a/spec/scansion/basic_message.scs
+++ b/spec/scansion/basic_message.scs
@@ -79,7 +79,7 @@ Juliet's phone receives:
<message from="${Romeo's full JID}" type="chat">
<body>Hello Juliet, are you there?</body>
<delay xmlns='urn:xmpp:delay' from='localhost' stamp='{scansion:any}' />
- </message>
+ </message>
# Romeo sends another bare-JID message, it should be delivered
# instantly to Juliet's phone
@@ -92,7 +92,7 @@ Romeo sends:
Juliet's phone receives:
<message from="${Romeo's full JID}" type="chat">
<body>Oh, hi!</body>
- </message>
+ </message>
# Juliet's laptop goes online, but with a negative priority
@@ -122,7 +122,7 @@ Romeo sends:
Juliet's phone receives:
<message from="${Romeo's full JID}" type="chat">
<body>How are you?</body>
- </message>
+ </message>
# Romeo sends direct to Juliet's full JID, and she should receive it
diff --git a/spec/scansion/blocking.scs b/spec/scansion/blocking.scs
index 6a9f199e..5bb5a41b 100644
--- a/spec/scansion/blocking.scs
+++ b/spec/scansion/blocking.scs
@@ -145,16 +145,16 @@ Juliet receives:
</message>
# Bye
-Juliet disconnects
-
Juliet sends:
<presence type="unavailable"/>
+Juliet disconnects
+
Romeo receives:
<presence from="${Juliet's full JID}" to="${Romeo's JID}" type="unavailable"/>
-Romeo disconnects
-
Romeo sends:
<presence type="unavailable"/>
+Romeo disconnects
+
diff --git a/spec/scansion/extdisco.scs b/spec/scansion/extdisco.scs
new file mode 100644
index 00000000..fd73c9da
--- /dev/null
+++ b/spec/scansion/extdisco.scs
@@ -0,0 +1,57 @@
+# XEP-0215: External Service Discovery
+
+[Client] Romeo
+ password: password
+ jid: user@localhost/mFquWxSr
+
+-----
+
+Romeo connects
+
+Romeo sends:
+ <iq type='get' xml:lang='sv' id='lx2' to='localhost'>
+ <services xmlns='urn:xmpp:extdisco:2'/>
+ </iq>
+
+Romeo receives:
+ <iq type='result' id='lx2' from='localhost'>
+ <services xmlns='urn:xmpp:extdisco:2'>
+ <service host='default.example' transport='udp' port='9876' type='stun'/>
+ <service port='9876' type='turn' restricted='1' password='yHYYBDN7M3mdlug0LTdJbW0GvvQ=' transport='udp' host='default.example' username='1219525744'/>
+ <service port='9876' type='turn' restricted='1' password='1Uc6QfrDhIlbK97rGCUQ/cUICxs=' transport='udp' host='default.example' username='1219525744'/>
+ <service port='2121' type='ftp' restricted='1' password='password' transport='tcp' host='default.example' username='john'/>
+ <service port='21' type='ftp' restricted='1' password='password' transport='tcp' host='ftp.example.com' username='john'/>
+ </services>
+ </iq>
+
+Romeo sends:
+ <iq type='get' xml:lang='sv' id='lx3' to='localhost'>
+ <services xmlns='urn:xmpp:extdisco:2' type='ftp'/>
+ </iq>
+
+Romeo receives:
+ <iq type='result' id='lx3' from='localhost'>
+ <services xmlns='urn:xmpp:extdisco:2'>
+ <service port='2121' type='ftp' restricted='1' password='password' transport='tcp' host='default.example' username='john'/>
+ <service port='21' type='ftp' restricted='1' password='password' transport='tcp' host='ftp.example.com' username='john'/>
+ </services>
+ </iq>
+
+Romeo sends:
+ <iq type='get' xml:lang='sv' id='lx4' to='localhost'>
+ <credentials xmlns='urn:xmpp:extdisco:2'>
+ <service host='default.example' type='turn'/>
+ </credentials>
+ </iq>
+
+Romeo receives:
+ <iq type='result' id='lx4' from='localhost'>
+ <credentials xmlns='urn:xmpp:extdisco:2'>
+ <service port='9876' type='turn' restricted='1' password='yHYYBDN7M3mdlug0LTdJbW0GvvQ=' transport='udp' host='default.example' username='1219525744'/>
+ <service port='9876' type='turn' restricted='1' password='1Uc6QfrDhIlbK97rGCUQ/cUICxs=' transport='udp' host='default.example' username='1219525744'/>
+ </credentials>
+ </iq>
+
+Romeo disconnects
+
+# recording ended on 2020-07-18T16:47:57Z
diff --git a/spec/scansion/keep_full_sub_req.scs b/spec/scansion/keep_full_sub_req.scs
new file mode 100644
index 00000000..41ffec0d
--- /dev/null
+++ b/spec/scansion/keep_full_sub_req.scs
@@ -0,0 +1,58 @@
+# server MUST keep a record of the complete presence stanza comprising the subscription request (#689)
+
+[Client] Alice
+ jid: pars-a@localhost
+ password: password
+
+[Client] Bob
+ jid: pars-b@localhost
+ password: password
+
+[Client] Bob's phone
+ jid: pars-b@localhost/phone
+ password: password
+
+---------
+
+Alice connects
+
+Alice sends:
+ <presence to="${Bob's JID}" type="subscribe">
+ <preauth xmlns="urn:xmpp:pars:0" token="1tMFqYDdKhfe2pwp" />
+ </presence>
+
+Alice disconnects
+
+Bob connects
+
+Bob sends:
+ <presence/>
+
+Bob receives:
+ <presence from="${Bob's full JID}"/>
+
+Bob receives:
+ <presence from="${Alice's JID}" type="subscribe">
+ <preauth xmlns="urn:xmpp:pars:0" token="1tMFqYDdKhfe2pwp" />
+ </presence>
+
+Bob disconnects
+
+# Works if they reconnect too
+
+Bob's phone connects
+
+Bob's phone sends:
+ <presence/>
+
+Bob's phone receives:
+ <presence from="${Bob's phone's full JID}"/>
+
+
+Bob's phone receives:
+ <presence from="${Alice's JID}" type="subscribe">
+ <preauth xmlns="urn:xmpp:pars:0" token="1tMFqYDdKhfe2pwp" />
+ </presence>
+
+Bob's phone disconnects
+
diff --git a/spec/scansion/lastactivity.scs b/spec/scansion/lastactivity.scs
new file mode 100644
index 00000000..44f4e516
--- /dev/null
+++ b/spec/scansion/lastactivity.scs
@@ -0,0 +1,45 @@
+# XEP-0012: Last Activity / mod_lastactivity
+
+[Client] Romeo
+ jid: romeo@localhost
+ password: password
+
+-----
+
+Romeo connects
+
+Romeo sends:
+ <presence>
+ <status>Hello</status>
+ </presence>
+
+Romeo receives:
+ <presence from="${Romeo's full JID}">
+ <status>Hello</status>
+ </presence>
+
+Romeo sends:
+ <presence type="unavailable">
+ <status>Goodbye</status>
+ </presence>
+
+Romeo receives:
+ <presence from="${Romeo's full JID}" type="unavailable">
+ <status>Goodbye</status>
+ </presence>
+
+# mod_lastlog saves time + status message from the last unavailable presence
+
+Romeo sends:
+ <iq id='a' type='get'>
+ <query xmlns='jabber:iq:last'/>
+ </iq>
+
+Romeo receives:
+ <iq type='result' id='a'>
+ <query xmlns='jabber:iq:last' seconds='0'>Goodbye</query>
+ </iq>
+
+Romeo disconnects
+
+# recording ended on 2020-04-20T14:39:47Z
diff --git a/spec/scansion/muc_create_destroy.scs b/spec/scansion/muc_create_destroy.scs
new file mode 100644
index 00000000..789d4c41
--- /dev/null
+++ b/spec/scansion/muc_create_destroy.scs
@@ -0,0 +1,316 @@
+# MUC creation, basic messages and destruction
+
+[Client] Romeo
+ jid: romeo@localhost/mK0dD6Ha
+ password: password
+
+[Client] Juliet
+ jid: juliet@localhost/lVwkim_k
+ password: password
+
+[Client] Admin
+ jid: admin@localhost/DfNgg9VE
+ password: password
+
+-----
+
+Romeo connects
+
+Romeo sends:
+ <presence to="garden@conference.localhost/romeo">
+ <x xmlns="http://jabber.org/protocol/muc"/>
+ </presence>
+
+Romeo receives:
+ <presence from="garden@conference.localhost/romeo">
+ <x xmlns="vcard-temp:x:update">
+ <photo/>
+ </x>
+ <x xmlns="http://jabber.org/protocol/muc#user">
+ <status code="201"/>
+ <item affiliation="owner" jid="${Romeo's full JID}" role="moderator"/>
+ <status code="110"/>
+ </x>
+ </presence>
+
+Romeo receives:
+ <message from="garden@conference.localhost" type="groupchat">
+ <subject/>
+ </message>
+
+Romeo sends:
+ <iq to="garden@conference.localhost" id="lx3" type="set">
+ <query xmlns="http://jabber.org/protocol/muc#owner">
+ <x type="submit" xmlns="jabber:x:data"/>
+ </query>
+ </iq>
+
+Romeo receives:
+ <iq id="lx3" type="result" from="garden@conference.localhost"/>
+
+Juliet connects
+
+Romeo sends:
+ <message to="garden@conference.localhost" type="groupchat" id="rm1">
+ <body>Where are thou my Juliet?</body>
+ </message>
+
+Romeo receives:
+ <message type="groupchat" from="garden@conference.localhost/romeo" id="rm1">
+ <body>Where are thou my Juliet?</body>
+ </message>
+
+Juliet sends:
+ <presence to="garden@conference.localhost/juliet">
+ <x xmlns="http://jabber.org/protocol/muc"/>
+ </presence>
+
+Juliet receives:
+ <presence from="garden@conference.localhost/romeo">
+ <x xmlns="vcard-temp:x:update">
+ <photo/>
+ </x>
+ <x xmlns="http://jabber.org/protocol/muc#user">
+ <item affiliation="owner" role="moderator"/>
+ </x>
+ </presence>
+
+Juliet receives:
+ <presence from="garden@conference.localhost/juliet">
+ <x xmlns="vcard-temp:x:update">
+ <photo/>
+ </x>
+ <x xmlns="http://jabber.org/protocol/muc#user">
+ <item affiliation="none" jid="${Juliet's full JID}" role="participant"/>
+ <status code="110"/>
+ </x>
+ </presence>
+
+Juliet receives:
+ <message from="garden@conference.localhost/romeo" id="rm1" type="groupchat">
+ <body>Where are thou my Juliet?</body>
+ <delay stamp="{scansion:any}" xmlns="urn:xmpp:delay" from="garden@conference.localhost"/>
+ </message>
+
+Juliet receives:
+ <message from="garden@conference.localhost" type="groupchat">
+ <subject/>
+ </message>
+
+Romeo receives:
+ <presence from="garden@conference.localhost/juliet">
+ <x xmlns="vcard-temp:x:update">
+ <photo/>
+ </x>
+ <x xmlns="http://jabber.org/protocol/muc#user">
+ <item affiliation="none" jid="${Juliet's full JID}" role="participant"/>
+ </x>
+ </presence>
+
+Juliet sends:
+ <message to="garden@conference.localhost" type="groupchat" id="jm1">
+ <body>/me jumps out from behind a tree</body>
+ </message>
+
+Romeo receives:
+ <message type="groupchat" id="jm1" from="garden@conference.localhost/juliet">
+ <body>/me jumps out from behind a tree</body>
+ </message>
+
+Juliet receives:
+ <message type="groupchat" id="jm1" from="garden@conference.localhost/juliet">
+ <body>/me jumps out from behind a tree</body>
+ </message>
+
+Juliet sends:
+ <message to="garden@conference.localhost" type="groupchat" id="jm2">
+ <body>Here I am!</body>
+ </message>
+
+Romeo receives:
+ <message type="groupchat" id="jm2" from="garden@conference.localhost/juliet">
+ <body>Here I am!</body>
+ </message>
+
+Juliet receives:
+ <message type="groupchat" id="jm2" from="garden@conference.localhost/juliet">
+ <body>Here I am!</body>
+ </message>
+
+Romeo sends:
+ <message to="garden@conference.localhost" type="groupchat" id="rm2">
+ <body>What is this place?</body>
+ </message>
+
+Romeo receives:
+ <message type="groupchat" id="rm2" from="garden@conference.localhost/romeo">
+ <body>What is this place?</body>
+ </message>
+
+Juliet receives:
+ <message type="groupchat" id="rm2" from="garden@conference.localhost/romeo">
+ <body>What is this place?</body>
+ </message>
+
+Juliet sends:
+ <message to="garden@conference.localhost" type="groupchat" id="jm3">
+ <body>I think we&apos;re in a script!</body>
+ </message>
+
+Romeo receives:
+ <message type="groupchat" id="jm3" from="garden@conference.localhost/juliet">
+ <body>I think we&apos;re in a script!</body>
+ </message>
+
+Juliet receives:
+ <message type="groupchat" id="jm3" from="garden@conference.localhost/juliet">
+ <body>I think we&apos;re in a script!</body>
+ </message>
+
+Romeo sends:
+ <message to="garden@conference.localhost" type="groupchat" id="rm3">
+ <body>Oh no! Does that mean our love is not real?!</body>
+ </message>
+
+Romeo receives:
+ <message type="groupchat" id="rm3" from="garden@conference.localhost/romeo">
+ <body>Oh no! Does that mean our love is not real?!</body>
+ </message>
+
+Juliet receives:
+ <message type="groupchat" id="rm3" from="garden@conference.localhost/romeo">
+ <body>Oh no! Does that mean our love is not real?!</body>
+ </message>
+
+Juliet sends:
+ <message to="garden@conference.localhost" type="groupchat" id="jm4">
+ <body>I refuse to accept this! Let&apos;s burn this place to the ground!</body>
+ </message>
+
+Romeo receives:
+ <message type="groupchat" id="jm4" from="garden@conference.localhost/juliet">
+ <body>I refuse to accept this! Let&apos;s burn this place to the ground!</body>
+ </message>
+
+Juliet receives:
+ <message type="groupchat" id="jm4" from="garden@conference.localhost/juliet">
+ <body>I refuse to accept this! Let&apos;s burn this place to the ground!</body>
+ </message>
+
+Romeo sends:
+ <message to="garden@conference.localhost" type="groupchat" id="rm4">
+ <body>Yes!</body>
+ </message>
+
+Romeo receives:
+ <message type="groupchat" id="rm4" from="garden@conference.localhost/romeo">
+ <body>Yes!</body>
+ </message>
+
+Juliet receives:
+ <message type="groupchat" id="rm4" from="garden@conference.localhost/romeo">
+ <body>Yes!</body>
+ </message>
+
+Romeo sends:
+ <iq to="garden@conference.localhost" id="lx4" type="set">
+ <query xmlns="http://jabber.org/protocol/muc#owner">
+ <destroy>
+ <reason>We refuse to live in this fantasy!</reason>
+ </destroy>
+ </query>
+ </iq>
+
+Juliet receives:
+ <presence from="garden@conference.localhost/juliet" type="unavailable">
+ <x xmlns="http://jabber.org/protocol/muc#user">
+ <destroy>
+ <reason>We refuse to live in this fantasy!</reason>
+ </destroy>
+ <item affiliation="none" jid="${Juliet's full JID}" role="none"/>
+ <status code="110"/>
+ </x>
+ </presence>
+
+Romeo receives:
+ <presence from="garden@conference.localhost/romeo" type="unavailable">
+ <x xmlns="http://jabber.org/protocol/muc#user">
+ <destroy>
+ <reason>We refuse to live in this fantasy!</reason>
+ </destroy>
+ <item affiliation="owner" jid="${Romeo's full JID}" role="none"/>
+ <status code="110"/>
+ </x>
+ </presence>
+
+Romeo receives:
+ <iq id="lx4" type="result" from="garden@conference.localhost"/>
+
+Juliet disconnects
+
+Romeo sends:
+ <presence to="elsewhere@conference.localhost/romeo">
+ <x xmlns="http://jabber.org/protocol/muc"/>
+ </presence>
+
+Romeo receives:
+ <presence from="elsewhere@conference.localhost/romeo">
+ <x xmlns="vcard-temp:x:update">
+ <photo/>
+ </x>
+ <x xmlns="http://jabber.org/protocol/muc#user">
+ <status code="201"/>
+ <item affiliation="owner" jid="${Romeo's full JID}" role="moderator"/>
+ <status code="110"/>
+ </x>
+ </presence>
+
+Romeo receives:
+ <message from="elsewhere@conference.localhost" type="groupchat">
+ <subject/>
+ </message>
+
+Romeo sends:
+ <iq to="elsewhere@conference.localhost" id="lx5" type="set">
+ <query xmlns="http://jabber.org/protocol/muc#owner">
+ <x type="submit" xmlns="jabber:x:data"/>
+ </query>
+ </iq>
+
+Romeo receives:
+ <iq id="lx5" type="result" from="elsewhere@conference.localhost"/>
+
+Admin connects
+
+Admin sends:
+ <iq id="destroy" type="set" to="conference.localhost">
+ <command xmlns="http://jabber.org/protocol/commands" node="http://prosody.im/protocol/muc#destroy">
+ <x xmlns="jabber:x:data">
+ <field var="rooms">
+ <value>elsewhere@conference.localhost</value>
+ </field>
+ </x>
+ </command>
+ </iq>
+
+Romeo receives:
+ <presence from="elsewhere@conference.localhost/romeo" type="unavailable">
+ <x xmlns="http://jabber.org/protocol/muc#user">
+ <destroy/>
+ <item affiliation="owner" jid="${Romeo's full JID}" role="none"/>
+ <status code="110"/>
+ </x>
+ </presence>
+
+Romeo disconnects
+
+Admin receives:
+ <iq id="destroy" type="result" from="conference.localhost">
+ <command xmlns="http://jabber.org/protocol/commands" node="http://prosody.im/protocol/muc#destroy" status="completed" sessionid="{scansion:any}">
+ <note type="info">The following rooms were destroyed:&#10;elsewhere@conference.localhost</note>
+ </command>
+ </iq>
+
+Admin disconnects
+
+# recording ended on 2019-08-31T13:45:32Z
diff --git a/spec/scansion/muc_members_only_change.scs b/spec/scansion/muc_members_only_change.scs
index dc40b5a0..a708dbfb 100644
--- a/spec/scansion/muc_members_only_change.scs
+++ b/spec/scansion/muc_members_only_change.scs
@@ -94,7 +94,7 @@ Romeo sends:
<item affiliation='none' jid="${Juliet's JID}" />
</query>
</iq>
-
+
# As a non-member, Juliet must now be removed from the room
Romeo receives:
<presence type='unavailable' from='room@conference.localhost/Juliet'>
diff --git a/spec/scansion/muc_nickname_change.scs b/spec/scansion/muc_nickname_change.scs
new file mode 100644
index 00000000..73f81203
--- /dev/null
+++ b/spec/scansion/muc_nickname_change.scs
@@ -0,0 +1,127 @@
+# MUC: Change nickname
+# Make sure a role is not reset, see #1466
+
+[Client] Romeo
+ jid: user@localhost
+ password: password
+
+[Client] Juliet
+ jid: user2@localhost
+ password: password
+
+-----
+
+Romeo connects
+
+Romeo sends:
+ <presence to="room@conference.localhost/Romeo">
+ <x xmlns="http://jabber.org/protocol/muc"/>
+ </presence>
+
+Romeo receives:
+ <presence from='room@conference.localhost/Romeo'>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <status code='201'/>
+ <item jid="${Romeo's full JID}" affiliation='owner' role='moderator'/>
+ <status code='110'/>
+ </x>
+ </presence>
+
+Romeo receives:
+ <message type='groupchat' from='room@conference.localhost'><subject/></message>
+
+Romeo sends:
+ <iq id='config1' to='room@conference.localhost' type='set'>
+ <query xmlns='http://jabber.org/protocol/muc#owner'>
+ <x xmlns='jabber:x:data' type='submit'>
+ <field var='FORM_TYPE'>
+ <value>http://jabber.org/protocol/muc#roomconfig</value>
+ </field>
+ <field var='muc#roomconfig_moderatedroom'>
+ <value>1</value>
+ </field>
+ </x>
+ </query>
+ </iq>
+
+Romeo receives:
+ <iq id="config1" from="room@conference.localhost" type="result"/>
+
+Juliet connects
+
+Juliet sends:
+ <presence to="room@conference.localhost/Juliet">
+ <x xmlns="http://jabber.org/protocol/muc"/>
+ </presence>
+
+Juliet receives:
+ <presence from='room@conference.localhost/Romeo'>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <item role='moderator' xmlns='http://jabber.org/protocol/muc#user' affiliation='owner'/>
+ </x>
+ </presence>
+
+Juliet receives:
+ <presence from='room@conference.localhost/Juliet'>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <item jid="${Juliet's full JID}" affiliation='none' role='visitor'/>
+ <status code='110'/>
+ </x>
+ </presence>
+
+
+Juliet receives:
+ <message type='groupchat' from='room@conference.localhost'><subject/></message>
+
+Romeo receives:
+ <presence from='room@conference.localhost/Juliet'>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <item affiliation="none" role="visitor" jid="${Juliet's full JID}"/>
+ </x>
+ </presence>
+
+Romeo sends:
+ <iq id='config1' to='room@conference.localhost' type='set'>
+ <query xmlns='http://jabber.org/protocol/muc#owner'>
+ <x xmlns='jabber:x:data' type='submit'>
+ <field var='FORM_TYPE'>
+ <value>http://jabber.org/protocol/muc#roomconfig</value>
+ </field>
+ <field var='muc#roomconfig_moderatedroom'>
+ <value>0</value>
+ </field>
+ </x>
+ </query>
+ </iq>
+
+Romeo receives:
+ <iq id="config1" from="room@conference.localhost" type="result"/>
+
+Juliet receives:
+ <message type='groupchat' from='room@conference.localhost'>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <status xmlns='http://jabber.org/protocol/muc#user' code='104'/>
+ </x>
+ </message>
+
+Juliet sends:
+ <presence to="room@conference.localhost/Juliet2">
+ </presence>
+
+Juliet receives:
+ <presence from='room@conference.localhost/Juliet' type='unavailable'>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <status code='303'/>
+ <item nick='Juliet2' jid="${Juliet's full JID}" affiliation='none' role='visitor'/>
+ <status code='110'/>
+ </x>
+ </presence>
+
+Juliet receives:
+ <presence from='room@conference.localhost/Juliet2'>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <item jid="${Juliet's full JID}" affiliation='none' role='visitor'/>
+ <status code='110'/>
+ </x>
+ </presence>
+
diff --git a/spec/scansion/muc_password.scs b/spec/scansion/muc_password.scs
index 82611183..63c821e0 100644
--- a/spec/scansion/muc_password.scs
+++ b/spec/scansion/muc_password.scs
@@ -58,7 +58,7 @@ Juliet sends:
Juliet receives:
<presence from="room@conference.localhost/Juliet" type="error">
- <error type="auth" code="401">
+ <error type="auth" code="401" by="room@conference.localhost">
<not-authorized xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
</error>
</presence>
diff --git a/spec/scansion/muc_register.scs b/spec/scansion/muc_register.scs
index a7e57177..9fcce688 100644
--- a/spec/scansion/muc_register.scs
+++ b/spec/scansion/muc_register.scs
@@ -100,7 +100,9 @@ Juliet receives:
<field type='hidden' var='FORM_TYPE'>
<value>http://jabber.org/protocol/muc#register</value>
</field>
- <field type='text-single' label='Nickname' var='muc#register_roomnick'/>
+ <field type='text-single' label='Nickname' var='muc#register_roomnick'>
+ <required/>
+ </field>
</x>
</query>
</iq>
@@ -175,7 +177,7 @@ Rosaline sends:
Rosaline receives:
<presence type='error' from='room@conference.localhost/Juliet'>
- <error type='cancel'>
+ <error type='cancel' by='room@conference.localhost'>
<conflict xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
</error>
<x xmlns='http://jabber.org/protocol/muc'/>
@@ -286,7 +288,7 @@ Rosaline sends:
Rosaline receives:
<presence type='error' from='room@conference.localhost/Juliet'>
- <error type='cancel'>
+ <error type='cancel' by='room@conference.localhost'>
<conflict xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
</error>
<x xmlns='http://jabber.org/protocol/muc'/>
@@ -326,7 +328,7 @@ Romeo receives:
</iq>
# Romeo updates his own registration
-
+
Romeo sends:
<iq id='jw81b36f' to='room@conference.localhost' type='get'>
<query xmlns='jabber:iq:register'/>
@@ -339,7 +341,9 @@ Romeo receives:
<field type='hidden' var='FORM_TYPE'>
<value>http://jabber.org/protocol/muc#register</value>
</field>
- <field type='text-single' label='Nickname' var='muc#register_roomnick'/>
+ <field type='text-single' label='Nickname' var='muc#register_roomnick'>
+ <required/>
+ </field>
</x>
</query>
</iq>
diff --git a/spec/scansion/muc_show_offline.scs b/spec/scansion/muc_show_offline.scs
new file mode 100644
index 00000000..57b75ec7
--- /dev/null
+++ b/spec/scansion/muc_show_offline.scs
@@ -0,0 +1,544 @@
+# MUC: Room registration and presence broadcast of unavailable members
+
+[Client] Romeo
+ jid: user@localhost
+ password: password
+
+[Client] Juliet
+ jid: user2@localhost
+ password: password
+
+[Client] Rosaline
+ jid: user3@localhost
+ password: password
+
+-----
+
+Romeo connects
+
+Romeo sends:
+ <presence to="room@conference.localhost/Romeo">
+ <x xmlns="http://jabber.org/protocol/muc"/>
+ </presence>
+
+Romeo receives:
+ <presence from='room@conference.localhost/Romeo'>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <status code='201'/>
+ <item jid="${Romeo's full JID}" affiliation='owner' role='moderator'/>
+ <status code='110'/>
+ </x>
+ </presence>
+
+Romeo receives:
+ <message type='groupchat' from='room@conference.localhost'><subject/></message>
+
+# Submit config form
+Romeo sends:
+ <iq id='config1' to='room@conference.localhost' type='set'>
+ <query xmlns='http://jabber.org/protocol/muc#owner'>
+ <x xmlns='jabber:x:data' type='submit'>
+ <field var='FORM_TYPE'>
+ <value>http://jabber.org/protocol/muc#roomconfig</value>
+ </field>
+ <field var='muc#roomconfig_presencebroadcast'>
+ <value>none</value>
+ <value>participant</value>
+ <value>moderator</value>
+ </field>
+ </x>
+ </query>
+ </iq>
+
+Romeo receives:
+ <iq id="config1" from="room@conference.localhost" type="result">
+ </iq>
+
+Romeo sends:
+ <iq id='member1' to='room@conference.localhost' type='set'>
+ <query xmlns='http://jabber.org/protocol/muc#admin'>
+ <item affiliation='member' jid="${Juliet's JID}" />
+ </query>
+ </iq>
+
+Romeo receives:
+ <message from='room@conference.localhost'>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <item jid="${Juliet's JID}" affiliation='member' />
+ </x>
+ </message>
+
+Romeo receives:
+ <iq from='room@conference.localhost' id='member1' type='result'/>
+
+# Juliet connects, and joins the room
+Juliet connects
+
+Juliet sends:
+ <presence to="room@conference.localhost/Juliet">
+ <x xmlns="http://jabber.org/protocol/muc"/>
+ </presence>
+
+Juliet receives:
+ <presence from="room@conference.localhost/Romeo" />
+
+Juliet receives:
+ <presence from="room@conference.localhost/Juliet" />
+
+Juliet receives:
+ <message type='groupchat' from='room@conference.localhost'><subject/></message>
+
+Romeo receives:
+ <presence from="room@conference.localhost/Juliet" />
+
+# Juliet retrieves the registration form
+
+Juliet sends:
+ <iq id='jw81b36f' to='room@conference.localhost' type='get'>
+ <query xmlns='jabber:iq:register'/>
+ </iq>
+
+Juliet receives:
+ <iq type='result' from='room@conference.localhost' id='jw81b36f'>
+ <query xmlns='jabber:iq:register'>
+ <x type='form' xmlns='jabber:x:data'>
+ <field type='hidden' var='FORM_TYPE'>
+ <value>http://jabber.org/protocol/muc#register</value>
+ </field>
+ <field type='text-single' label='Nickname' var='muc#register_roomnick'>
+ <required/>
+ </field>
+ </x>
+ </query>
+ </iq>
+
+Juliet sends:
+ <iq id='nv71va54' to='room@conference.localhost' type='set'>
+ <query xmlns='jabber:iq:register'>
+ <x xmlns='jabber:x:data' type='submit'>
+ <field var='FORM_TYPE'>
+ <value>http://jabber.org/protocol/muc#register</value>
+ </field>
+ <field var='muc#register_roomnick'>
+ <value>Juliet</value>
+ </field>
+ </x>
+ </query>
+ </iq>
+
+Juliet receives:
+ <presence from='room@conference.localhost/Juliet'>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <item affiliation='member' jid="${Juliet's full JID}" role='participant'/>
+ <status code='110'/>
+ </x>
+ </presence>
+
+Juliet receives:
+ <iq type='result' from='room@conference.localhost' id='nv71va54'/>
+
+# Juliet discovers her reserved nick
+
+Juliet sends:
+ <iq id='getnick1' to='room@conference.localhost' type='get'>
+ <query xmlns='http://jabber.org/protocol/disco#info' node='x-roomuser-item'/>
+ </iq>
+
+Juliet receives:
+ <iq type='result' from='room@conference.localhost' id='getnick1'>
+ <query xmlns='http://jabber.org/protocol/disco#info' node='x-roomuser-item'>
+ <identity category='conference' name='Juliet' type='text'/>
+ </query>
+ </iq>
+
+# Juliet leaves the room:
+
+Juliet sends:
+ <presence type="unavailable" to="room@conference.localhost/Juliet" />
+
+Juliet receives:
+ <presence type='unavailable' from='room@conference.localhost/Juliet'>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <item jid="${Juliet's full JID}" affiliation='member' role='none'/>
+ <status code='110'/>
+ </x>
+ </presence>
+
+Romeo receives:
+ <presence from='room@conference.localhost/Juliet'>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <item jid="${Juliet's full JID}" affiliation='member' role='participant'/>
+ </x>
+ </presence>
+
+# Rosaline connect and tries to join the room as Juliet
+
+Rosaline connects
+
+Rosaline sends:
+ <presence to="room@conference.localhost/Juliet">
+ <x xmlns="http://jabber.org/protocol/muc"/>
+ </presence>
+
+Rosaline receives:
+ <presence type='error' from='room@conference.localhost/Juliet'>
+ <error type='cancel' by='room@conference.localhost'>
+ <conflict xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
+ </error>
+ <x xmlns='http://jabber.org/protocol/muc'/>
+ </presence>
+
+# In a heated moment, Juliet unregisters from the room
+
+Juliet sends:
+ <iq type='set' to='room@conference.localhost' id='unreg1'>
+ <query xmlns='jabber:iq:register'>
+ <remove/>
+ </query>
+ </iq>
+
+Juliet receives:
+ <iq type='result' from='room@conference.localhost' id='unreg1'/>
+
+# Romeo is notified of Juliet's sad decision
+
+Romeo receives:
+ <message from='room@conference.localhost'>
+ <x xmlns='http://jabber.org/protocol/muc#user' scansion:strict='true'>
+ <item jid="${Juliet's JID}" affiliation='none' />
+ </x>
+ </message>
+
+# Rosaline attempts once more to sneak into the room, disguised as Juliet
+
+Rosaline sends:
+ <presence to="room@conference.localhost/Juliet">
+ <x xmlns="http://jabber.org/protocol/muc"/>
+ </presence>
+
+Rosaline receives:
+ <presence from='room@conference.localhost/Romeo'>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <item affiliation='owner' role='moderator'/>
+ </x>
+ </presence>
+
+Rosaline receives:
+ <presence from='room@conference.localhost/Juliet'>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <item affiliation='none' jid="${Rosaline's full JID}" role='participant'/>
+ <status code='110'/>
+ </x>
+ </presence>
+
+Romeo receives:
+ <presence from='room@conference.localhost/Juliet'>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <item affiliation='none' jid="${Rosaline's full JID}" role='participant'/>
+ </x>
+ </presence>
+
+# On discovering the ruse, Romeo restores Juliet's nick and status within the room
+
+Romeo sends:
+ <iq id='member1' to='room@conference.localhost' type='set'>
+ <query xmlns='http://jabber.org/protocol/muc#admin'>
+ <item affiliation='member' jid="${Juliet's JID}" nick='Juliet' />
+ </query>
+ </iq>
+
+# Rosaline is evicted from the room
+
+Romeo receives:
+ <presence from='room@conference.localhost/Juliet' type='unavailable'>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <status code='307'/>
+ <item affiliation='none' role='none' jid="${Rosaline's full JID}">
+ <reason>This nickname is reserved</reason>
+ </item>
+ </x>
+ </presence>
+
+# An out-of-room affiliation change is received for Juliet
+
+Romeo receives:
+ <message from='room@conference.localhost'>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <item jid="${Juliet's JID}" affiliation='member' />
+ </x>
+ </message>
+
+Romeo receives:
+ <iq type='result' id='member1' from='room@conference.localhost' />
+
+Rosaline receives:
+ <presence type='unavailable' from='room@conference.localhost/Juliet'>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <status code='307'/>
+ <item affiliation='none' jid="${Rosaline's full JID}" role='none'>
+ <reason>This nickname is reserved</reason>
+ </item>
+ <status code='110'/>
+ </x>
+ </presence>
+
+# Rosaline, frustrated, attempts to get back into the room...
+
+Rosaline sends:
+ <presence to="room@conference.localhost/Juliet">
+ <x xmlns="http://jabber.org/protocol/muc"/>
+ </presence>
+
+# ...but once again, is denied
+
+Rosaline receives:
+ <presence type='error' from='room@conference.localhost/Juliet'>
+ <error type='cancel' by='room@conference.localhost'>
+ <conflict xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
+ </error>
+ <x xmlns='http://jabber.org/protocol/muc'/>
+ </presence>
+
+# Juliet, however, quietly joins the room with success
+
+Juliet sends:
+ <presence to="room@conference.localhost/Juliet">
+ <x xmlns="http://jabber.org/protocol/muc"/>
+ </presence>
+
+Juliet receives:
+ <presence from="room@conference.localhost/Romeo" />
+
+Juliet receives:
+ <presence from="room@conference.localhost/Juliet" />
+
+Juliet receives:
+ <message type='groupchat' from='room@conference.localhost'><subject/></message>
+
+Romeo receives:
+ <presence from="room@conference.localhost/Juliet" />
+
+# Romeo checks whether he has reserved his own nick yet
+
+Romeo sends:
+ <iq id='getnick1' to='room@conference.localhost' type='get'>
+ <query xmlns='http://jabber.org/protocol/disco#info' node='x-roomuser-item'/>
+ </iq>
+
+# But no nick is returned, as he hasn't registered yet!
+
+Romeo receives:
+ <iq type='result' from='room@conference.localhost' id='getnick1'>
+ <query xmlns='http://jabber.org/protocol/disco#info' node='x-roomuser-item' scansion:strict='true' />
+ </iq>
+
+# Romeo updates his own registration
+
+Romeo sends:
+ <iq id='jw81b36f' to='room@conference.localhost' type='get'>
+ <query xmlns='jabber:iq:register'/>
+ </iq>
+
+Romeo receives:
+ <iq type='result' from='room@conference.localhost' id='jw81b36f'>
+ <query xmlns='jabber:iq:register'>
+ <x type='form' xmlns='jabber:x:data'>
+ <field type='hidden' var='FORM_TYPE'>
+ <value>http://jabber.org/protocol/muc#register</value>
+ </field>
+ <field type='text-single' label='Nickname' var='muc#register_roomnick'>
+ <required/>
+ </field>
+ </x>
+ </query>
+ </iq>
+
+Romeo sends:
+ <iq id='nv71va54' to='room@conference.localhost' type='set'>
+ <query xmlns='jabber:iq:register'>
+ <x xmlns='jabber:x:data' type='submit'>
+ <field var='FORM_TYPE'>
+ <value>http://jabber.org/protocol/muc#register</value>
+ </field>
+ <field var='muc#register_roomnick'>
+ <value>Romeo</value>
+ </field>
+ </x>
+ </query>
+ </iq>
+
+Romeo receives:
+ <presence from='room@conference.localhost/Romeo'>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <item affiliation='owner' jid="${Romeo's full JID}" role='moderator'/>
+ <status code='110'/>
+ </x>
+ </presence>
+
+Romeo receives:
+ <iq type='result' from='room@conference.localhost' id='nv71va54'/>
+
+Juliet receives:
+ <presence from='room@conference.localhost/Romeo'>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <item role='moderator' xmlns='http://jabber.org/protocol/muc#user' affiliation='owner'/>
+ </x>
+ </presence>
+
+# Romeo discovers his reserved nick
+
+Romeo sends:
+ <iq id='getnick1' to='room@conference.localhost' type='get'>
+ <query xmlns='http://jabber.org/protocol/disco#info' node='x-roomuser-item'/>
+ </iq>
+
+Romeo receives:
+ <iq type='result' from='room@conference.localhost' id='getnick1'>
+ <query xmlns='http://jabber.org/protocol/disco#info' node='x-roomuser-item'>
+ <identity category='conference' name='Romeo' type='text'/>
+ </query>
+ </iq>
+
+# To check the status of the room is as expected, Romeo requests the member list
+
+Romeo sends:
+ <iq id='member3' to='room@conference.localhost' type='get'>
+ <query xmlns='http://jabber.org/protocol/muc#admin'>
+ <item affiliation='member'/>
+ </query>
+ </iq>
+
+Romeo receives:
+ <iq from='room@conference.localhost' type='result' id='member3'>
+ <query xmlns='http://jabber.org/protocol/muc#admin'>
+ <item jid="${Juliet's JID}" affiliation='member' nick='Juliet'/>
+ </query>
+ </iq>
+
+Juliet sends:
+ <presence type="unavailable" to="room@conference.localhost/Juliet" />
+
+Juliet receives:
+ <presence from='room@conference.localhost/Juliet' type='unavailable' />
+
+Romeo receives:
+ <presence type='unavailable' from='room@conference.localhost/Juliet' />
+
+# Rosaline joins as herself
+
+Rosaline sends:
+ <presence to="room@conference.localhost/Rosaline">
+ <x xmlns="http://jabber.org/protocol/muc"/>
+ </presence>
+
+Rosaline receives:
+ <presence from="room@conference.localhost/Romeo" />
+
+Rosaline receives:
+ <presence from='room@conference.localhost/Juliet' type='unavailable'>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <item affiliation='member' role='none' nick='Juliet' xmlns='http://jabber.org/protocol/muc#user'/>
+ </x>
+ </presence>
+
+Rosaline receives:
+ <presence from="room@conference.localhost/Rosaline" />
+
+Rosaline receives:
+ <message type='groupchat' from='room@conference.localhost'><subject/></message>
+
+Romeo receives:
+ <presence from='room@conference.localhost/Rosaline'>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <item jid="${Rosaline's full JID}" affiliation='none' role='participant'/>
+ </x>
+ </presence>
+
+# Rosaline tries to register her own nickname, but unaffiliated
+# registration is disabled by default
+
+Rosaline sends:
+ <iq id='reg990' to='room@conference.localhost' type='get'>
+ <query xmlns='jabber:iq:register'/>
+ </iq>
+
+Rosaline receives:
+ <iq type='error' from='room@conference.localhost' id='reg990'>
+ <error type='auth'>
+ <registration-required xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
+ </error>
+ </iq>
+
+Rosaline sends:
+ <iq id='reg991' to='room@conference.localhost' type='set'>
+ <query xmlns='jabber:iq:register'>
+ <x xmlns='jabber:x:data' type='submit'>
+ <field var='FORM_TYPE'>
+ <value>http://jabber.org/protocol/muc#register</value>
+ </field>
+ <field var='muc#register_roomnick'>
+ <value>Romeo</value>
+ </field>
+ </x>
+ </query>
+ </iq>
+
+Rosaline receives:
+ <iq id='reg991' type='error'>
+ <error type='auth'>
+ <registration-required xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
+ </error>
+ </iq>
+
+# Romeo reserves her nickname for her
+
+Romeo sends:
+ <iq id='member2' to='room@conference.localhost' type='set'>
+ <query xmlns='http://jabber.org/protocol/muc#admin'>
+ <item affiliation='member' jid="${Rosaline's JID}" nick='Rosaline' />
+ </query>
+ </iq>
+
+Romeo receives:
+ <presence from='room@conference.localhost/Rosaline'>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <item affiliation='member' role='participant' jid="${Rosaline's full JID}">
+ <actor jid="${Romeo's full JID}" nick='Romeo'/>
+ </item>
+ </x>
+ </presence>
+
+Romeo receives:
+ <iq type='result' id='member2' from='room@conference.localhost' />
+
+Rosaline receives:
+ <presence from='room@conference.localhost/Rosaline'>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <item affiliation='member' role='participant' jid="${Rosaline's full JID}">
+ <actor nick='Romeo' />
+ </item>
+ <status xmlns='http://jabber.org/protocol/muc#user' code='110'/>
+ </x>
+ </presence>
+
+# Romeo sets their their own nickname via admin query (see #1273)
+Romeo sends:
+ <iq to="room@conference.localhost" id="reserve" type="set">
+ <query xmlns="http://jabber.org/protocol/muc#admin">
+ <item nick="Romeo" affiliation="owner" jid="${Romeo's JID}"/>
+ </query>
+ </iq>
+
+Romeo receives:
+ <presence from="room@conference.localhost/Romeo">
+ <x xmlns="http://jabber.org/protocol/muc#user">
+ <item xmlns="http://jabber.org/protocol/muc#user" role="moderator" jid="${Romeo's full JID}" affiliation="owner">
+ <actor xmlns="http://jabber.org/protocol/muc#user" nick="Romeo"/>
+ </item>
+ <status xmlns="http://jabber.org/protocol/muc#user" code="110"/>
+ </x>
+ </presence>
+
+Romeo receives:
+ <iq from="room@conference.localhost" id="reserve" type="result"/>
+
diff --git a/spec/scansion/muc_subject_issue_667.scs b/spec/scansion/muc_subject_issue_667.scs
new file mode 100644
index 00000000..74980073
--- /dev/null
+++ b/spec/scansion/muc_subject_issue_667.scs
@@ -0,0 +1,129 @@
+# #667 MUC message with subject and body SHALL NOT be interpreted as a subject change
+
+[Client] Romeo
+ password: password
+ jid: romeo@localhost
+
+-----
+
+Romeo connects
+
+# and creates a room
+Romeo sends:
+ <presence to="issue667@conference.localhost/Romeo">
+ <x xmlns="http://jabber.org/protocol/muc"/>
+ </presence>
+
+Romeo receives:
+ <presence from="issue667@conference.localhost/Romeo">
+ <x xmlns="http://jabber.org/protocol/muc#user">
+ <status code="201"/>
+ <item affiliation="owner" role="moderator" jid="${Romeo's full JID}"/>
+ <status code="110"/>
+ </x>
+ </presence>
+
+# the default (empty) subject
+Romeo receives:
+ <message type="groupchat" from="issue667@conference.localhost">
+ <subject/>
+ </message>
+
+# this should be treated as a normal message
+Romeo sends:
+ <message to="issue667@conference.localhost" type="groupchat">
+ <subject>Greetings</subject>
+ <body>Hello everyone</body>
+ </message>
+
+Romeo receives:
+ <message type="groupchat" from="issue667@conference.localhost/Romeo">
+ <subject>Greetings</subject>
+ <body>Hello everyone</body>
+ </message>
+
+# Resync
+Romeo sends:
+ <presence to="issue667@conference.localhost/Romeo">
+ <x xmlns="http://jabber.org/protocol/muc"/>
+ </presence>
+
+# Presences
+Romeo receives:
+ <presence from="issue667@conference.localhost/Romeo">
+ <x xmlns="http://jabber.org/protocol/muc#user">
+ <item affiliation="owner" role="moderator" jid="${Romeo's full JID}"/>
+ <status code="110"/>
+ </x>
+ </presence>
+
+Romeo receives:
+ <message type="groupchat" from="issue667@conference.localhost/Romeo">
+ <subject>Greetings</subject>
+ <body>Hello everyone</body>
+ </message>
+
+# the still empty subject
+Romeo receives:
+ <message type="groupchat" from="issue667@conference.localhost">
+ <subject/>
+ </message>
+
+# this is a subject change
+Romeo sends:
+ <message to="issue667@conference.localhost" type="groupchat">
+ <subject>Something to talk about</subject>
+ </message>
+
+Romeo receives:
+ <message type="groupchat" from="issue667@conference.localhost/Romeo">
+ <subject>Something to talk about</subject>
+ </message>
+
+# a message without <subject>
+Romeo sends:
+ <message to="issue667@conference.localhost" type="groupchat">
+ <body>Lorem ipsum dolor sit amet</body>
+ </message>
+
+Romeo receives:
+ <message type="groupchat" from="issue667@conference.localhost/Romeo">
+ <body>Lorem ipsum dolor sit amet</body>
+ </message>
+
+# Resync
+Romeo sends:
+ <presence to="issue667@conference.localhost/Romeo">
+ <x xmlns="http://jabber.org/protocol/muc"/>
+ </presence>
+
+# Presences
+Romeo receives:
+ <presence from="issue667@conference.localhost/Romeo">
+ <x xmlns="http://jabber.org/protocol/muc#user">
+ <item affiliation="owner" role="moderator" jid="${Romeo's full JID}"/>
+ <status code="110"/>
+ </x>
+ </presence>
+
+# History
+# These have delay tags but we ignore those for now
+Romeo receives:
+ <message type="groupchat" from="issue667@conference.localhost/Romeo">
+ <subject>Greetings</subject>
+ <body>Hello everyone</body>
+ </message>
+
+Romeo receives:
+ <message type="groupchat" from="issue667@conference.localhost/Romeo">
+ <body>Lorem ipsum dolor sit amet</body>
+ </message>
+
+# Finally, the topic
+Romeo receives:
+ <message type="groupchat" from="issue667@conference.localhost/Romeo">
+ <subject>Something to talk about</subject>
+ </message>
+
+Romeo disconnects
+
diff --git a/spec/scansion/presence_preapproval.scs b/spec/scansion/presence_preapproval.scs
new file mode 100644
index 00000000..e34ac7cf
--- /dev/null
+++ b/spec/scansion/presence_preapproval.scs
@@ -0,0 +1,74 @@
+# server supports contact subscription pre-approval (RFC 6121 3.4)
+
+[Client] Alice
+ jid: preappove-a@localhost
+ password: password
+
+[Client] Bob
+ jid: preapprove-b@localhost
+ password: password
+
+---------
+
+Alice connects
+
+Alice sends:
+ <presence/>
+
+Alice receives:
+ <presence/>
+
+Alice sends:
+ <presence to="${Bob's JID}" type="subscribed"/>
+
+Bob connects
+
+Bob sends:
+ <iq type="get" id="roster1">
+ <query xmlns="jabber:iq:roster"/>
+ </iq>
+
+Bob receives:
+ <iq type="result" id="roster1">
+ <query xmlns="jabber:iq:roster" ver="{scansion:any}">
+ </query>
+ </iq>
+
+Bob sends:
+ <presence/>
+
+Bob receives:
+ <presence from="${Bob's full JID}"/>
+
+Bob sends:
+ <presence to="${Alice's JID}" type="subscribe" />
+
+Bob receives:
+ <iq type='set' id='{scansion:any}'>
+ <query ver='1' xmlns='jabber:iq:roster'>
+ <item jid="${Alice's JID}" subscription='none' ask='subscribe' />
+ </query>
+ </iq>
+
+
+
+Bob receives:
+ <presence from="${Alice's JID}" type="subscribed" />
+
+Bob disconnects
+
+Alice sends:
+ <iq type="get" id="roster1">
+ <query xmlns="jabber:iq:roster"/>
+ </iq>
+
+Alice receives:
+ <iq type="result" id="roster1">
+ <query xmlns="jabber:iq:roster" ver="{scansion:any}">
+ <item jid="${Bob's JID}" subscription="from" />
+ </query>
+ </iq>
+
+Alice disconnects
+
+Bob disconnects
diff --git a/spec/scansion/prosody.cfg.lua b/spec/scansion/prosody.cfg.lua
index f95ea31b..4e43d312 100644
--- a/spec/scansion/prosody.cfg.lua
+++ b/spec/scansion/prosody.cfg.lua
@@ -1,23 +1,37 @@
--luacheck: ignore
+-- Mock time functions to simplify tests
+function _G.os.time()
+ return 1219439344;
+end
+package.preload["util.time"] = function ()
+ return {
+ now = function () return 1219439344.1; end;
+ monotonic = function () return 0.1; end;
+ }
+end
+
admins = { "admin@localhost" }
-use_libevent = true
+network_backend = "epoll"
+network_settings = {
+}
modules_enabled = {
-- Generally required
"roster"; -- Allow users to have a roster. Recommended ;)
"saslauth"; -- Authentication for clients and servers. Recommended if you want to log in.
- "tls"; -- Add support for secure TLS on c2s/s2s connections
- "dialback"; -- s2s dialback support
+ --"tls"; -- Add support for secure TLS on c2s/s2s connections
+ --"dialback"; -- s2s dialback support
"disco"; -- Service discovery
-- Not essential, but recommended
"carbons"; -- Keep multiple clients in sync
- "pep"; -- Enables users to publish their mood, activity, playing music and more
+ "pep"; -- Enables users to publish their avatar, mood, activity, playing music and more
"private"; -- Private XML storage (for room bookmarks, etc.)
"blocklist"; -- Allow users to block communications with other users
- "vcard"; -- Allow users to set vCards
+ "vcard4"; -- User profiles (stored in PEP)
+ "vcard_legacy"; -- Conversion between legacy vCard and PEP Avatar, vcard
-- Nice to have
"version"; -- Replies to server version requests
@@ -26,6 +40,11 @@ modules_enabled = {
"ping"; -- Replies to XMPP pings with pongs
"register"; -- Allow users to register on this server using a client and change passwords
"mam"; -- Store messages in an archive and allow users to access it
+ --"csi_simple"; -- Simple Mobile optimizations
+
+ -- Admin interfaces
+ --"admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands
+ --"admin_telnet"; -- Opens telnet console interface on localhost port 5582
-- HTTP modules
--"bosh"; -- Enable BOSH clients, aka "Jabber over HTTP"
@@ -35,18 +54,44 @@ modules_enabled = {
-- Other specific functionality
--"limits"; -- Enable bandwidth limiting for XMPP connections
--"groups"; -- Shared roster support
- --"server_contact_info"; -- Publish contact information for this service
+ "server_contact_info"; -- Publish contact information for this service
--"announce"; -- Send announcement to all online users
--"welcome"; -- Welcome users who register accounts
--"watchregistrations"; -- Alert admins of registrations
--"motd"; -- Send a message to users when they log in
--"legacyauth"; -- Legacy authentication. Only used by some old clients and bots.
--"proxy65"; -- Enables a file transfer proxy service which clients behind NAT can use
+ "lastactivity";
+ "external_services";
-- Useful for testing
--"scansion_record"; -- Records things that happen in scansion test case format
}
+contact_info = {
+ abuse = { "mailto:abuse@localhost", "xmpp:abuse@localhost" };
+ admin = { "mailto:admin@localhost", "xmpp:admin@localhost" };
+ feedback = { "http://localhost/feedback.html", "mailto:feedback@localhost", "xmpp:feedback@localhost" };
+ sales = { "xmpp:sales@localhost" };
+ security = { "xmpp:security@localhost" };
+ status = { "gopher://status.localhost" };
+ support = { "https://localhost/support.html", "xmpp:support@localhost" };
+}
+
+external_service_host = "default.example"
+external_service_port = 9876
+external_service_secret = "<secret>"
+external_services = {
+ {type = "stun"; transport = "udp"};
+ {type = "turn"; transport = "udp"; secret = true};
+ {type = "turn"; transport = "udp"; secret = "foo"};
+ {type = "ftp"; transport = "tcp"; port = 2121; username = "john"; password = "password"};
+ {type = "ftp"; transport = "tcp"; host = "ftp.example.com"; port = 21; username = "john"; password = "password"};
+}
+
+modules_disabled = {
+ "s2s";
+}
certificate = "certs"
allow_registration = false
@@ -69,15 +114,20 @@ mam_smart_enable = true
-- Logging configuration
-- For advanced logging see https://prosody.im/doc/logging
-log = "*console"
+log = {debug = "*console" }
-daemonize = true
pidfile = "prosody.pid"
VirtualHost "localhost"
+hide_os_type = true -- absence tested for in version.scs
+
Component "conference.localhost" "muc"
storage = "memory"
+ modules_enabled = {
+ "muc_mam";
+ }
+
Component "pubsub.localhost" "pubsub"
storage = "memory"
diff --git a/spec/scansion/pubsub_basic.scs b/spec/scansion/pubsub_basic.scs
index d983ff66..0adf6857 100644
--- a/spec/scansion/pubsub_basic.scs
+++ b/spec/scansion/pubsub_basic.scs
@@ -32,7 +32,7 @@ Juliet connects
-- <subscribe node="princely_musings" jid="${Romeo's full JID}"/>
-- </pubsub>
-- </iq>
---
+--
-- Juliet receives:
-- <iq type="error"/>
diff --git a/spec/scansion/pubsub_preconditions.scs b/spec/scansion/pubsub_preconditions.scs
new file mode 100644
index 00000000..25afaa8d
--- /dev/null
+++ b/spec/scansion/pubsub_preconditions.scs
@@ -0,0 +1,234 @@
+# Pubsub preconditions are enforced
+
+[Client] Romeo
+ password: password
+ jid: jqpcrbq2@localhost
+
+-----
+
+Romeo connects
+
+Romeo sends:
+ <iq id="67eb1f47-1e69-4cb3-91e2-4d5943e72d4c" type="set">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <publish node="http://jabber.org/protocol/tune">
+ <item id="current">
+ <tune xmlns="http://jabber.org/protocol/tune"/>
+ </item>
+ </publish>
+ </pubsub>
+ </iq>
+
+Romeo receives:
+ <iq id="67eb1f47-1e69-4cb3-91e2-4d5943e72d4c" type="result">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <publish node="http://jabber.org/protocol/tune">
+ <item id="current"/>
+ </publish>
+ </pubsub>
+ </iq>
+
+Romeo sends:
+ <iq id="52d74a36-afb0-4028-87ed-b25b988b049e" type="get">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
+ <configure node="http://jabber.org/protocol/tune"/>
+ </pubsub>
+ </iq>
+
+Romeo receives:
+ <iq id="52d74a36-afb0-4028-87ed-b25b988b049e" type="result">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
+ <configure node="http://jabber.org/protocol/tune">
+ <x xmlns="jabber:x:data" type="form">
+ <field var="FORM_TYPE" type="hidden">
+ <value>http://jabber.org/protocol/pubsub#node_config</value>
+ </field>
+ <field var="pubsub#title" label="Title" type="text-single"/>
+ <field var="pubsub#description" label="Description" type="text-single"/>
+ <field var="pubsub#type" label="The type of node data, usually specified by the namespace of the payload (if any)" type="text-single"/>
+ <field var="pubsub#max_items" label="Max # of items to persist" type="text-single">
+ <validate xmlns="http://jabber.org/protocol/xdata-validate" datatype="xs:integer"/>
+ <value>1</value>
+ </field>
+ <field var="pubsub#persist_items" label="Persist items to storage" type="boolean">
+ <value>1</value>
+ </field>
+ <field var="pubsub#access_model" label="Specify the subscriber model" type="list-single">
+ <option label="authorize">
+ <value>authorize</value>
+ </option>
+ <option label="open">
+ <value>open</value>
+ </option>
+ <option label="presence">
+ <value>presence</value>
+ </option>
+ <option label="roster">
+ <value>roster</value>
+ </option>
+ <option label="whitelist">
+ <value>whitelist</value>
+ </option>
+ <value>presence</value>
+ </field>
+ <field var="pubsub#publish_model" label="Specify the publisher model" type="list-single">
+ <option label="publishers">
+ <value>publishers</value>
+ </option>
+ <option label="subscribers">
+ <value>subscribers</value>
+ </option>
+ <option label="open">
+ <value>open</value>
+ </option>
+ <value>publishers</value>
+ </field>
+ <field var="pubsub#deliver_notifications" label="Whether to deliver event notifications" type="boolean">
+ <value>1</value>
+ </field>
+ <field var="pubsub#deliver_payloads" label="Whether to deliver payloads with event notifications" type="boolean">
+ <value>1</value>
+ </field>
+ <field var="pubsub#notification_type" label="Specify the delivery style for notifications" type="list-single">
+ <option label="Messages of type normal">
+ <value>normal</value>
+ </option>
+ <option label="Messages of type headline">
+ <value>headline</value>
+ </option>
+ <value>headline</value>
+ </field>
+ <field var="pubsub#notify_delete" label="Whether to notify subscribers when the node is deleted" type="boolean">
+ <value>1</value>
+ </field>
+ <field var="pubsub#notify_retract" label="Whether to notify subscribers when items are removed from the node" type="boolean">
+ <value>1</value>
+ </field>
+ </x>
+ </configure>
+ </pubsub>
+ </iq>
+
+Romeo sends:
+ <iq id="a73aac09-74be-4ee2-97e5-571bbdbcd956" type="set">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
+ <configure node="http://jabber.org/protocol/tune">
+ <x xmlns="jabber:x:data" type="submit">
+ <field var="FORM_TYPE" type="hidden">
+ <value>http://jabber.org/protocol/pubsub#node_config</value>
+ </field>
+ <field var="pubsub#title" type="text-single" label="Title">
+ <value>Nice tunes</value>
+ </field>
+ <field var="pubsub#description" type="text-single" label="Description"/>
+ <field var="pubsub#type" type="text-single" label="The type of node data, usually specified by the namespace of the payload (if any)"/>
+ <field var="pubsub#max_items" type="text-single" label="Max # of items to persist">
+ <validate xmlns="http://jabber.org/protocol/xdata-validate" datatype="xs:integer"/>
+ <value>1</value>
+ </field>
+ <field var="pubsub#persist_items" type="boolean" label="Persist items to storage">
+ <value>1</value>
+ </field>
+ <field var="pubsub#access_model" type="list-single" label="Specify the subscriber model">
+ <option label="authorize">
+ <value>authorize</value>
+ </option>
+ <option label="open">
+ <value>open</value>
+ </option>
+ <option label="presence">
+ <value>presence</value>
+ </option>
+ <option label="roster">
+ <value>roster</value>
+ </option>
+ <option label="whitelist">
+ <value>whitelist</value>
+ </option>
+ <value>presence</value>
+ </field>
+ <field var="pubsub#publish_model" type="list-single" label="Specify the publisher model">
+ <option label="publishers">
+ <value>publishers</value>
+ </option>
+ <option label="subscribers">
+ <value>subscribers</value>
+ </option>
+ <option label="open">
+ <value>open</value>
+ </option>
+ <value>publishers</value>
+ </field>
+ <field var="pubsub#deliver_notifications" type="boolean" label="Whether to deliver event notifications">
+ <value>1</value>
+ </field>
+ <field var="pubsub#deliver_payloads" type="boolean" label="Whether to deliver payloads with event notifications">
+ <value>1</value>
+ </field>
+ <field var="pubsub#notification_type" type="list-single" label="Specify the delivery style for notifications">
+ <option label="Messages of type normal">
+ <value>normal</value>
+ </option>
+ <option label="Messages of type headline">
+ <value>headline</value>
+ </option>
+ <value>headline</value>
+ </field>
+ <field var="pubsub#notify_delete" type="boolean" label="Whether to notify subscribers when the node is deleted">
+ <value>1</value>
+ </field>
+ <field var="pubsub#notify_retract" type="boolean" label="Whether to notify subscribers when items are removed from the node">
+ <value>1</value>
+ </field>
+ </x>
+ </configure>
+ </pubsub>
+ </iq>
+
+Romeo receives:
+ <iq id="a73aac09-74be-4ee2-97e5-571bbdbcd956" type="result"/>
+
+Romeo sends:
+ <iq id="ab0e92d2-c06b-4987-9d45-f9f9e7721709" type="get">
+ <query xmlns="http://jabber.org/protocol/disco#items"/>
+ </iq>
+
+Romeo receives:
+ <iq id="ab0e92d2-c06b-4987-9d45-f9f9e7721709" type="result">
+ <query xmlns="http://jabber.org/protocol/disco#items">
+ <item name="Nice tunes" node="http://jabber.org/protocol/tune" jid="${Romeo's JID}"/>
+ </query>
+ </iq>
+
+Romeo sends:
+ <iq id="67eb1f47-1e69-4cb3-91e2-4d5943e72d4c" type="set">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <publish node="http://jabber.org/protocol/tune">
+ <item id="current">
+ <tune xmlns="http://jabber.org/protocol/tune"/>
+ </item>
+ </publish>
+ <publish-options>
+ <x xmlns="jabber:x:data">
+ <field var="FORM_TYPE" type="hidden">
+ <value>http://jabber.org/protocol/pubsub#publish-options</value>
+ </field>
+ <field var="pubsub#access_model">
+ <value>whitelist</value>
+ </field>
+ </x>
+ </publish-options>
+ </pubsub>
+ </iq>
+
+Romeo receives:
+ <iq type='error' id='67eb1f47-1e69-4cb3-91e2-4d5943e72d4c'>
+ <error type='cancel'>
+ <conflict xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
+ <text xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'>Field does not match: access_model</text>
+ <precondition-not-met xmlns='http://jabber.org/protocol/pubsub#errors'/>
+ </error>
+ </iq>
+
+Romeo disconnects
+
diff --git a/spec/scansion/server_contact_info.scs b/spec/scansion/server_contact_info.scs
new file mode 100644
index 00000000..6efc7fd1
--- /dev/null
+++ b/spec/scansion/server_contact_info.scs
@@ -0,0 +1,56 @@
+# XEP-0157: Contact Addresses for XMPP Services
+# mod_server_contact_info
+
+[Client] Romeo
+ jid: romeo@localhost
+ password: password
+
+-----
+
+Romeo connects
+
+Romeo sends:
+ <iq type='get' id='lx2' to='localhost'>
+ <query xmlns='http://jabber.org/protocol/disco#info'/>
+ </iq>
+
+# Ignore other disco#info features, identities etc
+
+Romeo receives:
+ <iq from='localhost' id='lx2' type='result'>
+ <query xmlns='http://jabber.org/protocol/disco#info' scansion:strict='false'>
+ <x xmlns='jabber:x:data' type='result'>
+ <field type='hidden' var='FORM_TYPE'>
+ <value>http://jabber.org/network/serverinfo</value>
+ </field>
+ <field type='list-multi' var='abuse-addresses'>
+ <value>mailto:abuse@localhost</value>
+ <value>xmpp:abuse@localhost</value>
+ </field>
+ <field type='list-multi' var='admin-addresses'>
+ <value>mailto:admin@localhost</value>
+ <value>xmpp:admin@localhost</value>
+ </field>
+ <field type='list-multi' var='feedback-addresses'>
+ <value>http://localhost/feedback.html</value>
+ <value>mailto:feedback@localhost</value>
+ <value>xmpp:feedback@localhost</value>
+ </field>
+ <field type='list-multi' var='sales-addresses'>
+ <value>xmpp:sales@localhost</value>
+ </field>
+ <field type='list-multi' var='security-addresses'>
+ <value>xmpp:security@localhost</value>
+ </field>
+ <field type='list-multi' var='status-addresses'>
+ <value>gopher://status.localhost</value>
+ </field>
+ <field type='list-multi' var='support-addresses'>
+ <value>https://localhost/support.html</value>
+ <value>xmpp:support@localhost</value>
+ </field>
+ </x>
+ </query>
+ </iq>
+
+Romeo disconnects
diff --git a/spec/scansion/uptime.scs b/spec/scansion/uptime.scs
new file mode 100644
index 00000000..188b9eb5
--- /dev/null
+++ b/spec/scansion/uptime.scs
@@ -0,0 +1,21 @@
+# XEP-0012: Last Activity / mod_uptime
+
+[Client] Romeo
+ jid: romeo@localhost
+ password: password
+
+-----
+
+Romeo connects
+
+Romeo sends:
+ <iq id='a' type='get' to='localhost'>
+ <query xmlns='jabber:iq:last'/>
+ </iq>
+
+Romeo receives:
+ <iq type='result' id='a' from='localhost'>
+ <query xmlns='jabber:iq:last' seconds='0'/>
+ </iq>
+
+Romeo disconnects
diff --git a/spec/scansion/version.scs b/spec/scansion/version.scs
new file mode 100644
index 00000000..6c841dd9
--- /dev/null
+++ b/spec/scansion/version.scs
@@ -0,0 +1,27 @@
+# XEP-0092: Software Version / mod_version
+
+[Client] Romeo
+ password: password
+ jid: romeo@localhost/dfaZpuxV
+
+-----
+
+Romeo connects
+
+Romeo sends:
+ <iq id='lx2' to='localhost' type='get'>
+ <query xmlns='jabber:iq:version'/>
+ </iq>
+
+# Version string would vary so we can't do an exact match atm
+# Inclusion of <os/> is disabled in the config, it should be absent
+Romeo receives:
+ <iq id='lx2' from='localhost' type='result'>
+ <query xmlns='jabber:iq:version' scansion:strict='true'>
+ <name>Prosody</name>
+ <version scansion:strict='false'/>
+ </query>
+ </iq>
+
+
+Romeo disconnects
diff --git a/spec/util_array_spec.lua b/spec/util_array_spec.lua
new file mode 100644
index 00000000..1d9da947
--- /dev/null
+++ b/spec/util_array_spec.lua
@@ -0,0 +1,155 @@
+local array = require "util.array";
+describe("util.array", function ()
+ describe("creation", function ()
+ describe("from table", function ()
+ it("works", function ()
+ local a = array({"a", "b", "c"});
+ assert.same({"a", "b", "c"}, a);
+ end);
+ end);
+
+ describe("from iterator", function ()
+ it("works", function ()
+ -- collects the first value, ie the keys
+ local a = array(ipairs({true, true, true}));
+ assert.same({1, 2, 3}, a);
+ end);
+ end);
+
+ describe("collect", function ()
+ it("works", function ()
+ -- collects the first value, ie the keys
+ local a = array.collect(ipairs({true, true, true}));
+ assert.same({1, 2, 3}, a);
+ end);
+ end);
+
+ end);
+
+ describe("metatable", function ()
+ describe("operator", function ()
+ describe("addition", function ()
+ it("works", function ()
+ local a = array({ "a", "b" });
+ local b = array({ "c", "d" });
+ assert.same({"a", "b", "c", "d"}, a + b);
+ end);
+ end);
+
+ describe("equality", function ()
+ it("works", function ()
+ local a1 = array({ "a", "b" });
+ local a2 = array({ "a", "b" });
+ local b = array({ "c", "d" });
+ assert.truthy(a1 == a2);
+ assert.falsy(a1 == b);
+ assert.falsy(a1 == { "a", "b" }, "Behavior of metatables changed in Lua 5.3");
+ end);
+ end);
+
+ describe("division", function ()
+ it("works", function ()
+ local a = array({ "a", "b", "c" });
+ local b = a / function (i) if i ~= "b" then return i .. "x" end end;
+ assert.same({ "ax", "cx" }, b);
+ end);
+ end);
+
+ end);
+ end);
+
+ describe("methods", function ()
+ describe("map", function ()
+ it("works", function ()
+ local a = array({ "a", "b", "c" });
+ local b = a:map(string.upper);
+ assert.same({ "A", "B", "C" }, b);
+ end);
+ end);
+
+ describe("filter", function ()
+ it("works", function ()
+ local a = array({ "a", "b", "c" });
+ a:filter(function (i) return i ~= "b" end);
+ assert.same({ "a", "c" }, a);
+ end);
+ end);
+
+ describe("sort", function ()
+ it("works", function ()
+ local a = array({ 5, 4, 3, 1, 2, });
+ a:sort();
+ assert.same({ 1, 2, 3, 4, 5, }, a);
+ end);
+ end);
+
+ describe("unique", function ()
+ it("works", function ()
+ local a = array({ "a", "b", "c", "c", "a", "b" });
+ a:unique();
+ assert.same({ "a", "b", "c" }, a);
+ end);
+ end);
+
+ describe("pluck", function ()
+ it("works", function ()
+ local a = array({ { a = 1, b = -1 }, { a = 2, b = -2 }, });
+ a:pluck("a");
+ assert.same({ 1, 2 }, a);
+ end);
+ end);
+
+
+ describe("reverse", function ()
+ it("works", function ()
+ local a = array({ "a", "b", "c" });
+ a:reverse();
+ assert.same({ "c", "b", "a" }, a);
+ end);
+ end);
+
+ -- TODO :shuffle
+
+ describe("append", function ()
+ it("works", function ()
+ local a = array({ "a", "b", "c" });
+ a:append(array({ "d", "e", }));
+ assert.same({ "a", "b", "c", "d", "e" }, a);
+ end);
+ end);
+
+ describe("push", function ()
+ it("works", function ()
+ local a = array({ "a", "b", "c" });
+ a:push("d"):push("e");
+ assert.same({ "a", "b", "c", "d", "e" }, a);
+ end);
+ end);
+
+ describe("pop", function ()
+ it("works", function ()
+ local a = array({ "a", "b", "c" });
+ assert.equal("c", a:pop());
+ assert.same({ "a", "b", }, a);
+ end);
+ end);
+
+ describe("concat", function ()
+ it("works", function ()
+ local a = array({ "a", "b", "c" });
+ assert.equal("a,b,c", a:concat(","));
+ end);
+ end);
+
+ describe("length", function ()
+ it("works", function ()
+ local a = array({ "a", "b", "c" });
+ assert.equal(3, a:length());
+ end);
+ end);
+
+ end);
+
+ -- TODO The various array.foo(array ina, array outa) functions
+end);
+
diff --git a/spec/util_async_spec.lua b/spec/util_async_spec.lua
index d2de8c94..8123503b 100644
--- a/spec/util_async_spec.lua
+++ b/spec/util_async_spec.lua
@@ -544,6 +544,8 @@ describe("util.async", function()
assert.equal(r1.state, "ready");
end);
+ -- luacheck: ignore 211/rf
+ -- FIXME what's rf?
it("should support multiple done() calls", function ()
local processed_item;
local wait, done;
diff --git a/spec/util_dataforms_spec.lua b/spec/util_dataforms_spec.lua
index 89759035..d2d1264a 100644
--- a/spec/util_dataforms_spec.lua
+++ b/spec/util_dataforms_spec.lua
@@ -106,11 +106,26 @@ describe("util.dataforms", function ()
name = "text-single-field",
value = "text-single-value",
},
+ {
+ -- XEP-0221
+ -- TODO Validate the XML produced by this.
+ type = "text-single",
+ label = "text-single-with-media-label",
+ name = "text-single-with-media-field",
+ media = {
+ height = 24,
+ width = 32,
+ {
+ type = "image/png",
+ uri = "data:",
+ },
+ },
+ },
});
xform = some_form:form();
end);
- it("works", function ()
+ it("XML serialization looks like it should", function ()
assert.truthy(xform);
assert.truthy(st.is_stanza(xform));
assert.equal("x", xform.name);
@@ -316,7 +331,7 @@ describe("util.dataforms", function ()
end);
describe(":data", function ()
- it("works", function ()
+ it("returns something", function ()
assert.truthy(some_form:data(xform));
end);
end);
@@ -402,26 +417,63 @@ describe("util.dataforms", function ()
end);
end);
- describe("validation", function ()
+ describe("datatype validation", function ()
local f = dataforms.new {
{
name = "number",
type = "text-single",
datatype = "xs:integer",
+ range_min = -10,
+ range_max = 10,
},
};
- it("works", function ()
+ it("integer roundtrip works", function ()
local d = f:data(f:form({number = 1}));
assert.equal(1, d.number);
end);
- it("works", function ()
+ it("integer error handling works", function ()
local d,e = f:data(f:form({number = "nan"}));
assert.not_equal(1, d.number);
assert.table(e);
assert.string(e.number);
end);
+
+ it("works", function ()
+ local d,e = f:data(f:form({number = 100}));
+ assert.not_equal(100, d.number);
+ assert.table(e);
+ assert.string(e.number);
+ end);
+ end);
+ describe("media element", function ()
+ it("produced media element correctly", function ()
+ local f;
+ for field in xform:childtags("field") do
+ if field.attr.var == "text-single-with-media-field" then
+ f = field;
+ break;
+ end
+ end
+
+ assert.truthy(st.is_stanza(f));
+ assert.equal("text-single-with-media-field", f.attr.var);
+ assert.equal("text-single", f.attr.type);
+ assert.equal("text-single-with-media-label", f.attr.label);
+ assert.equal(0, iter.count(f:childtags("value")));
+
+ local m = f:get_child("media", "urn:xmpp:media-element");
+ assert.truthy(st.is_stanza(m));
+ assert.equal("24", m.attr.height);
+ assert.equal("32", m.attr.width);
+ assert.equal(1, iter.count(m:childtags("uri")));
+
+ local u = m:get_child("uri");
+ assert.truthy(st.is_stanza(u));
+ assert.equal("image/png", u.attr.type);
+ assert.equal("data:", u:get_text());
+ end);
end);
end);
diff --git a/spec/util_error_spec.lua b/spec/util_error_spec.lua
new file mode 100644
index 00000000..34edd313
--- /dev/null
+++ b/spec/util_error_spec.lua
@@ -0,0 +1,146 @@
+local errors = require "util.error"
+
+describe("util.error", function ()
+ describe("new()", function ()
+ it("works", function ()
+ local err = errors.new("bork", "bork bork");
+ assert.not_nil(err);
+ assert.equal("cancel", err.type);
+ assert.equal("undefined-condition", err.condition);
+ assert.same("bork bork", err.context);
+ end);
+
+ describe("templates", function ()
+ it("works", function ()
+ local templates = {
+ ["fail"] = {
+ type = "wait",
+ condition = "internal-server-error",
+ code = 555;
+ };
+ };
+ local err = errors.new("fail", { traceback = "in some file, somewhere" }, templates);
+ assert.equal("wait", err.type);
+ assert.equal("internal-server-error", err.condition);
+ assert.equal(555, err.code);
+ assert.same({ traceback = "in some file, somewhere" }, err.context);
+ end);
+ end);
+
+ end);
+
+ describe("is_err()", function ()
+ it("works", function ()
+ assert.truthy(errors.is_err(errors.new()));
+ assert.falsy(errors.is_err("not an error"));
+ end);
+ end);
+
+ describe("coerce", function ()
+ it("works", function ()
+ local ok, err = errors.coerce(nil, "it dun goofed");
+ assert.is_nil(ok);
+ assert.truthy(errors.is_err(err))
+ end);
+ end);
+
+ describe("from_stanza", function ()
+ it("works", function ()
+ local st = require "util.stanza";
+ local m = st.message({ type = "chat" });
+ local e = st.error_reply(m, "modify", "bad-request", nil, "error.example"):tag("extra", { xmlns = "xmpp:example.test" });
+ local err = errors.from_stanza(e);
+ assert.truthy(errors.is_err(err));
+ assert.equal("modify", err.type);
+ assert.equal("bad-request", err.condition);
+ assert.equal(e, err.context.stanza);
+ assert.equal("error.example", err.context.by);
+ assert.not_nil(err.extra.tag);
+ end);
+ end);
+
+ describe("__tostring", function ()
+ it("doesn't throw", function ()
+ assert.has_no.errors(function ()
+ -- See 6f317e51544d
+ tostring(errors.new());
+ end);
+ end);
+ end);
+
+ describe("extra", function ()
+ it("keeps some extra fields", function ()
+ local err = errors.new({condition="gone",text="Sorry mate, it's all gone",extra={uri="file:///dev/null"}});
+ assert.is_table(err.extra);
+ assert.equal("file:///dev/null", err.extra.uri);
+ end);
+ end)
+
+ describe("init", function()
+ it("basics works", function()
+ local reg = errors.init("test", {
+ broke = {type = "cancel"; condition = "internal-server-error"; text = "It broke :("};
+ nope = {type = "auth"; condition = "not-authorized"; text = "Can't let you do that Dave"};
+ });
+
+ local broke = reg.new("broke");
+ assert.equal("cancel", broke.type);
+ assert.equal("internal-server-error", broke.condition);
+ assert.equal("It broke :(", broke.text);
+ assert.equal("test", broke.source);
+
+ local nope = reg.new("nope");
+ assert.equal("auth", nope.type);
+ assert.equal("not-authorized", nope.condition);
+ assert.equal("Can't let you do that Dave", nope.text);
+ end);
+
+ it("compact mode works", function()
+ local reg = errors.init("test", "spec", {
+ broke = {"cancel"; "internal-server-error"; "It broke :("};
+ nope = {"auth"; "not-authorized"; "Can't let you do that Dave"; "sorry-dave"};
+ });
+
+ local broke = reg.new("broke");
+ assert.equal("cancel", broke.type);
+ assert.equal("internal-server-error", broke.condition);
+ assert.equal("It broke :(", broke.text);
+ assert.is_nil(broke.extra);
+
+ local nope = reg.new("nope");
+ assert.equal("auth", nope.type);
+ assert.equal("not-authorized", nope.condition);
+ assert.equal("Can't let you do that Dave", nope.text);
+ assert.equal("spec", nope.extra.namespace);
+ assert.equal("sorry-dave", nope.extra.condition);
+ end);
+
+ it("registry looks the same regardless of syntax", function()
+ local normal = errors.init("test", {
+ broke = {type = "cancel"; condition = "internal-server-error"; text = "It broke :("};
+ nope = {
+ type = "auth";
+ condition = "not-authorized";
+ text = "Can't let you do that Dave";
+ extra = {namespace = "spec"; condition = "sorry-dave"};
+ };
+ });
+ local compact1 = errors.init("test", "spec", {
+ broke = {"cancel"; "internal-server-error"; "It broke :("};
+ nope = {"auth"; "not-authorized"; "Can't let you do that Dave"; "sorry-dave"};
+ });
+ local compact2 = errors.init("test", {
+ broke = {"cancel"; "internal-server-error"; "It broke :("};
+ nope = {"auth"; "not-authorized"; "Can't let you do that Dave"};
+ });
+ assert.same(normal.registry, compact1.registry);
+
+ assert.same({
+ broke = {type = "cancel"; condition = "internal-server-error"; text = "It broke :("};
+ nope = {type = "auth"; condition = "not-authorized"; text = "Can't let you do that Dave"};
+ }, compact2.registry);
+ end);
+ end);
+
+end);
+
diff --git a/spec/util_events_spec.lua b/spec/util_events_spec.lua
index fee60f8f..fcfa6e53 100644
--- a/spec/util_events_spec.lua
+++ b/spec/util_events_spec.lua
@@ -208,5 +208,43 @@ describe("util.events", function ()
assert.spy(h).was_called(2);
end);
end);
+
+ describe("debug hooks", function ()
+ it("should get called", function ()
+ local d = spy.new(function (handler, event_name, event_data) --luacheck: ignore 212/event_name
+ return handler(event_data);
+ end);
+
+ e.add_handler("myevent", h);
+ e.fire_event("myevent");
+
+ assert.spy(h).was_called(1);
+ assert.spy(d).was_called(0);
+
+ assert.is_nil(e.set_debug_hook(d));
+
+ e.fire_event("myevent", { mydata = true });
+
+ assert.spy(h).was_called(2);
+ assert.spy(d).was_called(1);
+ assert.spy(d).was_called_with(h, "myevent", { mydata = true });
+
+ assert.equal(d, e.set_debug_hook(nil));
+
+ e.fire_event("myevent", { mydata = false });
+
+ assert.spy(h).was_called(3);
+ assert.spy(d).was_called(1);
+ end);
+ it("setting should return any existing debug hook", function ()
+ local function f() end
+ local function g() end
+ assert.is_nil(e.set_debug_hook(f));
+ assert.is_equal(f, e.set_debug_hook(g));
+ assert.is_equal(g, e.set_debug_hook(f));
+ assert.is_equal(f, e.set_debug_hook(nil));
+ assert.is_nil(e.set_debug_hook(f));
+ end);
+ end);
end);
end);
diff --git a/spec/util_format_spec.lua b/spec/util_format_spec.lua
index 7e6a0c6e..50509630 100644
--- a/spec/util_format_spec.lua
+++ b/spec/util_format_spec.lua
@@ -5,10 +5,15 @@ describe("util.format", function()
it("should work", function()
assert.equal("hello", format("%s", "hello"));
assert.equal("<nil>", format("%s"));
+ assert.equal("<nil>", format("%d"));
+ assert.equal("<nil>", format("%q"));
assert.equal(" [<nil>]", format("", nil));
assert.equal("true", format("%s", true));
assert.equal("[true]", format("%d", true));
assert.equal("% [true]", format("%%", true));
+ assert.equal("{ }", format("%q", { }));
+ assert.equal("[1.5]", format("%d", 1.5));
+ assert.equal("[7.3786976294838e+19]", format("%d", 73786976294838206464));
end);
end);
end);
diff --git a/spec/util_hashes_spec.lua b/spec/util_hashes_spec.lua
new file mode 100644
index 00000000..3639dd4e
--- /dev/null
+++ b/spec/util_hashes_spec.lua
@@ -0,0 +1,55 @@
+-- Test vectors from RFC 6070
+local hashes = require "util.hashes";
+local hex = require "util.hex";
+
+-- Also see spec for util.hmac where HMAC test cases reside
+
+describe("PBKDF2-HMAC-SHA1", function ()
+ it("test vector 1", function ()
+ local P = "password"
+ local S = "salt"
+ local c = 1
+ local DK = "0c60c80f961f0e71f3a9b524af6012062fe037a6";
+ assert.equal(DK, hex.to(hashes.pbkdf2_hmac_sha1(P, S, c)));
+ end);
+ it("test vector 2", function ()
+ local P = "password"
+ local S = "salt"
+ local c = 2
+ local DK = "ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957";
+ assert.equal(DK, hex.to(hashes.pbkdf2_hmac_sha1(P, S, c)));
+ end);
+ it("test vector 3", function ()
+ local P = "password"
+ local S = "salt"
+ local c = 4096
+ local DK = "4b007901b765489abead49d926f721d065a429c1";
+ assert.equal(DK, hex.to(hashes.pbkdf2_hmac_sha1(P, S, c)));
+ end);
+ it("test vector 4 #SLOW", function ()
+ local P = "password"
+ local S = "salt"
+ local c = 16777216
+ local DK = "eefe3d61cd4da4e4e9945b3d6ba2158c2634e984";
+ assert.equal(DK, hex.to(hashes.pbkdf2_hmac_sha1(P, S, c)));
+ end);
+end);
+
+describe("PBKDF2-HMAC-SHA256", function ()
+ it("test vector 1", function ()
+ local P = "password";
+ local S = "salt";
+ local c = 1
+ local DK = "120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b";
+ assert.equal(DK, hex.to(hashes.pbkdf2_hmac_sha256(P, S, c)));
+ end);
+ it("test vector 2", function ()
+ local P = "password";
+ local S = "salt";
+ local c = 2
+ local DK = "ae4d0c95af6b46d32d0adff928f06dd02a303f8ef3c251dfd6e2d85a95474c43";
+ assert.equal(DK, hex.to(hashes.pbkdf2_hmac_sha256(P, S, c)));
+ end);
+end);
+
+
diff --git a/spec/util_hashring_spec.lua b/spec/util_hashring_spec.lua
new file mode 100644
index 00000000..d8801774
--- /dev/null
+++ b/spec/util_hashring_spec.lua
@@ -0,0 +1,85 @@
+local hashring = require "util.hashring";
+
+describe("util.hashring", function ()
+
+ local sha256 = require "util.hashes".sha256;
+
+ local ring = hashring.new(128, sha256);
+
+ it("should fail to get a node that does not exist", function ()
+ assert.is_nil(ring:get_node("foo"))
+ end);
+
+ it("should support adding nodes", function ()
+ ring:add_node("node1");
+ end);
+
+ it("should return a single node for all keys if only one node exists", function ()
+ for i = 1, 100 do
+ assert.is_equal("node1", ring:get_node(tostring(i)))
+ end
+ end);
+
+ it("should support adding a second node", function ()
+ ring:add_node("node2");
+ end);
+
+ it("should fail to remove a non-existent node", function ()
+ assert.is_falsy(ring:remove_node("node3"));
+ end);
+
+ it("should succeed to remove a node", function ()
+ assert.is_truthy(ring:remove_node("node1"));
+ end);
+
+ it("should return the only node for all keys", function ()
+ for i = 1, 100 do
+ assert.is_equal("node2", ring:get_node(tostring(i)))
+ end
+ end);
+
+ it("should support adding multiple nodes", function ()
+ ring:add_nodes({ "node1", "node3", "node4", "node5" });
+ end);
+
+ it("should disrupt a minimal number of keys on node removal", function ()
+ local orig_ring = ring:clone();
+ local node_tallies = {};
+
+ local n = 1000;
+
+ for i = 1, n do
+ local key = tostring(i);
+ local node = ring:get_node(key);
+ node_tallies[node] = (node_tallies[node] or 0) + 1;
+ end
+
+ --[[
+ for node, key_count in pairs(node_tallies) do
+ print(node, key_count, ("%.2f%%"):format((key_count/n)*100));
+ end
+ ]]
+
+ ring:remove_node("node5");
+
+ local disrupted_keys = 0;
+ for i = 1, n do
+ local key = tostring(i);
+ if orig_ring:get_node(key) ~= ring:get_node(key) then
+ disrupted_keys = disrupted_keys + 1;
+ end
+ end
+ assert.is_equal(node_tallies["node5"], disrupted_keys);
+ end);
+
+ it("should support removing multiple nodes", function ()
+ ring:remove_nodes({"node2", "node3", "node4", "node5"});
+ end);
+
+ it("should return a single node for all keys if only one node remains", function ()
+ for i = 1, 100 do
+ assert.is_equal("node1", ring:get_node(tostring(i)))
+ end
+ end);
+
+end);
diff --git a/spec/util_hmac_spec.lua b/spec/util_hmac_spec.lua
new file mode 100644
index 00000000..a2125c3a
--- /dev/null
+++ b/spec/util_hmac_spec.lua
@@ -0,0 +1,106 @@
+-- Test cases from RFC 4231
+
+-- Yes, the lines are long, it's annoying to split the long hex things.
+-- luacheck: ignore 631
+
+local hmac = require "util.hmac";
+local hex = require "util.hex";
+
+describe("Test case 1", function ()
+ local Key = hex.from("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
+ local Data = hex.from("4869205468657265");
+ describe("HMAC-SHA-256", function ()
+ it("works", function()
+ assert.equal("b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7", hmac.sha256(Key, Data, true))
+ end);
+ end);
+ describe("HMAC-SHA-512", function ()
+ it("works", function()
+ assert.equal("87aa7cdea5ef619d4ff0b4241a1d6cb02379f4e2ce4ec2787ad0b30545e17cdedaa833b7d6b8a702038b274eaea3f4e4be9d914eeb61f1702e696c203a126854", hmac.sha512(Key, Data, true))
+ end);
+ end);
+end);
+describe("Test case 2", function ()
+ local Key = hex.from("4a656665");
+ local Data = hex.from("7768617420646f2079612077616e7420666f72206e6f7468696e673f");
+ describe("HMAC-SHA-256", function ()
+ it("works", function()
+ assert.equal("5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843", hmac.sha256(Key, Data, true))
+ end);
+ end);
+ describe("HMAC-SHA-512", function ()
+ it("works", function()
+ assert.equal("164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7ea2505549758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b636e070a38bce737", hmac.sha512(Key, Data, true))
+ end);
+ end);
+end);
+describe("Test case 3", function ()
+ local Key = hex.from("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
+ local Data = hex.from("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd");
+ describe("HMAC-SHA-256", function ()
+ it("works", function()
+ assert.equal("773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe", hmac.sha256(Key, Data, true))
+ end);
+ end);
+ describe("HMAC-SHA-512", function ()
+ it("works", function()
+ assert.equal("fa73b0089d56a284efb0f0756c890be9b1b5dbdd8ee81a3655f83e33b2279d39bf3e848279a722c806b485a47e67c807b946a337bee8942674278859e13292fb", hmac.sha512(Key, Data, true))
+ end);
+ end);
+end);
+describe("Test case 4", function ()
+ local Key = hex.from("0102030405060708090a0b0c0d0e0f10111213141516171819");
+ local Data = hex.from("cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd");
+ describe("HMAC-SHA-256", function ()
+ it("works", function()
+ assert.equal("82558a389a443c0ea4cc819899f2083a85f0faa3e578f8077a2e3ff46729665b", hmac.sha256(Key, Data, true))
+ end);
+ end);
+ describe("HMAC-SHA-512", function ()
+ it("works", function()
+ assert.equal("b0ba465637458c6990e5a8c5f61d4af7e576d97ff94b872de76f8050361ee3dba91ca5c11aa25eb4d679275cc5788063a5f19741120c4f2de2adebeb10a298dd", hmac.sha512(Key, Data, true))
+ end);
+ end);
+end);
+describe("Test case 5", function ()
+ local Key = hex.from("0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c");
+ local Data = hex.from("546573742057697468205472756e636174696f6e");
+ describe("HMAC-SHA-256", function ()
+ it("works", function()
+ assert.equal("a3b6167473100ee06e0c796c2955552b", hmac.sha256(Key, Data, true):sub(1,128/4))
+ end);
+ end);
+ describe("HMAC-SHA-512", function ()
+ it("works", function()
+ assert.equal("415fad6271580a531d4179bc891d87a6", hmac.sha512(Key, Data, true):sub(1,128/4))
+ end);
+ end);
+end);
+describe("Test case 6", function ()
+ local Key = hex.from("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
+ local Data = hex.from("54657374205573696e67204c6172676572205468616e20426c6f636b2d53697a65204b6579202d2048617368204b6579204669727374");
+ describe("HMAC-SHA-256", function ()
+ it("works", function()
+ assert.equal("60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54", hmac.sha256(Key, Data, true))
+ end);
+ end);
+ describe("HMAC-SHA-512", function ()
+ it("works", function()
+ assert.equal("80b24263c7c1a3ebb71493c1dd7be8b49b46d1f41b4aeec1121b013783f8f3526b56d037e05f2598bd0fd2215d6a1e5295e64f73f63f0aec8b915a985d786598", hmac.sha512(Key, Data, true))
+ end);
+ end);
+end);
+describe("Test case 7", function ()
+ local Key = hex.from("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
+ local Data = hex.from("5468697320697320612074657374207573696e672061206c6172676572207468616e20626c6f636b2d73697a65206b657920616e642061206c6172676572207468616e20626c6f636b2d73697a6520646174612e20546865206b6579206e6565647320746f20626520686173686564206265666f7265206265696e6720757365642062792074686520484d414320616c676f726974686d2e");
+ describe("HMAC-SHA-256", function ()
+ it("works", function()
+ assert.equal("9b09ffa71b942fcb27635fbcd5b0e944bfdc63644f0713938a7f51535c3a35e2", hmac.sha256(Key, Data, true))
+ end);
+ end);
+ describe("HMAC-SHA-512", function ()
+ it("works", function()
+ assert.equal("e37b6a775dc87dbaa4dfa9f96e5e3ffddebd71f8867289865df5a32d20cdc944b6022cac3c4982b10d5eeb55c3e4de15134676fb6de0446065c97440fa8c6a58", hmac.sha512(Key, Data, true))
+ end);
+ end);
+end);
diff --git a/spec/util_http_spec.lua b/spec/util_http_spec.lua
index 0f51a86c..c6087450 100644
--- a/spec/util_http_spec.lua
+++ b/spec/util_http_spec.lua
@@ -28,6 +28,11 @@ describe("util.http", function()
it("should decode important URL characters", function()
assert.are.equal("This & that = something", http.urldecode("This%20%26%20that%20%3d%20something"), "Important URL chars escaped");
end);
+
+ it("should decode both lower and uppercase", function ()
+ assert.are.equal("This & that = {something}.", http.urldecode("This%20%26%20that%20%3D%20%7Bsomething%7D%2E"), "Important URL chars escaped");
+ end);
+
end);
describe("#formencode()", function()
@@ -84,4 +89,23 @@ describe("util.http", function()
assert.equal("/foo/", http.normalize_path("/foo/", true));
end);
end);
+
+ describe("contains_token", function ()
+ it("is present in field", function ()
+ assert.is_true(http.contains_token("foo", "foo"));
+ assert.is_true(http.contains_token("foo, bar", "foo"));
+ assert.is_true(http.contains_token("foo,bar", "foo"));
+ assert.is_true(http.contains_token("bar, foo,baz", "foo"));
+ end);
+
+ it("is absent from field", function ()
+ assert.is_false(http.contains_token("bar", "foo"));
+ assert.is_false(http.contains_token("fooo", "foo"));
+ assert.is_false(http.contains_token("foo o,bar", "foo"));
+ end);
+
+ it("is weird", function ()
+ assert.is_(http.contains_token("fo o", "foo"));
+ end);
+ end);
end);
diff --git a/spec/util_human_io_spec.lua b/spec/util_human_io_spec.lua
new file mode 100644
index 00000000..ead117df
--- /dev/null
+++ b/spec/util_human_io_spec.lua
@@ -0,0 +1,29 @@
+describe("util.human.io", function ()
+ local human_io
+ setup(function ()
+ human_io = require "util.human.io";
+ end);
+ describe("table", function ()
+
+ it("alignment works", function ()
+ local row = human_io.table({
+ {
+ width = 3,
+ align = "right"
+ },
+ {
+ width = 3,
+ },
+ });
+
+ assert.equal(" 1 | . ", row({ 1, "." }));
+ assert.equal(" 10 | .. ", row({ 10, ".." }));
+ assert.equal("100 | ...", row({ 100, "..." }));
+ assert.equal("10… | ..…", row({ 1000, "...." }));
+
+ end);
+ end);
+end);
+
+
+
diff --git a/spec/util_human_units_spec.lua b/spec/util_human_units_spec.lua
new file mode 100644
index 00000000..4326cdd4
--- /dev/null
+++ b/spec/util_human_units_spec.lua
@@ -0,0 +1,15 @@
+local units = require "util.human.units";
+
+describe("util.human.units", function ()
+ describe("format", function ()
+ it("formats numbers with SI units", function ()
+ assert.equal("1 km", units.format(1000, "m"));
+ assert.equal("1 GJ", units.format(1000000000, "J"));
+ assert.equal("1 ms", units.format(1/1000, "s"));
+ assert.equal("10 ms", units.format(10/1000, "s"));
+ assert.equal("1 ns", units.format(1/1000000000, "s"));
+ assert.equal("1 KiB", units.format(1024, "B", 'b'));
+ assert.equal("1 MiB", units.format(1024*1024, "B", 'b'));
+ end);
+ end);
+end);
diff --git a/spec/util_indexedbheap_spec.lua b/spec/util_indexedbheap_spec.lua
new file mode 100644
index 00000000..a76f94cb
--- /dev/null
+++ b/spec/util_indexedbheap_spec.lua
@@ -0,0 +1,15 @@
+local ibh = require"util.indexedbheap";
+local h
+setup(function ()
+ h = ibh.create();
+end)
+describe("util.indexedbheap", function ()
+ pending("item can be moved from end to top", function ()
+ h:insert("a", 1);
+ h:insert("b", 2);
+ h:insert("c", 3);
+ local id = h:insert("*", 10);
+ h:reprioritize(id, 0);
+ assert.same({ 0, "*", id }, { h:pop() });
+ end)
+end);
diff --git a/spec/util_interpolation_spec.lua b/spec/util_interpolation_spec.lua
new file mode 100644
index 00000000..614d1f54
--- /dev/null
+++ b/spec/util_interpolation_spec.lua
@@ -0,0 +1,61 @@
+local template = [[
+{greet!?Hi}, {name?world}!
+]];
+local expect1 = [[
+Hello, WORLD!
+]];
+local expect2 = [[
+Hello, world!
+]];
+local expect3 = [[
+Hi, YOU!
+]];
+local template_array = [[
+{foo#{idx}. {item}
+}]]
+local expect_array = [[
+1. HELLO
+2. WORLD
+]]
+local template_func_pipe = [[
+{foo|sort#{idx}. {item}
+}]]
+local expect_func_pipe = [[
+1. A
+2. B
+3. C
+4. D
+]]
+local template_map = [[
+{foo%{idx}: {item!}
+}]]
+local expect_map = [[
+FOO: bar
+]]
+local template_not = [[
+{thing~Thing is falsy}{thing&Thing is truthy}
+]]
+local expect_not_true = [[
+Thing is truthy
+]]
+local expect_not_nil = [[
+Thing is falsy
+]]
+local expect_not_false = [[
+Thing is falsy
+]]
+describe("util.interpolation", function ()
+ it("renders", function ()
+ local render = require "util.interpolation".new("%b{}", string.upper, { sort = function (t) table.sort(t) return t end });
+ assert.equal(expect1, render(template, { greet = "Hello", name = "world" }));
+ assert.equal(expect2, render(template, { greet = "Hello" }));
+ assert.equal(expect3, render(template, { name = "you" }));
+ assert.equal(expect_array, render(template_array, { foo = { "Hello", "World" } }));
+ assert.equal(expect_func_pipe, render(template_func_pipe, { foo = { "c", "a", "d", "b", } }));
+ -- assert.equal("", render(template_func_pipe, { foo = nil })); -- FIXME
+ assert.equal(expect_map, render(template_map, { foo = { foo = "bar" } }));
+ assert.equal(expect_not_true, render(template_not, { thing = true }));
+ assert.equal(expect_not_nil, render(template_not, { thing = nil }));
+ assert.equal(expect_not_false, render(template_not, { thing = false }));
+ end);
+end);
diff --git a/spec/util_jid_spec.lua b/spec/util_jid_spec.lua
index c075212f..17cadbee 100644
--- a/spec/util_jid_spec.lua
+++ b/spec/util_jid_spec.lua
@@ -75,6 +75,56 @@ describe("util.jid", function()
end);
end);
+ local jid_escaping_test_vectors = {
+ -- From https://xmpp.org/extensions/xep-0106.xml#examples sans @example.com
+ [[space cadet]], [[space\20cadet]],
+ [[call me "ishmael"]], [[call\20me\20\22ishmael\22]],
+ [[at&t guy]], [[at\26t\20guy]],
+ [[d'artagnan]], [[d\27artagnan]],
+ [[/.fanboy]], [[\2f.fanboy]],
+ [[::foo::]], [[\3a\3afoo\3a\3a]],
+ [[<foo>]], [[\3cfoo\3e]],
+ [[user@host]], [[user\40host]],
+ [[c:\net]], [[c\3a\net]],
+ [[c:\\net]], [[c\3a\\net]],
+ [[c:\cool stuff]], [[c\3a\cool\20stuff]],
+ [[c:\5commas]], [[c\3a\5c5commas]],
+
+ -- Section 4.2
+ [[\3and\2is\5cool]], [[\5c3and\2is\5c5cool]],
+
+ -- From aioxmpp
+ [[\5c]], [[\5c5c]],
+ -- [[\5C]], [[\5C]],
+ [[\2plus\2is\4]], [[\2plus\2is\4]],
+ [[foo\bar]], [[foo\bar]],
+ [[foo\41r]], [[foo\41r]],
+ -- additional test vectors
+ [[call\20me]], [[call\5c20me]],
+ };
+
+ describe("#escape()", function ()
+ it("should work", function ()
+ for i = 1, #jid_escaping_test_vectors, 2 do
+ local original = jid_escaping_test_vectors[i];
+ local escaped = jid_escaping_test_vectors[i+1];
+
+ assert.are.equal(escaped, jid.escape(original), ("Escapes '%s' -> '%s'"):format(original, escaped));
+ end
+ end);
+ end)
+
+ describe("#unescape()", function ()
+ it("should work", function ()
+ for i = 1, #jid_escaping_test_vectors, 2 do
+ local original = jid_escaping_test_vectors[i];
+ local escaped = jid_escaping_test_vectors[i+1];
+
+ assert.are.equal(original, jid.unescape(escaped), ("Unescapes '%s' -> '%s'"):format(escaped, original));
+ end
+ end);
+ end)
+
it("should work with nodes", function()
local function test(_jid, expected_node)
assert.are.equal(jid.node(_jid), expected_node, "Unexpected node for "..tostring(_jid));
diff --git a/spec/util_json_spec.lua b/spec/util_json_spec.lua
index 43360540..f07cd525 100644
--- a/spec/util_json_spec.lua
+++ b/spec/util_json_spec.lua
@@ -1,5 +1,6 @@
local json = require "util.json";
+local array = require "util.array";
describe("util.json", function()
describe("#encode()", function()
@@ -67,4 +68,13 @@ describe("util.json", function()
end
end);
end)
+
+ describe("util.array integration", function ()
+ it("works", function ()
+ assert.equal("[]", json.encode(array()));
+ assert.equal("[1,2,3]", json.encode(array({1,2,3})));
+ assert.equal(getmetatable(array()), getmetatable(json.decode("[]")));
+ end);
+ end);
+
end);
diff --git a/spec/util_jwt_spec.lua b/spec/util_jwt_spec.lua
new file mode 100644
index 00000000..b391a870
--- /dev/null
+++ b/spec/util_jwt_spec.lua
@@ -0,0 +1,20 @@
+local jwt = require "util.jwt";
+
+describe("util.jwt", function ()
+ it("validates", function ()
+ local key = "secret";
+ local token = jwt.sign(key, { payload = "this" });
+ assert.string(token);
+ local ok, parsed = jwt.verify(key, token);
+ assert.truthy(ok)
+ assert.same({ payload = "this" }, parsed);
+ end);
+ it("rejects invalid", function ()
+ local key = "secret";
+ local token = jwt.sign("wrong", { payload = "this" });
+ assert.string(token);
+ local ok = jwt.verify(key, token);
+ assert.falsy(ok)
+ end);
+end);
+
diff --git a/spec/util_promise_spec.lua b/spec/util_promise_spec.lua
index 65d252f6..6fb5d6bd 100644
--- a/spec/util_promise_spec.lua
+++ b/spec/util_promise_spec.lua
@@ -248,6 +248,30 @@ describe("util.promise", function ()
assert.spy(cb3).was_called(1);
assert.spy(cb3).was_called_with("goodbye");
end);
+
+ it("ordinary values", function ()
+ local p = promise.resolve()
+ local cb = spy.new(function ()
+ return "hello"
+ end);
+ local cb2 = spy.new(function () end);
+ p:next(cb):next(cb2);
+ assert.spy(cb).was_called(1);
+ assert.spy(cb2).was_called(1);
+ assert.spy(cb2).was_called_with("hello");
+ end);
+
+ it("nil", function ()
+ local p = promise.resolve()
+ local cb = spy.new(function ()
+ return
+ end);
+ local cb2 = spy.new(function () end);
+ p:next(cb):next(cb2);
+ assert.spy(cb).was_called(1);
+ assert.spy(cb2).was_called(1);
+ assert.spy(cb2).was_called_with(nil);
+ end);
end);
describe("race()", function ()
@@ -329,6 +353,60 @@ describe("util.promise", function ()
assert.equal("fail", result);
end);
end);
+ describe("all_settled()", function ()
+ it("works with fulfilled promises", function ()
+ local p1, p2 = promise.resolve("yep"), promise.resolve("nope");
+ local p = promise.all_settled({ p1, p2 });
+ local result;
+ p:next(function (v)
+ result = v;
+ end);
+ assert.same({
+ { status = "fulfilled", value = "yep" };
+ { status = "fulfilled", value = "nope" };
+ }, result);
+ end);
+ it("works with pending promises", function ()
+ local r1, r2;
+ local p1, p2 = promise.new(function (resolve) r1 = resolve end), promise.new(function (resolve) r2 = resolve end);
+ local p = promise.all_settled({ p1, p2 });
+
+ local result;
+ local cb = spy.new(function (v)
+ result = v;
+ end);
+ p:next(cb);
+ assert.spy(cb).was_called(0);
+ r2("yep");
+ assert.spy(cb).was_called(0);
+ r1("nope");
+ assert.spy(cb).was_called(1);
+ assert.same({
+ { status = "fulfilled", value = "nope" };
+ { status = "fulfilled", value = "yep" };
+ }, result);
+ end);
+ it("works when some promises reject", function ()
+ local r1, r2;
+ local p1, p2 = promise.new(function (resolve) r1 = resolve end), promise.new(function (_, reject) r2 = reject end);
+ local p = promise.all_settled({ p1, p2 });
+
+ local result;
+ local cb = spy.new(function (v)
+ result = v;
+ end);
+ p:next(cb);
+ assert.spy(cb).was_called(0);
+ r2("this fails");
+ assert.spy(cb).was_called(0);
+ r1("this succeeds");
+ assert.spy(cb).was_called(1);
+ assert.same({
+ { status = "fulfilled", value = "this succeeds" };
+ { status = "rejected", reason = "this fails" };
+ }, result);
+ end);
+ end);
describe("catch()", function ()
it("works", function ()
local result;
diff --git a/spec/util_pubsub_spec.lua b/spec/util_pubsub_spec.lua
index c982fb36..f876089e 100644
--- a/spec/util_pubsub_spec.lua
+++ b/spec/util_pubsub_spec.lua
@@ -101,13 +101,14 @@ describe("util.pubsub", function ()
assert(service:publish("node", true, "1", "item 1", { myoption = true }));
local ok, config = assert(service:get_node_config("node", true));
+ assert.truthy(ok);
assert.equals(true, config.myoption);
end);
it("fails to publish to a node with differing config", function ()
local ok, err = service:publish("node", true, "1", "item 2", { myoption = false });
assert.falsy(ok);
- assert.equals("precondition-not-met", err);
+ assert.equals("precondition-not-met", err.pubsub_condition);
end);
it("allows to publish to a node with differing config when only defaults are suggested", function ()
@@ -229,6 +230,7 @@ describe("util.pubsub", function ()
end);
it("should be the default", function ()
local ok, config = service:get_node_config("test", true);
+ assert.truthy(ok);
assert.equal("open", config.access_model);
end);
it("should allow anyone to subscribe", function ()
@@ -250,6 +252,7 @@ describe("util.pubsub", function ()
end);
it("should be present in the configuration", function ()
local ok, config = service:get_node_config("test", true);
+ assert.truthy(ok);
assert.equal("whitelist", config.access_model);
end);
it("should not allow anyone to subscribe", function ()
@@ -294,6 +297,7 @@ describe("util.pubsub", function ()
end);
it("should be the default", function ()
local ok, config = service:get_node_config("test", true);
+ assert.truthy(ok);
assert.equal("publishers", config.publish_model);
end);
it("should not allow anyone to publish", function ()
@@ -304,6 +308,7 @@ describe("util.pubsub", function ()
end);
it("should allow publishers to publish", function ()
assert(service:set_affiliation("test", true, "mypublisher", "publisher"));
+ -- luacheck: ignore 211/err
local ok, err = service:publish("test", "mypublisher", "item1", "foo");
assert.is_true(ok);
end);
@@ -342,6 +347,7 @@ describe("util.pubsub", function ()
end);
it("should allow publishers to publish without a subscription", function ()
assert(service:set_affiliation("test", true, "mypublisher", "publisher"));
+ -- luacheck: ignore 211/err
local ok, err = service:publish("test", "mypublisher", "item1", "foo");
assert.is_true(ok);
end);
@@ -477,4 +483,30 @@ describe("util.pubsub", function ()
end);
+ describe("subscriber filter", function ()
+ it("works", function ()
+ local filter = spy.new(function (subs) -- luacheck: ignore 212/subs
+ return {["modified"] = true};
+ end);
+ local broadcaster = spy.new(function (notif_type, node_name, subscribers, item) -- luacheck: ignore 212
+ end);
+ local service = pubsub.new({
+ subscriber_filter = filter;
+ broadcaster = broadcaster;
+ });
+
+ local ok = service:create("node", true);
+ assert.truthy(ok);
+
+ local ok = service:add_subscription("node", true, "someone");
+ assert.truthy(ok);
+
+ local ok = service:publish("node", true, "1", "item");
+ assert.truthy(ok);
+ -- TODO how to match table arguments?
+ assert.spy(filter).was_called();
+ assert.spy(broadcaster).was_called();
+ end);
+ end);
+
end);
diff --git a/spec/util_queue_spec.lua b/spec/util_queue_spec.lua
index 7cd3d695..d73f523d 100644
--- a/spec/util_queue_spec.lua
+++ b/spec/util_queue_spec.lua
@@ -100,4 +100,41 @@ describe("util.queue", function()
end);
end);
+ describe("consume()", function ()
+ it("should work", function ()
+ local q = queue.new(10);
+ for i = 1, 5 do
+ q:push(i);
+ end
+ local c = 0;
+ for i in q:consume() do
+ assert(i == c + 1);
+ assert(q:count() == (5-i));
+ c = i;
+ end
+ end);
+
+ it("should work even if items are pushed in the loop", function ()
+ local q = queue.new(10);
+ for i = 1, 5 do
+ q:push(i);
+ end
+ local c = 0;
+ for i in q:consume() do
+ assert(i == c + 1);
+ if c < 3 then
+ assert(q:count() == (5-i));
+ else
+ assert(q:count() == (6-i));
+ end
+
+ c = i;
+
+ if c == 3 then
+ q:push(6);
+ end
+ end
+ assert.equal(c, 6);
+ end);
+ end);
end);
diff --git a/spec/util_ringbuffer_spec.lua b/spec/util_ringbuffer_spec.lua
new file mode 100644
index 00000000..633885a8
--- /dev/null
+++ b/spec/util_ringbuffer_spec.lua
@@ -0,0 +1,103 @@
+local rb = require "util.ringbuffer";
+describe("util.ringbuffer", function ()
+ describe("#new", function ()
+ it("has a constructor", function ()
+ assert.Function(rb.new);
+ end);
+ it("can be created", function ()
+ assert.truthy(rb.new());
+ end);
+ it("won't create an empty buffer", function ()
+ assert.has_error(function ()
+ rb.new(0);
+ end);
+ end);
+ it("won't create a negatively sized buffer", function ()
+ assert.has_error(function ()
+ rb.new(-1);
+ end);
+ end);
+ end);
+ describe(":write", function ()
+ local b = rb.new();
+ it("works", function ()
+ assert.truthy(b:write("hi"));
+ end);
+ end);
+
+ describe(":discard", function ()
+ local b = rb.new();
+ it("works", function ()
+ assert.truthy(b:write("hello world"));
+ assert.truthy(b:discard(6));
+ assert.equal(5, #b);
+ assert.equal("world", b:read(5));
+ end);
+ end);
+
+ describe(":sub", function ()
+ -- Helper function to compare buffer:sub() with string:sub()
+ local function test_sub(b, x, y)
+ local s = b:read(#b, true);
+ local string_result, buffer_result = s:sub(x, y), b:sub(x, y);
+ assert.equals(string_result, buffer_result, ("buffer:sub(%d, %s) does not match string:sub()"):format(x, y and ("%d"):format(y) or "nil"));
+ end
+
+ it("works", function ()
+ local b = rb.new();
+ assert.truthy(b:write("hello world"));
+ assert.equals("hello", b:sub(1, 5));
+ end);
+
+ it("supports optional end parameter", function ()
+ local b = rb.new();
+ assert.truthy(b:write("hello world"));
+ assert.equals("hello world", b:sub(1));
+ assert.equals("world", b:sub(-5));
+ end);
+
+ it("is equivalent to string:sub", function ()
+ local b = rb.new(6);
+ assert.truthy(b:write("foobar"));
+ b:read(3);
+ b:write("foo");
+ for i = -13, 13 do
+ for j = -13, 13 do
+ test_sub(b, i, j);
+ end
+ end
+ end);
+ end);
+
+ describe(":byte", function ()
+ -- Helper function to compare buffer:byte() with string:byte()
+ local function test_byte(b, x, y)
+ local s = b:read(#b, true);
+ local string_result, buffer_result = {s:byte(x, y)}, {b:byte(x, y)};
+ assert.same(string_result, buffer_result, ("buffer:byte(%d, %s) does not match string:byte()"):format(x, y and ("%d"):format(y) or "nil"));
+ end
+
+ it("is equivalent to string:byte", function ()
+ local b = rb.new(6);
+ assert.truthy(b:write("foobar"));
+ b:read(3);
+ b:write("foo");
+ test_byte(b, 1);
+ test_byte(b, 3);
+ test_byte(b, -1);
+ test_byte(b, -3);
+ for i = -13, 13 do
+ for j = -13, 13 do
+ test_byte(b, i, j);
+ end
+ end
+ end);
+
+ it("works with characters > 127", function ()
+ local b = rb.new();
+ b:write(string.char(0, 140));
+ local r = { b:byte(1, 2) };
+ assert.same({ 0, 140 }, r);
+ end);
+ end);
+end);
diff --git a/spec/util_rsm_spec.lua b/spec/util_rsm_spec.lua
new file mode 100644
index 00000000..08fb1b02
--- /dev/null
+++ b/spec/util_rsm_spec.lua
@@ -0,0 +1,89 @@
+local rsm = require "util.rsm";
+local xml = require "util.xml";
+
+local function strip(s)
+ return (s:gsub(">%s+<", "><"));
+end
+
+describe("util.rsm", function ()
+ describe("parse", function ()
+ it("works", function ()
+ local test = xml.parse(strip([[
+ <set xmlns='http://jabber.org/protocol/rsm'>
+ <max>10</max>
+ </set>
+ ]]));
+ assert.same({ max = 10 }, rsm.parse(test));
+ end);
+
+ it("works", function ()
+ local test = xml.parse(strip([[
+ <set xmlns='http://jabber.org/protocol/rsm'>
+ <first index='0'>saint@example.org</first>
+ <last>peterpan@neverland.lit</last>
+ <count>800</count>
+ </set>
+ ]]));
+ assert.same({ first = { index = 0, "saint@example.org" }, last = "peterpan@neverland.lit", count = 800 }, rsm.parse(test));
+ end);
+
+ it("works", function ()
+ local test = xml.parse(strip([[
+ <set xmlns='http://jabber.org/protocol/rsm'>
+ <max>10</max>
+ <before>peter@pixyland.org</before>
+ </set>
+ ]]));
+ assert.same({ max = 10, before = "peter@pixyland.org" }, rsm.parse(test));
+ end);
+
+ end);
+
+ describe("generate", function ()
+ it("works", function ()
+ local test = xml.parse(strip([[
+ <set xmlns='http://jabber.org/protocol/rsm'>
+ <max>10</max>
+ </set>
+ ]]));
+ local res = rsm.generate({ max = 10 });
+ assert.same(test:get_child_text("max"), res:get_child_text("max"));
+ end);
+
+ it("works", function ()
+ local test = xml.parse(strip([[
+ <set xmlns='http://jabber.org/protocol/rsm'>
+ <first index='0'>saint@example.org</first>
+ <last>peterpan@neverland.lit</last>
+ <count>800</count>
+ </set>
+ ]]));
+ local res = rsm.generate({ first = { index = 0, "saint@example.org" }, last = "peterpan@neverland.lit", count = 800 });
+ assert.same(test:get_child("first").attr.index, res:get_child("first").attr.index);
+ assert.same(test:get_child_text("first"), res:get_child_text("first"));
+ assert.same(test:get_child_text("last"), res:get_child_text("last"));
+ assert.same(test:get_child_text("count"), res:get_child_text("count"));
+ end);
+
+ it("works", function ()
+ local test = xml.parse(strip([[
+ <set xmlns='http://jabber.org/protocol/rsm'>
+ <max>10</max>
+ <before>peter@pixyland.org</before>
+ </set>
+ ]]));
+ local res = rsm.generate({ max = 10, before = "peter@pixyland.org" });
+ assert.same(test:get_child_text("max"), res:get_child_text("max"));
+ assert.same(test:get_child_text("before"), res:get_child_text("before"));
+ end);
+
+ it("handles floats", function ()
+ local r1 = rsm.generate({ max = 10.0, count = 100.0, first = { index = 1.0, "foo" } });
+ assert.equal("10", r1:get_child_text("max"));
+ assert.equal("100", r1:get_child_text("count"));
+ assert.equal("1", r1:get_child("first").attr.index);
+ end);
+
+ end);
+end);
+
diff --git a/spec/util_sasl_spec.lua b/spec/util_sasl_spec.lua
new file mode 100644
index 00000000..368291b3
--- /dev/null
+++ b/spec/util_sasl_spec.lua
@@ -0,0 +1,43 @@
+local sasl = require "util.sasl";
+
+-- profile * mechanism
+-- callbacks could use spies instead
+
+describe("util.sasl", function ()
+ describe("plain_test profile", function ()
+ local profile = {
+ plain_test = function (_, username, password, realm)
+ assert.equals("user", username)
+ assert.equals("pencil", password)
+ assert.equals("sasl.test", realm)
+ return true, true;
+ end;
+ };
+ it("works with PLAIN", function ()
+ local plain = sasl.new("sasl.test", profile);
+ assert.truthy(plain:select("PLAIN"));
+ assert.truthy(plain:process("\000user\000pencil"));
+ assert.equals("user", plain.username);
+ end);
+ end);
+
+ describe("plain profile", function ()
+ local profile = {
+ plain = function (_, username, realm)
+ assert.equals("user", username)
+ assert.equals("sasl.test", realm)
+ return "pencil", true;
+ end;
+ };
+
+ it("works with PLAIN", function ()
+ local plain = sasl.new("sasl.test", profile);
+ assert.truthy(plain:select("PLAIN"));
+ assert.truthy(plain:process("\000user\000pencil"));
+ assert.equals("user", plain.username);
+ end);
+
+ -- TODO SCRAM
+ end);
+end);
+
diff --git a/spec/util_stanza_spec.lua b/spec/util_stanza_spec.lua
index 6fbae41a..535f783b 100644
--- a/spec/util_stanza_spec.lua
+++ b/spec/util_stanza_spec.lua
@@ -1,5 +1,6 @@
local st = require "util.stanza";
+local errors = require "util.error";
describe("util.stanza", function()
describe("#preserialize()", function()
@@ -95,20 +96,31 @@ describe("util.stanza", function()
describe("#iq()", function()
it("should create an iq stanza", function()
- local i = st.iq({ id = "foo" });
+ local i = st.iq({ type = "get", id = "foo" });
assert.are.equal("iq", i.name);
assert.are.equal("foo", i.attr.id);
+ assert.are.equal("get", i.attr.type);
end);
- it("should reject stanzas with no id", function ()
+ it("should reject stanzas with no attributes", function ()
assert.has.error_match(function ()
st.iq();
- end, "id attribute");
+ end, "attributes");
+ end);
+
+ it("should reject stanzas with no id", function ()
assert.has.error_match(function ()
- st.iq({ foo = "bar" });
+ st.iq({ type = "get" });
end, "id attribute");
end);
+
+ it("should reject stanzas with no type", function ()
+ assert.has.error_match(function ()
+ st.iq({ id = "foo" });
+ end, "type attribute");
+
+ end);
end);
describe("#presence()", function ()
@@ -159,6 +171,19 @@ describe("util.stanza", function()
assert.are.equal(r.attr.type, "result");
assert.are.equal(#r.tags, 0, "A reply should not include children of the original stanza");
end);
+
+ it("should reject not-stanzas", function ()
+ assert.has.error_match(function ()
+ st.reply(not "a stanza");
+ end, "expected stanza");
+ end);
+
+ it("should reject not-stanzas", function ()
+ assert.has.error_match(function ()
+ st.reply({name="x"});
+ end, "expected stanza");
+ end);
+
end);
describe("#error_reply()", function()
@@ -167,13 +192,14 @@ describe("util.stanza", function()
local s = st.stanza("s", { to = "touser", from = "fromuser", id = "123" })
:tag("child1");
-- Make reply stanza
- local r = st.error_reply(s, "cancel", "service-unavailable");
+ local r = st.error_reply(s, "cancel", "service-unavailable", nil, "host");
assert.are.equal(r.name, s.name);
assert.are.equal(r.id, s.id);
assert.are.equal(r.attr.to, s.attr.from);
assert.are.equal(r.attr.from, s.attr.to);
assert.are.equal(#r.tags, 1);
assert.are.equal(r.tags[1].tags[1].name, "service-unavailable");
+ assert.are.equal(r.tags[1].attr.by, "host");
end);
it("should work for <iq get>", function()
@@ -190,8 +216,79 @@ describe("util.stanza", function()
assert.are.equal(#r.tags, 1);
assert.are.equal(r.tags[1].tags[1].name, "service-unavailable");
end);
+
+ it("should reject not-stanzas", function ()
+ assert.has.error_match(function ()
+ st.error_reply(not "a stanza", "modify", "bad-request");
+ end, "expected stanza");
+ end);
+
+ it("should reject stanzas of type error", function ()
+ assert.has.error_match(function ()
+ st.error_reply(st.message({type="error"}), "cancel", "conflict");
+ end, "got stanza of type error");
+ assert.has.error_match(function ()
+ st.error_reply(st.error_reply(st.message({type="chat"}), "modify", "forbidden"), "cancel", "service-unavailable");
+ end, "got stanza of type error");
+ end);
+
+ describe("util.error integration", function ()
+ it("should accept util.error objects", function ()
+ local s = st.message({ to = "touser", from = "fromuser", id = "123", type = "chat" }, "Hello");
+ local e = errors.new({ type = "modify", condition = "not-acceptable", text = "Bork bork bork" }, { by = "this.test" });
+ local r = st.error_reply(s, e);
+
+ assert.are.equal(r.name, s.name);
+ assert.are.equal(r.id, s.id);
+ assert.are.equal(r.attr.to, s.attr.from);
+ assert.are.equal(r.attr.from, s.attr.to);
+ assert.are.equal(r.attr.type, "error");
+ assert.are.equal(r.tags[1].name, "error");
+ assert.are.equal(r.tags[1].attr.type, e.type);
+ assert.are.equal(r.tags[1].tags[1].name, e.condition);
+ assert.are.equal(r.tags[1].tags[2]:get_text(), e.text);
+ assert.are.equal("this.test", r.tags[1].attr.by);
+ end);
+
+ it("should accept util.error objects with an URI", function ()
+ local s = st.message({ to = "touser", from = "fromuser", id = "123", type = "chat" }, "Hello");
+ local gone = errors.new({ condition = "gone", extra = { uri = "file:///dev/null" } })
+ local gonner = st.error_reply(s, gone);
+ assert.are.equal("gone", gonner.tags[1].tags[1].name);
+ assert.are.equal("file:///dev/null", gonner.tags[1].tags[1][1]);
+ end);
+
+ it("should accept util.error objects with application specific error", function ()
+ local s = st.message({ to = "touser", from = "fromuser", id = "123", type = "chat" }, "Hello");
+ local e = errors.new({ condition = "internal-server-error", text = "Namespaced thing happened",
+ extra = {namespace="xmpp:example.test", condition="this-happened"} })
+ local r = st.error_reply(s, e);
+ assert.are.equal("xmpp:example.test", r.tags[1].tags[3].attr.xmlns);
+ assert.are.equal("this-happened", r.tags[1].tags[3].name);
+
+ local e2 = errors.new({ condition = "internal-server-error", text = "Namespaced thing happened",
+ extra = {tag=st.stanza("that-happened", { xmlns = "xmpp:example.test", ["another-attribute"] = "here" })} })
+ local r2 = st.error_reply(s, e2);
+ assert.are.equal("xmpp:example.test", r2.tags[1].tags[3].attr.xmlns);
+ assert.are.equal("that-happened", r2.tags[1].tags[3].name);
+ assert.are.equal("here", r2.tags[1].tags[3].attr["another-attribute"]);
+ end);
+ end);
end);
+ describe("#get_error()", function ()
+ describe("basics", function ()
+ local s = st.message();
+ local e = st.error_reply(s, "cancel", "not-acceptable", "UNACCEPTABLE!!!! ONE MILLION YEARS DUNGEON!")
+ :tag("dungeon", { xmlns = "urn:uuid:c9026187-5b05-4e70-b265-c3b6338a7d0f", period="1000000years"});
+ local typ, cond, text, extra = e:get_error();
+ assert.equal("cancel", typ);
+ assert.equal("not-acceptable", cond);
+ assert.equal("UNACCEPTABLE!!!! ONE MILLION YEARS DUNGEON!", text);
+ assert.not_nil(extra)
+ end)
+ end)
+
describe("should reject #invalid", function ()
local invalid_names = {
["empty string"] = "", ["characters"] = "<>";
@@ -370,4 +467,43 @@ describe("util.stanza", function()
end);
end);
end);
+
+ describe("top_tag", function ()
+ local xml_parse = require "util.xml".parse;
+ it("works", function ()
+ local s = st.message({type="chat"}, "Hello");
+ local top_tag = s:top_tag();
+ assert.is_string(top_tag);
+ assert.not_equal("/>", top_tag:sub(-2, -1));
+ assert.equal(">", top_tag:sub(-1, -1));
+ local s2 = xml_parse(top_tag.."</message>");
+ assert(st.is_stanza(s2));
+ assert.equal("message", s2.name);
+ assert.equal(0, #s2);
+ assert.equal(0, #s2.tags);
+ assert.equal("chat", s2.attr.type);
+ end);
+
+ it("works with namespaced attributes", function ()
+ local s = xml_parse[[<message foo:bar='true' xmlns:foo='my-awesome-ns'/>]];
+ local top_tag = s:top_tag();
+ assert.is_string(top_tag);
+ assert.not_equal("/>", top_tag:sub(-2, -1));
+ assert.equal(">", top_tag:sub(-1, -1));
+ local s2 = xml_parse(top_tag.."</message>");
+ assert(st.is_stanza(s2));
+ assert.equal("message", s2.name);
+ assert.equal(0, #s2);
+ assert.equal(0, #s2.tags);
+ assert.equal("true", s2.attr["my-awesome-ns\1bar"]);
+ end);
+ end);
+
+ describe("indent", function ()
+ local s = st.stanza("foo"):text("\n"):tag("bar"):tag("baz"):up():text_tag("cow", "moo");
+ assert.equal("<foo>\n\t<bar>\n\t\t<baz/>\n\t\t<cow>moo</cow>\n\t</bar>\n</foo>", tostring(s:indent()));
+ assert.equal("<foo>\n <bar>\n <baz/>\n <cow>moo</cow>\n </bar>\n</foo>", tostring(s:indent(1, " ")));
+ assert.equal("<foo>\n\t\t<bar>\n\t\t\t<baz/>\n\t\t\t<cow>moo</cow>\n\t\t</bar>\n\t</foo>", tostring(s:indent(2, "\t")));
+ end);
+
end);
diff --git a/spec/util_table_spec.lua b/spec/util_table_spec.lua
new file mode 100644
index 00000000..76f54b69
--- /dev/null
+++ b/spec/util_table_spec.lua
@@ -0,0 +1,17 @@
+local u_table = require "util.table";
+describe("util.table", function ()
+ describe("create()", function ()
+ it("works", function ()
+ -- Can't test the allocated sizes of the table, so what you gonna do?
+ assert.is.table(u_table.create(1,1));
+ end);
+ end);
+
+ describe("pack()", function ()
+ it("works", function ()
+ assert.same({ "lorem", "ipsum", "dolor", "sit", "amet", n = 5 }, u_table.pack("lorem", "ipsum", "dolor", "sit", "amet"));
+ end);
+ end);
+end);
+
+
diff --git a/spec/util_throttle_spec.lua b/spec/util_throttle_spec.lua
index 75daf1b9..985afae8 100644
--- a/spec/util_throttle_spec.lua
+++ b/spec/util_throttle_spec.lua
@@ -88,7 +88,7 @@ describe("util.throttle", function()
later(0.1);
a:update();
end
- assert(math.abs(a.balance - 1) < 0.0001); -- incremental updates cause rouding errors
+ assert(math.abs(a.balance - 1) < 0.0001); -- incremental updates cause rounding errors
end);
end);