File

spec/util_stanza_spec.lua @ 11540:1937b3c3efb5 0.11

mod_c2s, mod_s2s, mod_component, mod_bosh, mod_websockets: Set default stanza size limits c2s/bosh/ws streams will default to 256KB, s2s and components to 512KB. These values are aligned with ejabberd's default settings, which should reduce issues related to inconsistent size limits between servers on the XMPP network. The previous default (10MB) is excessive for any production server, and allows significant memory usage by even unauthenticated sessions.
author Matthew Wild <mwild1@gmail.com>
date Fri, 07 May 2021 17:03:49 +0100
parent 11205:9d1e21c23784
child 11206:f051394762ff
line wrap: on
line source


local st = require "util.stanza";

describe("util.stanza", function()
	describe("#preserialize()", function()
		it("should work", function()
			local stanza = st.stanza("message", { type = "chat" }):text_tag("body", "Hello");
			local stanza2 = st.preserialize(stanza);
			assert.is_table(stanza2, "Preserialized stanza is a table");
			assert.is_nil(getmetatable(stanza2), "Preserialized stanza has no metatable");
			assert.is_string(stanza2.name, "Preserialized stanza has a name field");
			assert.equal(stanza.name, stanza2.name, "Preserialized stanza has same name as the input stanza");
			assert.same(stanza.attr, stanza2.attr, "Preserialized stanza same attr table as input stanza");
			assert.is_nil(stanza2.tags, "Preserialized stanza has no tag list");
			assert.is_nil(stanza2.last_add, "Preserialized stanza has no last_add marker");
			assert.is_table(stanza2[1], "Preserialized child element preserved");
			assert.equal("body", stanza2[1].name, "Preserialized child element name preserved");
		end);
	end);

	describe("#deserialize()", function()
		it("should work", function()
			local stanza = { name = "message", attr = { type = "chat" }, { name = "body", attr = { }, "Hello" } };
			local stanza2 = st.deserialize(st.preserialize(stanza));

			assert.is_table(stanza2, "Deserialized stanza is a table");
			assert.equal(st.stanza_mt, getmetatable(stanza2), "Deserialized stanza has stanza metatable");
			assert.is_string(stanza2.name, "Deserialized stanza has a name field");
			assert.equal(stanza.name, stanza2.name, "Deserialized stanza has same name as the input table");
			assert.same(stanza.attr, stanza2.attr, "Deserialized stanza same attr table as input table");
			assert.is_table(stanza2.tags, "Deserialized stanza has tag list");
			assert.is_table(stanza2[1], "Deserialized child element preserved");
			assert.equal("body", stanza2[1].name, "Deserialized child element name preserved");
		end);
	end);

	describe("#stanza()", function()
		it("should work", function()
			local s = st.stanza("foo", { xmlns = "myxmlns", a = "attr-a" });
			assert.are.equal(s.name, "foo");
			assert.are.equal(s.attr.xmlns, "myxmlns");
			assert.are.equal(s.attr.a, "attr-a");

			local s1 = st.stanza("s1");
			assert.are.equal(s1.name, "s1");
			assert.are.equal(s1.attr.xmlns, nil);
			assert.are.equal(#s1, 0);
			assert.are.equal(#s1.tags, 0);

			s1:tag("child1");
			assert.are.equal(#s1.tags, 1);
			assert.are.equal(s1.tags[1].name, "child1");

			s1:tag("grandchild1"):up();
			assert.are.equal(#s1.tags, 1);
			assert.are.equal(s1.tags[1].name, "child1");
			assert.are.equal(#s1.tags[1], 1);
			assert.are.equal(s1.tags[1][1].name, "grandchild1");

			s1:up():tag("child2");
			assert.are.equal(#s1.tags, 2, tostring(s1));
			assert.are.equal(s1.tags[1].name, "child1");
			assert.are.equal(s1.tags[2].name, "child2");
			assert.are.equal(#s1.tags[1], 1);
			assert.are.equal(s1.tags[1][1].name, "grandchild1");

			s1:up():text("Hello world");
			assert.are.equal(#s1.tags, 2);
			assert.are.equal(#s1, 3);
			assert.are.equal(s1.tags[1].name, "child1");
			assert.are.equal(s1.tags[2].name, "child2");
			assert.are.equal(#s1.tags[1], 1);
			assert.are.equal(s1.tags[1][1].name, "grandchild1");
		end);
		it("should work with unicode values", function ()
			local s = st.stanza("Объект", { xmlns = "myxmlns", ["Объект"] = "&" });
			assert.are.equal(s.name, "Объект");
			assert.are.equal(s.attr.xmlns, "myxmlns");
			assert.are.equal(s.attr["Объект"], "&");
		end);
		it("should allow :text() with nil and empty strings", function ()
			local s_control = st.stanza("foo");
			assert.same(st.stanza("foo"):text(), s_control);
			assert.same(st.stanza("foo"):text(nil), s_control);
			assert.same(st.stanza("foo"):text(""), s_control);
		end);
	end);

	describe("#message()", function()
		it("should work", function()
			local m = st.message();
			assert.are.equal(m.name, "message");
		end);
	end);

	describe("#iq()", function()
		it("should create an iq stanza", function()
			local i = st.iq({ id = "foo" });
			assert.are.equal("iq", i.name);
			assert.are.equal("foo", i.attr.id);
		end);

		it("should reject stanzas with no id", function ()
			assert.has.error_match(function ()
				st.iq();
			end, "id attribute");

			assert.has.error_match(function ()
				st.iq({ foo = "bar" });
			end, "id attribute");
		end);
	end);

	describe("#presence()", function ()
		it("should work", function()
			local p = st.presence();
			assert.are.equal(p.name, "presence");
		end);
	end);

	describe("#reply()", function()
		it("should work for <s>", function()
			-- Test stanza
			local s = st.stanza("s", { to = "touser", from = "fromuser", id = "123" })
				:tag("child1");
			-- Make reply stanza
			local r = st.reply(s);
			assert.are.equal(r.name, s.name);
			assert.are.equal(r.id, s.id);
			assert.are.equal(r.attr.to, s.attr.from);
			assert.are.equal(r.attr.from, s.attr.to);
			assert.are.equal(#r.tags, 0, "A reply should not include children of the original stanza");
		end);

		it("should work for <iq get>", function()
			-- Test stanza
			local s = st.stanza("iq", { to = "touser", from = "fromuser", id = "123", type = "get" })
				:tag("child1");
			-- Make reply stanza
			local r = st.reply(s);
			assert.are.equal(r.name, s.name);
			assert.are.equal(r.id, s.id);
			assert.are.equal(r.attr.to, s.attr.from);
			assert.are.equal(r.attr.from, s.attr.to);
			assert.are.equal(r.attr.type, "result");
			assert.are.equal(#r.tags, 0, "A reply should not include children of the original stanza");
		end);

		it("should work for <iq set>", function()
			-- Test stanza
			local s = st.stanza("iq", { to = "touser", from = "fromuser", id = "123", type = "set" })
				:tag("child1");
			-- Make reply stanza
			local r = st.reply(s);
			assert.are.equal(r.name, s.name);
			assert.are.equal(r.id, s.id);
			assert.are.equal(r.attr.to, s.attr.from);
			assert.are.equal(r.attr.from, s.attr.to);
			assert.are.equal(r.attr.type, "result");
			assert.are.equal(#r.tags, 0, "A reply should not include children of the original stanza");
		end);
	end);

	describe("#error_reply()", function()
		it("should work for <s>", function()
			-- Test stanza
			local s = st.stanza("s", { to = "touser", from = "fromuser", id = "123" })
				:tag("child1");
			-- Make reply stanza
			local r = st.error_reply(s, "cancel", "service-unavailable");
			assert.are.equal(r.name, s.name);
			assert.are.equal(r.id, s.id);
			assert.are.equal(r.attr.to, s.attr.from);
			assert.are.equal(r.attr.from, s.attr.to);
			assert.are.equal(#r.tags, 1);
			assert.are.equal(r.tags[1].tags[1].name, "service-unavailable");
		end);

		it("should work for <iq get>", function()
			-- Test stanza
			local s = st.stanza("iq", { to = "touser", from = "fromuser", id = "123", type = "get" })
				:tag("child1");
			-- Make reply stanza
			local r = st.error_reply(s, "cancel", "service-unavailable");
			assert.are.equal(r.name, s.name);
			assert.are.equal(r.id, s.id);
			assert.are.equal(r.attr.to, s.attr.from);
			assert.are.equal(r.attr.from, s.attr.to);
			assert.are.equal(r.attr.type, "error");
			assert.are.equal(#r.tags, 1);
			assert.are.equal(r.tags[1].tags[1].name, "service-unavailable");
		end);
	end);

	describe("should reject #invalid", function ()
		local invalid_names = {
			["empty string"] = "", ["characters"] = "<>";
		}
		local invalid_data = {
			["number"] = 1234, ["table"] = {};
			["utf8"] = string.char(0xF4, 0x90, 0x80, 0x80);
			["nil"] = "nil"; ["boolean"] = true;
			["control characters"] = "\0\1\2\3";
		};

		for value_type, value in pairs(invalid_names) do
			it(value_type.." in tag names", function ()
				assert.error_matches(function ()
					st.stanza(value);
				end, value_type);
			end);
			it(value_type.." in attribute names", function ()
				assert.error_matches(function ()
					st.stanza("valid", { [value] = "valid" });
				end, value_type);
			end);
		end
		for value_type, value in pairs(invalid_data) do
			if value == "nil" then value = nil; end
			it(value_type.." in tag names", function ()
				assert.error_matches(function ()
					st.stanza(value);
				end, value_type);
			end);
			it(value_type.." in attribute names", function ()
				assert.error_matches(function ()
					st.stanza("valid", { [value] = "valid" });
				end, value_type);
			end);
			if value ~= nil then
				it(value_type.." in attribute values", function ()
					assert.error_matches(function ()
						st.stanza("valid", { valid = value });
					end, value_type);
				end);
				it(value_type.." in text node", function ()
					assert.error_matches(function ()
						st.stanza("valid"):text(value);
					end, value_type);
				end);
			end
		end
	end);

	describe("#is_stanza", function ()
		-- is_stanza(any) -> boolean
		it("identifies stanzas as stanzas", function ()
			assert.truthy(st.is_stanza(st.stanza("x")));
		end);
		it("identifies strings as not stanzas", function ()
			assert.falsy(st.is_stanza(""));
		end);
		it("identifies numbers as not stanzas", function ()
			assert.falsy(st.is_stanza(1));
		end);
		it("identifies tables as not stanzas", function ()
			assert.falsy(st.is_stanza({}));
		end);
	end);

	describe("#remove_children", function ()
		it("should work", function ()
			local s = st.stanza("x", {xmlns="test"})
				:tag("y", {xmlns="test"}):up()
				:tag("z", {xmlns="test2"}):up()
				:tag("x", {xmlns="test2"}):up()

			s:remove_children("x");
			assert.falsy(s:get_child("x"))
			assert.truthy(s:get_child("z","test2"));
			assert.truthy(s:get_child("x","test2"));

			s:remove_children(nil, "test2");
			assert.truthy(s:get_child("y"))
			assert.falsy(s:get_child(nil,"test2"));

			s:remove_children();
			assert.falsy(s.tags[1]);
		end);
	end);

	describe("#maptags", function ()
		it("should work", function ()
			local s = st.stanza("test")
				:tag("one"):up()
				:tag("two"):up()
				:tag("one"):up()
				:tag("three"):up();

			local function one_filter(tag)
				if tag.name == "one" then
					return nil;
				end
				return tag;
			end
			assert.equal(4, #s.tags);
			s:maptags(one_filter);
			assert.equal(2, #s.tags);
		end);

		it("should work with multiple consecutive text nodes", function ()
			local s = st.deserialize({
				"\n";
				{
					"away";
					name = "show";
					attr = {};
				};
				"\n";
				{
					"I am away";
					name = "status";
					attr = {};
				};
				"\n";
				{
					"0";
					name = "priority";
					attr = {};
				};
				"\n";
				{
					name = "c";
					attr = {
						xmlns = "http://jabber.org/protocol/caps";
						node = "http://psi-im.org";
						hash = "sha-1";
					};
				};
				"\n";
				"\n";
				name = "presence";
				attr = {
					to = "user@example.com/jflsjfld";
					from = "room@chat.example.org/nick";
				};
			});

			assert.equal(4, #s.tags);

			s:maptags(function (tag) return tag; end);
			assert.equal(4, #s.tags);

			s:maptags(function (tag)
				if tag.name == "c" then
					return nil;
				end
				return tag;
			end);
			assert.equal(3, #s.tags);
		end);
		it("errors on invalid data - #981", function ()
			local s = st.message({}, "Hello");
			s.tags[1] = st.clone(s.tags[1]);
			assert.has_error_match(function ()
				s:maptags(function () end);
			end, "Invalid stanza");
		end);
	end);

	describe("#clone", function ()
		it("works", function ()
			local s = st.message({type="chat"}, "Hello"):reset();
			local c = st.clone(s);
			assert.same(s, c);
		end);

		it("works", function ()
			assert.has_error(function ()
				st.clone("this is not a stanza");
			end);
		end);
	end);
end);