diff options
Diffstat (limited to 'spec')
63 files changed, 3310 insertions, 0 deletions
diff --git a/spec/core_configmanager_spec.lua b/spec/core_configmanager_spec.lua new file mode 100644 index 00000000..b68d2756 --- /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(configmanager.get("example.com", "testkey"), 123, "Retrieving a set key"); + + configmanager.set("*", "testkey1", 321); + assert.are.equal(configmanager.get("*", "testkey1"), 321, "Retrieving a set global key"); + assert.are.equal(configmanager.get("example.com", "testkey1"), 321, "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(configmanager.get("example.com", "testkey1"), 321, "Retrieving a set key, of which only a globally set one exists"); + + assert.are.equal(configmanager.get(), nil, "No parameters to get()"); + assert.are.equal(configmanager.get("undefined host"), nil, "Getting for undefined host"); + assert.are.equal(configmanager.get("undefined host", "undefined key"), nil, "Getting for undefined host & key"); + end); + end); + + describe("#set()", function() + it("should work", function() + assert.are.equal(configmanager.set("*"), false, "Set with no key"); + + assert.are.equal(configmanager.set("*", "set_test", "testkey"), true, "Setting a nil global value"); + assert.are.equal(configmanager.set("*", "set_test", "testkey", 123), true, "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": "" \u0022 %22 0x22 034 "", + "\/\\\"\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/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/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..f4c0d8b5 --- /dev/null +++ b/spec/util_dataforms_spec.lua @@ -0,0 +1,324 @@ +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); +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..fcf0b031 --- /dev/null +++ b/spec/util_pubsub_spec.lua @@ -0,0 +1,120 @@ +local pubsub; +setup(function () + pubsub = require "util.pubsub"; +end); + +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 broadcaster = spy.new(function () end); + local service = pubsub.new({ + broadcaster = broadcaster; + capabilities = { + none = { + subscribe = true; + be_subscribed = true; + }; + } + }); + + 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")); + 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); + + 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); +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..8abc6096 --- /dev/null +++ b/spec/util_stanza_spec.lua @@ -0,0 +1,260 @@ + +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); + +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_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); |