mod_bosh: Add tests (run with 'busted -r bosh')
authorMatthew Wild <mwild1@gmail.com>
Sun, 23 Sep 2018 17:12:21 +0100
changeset 9377 a1a39d395260
parent 9376 1a69803d5d5d
child 9378 816591db764d
mod_bosh: Add tests (run with 'busted -r bosh')
.busted
spec/mod_bosh_spec.lua
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.busted	Sun Sep 23 17:12:21 2018 +0100
@@ -0,0 +1,10 @@
+return {
+  _all = {
+  },
+  default = {
+    ["exclude-tags"] = "mod_bosh";
+  },
+  bosh = {
+    tags = "mod_bosh";
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/spec/mod_bosh_spec.lua	Sun Sep 23 17:12:21 2018 +0100
@@ -0,0 +1,674 @@
+
+-- Requires a host 'localhost' with SASL ANONYMOUS
+
+local bosh_url = "http://localhost:5280/http-bind"
+
+local logger = require "util.logger";
+
+local debug = false;
+
+local print = print;
+if debug then
+	logger.add_simple_sink(print, {
+		--"debug";
+		"info";
+		"warn";
+		"error";
+	});
+else
+	print = function () end
+end
+
+describe("#mod_bosh", function ()
+	local server = require "net.server_select";
+	package.loaded["net.server"] = server;
+	local async = require "util.async";
+	local timer = require "util.timer";
+	local http = require "net.http".new({ suppress_errors = false });
+
+	local function sleep(n)
+		local wait, done = async.waiter();
+		timer.add_task(n, function () done() end);
+		wait();
+	end
+
+	local st = require "util.stanza";
+	local xml = require "util.xml";
+
+	local function request(url, opt, cb, auto_wait)
+		local wait, done = async.waiter();
+		local ok, err;
+		http:request(url, opt, function (...)
+			ok, err = pcall(cb, ...);
+			if not ok then print("CAUGHT", err) end
+			done();
+		end);
+		local function err_wait(throw)
+			wait();
+			if throw ~= false and not ok then
+				error(err);
+			end
+			return ok, err;
+		end
+		if auto_wait == false then
+			return err_wait;
+		else
+			err_wait();
+		end
+	end
+
+	local function run_async(f)
+		local err;
+		local r = async.runner();
+		r:onerror(function (_, err_)
+			print("EER", err_)
+			err = err_;
+			server.setquitting("once");
+		end)
+		:onwaiting(function ()
+			--server.loop();
+		end)
+		:run(function ()
+			f()
+			server.setquitting("once");
+		end);
+		server.loop();
+		if err then
+			error(err);
+		end
+		if r.state ~= "ready" then
+			error("Runner in unexpected state: "..r.state);
+		end
+	end
+
+	it("test endpoint should be reachable", function ()
+		-- This is partly just to ensure the other tests have a chance to succeed
+		-- (i.e. the BOSH endpoint is up and functioning)
+		local function test()
+			request(bosh_url, nil, function (resp, code)
+				if code ~= 200 then
+					error("Unable to reach BOSH endpoint "..bosh_url);
+				end
+				assert.is_string(resp);
+			end);
+		end
+		run_async(test);
+	end);
+
+	it("should respond to past rids with past responses", function ()
+		local resp_1000_1, resp_1000_2 = "1", "2";
+
+		local function test_bosh()
+			local sid;
+
+		-- Set up BOSH session
+			request(bosh_url, {
+				body = tostring(st.stanza("body", {
+					to = "localhost";
+					from = "test@localhost";
+					content = "text/xml; charset=utf-8";
+					hold = "1";
+					rid = "998";
+					wait = "10";
+					["xml:lang"] = "en";
+					["xmpp:version"] = "1.0";
+					xmlns = "http://jabber.org/protocol/httpbind";
+					["xmlns:xmpp"] = "urn:xmpp:xbosh";
+				})
+				:tag("auth", { xmlns = "urn:ietf:params:xml:ns:xmpp-sasl", mechanism = "ANONYMOUS" }):up()
+				:tag("iq", { xmlns = "jabber:client", type = "set", id = "bind1" })
+					:tag("bind", { xmlns = "urn:ietf:params:xml:ns:xmpp-bind" })
+						:tag("resource"):text("bosh-test1"):up()
+					:up()
+				:up()
+				);
+			}, function (response_body)
+				local resp = xml.parse(response_body);
+				if not response_body:find("<jid>", 1, true) then
+					print("ERR", resp:pretty_print());
+					error("Failed to set up BOSH session");
+				end
+				sid = assert(resp.attr.sid);
+				print("SID", sid);
+			end);
+
+		-- Receive some additional post-login stuff
+			request(bosh_url, {
+				body = tostring(st.stanza("body", {
+					sid = sid;
+					rid = "999";
+					content = "text/xml; charset=utf-8";
+					["xml:lang"] = "en";
+					xmlns = "http://jabber.org/protocol/httpbind";
+					["xmlns:xmpp"] = "urn:xmpp:xbosh";
+				})
+				)
+			}, function (response_body)
+				local resp = xml.parse(response_body);
+				print("RESP 999", resp:pretty_print());
+			end);
+
+		-- Send first long poll
+			print "SEND 1000#1"
+			local wait1000 = request(bosh_url, {
+				body = tostring(st.stanza("body", {
+					sid = sid;
+					rid = "1000";
+					content = "text/xml; charset=utf-8";
+					["xml:lang"] = "en";
+					xmlns = "http://jabber.org/protocol/httpbind";
+					["xmlns:xmpp"] = "urn:xmpp:xbosh";
+				}))
+			}, function (response_body)
+				local resp = xml.parse(response_body);
+				resp_1000_1 = resp;
+				print("RESP 1000#1", resp:pretty_print());
+			end, false);
+
+		-- Wait a couple of seconds
+			sleep(2)
+
+		-- Send an early request, causing rid 1000 to return early
+			print "SEND 1001"
+			local wait1001 = request(bosh_url, {
+				body = tostring(st.stanza("body", {
+					sid = sid;
+					rid = "1001";
+					content = "text/xml; charset=utf-8";
+					["xml:lang"] = "en";
+					xmlns = "http://jabber.org/protocol/httpbind";
+					["xmlns:xmpp"] = "urn:xmpp:xbosh";
+				}))
+			}, function (response_body)
+				local resp = xml.parse(response_body);
+				print("RESP 1001", resp:pretty_print());
+			end, false);
+		-- Ensure we've received the response for rid 1000
+			wait1000();
+
+		-- Sleep a couple of seconds
+			print "...pause..."
+			sleep(2);
+
+		-- Re-send rid 1000, we should get the same response
+			print "SEND 1000#2"
+			request(bosh_url, {
+				body = tostring(st.stanza("body", {
+					sid = sid;
+					rid = "1000";
+					content = "text/xml; charset=utf-8";
+					["xml:lang"] = "en";
+					xmlns = "http://jabber.org/protocol/httpbind";
+					["xmlns:xmpp"] = "urn:xmpp:xbosh";
+				}))
+			}, function (response_body)
+				local resp = xml.parse(response_body);
+				resp_1000_2 = resp;
+				print("RESP 1000#2", resp:pretty_print());
+			end);
+
+			local wait_final = request(bosh_url, {
+				body = tostring(st.stanza("body", {
+					sid = sid;
+					rid = "1002";
+					type = "terminate";
+					content = "text/xml; charset=utf-8";
+					["xml:lang"] = "en";
+					xmlns = "http://jabber.org/protocol/httpbind";
+					["xmlns:xmpp"] = "urn:xmpp:xbosh";
+				}))
+			}, function ()
+			end, false);
+
+			print "WAIT 1001"
+			wait1001();
+			wait_final();
+			print "DONE ALL"
+		end
+		run_async(test_bosh);
+		assert.truthy(resp_1000_1);
+		assert.same(resp_1000_1, resp_1000_2);
+	end);
+
+	it("should handle out-of-order requests", function ()
+		local function test()
+			local sid;
+		-- Set up BOSH session
+			local wait, done = async.waiter();
+			http:request(bosh_url, {
+				body = tostring(st.stanza("body", {
+					to = "localhost";
+					from = "test@localhost";
+					content = "text/xml; charset=utf-8";
+					hold = "1";
+					rid = "1";
+					wait = "10";
+					["xml:lang"] = "en";
+					["xmpp:version"] = "1.0";
+					xmlns = "http://jabber.org/protocol/httpbind";
+					["xmlns:xmpp"] = "urn:xmpp:xbosh";
+				}));
+			}, function (response_body)
+				local resp = xml.parse(response_body);
+				sid = assert(resp.attr.sid, "Failed to set up BOSH session");
+				print("SID", sid);
+				done();
+			end);
+			print "WAIT 1"
+			wait();
+			print "DONE 1"
+
+			local rid2_response_received = false;
+
+		-- Temporarily skip rid 2, to simulate missed request
+			local wait3, done3 = async.waiter();
+			http:request(bosh_url, {
+				body = tostring(st.stanza("body", {
+					sid = sid;
+					rid = "3";
+					content = "text/xml; charset=utf-8";
+					["xml:lang"] = "en";
+					xmlns = "http://jabber.org/protocol/httpbind";
+					["xmlns:xmpp"] = "urn:xmpp:xbosh";
+				}):tag("iq", { xmlns = "jabber:client", type = "set", id = "bind" })
+					:tag("bind", { xmlns = "urn:ietf:params:xml:ns:xmpp-bind" }):up()
+				:up()
+				)
+			}, function (response_body)
+				local resp = xml.parse(response_body);
+				print("RESP 3", resp:pretty_print());
+				done3();
+				-- The server should not respond to this request until
+				-- it has responded to rid 2
+				assert.is_true(rid2_response_received);
+			end);
+
+			print "SLEEPING"
+			sleep(2);
+			print "SLEPT"
+
+		-- Send the "missed" rid 2
+			local wait2, done2 = async.waiter();
+			http:request(bosh_url, {
+				body = tostring(st.stanza("body", {
+					sid = sid;
+					rid = "2";
+					content = "text/xml; charset=utf-8";
+					["xml:lang"] = "en";
+					xmlns = "http://jabber.org/protocol/httpbind";
+					["xmlns:xmpp"] = "urn:xmpp:xbosh";
+				}):tag("auth", { xmlns = "urn:ietf:params:xml:ns:xmpp-sasl", mechanism = "ANONYMOUS" }):up()
+				)
+			}, function (response_body)
+				local resp = xml.parse(response_body);
+				print("RESP 2", resp:pretty_print());
+				rid2_response_received = true;
+				done2();
+			end);
+			print "WAIT 2"
+			wait2();
+			print "WAIT 3"
+			wait3();
+			print "QUIT"
+		end
+		run_async(test);
+	end);
+
+	it("should work", function ()
+		local function test()
+			local sid;
+		-- Set up BOSH session
+			local wait, done = async.waiter();
+			http:request(bosh_url, {
+				body = tostring(st.stanza("body", {
+					to = "localhost";
+					from = "test@localhost";
+					content = "text/xml; charset=utf-8";
+					hold = "1";
+					rid = "1";
+					wait = "10";
+					["xml:lang"] = "en";
+					["xmpp:version"] = "1.0";
+					xmlns = "http://jabber.org/protocol/httpbind";
+					["xmlns:xmpp"] = "urn:xmpp:xbosh";
+				}));
+			}, function (response_body)
+				local resp = xml.parse(response_body);
+				sid = assert(resp.attr.sid, "Failed to set up BOSH session");
+				print("SID", sid);
+				done();
+			end);
+			print "WAIT 1"
+			wait();
+			print "DONE 1"
+
+			local rid2_response_received = false;
+
+		-- Send the "missed" rid 2
+			local wait2, done2 = async.waiter();
+			http:request(bosh_url, {
+				body = tostring(st.stanza("body", {
+					sid = sid;
+					rid = "2";
+					content = "text/xml; charset=utf-8";
+					["xml:lang"] = "en";
+					xmlns = "http://jabber.org/protocol/httpbind";
+					["xmlns:xmpp"] = "urn:xmpp:xbosh";
+				}):tag("auth", { xmlns = "urn:ietf:params:xml:ns:xmpp-sasl", mechanism = "ANONYMOUS" }):up()
+				)
+			}, function (response_body)
+				local resp = xml.parse(response_body);
+				print("RESP 2", resp:pretty_print());
+				rid2_response_received = true;
+				done2();
+			end);
+
+			local wait3, done3 = async.waiter();
+			http:request(bosh_url, {
+				body = tostring(st.stanza("body", {
+					sid = sid;
+					rid = "3";
+					content = "text/xml; charset=utf-8";
+					["xml:lang"] = "en";
+					xmlns = "http://jabber.org/protocol/httpbind";
+					["xmlns:xmpp"] = "urn:xmpp:xbosh";
+				}):tag("iq", { xmlns = "jabber:client", type = "set", id = "bind" })
+					:tag("bind", { xmlns = "urn:ietf:params:xml:ns:xmpp-bind" }):up()
+				:up()
+				)
+			}, function (response_body)
+				local resp = xml.parse(response_body);
+				print("RESP 3", resp:pretty_print());
+				done3();
+				-- The server should not respond to this request until
+				-- it has responded to rid 2
+				assert.is_true(rid2_response_received);
+			end);
+
+			print "SLEEPING"
+			sleep(2);
+			print "SLEPT"
+
+			print "WAIT 2"
+			wait2();
+			print "WAIT 3"
+			wait3();
+			print "QUIT"
+		end
+		run_async(test);
+	end);
+
+	it("should handle aborted pending requests", function ()
+		local resp_1000_1, resp_1000_2 = "1", "2";
+
+		local function test_bosh()
+			local sid;
+
+		-- Set up BOSH session
+			request(bosh_url, {
+				body = tostring(st.stanza("body", {
+					to = "localhost";
+					from = "test@localhost";
+					content = "text/xml; charset=utf-8";
+					hold = "1";
+					rid = "998";
+					wait = "10";
+					["xml:lang"] = "en";
+					["xmpp:version"] = "1.0";
+					xmlns = "http://jabber.org/protocol/httpbind";
+					["xmlns:xmpp"] = "urn:xmpp:xbosh";
+				})
+				:tag("auth", { xmlns = "urn:ietf:params:xml:ns:xmpp-sasl", mechanism = "ANONYMOUS" }):up()
+				:tag("iq", { xmlns = "jabber:client", type = "set", id = "bind1" })
+					:tag("bind", { xmlns = "urn:ietf:params:xml:ns:xmpp-bind" })
+						:tag("resource"):text("bosh-test1"):up()
+					:up()
+				:up()
+				);
+			}, function (response_body)
+				local resp = xml.parse(response_body);
+				if not response_body:find("<jid>", 1, true) then
+					print("ERR", resp:pretty_print());
+					error("Failed to set up BOSH session");
+				end
+				sid = assert(resp.attr.sid);
+				print("SID", sid);
+			end);
+
+		-- Receive some additional post-login stuff
+			request(bosh_url, {
+				body = tostring(st.stanza("body", {
+					sid = sid;
+					rid = "999";
+					content = "text/xml; charset=utf-8";
+					["xml:lang"] = "en";
+					xmlns = "http://jabber.org/protocol/httpbind";
+					["xmlns:xmpp"] = "urn:xmpp:xbosh";
+				})
+				)
+			}, function (response_body)
+				local resp = xml.parse(response_body);
+				print("RESP 999", resp:pretty_print());
+			end);
+
+		-- Send first long poll
+			print "SEND 1000#1"
+			local wait1000_1 = request(bosh_url, {
+				body = tostring(st.stanza("body", {
+					sid = sid;
+					rid = "1000";
+					content = "text/xml; charset=utf-8";
+					["xml:lang"] = "en";
+					xmlns = "http://jabber.org/protocol/httpbind";
+					["xmlns:xmpp"] = "urn:xmpp:xbosh";
+				}))
+			}, function (response_body)
+				local resp = xml.parse(response_body);
+				resp_1000_1 = resp;
+				assert.is_nil(resp.attr.type);
+				print("RESP 1000#1", resp:pretty_print());
+			end, false);
+
+		-- Wait a couple of seconds
+			sleep(2)
+
+		-- Re-send rid 1000, we should eventually get a normal response (with no stanzas)
+			print "SEND 1000#2"
+			request(bosh_url, {
+				body = tostring(st.stanza("body", {
+					sid = sid;
+					rid = "1000";
+					content = "text/xml; charset=utf-8";
+					["xml:lang"] = "en";
+					xmlns = "http://jabber.org/protocol/httpbind";
+					["xmlns:xmpp"] = "urn:xmpp:xbosh";
+				}))
+			}, function (response_body)
+				local resp = xml.parse(response_body);
+				resp_1000_2 = resp;
+				assert.is_nil(resp.attr.type);
+				print("RESP 1000#2", resp:pretty_print());
+			end);
+
+			wait1000_1();
+			print "DONE ALL"
+		end
+		run_async(test_bosh);
+		assert.truthy(resp_1000_1);
+		assert.same(resp_1000_1, resp_1000_2);
+	end);
+
+	it("should fail on requests beyond rid window", function ()
+		local function test_bosh()
+			local sid;
+
+		-- Set up BOSH session
+			request(bosh_url, {
+				body = tostring(st.stanza("body", {
+					to = "localhost";
+					from = "test@localhost";
+					content = "text/xml; charset=utf-8";
+					hold = "1";
+					rid = "998";
+					wait = "10";
+					["xml:lang"] = "en";
+					["xmpp:version"] = "1.0";
+					xmlns = "http://jabber.org/protocol/httpbind";
+					["xmlns:xmpp"] = "urn:xmpp:xbosh";
+				})
+				:tag("auth", { xmlns = "urn:ietf:params:xml:ns:xmpp-sasl", mechanism = "ANONYMOUS" }):up()
+				:tag("iq", { xmlns = "jabber:client", type = "set", id = "bind1" })
+					:tag("bind", { xmlns = "urn:ietf:params:xml:ns:xmpp-bind" })
+						:tag("resource"):text("bosh-test1"):up()
+					:up()
+				:up()
+				);
+			}, function (response_body)
+				local resp = xml.parse(response_body);
+				if not response_body:find("<jid>", 1, true) then
+					print("ERR", resp:pretty_print());
+					error("Failed to set up BOSH session");
+				end
+				sid = assert(resp.attr.sid);
+				print("SID", sid);
+			end);
+
+		-- Receive some additional post-login stuff
+			request(bosh_url, {
+				body = tostring(st.stanza("body", {
+					sid = sid;
+					rid = "999";
+					content = "text/xml; charset=utf-8";
+					["xml:lang"] = "en";
+					xmlns = "http://jabber.org/protocol/httpbind";
+					["xmlns:xmpp"] = "urn:xmpp:xbosh";
+				})
+				)
+			}, function (response_body)
+				local resp = xml.parse(response_body);
+				print("RESP 999", resp:pretty_print());
+			end);
+
+		-- Send poll with a rid that's too high (current + 2, where only current + 1 is allowed)
+			print "SEND 1002(!)"
+			request(bosh_url, {
+				body = tostring(st.stanza("body", {
+					sid = sid;
+					rid = "1002";
+					content = "text/xml; charset=utf-8";
+					["xml:lang"] = "en";
+					xmlns = "http://jabber.org/protocol/httpbind";
+					["xmlns:xmpp"] = "urn:xmpp:xbosh";
+				}))
+			}, function (response_body)
+				local resp = xml.parse(response_body);
+				assert.equal("terminate", resp.attr.type);
+				print("RESP 1002(!)", resp:pretty_print());
+			end);
+
+			print "DONE ALL"
+		end
+		run_async(test_bosh);
+	end);
+
+	it("should always succeed for requests within the rid window", function ()
+		local function test()
+			local sid;
+		-- Set up BOSH session
+			request(bosh_url, {
+				body = tostring(st.stanza("body", {
+					to = "localhost";
+					from = "test@localhost";
+					content = "text/xml; charset=utf-8";
+					hold = "1";
+					rid = "1";
+					wait = "10";
+					["xml:lang"] = "en";
+					["xmpp:version"] = "1.0";
+					xmlns = "http://jabber.org/protocol/httpbind";
+					["xmlns:xmpp"] = "urn:xmpp:xbosh";
+				}));
+			}, function (response_body)
+				local resp = xml.parse(response_body);
+				sid = assert(resp.attr.sid, "Failed to set up BOSH session");
+				print("SID", sid);
+			end);
+			print "DONE 1"
+
+			request(bosh_url, {
+				body = tostring(st.stanza("body", {
+					sid = sid;
+					rid = "2";
+					content = "text/xml; charset=utf-8";
+					["xml:lang"] = "en";
+					xmlns = "http://jabber.org/protocol/httpbind";
+					["xmlns:xmpp"] = "urn:xmpp:xbosh";
+				}):tag("auth", { xmlns = "urn:ietf:params:xml:ns:xmpp-sasl", mechanism = "ANONYMOUS" }):up()
+				)
+			}, function (response_body)
+				local resp = xml.parse(response_body);
+				print("RESP 2", resp:pretty_print());
+			end);
+
+			local resp3;
+			request(bosh_url, {
+				body = tostring(st.stanza("body", {
+					sid = sid;
+					rid = "3";
+					content = "text/xml; charset=utf-8";
+					["xml:lang"] = "en";
+					xmlns = "http://jabber.org/protocol/httpbind";
+					["xmlns:xmpp"] = "urn:xmpp:xbosh";
+				}):tag("iq", { xmlns = "jabber:client", type = "set", id = "bind" })
+					:tag("bind", { xmlns = "urn:ietf:params:xml:ns:xmpp-bind" }):up()
+				:up()
+				)
+			}, function (response_body)
+				local resp = xml.parse(response_body);
+				print("RESP 3#1", resp:pretty_print());
+				resp3 = resp;
+			end);
+
+
+			request(bosh_url, {
+				body = tostring(st.stanza("body", {
+					sid = sid;
+					rid = "4";
+					content = "text/xml; charset=utf-8";
+					["xml:lang"] = "en";
+					xmlns = "http://jabber.org/protocol/httpbind";
+					["xmlns:xmpp"] = "urn:xmpp:xbosh";
+				}):tag("iq", { xmlns = "jabber:client", type = "get", id = "ping1" })
+					:tag("ping", { xmlns = "urn:xmpp:ping" }):up()
+				:up()
+				)
+			}, function (response_body)
+				local resp = xml.parse(response_body);
+				print("RESP 4", resp:pretty_print());
+			end);
+
+			request(bosh_url, {
+				body = tostring(st.stanza("body", {
+					sid = sid;
+					rid = "3";
+					content = "text/xml; charset=utf-8";
+					["xml:lang"] = "en";
+					xmlns = "http://jabber.org/protocol/httpbind";
+					["xmlns:xmpp"] = "urn:xmpp:xbosh";
+				}):tag("iq", { xmlns = "jabber:client", type = "set", id = "bind" })
+					:tag("bind", { xmlns = "urn:ietf:params:xml:ns:xmpp-bind" }):up()
+				:up()
+				)
+			}, function (response_body)
+				local resp = xml.parse(response_body);
+				print("RESP 3#2", resp:pretty_print());
+				assert.not_equal("terminate", resp.attr.type);
+				assert.same(resp3, resp);
+			end);
+
+
+			print "QUIT"
+		end
+		run_async(test);
+	end);
+end);