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