aboutsummaryrefslogtreecommitdiffstats
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/core_configmanager_spec.lua31
-rw-r--r--spec/core_moduleapi_spec.lua76
-rw-r--r--spec/json/fail1.json1
-rw-r--r--spec/json/fail10.json1
-rw-r--r--spec/json/fail11.json1
-rw-r--r--spec/json/fail12.json1
-rw-r--r--spec/json/fail13.json1
-rw-r--r--spec/json/fail14.json1
-rw-r--r--spec/json/fail15.json1
-rw-r--r--spec/json/fail16.json1
-rw-r--r--spec/json/fail17.json1
-rw-r--r--spec/json/fail18.json1
-rw-r--r--spec/json/fail19.json1
-rw-r--r--spec/json/fail2.json1
-rw-r--r--spec/json/fail20.json1
-rw-r--r--spec/json/fail21.json1
-rw-r--r--spec/json/fail22.json1
-rw-r--r--spec/json/fail23.json1
-rw-r--r--spec/json/fail24.json1
-rw-r--r--spec/json/fail25.json1
-rw-r--r--spec/json/fail26.json1
-rw-r--r--spec/json/fail27.json2
-rw-r--r--spec/json/fail28.json2
-rw-r--r--spec/json/fail29.json1
-rw-r--r--spec/json/fail3.json1
-rw-r--r--spec/json/fail30.json1
-rw-r--r--spec/json/fail31.json1
-rw-r--r--spec/json/fail32.json1
-rw-r--r--spec/json/fail33.json1
-rw-r--r--spec/json/fail4.json1
-rw-r--r--spec/json/fail5.json1
-rw-r--r--spec/json/fail6.json1
-rw-r--r--spec/json/fail7.json1
-rw-r--r--spec/json/fail8.json1
-rw-r--r--spec/json/fail9.json1
-rw-r--r--spec/json/pass1.json58
-rw-r--r--spec/json/pass2.json1
-rw-r--r--spec/json/pass3.json6
-rw-r--r--spec/muc_util_spec.lua35
-rw-r--r--spec/net_http_parser_spec.lua52
-rw-r--r--spec/net_http_server_spec.lua13
-rw-r--r--spec/scansion/basic.scs16
-rw-r--r--spec/scansion/basic_message.scs174
-rw-r--r--spec/scansion/basic_roster.scs39
-rw-r--r--spec/scansion/issue505.scs77
-rw-r--r--spec/scansion/issue978-multi.scs109
-rw-r--r--spec/scansion/issue978.scs84
-rw-r--r--spec/scansion/muc_mediated_invite.scs74
-rw-r--r--spec/scansion/muc_password-trunk.scs141
-rw-r--r--spec/scansion/muc_password.scs149
-rw-r--r--spec/scansion/muc_register.scs286
-rw-r--r--spec/scansion/pubsub_advanced.scs162
-rw-r--r--spec/scansion/pubsub_basic.scs102
-rw-r--r--spec/scansion/pubsub_createdelete.scs61
-rw-r--r--spec/utf8_sequences.txt52
-rw-r--r--spec/util_async_spec.lua616
-rw-r--r--spec/util_cache_spec.lua316
-rw-r--r--spec/util_dataforms_spec.lua427
-rw-r--r--spec/util_datetime_spec.lua76
-rw-r--r--spec/util_encodings_spec.lua41
-rw-r--r--spec/util_events_spec.lua212
-rw-r--r--spec/util_format_spec.lua14
-rw-r--r--spec/util_http_spec.lua64
-rw-r--r--spec/util_ip_spec.lua103
-rw-r--r--spec/util_iterators_spec.lua14
-rw-r--r--spec/util_jid_spec.lua146
-rw-r--r--spec/util_json_spec.lua70
-rw-r--r--spec/util_multitable_spec.lua60
-rw-r--r--spec/util_pubsub_spec.lua342
-rw-r--r--spec/util_queue_spec.lua103
-rw-r--r--spec/util_random_spec.lua19
-rw-r--r--spec/util_rfc6724_spec.lua97
-rw-r--r--spec/util_stanza_spec.lua338
-rw-r--r--spec/util_throttle_spec.lua150
-rw-r--r--spec/util_time_spec.lua31
-rw-r--r--spec/util_uuid_spec.lua25
-rw-r--r--spec/util_xml_spec.lua20
-rw-r--r--spec/util_xmppstream_spec.lua136
78 files changed, 5253 insertions, 0 deletions
diff --git a/spec/core_configmanager_spec.lua b/spec/core_configmanager_spec.lua
new file mode 100644
index 00000000..afb7d492
--- /dev/null
+++ b/spec/core_configmanager_spec.lua
@@ -0,0 +1,31 @@
+
+local configmanager = require "core.configmanager";
+
+describe("core.configmanager", function()
+ describe("#get()", function()
+ it("should work", function()
+ configmanager.set("example.com", "testkey", 123);
+ assert.are.equal(123, configmanager.get("example.com", "testkey"), "Retrieving a set key");
+
+ 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");
+
+ 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");
+
+ assert.are.equal(nil, configmanager.get(), "No parameters to get()");
+ assert.are.equal(nil, configmanager.get("undefined host"), "Getting for undefined host");
+ assert.are.equal(nil, configmanager.get("undefined host", "undefined key"), "Getting for undefined host & key");
+ end);
+ end);
+
+ describe("#set()", function()
+ it("should work", function()
+ assert.are.equal(false, configmanager.set("*"), "Set with no key");
+
+ assert.are.equal(true, configmanager.set("*", "set_test", "testkey"), "Setting a nil global value");
+ assert.are.equal(true, configmanager.set("*", "set_test", "testkey", 123), "Setting a global value");
+ end);
+ end);
+end);
diff --git a/spec/core_moduleapi_spec.lua b/spec/core_moduleapi_spec.lua
new file mode 100644
index 00000000..20431935
--- /dev/null
+++ b/spec/core_moduleapi_spec.lua
@@ -0,0 +1,76 @@
+
+package.loaded["core.configmanager"] = {};
+package.loaded["core.statsmanager"] = {};
+package.loaded["net.server"] = {};
+
+local set = require "util.set";
+
+_G.prosody = { hosts = {}, core_post_stanza = true };
+
+local api = require "core.moduleapi";
+
+local module = setmetatable({}, {__index = api});
+local opt = nil;
+function module:log() end
+function module:get_option(name)
+ if name == "opt" then
+ return opt;
+ else
+ return nil;
+ end
+end
+
+function test_option_value(value, returns)
+ opt = value;
+ assert(module:get_option_number("opt") == returns.number, "number doesn't match");
+ assert(module:get_option_string("opt") == returns.string, "string doesn't match");
+ assert(module:get_option_boolean("opt") == returns.boolean, "boolean doesn't match");
+
+ if type(returns.array) == "table" then
+ local target_array, returned_array = returns.array, module:get_option_array("opt");
+ assert(#target_array == #returned_array, "array length doesn't match");
+ for i=1,#target_array do
+ assert(target_array[i] == returned_array[i], "array item doesn't match");
+ end
+ else
+ assert(module:get_option_array("opt") == returns.array, "array is returned (not nil)");
+ end
+
+ if type(returns.set) == "table" then
+ local target_items, returned_items = set.new(returns.set), module:get_option_set("opt");
+ assert(target_items == returned_items, "set doesn't match");
+ else
+ assert(module:get_option_set("opt") == returns.set, "set is returned (not nil)");
+ end
+end
+
+describe("core.moduleapi", function()
+ describe("#get_option_*()", function()
+ it("should handle missing options", function()
+ test_option_value(nil, {});
+ end);
+
+ it("should return correctly handle boolean options", function()
+ test_option_value(true, { boolean = true, string = "true", array = {true}, set = {true} });
+ test_option_value(false, { boolean = false, string = "false", array = {false}, set = {false} });
+ test_option_value("true", { boolean = true, string = "true", array = {"true"}, set = {"true"} });
+ test_option_value("false", { boolean = false, string = "false", array = {"false"}, set = {"false"} });
+ test_option_value(1, { boolean = true, string = "1", array = {1}, set = {1}, number = 1 });
+ test_option_value(0, { boolean = false, string = "0", array = {0}, set = {0}, number = 0 });
+ end);
+
+ it("should return handle strings", function()
+ test_option_value("hello world", { string = "hello world", array = {"hello world"}, set = {"hello world"} });
+ end);
+
+ it("should return handle numbers", function()
+ test_option_value(1234, { string = "1234", number = 1234, array = {1234}, set = {1234} });
+ end);
+
+ it("should return handle arrays", function()
+ test_option_value({1, 2, 3}, { boolean = true, string = "1", number = 1, array = {1, 2, 3}, set = {1, 2, 3} });
+ test_option_value({1, 2, 3, 3, 4}, {boolean = true, string = "1", number = 1, array = {1, 2, 3, 3, 4}, set = {1, 2, 3, 4} });
+ test_option_value({0, 1, 2, 3}, { boolean = false, string = "0", number = 0, array = {0, 1, 2, 3}, set = {0, 1, 2, 3} });
+ end);
+ end)
+end)
diff --git a/spec/json/fail1.json b/spec/json/fail1.json
new file mode 100644
index 00000000..6216b865
--- /dev/null
+++ b/spec/json/fail1.json
@@ -0,0 +1 @@
+"A JSON payload should be an object or array, not a string." \ No newline at end of file
diff --git a/spec/json/fail10.json b/spec/json/fail10.json
new file mode 100644
index 00000000..5d8c0047
--- /dev/null
+++ b/spec/json/fail10.json
@@ -0,0 +1 @@
+{"Extra value after close": true} "misplaced quoted value" \ No newline at end of file
diff --git a/spec/json/fail11.json b/spec/json/fail11.json
new file mode 100644
index 00000000..76eb95b4
--- /dev/null
+++ b/spec/json/fail11.json
@@ -0,0 +1 @@
+{"Illegal expression": 1 + 2} \ No newline at end of file
diff --git a/spec/json/fail12.json b/spec/json/fail12.json
new file mode 100644
index 00000000..77580a45
--- /dev/null
+++ b/spec/json/fail12.json
@@ -0,0 +1 @@
+{"Illegal invocation": alert()} \ No newline at end of file
diff --git a/spec/json/fail13.json b/spec/json/fail13.json
new file mode 100644
index 00000000..379406b5
--- /dev/null
+++ b/spec/json/fail13.json
@@ -0,0 +1 @@
+{"Numbers cannot have leading zeroes": 013} \ No newline at end of file
diff --git a/spec/json/fail14.json b/spec/json/fail14.json
new file mode 100644
index 00000000..0ed366b3
--- /dev/null
+++ b/spec/json/fail14.json
@@ -0,0 +1 @@
+{"Numbers cannot be hex": 0x14} \ No newline at end of file
diff --git a/spec/json/fail15.json b/spec/json/fail15.json
new file mode 100644
index 00000000..fc8376b6
--- /dev/null
+++ b/spec/json/fail15.json
@@ -0,0 +1 @@
+["Illegal backslash escape: \x15"] \ No newline at end of file
diff --git a/spec/json/fail16.json b/spec/json/fail16.json
new file mode 100644
index 00000000..3fe21d4b
--- /dev/null
+++ b/spec/json/fail16.json
@@ -0,0 +1 @@
+[\naked] \ No newline at end of file
diff --git a/spec/json/fail17.json b/spec/json/fail17.json
new file mode 100644
index 00000000..62b9214a
--- /dev/null
+++ b/spec/json/fail17.json
@@ -0,0 +1 @@
+["Illegal backslash escape: \017"] \ No newline at end of file
diff --git a/spec/json/fail18.json b/spec/json/fail18.json
new file mode 100644
index 00000000..edac9271
--- /dev/null
+++ b/spec/json/fail18.json
@@ -0,0 +1 @@
+[[[[[[[[[[[[[[[[[[[["Too deep"]]]]]]]]]]]]]]]]]]]] \ No newline at end of file
diff --git a/spec/json/fail19.json b/spec/json/fail19.json
new file mode 100644
index 00000000..3b9c46fa
--- /dev/null
+++ b/spec/json/fail19.json
@@ -0,0 +1 @@
+{"Missing colon" null} \ No newline at end of file
diff --git a/spec/json/fail2.json b/spec/json/fail2.json
new file mode 100644
index 00000000..6b7c11e5
--- /dev/null
+++ b/spec/json/fail2.json
@@ -0,0 +1 @@
+["Unclosed array" \ No newline at end of file
diff --git a/spec/json/fail20.json b/spec/json/fail20.json
new file mode 100644
index 00000000..27c1af3e
--- /dev/null
+++ b/spec/json/fail20.json
@@ -0,0 +1 @@
+{"Double colon":: null} \ No newline at end of file
diff --git a/spec/json/fail21.json b/spec/json/fail21.json
new file mode 100644
index 00000000..62474573
--- /dev/null
+++ b/spec/json/fail21.json
@@ -0,0 +1 @@
+{"Comma instead of colon", null} \ No newline at end of file
diff --git a/spec/json/fail22.json b/spec/json/fail22.json
new file mode 100644
index 00000000..a7752581
--- /dev/null
+++ b/spec/json/fail22.json
@@ -0,0 +1 @@
+["Colon instead of comma": false] \ No newline at end of file
diff --git a/spec/json/fail23.json b/spec/json/fail23.json
new file mode 100644
index 00000000..494add1c
--- /dev/null
+++ b/spec/json/fail23.json
@@ -0,0 +1 @@
+["Bad value", truth] \ No newline at end of file
diff --git a/spec/json/fail24.json b/spec/json/fail24.json
new file mode 100644
index 00000000..caff239b
--- /dev/null
+++ b/spec/json/fail24.json
@@ -0,0 +1 @@
+['single quote'] \ No newline at end of file
diff --git a/spec/json/fail25.json b/spec/json/fail25.json
new file mode 100644
index 00000000..8b7ad23e
--- /dev/null
+++ b/spec/json/fail25.json
@@ -0,0 +1 @@
+[" tab character in string "] \ No newline at end of file
diff --git a/spec/json/fail26.json b/spec/json/fail26.json
new file mode 100644
index 00000000..845d26a6
--- /dev/null
+++ b/spec/json/fail26.json
@@ -0,0 +1 @@
+["tab\ character\ in\ string\ "] \ No newline at end of file
diff --git a/spec/json/fail27.json b/spec/json/fail27.json
new file mode 100644
index 00000000..6b01a2ca
--- /dev/null
+++ b/spec/json/fail27.json
@@ -0,0 +1,2 @@
+["line
+break"] \ No newline at end of file
diff --git a/spec/json/fail28.json b/spec/json/fail28.json
new file mode 100644
index 00000000..621a0101
--- /dev/null
+++ b/spec/json/fail28.json
@@ -0,0 +1,2 @@
+["line\
+break"] \ No newline at end of file
diff --git a/spec/json/fail29.json b/spec/json/fail29.json
new file mode 100644
index 00000000..47ec421b
--- /dev/null
+++ b/spec/json/fail29.json
@@ -0,0 +1 @@
+[0e] \ No newline at end of file
diff --git a/spec/json/fail3.json b/spec/json/fail3.json
new file mode 100644
index 00000000..168c81eb
--- /dev/null
+++ b/spec/json/fail3.json
@@ -0,0 +1 @@
+{unquoted_key: "keys must be quoted"} \ No newline at end of file
diff --git a/spec/json/fail30.json b/spec/json/fail30.json
new file mode 100644
index 00000000..8ab0bc4b
--- /dev/null
+++ b/spec/json/fail30.json
@@ -0,0 +1 @@
+[0e+] \ No newline at end of file
diff --git a/spec/json/fail31.json b/spec/json/fail31.json
new file mode 100644
index 00000000..1cce602b
--- /dev/null
+++ b/spec/json/fail31.json
@@ -0,0 +1 @@
+[0e+-1] \ No newline at end of file
diff --git a/spec/json/fail32.json b/spec/json/fail32.json
new file mode 100644
index 00000000..45cba739
--- /dev/null
+++ b/spec/json/fail32.json
@@ -0,0 +1 @@
+{"Comma instead if closing brace": true, \ No newline at end of file
diff --git a/spec/json/fail33.json b/spec/json/fail33.json
new file mode 100644
index 00000000..ca5eb19d
--- /dev/null
+++ b/spec/json/fail33.json
@@ -0,0 +1 @@
+["mismatch"} \ No newline at end of file
diff --git a/spec/json/fail4.json b/spec/json/fail4.json
new file mode 100644
index 00000000..9de168bf
--- /dev/null
+++ b/spec/json/fail4.json
@@ -0,0 +1 @@
+["extra comma",] \ No newline at end of file
diff --git a/spec/json/fail5.json b/spec/json/fail5.json
new file mode 100644
index 00000000..ddf3ce3d
--- /dev/null
+++ b/spec/json/fail5.json
@@ -0,0 +1 @@
+["double extra comma",,] \ No newline at end of file
diff --git a/spec/json/fail6.json b/spec/json/fail6.json
new file mode 100644
index 00000000..ed91580e
--- /dev/null
+++ b/spec/json/fail6.json
@@ -0,0 +1 @@
+[ , "<-- missing value"] \ No newline at end of file
diff --git a/spec/json/fail7.json b/spec/json/fail7.json
new file mode 100644
index 00000000..8a96af3e
--- /dev/null
+++ b/spec/json/fail7.json
@@ -0,0 +1 @@
+["Comma after the close"], \ No newline at end of file
diff --git a/spec/json/fail8.json b/spec/json/fail8.json
new file mode 100644
index 00000000..b28479c6
--- /dev/null
+++ b/spec/json/fail8.json
@@ -0,0 +1 @@
+["Extra close"]] \ No newline at end of file
diff --git a/spec/json/fail9.json b/spec/json/fail9.json
new file mode 100644
index 00000000..5815574f
--- /dev/null
+++ b/spec/json/fail9.json
@@ -0,0 +1 @@
+{"Extra comma": true,} \ No newline at end of file
diff --git a/spec/json/pass1.json b/spec/json/pass1.json
new file mode 100644
index 00000000..70e26854
--- /dev/null
+++ b/spec/json/pass1.json
@@ -0,0 +1,58 @@
+[
+ "JSON Test Pattern pass1",
+ {"object with 1 member":["array with 1 element"]},
+ {},
+ [],
+ -42,
+ true,
+ false,
+ null,
+ {
+ "integer": 1234567890,
+ "real": -9876.543210,
+ "e": 0.123456789e-12,
+ "E": 1.234567890E+34,
+ "": 23456789012E66,
+ "zero": 0,
+ "one": 1,
+ "space": " ",
+ "quote": "\"",
+ "backslash": "\\",
+ "controls": "\b\f\n\r\t",
+ "slash": "/ & \/",
+ "alpha": "abcdefghijklmnopqrstuvwyz",
+ "ALPHA": "ABCDEFGHIJKLMNOPQRSTUVWYZ",
+ "digit": "0123456789",
+ "0123456789": "digit",
+ "special": "`1~!@#$%^&*()_+-={':[,]}|;.</>?",
+ "hex": "\u0123\u4567\u89AB\uCDEF\uabcd\uef4A",
+ "true": true,
+ "false": false,
+ "null": null,
+ "array":[ ],
+ "object":{ },
+ "address": "50 St. James Street",
+ "url": "http://www.JSON.org/",
+ "comment": "// /* <!-- --",
+ "# -- --> */": " ",
+ " s p a c e d " :[1,2 , 3
+
+,
+
+4 , 5 , 6 ,7 ],"compact":[1,2,3,4,5,6,7],
+ "jsontext": "{\"object with 1 member\":[\"array with 1 element\"]}",
+ "quotes": "&#34; \u0022 %22 0x22 034 &#x22;",
+ "\/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?"
+: "A key can be any string"
+ },
+ 0.5 ,98.6
+,
+99.44
+,
+
+1066,
+1e1,
+0.1e1,
+1e-1,
+1e00,2e+00,2e-00
+,"rosebud"] \ No newline at end of file
diff --git a/spec/json/pass2.json b/spec/json/pass2.json
new file mode 100644
index 00000000..d3c63c7a
--- /dev/null
+++ b/spec/json/pass2.json
@@ -0,0 +1 @@
+[[[[[[[[[[[[[[[[[[["Not too deep"]]]]]]]]]]]]]]]]]]] \ No newline at end of file
diff --git a/spec/json/pass3.json b/spec/json/pass3.json
new file mode 100644
index 00000000..4528d51f
--- /dev/null
+++ b/spec/json/pass3.json
@@ -0,0 +1,6 @@
+{
+ "JSON Test Pattern pass3": {
+ "The outermost value": "must be an object or array.",
+ "In this test": "It is an object."
+ }
+}
diff --git a/spec/muc_util_spec.lua b/spec/muc_util_spec.lua
new file mode 100644
index 00000000..cef68e80
--- /dev/null
+++ b/spec/muc_util_spec.lua
@@ -0,0 +1,35 @@
+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
+
+describe("muc/util", function ()
+ describe("filter_muc_x()", function ()
+ it("correctly filters muc#user", function ()
+ local stanza = st.message({ to = "to", from = "from", id = "foo" })
+ :tag("x", { xmlns = "http://jabber.org/protocol/muc#user" })
+ :tag("invite", { to = "user@example.com" });
+
+ assert.equal(1, #stanza.tags);
+ assert.equal(stanza, muc_util.filter_muc_x(stanza));
+ assert.equal(0, #stanza.tags);
+ end);
+
+ it("correctly filters muc#user on a cloned stanza", function ()
+ local stanza = st.message({ to = "to", from = "from", id = "foo" })
+ :tag("x", { xmlns = "http://jabber.org/protocol/muc#user" })
+ :tag("invite", { to = "user@example.com" });
+
+ assert.equal(1, #stanza.tags);
+ local filtered = muc_util.filter_muc_x(st.clone(stanza));
+ assert.equal(1, #stanza.tags);
+ assert.equal(0, #filtered.tags);
+ end);
+ end);
+end);
diff --git a/spec/net_http_parser_spec.lua b/spec/net_http_parser_spec.lua
new file mode 100644
index 00000000..6bba087c
--- /dev/null
+++ b/spec/net_http_parser_spec.lua
@@ -0,0 +1,52 @@
+local httpstreams = { [[
+GET / HTTP/1.1
+Host: example.com
+
+]], [[
+HTTP/1.1 200 OK
+Content-Length: 0
+
+]], [[
+HTTP/1.1 200 OK
+Content-Length: 7
+
+Hello
+HTTP/1.1 200 OK
+Transfer-Encoding: chunked
+
+1
+H
+1
+e
+2
+ll
+1
+o
+0
+
+
+]]
+}
+
+
+local http_parser = require "net.http.parser";
+
+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
+ end);
+ end);
+end);
diff --git a/spec/net_http_server_spec.lua b/spec/net_http_server_spec.lua
new file mode 100644
index 00000000..758b619d
--- /dev/null
+++ b/spec/net_http_server_spec.lua
@@ -0,0 +1,13 @@
+describe("net.http.server", function ()
+ package.loaded["net.server"] = {}
+ local server = require "net.http.server";
+ describe("events", function ()
+ it("should work with util.helpers", function ()
+ -- See #1044
+ server.add_handler("GET host/foo/*", function () end, 0);
+ server.add_handler("GET host/foo/bar", function () end, 0);
+ local helpers = require "util.helpers";
+ assert.is.string(helpers.show_events(server._events));
+ end);
+ end);
+end);
diff --git a/spec/scansion/basic.scs b/spec/scansion/basic.scs
new file mode 100644
index 00000000..c88672c1
--- /dev/null
+++ b/spec/scansion/basic.scs
@@ -0,0 +1,16 @@
+[Client] Romeo
+ jid: user@localhost
+ password: password
+
+---------
+
+Romeo connects
+
+Romeo sends:
+ <presence/>
+
+Romeo receives:
+ <presence/>
+
+Romeo disconnects
+
diff --git a/spec/scansion/basic_message.scs b/spec/scansion/basic_message.scs
new file mode 100644
index 00000000..f01d8925
--- /dev/null
+++ b/spec/scansion/basic_message.scs
@@ -0,0 +1,174 @@
+# A script testing basic message routing and delivery
+
+[Client] Romeo
+ jid: user@localhost
+ password: password
+
+[Client] Juliet
+ jid: juliet@localhost
+ password: password
+
+[Client] Juliet's phone
+ jid: juliet@localhost
+ password: password
+ resource: mobile
+
+---------
+
+# Act 1, scene 1
+# The clients connect
+
+Romeo connects
+
+Juliet connects
+
+Juliet's phone connects
+
+# Romeo publishes his presence. Juliet has not, and so does not receive presence.
+
+Romeo sends:
+ <presence/>
+
+Romeo receives:
+ <presence from="${Romeo's full JID}" />
+
+# Romeo sends a message to Juliet's full JID
+
+Romeo sends:
+ <message to="${Juliet's full JID}" type="chat">
+ <body>Hello Juliet!</body>
+ </message>
+
+Juliet receives:
+ <message to="${Juliet's full JID}" from="${Romeo's full JID}" type="chat">
+ <body>Hello Juliet!</body>
+ </message>
+
+# Romeo sends a message to Juliet's phone
+
+Romeo sends:
+ <message to="${Juliet's phone's full JID}" type="chat">
+ <body>Hello Juliet, on your phone.</body>
+ </message>
+
+Juliet's phone receives:
+ <message to="${Juliet's phone's full JID}" from="${Romeo's full JID}" type="chat">
+ <body>Hello Juliet, on your phone.</body>
+ </message>
+
+# Scene 2
+# This requires the server to support offline messages (which is optional).
+
+# Romeo sends a message to Juliet's bare JID. This is not immediately delivered, as she
+# has not published presence on either of her resources.
+
+Romeo sends:
+ <message to="juliet@localhost" type="chat">
+ <body>Hello Juliet, are you there?</body>
+ </message>
+
+# Juliet sends presence on her phone, and should receive the message there
+
+Juliet's phone sends:
+ <presence/>
+
+Juliet's phone receives:
+ <presence/>
+
+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' />
+ </message>
+
+# Romeo sends another bare-JID message, it should be delivered
+# instantly to Juliet's phone
+
+Romeo sends:
+ <message to="juliet@localhost" type="chat">
+ <body>Oh, hi!</body>
+ </message>
+
+Juliet's phone receives:
+ <message from="${Romeo's full JID}" type="chat">
+ <body>Oh, hi!</body>
+ </message>
+
+# Juliet's laptop goes online, but with a negative priority
+
+Juliet sends:
+ <presence>
+ <priority>-1</priority>
+ </presence>
+
+Juliet receives:
+ <presence from="${Juliet's full JID}">
+ <priority>-1</priority>
+ </presence>
+
+Juliet's phone receives:
+ <presence from="${Juliet's full JID}">
+ <priority>-1</priority>
+ </presence>
+
+# Again, Romeo sends a message to her bare JID, but it should
+# only get delivered to her phone:
+
+Romeo sends:
+ <message to="juliet@localhost" type="chat">
+ <body>How are you?</body>
+ </message>
+
+Juliet's phone receives:
+ <message from="${Romeo's full JID}" type="chat">
+ <body>How are you?</body>
+ </message>
+
+# Romeo sends direct to Juliet's full JID, and she should receive it
+
+Romeo sends:
+ <message to="${Juliet's full JID}" type="chat">
+ <body>Are you hiding?</body>
+ </message>
+
+Juliet receives:
+ <message from="${Romeo's full JID}" type="chat">
+ <body>Are you hiding?</body>
+ </message>
+
+# Juliet publishes non-negative presence
+
+Juliet sends:
+ <presence/>
+
+Juliet receives:
+ <presence from="${Juliet's full JID}"/>
+
+Juliet's phone receives:
+ <presence from="${Juliet's full JID}"/>
+
+# And now Romeo's bare JID messages get delivered to both resources
+# (server behaviour may vary here)
+
+Romeo sends:
+ <message to="juliet@localhost" type="chat">
+ <body>There!</body>
+ </message>
+
+Juliet receives:
+ <message from="${Romeo's full JID}" type="chat">
+ <body>There!</body>
+ </message>
+
+Juliet's phone receives:
+ <message from="${Romeo's full JID}" type="chat">
+ <body>There!</body>
+ </message>
+
+# The End
+
+Romeo disconnects
+
+Juliet disconnects
+
+Juliet's phone disconnects
diff --git a/spec/scansion/basic_roster.scs b/spec/scansion/basic_roster.scs
new file mode 100644
index 00000000..2655639a
--- /dev/null
+++ b/spec/scansion/basic_roster.scs
@@ -0,0 +1,39 @@
+[Client] Romeo
+ jid: user@localhost
+ password: password
+
+[Client] Juliet
+ jid: juliet@localhost
+ password: password
+
+---------
+
+Romeo connects
+
+Juliet connects
+
+Romeo sends:
+ <presence/>
+
+Romeo receives:
+ <presence from="${Romeo's full JID}" />
+
+Romeo sends:
+ <iq type="get" id="roster1">
+ <query xmlns='jabber:iq:roster'/>
+ </iq>
+
+Romeo receives:
+ <iq type="result" id="roster1">
+ <query xmlns="jabber:iq:roster"/>
+ </iq>
+
+Romeo disconnects
+
+#Juliet receives:
+# <presence from="${Romeo's full JID}" />
+#
+#Juliet receives:
+# <presence from="${Romeo's full JID}" type="unavailable" />
+#
+#Juliet disconnects
diff --git a/spec/scansion/issue505.scs b/spec/scansion/issue505.scs
new file mode 100644
index 00000000..6d4d27b7
--- /dev/null
+++ b/spec/scansion/issue505.scs
@@ -0,0 +1,77 @@
+[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>
+
+# 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>
+ </x>
+ </query>
+ </iq>
+
+Romeo receives:
+ <iq id="config1" from="room@conference.localhost" type="result">
+ </iq>
+
+# 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 sends:
+ <presence type='unavailable' to='room@conference.localhost'>
+ <status>Farewell</status>
+ </presence>
+
+Romeo receives:
+ <presence type='unavailable' from='room@conference.localhost/Juliet'>
+ <status>Farewell</status>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <item jid="${Juliet's full JID}" affiliation='none' role='none'/>
+ </x>
+ </presence>
diff --git a/spec/scansion/issue978-multi.scs b/spec/scansion/issue978-multi.scs
new file mode 100644
index 00000000..340f2b27
--- /dev/null
+++ b/spec/scansion/issue978-multi.scs
@@ -0,0 +1,109 @@
+[Client] Romeo
+ jid: user@localhost
+ password: password
+
+[Client] Juliet
+ jid: user2@localhost
+ password: password
+
+[Client] Juliet's phone
+ 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>
+
+# 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>
+ </x>
+ </query>
+ </iq>
+
+Romeo receives:
+ <iq id="config1" from="room@conference.localhost" type="result">
+ </iq>
+
+# 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's phone connects, and joins the room
+Juliet's phone connects
+
+Juliet's phone sends:
+ <presence to="room@conference.localhost/Juliet">
+ <x xmlns="http://jabber.org/protocol/muc"/>
+ </presence>
+
+Juliet's phone receives:
+ <presence from="room@conference.localhost/Romeo" />
+
+Juliet's phone receives:
+ <presence from="room@conference.localhost/Juliet" />
+
+Juliet's phone 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' jid="${Juliet's phone's full JID}" role='participant'/>
+ <item affiliation='none' jid="${Juliet's full JID}" role='participant'/>
+ </x>
+ </presence>
+
+# Juliet leaves with an error
+Juliet sends:
+ <presence type='error' to='room@conference.localhost'>
+ <error type='cancel'>
+ <service-unavailable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
+ <text xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'>Test error</text>
+ </error>
+ </presence>
+
+Romeo receives:
+ <presence from='room@conference.localhost/Juliet'>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <item jid="${Juliet's phone's full JID}" affiliation='none' role='participant'/>
+ </x>
+ </presence>
diff --git a/spec/scansion/issue978.scs b/spec/scansion/issue978.scs
new file mode 100644
index 00000000..d47473be
--- /dev/null
+++ b/spec/scansion/issue978.scs
@@ -0,0 +1,84 @@
+[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>
+
+# 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_whois'>
+ <value>anyone</value>
+ </field>
+ </x>
+ </query>
+ </iq>
+
+Romeo receives:
+ <iq id="config1" from="room@conference.localhost" type="result">
+ </iq>
+
+# 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 sends:
+ <presence type='error' to='room@conference.localhost'>
+ <error type='cancel'>
+ <service-unavailable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
+ <text xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'>Test error</text>
+ </error>
+ </presence>
+
+Romeo receives:
+ <presence type='unavailable' from='room@conference.localhost/Juliet'>
+ <status>Kicked: service unavailable: Test error</status>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <status code='333'/>
+ <item jid="${Juliet's full JID}" affiliation='none' role='none'/>
+ </x>
+ </presence>
diff --git a/spec/scansion/muc_mediated_invite.scs b/spec/scansion/muc_mediated_invite.scs
new file mode 100644
index 00000000..7662fc7b
--- /dev/null
+++ b/spec/scansion/muc_mediated_invite.scs
@@ -0,0 +1,74 @@
+[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>
+
+# 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>
+ </x>
+ </query>
+ </iq>
+
+Romeo receives:
+ <iq id="config1" from="room@conference.localhost" type="result">
+ </iq>
+
+# Juliet connects
+Juliet connects
+
+Juliet sends:
+ <presence/>
+
+Juliet receives:
+ <presence/>
+
+
+# Romeo invites Juliet to join the room
+
+Romeo sends:
+ <message to="room@conference.localhost" id="invite1">
+ <x xmlns="http://jabber.org/protocol/muc#user">
+ <invite to="${Juliet's JID}" />
+ </x>
+ </message>
+
+Juliet receives:
+ <message from="room@conference.localhost" id="invite1">
+ <x xmlns="http://jabber.org/protocol/muc#user">
+ <invite from="room@conference.localhost/Romeo">
+ <reason/>
+ </invite>
+ </x>
+ <body>room@conference.localhost/Romeo invited you to the room room@conference.localhost</body>
+ <x xmlns="jabber:x:conference" jid="room@conference.localhost"/>
+ </message>
diff --git a/spec/scansion/muc_password-trunk.scs b/spec/scansion/muc_password-trunk.scs
new file mode 100644
index 00000000..0a343b91
--- /dev/null
+++ b/spec/scansion/muc_password-trunk.scs
@@ -0,0 +1,141 @@
+[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>
+
+# 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_roomsecret'>
+ <value>cauldronburn</value>
+ </field>
+ </x>
+ </query>
+ </iq>
+
+Romeo receives:
+ <iq id="config1" from="room@conference.localhost" type="result">
+ </iq>
+
+# Juliet connects, and tries to join the room (password-protected)
+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/Juliet" type="error">
+ <error type="auth">
+ <not-authorized xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
+ </error>
+ </presence>
+
+# Retry with the correct password
+Juliet sends:
+ <presence to="room@conference.localhost/Juliet">
+ <x xmlns="http://jabber.org/protocol/muc">
+ <password>cauldronburn</password>
+ </x>
+ </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" />
+
+# Ok, now Juliet leaves, and Romeo unsets the password
+
+Juliet sends:
+ <presence type="unavailable" to="room@conference.localhost"/>
+
+Romeo receives:
+ <presence type="unavailable" from="room@conference.localhost/Juliet"/>
+
+Juliet receives:
+ <presence type="unavailable" from="room@conference.localhost/Juliet"/>
+
+# Remove room password
+Romeo sends:
+ <iq id='config2' 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_roomsecret'>
+ </field>
+ </x>
+ </query>
+ </iq>
+
+# Config change success
+Romeo receives:
+ <iq id="config2" from="room@conference.localhost" type="result">
+ </iq>
+
+# Notification of room configuration update
+Romeo receives:
+ <message type='groupchat' from='room@conference.localhost'>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <status code='104'/>
+ </x>
+ </message>
+
+# Juliet tries to join (should succeed)
+Juliet sends:
+ <presence to="room@conference.localhost/Juliet">
+ <x xmlns="http://jabber.org/protocol/muc"/>
+ </presence>
+
+# Notification of Romeo's presence in the room
+Juliet receives:
+ <presence from="room@conference.localhost/Romeo" />
+
+Juliet receives:
+ <presence from="room@conference.localhost/Juliet" />
+
+# Room topic
+Juliet receives:
+ <message type='groupchat' from='room@conference.localhost'><subject/></message>
+
+Romeo receives:
+ <presence from="room@conference.localhost/Juliet" />
+
diff --git a/spec/scansion/muc_password.scs b/spec/scansion/muc_password.scs
new file mode 100644
index 00000000..813265a5
--- /dev/null
+++ b/spec/scansion/muc_password.scs
@@ -0,0 +1,149 @@
+# MUC password test
+# Ensures that setting and unsetting a MUC password works, and that the password is
+# required to join a password-protected room.
+
+## prosody-0.9 result: fails because of missing 201 status code (related to issue #328)
+## prosody-0.10 result: fails because of missing 201 status code (related to issue #328)
+
+[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>
+
+# 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_roomsecret'>
+ <value>cauldronburn</value>
+ </field>
+ </x>
+ </query>
+ </iq>
+
+Romeo receives:
+ <iq id="config1" from="room@conference.localhost" type="result">
+ </iq>
+
+# Juliet connects, and tries to join the room (password-protected)
+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/Juliet" type="error">
+ <error type="auth">
+ <not-authorized xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
+ </error>
+ </presence>
+
+# Retry with the correct password
+Juliet sends:
+ <presence to="room@conference.localhost/Juliet">
+ <x xmlns="http://jabber.org/protocol/muc">
+ <password>cauldronburn</password>
+ </x>
+ </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" />
+
+# Ok, now Juliet leaves, and Romeo unsets the password
+
+Juliet sends:
+ <presence type="unavailable" to="room@conference.localhost"/>
+
+Romeo receives:
+ <presence type="unavailable" from="room@conference.localhost/Juliet"/>
+
+Juliet receives:
+ <presence type="unavailable" from="room@conference.localhost/Juliet"/>
+
+# Remove room password
+Romeo sends:
+ <iq id='config2' 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_roomsecret'>
+ <value></value>
+ </field>
+ </x>
+ </query>
+ </iq>
+
+# Config change success
+Romeo receives:
+ <iq id="config2" from="room@conference.localhost" type="result">
+ </iq>
+
+# Notification of room configuration update
+Romeo receives:
+ <message type='groupchat' from='room@conference.localhost'>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <status code='104'/>
+ </x>
+ </message>
+
+# Juliet tries to join (should succeed)
+Juliet sends:
+ <presence to="room@conference.localhost/Juliet">
+ <x xmlns="http://jabber.org/protocol/muc"/>
+ </presence>
+
+# Notification of Romeo's presence in the room
+Juliet receives:
+ <presence from="room@conference.localhost/Romeo" />
+
+Juliet receives:
+ <presence from="room@conference.localhost/Juliet" />
+
+# Room topic
+Juliet receives:
+ <message type='groupchat' from='room@conference.localhost'><subject/></message>
+
+Romeo receives:
+ <presence from="room@conference.localhost/Juliet" />
+
diff --git a/spec/scansion/muc_register.scs b/spec/scansion/muc_register.scs
new file mode 100644
index 00000000..3bbf251b
--- /dev/null
+++ b/spec/scansion/muc_register.scs
@@ -0,0 +1,286 @@
+[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>
+ </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:
+ <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'/>
+ </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'>
+ <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'>
+ <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'/>
+
+# 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>
+
+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'>
+ <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" />
+
diff --git a/spec/scansion/pubsub_advanced.scs b/spec/scansion/pubsub_advanced.scs
new file mode 100644
index 00000000..b7b96a22
--- /dev/null
+++ b/spec/scansion/pubsub_advanced.scs
@@ -0,0 +1,162 @@
+[Client] Balthasar
+ jid: admin@localhost
+ password: password
+
+[Client] Romeo
+ jid: romeo@localhost
+ password: password
+
+[Client] Juliet
+ jid: juliet@localhost
+ password: password
+
+---------
+
+Romeo connects
+
+Romeo sends:
+ <iq type="set" to="pubsub.localhost" id='create1'>
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <create node="princely_musings"/>
+ </pubsub>
+ </iq>
+
+Romeo receives:
+ <iq type="error" id='create1'>
+ <error type="auth">
+ <forbidden xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
+ </error>
+ </iq>
+
+Balthasar connects
+
+Balthasar sends:
+ <iq type='set' to='pubsub.localhost' id='create2'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <create node='princely_musings'/>
+ </pubsub>
+ </iq>
+
+Balthasar receives:
+ <iq type="result" id='create2'/>
+
+Balthasar sends:
+ <iq type="set" to="pubsub.localhost" id='create3'>
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <create node="princely_musings"/>
+ </pubsub>
+ </iq>
+
+Balthasar receives:
+ <iq type="error" id='create3'>
+ <error type="cancel">
+ <conflict xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
+ </error>
+ </iq>
+
+Juliet connects
+
+Juliet sends:
+ <iq type="set" to="pubsub.localhost" id='sub1'>
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <subscribe node="princely_musings" jid="${Romeo's full JID}"/>
+ </pubsub>
+ </iq>
+
+Juliet receives:
+ <iq type="error" id='sub1'/>
+
+Juliet sends:
+ <iq type="set" to="pubsub.localhost" id='sub2'>
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <subscribe node="princely_musings" jid="${Juliet's full JID}"/>
+ </pubsub>
+ </iq>
+
+Juliet receives:
+ <iq type="result" id='sub2'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <subscription jid="${Juliet's full JID}" node='princely_musings' subscription='subscribed'/>
+ </pubsub>
+ </iq>
+
+Balthasar sends:
+ <iq type="get" id='aff1' to='pubsub.localhost'>
+ <pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
+ <affiliations node="princely_musings"/>
+ </pubsub>
+ </iq>
+
+Balthasar receives:
+ <iq type="result" id='aff1' from='pubsub.localhost'>
+ <pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
+ <affiliations node="princely_musings"/>
+ </pubsub>
+ </iq>
+
+Balthasar sends:
+ <iq type="set" id='aff2' to='pubsub.localhost'>
+ <pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
+ <affiliations node="princely_musings">
+ <affiliation jid="${Romeo's JID}" affiliation="publisher"/>
+ </affiliations>
+ </pubsub>
+ </iq>
+
+Balthasar receives:
+ <iq type="result" id='aff2' from='pubsub.localhost'/>
+
+Romeo sends:
+ <iq type="set" to="pubsub.localhost" id='pub1'>
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <publish node="princely_musings">
+ <item id="current">
+ <entry xmlns="http://www.w3.org/2005/Atom">
+ <title>Soliloquy</title>
+ <summary>Lorem ipsum dolor sit amet</summary>
+ </entry>
+ </item>
+ </publish>
+ </pubsub>
+ </iq>
+
+Juliet receives:
+ <message type="headline" from="pubsub.localhost">
+ <event xmlns="http://jabber.org/protocol/pubsub#event">
+ <items node="princely_musings">
+ <item id="current">
+ <entry xmlns="http://www.w3.org/2005/Atom">
+ <title>Soliloquy</title>
+ <summary>Lorem ipsum dolor sit amet</summary>
+ </entry>
+ </item>
+ </items>
+ </event>
+ </message>
+
+Romeo receives:
+ <iq type="result" id='pub1'/>
+
+Juliet sends:
+ <iq type="set" to="pubsub.localhost" id='unsub1'>
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <unsubscribe node="princely_musings" jid="${Juliet's full JID}"/>
+ </pubsub>
+ </iq>
+
+Juliet receives:
+ <iq type="result" id='unsub1'/>
+
+Balthasar sends:
+ <iq type="set" to="pubsub.localhost" id='del1'>
+ <pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
+ <delete node="princely_musings"/>
+ </pubsub>
+ </iq>
+
+Balthasar receives:
+ <iq type="result" from='pubsub.localhost' id='del1'/>
+
+Romeo disconnects
+
+// vim: syntax=xml:
diff --git a/spec/scansion/pubsub_basic.scs b/spec/scansion/pubsub_basic.scs
new file mode 100644
index 00000000..ec21afdc
--- /dev/null
+++ b/spec/scansion/pubsub_basic.scs
@@ -0,0 +1,102 @@
+[Client] Romeo
+ jid: admin@localhost
+ password: password
+
+// admin@localhost is assumed to have node creation privileges
+
+[Client] Juliet
+ jid: juliet@localhost
+ password: password
+
+---------
+
+Romeo connects
+
+Romeo sends:
+ <iq type="set" to="pubsub.localhost" id='create1'>
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <create node="princely_musings"/>
+ </pubsub>
+ </iq>
+
+Romeo receives:
+ <iq type="result" id='create1'/>
+
+Juliet connects
+
+-- Juliet sends:
+-- <iq type="set" to="pubsub.localhost">
+-- <pubsub xmlns="http://jabber.org/protocol/pubsub">
+-- <subscribe node="princely_musings" jid="${Romeo's full JID}"/>
+-- </pubsub>
+-- </iq>
+--
+-- Juliet receives:
+-- <iq type="error"/>
+
+Juliet sends:
+ <iq type="set" to="pubsub.localhost" id='sub1'>
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <subscribe node="princely_musings" jid="${Juliet's full JID}"/>
+ </pubsub>
+ </iq>
+
+Juliet receives:
+ <iq type="result" id='sub1'/>
+
+Romeo sends:
+ <iq type="set" to="pubsub.localhost" id='pub1'>
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <publish node="princely_musings">
+ <item id="current">
+ <entry xmlns="http://www.w3.org/2005/Atom">
+ <title>Soliloquy</title>
+ <summary>Lorem ipsum dolor sit amet</summary>
+ </entry>
+ </item>
+ </publish>
+ </pubsub>
+ </iq>
+
+Romeo receives:
+ <iq type="result" id='pub1'/>
+
+Juliet receives:
+ <message type="headline" from="pubsub.localhost">
+ <event xmlns="http://jabber.org/protocol/pubsub#event">
+ <items node="princely_musings">
+ <item id="current">
+ <entry xmlns="http://www.w3.org/2005/Atom">
+ <title>Soliloquy</title>
+ <summary>Lorem ipsum dolor sit amet</summary>
+ </entry>
+ </item>
+ </items>
+ </event>
+ </message>
+
+Juliet sends:
+ <iq type="set" to="pubsub.localhost" id='unsub1'>
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <unsubscribe node="princely_musings" jid="${Juliet's full JID}"/>
+ </pubsub>
+ </iq>
+
+Juliet receives:
+ <iq type="result" id='unsub1'/>
+
+Juliet disconnects
+
+Romeo sends:
+ <iq type="set" to="pubsub.localhost" id='del1'>
+ <pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
+ <delete node="princely_musings"/>
+ </pubsub>
+ </iq>
+
+Romeo receives:
+ <iq type="result" id='del1'/>
+
+Romeo disconnects
+
+// vim: syntax=xml:
diff --git a/spec/scansion/pubsub_createdelete.scs b/spec/scansion/pubsub_createdelete.scs
new file mode 100644
index 00000000..f6fd1831
--- /dev/null
+++ b/spec/scansion/pubsub_createdelete.scs
@@ -0,0 +1,61 @@
+[Client] Romeo
+ jid: admin@localhost
+ password: password
+
+// admin@localhost is assumed to have node creation privileges
+
+---------
+
+Romeo connects
+
+Romeo sends:
+ <iq type="set" to="pubsub.localhost" id='create1'>
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <create node="princely_musings"/>
+ </pubsub>
+ </iq>
+
+Romeo receives:
+ <iq type="result" id='create1'/>
+
+Romeo sends:
+ <iq type="set" to="pubsub.localhost" id='create2'>
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <create node="princely_musings"/>
+ </pubsub>
+ </iq>
+
+Romeo receives:
+ <iq type="error" id='create2'>
+ <error type="cancel">
+ <conflict xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
+ </error>
+ </iq>
+
+Romeo sends:
+ <iq type="set" to="pubsub.localhost" id='delete1'>
+ <pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
+ <delete node="princely_musings"/>
+ </pubsub>
+ </iq>
+
+Romeo receives:
+ <iq type="result" id='delete1'/>
+
+Romeo sends:
+ <iq type="set" to="pubsub.localhost" id='delete2'>
+ <pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
+ <delete node="princely_musings"/>
+ </pubsub>
+ </iq>
+
+Romeo receives:
+ <iq type="error" id='delete2'>
+ <error type="cancel">
+ <item-not-found xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
+ </error>
+ </iq>
+
+Romeo disconnects
+
+// vim: syntax=xml:
diff --git a/spec/utf8_sequences.txt b/spec/utf8_sequences.txt
new file mode 100644
index 00000000..1b967b2e
--- /dev/null
+++ b/spec/utf8_sequences.txt
@@ -0,0 +1,52 @@
+Should pass: 41 42 43 # Simple ASCII - abc
+Should pass: 41 42 c3 87 # "ABÇ"
+Should pass: 41 42 e1 b8 88 # "ABḈ"
+Should pass: 41 42 f0 9d 9c 8d # "AB𝜍"
+Should pass: F4 8F BF BF # Last valid sequence (U+10FFFF)
+Should fail: F4 90 80 80 # First invalid sequence (U+110000)
+Should fail: 80 81 82 83 # Invalid sequence (invalid start byte)
+Should fail: C2 C3 # Invalid sequence (invalid continuation byte)
+Should fail: C0 43 # Overlong sequence
+Should fail: F5 80 80 80 # U+140000 (out of range)
+Should fail: ED A0 80 # U+D800 (forbidden by RFC 3629)
+Should fail: ED BF BF # U+DFFF (forbidden by RFC 3629)
+Should pass: ED 9F BF # U+D7FF (U+D800 minus 1: allowed)
+Should pass: EE 80 80 # U+E000 (U+D7FF plus 1: allowed)
+Should fail: C0 # Invalid start byte
+Should fail: C1 # Invalid start byte
+Should fail: C2 # Incomplete sequence
+Should fail: F8 88 80 80 80 # 6-byte sequence
+Should pass: 7F # Last valid 1-byte sequence (U+00007F)
+Should pass: DF BF # Last valid 2-byte sequence (U+0007FF)
+Should pass: EF BF BF # Last valid 3-byte sequence (U+00FFFF)
+Should pass: 00 # First valid 1-byte sequence (U+000000)
+Should pass: C2 80 # First valid 2-byte sequence (U+000080)
+Should pass: E0 A0 80 # First valid 3-byte sequence (U+000800)
+Should pass: F0 90 80 80 # First valid 4-byte sequence (U+000800)
+Should fail: F8 88 80 80 80 # First 5-byte sequence - invalid per RFC 3629
+Should fail: FC 84 80 80 80 80 # First 6-byte sequence - invalid per RFC 3629
+Should pass: EF BF BD # U+00FFFD (replacement character)
+Should fail: 80 # First continuation byte
+Should fail: BF # Last continuation byte
+Should fail: 80 BF # 2 continuation bytes
+Should fail: 80 BF 80 # 3 continuation bytes
+Should fail: 80 BF 80 BF # 4 continuation bytes
+Should fail: 80 BF 80 BF 80 # 5 continuation bytes
+Should fail: 80 BF 80 BF 80 BF # 6 continuation bytes
+Should fail: 80 BF 80 BF 80 BF 80 # 7 continuation bytes
+Should fail: FE # Impossible byte
+Should fail: FF # Impossible byte
+Should fail: FE FE FF FF # Impossible bytes
+Should fail: C0 AF # Overlong "/"
+Should fail: E0 80 AF # Overlong "/"
+Should fail: F0 80 80 AF # Overlong "/"
+Should fail: F8 80 80 80 AF # Overlong "/"
+Should fail: FC 80 80 80 80 AF # Overlong "/"
+Should fail: C0 80 AF # Overlong "/" (invalid)
+Should fail: C1 BF # Overlong
+Should fail: E0 9F BF # Overlong
+Should fail: F0 8F BF BF # Overlong
+Should fail: F8 87 BF BF BF # Overlong
+Should fail: FC 83 BF BF BF BF # Overlong
+Should pass: EF BF BE # U+FFFE (invalid unicode, valid UTF-8)
+Should pass: EF BF BF # U+FFFF (invalid unicode, valid UTF-8)
diff --git a/spec/util_async_spec.lua b/spec/util_async_spec.lua
new file mode 100644
index 00000000..d2de8c94
--- /dev/null
+++ b/spec/util_async_spec.lua
@@ -0,0 +1,616 @@
+local async = require "util.async";
+
+describe("util.async", function()
+ local debug = false;
+ local print = print;
+ if debug then
+ require "util.logger".add_simple_sink(print);
+ else
+ print = function () end
+ end
+
+ local function mock_watchers(event_log)
+ local function generic_logging_watcher(name)
+ return function (...)
+ table.insert(event_log, { name = name, n = select("#", ...)-1, select(2, ...) });
+ end;
+ end;
+ return setmetatable(mock{
+ ready = generic_logging_watcher("ready");
+ waiting = generic_logging_watcher("waiting");
+ error = generic_logging_watcher("error");
+ }, {
+ __index = function (_, event)
+ -- Unexpected watcher called
+ assert(false, "unexpected watcher called: "..event);
+ end;
+ })
+ end
+
+ local function new(func)
+ local event_log = {};
+ local spy_func = spy.new(func);
+ return async.runner(spy_func, mock_watchers(event_log)), spy_func, event_log;
+ end
+ describe("#runner", function()
+ it("should work", function()
+ local r = new(function (item) assert(type(item) == "number") end);
+ r:run(1);
+ r:run(2);
+ end);
+
+ it("should be ready after creation", function ()
+ local r = new(function () end);
+ assert.equal(r.state, "ready");
+ end);
+
+ it("should do nothing if the queue is empty", function ()
+ local did_run;
+ local r = new(function () did_run = true end);
+ r:run();
+ assert.equal(r.state, "ready");
+ assert.is_nil(did_run);
+ r:run("hello");
+ assert.is_true(did_run);
+ end);
+
+ it("should support queuing work items without running", function ()
+ local did_run;
+ local r = new(function () did_run = true end);
+ r:enqueue("hello");
+ assert.equal(r.state, "ready");
+ assert.is_nil(did_run);
+ r:run();
+ assert.is_true(did_run);
+ end);
+
+ it("should support queuing multiple work items", function ()
+ local last_item;
+ local r, s = new(function (item) last_item = item; end);
+ r:enqueue("hello");
+ r:enqueue("there");
+ r:enqueue("world");
+ assert.equal(r.state, "ready");
+ r:run();
+ assert.equal(r.state, "ready");
+ assert.spy(s).was.called(3);
+ assert.equal(last_item, "world");
+ end);
+
+ it("should support all simple data types", function ()
+ local last_item;
+ local r, s = new(function (item) last_item = item; end);
+ local values = { {}, 123, "hello", true, false };
+ for i = 1, #values do
+ r:enqueue(values[i]);
+ end
+ assert.equal(r.state, "ready");
+ r:run();
+ assert.equal(r.state, "ready");
+ assert.spy(s).was.called(#values);
+ for i = 1, #values do
+ assert.spy(s).was.called_with(values[i]);
+ end
+ assert.equal(last_item, values[#values]);
+ end);
+
+ it("should work with no parameters", function ()
+ local item = "fail";
+ local r = async.runner();
+ local f = spy.new(function () item = "success"; end);
+ r:run(f);
+ assert.spy(f).was.called();
+ assert.equal(item, "success");
+ end);
+
+ it("supports a default error handler", function ()
+ local item = "fail";
+ local r = async.runner();
+ local f = spy.new(function () error("test error"); end);
+ assert.error_matches(function ()
+ r:run(f);
+ end, "test error");
+ assert.spy(f).was.called();
+ assert.equal(item, "fail");
+ end);
+
+ describe("#errors", function ()
+ describe("should notify", function ()
+ local last_processed_item, last_error;
+ local r;
+ r = async.runner(function (item)
+ if item == "error" then
+ error({ e = "test error" });
+ end
+ last_processed_item = item;
+ end, mock{
+ ready = function () end;
+ waiting = function () end;
+ error = function (runner, err)
+ assert.equal(r, runner);
+ last_error = err;
+ end;
+ });
+
+ -- Simple item, no error
+ r:run("hello");
+ assert.equal(r.state, "ready");
+ assert.equal(last_processed_item, "hello");
+ assert.spy(r.watchers.ready).was_not.called();
+ assert.spy(r.watchers.error).was_not.called();
+
+ -- Trigger an error inside the runner
+ assert.equal(last_error, nil);
+ r:run("error");
+ test("the correct watcher functions", function ()
+ -- Only the error watcher should have been called
+ assert.spy(r.watchers.ready).was_not.called();
+ assert.spy(r.watchers.waiting).was_not.called();
+ assert.spy(r.watchers.error).was.called(1);
+ end);
+ test("with the correct error", function ()
+ -- The error watcher state should be correct, to
+ -- demonstrate the error was passed correctly
+ assert.is_table(last_error);
+ assert.equal(last_error.e, "test error");
+ last_error = nil;
+ end);
+ assert.equal(r.state, "ready");
+ assert.equal(last_processed_item, "hello");
+ end);
+
+ do
+ local last_processed_item, last_error;
+ local r;
+ local wait, done;
+ r = async.runner(function (item)
+ if item == "error" then
+ error({ e = "test error" });
+ elseif item == "wait" then
+ wait, done = async.waiter();
+ wait();
+ error({ e = "post wait error" });
+ end
+ last_processed_item = item;
+ end, mock({
+ ready = function () end;
+ waiting = function () end;
+ error = function (runner, err)
+ assert.equal(r, runner);
+ last_error = err;
+ end;
+ }));
+
+ randomize(false); --luacheck: ignore 113/randomize
+
+ it("should not be fatal to the runner", function ()
+ r:run("world");
+ assert.equal(r.state, "ready");
+ assert.spy(r.watchers.ready).was_not.called();
+ assert.equal(last_processed_item, "world");
+ end);
+ it("should work despite a #waiter", function ()
+ -- This test covers an important case where a runner
+ -- throws an error while being executed outside of the
+ -- main loop. This happens when it was blocked ('waiting'),
+ -- and then released (via a call to done()).
+ last_error = nil;
+ r:run("wait");
+ assert.equal(r.state, "waiting");
+ assert.spy(r.watchers.waiting).was.called(1);
+ done();
+ -- At this point an error happens (state goes error->ready)
+ assert.equal(r.state, "ready");
+ assert.spy(r.watchers.error).was.called(1);
+ assert.spy(r.watchers.ready).was.called(1);
+ assert.is_table(last_error);
+ assert.equal(last_error.e, "post wait error");
+ last_error = nil;
+ r:run("hello again");
+ assert.spy(r.watchers.ready).was.called(1);
+ assert.spy(r.watchers.waiting).was.called(1);
+ assert.spy(r.watchers.error).was.called(1);
+ assert.equal(r.state, "ready");
+ assert.equal(last_processed_item, "hello again");
+ end);
+ end
+
+ it("should continue to process work items", function ()
+ local last_item;
+ local runner, runner_func = new(function (item)
+ if item == "error" then
+ error("test error");
+ end
+ last_item = item;
+ end);
+ runner:enqueue("one");
+ runner:enqueue("error");
+ runner:enqueue("two");
+ runner:run();
+ assert.equal(runner.state, "ready");
+ assert.spy(runner_func).was.called(3);
+ assert.spy(runner.watchers.error).was.called(1);
+ assert.spy(runner.watchers.ready).was.called(0);
+ assert.spy(runner.watchers.waiting).was.called(0);
+ assert.equal(last_item, "two");
+ end);
+
+ it("should continue to process work items during resume", function ()
+ local wait, done, last_item;
+ local runner, runner_func = new(function (item)
+ if item == "wait-error" then
+ wait, done = async.waiter();
+ wait();
+ error("test error");
+ end
+ last_item = item;
+ end);
+ runner:enqueue("one");
+ runner:enqueue("wait-error");
+ runner:enqueue("two");
+ runner:run();
+ done();
+ assert.equal(runner.state, "ready");
+ assert.spy(runner_func).was.called(3);
+ assert.spy(runner.watchers.error).was.called(1);
+ assert.spy(runner.watchers.waiting).was.called(1);
+ assert.spy(runner.watchers.ready).was.called(1);
+ assert.equal(last_item, "two");
+ end);
+ end);
+ end);
+ describe("#waiter", function()
+ it("should error outside of async context", function ()
+ assert.has_error(function ()
+ async.waiter();
+ end);
+ end);
+ it("should work", function ()
+ local wait, done;
+
+ local r = new(function (item)
+ assert(type(item) == "number")
+ if item == 3 then
+ wait, done = async.waiter();
+ wait();
+ end
+ end);
+
+ r:run(1);
+ assert(r.state == "ready");
+ r:run(2);
+ assert(r.state == "ready");
+ r:run(3);
+ assert(r.state == "waiting");
+ done();
+ assert(r.state == "ready");
+ --for k, v in ipairs(l) do print(k,v) end
+ end);
+
+ it("should work", function ()
+ --------------------
+ local wait, done;
+ local last_item = 0;
+ local r = new(function (item)
+ assert(type(item) == "number")
+ assert(item == last_item + 1);
+ last_item = item;
+ if item == 3 then
+ wait, done = async.waiter();
+ wait();
+ end
+ end);
+
+ r:run(1);
+ assert(r.state == "ready");
+ r:run(2);
+ assert(r.state == "ready");
+ r:run(3);
+ assert(r.state == "waiting");
+ r:run(4);
+ assert(r.state == "waiting");
+ done();
+ assert(r.state == "ready");
+ --for k, v in ipairs(l) do print(k,v) end
+ end);
+ it("should work", function ()
+ --------------------
+ local wait, done;
+ local last_item = 0;
+ local r = new(function (item)
+ assert(type(item) == "number")
+ assert((item == last_item + 1) or item == 3);
+ last_item = item;
+ if item == 3 then
+ wait, done = async.waiter();
+ wait();
+ end
+ end);
+
+ r:run(1);
+ assert(r.state == "ready");
+ r:run(2);
+ assert(r.state == "ready");
+
+ r:run(3);
+ assert(r.state == "waiting");
+ r:run(3);
+ assert(r.state == "waiting");
+ r:run(3);
+ assert(r.state == "waiting");
+ r:run(4);
+ assert(r.state == "waiting");
+
+ for i = 1, 3 do
+ done();
+ if i < 3 then
+ assert(r.state == "waiting");
+ end
+ end
+
+ assert(r.state == "ready");
+ --for k, v in ipairs(l) do print(k,v) end
+ end);
+ it("should work", function ()
+ --------------------
+ local wait, done;
+ local last_item = 0;
+ local r = new(function (item)
+ assert(type(item) == "number")
+ assert((item == last_item + 1) or item == 3);
+ last_item = item;
+ if item == 3 then
+ wait, done = async.waiter();
+ wait();
+ end
+ end);
+
+ r:run(1);
+ assert(r.state == "ready");
+ r:run(2);
+ assert(r.state == "ready");
+
+ r:run(3);
+ assert(r.state == "waiting");
+ r:run(3);
+ assert(r.state == "waiting");
+
+ for i = 1, 2 do
+ done();
+ if i < 2 then
+ assert(r.state == "waiting");
+ end
+ end
+
+ assert(r.state == "ready");
+ r:run(4);
+ assert(r.state == "ready");
+
+ assert(r.state == "ready");
+ --for k, v in ipairs(l) do print(k,v) end
+ end);
+ it("should work with multiple runners in parallel", function ()
+ -- Now with multiple runners
+ --------------------
+ local wait1, done1;
+ local last_item1 = 0;
+ local r1 = new(function (item)
+ assert(type(item) == "number")
+ assert((item == last_item1 + 1) or item == 3);
+ last_item1 = item;
+ if item == 3 then
+ wait1, done1 = async.waiter();
+ wait1();
+ end
+ end, "r1");
+
+ local wait2, done2;
+ local last_item2 = 0;
+ local r2 = new(function (item)
+ assert(type(item) == "number")
+ assert((item == last_item2 + 1) or item == 3);
+ last_item2 = item;
+ if item == 3 then
+ wait2, done2 = async.waiter();
+ wait2();
+ end
+ end, "r2");
+
+ r1:run(1);
+ assert(r1.state == "ready");
+ r1:run(2);
+ assert(r1.state == "ready");
+
+ r1:run(3);
+ assert(r1.state == "waiting");
+ r1:run(3);
+ assert(r1.state == "waiting");
+
+ r2:run(1);
+ assert(r1.state == "waiting");
+ assert(r2.state == "ready");
+
+ r2:run(2);
+ assert(r1.state == "waiting");
+ assert(r2.state == "ready");
+
+ r2:run(3);
+ assert(r1.state == "waiting");
+ assert(r2.state == "waiting");
+ done2();
+
+ r2:run(3);
+ assert(r1.state == "waiting");
+ assert(r2.state == "waiting");
+ done2();
+
+ r2:run(4);
+ assert(r1.state == "waiting");
+ assert(r2.state == "ready");
+
+ for i = 1, 2 do
+ done1();
+ if i < 2 then
+ assert(r1.state == "waiting");
+ end
+ end
+
+ assert(r1.state == "ready");
+ r1:run(4);
+ assert(r1.state == "ready");
+
+ assert(r1.state == "ready");
+ --for k, v in ipairs(l1) do print(k,v) end
+ end);
+ it("should work work with multiple runners in parallel", function ()
+ --------------------
+ local wait1, done1;
+ local last_item1 = 0;
+ local r1 = new(function (item)
+ print("r1 processing ", item);
+ assert(type(item) == "number")
+ assert((item == last_item1 + 1) or item == 3);
+ last_item1 = item;
+ if item == 3 then
+ wait1, done1 = async.waiter();
+ wait1();
+ end
+ end, "r1");
+
+ local wait2, done2;
+ local last_item2 = 0;
+ local r2 = new(function (item)
+ print("r2 processing ", item);
+ assert.is_number(item);
+ assert((item == last_item2 + 1) or item == 3);
+ last_item2 = item;
+ if item == 3 then
+ wait2, done2 = async.waiter();
+ wait2();
+ end
+ end, "r2");
+
+ r1:run(1);
+ assert.equal(r1.state, "ready");
+ r1:run(2);
+ assert.equal(r1.state, "ready");
+
+ r1:run(5);
+ assert.equal(r1.state, "ready");
+
+ r1:run(3);
+ assert.equal(r1.state, "waiting");
+ r1:run(5); -- Will error, when we get to it
+ assert.equal(r1.state, "waiting");
+ done1();
+ assert.equal(r1.state, "ready");
+ r1:run(3);
+ assert.equal(r1.state, "waiting");
+
+ r2:run(1);
+ assert.equal(r1.state, "waiting");
+ assert.equal(r2.state, "ready");
+
+ r2:run(2);
+ assert.equal(r1.state, "waiting");
+ assert.equal(r2.state, "ready");
+
+ r2:run(3);
+ assert.equal(r1.state, "waiting");
+ assert.equal(r2.state, "waiting");
+
+ done2();
+ assert.equal(r1.state, "waiting");
+ assert.equal(r2.state, "ready");
+
+ r2:run(3);
+ assert.equal(r1.state, "waiting");
+ assert.equal(r2.state, "waiting");
+
+ done2();
+ assert.equal(r1.state, "waiting");
+ assert.equal(r2.state, "ready");
+
+ r2:run(4);
+ assert.equal(r1.state, "waiting");
+ assert.equal(r2.state, "ready");
+
+ done1();
+
+ assert.equal(r1.state, "ready");
+ r1:run(4);
+ assert.equal(r1.state, "ready");
+
+ assert.equal(r1.state, "ready");
+ end);
+
+ it("should support multiple done() calls", function ()
+ local processed_item;
+ local wait, done;
+ local r, rf = new(function (item)
+ wait, done = async.waiter(4);
+ wait();
+ processed_item = item;
+ end);
+ r:run("test");
+ for _ = 1, 3 do
+ done();
+ assert.equal(r.state, "waiting");
+ assert.is_nil(processed_item);
+ end
+ done();
+ assert.equal(r.state, "ready");
+ assert.equal(processed_item, "test");
+ assert.spy(r.watchers.error).was_not.called();
+ end);
+
+ it("should not allow done() to be called more than specified", function ()
+ local processed_item;
+ local wait, done;
+ local r, rf = new(function (item)
+ wait, done = async.waiter(4);
+ wait();
+ processed_item = item;
+ end);
+ r:run("test");
+ for _ = 1, 4 do
+ done();
+ end
+ assert.has_error(done);
+ assert.equal(r.state, "ready");
+ assert.equal(processed_item, "test");
+ assert.spy(r.watchers.error).was_not.called();
+ end);
+
+ it("should allow done() to be called before wait()", function ()
+ local processed_item;
+ local r, rf = new(function (item)
+ local wait, done = async.waiter();
+ done();
+ wait();
+ processed_item = item;
+ end);
+ r:run("test");
+ assert.equal(processed_item, "test");
+ assert.equal(r.state, "ready");
+ -- Since the observable state did not change,
+ -- the watchers should not have been called
+ assert.spy(r.watchers.waiting).was_not.called();
+ assert.spy(r.watchers.ready).was_not.called();
+ end);
+ end);
+
+ describe("#ready()", function ()
+ it("should return false outside an async context", function ()
+ assert.falsy(async.ready());
+ end);
+ it("should return true inside an async context", function ()
+ local r = new(function ()
+ assert.truthy(async.ready());
+ end);
+ r:run(true);
+ assert.spy(r.func).was.called();
+ assert.spy(r.watchers.error).was_not.called();
+ end);
+ end);
+end);
diff --git a/spec/util_cache_spec.lua b/spec/util_cache_spec.lua
new file mode 100644
index 00000000..9c7d75fe
--- /dev/null
+++ b/spec/util_cache_spec.lua
@@ -0,0 +1,316 @@
+
+local cache = require "util.cache";
+
+describe("util.cache", function()
+ describe("#new()", function()
+ it("should work", function()
+
+ local c = cache.new(5);
+
+ local function expect_kv(key, value, actual_key, actual_value)
+ assert.are.equal(key, actual_key, "key incorrect");
+ assert.are.equal(value, actual_value, "value incorrect");
+ end
+
+ expect_kv(nil, nil, c:head());
+ expect_kv(nil, nil, c:tail());
+
+ assert.are.equal(c:count(), 0);
+
+ c:set("one", 1)
+ assert.are.equal(c:count(), 1);
+ expect_kv("one", 1, c:head());
+ expect_kv("one", 1, c:tail());
+
+ c:set("two", 2)
+ expect_kv("two", 2, c:head());
+ expect_kv("one", 1, c:tail());
+
+ c:set("three", 3)
+ expect_kv("three", 3, c:head());
+ expect_kv("one", 1, c:tail());
+
+ c:set("four", 4)
+ c:set("five", 5);
+ assert.are.equal(c:count(), 5);
+ expect_kv("five", 5, c:head());
+ expect_kv("one", 1, c:tail());
+
+ c:set("foo", nil);
+ assert.are.equal(c:count(), 5);
+ expect_kv("five", 5, c:head());
+ expect_kv("one", 1, c:tail());
+
+ assert.are.equal(c:get("one"), 1);
+ expect_kv("five", 5, c:head());
+ expect_kv("one", 1, c:tail());
+
+ assert.are.equal(c:get("two"), 2);
+ assert.are.equal(c:get("three"), 3);
+ assert.are.equal(c:get("four"), 4);
+ assert.are.equal(c:get("five"), 5);
+
+ assert.are.equal(c:get("foo"), nil);
+ assert.are.equal(c:get("bar"), nil);
+
+ c:set("six", 6);
+ assert.are.equal(c:count(), 5);
+ expect_kv("six", 6, c:head());
+ expect_kv("two", 2, c:tail());
+
+ assert.are.equal(c:get("one"), nil);
+ assert.are.equal(c:get("two"), 2);
+ assert.are.equal(c:get("three"), 3);
+ assert.are.equal(c:get("four"), 4);
+ assert.are.equal(c:get("five"), 5);
+ assert.are.equal(c:get("six"), 6);
+
+ c:set("three", nil);
+ assert.are.equal(c:count(), 4);
+
+ assert.are.equal(c:get("one"), nil);
+ assert.are.equal(c:get("two"), 2);
+ assert.are.equal(c:get("three"), nil);
+ assert.are.equal(c:get("four"), 4);
+ assert.are.equal(c:get("five"), 5);
+ assert.are.equal(c:get("six"), 6);
+
+ c:set("seven", 7);
+ assert.are.equal(c:count(), 5);
+
+ assert.are.equal(c:get("one"), nil);
+ assert.are.equal(c:get("two"), 2);
+ assert.are.equal(c:get("three"), nil);
+ assert.are.equal(c:get("four"), 4);
+ assert.are.equal(c:get("five"), 5);
+ assert.are.equal(c:get("six"), 6);
+ assert.are.equal(c:get("seven"), 7);
+
+ c:set("eight", 8);
+ assert.are.equal(c:count(), 5);
+
+ assert.are.equal(c:get("one"), nil);
+ assert.are.equal(c:get("two"), nil);
+ assert.are.equal(c:get("three"), nil);
+ assert.are.equal(c:get("four"), 4);
+ assert.are.equal(c:get("five"), 5);
+ assert.are.equal(c:get("six"), 6);
+ assert.are.equal(c:get("seven"), 7);
+ assert.are.equal(c:get("eight"), 8);
+
+ c:set("four", 4);
+ assert.are.equal(c:count(), 5);
+
+ assert.are.equal(c:get("one"), nil);
+ assert.are.equal(c:get("two"), nil);
+ assert.are.equal(c:get("three"), nil);
+ assert.are.equal(c:get("four"), 4);
+ assert.are.equal(c:get("five"), 5);
+ assert.are.equal(c:get("six"), 6);
+ assert.are.equal(c:get("seven"), 7);
+ assert.are.equal(c:get("eight"), 8);
+
+ c:set("nine", 9);
+ assert.are.equal(c:count(), 5);
+
+ assert.are.equal(c:get("one"), nil);
+ assert.are.equal(c:get("two"), nil);
+ assert.are.equal(c:get("three"), nil);
+ assert.are.equal(c:get("four"), 4);
+ assert.are.equal(c:get("five"), nil);
+ assert.are.equal(c:get("six"), 6);
+ assert.are.equal(c:get("seven"), 7);
+ assert.are.equal(c:get("eight"), 8);
+ assert.are.equal(c:get("nine"), 9);
+
+ do
+ local keys = { "nine", "four", "eight", "seven", "six" };
+ local values = { 9, 4, 8, 7, 6 };
+ local i = 0;
+ for k, v in c:items() do
+ i = i + 1;
+ assert.are.equal(k, keys[i]);
+ assert.are.equal(v, values[i]);
+ end
+ assert.are.equal(i, 5);
+
+ c:set("four", "2+2");
+ assert.are.equal(c:count(), 5);
+
+ assert.are.equal(c:get("one"), nil);
+ assert.are.equal(c:get("two"), nil);
+ assert.are.equal(c:get("three"), nil);
+ assert.are.equal(c:get("four"), "2+2");
+ assert.are.equal(c:get("five"), nil);
+ assert.are.equal(c:get("six"), 6);
+ assert.are.equal(c:get("seven"), 7);
+ assert.are.equal(c:get("eight"), 8);
+ assert.are.equal(c:get("nine"), 9);
+ end
+
+ do
+ local keys = { "four", "nine", "eight", "seven", "six" };
+ local values = { "2+2", 9, 8, 7, 6 };
+ local i = 0;
+ for k, v in c:items() do
+ i = i + 1;
+ assert.are.equal(k, keys[i]);
+ assert.are.equal(v, values[i]);
+ end
+ assert.are.equal(i, 5);
+
+ c:set("foo", nil);
+ assert.are.equal(c:count(), 5);
+
+ assert.are.equal(c:get("one"), nil);
+ assert.are.equal(c:get("two"), nil);
+ assert.are.equal(c:get("three"), nil);
+ assert.are.equal(c:get("four"), "2+2");
+ assert.are.equal(c:get("five"), nil);
+ assert.are.equal(c:get("six"), 6);
+ assert.are.equal(c:get("seven"), 7);
+ assert.are.equal(c:get("eight"), 8);
+ assert.are.equal(c:get("nine"), 9);
+ end
+
+ do
+ local keys = { "four", "nine", "eight", "seven", "six" };
+ local values = { "2+2", 9, 8, 7, 6 };
+ local i = 0;
+ for k, v in c:items() do
+ i = i + 1;
+ assert.are.equal(k, keys[i]);
+ assert.are.equal(v, values[i]);
+ end
+ assert.are.equal(i, 5);
+
+ c:set("four", nil);
+
+ assert.are.equal(c:get("one"), nil);
+ assert.are.equal(c:get("two"), nil);
+ assert.are.equal(c:get("three"), nil);
+ assert.are.equal(c:get("four"), nil);
+ assert.are.equal(c:get("five"), nil);
+ assert.are.equal(c:get("six"), 6);
+ assert.are.equal(c:get("seven"), 7);
+ assert.are.equal(c:get("eight"), 8);
+ assert.are.equal(c:get("nine"), 9);
+ end
+
+ do
+ local keys = { "nine", "eight", "seven", "six" };
+ local values = { 9, 8, 7, 6 };
+ local i = 0;
+ for k, v in c:items() do
+ i = i + 1;
+ assert.are.equal(k, keys[i]);
+ assert.are.equal(v, values[i]);
+ end
+ assert.are.equal(i, 4);
+ end
+
+ do
+ local evicted_key, evicted_value;
+ local c2 = cache.new(3, function (_key, _value)
+ evicted_key, evicted_value = _key, _value;
+ end);
+ local function set(k, v, should_evict_key, should_evict_value)
+ evicted_key, evicted_value = nil, nil;
+ c2:set(k, v);
+ assert.are.equal(evicted_key, should_evict_key);
+ assert.are.equal(evicted_value, should_evict_value);
+ end
+ set("a", 1)
+ set("a", 1)
+ set("a", 1)
+ set("a", 1)
+ set("a", 1)
+
+ set("b", 2)
+ set("c", 3)
+ set("b", 2)
+ set("d", 4, "a", 1)
+ set("e", 5, "c", 3)
+ end
+
+ do
+ local evicted_key, evicted_value;
+ local c3 = cache.new(1, function (_key, _value)
+ evicted_key, evicted_value = _key, _value;
+ if _key == "a" then
+ -- Sanity check for what we're evicting
+ assert.are.equal(_key, "a");
+ assert.are.equal(_value, 1);
+ -- We're going to block eviction of this key/value, so set to nil...
+ evicted_key, evicted_value = nil, nil;
+ -- Returning false to block eviction
+ return false
+ end
+ end);
+ local function set(k, v, should_evict_key, should_evict_value)
+ evicted_key, evicted_value = nil, nil;
+ local ret = c3:set(k, v);
+ assert.are.equal(evicted_key, should_evict_key);
+ assert.are.equal(evicted_value, should_evict_value);
+ return ret;
+ end
+ set("a", 1)
+ set("a", 1)
+ set("a", 1)
+ set("a", 1)
+ set("a", 1)
+
+ -- Our on_evict prevents "a" from being evicted, causing this to fail...
+ assert.are.equal(set("b", 2), false, "Failed to prevent eviction, or signal result");
+
+ expect_kv("a", 1, c3:head());
+ expect_kv("a", 1, c3:tail());
+
+ -- Check the final state is what we expect
+ assert.are.equal(c3:get("a"), 1);
+ assert.are.equal(c3:get("b"), nil);
+ assert.are.equal(c3:count(), 1);
+ end
+
+
+ local c4 = cache.new(3, false);
+
+ assert.are.equal(c4:set("a", 1), true);
+ assert.are.equal(c4:set("a", 1), true);
+ assert.are.equal(c4:set("a", 1), true);
+ assert.are.equal(c4:set("a", 1), true);
+ assert.are.equal(c4:set("b", 2), true);
+ assert.are.equal(c4:set("c", 3), true);
+ assert.are.equal(c4:set("d", 4), false);
+ assert.are.equal(c4:set("d", 4), false);
+ assert.are.equal(c4:set("d", 4), false);
+
+ expect_kv("c", 3, c4:head());
+ expect_kv("a", 1, c4:tail());
+
+ local c5 = cache.new(3, function (k, v)
+ if k == "a" then
+ return nil;
+ elseif k == "b" then
+ return true;
+ end
+ return false;
+ end);
+
+ assert.are.equal(c5:set("a", 1), true);
+ assert.are.equal(c5:set("a", 1), true);
+ assert.are.equal(c5:set("a", 1), true);
+ assert.are.equal(c5:set("a", 1), true);
+ assert.are.equal(c5:set("b", 2), true);
+ assert.are.equal(c5:set("c", 3), true);
+ assert.are.equal(c5:set("d", 4), true); -- "a" evicted (cb returned nil)
+ assert.are.equal(c5:set("d", 4), true); -- nop
+ assert.are.equal(c5:set("d", 4), true); -- nop
+ assert.are.equal(c5:set("e", 5), true); -- "b" evicted (cb returned true)
+ assert.are.equal(c5:set("f", 6), false); -- "c" won't evict (cb returned false)
+
+ expect_kv("e", 5, c5:head());
+ expect_kv("c", 3, c5:tail());
+ end);
+ end);
+end);
diff --git a/spec/util_dataforms_spec.lua b/spec/util_dataforms_spec.lua
new file mode 100644
index 00000000..89759035
--- /dev/null
+++ b/spec/util_dataforms_spec.lua
@@ -0,0 +1,427 @@
+local dataforms = require "util.dataforms";
+local st = require "util.stanza";
+local jid = require "util.jid";
+local iter = require "util.iterators";
+
+describe("util.dataforms", function ()
+ local some_form, xform;
+ setup(function ()
+ some_form = dataforms.new({
+ title = "form-title",
+ instructions = "form-instructions",
+ {
+ type = "hidden",
+ name = "FORM_TYPE",
+ value = "xmpp:prosody.im/spec/util.dataforms#1",
+ };
+ {
+ type = "fixed";
+ value = "Fixed field";
+ },
+ {
+ type = "boolean",
+ label = "boolean-label",
+ name = "boolean-field",
+ value = true,
+ },
+ {
+ type = "fixed",
+ label = "fixed-label",
+ name = "fixed-field",
+ value = "fixed-value",
+ },
+ {
+ type = "hidden",
+ label = "hidden-label",
+ name = "hidden-field",
+ value = "hidden-value",
+ },
+ {
+ type = "jid-multi",
+ label = "jid-multi-label",
+ name = "jid-multi-field",
+ value = {
+ "jid@multi/value#1",
+ "jid@multi/value#2",
+ },
+ },
+ {
+ type = "jid-single",
+ label = "jid-single-label",
+ name = "jid-single-field",
+ value = "jid@single/value",
+ },
+ {
+ type = "list-multi",
+ label = "list-multi-label",
+ name = "list-multi-field",
+ value = {
+ "list-multi-option-value#1",
+ "list-multi-option-value#3",
+ },
+ options = {
+ {
+ label = "list-multi-option-label#1",
+ value = "list-multi-option-value#1",
+ default = true,
+ },
+ {
+ label = "list-multi-option-label#2",
+ value = "list-multi-option-value#2",
+ default = false,
+ },
+ {
+ label = "list-multi-option-label#3",
+ value = "list-multi-option-value#3",
+ default = true,
+ },
+ }
+ },
+ {
+ type = "list-single",
+ label = "list-single-label",
+ name = "list-single-field",
+ value = "list-single-value",
+ options = {
+ "list-single-value",
+ "list-single-value#2",
+ "list-single-value#3",
+ }
+ },
+ {
+ type = "text-multi",
+ label = "text-multi-label",
+ name = "text-multi-field",
+ value = "text\nmulti\nvalue",
+ },
+ {
+ type = "text-private",
+ label = "text-private-label",
+ name = "text-private-field",
+ value = "text-private-value",
+ },
+ {
+ type = "text-single",
+ label = "text-single-label",
+ name = "text-single-field",
+ value = "text-single-value",
+ },
+ });
+ xform = some_form:form();
+ end);
+
+ it("works", function ()
+ assert.truthy(xform);
+ assert.truthy(st.is_stanza(xform));
+ assert.equal("x", xform.name);
+ assert.equal("jabber:x:data", xform.attr.xmlns);
+ assert.equal("FORM_TYPE", xform:find("field@var"));
+ assert.equal("xmpp:prosody.im/spec/util.dataforms#1", xform:find("field/value#"));
+ local allowed_direct_children = {
+ title = true,
+ instructions = true,
+ field = true,
+ }
+ for tag in xform:childtags() do
+ assert.truthy(allowed_direct_children[tag.name], "unknown direct child");
+ end
+ end);
+
+ it("produced boolean field correctly", function ()
+ local f;
+ for field in xform:childtags("field") do
+ if field.attr.var == "boolean-field" then
+ f = field;
+ break;
+ end
+ end
+
+ assert.truthy(st.is_stanza(f));
+ assert.equal("boolean-field", f.attr.var);
+ assert.equal("boolean", f.attr.type);
+ assert.equal("boolean-label", f.attr.label);
+ assert.equal(1, iter.count(f:childtags("value")));
+ local val = f:get_child_text("value");
+ assert.truthy(val == "true" or val == "1");
+ end);
+
+ it("produced fixed field correctly", function ()
+ local f;
+ for field in xform:childtags("field") do
+ if field.attr.var == "fixed-field" then
+ f = field;
+ break;
+ end
+ end
+
+ assert.truthy(st.is_stanza(f));
+ assert.equal("fixed-field", f.attr.var);
+ assert.equal("fixed", f.attr.type);
+ assert.equal("fixed-label", f.attr.label);
+ assert.equal(1, iter.count(f:childtags("value")));
+ assert.equal("fixed-value", f:get_child_text("value"));
+ end);
+
+ it("produced hidden field correctly", function ()
+ local f;
+ for field in xform:childtags("field") do
+ if field.attr.var == "hidden-field" then
+ f = field;
+ break;
+ end
+ end
+
+ assert.truthy(st.is_stanza(f));
+ assert.equal("hidden-field", f.attr.var);
+ assert.equal("hidden", f.attr.type);
+ assert.equal("hidden-label", f.attr.label);
+ assert.equal(1, iter.count(f:childtags("value")));
+ assert.equal("hidden-value", f:get_child_text("value"));
+ end);
+
+ it("produced jid-multi field correctly", function ()
+ local f;
+ for field in xform:childtags("field") do
+ if field.attr.var == "jid-multi-field" then
+ f = field;
+ break;
+ end
+ end
+
+ assert.truthy(st.is_stanza(f));
+ assert.equal("jid-multi-field", f.attr.var);
+ assert.equal("jid-multi", f.attr.type);
+ assert.equal("jid-multi-label", f.attr.label);
+ assert.equal(2, iter.count(f:childtags("value")));
+
+ local i = 0;
+ for value in f:childtags("value") do
+ i = i + 1;
+ assert.equal(("jid@multi/value#%d"):format(i), value:get_text());
+ end
+ end);
+
+ it("produced jid-single field correctly", function ()
+ local f;
+ for field in xform:childtags("field") do
+ if field.attr.var == "jid-single-field" then
+ f = field;
+ break;
+ end
+ end
+
+ assert.truthy(st.is_stanza(f));
+ assert.equal("jid-single-field", f.attr.var);
+ assert.equal("jid-single", f.attr.type);
+ assert.equal("jid-single-label", f.attr.label);
+ assert.equal(1, iter.count(f:childtags("value")));
+ assert.equal("jid@single/value", f:get_child_text("value"));
+ assert.truthy(jid.prep(f:get_child_text("value")));
+ end);
+
+ it("produced list-multi field correctly", function ()
+ local f;
+ for field in xform:childtags("field") do
+ if field.attr.var == "list-multi-field" then
+ f = field;
+ break;
+ end
+ end
+
+ assert.truthy(st.is_stanza(f));
+ assert.equal("list-multi-field", f.attr.var);
+ assert.equal("list-multi", f.attr.type);
+ assert.equal("list-multi-label", f.attr.label);
+ assert.equal(2, iter.count(f:childtags("value")));
+ assert.equal("list-multi-option-value#1", f:get_child_text("value"));
+ assert.equal(3, iter.count(f:childtags("option")));
+ end);
+
+ it("produced list-single field correctly", function ()
+ local f;
+ for field in xform:childtags("field") do
+ if field.attr.var == "list-single-field" then
+ f = field;
+ break;
+ end
+ end
+
+ assert.truthy(st.is_stanza(f));
+ assert.equal("list-single-field", f.attr.var);
+ assert.equal("list-single", f.attr.type);
+ assert.equal("list-single-label", f.attr.label);
+ assert.equal(1, iter.count(f:childtags("value")));
+ assert.equal("list-single-value", f:get_child_text("value"));
+ assert.equal(3, iter.count(f:childtags("option")));
+ end);
+
+ it("produced text-multi field correctly", function ()
+ local f;
+ for field in xform:childtags("field") do
+ if field.attr.var == "text-multi-field" then
+ f = field;
+ break;
+ end
+ end
+
+ assert.truthy(st.is_stanza(f));
+ assert.equal("text-multi-field", f.attr.var);
+ assert.equal("text-multi", f.attr.type);
+ assert.equal("text-multi-label", f.attr.label);
+ assert.equal(3, iter.count(f:childtags("value")));
+ end);
+
+ it("produced text-private field correctly", function ()
+ local f;
+ for field in xform:childtags("field") do
+ if field.attr.var == "text-private-field" then
+ f = field;
+ break;
+ end
+ end
+
+ assert.truthy(st.is_stanza(f));
+ assert.equal("text-private-field", f.attr.var);
+ assert.equal("text-private", f.attr.type);
+ assert.equal("text-private-label", f.attr.label);
+ assert.equal(1, iter.count(f:childtags("value")));
+ assert.equal("text-private-value", f:get_child_text("value"));
+ end);
+
+ it("produced text-single field correctly", function ()
+ local f;
+ for field in xform:childtags("field") do
+ if field.attr.var == "text-single-field" then
+ f = field;
+ break;
+ end
+ end
+
+ assert.truthy(st.is_stanza(f));
+ assert.equal("text-single-field", f.attr.var);
+ assert.equal("text-single", f.attr.type);
+ assert.equal("text-single-label", f.attr.label);
+ assert.equal(1, iter.count(f:childtags("value")));
+ assert.equal("text-single-value", f:get_child_text("value"));
+ end);
+
+ describe("get_type()", function ()
+ it("identifes dataforms", function ()
+ assert.equal(nil, dataforms.get_type(nil));
+ assert.equal(nil, dataforms.get_type(""));
+ assert.equal(nil, dataforms.get_type({}));
+ assert.equal(nil, dataforms.get_type(st.stanza("no-a-form")));
+ assert.equal("xmpp:prosody.im/spec/util.dataforms#1", dataforms.get_type(xform));
+ end);
+ end);
+
+ describe(":data", function ()
+ it("works", function ()
+ assert.truthy(some_form:data(xform));
+ end);
+ end);
+
+ describe("issue1177", function ()
+ local form_with_stuff;
+ setup(function ()
+ form_with_stuff = dataforms.new({
+ {
+ type = "list-single";
+ name = "abtest";
+ label = "A or B?";
+ options = {
+ { label = "A", value = "a", default = true },
+ { label = "B", value = "b" },
+ };
+ },
+ });
+ end);
+
+ it("includes options when value is included", function ()
+ local f = form_with_stuff:form({ abtest = "a" });
+ assert.truthy(f:find("field/option"));
+ end);
+
+ it("includes options when value is excluded", function ()
+ local f = form_with_stuff:form({});
+ assert.truthy(f:find("field/option"));
+ end);
+ end);
+
+ describe("using current values in place of missing fields", function ()
+ it("gets back the previous values when given an empty form", function ()
+ local current = {
+ ["list-multi-field"] = {
+ "list-multi-option-value#2";
+ };
+ ["list-single-field"] = "list-single-value#2";
+ ["hidden-field"] = "hidden-value";
+ ["boolean-field"] = false;
+ ["text-multi-field"] = "words\ngo\nhere";
+ ["jid-single-field"] = "alice@example.com";
+ ["text-private-field"] = "hunter2";
+ ["text-single-field"] = "text-single-value";
+ ["jid-multi-field"] = {
+ "bob@example.net";
+ };
+ };
+ local expect = {
+ -- FORM_TYPE = "xmpp:prosody.im/spec/util.dataforms#1"; -- does this need to be included?
+ ["list-multi-field"] = {
+ "list-multi-option-value#2";
+ };
+ ["list-single-field"] = "list-single-value#2";
+ ["hidden-field"] = "hidden-value";
+ ["boolean-field"] = false;
+ ["text-multi-field"] = "words\ngo\nhere";
+ ["jid-single-field"] = "alice@example.com";
+ ["text-private-field"] = "hunter2";
+ ["text-single-field"] = "text-single-value";
+ ["jid-multi-field"] = {
+ "bob@example.net";
+ };
+ };
+ local data, err = some_form:data(st.stanza("x", {xmlns="jabber:x:data"}), current);
+ assert.is.table(data, err);
+ assert.same(expect, data, "got back the same data");
+ end);
+ end);
+
+ describe("field 'var' property", function ()
+ it("works as expected", function ()
+ local f = dataforms.new {
+ {
+ var = "someprefix#the-field",
+ name = "the_field",
+ type = "text-single",
+ }
+ };
+ local x = f:form({the_field = "hello"});
+ assert.equal("someprefix#the-field", x:find"field@var");
+ assert.equal("hello", x:find"field/value#");
+ end);
+ end);
+
+ describe("validation", function ()
+ local f = dataforms.new {
+ {
+ name = "number",
+ type = "text-single",
+ datatype = "xs:integer",
+ },
+ };
+
+ it("works", function ()
+ local d = f:data(f:form({number = 1}));
+ assert.equal(1, d.number);
+ end);
+
+ it("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);
+ end);
+end);
+
diff --git a/spec/util_datetime_spec.lua b/spec/util_datetime_spec.lua
new file mode 100644
index 00000000..497ab7d3
--- /dev/null
+++ b/spec/util_datetime_spec.lua
@@ -0,0 +1,76 @@
+local util_datetime = require "util.datetime";
+
+describe("util.datetime", function ()
+ it("should have been loaded", function ()
+ assert.is_table(util_datetime);
+ end);
+ describe("#date", function ()
+ local date = util_datetime.date;
+ it("should exist", function ()
+ assert.is_function(date);
+ end);
+ it("should return a string", function ()
+ assert.is_string(date());
+ end);
+ it("should look like a date", function ()
+ assert.truthy(string.find(date(), "^%d%d%d%d%-%d%d%-%d%d$"));
+ end);
+ it("should work", function ()
+ assert.equals(date(1136239445), "2006-01-02");
+ end);
+ end);
+ describe("#time", function ()
+ local time = util_datetime.time;
+ it("should exist", function ()
+ assert.is_function(time);
+ end);
+ it("should return a string", function ()
+ assert.is_string(time());
+ end);
+ it("should look like a timestamp", function ()
+ -- Note: Sub-second precision and timezones are ignored
+ assert.truthy(string.find(time(), "^%d%d:%d%d:%d%d"));
+ end);
+ it("should work", function ()
+ assert.equals(time(1136239445), "22:04:05");
+ end);
+ end);
+ describe("#datetime", function ()
+ local datetime = util_datetime.datetime;
+ it("should exist", function ()
+ assert.is_function(datetime);
+ end);
+ it("should return a string", function ()
+ assert.is_string(datetime());
+ end);
+ it("should look like a timestamp", function ()
+ -- Note: Sub-second precision and timezones are ignored
+ assert.truthy(string.find(datetime(), "^%d%d%d%d%-%d%d%-%d%dT%d%d:%d%d:%d%d"));
+ end);
+ it("should work", function ()
+ assert.equals(datetime(1136239445), "2006-01-02T22:04:05Z");
+ end);
+ end);
+ describe("#legacy", function ()
+ local legacy = util_datetime.legacy;
+ it("should exist", function ()
+ assert.is_function(legacy);
+ end);
+ end);
+ describe("#parse", function ()
+ local parse = util_datetime.parse;
+ it("should exist", function ()
+ assert.is_function(parse);
+ end);
+ it("should work", function ()
+ -- Timestamp used by Go
+ assert.equals(parse("2017-11-19T17:58:13Z"), 1511114293);
+ assert.equals(parse("2017-11-19T18:58:50+0100"), 1511114330);
+ assert.equals(parse("2006-01-02T15:04:05-0700"), 1136239445);
+ end);
+ it("should handle timezones", function ()
+ -- https://xmpp.org/extensions/xep-0082.html#example-2 and 3
+ assert.equals(parse("1969-07-21T02:56:15Z"), parse("1969-07-20T21:56:15-05:00"));
+ end);
+ end);
+end);
diff --git a/spec/util_encodings_spec.lua b/spec/util_encodings_spec.lua
new file mode 100644
index 00000000..0f4fc2b7
--- /dev/null
+++ b/spec/util_encodings_spec.lua
@@ -0,0 +1,41 @@
+
+local encodings = require "util.encodings";
+local utf8 = assert(encodings.utf8, "no encodings.utf8 module");
+
+describe("util.encodings", function ()
+ describe("#encode()", function()
+ it("should work", function ()
+ assert.is.equal(encodings.base64.encode(""), "");
+ assert.is.equal(encodings.base64.encode('coucou'), "Y291Y291");
+ assert.is.equal(encodings.base64.encode("\0\0\0"), "AAAA");
+ assert.is.equal(encodings.base64.encode("\255\255\255"), "////");
+ end);
+ end);
+ describe("#decode()", function()
+ it("should work", function ()
+ assert.is.equal(encodings.base64.decode(""), "");
+ assert.is.equal(encodings.base64.decode("="), "");
+ assert.is.equal(encodings.base64.decode('Y291Y291'), "coucou");
+ assert.is.equal(encodings.base64.decode("AAAA"), "\0\0\0");
+ assert.is.equal(encodings.base64.decode("////"), "\255\255\255");
+ end);
+ end);
+end);
+describe("util.encodings.utf8", function()
+ describe("#valid()", function()
+ it("should work", function()
+
+ for line in io.lines("spec/utf8_sequences.txt") do
+ local data = line:match(":%s*([^#]+)"):gsub("%s+", ""):gsub("..", function (c) return string.char(tonumber(c, 16)); end)
+ local expect = line:match("(%S+):");
+
+ assert(expect == "pass" or expect == "fail", "unknown expectation: "..line:match("^[^:]+"));
+
+ local valid = utf8.valid(data);
+ assert.is.equal(valid, utf8.valid(data.." "));
+ assert.is.equal(valid, expect == "pass", line);
+ end
+
+ end);
+ end);
+end);
diff --git a/spec/util_events_spec.lua b/spec/util_events_spec.lua
new file mode 100644
index 00000000..fee60f8f
--- /dev/null
+++ b/spec/util_events_spec.lua
@@ -0,0 +1,212 @@
+local events = require "util.events";
+
+describe("util.events", function ()
+ it("should export a new() function", function ()
+ assert.is_function(events.new);
+ end);
+ describe("new()", function ()
+ it("should return return a new events object", function ()
+ local e = events.new();
+ assert.is_function(e.add_handler);
+ assert.is_function(e.remove_handler);
+ end);
+ end);
+
+ local e, h;
+
+
+ describe("API", function ()
+ before_each(function ()
+ e = events.new();
+ h = spy.new(function () end);
+ end);
+
+ it("should call handlers when an event is fired", function ()
+ e.add_handler("myevent", h);
+ e.fire_event("myevent");
+ assert.spy(h).was_called();
+ end);
+
+ it("should not call handlers when a different event is fired", function ()
+ e.add_handler("myevent", h);
+ e.fire_event("notmyevent");
+ assert.spy(h).was_not_called();
+ end);
+
+ it("should pass the data argument to handlers", function ()
+ e.add_handler("myevent", h);
+ e.fire_event("myevent", "mydata");
+ assert.spy(h).was_called_with("mydata");
+ end);
+
+ it("should support non-string events", function ()
+ local myevent = {};
+ e.add_handler(myevent, h);
+ e.fire_event(myevent, "mydata");
+ assert.spy(h).was_called_with("mydata");
+ end);
+
+ it("should call handlers in priority order", function ()
+ local data = {};
+ e.add_handler("myevent", function () table.insert(data, "h1"); end, 5);
+ e.add_handler("myevent", function () table.insert(data, "h2"); end, 3);
+ e.add_handler("myevent", function () table.insert(data, "h3"); end);
+ e.fire_event("myevent", "mydata");
+ assert.same(data, { "h1", "h2", "h3" });
+ end);
+
+ it("should support non-integer priority values", function ()
+ local data = {};
+ e.add_handler("myevent", function () table.insert(data, "h1"); end, 1);
+ e.add_handler("myevent", function () table.insert(data, "h2"); end, 0.5);
+ e.add_handler("myevent", function () table.insert(data, "h3"); end, 0.25);
+ e.fire_event("myevent", "mydata");
+ assert.same(data, { "h1", "h2", "h3" });
+ end);
+
+ it("should support negative priority values", function ()
+ local data = {};
+ e.add_handler("myevent", function () table.insert(data, "h1"); end, 1);
+ e.add_handler("myevent", function () table.insert(data, "h2"); end, 0);
+ e.add_handler("myevent", function () table.insert(data, "h3"); end, -1);
+ e.fire_event("myevent", "mydata");
+ assert.same(data, { "h1", "h2", "h3" });
+ end);
+
+ it("should support removing handlers", function ()
+ e.add_handler("myevent", h);
+ e.fire_event("myevent");
+ e.remove_handler("myevent", h);
+ e.fire_event("myevent");
+ assert.spy(h).was_called(1);
+ end);
+
+ it("should support adding multiple handlers at the same time", function ()
+ local ht = {
+ myevent1 = spy.new(function () end);
+ myevent2 = spy.new(function () end);
+ myevent3 = spy.new(function () end);
+ };
+ e.add_handlers(ht);
+ e.fire_event("myevent1");
+ e.fire_event("myevent2");
+ assert.spy(ht.myevent1).was_called();
+ assert.spy(ht.myevent2).was_called();
+ assert.spy(ht.myevent3).was_not_called();
+ end);
+
+ it("should support removing multiple handlers at the same time", function ()
+ local ht = {
+ myevent1 = spy.new(function () end);
+ myevent2 = spy.new(function () end);
+ myevent3 = spy.new(function () end);
+ };
+ e.add_handlers(ht);
+ e.remove_handlers(ht);
+ e.fire_event("myevent1");
+ e.fire_event("myevent2");
+ assert.spy(ht.myevent1).was_not_called();
+ assert.spy(ht.myevent2).was_not_called();
+ assert.spy(ht.myevent3).was_not_called();
+ end);
+
+ pending("should support adding handlers within an event handler")
+ pending("should support removing handlers within an event handler")
+
+ it("should support getting the current handlers for an event", function ()
+ e.add_handler("myevent", h);
+ local handlers = e.get_handlers("myevent");
+ assert.equal(h, handlers[1]);
+ end);
+
+ describe("wrappers", function ()
+ local w
+ before_each(function ()
+ w = spy.new(function (handlers, event_name, event_data)
+ assert.is_function(handlers);
+ assert.equal("myevent", event_name)
+ assert.equal("abc", event_data);
+ return handlers(event_name, event_data);
+ end);
+ end);
+
+ it("should get called", function ()
+ e.add_wrapper("myevent", w);
+ e.add_handler("myevent", h);
+ e.fire_event("myevent", "abc");
+ assert.spy(w).was_called(1);
+ assert.spy(h).was_called(1);
+ end);
+
+ it("should be removable", function ()
+ e.add_wrapper("myevent", w);
+ e.add_handler("myevent", h);
+ e.fire_event("myevent", "abc");
+ e.remove_wrapper("myevent", w);
+ e.fire_event("myevent", "abc");
+ assert.spy(w).was_called(1);
+ assert.spy(h).was_called(2);
+ end);
+
+ it("should allow multiple wrappers", function ()
+ local w2 = spy.new(function (handlers, event_name, event_data)
+ return handlers(event_name, event_data);
+ end);
+ e.add_wrapper("myevent", w);
+ e.add_handler("myevent", h);
+ e.add_wrapper("myevent", w2);
+ e.fire_event("myevent", "abc");
+ e.remove_wrapper("myevent", w);
+ e.fire_event("myevent", "abc");
+ assert.spy(w).was_called(1);
+ assert.spy(w2).was_called(2);
+ assert.spy(h).was_called(2);
+ end);
+
+ it("should support a mix of global and event wrappers", function ()
+ local w2 = spy.new(function (handlers, event_name, event_data)
+ return handlers(event_name, event_data);
+ end);
+ e.add_wrapper(false, w);
+ e.add_handler("myevent", h);
+ e.add_wrapper("myevent", w2);
+ e.fire_event("myevent", "abc");
+ e.remove_wrapper(false, w);
+ e.fire_event("myevent", "abc");
+ assert.spy(w).was_called(1);
+ assert.spy(w2).was_called(2);
+ assert.spy(h).was_called(2);
+ end);
+ end);
+
+ describe("global wrappers", function ()
+ local w
+ before_each(function ()
+ w = spy.new(function (handlers, event_name, event_data)
+ assert.is_function(handlers);
+ assert.equal("myevent", event_name)
+ assert.equal("abc", event_data);
+ return handlers(event_name, event_data);
+ end);
+ end);
+
+ it("should get called", function ()
+ e.add_wrapper(false, w);
+ e.add_handler("myevent", h);
+ e.fire_event("myevent", "abc");
+ assert.spy(w).was_called(1);
+ assert.spy(h).was_called(1);
+ end);
+
+ it("should be removable", function ()
+ e.add_wrapper(false, w);
+ e.add_handler("myevent", h);
+ e.fire_event("myevent", "abc");
+ e.remove_wrapper(false, w);
+ e.fire_event("myevent", "abc");
+ assert.spy(w).was_called(1);
+ assert.spy(h).was_called(2);
+ end);
+ end);
+ end);
+end);
diff --git a/spec/util_format_spec.lua b/spec/util_format_spec.lua
new file mode 100644
index 00000000..7e6a0c6e
--- /dev/null
+++ b/spec/util_format_spec.lua
@@ -0,0 +1,14 @@
+local format = require "util.format".format;
+
+describe("util.format", function()
+ describe("#format()", function()
+ it("should work", function()
+ assert.equal("hello", format("%s", "hello"));
+ assert.equal("<nil>", format("%s"));
+ assert.equal(" [<nil>]", format("", nil));
+ assert.equal("true", format("%s", true));
+ assert.equal("[true]", format("%d", true));
+ assert.equal("% [true]", format("%%", true));
+ end);
+ end);
+end);
diff --git a/spec/util_http_spec.lua b/spec/util_http_spec.lua
new file mode 100644
index 00000000..bacfcfb5
--- /dev/null
+++ b/spec/util_http_spec.lua
@@ -0,0 +1,64 @@
+
+local http = require "util.http";
+
+describe("util.http", function()
+ describe("#urlencode()", function()
+ it("should not change normal characters", function()
+ assert.are.equal(http.urlencode("helloworld123"), "helloworld123");
+ end);
+
+ it("should escape spaces", function()
+ assert.are.equal(http.urlencode("hello world"), "hello%20world");
+ end);
+
+ it("should escape important URL characters", function()
+ assert.are.equal(http.urlencode("This & that = something"), "This%20%26%20that%20%3d%20something");
+ end);
+ end);
+
+ describe("#urldecode()", function()
+ it("should not change normal characters", function()
+ assert.are.equal("helloworld123", http.urldecode("helloworld123"), "Normal characters not escaped");
+ end);
+
+ it("should decode spaces", function()
+ assert.are.equal("hello world", http.urldecode("hello%20world"), "Spaces escaped");
+ end);
+
+ 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);
+ end);
+
+ describe("#formencode()", function()
+ it("should encode basic data", function()
+ assert.are.equal(http.formencode({ { name = "one", value = "1"}, { name = "two", value = "2" } }), "one=1&two=2", "Form encoded");
+ end);
+
+ it("should encode special characters with escaping", function()
+ assert.are.equal(http.formencode({ { name = "one two", value = "1"}, { name = "two one&", value = "2" } }), "one+two=1&two+one%26=2", "Form encoded");
+ end);
+ end);
+
+ describe("#formdecode()", function()
+ it("should decode basic data", function()
+ local t = http.formdecode("one=1&two=2");
+ assert.are.same(t, {
+ { name = "one", value = "1" };
+ { name = "two", value = "2" };
+ one = "1";
+ two = "2";
+ });
+ end);
+
+ it("should decode special characters", function()
+ local t = http.formdecode("one+two=1&two+one%26=2");
+ assert.are.same(t, {
+ { name = "one two", value = "1" };
+ { name = "two one&", value = "2" };
+ ["one two"] = "1";
+ ["two one&"] = "2";
+ });
+ end);
+ end);
+end);
diff --git a/spec/util_ip_spec.lua b/spec/util_ip_spec.lua
new file mode 100644
index 00000000..be5e4cff
--- /dev/null
+++ b/spec/util_ip_spec.lua
@@ -0,0 +1,103 @@
+
+local ip = require "util.ip";
+
+local new_ip = ip.new_ip;
+local match = ip.match;
+local parse_cidr = ip.parse_cidr;
+local commonPrefixLength = ip.commonPrefixLength;
+
+describe("util.ip", function()
+ describe("#match()", function()
+ it("should work", function()
+ local _ = new_ip;
+ local ip = _"10.20.30.40";
+ assert.are.equal(match(ip, _"10.0.0.0", 8), true);
+ assert.are.equal(match(ip, _"10.0.0.0", 16), false);
+ assert.are.equal(match(ip, _"10.0.0.0", 24), false);
+ assert.are.equal(match(ip, _"10.0.0.0", 32), false);
+
+ assert.are.equal(match(ip, _"10.20.0.0", 8), true);
+ assert.are.equal(match(ip, _"10.20.0.0", 16), true);
+ assert.are.equal(match(ip, _"10.20.0.0", 24), false);
+ assert.are.equal(match(ip, _"10.20.0.0", 32), false);
+
+ assert.are.equal(match(ip, _"0.0.0.0", 32), false);
+ assert.are.equal(match(ip, _"0.0.0.0", 0), true);
+ assert.are.equal(match(ip, _"0.0.0.0"), false);
+
+ assert.are.equal(match(ip, _"10.0.0.0", 255), false, "excessive number of bits");
+ assert.are.equal(match(ip, _"10.0.0.0", -8), true, "negative number of bits");
+ assert.are.equal(match(ip, _"10.0.0.0", -32), true, "negative number of bits");
+ assert.are.equal(match(ip, _"10.0.0.0", 0), true, "zero bits");
+ assert.are.equal(match(ip, _"10.0.0.0"), false, "no specified number of bits (differing ip)");
+ assert.are.equal(match(ip, _"10.20.30.40"), true, "no specified number of bits (same ip)");
+
+ assert.are.equal(match(_"127.0.0.1", _"127.0.0.1"), true, "simple ip");
+
+ assert.are.equal(match(_"8.8.8.8", _"8.8.0.0", 16), true);
+ assert.are.equal(match(_"8.8.4.4", _"8.8.0.0", 16), true);
+ end);
+ end);
+
+ describe("#parse_cidr()", function()
+ it("should work", function()
+ assert.are.equal(new_ip"0.0.0.0", new_ip"0.0.0.0")
+
+ local function assert_cidr(cidr, ip, bits)
+ local parsed_ip, parsed_bits = parse_cidr(cidr);
+ assert.are.equal(new_ip(ip), parsed_ip, cidr.." parsed ip is "..ip);
+ assert.are.equal(bits, parsed_bits, cidr.." parsed bits is "..tostring(bits));
+ end
+ assert_cidr("0.0.0.0", "0.0.0.0", nil);
+ assert_cidr("127.0.0.1", "127.0.0.1", nil);
+ assert_cidr("127.0.0.1/0", "127.0.0.1", 0);
+ assert_cidr("127.0.0.1/8", "127.0.0.1", 8);
+ assert_cidr("127.0.0.1/32", "127.0.0.1", 32);
+ assert_cidr("127.0.0.1/256", "127.0.0.1", 256);
+ assert_cidr("::/48", "::", 48);
+ end);
+ end);
+
+ describe("#new_ip()", function()
+ it("should work", function()
+ local v4, v6 = "IPv4", "IPv6";
+ local function assert_proto(s, proto)
+ local ip = new_ip(s);
+ if proto then
+ assert.are.equal(ip and ip.proto, proto, "protocol is correct for "..("%q"):format(s));
+ else
+ assert.are.equal(ip, nil, "address is invalid");
+ end
+ end
+ assert_proto("127.0.0.1", v4);
+ assert_proto("::1", v6);
+ assert_proto("", nil);
+ assert_proto("abc", nil);
+ assert_proto(" ", nil);
+ end);
+ end);
+
+ describe("#commonPrefixLength()", function()
+ it("should work", function()
+ local function assert_cpl6(a, b, len, v4)
+ local ipa, ipb = new_ip(a), new_ip(b);
+ if v4 then len = len+96; end
+ assert.are.equal(commonPrefixLength(ipa, ipb), len, "common prefix length of "..a.." and "..b.." is "..len);
+ assert.are.equal(commonPrefixLength(ipb, ipa), len, "common prefix length of "..b.." and "..a.." is "..len);
+ end
+ local function assert_cpl4(a, b, len)
+ return assert_cpl6(a, b, len, "IPv4");
+ end
+ assert_cpl4("0.0.0.0", "0.0.0.0", 32);
+ assert_cpl4("255.255.255.255", "0.0.0.0", 0);
+ assert_cpl4("255.255.255.255", "255.255.0.0", 16);
+ assert_cpl4("255.255.255.255", "255.255.255.255", 32);
+ assert_cpl4("255.255.255.255", "255.255.255.255", 32);
+
+ assert_cpl6("::1", "::1", 128);
+ assert_cpl6("abcd::1", "abcd::1", 128);
+ assert_cpl6("abcd::abcd", "abcd::", 112);
+ assert_cpl6("abcd::abcd", "abcd::abcd:abcd", 96);
+ end);
+ end);
+end);
diff --git a/spec/util_iterators_spec.lua b/spec/util_iterators_spec.lua
new file mode 100644
index 00000000..d00058f4
--- /dev/null
+++ b/spec/util_iterators_spec.lua
@@ -0,0 +1,14 @@
+local iter = require "util.iterators";
+
+describe("util.iterators", function ()
+ describe("join", function ()
+ it("should produce a joined iterator", function ()
+ local expect = { "a", "b", "c", 1, 2, 3 };
+ local output = {};
+ for x in iter.join(iter.values({"a", "b", "c"})):append(iter.values({1, 2, 3})) do
+ table.insert(output, x);
+ end
+ assert.same(output, expect);
+ end);
+ end);
+end);
diff --git a/spec/util_jid_spec.lua b/spec/util_jid_spec.lua
new file mode 100644
index 00000000..c075212f
--- /dev/null
+++ b/spec/util_jid_spec.lua
@@ -0,0 +1,146 @@
+
+local jid = require "util.jid";
+
+describe("util.jid", function()
+ describe("#join()", function()
+ it("should work", function()
+ assert.are.equal(jid.join("a", "b", "c"), "a@b/c", "builds full JID");
+ assert.are.equal(jid.join("a", "b", nil), "a@b", "builds bare JID");
+ assert.are.equal(jid.join(nil, "b", "c"), "b/c", "builds full host JID");
+ assert.are.equal(jid.join(nil, "b", nil), "b", "builds bare host JID");
+ assert.are.equal(jid.join(nil, nil, nil), nil, "invalid JID is nil");
+ assert.are.equal(jid.join("a", nil, nil), nil, "invalid JID is nil");
+ assert.are.equal(jid.join(nil, nil, "c"), nil, "invalid JID is nil");
+ assert.are.equal(jid.join("a", nil, "c"), nil, "invalid JID is nil");
+ end);
+ end);
+ describe("#split()", function()
+ it("should work", function()
+ local function test(input_jid, expected_node, expected_server, expected_resource)
+ local rnode, rserver, rresource = jid.split(input_jid);
+ assert.are.equal(expected_node, rnode, "split("..tostring(input_jid)..") failed");
+ assert.are.equal(expected_server, rserver, "split("..tostring(input_jid)..") failed");
+ assert.are.equal(expected_resource, rresource, "split("..tostring(input_jid)..") failed");
+ end
+
+ -- Valid JIDs
+ test("node@server", "node", "server", nil );
+ test("node@server/resource", "node", "server", "resource" );
+ test("server", nil, "server", nil );
+ test("server/resource", nil, "server", "resource" );
+ test("server/resource@foo", nil, "server", "resource@foo" );
+ test("server/resource@foo/bar", nil, "server", "resource@foo/bar");
+
+ -- Always invalid JIDs
+ test(nil, nil, nil, nil);
+ test("node@/server", nil, nil, nil);
+ test("@server", nil, nil, nil);
+ test("@server/resource", nil, nil, nil);
+ test("@/resource", nil, nil, nil);
+ end);
+ end);
+
+
+ describe("#bare()", function()
+ it("should work", function()
+ assert.are.equal(jid.bare("user@host"), "user@host", "bare JID remains bare");
+ assert.are.equal(jid.bare("host"), "host", "Host JID remains host");
+ assert.are.equal(jid.bare("host/resource"), "host", "Host JID with resource becomes host");
+ assert.are.equal(jid.bare("user@host/resource"), "user@host", "user@host JID with resource becomes user@host");
+ assert.are.equal(jid.bare("user@/resource"), nil, "invalid JID is nil");
+ assert.are.equal(jid.bare("@/resource"), nil, "invalid JID is nil");
+ assert.are.equal(jid.bare("@/"), nil, "invalid JID is nil");
+ assert.are.equal(jid.bare("/"), nil, "invalid JID is nil");
+ assert.are.equal(jid.bare(""), nil, "invalid JID is nil");
+ assert.are.equal(jid.bare("@"), nil, "invalid JID is nil");
+ assert.are.equal(jid.bare("user@"), nil, "invalid JID is nil");
+ assert.are.equal(jid.bare("user@@"), nil, "invalid JID is nil");
+ assert.are.equal(jid.bare("user@@host"), nil, "invalid JID is nil");
+ assert.are.equal(jid.bare("user@@host/resource"), nil, "invalid JID is nil");
+ assert.are.equal(jid.bare("user@host/"), nil, "invalid JID is nil");
+ end);
+ end);
+
+ describe("#compare()", function()
+ it("should work", function()
+ assert.are.equal(jid.compare("host", "host"), true, "host should match");
+ assert.are.equal(jid.compare("host", "other-host"), false, "host should not match");
+ assert.are.equal(jid.compare("other-user@host/resource", "host"), true, "host should match");
+ assert.are.equal(jid.compare("other-user@host", "user@host"), false, "user should not match");
+ assert.are.equal(jid.compare("user@host", "host"), true, "host should match");
+ assert.are.equal(jid.compare("user@host/resource", "host"), true, "host should match");
+ assert.are.equal(jid.compare("user@host/resource", "user@host"), true, "user and host should match");
+ assert.are.equal(jid.compare("user@other-host", "host"), false, "host should not match");
+ assert.are.equal(jid.compare("user@other-host", "user@host"), false, "host should not match");
+ 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));
+ end
+
+ test("example.com", nil);
+ test("foo.example.com", nil);
+ test("foo.example.com/resource", nil);
+ test("foo.example.com/some resource", nil);
+ test("foo.example.com/some@resource", nil);
+
+ test("foo@foo.example.com/some@resource", "foo");
+ test("foo@example/some@resource", "foo");
+
+ test("foo@example/@resource", "foo");
+ test("foo@example@resource", nil);
+ test("foo@example", "foo");
+ test("foo", nil);
+
+ test(nil, nil);
+ end);
+
+ it("should work with hosts", function()
+ local function test(_jid, expected_host)
+ assert.are.equal(jid.host(_jid), expected_host, "Unexpected host for "..tostring(_jid));
+ end
+
+ test("example.com", "example.com");
+ test("foo.example.com", "foo.example.com");
+ test("foo.example.com/resource", "foo.example.com");
+ test("foo.example.com/some resource", "foo.example.com");
+ test("foo.example.com/some@resource", "foo.example.com");
+
+ test("foo@foo.example.com/some@resource", "foo.example.com");
+ test("foo@example/some@resource", "example");
+
+ test("foo@example/@resource", "example");
+ test("foo@example@resource", nil);
+ test("foo@example", "example");
+ test("foo", "foo");
+
+ test(nil, nil);
+ end);
+
+ it("should work with resources", function()
+ local function test(_jid, expected_resource)
+ assert.are.equal(jid.resource(_jid), expected_resource, "Unexpected resource for "..tostring(_jid));
+ end
+
+ test("example.com", nil);
+ test("foo.example.com", nil);
+ test("foo.example.com/resource", "resource");
+ test("foo.example.com/some resource", "some resource");
+ test("foo.example.com/some@resource", "some@resource");
+
+ test("foo@foo.example.com/some@resource", "some@resource");
+ test("foo@example/some@resource", "some@resource");
+
+ test("foo@example/@resource", "@resource");
+ test("foo@example@resource", nil);
+ test("foo@example", nil);
+ test("foo", nil);
+ test("/foo", nil);
+ test("@x/foo", nil);
+ test("@/foo", nil);
+
+ test(nil, nil);
+ end);
+end);
diff --git a/spec/util_json_spec.lua b/spec/util_json_spec.lua
new file mode 100644
index 00000000..dc66c7ba
--- /dev/null
+++ b/spec/util_json_spec.lua
@@ -0,0 +1,70 @@
+
+local json = require "util.json";
+
+describe("util.json", function()
+ describe("#encode()", function()
+ it("should work", function()
+ local function test(f, j, e)
+ if e then
+ assert.are.equal(f(j), e);
+ end
+ assert.are.equal(f(j), f(json.decode(f(j))));
+ end
+ test(json.encode, json.null, "null")
+ test(json.encode, {}, "{}")
+ test(json.encode, {a=1});
+ test(json.encode, {a={1,2,3}});
+ test(json.encode, {1}, "[1]");
+ end);
+ end);
+
+ describe("#decode()", function()
+ it("should work", function()
+ local empty_array = json.decode("[]");
+ assert.are.equal(type(empty_array), "table");
+ assert.are.equal(#empty_array, 0);
+ assert.are.equal(next(empty_array), nil);
+ end);
+ end);
+
+ describe("testcases", function()
+
+ local valid_data = {};
+ local invalid_data = {};
+
+ local skip = "fail1.json fail9.json fail18.json fail15.json fail13.json fail25.json fail26.json fail27.json fail28.json fail17.json pass1.json";
+
+ setup(function()
+ local lfs = require "lfs";
+ local path = "spec/json";
+ for name in lfs.dir(path) do
+ if name:match("%.json$") then
+ local f = assert(io.open(path.."/"..name));
+ local content = assert(f:read("*a"));
+ assert(f:close());
+ if skip:find(name) then
+ -- Skip
+ elseif name:match("^pass") then
+ valid_data[name] = content;
+ elseif name:match("^fail") then
+ invalid_data[name] = content;
+ end
+ end
+ end
+ end)
+
+ it("should pass valid testcases", function()
+ for name, content in pairs(valid_data) do
+ local parsed, err = json.decode(content);
+ assert(parsed, name..": "..tostring(err));
+ end
+ end);
+
+ it("should fail invalid testcases", function()
+ for name, content in pairs(invalid_data) do
+ local parsed, err = json.decode(content);
+ assert(not parsed, name..": "..tostring(err));
+ end
+ end);
+ end)
+end);
diff --git a/spec/util_multitable_spec.lua b/spec/util_multitable_spec.lua
new file mode 100644
index 00000000..40759f7a
--- /dev/null
+++ b/spec/util_multitable_spec.lua
@@ -0,0 +1,60 @@
+
+local multitable = require "util.multitable";
+
+describe("util.multitable", function()
+ describe("#new()", function()
+ it("should create a multitable", function()
+ local mt = multitable.new();
+ assert.is_table(mt, "Multitable is a table");
+ assert.is_function(mt.add, "Multitable has method add");
+ assert.is_function(mt.get, "Multitable has method get");
+ assert.is_function(mt.remove, "Multitable has method remove");
+ end);
+ end);
+
+ describe("#get()", function()
+ it("should allow getting correctly", function()
+ local function has_items(list, ...)
+ local should_have = {};
+ if select('#', ...) > 0 then
+ assert.is_table(list, "has_items: list is table", 3);
+ else
+ assert.is.falsy(list and #list > 0, "No items, and no list");
+ return true, "has-all";
+ end
+ for n=1,select('#', ...) do should_have[select(n, ...)] = true; end
+ for _, item in ipairs(list) do
+ if not should_have[item] then return false, "too-many"; end
+ should_have[item] = nil;
+ end
+ if next(should_have) then
+ return false, "not-enough";
+ end
+ return true, "has-all";
+ end
+ local function assert_has_all(message, list, ...)
+ return assert.are.equal(select(2, has_items(list, ...)), "has-all", message or "List has all expected items, and no more", 2);
+ end
+
+ local mt = multitable.new();
+
+ local trigger1, trigger2, trigger3 = {}, {}, {};
+ local item1, item2, item3 = {}, {}, {};
+
+ assert_has_all("Has no items with trigger1", mt:get(trigger1));
+
+
+ mt:add(1, 2, 3, item1);
+
+ assert_has_all("Has item1 for 1, 2, 3", mt:get(1, 2, 3), item1);
+ end);
+ end);
+
+ -- Doesn't support nil
+ --[[ mt:add(nil, item1);
+ mt:add(nil, item2);
+ mt:add(nil, item3);
+
+ assert_has_all("Has all items with (nil)", mt:get(nil), item1, item2, item3);
+ ]]
+end);
diff --git a/spec/util_pubsub_spec.lua b/spec/util_pubsub_spec.lua
new file mode 100644
index 00000000..3f463ef2
--- /dev/null
+++ b/spec/util_pubsub_spec.lua
@@ -0,0 +1,342 @@
+local pubsub;
+setup(function ()
+ pubsub = require "util.pubsub";
+end);
+
+--[[TODO:
+ Retract
+ Purge
+ auto-create/auto-subscribe
+ Item store/node store
+ resize on max_items change
+ service creation config provides alternative node_defaults
+ get subscriptions
+]]
+
+describe("util.pubsub", function ()
+ describe("simple node creation and deletion", function ()
+ -- Roughly a port of scansion/scripts/pubsub_createdelete.scs
+ local service = pubsub.new();
+
+ describe("#create", function ()
+ it("creates a new node", function ()
+ assert.truthy(service:create("princely_musings", true));
+ end);
+
+ it("fails to create the same node again", function ()
+ assert.falsy(service:create("princely_musings", true));
+ end);
+ end);
+
+ describe("#delete", function ()
+ it("deletes the node", function ()
+ assert.truthy(service:delete("princely_musings", true));
+ end);
+
+ it("can't delete an already deleted node", function ()
+ assert.falsy(service:delete("princely_musings", true));
+ end);
+ end);
+ end);
+
+ describe("simple publishing", function ()
+ local notified;
+ local broadcaster = spy.new(function (notif_type, node_name, subscribers, item) -- luacheck: ignore 212
+ notified = subscribers;
+ end);
+ local service = pubsub.new({
+ broadcaster = broadcaster;
+ });
+
+ it("creates a node", function ()
+ assert.truthy(service:create("node", true));
+ end);
+
+ it("lets someone subscribe", function ()
+ assert.truthy(service:add_subscription("node", true, "someone"));
+ end);
+
+ it("publishes an item", function ()
+ assert.truthy(service:publish("node", true, "1", "item 1"));
+ assert.truthy(notified["someone"]);
+ end);
+
+ it("called the broadcaster", function ()
+ assert.spy(broadcaster).was_called();
+ end);
+
+ it("should return one item", function ()
+ local ok, ret = service:get_items("node", true);
+ assert.truthy(ok);
+ assert.same({ "1", ["1"] = "item 1" }, ret);
+ end);
+
+ it("lets someone unsubscribe", function ()
+ assert.truthy(service:remove_subscription("node", true, "someone"));
+ end);
+
+ it("does not send notifications after subscription is removed", function ()
+ assert.truthy(service:publish("node", true, "1", "item 1"));
+ assert.is_nil(notified["someone"]);
+ end);
+ end);
+
+ describe("#issue1082", function ()
+ local service = pubsub.new();
+
+ it("creates a node with max_items = 1", function ()
+ assert.truthy(service:create("node", true, { max_items = 1 }));
+ end);
+
+ it("changes max_items to 2", function ()
+ assert.truthy(service:set_node_config("node", true, { max_items = 2 }));
+ end);
+
+ it("publishes one item", function ()
+ assert.truthy(service:publish("node", true, "1", "item 1"));
+ end);
+
+ it("should return one item", function ()
+ local ok, ret = service:get_items("node", true);
+ assert.truthy(ok);
+ assert.same({ "1", ["1"] = "item 1" }, ret);
+ end);
+
+ it("publishes another item", function ()
+ assert.truthy(service:publish("node", true, "2", "item 2"));
+ end);
+
+ it("should return two items", function ()
+ local ok, ret = service:get_items("node", true);
+ assert.truthy(ok);
+ assert.same({
+ "2",
+ "1",
+ ["1"] = "item 1",
+ ["2"] = "item 2",
+ }, ret);
+ end);
+
+ it("publishes yet another item", function ()
+ assert.truthy(service:publish("node", true, "3", "item 3"));
+ end);
+
+ it("should still return only two items", function ()
+ local ok, ret = service:get_items("node", true);
+ assert.truthy(ok);
+ assert.same({
+ "3",
+ "2",
+ ["2"] = "item 2",
+ ["3"] = "item 3",
+ }, ret);
+ end);
+
+ end);
+
+ describe("node config", function ()
+ local service;
+ before_each(function ()
+ service = pubsub.new();
+ service:create("test", true);
+ end);
+ it("access is forbidden for unaffiliated entities", function ()
+ local ok, err = service:get_node_config("test", "stranger");
+ assert.is_falsy(ok);
+ assert.equals("forbidden", err);
+ end);
+ it("returns an error for nodes that do not exist", function ()
+ local ok, err = service:get_node_config("nonexistent", true);
+ assert.is_falsy(ok);
+ assert.equals("item-not-found", err);
+ end);
+ end);
+
+ describe("access model", function ()
+ describe("open", function ()
+ local service;
+ before_each(function ()
+ service = pubsub.new();
+ -- Do not supply any config, 'open' should be default
+ service:create("test", true);
+ end);
+ it("should be the default", function ()
+ local ok, config = service:get_node_config("test", true);
+ assert.equal("open", config.access_model);
+ end);
+ it("should allow anyone to subscribe", function ()
+ local ok = service:add_subscription("test", "stranger", "stranger");
+ assert.is_true(ok);
+ end);
+ it("should still reject outcast-affiliated entities", function ()
+ assert(service:set_affiliation("test", true, "enemy", "outcast"));
+ local ok, err = service:add_subscription("test", "enemy", "enemy");
+ assert.is_falsy(ok);
+ assert.equal("forbidden", err);
+ end);
+ end);
+ describe("whitelist", function ()
+ local service;
+ before_each(function ()
+ service = assert(pubsub.new());
+ assert.is_true(service:create("test", true, { access_model = "whitelist" }));
+ end);
+ it("should be present in the configuration", function ()
+ local ok, config = service:get_node_config("test", true);
+ assert.equal("whitelist", config.access_model);
+ end);
+ it("should not allow anyone to subscribe", function ()
+ local ok, err = service:add_subscription("test", "stranger", "stranger");
+ assert.is_false(ok);
+ assert.equals("forbidden", err);
+ end);
+ end);
+ describe("change", function ()
+ local service;
+ before_each(function ()
+ service = pubsub.new();
+ service:create("test", true, { access_model = "open" });
+ end);
+ it("affects existing subscriptions", function ()
+ do
+ local ok = service:add_subscription("test", "stranger", "stranger");
+ assert.is_true(ok);
+ end
+ do
+ local ok, sub = service:get_subscription("test", "stranger", "stranger");
+ assert.is_true(ok);
+ assert.is_true(sub);
+ end
+ assert(service:set_node_config("test", true, { access_model = "whitelist" }));
+ do
+ local ok, sub = service:get_subscription("test", "stranger", "stranger");
+ assert.is_true(ok);
+ assert.is_nil(sub);
+ end
+ end);
+ end);
+ end);
+
+ describe("publish model", function ()
+ describe("publishers", function ()
+ local service;
+ before_each(function ()
+ service = pubsub.new();
+ -- Do not supply any config, 'publishers' should be default
+ service:create("test", true);
+ end);
+ it("should be the default", function ()
+ local ok, config = service:get_node_config("test", true);
+ assert.equal("publishers", config.publish_model);
+ end);
+ it("should not allow anyone to publish", function ()
+ assert.is_true(service:add_subscription("test", "stranger", "stranger"));
+ local ok, err = service:publish("test", "stranger", "item1", "foo");
+ assert.is_falsy(ok);
+ assert.equals("forbidden", err);
+ end);
+ it("should allow publishers to publish", function ()
+ assert(service:set_affiliation("test", true, "mypublisher", "publisher"));
+ local ok, err = service:publish("test", "mypublisher", "item1", "foo");
+ assert.is_true(ok);
+ end);
+ it("should allow owners to publish", function ()
+ assert(service:set_affiliation("test", true, "myowner", "owner"));
+ local ok = service:publish("test", "myowner", "item1", "foo");
+ assert.is_true(ok);
+ end);
+ end);
+ describe("open", function ()
+ local service;
+ before_each(function ()
+ service = pubsub.new();
+ service:create("test", true, { publish_model = "open" });
+ end);
+ it("should allow anyone to publish", function ()
+ local ok = service:publish("test", "stranger", "item1", "foo");
+ assert.is_true(ok);
+ end);
+ end);
+ describe("subscribers", function ()
+ local service;
+ before_each(function ()
+ service = pubsub.new();
+ service:create("test", true, { publish_model = "subscribers" });
+ end);
+ it("should not allow non-subscribers to publish", function ()
+ local ok, err = service:publish("test", "stranger", "item1", "foo");
+ assert.is_falsy(ok);
+ assert.equals("forbidden", err);
+ end);
+ it("should allow subscribers to publish without an affiliation", function ()
+ assert.is_true(service:add_subscription("test", "stranger", "stranger"));
+ local ok = service:publish("test", "stranger", "item1", "foo");
+ assert.is_true(ok);
+ end);
+ it("should allow publishers to publish without a subscription", function ()
+ assert(service:set_affiliation("test", true, "mypublisher", "publisher"));
+ local ok, err = service:publish("test", "mypublisher", "item1", "foo");
+ assert.is_true(ok);
+ end);
+ it("should allow owners to publish without a subscription", function ()
+ assert(service:set_affiliation("test", true, "myowner", "owner"));
+ local ok = service:publish("test", "myowner", "item1", "foo");
+ assert.is_true(ok);
+ end);
+ end);
+ end);
+
+ describe("item API", function ()
+ local service;
+ before_each(function ()
+ service = pubsub.new();
+ service:create("test", true, { publish_model = "subscribers" });
+ end);
+ describe("get_last_item()", function ()
+ it("succeeds with nil on empty nodes", function ()
+ local ok, id, item = service:get_last_item("test", true);
+ assert.is_true(ok);
+ assert.is_nil(id);
+ assert.is_nil(item);
+ end);
+ it("succeeds and returns the last item", function ()
+ service:publish("test", true, "one", "hello world");
+ service:publish("test", true, "two", "hello again");
+ service:publish("test", true, "three", "hey");
+ service:publish("test", true, "one", "bye");
+ local ok, id, item = service:get_last_item("test", true);
+ assert.is_true(ok);
+ assert.equal("one", id);
+ assert.equal("bye", item);
+ end);
+ end);
+ describe("get_items()", function ()
+ it("fails on non-existent nodes", function ()
+ local ok, err = service:get_items("no-node", true);
+ assert.is_falsy(ok);
+ assert.equal("item-not-found", err);
+ end);
+ it("returns no items on an empty node", function ()
+ local ok, items = service:get_items("test", true);
+ assert.is_true(ok);
+ assert.equal(0, #items);
+ assert.is_nil(next(items));
+ end);
+ it("returns no items on an empty node", function ()
+ local ok, items = service:get_items("test", true);
+ assert.is_true(ok);
+ assert.equal(0, #items);
+ assert.is_nil((next(items)));
+ end);
+ it("returns all published items", function ()
+ service:publish("test", true, "one", "hello world");
+ service:publish("test", true, "two", "hello again");
+ service:publish("test", true, "three", "hey");
+ service:publish("test", true, "one", "bye");
+ local ok, items = service:get_items("test", true);
+ assert.is_true(ok);
+ assert.same({ "one", "three", "two", two = "hello again", three = "hey", one = "bye" }, items);
+ end);
+ end);
+ end);
+end);
diff --git a/spec/util_queue_spec.lua b/spec/util_queue_spec.lua
new file mode 100644
index 00000000..7cd3d695
--- /dev/null
+++ b/spec/util_queue_spec.lua
@@ -0,0 +1,103 @@
+
+local queue = require "util.queue";
+
+describe("util.queue", function()
+ describe("#new()", function()
+ it("should work", function()
+
+ do
+ local q = queue.new(10);
+
+ assert.are.equal(q.size, 10);
+ assert.are.equal(q:count(), 0);
+
+ assert.is_true(q:push("one"));
+ assert.is_true(q:push("two"));
+ assert.is_true(q:push("three"));
+
+ for i = 4, 10 do
+ assert.is_true(q:push("hello"));
+ assert.are.equal(q:count(), i, "count is not "..i.."("..q:count()..")");
+ end
+ assert.are.equal(q:push("hello"), nil, "queue overfull!");
+ assert.are.equal(q:push("hello"), nil, "queue overfull!");
+ assert.are.equal(q:pop(), "one", "queue item incorrect");
+ assert.are.equal(q:pop(), "two", "queue item incorrect");
+ assert.is_true(q:push("hello"));
+ assert.is_true(q:push("hello"));
+ assert.are.equal(q:pop(), "three", "queue item incorrect");
+ assert.is_true(q:push("hello"));
+ assert.are.equal(q:push("hello"), nil, "queue overfull!");
+ assert.are.equal(q:push("hello"), nil, "queue overfull!");
+
+ assert.are.equal(q:count(), 10, "queue count incorrect");
+
+ for _ = 1, 10 do
+ assert.are.equal(q:pop(), "hello", "queue item incorrect");
+ end
+
+ assert.are.equal(q:count(), 0, "queue count incorrect");
+ assert.are.equal(q:pop(), nil, "empty queue pops non-nil result");
+ assert.are.equal(q:count(), 0, "popping empty queue affects count");
+
+ assert.are.equal(q:peek(), nil, "empty queue peeks non-nil result");
+ assert.are.equal(q:count(), 0, "peeking empty queue affects count");
+
+ assert.is_true(q:push(1));
+ for i = 1, 1001 do
+ assert.are.equal(q:pop(), i);
+ assert.are.equal(q:count(), 0);
+ assert.is_true(q:push(i+1));
+ assert.are.equal(q:count(), 1);
+ end
+ assert.are.equal(q:pop(), 1002);
+ assert.is_true(q:push(1));
+ for i = 1, 1000 do
+ assert.are.equal(q:pop(), i);
+ assert.is_true(q:push(i+1));
+ end
+ assert.are.equal(q:pop(), 1001);
+ assert.are.equal(q:count(), 0);
+ end
+
+ do
+ -- Test queues that purge old items when pushing to a full queue
+ local q = queue.new(10, true);
+
+ for i = 1, 10 do
+ q:push(i);
+ end
+
+ assert.are.equal(q:count(), 10);
+
+ assert.is_true(q:push(11));
+ assert.are.equal(q:count(), 10);
+ assert.are.equal(q:pop(), 2); -- First item should have been purged
+ assert.are.equal(q:peek(), 3);
+
+ for i = 12, 32 do
+ assert.is_true(q:push(i));
+ end
+
+ assert.are.equal(q:count(), 10);
+ assert.are.equal(q:pop(), 23);
+ end
+
+ do
+ -- Test iterator
+ local q = queue.new(10, true);
+
+ for i = 1, 10 do
+ q:push(i);
+ end
+
+ local i = 0;
+ for item in q:items() do
+ i = i + 1;
+ assert.are.equal(item, i, "unexpected item returned by iterator")
+ end
+ end
+
+ end);
+ end);
+end);
diff --git a/spec/util_random_spec.lua b/spec/util_random_spec.lua
new file mode 100644
index 00000000..c080a2c9
--- /dev/null
+++ b/spec/util_random_spec.lua
@@ -0,0 +1,19 @@
+
+local random = require "util.random";
+
+describe("util.random", function()
+ describe("#bytes()", function()
+ it("should return a string", function()
+ assert.is_string(random.bytes(16));
+ end);
+
+ it("should return the requested number of bytes", function()
+ -- Makes no attempt at testing how random the bytes are,
+ -- just that it returns the number of bytes requested
+
+ for i = 1, 20 do
+ assert.are.equal(2^i, #random.bytes(2^i));
+ end
+ end);
+ end);
+end);
diff --git a/spec/util_rfc6724_spec.lua b/spec/util_rfc6724_spec.lua
new file mode 100644
index 00000000..30e935b6
--- /dev/null
+++ b/spec/util_rfc6724_spec.lua
@@ -0,0 +1,97 @@
+
+local rfc6724 = require "util.rfc6724";
+local new_ip = require"util.ip".new_ip;
+
+describe("util.rfc6724", function()
+ describe("#source()", function()
+ it("should work", function()
+ assert.are.equal(rfc6724.source(new_ip("2001:db8:1::1", "IPv6"),
+ {new_ip("2001:db8:3::1", "IPv6"), new_ip("fe80::1", "IPv6")}).addr,
+ "2001:db8:3::1",
+ "prefer appropriate scope");
+ assert.are.equal(rfc6724.source(new_ip("ff05::1", "IPv6"),
+ {new_ip("2001:db8:3::1", "IPv6"), new_ip("fe80::1", "IPv6")}).addr,
+ "2001:db8:3::1",
+ "prefer appropriate scope");
+ assert.are.equal(rfc6724.source(new_ip("2001:db8:1::1", "IPv6"),
+ {new_ip("2001:db8:1::1", "IPv6"), new_ip("2001:db8:2::1", "IPv6")}).addr,
+ "2001:db8:1::1",
+ "prefer same address"); -- "2001:db8:1::1" should be marked "deprecated" here, we don't handle that right now
+ assert.are.equal(rfc6724.source(new_ip("fe80::1", "IPv6"),
+ {new_ip("fe80::2", "IPv6"), new_ip("2001:db8:1::1", "IPv6")}).addr,
+ "fe80::2",
+ "prefer appropriate scope"); -- "fe80::2" should be marked "deprecated" here, we don't handle that right now
+ assert.are.equal(rfc6724.source(new_ip("2001:db8:1::1", "IPv6"),
+ {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3::2", "IPv6")}).addr,
+ "2001:db8:1::2",
+ "longest matching prefix");
+ --[[ "2001:db8:1::2" should be a care-of address and "2001:db8:3::2" a home address, we can't handle this and would fail
+ assert.are.equal(rfc6724.source(new_ip("2001:db8:1::1", "IPv6"),
+ {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3::2", "IPv6")}).addr,
+ "2001:db8:3::2",
+ "prefer home address");
+ ]]
+ assert.are.equal(rfc6724.source(new_ip("2002:c633:6401::1", "IPv6"),
+ {new_ip("2002:c633:6401::d5e3:7953:13eb:22e8", "IPv6"), new_ip("2001:db8:1::2", "IPv6")}).addr,
+ "2002:c633:6401::d5e3:7953:13eb:22e8",
+ "prefer matching label"); -- "2002:c633:6401::d5e3:7953:13eb:22e8" should be marked "temporary" here, we don't handle that right now
+ assert.are.equal(rfc6724.source(new_ip("2001:db8:1::d5e3:0:0:1", "IPv6"),
+ {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:1::d5e3:7953:13eb:22e8", "IPv6")}).addr,
+ "2001:db8:1::d5e3:7953:13eb:22e8",
+ "prefer temporary address") -- "2001:db8:1::2" should be marked "public" and "2001:db8:1::d5e3:7953:13eb:22e8" should be marked "temporary" here, we don't handle that right now
+ end);
+ end);
+ describe("#destination()", function()
+ it("should work", function()
+ local order;
+ order = rfc6724.destination({new_ip("2001:db8:1::1", "IPv6"), new_ip("198.51.100.121", "IPv4")},
+ {new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::1", "IPv6"), new_ip("169.254.13.78", "IPv4")})
+ assert.are.equal(order[1].addr, "2001:db8:1::1", "prefer matching scope");
+ assert.are.equal(order[2].addr, "198.51.100.121", "prefer matching scope");
+
+ order = rfc6724.destination({new_ip("2001:db8:1::1", "IPv6"), new_ip("198.51.100.121", "IPv4")},
+ {new_ip("fe80::1", "IPv6"), new_ip("198.51.100.117", "IPv4")})
+ assert.are.equal(order[1].addr, "198.51.100.121", "prefer matching scope");
+ assert.are.equal(order[2].addr, "2001:db8:1::1", "prefer matching scope");
+
+ order = rfc6724.destination({new_ip("2001:db8:1::1", "IPv6"), new_ip("10.1.2.3", "IPv4")},
+ {new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::1", "IPv6"), new_ip("10.1.2.4", "IPv4")})
+ assert.are.equal(order[1].addr, "2001:db8:1::1", "prefer higher precedence");
+ assert.are.equal(order[2].addr, "10.1.2.3", "prefer higher precedence");
+
+ order = rfc6724.destination({new_ip("2001:db8:1::1", "IPv6"), new_ip("fe80::1", "IPv6")},
+ {new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::2", "IPv6")})
+ assert.are.equal(order[1].addr, "fe80::1", "prefer smaller scope");
+ assert.are.equal(order[2].addr, "2001:db8:1::1", "prefer smaller scope");
+
+ --[[ "2001:db8:1::2" and "fe80::2" should be marked "care-of address", while "2001:db8:3::1" should be marked "home address", we can't currently handle this and would fail the test
+ order = rfc6724.destination({new_ip("2001:db8:1::1", "IPv6"), new_ip("fe80::1", "IPv6")},
+ {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3::1", "IPv6"), new_ip("fe80::2", "IPv6")})
+ assert.are.equal(order[1].addr, "2001:db8:1::1", "prefer home address");
+ assert.are.equal(order[2].addr, "fe80::1", "prefer home address");
+ ]]
+
+ --[[ "fe80::2" should be marked "deprecated", we can't currently handle this and would fail the test
+ order = rfc6724.destination({new_ip("2001:db8:1::1", "IPv6"), new_ip("fe80::1", "IPv6")},
+ {new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::2", "IPv6")})
+ assert.are.equal(order[1].addr, "2001:db8:1::1", "avoid deprecated addresses");
+ assert.are.equal(order[2].addr, "fe80::1", "avoid deprecated addresses");
+ ]]
+
+ order = rfc6724.destination({new_ip("2001:db8:1::1", "IPv6"), new_ip("2001:db8:3ffe::1", "IPv6")},
+ {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3f44::2", "IPv6"), new_ip("fe80::2", "IPv6")})
+ assert.are.equal(order[1].addr, "2001:db8:1::1", "longest matching prefix");
+ assert.are.equal(order[2].addr, "2001:db8:3ffe::1", "longest matching prefix");
+
+ order = rfc6724.destination({new_ip("2002:c633:6401::1", "IPv6"), new_ip("2001:db8:1::1", "IPv6")},
+ {new_ip("2002:c633:6401::2", "IPv6"), new_ip("fe80::2", "IPv6")})
+ assert.are.equal(order[1].addr, "2002:c633:6401::1", "prefer matching label");
+ assert.are.equal(order[2].addr, "2001:db8:1::1", "prefer matching label");
+
+ order = rfc6724.destination({new_ip("2002:c633:6401::1", "IPv6"), new_ip("2001:db8:1::1", "IPv6")},
+ {new_ip("2002:c633:6401::2", "IPv6"), new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::2", "IPv6")})
+ assert.are.equal(order[1].addr, "2001:db8:1::1", "prefer higher precedence");
+ assert.are.equal(order[2].addr, "2002:c633:6401::1", "prefer higher precedence");
+ end);
+ end);
+end);
diff --git a/spec/util_stanza_spec.lua b/spec/util_stanza_spec.lua
new file mode 100644
index 00000000..383beae3
--- /dev/null
+++ b/spec/util_stanza_spec.lua
@@ -0,0 +1,338 @@
+
+local st = require "util.stanza";
+
+describe("util.stanza", function()
+ describe("#preserialize()", function()
+ it("should work", function()
+ local stanza = st.stanza("message", { a = "a" });
+ local stanza2 = st.preserialize(stanza);
+ assert.is_string(stanza2 and stanza.name, "preserialize returns a stanza");
+ assert.is_nil(stanza2.tags, "Preserialized stanza has no tag list");
+ assert.is_nil(stanza2.last_add, "Preserialized stanza has no last_add marker");
+ assert.is_nil(getmetatable(stanza2), "Preserialized stanza has no metatable");
+ end);
+ end);
+
+ describe("#preserialize()", function()
+ it("should work", function()
+ local stanza = st.stanza("message", { a = "a" });
+ local stanza2 = st.deserialize(st.preserialize(stanza));
+ assert.is_string(stanza2 and stanza.name, "deserialize returns a stanza");
+ assert.is_table(stanza2.attr, "Deserialized stanza has attributes");
+ assert.are.equal(stanza2.attr.a, "a", "Deserialized stanza retains attributes");
+ assert.is_table(getmetatable(stanza2), "Deserialized stanza has metatable");
+ end);
+ end);
+
+ describe("#stanza()", function()
+ it("should work", function()
+ local s = st.stanza("foo", { xmlns = "myxmlns", a = "attr-a" });
+ assert.are.equal(s.name, "foo");
+ assert.are.equal(s.attr.xmlns, "myxmlns");
+ assert.are.equal(s.attr.a, "attr-a");
+
+ local s1 = st.stanza("s1");
+ assert.are.equal(s1.name, "s1");
+ assert.are.equal(s1.attr.xmlns, nil);
+ assert.are.equal(#s1, 0);
+ assert.are.equal(#s1.tags, 0);
+
+ s1:tag("child1");
+ assert.are.equal(#s1.tags, 1);
+ assert.are.equal(s1.tags[1].name, "child1");
+
+ s1:tag("grandchild1"):up();
+ assert.are.equal(#s1.tags, 1);
+ assert.are.equal(s1.tags[1].name, "child1");
+ assert.are.equal(#s1.tags[1], 1);
+ assert.are.equal(s1.tags[1][1].name, "grandchild1");
+
+ s1:up():tag("child2");
+ assert.are.equal(#s1.tags, 2, tostring(s1));
+ assert.are.equal(s1.tags[1].name, "child1");
+ assert.are.equal(s1.tags[2].name, "child2");
+ assert.are.equal(#s1.tags[1], 1);
+ assert.are.equal(s1.tags[1][1].name, "grandchild1");
+
+ s1:up():text("Hello world");
+ assert.are.equal(#s1.tags, 2);
+ assert.are.equal(#s1, 3);
+ assert.are.equal(s1.tags[1].name, "child1");
+ assert.are.equal(s1.tags[2].name, "child2");
+ assert.are.equal(#s1.tags[1], 1);
+ assert.are.equal(s1.tags[1][1].name, "grandchild1");
+ end);
+ it("should work with unicode values", function ()
+ local s = st.stanza("Объект", { xmlns = "myxmlns", ["Объект"] = "&" });
+ assert.are.equal(s.name, "Объект");
+ assert.are.equal(s.attr.xmlns, "myxmlns");
+ assert.are.equal(s.attr["Объект"], "&");
+ end);
+ it("should allow :text() with nil and empty strings", function ()
+ local s_control = st.stanza("foo");
+ assert.same(st.stanza("foo"):text(), s_control);
+ assert.same(st.stanza("foo"):text(nil), s_control);
+ assert.same(st.stanza("foo"):text(""), s_control);
+ end);
+ end);
+
+ describe("#message()", function()
+ it("should work", function()
+ local m = st.message();
+ assert.are.equal(m.name, "message");
+ end);
+ end);
+
+ describe("#iq()", function()
+ it("should work", function()
+ local i = st.iq();
+ assert.are.equal(i.name, "iq");
+ end);
+ end);
+
+ describe("#iq()", function()
+ it("should work", function()
+ local p = st.presence();
+ assert.are.equal(p.name, "presence");
+ end);
+ end);
+
+ describe("#reply()", function()
+ it("should work for <s>", function()
+ -- Test stanza
+ local s = st.stanza("s", { to = "touser", from = "fromuser", id = "123" })
+ :tag("child1");
+ -- Make reply stanza
+ local r = st.reply(s);
+ 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, 0, "A reply should not include children of the original stanza");
+ end);
+
+ it("should work for <iq get>", function()
+ -- Test stanza
+ local s = st.stanza("iq", { to = "touser", from = "fromuser", id = "123", type = "get" })
+ :tag("child1");
+ -- Make reply stanza
+ local r = st.reply(s);
+ 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, "result");
+ assert.are.equal(#r.tags, 0, "A reply should not include children of the original stanza");
+ end);
+
+ it("should work for <iq set>", function()
+ -- Test stanza
+ local s = st.stanza("iq", { to = "touser", from = "fromuser", id = "123", type = "set" })
+ :tag("child1");
+ -- Make reply stanza
+ local r = st.reply(s);
+ 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, "result");
+ assert.are.equal(#r.tags, 0, "A reply should not include children of the original stanza");
+ end);
+ end);
+
+ describe("#error_reply()", function()
+ it("should work for <s>", function()
+ -- Test stanza
+ 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");
+ 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");
+ end);
+
+ it("should work for <iq get>", function()
+ -- Test stanza
+ local s = st.stanza("iq", { to = "touser", from = "fromuser", id = "123", type = "get" })
+ :tag("child1");
+ -- Make reply stanza
+ local r = st.error_reply(s, "cancel", "service-unavailable");
+ 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);
+ assert.are.equal(r.tags[1].tags[1].name, "service-unavailable");
+ end);
+ end);
+
+ describe("should reject #invalid", function ()
+ local invalid_names = {
+ ["empty string"] = "", ["characters"] = "<>";
+ }
+ local invalid_data = {
+ ["number"] = 1234, ["table"] = {};
+ ["utf8"] = string.char(0xF4, 0x90, 0x80, 0x80);
+ ["nil"] = "nil"; ["boolean"] = true;
+ };
+
+ for value_type, value in pairs(invalid_names) do
+ it(value_type.." in tag names", function ()
+ assert.error_matches(function ()
+ st.stanza(value);
+ end, value_type);
+ end);
+ it(value_type.." in attribute names", function ()
+ assert.error_matches(function ()
+ st.stanza("valid", { [value] = "valid" });
+ end, value_type);
+ end);
+ end
+ for value_type, value in pairs(invalid_data) do
+ if value == "nil" then value = nil; end
+ it(value_type.." in tag names", function ()
+ assert.error_matches(function ()
+ st.stanza(value);
+ end, value_type);
+ end);
+ it(value_type.." in attribute names", function ()
+ assert.error_matches(function ()
+ st.stanza("valid", { [value] = "valid" });
+ end, value_type);
+ end);
+ if value ~= nil then
+ it(value_type.." in attribute values", function ()
+ assert.error_matches(function ()
+ st.stanza("valid", { valid = value });
+ end, value_type);
+ end);
+ it(value_type.." in text node", function ()
+ assert.error_matches(function ()
+ st.stanza("valid"):text(value);
+ end, value_type);
+ end);
+ end
+ end
+ end);
+
+ describe("#is_stanza", function ()
+ -- is_stanza(any) -> boolean
+ it("identifies stanzas as stanzas", function ()
+ assert.truthy(st.is_stanza(st.stanza("x")));
+ end);
+ it("identifies strings as not stanzas", function ()
+ assert.falsy(st.is_stanza(""));
+ end);
+ it("identifies numbers as not stanzas", function ()
+ assert.falsy(st.is_stanza(1));
+ end);
+ it("identifies tables as not stanzas", function ()
+ assert.falsy(st.is_stanza({}));
+ end);
+ end);
+
+ describe("#remove_children", function ()
+ it("should work", function ()
+ local s = st.stanza("x", {xmlns="test"})
+ :tag("y", {xmlns="test"}):up()
+ :tag("z", {xmlns="test2"}):up()
+ :tag("x", {xmlns="test2"}):up()
+
+ s:remove_children("x");
+ assert.falsy(s:get_child("x"))
+ assert.truthy(s:get_child("z","test2"));
+ assert.truthy(s:get_child("x","test2"));
+
+ s:remove_children(nil, "test2");
+ assert.truthy(s:get_child("y"))
+ assert.falsy(s:get_child(nil,"test2"));
+
+ s:remove_children();
+ assert.falsy(s.tags[1]);
+ end);
+ end);
+
+ describe("#maptags", function ()
+ it("should work", function ()
+ local s = st.stanza("test")
+ :tag("one"):up()
+ :tag("two"):up()
+ :tag("one"):up()
+ :tag("three"):up();
+
+ local function one_filter(tag)
+ if tag.name == "one" then
+ return nil;
+ end
+ return tag;
+ end
+ assert.equal(4, #s.tags);
+ s:maptags(one_filter);
+ assert.equal(2, #s.tags);
+ end);
+
+ it("should work with multiple consecutive text nodes", function ()
+ local s = st.deserialize({
+ "\n";
+ {
+ "away";
+ name = "show";
+ attr = {};
+ };
+ "\n";
+ {
+ "I am away";
+ name = "status";
+ attr = {};
+ };
+ "\n";
+ {
+ "0";
+ name = "priority";
+ attr = {};
+ };
+ "\n";
+ {
+ name = "c";
+ attr = {
+ xmlns = "http://jabber.org/protocol/caps";
+ node = "http://psi-im.org";
+ hash = "sha-1";
+ };
+ };
+ "\n";
+ "\n";
+ name = "presence";
+ attr = {
+ to = "user@example.com/jflsjfld";
+ from = "room@chat.example.org/nick";
+ };
+ });
+
+ assert.equal(4, #s.tags);
+
+ s:maptags(function (tag) return tag; end);
+ assert.equal(4, #s.tags);
+
+ s:maptags(function (tag)
+ if tag.name == "c" then
+ return nil;
+ end
+ return tag;
+ end);
+ assert.equal(3, #s.tags);
+ end);
+ it("errors on invalid data - #981", function ()
+ local s = st.message({}, "Hello");
+ s.tags[1] = st.clone(s.tags[1]);
+ assert.has_error_match(function ()
+ s:maptags(function () end);
+ end, "Invalid stanza");
+ end);
+ end);
+end);
diff --git a/spec/util_throttle_spec.lua b/spec/util_throttle_spec.lua
new file mode 100644
index 00000000..75daf1b9
--- /dev/null
+++ b/spec/util_throttle_spec.lua
@@ -0,0 +1,150 @@
+
+
+-- Mock util.time
+local now = 0; -- wibbly-wobbly... timey-wimey... stuff
+local function later(n)
+ now = now + n; -- time passes at a different rate
+end
+package.loaded["util.time"] = {
+ now = function() return now; end
+}
+
+
+local throttle = require "util.throttle";
+
+describe("util.throttle", function()
+ describe("#create()", function()
+ it("should be created with correct values", function()
+ now = 5;
+ local a = throttle.create(3, 10);
+ assert.same(a, { balance = 3, max = 3, rate = 0.3, t = 5 });
+
+ local a = throttle.create(3, 5);
+ assert.same(a, { balance = 3, max = 3, rate = 0.6, t = 5 });
+
+ local a = throttle.create(1, 1);
+ assert.same(a, { balance = 1, max = 1, rate = 1, t = 5 });
+
+ local a = throttle.create(10, 10);
+ assert.same(a, { balance = 10, max = 10, rate = 1, t = 5 });
+
+ local a = throttle.create(10, 1);
+ assert.same(a, { balance = 10, max = 10, rate = 10, t = 5 });
+ end);
+ end);
+
+ describe("#update()", function()
+ it("does nothing when no time has passed, even if balance is not full", function()
+ now = 5;
+ local a = throttle.create(10, 10);
+ for i=1,5 do
+ a:update();
+ assert.same(a, { balance = 10, max = 10, rate = 1, t = 5 });
+ end
+ a.balance = 0;
+ for i=1,5 do
+ a:update();
+ assert.same(a, { balance = 0, max = 10, rate = 1, t = 5 });
+ end
+ end);
+ it("updates only time when time passes but balance is full", function()
+ now = 5;
+ local a = throttle.create(10, 10);
+ for i=1,5 do
+ later(5);
+ a:update();
+ assert.same(a, { balance = 10, max = 10, rate = 1, t = 5 + i*5 });
+ end
+ end);
+ it("updates balance when balance has room to grow as time passes", function()
+ now = 5;
+ local a = throttle.create(10, 10);
+ a.balance = 0;
+ assert.same(a, { balance = 0, max = 10, rate = 1, t = 5 });
+
+ later(1);
+ a:update();
+ assert.same(a, { balance = 1, max = 10, rate = 1, t = 6 });
+
+ later(3);
+ a:update();
+ assert.same(a, { balance = 4, max = 10, rate = 1, t = 9 });
+
+ later(10);
+ a:update();
+ assert.same(a, { balance = 10, max = 10, rate = 1, t = 19 });
+ end);
+ it("handles 10 x 0.1s updates the same as 1 x 1s update ", function()
+ now = 5;
+ local a = throttle.create(1, 1);
+
+ a.balance = 0;
+ later(1);
+ a:update();
+ assert.same(a, { balance = 1, max = 1, rate = 1, t = now });
+
+ a.balance = 0;
+ for i=1,10 do
+ later(0.1);
+ a:update();
+ end
+ assert(math.abs(a.balance - 1) < 0.0001); -- incremental updates cause rouding errors
+ end);
+ end);
+
+ -- describe("po")
+
+ describe("#poll()", function()
+ it("should only allow successful polls until cost is hit", function()
+ now = 5;
+
+ local a = throttle.create(3, 10);
+ assert.same(a, { balance = 3, max = 3, rate = 0.3, t = 5 });
+
+ assert.is_true(a:poll(1)); -- 3 -> 2
+ assert.same(a, { balance = 2, max = 3, rate = 0.3, t = 5 });
+
+ assert.is_true(a:poll(2)); -- 2 -> 1
+ assert.same(a, { balance = 0, max = 3, rate = 0.3, t = 5 });
+
+ assert.is_false(a:poll(1)); -- MEEP, out of credits!
+ assert.is_false(a:poll(1)); -- MEEP, out of credits!
+ assert.same(a, { balance = 0, max = 3, rate = 0.3, t = 5 });
+ end);
+
+ it("should not allow polls more than the cost", function()
+ now = 0;
+
+ local a = throttle.create(10, 10);
+ assert.same(a, { balance = 10, max = 10, rate = 1, t = 0 });
+
+ assert.is_false(a:poll(11));
+ assert.same(a, { balance = 10, max = 10, rate = 1, t = 0 });
+
+ assert.is_true(a:poll(6));
+ assert.same(a, { balance = 4, max = 10, rate = 1, t = 0 });
+
+ assert.is_false(a:poll(5));
+ assert.same(a, { balance = 4, max = 10, rate = 1, t = 0 });
+
+ -- fractional
+ assert.is_true(a:poll(3.5));
+ assert.same(a, { balance = 0.5, max = 10, rate = 1, t = 0 });
+
+ assert.is_true(a:poll(0.25));
+ assert.same(a, { balance = 0.25, max = 10, rate = 1, t = 0 });
+
+ assert.is_false(a:poll(0.3));
+ assert.same(a, { balance = 0.25, max = 10, rate = 1, t = 0 });
+
+ assert.is_true(a:poll(0.25));
+ assert.same(a, { balance = 0, max = 10, rate = 1, t = 0 });
+
+ assert.is_false(a:poll(0.1));
+ assert.same(a, { balance = 0, max = 10, rate = 1, t = 0 });
+
+ assert.is_true(a:poll(0));
+ assert.same(a, { balance = 0, max = 10, rate = 1, t = 0 });
+ end);
+ end);
+end);
diff --git a/spec/util_time_spec.lua b/spec/util_time_spec.lua
new file mode 100644
index 00000000..54a99b82
--- /dev/null
+++ b/spec/util_time_spec.lua
@@ -0,0 +1,31 @@
+describe("util.time", function ()
+ local time;
+ setup(function ()
+ time = require "util.time";
+ end);
+ describe("now()", function ()
+ it("exists", function ()
+ assert.is_function(time.now);
+ end);
+ it("returns a number", function ()
+ assert.is_number(time.now());
+ end);
+ end);
+ describe("monotonic()", function ()
+ it("exists", function ()
+ assert.is_function(time.monotonic);
+ end);
+ it("returns a number", function ()
+ assert.is_number(time.monotonic());
+ end);
+ it("time goes in one direction", function ()
+ local a = time.monotonic();
+ local b = time.monotonic();
+ assert.truthy(a <= b);
+ end);
+ end);
+end);
+
+
+
+
diff --git a/spec/util_uuid_spec.lua b/spec/util_uuid_spec.lua
new file mode 100644
index 00000000..95ae0a20
--- /dev/null
+++ b/spec/util_uuid_spec.lua
@@ -0,0 +1,25 @@
+-- This tests the format, not the randomness
+
+local uuid = require "util.uuid";
+
+describe("util.uuid", function()
+ describe("#generate()", function()
+ it("should work follow the UUID pattern", function()
+ -- https://tools.ietf.org/html/rfc4122#section-4.4
+
+ local pattern = "^" .. table.concat({
+ string.rep("%x", 8),
+ string.rep("%x", 4),
+ "4" .. -- version
+ string.rep("%x", 3),
+ "[89ab]" .. -- reserved bits of 1 and 0
+ string.rep("%x", 3),
+ string.rep("%x", 12),
+ }, "%-") .. "$";
+
+ for _ = 1, 100 do
+ assert.is_string(uuid.generate():match(pattern));
+ end
+ end);
+ end);
+end);
diff --git a/spec/util_xml_spec.lua b/spec/util_xml_spec.lua
new file mode 100644
index 00000000..11820894
--- /dev/null
+++ b/spec/util_xml_spec.lua
@@ -0,0 +1,20 @@
+
+local xml = require "util.xml";
+
+describe("util.xml", function()
+ describe("#parse()", function()
+ it("should work", function()
+ local x =
+[[<x xmlns:a="b">
+ <y xmlns:a="c"> <!-- this overwrites 'a' -->
+ <a:z/>
+ </y>
+ <a:z/> <!-- prefix 'a' is nil here, but should be 'b' -->
+</x>
+]]
+ local stanza = xml.parse(x);
+ assert.are.equal(stanza.tags[2].attr.xmlns, "b");
+ assert.are.equal(stanza.tags[2].namespaces["a"], "b");
+ end);
+ end);
+end);
diff --git a/spec/util_xmppstream_spec.lua b/spec/util_xmppstream_spec.lua
new file mode 100644
index 00000000..38b3cbd2
--- /dev/null
+++ b/spec/util_xmppstream_spec.lua
@@ -0,0 +1,136 @@
+
+local xmppstream = require "util.xmppstream";
+
+describe("util.xmppstream", function()
+ local function test(xml, expect_success, ex)
+ local stanzas = {};
+ local session = { notopen = true };
+ local callbacks = {
+ stream_ns = "streamns";
+ stream_tag = "stream";
+ default_ns = "stanzans";
+ streamopened = function (_session)
+ assert.are.equal(session, _session);
+ assert.are.equal(session.notopen, true);
+ _session.notopen = nil;
+ return true;
+ end;
+ handlestanza = function (_session, stanza)
+ assert.are.equal(session, _session);
+ assert.are.equal(_session.notopen, nil);
+ table.insert(stanzas, stanza);
+ end;
+ streamclosed = function (_session)
+ assert.are.equal(session, _session);
+ assert.are.equal(_session.notopen, nil);
+ _session.notopen = nil;
+ end;
+ }
+ if type(ex) == "table" then
+ for k, v in pairs(ex) do
+ if k ~= "_size_limit" then
+ callbacks[k] = v;
+ end
+ end
+ end
+ local stream = xmppstream.new(session, callbacks, ex and ex._size_limit or nil);
+ local ok, err = pcall(function ()
+ assert(stream:feed(xml));
+ end);
+
+ if ok and type(expect_success) == "function" then
+ expect_success(stanzas);
+ end
+ assert.are.equal(not not ok, not not expect_success, "Expected "..(expect_success and ("success ("..tostring(err)..")") or "failure"));
+ end
+
+ local function test_stanza(stanza, expect_success, ex)
+ return test([[<stream:stream xmlns:stream="streamns" xmlns="stanzans">]]..stanza, expect_success, ex);
+ end
+
+ describe("#new()", function()
+ it("should work", function()
+ test([[<stream:stream xmlns:stream="streamns"/>]], true);
+ test([[<stream xmlns="streamns"/>]], true);
+
+ -- Incorrect stream tag name should be rejected
+ test([[<stream1 xmlns="streamns"/>]], false);
+ -- Incorrect stream namespace should be rejected
+ test([[<stream xmlns="streamns1"/>]], false);
+ -- Invalid XML should be rejected
+ test("<>", false);
+
+ test_stanza("<message/>", function (stanzas)
+ assert.are.equal(#stanzas, 1);
+ assert.are.equal(stanzas[1].name, "message");
+ end);
+ test_stanza("< message>>>>/>\n", false);
+
+ test_stanza([[<x xmlns:a="b">
+ <y xmlns:a="c">
+ <a:z/>
+ </y>
+ <a:z/>
+ </x>]], function (stanzas)
+ assert.are.equal(#stanzas, 1);
+ local s = stanzas[1];
+ assert.are.equal(s.name, "x");
+ assert.are.equal(#s.tags, 2);
+
+ assert.are.equal(s.tags[1].name, "y");
+ assert.are.equal(s.tags[1].attr.xmlns, nil);
+
+ assert.are.equal(s.tags[1].tags[1].name, "z");
+ assert.are.equal(s.tags[1].tags[1].attr.xmlns, "c");
+
+ assert.are.equal(s.tags[2].name, "z");
+ assert.are.equal(s.tags[2].attr.xmlns, "b");
+
+ assert.are.equal(s.namespaces, nil);
+ end);
+ end);
+ end);
+
+ it("should allow an XML declaration", function ()
+ test([[<?xml version="1.0" encoding="UTF-8"?><stream xmlns="streamns"/>]], true);
+ test([[<?xml version="1.0" encoding="UTF-8" standalone="yes" ?><stream xmlns="streamns"/>]], true);
+ test([[<?xml version="1.0" encoding="utf-8" ?><stream xmlns="streamns"/>]], true);
+ end);
+
+ it("should not accept XML versions other than 1.0", function ()
+ test([[<?xml version="1.1" encoding="utf-8" ?><stream xmlns="streamns"/>]], false);
+ end);
+
+ it("should not allow a misplaced XML declaration", function ()
+ test([[<stream xmlns="streamns"><?xml version="1.0" encoding="UTF-8"?></stream>]], false);
+ end);
+
+ describe("should forbid restricted XML:", function ()
+ it("comments", function ()
+ test_stanza("<!-- hello world -->", false);
+ end);
+ it("DOCTYPE", function ()
+ test([[<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE stream SYSTEM "mydtd.dtd">]], false);
+ end);
+ it("incorrect encoding specification", function ()
+ -- This is actually caught by the underlying XML parser
+ test([[<?xml version="1.0" encoding="UTF-16"?><stream xmlns="streamns"/>]], false);
+ end);
+ it("non-UTF8 encodings: ISO-8859-1", function ()
+ test([[<?xml version="1.0" encoding="ISO-8859-1"?><stream xmlns="streamns"/>]], false);
+ end);
+ it("non-UTF8 encodings: UTF-16", function ()
+ -- <?xml version="1.0" encoding="UTF-16"?><stream xmlns="streamns"/>
+ -- encoded into UTF-16
+ local hx = ([[fffe3c003f0078006d006c002000760065007200730069006f006e003d00
+ 220031002e0030002200200065006e0063006f00640069006e0067003d00
+ 22005500540046002d003100360022003f003e003c007300740072006500
+ 61006d00200078006d006c006e0073003d00220073007400720065006100
+ 6d006e00730022002f003e00]]):gsub("%x%x", function (c) return string.char(tonumber(c, 16)); end);
+ test(hx, false);
+ end);
+ it("processing instructions", function ()
+ test([[<stream xmlns="streamns"><?xml-stylesheet type="text/xsl" href="style.xsl"?></stream>]], false);
+ end);
+ end);
+end);