aboutsummaryrefslogtreecommitdiffstats
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/core_moduleapi_spec.lua6
-rw-r--r--spec/core_storagemanager_spec.lua201
-rw-r--r--spec/inputs/test_keys.lua179
-rw-r--r--spec/net_resolvers_service_spec.lua241
-rw-r--r--spec/scansion/bookmarks2.scs181
-rw-r--r--spec/scansion/extdisco.scs8
-rw-r--r--spec/scansion/lastactivity.scs2
-rw-r--r--spec/scansion/mam_extended.scs16
-rw-r--r--spec/scansion/muc_outcast_reason.scs72
-rw-r--r--spec/scansion/muc_subject_issue_667.scs29
-rw-r--r--spec/scansion/pep_itemreply.scs205
-rw-r--r--spec/scansion/pep_nickname.scs2
-rw-r--r--spec/scansion/pep_publish_subscribe.scs6
-rw-r--r--spec/scansion/prosody.cfg.lua16
-rw-r--r--spec/scansion/pubsub_config.scs14
-rw-r--r--spec/scansion/pubsub_max_items.scs13
-rw-r--r--spec/scansion/pubsub_multi_items.scs13
-rw-r--r--spec/scansion/pubsub_preconditions.scs17
-rw-r--r--spec/scansion/uptime.scs2
-rw-r--r--spec/scansion/vcard_temp.scs4
-rw-r--r--spec/util_argparse_spec.lua5
-rw-r--r--spec/util_bitcompat_spec.lua4
-rw-r--r--spec/util_cache_spec.lua74
-rw-r--r--spec/util_crypto_spec.lua205
-rw-r--r--spec/util_dataforms_spec.lua2
-rw-r--r--spec/util_datamapper_spec.lua24
-rw-r--r--spec/util_datetime_spec.lua44
-rw-r--r--spec/util_dbuffer_spec.lua72
-rw-r--r--spec/util_error_spec.lua17
-rw-r--r--spec/util_format_spec.lua48
-rw-r--r--spec/util_fsm_spec.lua250
-rw-r--r--spec/util_hashes_spec.lua55
-rw-r--r--spec/util_hashring_spec.lua8
-rw-r--r--spec/util_http_spec.lua22
-rw-r--r--spec/util_human_io_spec.lua58
-rw-r--r--spec/util_ip_spec.lua26
-rw-r--r--spec/util_iterators_spec.lua8
-rw-r--r--spec/util_jid_spec.lua41
-rw-r--r--spec/util_jsonpointer_spec.lua2
-rw-r--r--spec/util_jsonschema_spec.lua28
-rw-r--r--spec/util_jwt_spec.lua239
-rw-r--r--spec/util_paseto_spec.lua292
-rw-r--r--spec/util_poll_spec.lua35
-rw-r--r--spec/util_promise_spec.lua46
-rw-r--r--spec/util_pubsub_spec.lua12
-rw-r--r--spec/util_rfc6724_spec.lua97
-rw-r--r--spec/util_roles_spec.lua134
-rw-r--r--spec/util_sasl_spec.lua32
-rw-r--r--spec/util_smqueue_spec.lua3
-rw-r--r--spec/util_stanza_spec.lua14
-rw-r--r--spec/util_strbitop_spec.lua (renamed from spec/util_strbitop.lua)44
-rw-r--r--spec/util_table_spec.lua42
-rw-r--r--spec/util_throttle_spec.lua1
-rw-r--r--spec/util_uuid_spec.lua24
-rw-r--r--spec/util_xtemplate_spec.lua47
55 files changed, 3054 insertions, 228 deletions
diff --git a/spec/core_moduleapi_spec.lua b/spec/core_moduleapi_spec.lua
index 20431935..21261385 100644
--- a/spec/core_moduleapi_spec.lua
+++ b/spec/core_moduleapi_spec.lua
@@ -11,8 +11,8 @@ local api = require "core.moduleapi";
local module = setmetatable({}, {__index = api});
local opt = nil;
-function module:log() end
-function module:get_option(name)
+function module.log(_self) end
+function module.get_option(_self, name)
if name == "opt" then
return opt;
else
@@ -20,7 +20,7 @@ function module:get_option(name)
end
end
-function test_option_value(value, returns)
+local function test_option_value(value, returns)
opt = value;
assert(module:get_option_number("opt") == returns.number, "number doesn't match");
assert(module:get_option_string("opt") == returns.string, "string doesn't match");
diff --git a/spec/core_storagemanager_spec.lua b/spec/core_storagemanager_spec.lua
index ae4f44c8..f1adcd50 100644
--- a/spec/core_storagemanager_spec.lua
+++ b/spec/core_storagemanager_spec.lua
@@ -1,13 +1,11 @@
-local unpack = table.unpack or unpack; -- luacheck: ignore 113
-local server = require "net.server_select";
-package.loaded["net.server"] = server;
+local unpack = table.unpack;
-local st = require "util.stanza";
+local st = require "prosody.util.stanza";
local function mock_prosody()
_G.prosody = {
core_post_stanza = function () end;
- events = require "util.events".new();
+ events = require "prosody.util.events".new();
hosts = {};
paths = {
data = "./data";
@@ -36,6 +34,11 @@ local configs = {
};
};
+local test_only_driver = os.getenv "PROSODY_TEST_ONLY_STORAGE";
+if test_only_driver then
+ configs = { [test_only_driver] = configs[test_only_driver] }
+end
+
local test_host = "storage-unit-tests.invalid";
describe("storagemanager", function ()
@@ -47,10 +50,10 @@ describe("storagemanager", function ()
insulate(tagged_name.." #storage backend", function ()
mock_prosody();
- local config = require "core.configmanager";
- local sm = require "core.storagemanager";
- local hm = require "core.hostmanager";
- local mm = require "core.modulemanager";
+ local config = require "prosody.core.configmanager";
+ local sm = require "prosody.core.storagemanager";
+ local hm = require "prosody.core.hostmanager";
+ local mm = require "prosody.core.modulemanager";
-- Simple check to ensure insulation is working correctly
assert.is_nil(config.get(test_host, "storage"));
@@ -196,6 +199,136 @@ describe("storagemanager", function ()
end);
end);
+ describe("keyval+ stores", function ()
+ -- These tests rely on being executed in order, disable any order
+ -- randomization for this block
+ randomize(false);
+
+ local store, kv_store, map_store;
+ it("may be opened", function ()
+ store = assert(sm.open(test_host, "test-kv+", "keyval+"));
+ end);
+
+ local simple_data = { foo = "bar" };
+
+ it("may set data for a user", function ()
+ assert(store:set("user9999", simple_data));
+ end);
+
+ it("may get data for a user", function ()
+ assert.same(simple_data, assert(store:get("user9999")));
+ end);
+
+ it("may be opened as a keyval store", function ()
+ kv_store = assert(sm.open(test_host, "test-kv+", "keyval"));
+ assert.same(simple_data, assert(kv_store:get("user9999")));
+ end);
+
+ it("may be opened as a map store", function ()
+ map_store = assert(sm.open(test_host, "test-kv+", "map"));
+ assert.same("bar", assert(map_store:get("user9999", "foo")));
+ end);
+
+ it("may remove data for a user", function ()
+ assert(store:set("user9999", nil));
+ local ret, err = store:get("user9999");
+ assert.is_nil(ret);
+ assert.is_nil(err);
+ end);
+
+
+ it("may set a specific key for a user", function ()
+ assert(store:set_key("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_key("user9999", "foo"));
+ end);
+
+ it("may find all users with a specific key", function ()
+ assert.is_function(store.get_key_from_all);
+ assert(store:set_key("user9999b", "bar", "bar"));
+ assert(store:set_key("user9999c", "foo", "blah"));
+ local ret, err = store:get_key_from_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_key_from_all);
+ do
+ local ret, err = store:get_key_from_all("");
+ assert.is_nil(ret);
+ assert.is_not_nil(err);
+ end
+ do
+ local ret, err = store:get_key_from_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_key_from_all);
+ do
+ local ret, err = store:delete_key_from_all("");
+ assert.is_nil(ret);
+ assert.is_not_nil(err);
+ end
+ do
+ local ret, err = store:delete_key_from_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_key_from_all);
+ assert(store:set_key("user9999b", "foo", "hello"));
+
+ assert(store:delete_key_from_all("bar"));
+ -- Ensure key was deleted
+ do
+ local ret, err = store:get_key("user9999b", "bar");
+ assert.is_nil(ret);
+ assert.is_nil(err);
+ end
+ -- Ensure other users/keys are intact
+ do
+ local ret, err = store:get_key("user9999", "foo");
+ assert.equal("bar", ret);
+ assert.is_nil(err);
+ end
+ do
+ local ret, err = store:get_key("user9999b", "foo");
+ assert.equal("hello", ret);
+ assert.is_nil(err);
+ end
+ do
+ local ret, err = store:get_key("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_key("user9999", "foo", nil));
+ do
+ local ret, err = store:get_key("user9999", "foo");
+ assert.is_nil(ret);
+ assert.is_nil(err);
+ end
+
+ assert(store:set_key("user9999b", "foo", nil));
+ do
+ local ret, err = store:get_key("user9999b", "foo");
+ assert.is_nil(ret);
+ assert.is_nil(err);
+ end
+ end);
+ end);
+
describe("archive stores", function ()
randomize(false);
@@ -211,13 +344,13 @@ describe("storagemanager", function ()
local test_time = 1539204123;
local test_data = {
- { nil, test_stanza, test_time, "contact@example.com" };
- { nil, test_stanza, test_time+1, "contact2@example.com" };
- { nil, test_stanza, test_time+2, "contact2@example.com" };
+ { nil, test_stanza, test_time-3, "contact@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+0, "contact2@example.com" };
{ nil, test_stanza, test_time+1, "contact3@example.com" };
+ { nil, test_stanza, test_time+2, "contact3@example.com" };
+ { nil, test_stanza, test_time+3, "contact3@example.com" };
};
it("can be added to", function ()
@@ -260,7 +393,7 @@ describe("storagemanager", function ()
assert.equal("test", item.name);
assert.equal("urn:example:foo", item.attr.xmlns);
assert.equal(2, #item.tags);
- assert.equal(test_time, when);
+ assert.equal(test_time-3, when);
end
assert.equal(1, count);
end);
@@ -298,16 +431,16 @@ describe("storagemanager", function ()
assert.equal("test", item.name);
assert.equal("urn:example:foo", item.attr.xmlns);
assert.equal(2, #item.tags);
- assert(test_time <= when);
+ assert(when >= test_time, ("%d >= %d"):format(when, test_time));
end
- assert.equal(#test_data - 2, count);
+ assert.equal(#test_data - 3, 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;
+ ["start"] = test_time-1;
+ ["end"] = test_time+2;
});
assert.truthy(data);
local count = 0;
@@ -318,8 +451,8 @@ describe("storagemanager", function ()
assert.equal("test", item.name);
assert.equal("urn:example:foo", item.attr.xmlns);
assert.equal(2, #item.tags);
- assert(when >= test_time, ("%d >= %d"):format(when, test_time));
- assert(when <= test_time+1, ("%d <= %d"):format(when, test_time+1));
+ assert(when >= test_time-1, ("%d >= %d"):format(when, test_time));
+ assert(when <= test_time+2, ("%d <= %d"):format(when, test_time+1));
end
assert.equal(4, count);
end);
@@ -427,6 +560,30 @@ describe("storagemanager", function ()
end);
+ -- This tests combines the reverse flag with 'before' and 'after' to
+ -- ensure behaviour remains correct
+ it("by id (before and after) in reverse #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];
+ reverse = true;
+ });
+ assert.truthy(data, err);
+ local count = 0;
+ for id, item in data do
+ count = count + 1;
+ assert.truthy(id);
+ assert.equal(test_data[4-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);
+
+
end);
@@ -466,7 +623,7 @@ describe("storagemanager", function ()
local data, err = archive:find("user", {
with = "contact@example.com";
});
- assert.truthy(data);
+ assert.truthy(data, err);
local count = 0;
for id, item, when in data do -- luacheck: ignore id item when
count = count + 1;
diff --git a/spec/inputs/test_keys.lua b/spec/inputs/test_keys.lua
new file mode 100644
index 00000000..e0e9ff8c
--- /dev/null
+++ b/spec/inputs/test_keys.lua
@@ -0,0 +1,179 @@
+local test_keys = {
+ -- ECDSA keypair from jwt.io
+ ecdsa_private_pem = [[
+-----BEGIN PRIVATE KEY-----
+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgevZzL1gdAFr88hb2
+OF/2NxApJCzGCEDdfSp6VQO30hyhRANCAAQRWz+jn65BtOMvdyHKcvjBeBSDZH2r
+1RTwjmYSi9R/zpBnuQ4EiMnCqfMPWiZqB4QdbAd0E7oH50VpuZ1P087G
+-----END PRIVATE KEY-----
+]];
+
+ ecdsa_public_pem = [[
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9
+q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==
+-----END PUBLIC KEY-----
+]];
+
+ -- Self-generated ECDSA keypair
+ alt_ecdsa_private_pem = [[
+-----BEGIN PRIVATE KEY-----
+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgQnn4AHz2Zy+JMAgp
+AZfKAm9F3s6791PstPf5XjHtETKhRANCAAScv9jI3+BOXXlCOXwmQYosIbl9mf4V
+uOwfIoCYSLylAghyxO0n2of8Kji+D+4C1zxNKmZIQa4s8neaIIzXnMY1
+-----END PRIVATE KEY-----
+]];
+
+ alt_ecdsa_public_pem = [[
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEnL/YyN/gTl15Qjl8JkGKLCG5fZn+
+FbjsHyKAmEi8pQIIcsTtJ9qH/Co4vg/uAtc8TSpmSEGuLPJ3miCM15zGNQ==
+-----END PUBLIC KEY-----
+]];
+
+ -- JWT reference keys for ES512
+
+ ecdsa_521_public_pem = [[
+-----BEGIN PUBLIC KEY-----
+MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBgc4HZz+/fBbC7lmEww0AO3NK9wVZ
+PDZ0VEnsaUFLEYpTzb90nITtJUcPUbvOsdZIZ1Q8fnbquAYgxXL5UgHMoywAib47
+6MkyyYgPk0BXZq3mq4zImTRNuaU9slj9TVJ3ScT3L1bXwVuPJDzpr5GOFpaj+WwM
+Al8G7CqwoJOsW7Kddns=
+-----END PUBLIC KEY-----
+]];
+
+ ecdsa_521_private_pem = [[
+-----BEGIN PRIVATE KEY-----
+MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIBiyAa7aRHFDCh2qga
+9sTUGINE5jHAFnmM8xWeT/uni5I4tNqhV5Xx0pDrmCV9mbroFtfEa0XVfKuMAxxf
+Z6LM/yKhgYkDgYYABAGBzgdnP798FsLuWYTDDQA7c0r3BVk8NnRUSexpQUsRilPN
+v3SchO0lRw9Ru86x1khnVDx+duq4BiDFcvlSAcyjLACJvjvoyTLJiA+TQFdmrear
+jMiZNE25pT2yWP1NUndJxPcvVtfBW48kPOmvkY4WlqP5bAwCXwbsKrCgk6xbsp12
+ew==
+-----END PRIVATE KEY-----
+]];
+
+ -- Self-generated keys for ES512
+
+ alt_ecdsa_521_public_pem = [[
+-----BEGIN PUBLIC KEY-----
+MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBIxV0ecG/+qFc/kVPKs8Z6tjJEuRe
+dzrEaqABY6THu7BhCjEoxPr6iRYdiFPzNruFORsCAKf/NFLSoCqyrw9S0YMA1xc+
+uW01145oxT7Sp8BOH1MyOh7xNh+LFLi6X4lV6j5GQrM1sKSa3O5m0+VJmLy5b7cy
+oxNCzXrnEByz+EO2nYI=
+-----END PUBLIC KEY-----
+]];
+
+ alt_ecdsa_521_private_pem = [[
+-----BEGIN EC PRIVATE KEY-----
+MIHcAgEBBEIAV2XJQ4/5Pa5m43/AJdL4XzrRV/l7eQ1JObqmI95YDs3zxM5Mfygz
+DivhvuPdZCZUR+TdZQEdYN4LpllCzrDwmTCgBwYFK4EEACOhgYkDgYYABAEjFXR5
+wb/6oVz+RU8qzxnq2MkS5F53OsRqoAFjpMe7sGEKMSjE+vqJFh2IU/M2u4U5GwIA
+p/80UtKgKrKvD1LRgwDXFz65bTXXjmjFPtKnwE4fUzI6HvE2H4sUuLpfiVXqPkZC
+szWwpJrc7mbT5UmYvLlvtzKjE0LNeucQHLP4Q7adgg==
+-----END EC PRIVATE KEY-----
+]];
+
+ -- Self-generated EdDSA (Ed25519) keypair
+ eddsa_private_pem = [[
+-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VwBCIEIOmrajEfnqdzdJzkJ4irQMCGbYRqrl0RlwPHIw+a5b7M
+-----END PRIVATE KEY-----
+]];
+
+ eddsa_public_pem = [[
+-----BEGIN PUBLIC KEY-----
+MCowBQYDK2VwAyEAFipbSXeGvPVK7eA4+hIOdutZTUUyXswVSbMGi0j1QKE=
+-----END PUBLIC KEY-----
+]];
+
+ -- RSA keypair from jwt.io
+ rsa_private_pem = [[
+-----BEGIN PRIVATE KEY-----
+MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj
+MzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvu
+NMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZ
+qgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulg
+p2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlR
+ZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwi
+VuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskV
+laAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8
+sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83H
+mQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwY
+dgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cw
+ta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQ
+DM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2T
+N0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t
+0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPv
+t8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDU
+AhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk
+48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISL
+DY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnK
+xt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEA
+mNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh
+2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfz
+et6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhr
+VBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicD
+TQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cnc
+dn/RsYEONbwQSjIfMPkvxF+8HQ==
+-----END PRIVATE KEY-----
+]];
+
+ rsa_public_pem = [[
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo
+4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u
++qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh
+kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ
+0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg
+cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc
+mwIDAQAB
+-----END PUBLIC KEY-----
+]];
+
+
+ -- Self-generated RSA keypair
+ alt_rsa_private_pem = [[
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEA4bt6kor2TomqRXfjCFe6T42ibatloyHntZCUdlDDAkUh4oJ/
+4jDCXAUMYqmEsZKCPXxUGQgrmSmNnJPEDMTq3XLDsjhyN4stxEi0UVAiqqBkcEnk
+qbQIJSc9v5gpQF8IuJFWRvSNic0uClFL5W9R2s5AHcOhdFYKeDuitqHT5r+dC7cy
+WZs5YleKaESxmK6i6wMVhL9adAilTuETyMH0yLSh+aXsPYhjns4AbjGmiKOjqd5w
+sPwllEg6rGcIUi/o79z9HN8yLMXq3XNFCCA8RI4Zh3cADI1I5fe6wk1ETN+30cDw
+dGQ+uQbaQrzqmKVRNjZcorMwBjsOX5AMQBFx7wIDAQABAoIBAGxj5pZpTZ4msnEL
+ASQnY9oBS4ZXr8UmaamgU/mADDOR2JR4T0ngWeNvtSPG/GV70TgO9B7U8oJoFoyh
+05jCEXjmO5vfSNDs7rv6oUMONKczvybABKGMRgD5F8hhGyXCvGBLwV7u3OvXbw0b
+PlNcIbTsJpNkNam0CvDyyc3iZOq+HjIqituREV7lDw0rFeAR2YfEWn4VjZsQRZUZ
+XkpQJ5silrXgGemIEGqVA4YyM7i2HmTiLozfVYaVckMc02VFgOaoK9Z/wGlBxtS5
+evc/IGErSA4dc7uXBEeVjhtZoBkof2JV9BNt4hl4KN9wX3tkEX5Aq1K2lirSmg2r
+k+UEtwkCgYEA/5uYg25OR+jCFY/7uNS8e32Re1lgDeO+TeT1m+hcF1gCb2GBLifL
+yprnuytaz1/mPqawfwbilaxntLBoa5cmNKB3zDsgv4sM451yGZ0oxU0dXpDVHblu
+3nhxcaOXtb8jiSsr2MqgMbFlu7m8OupIliS+s8Pq72s6HUQQRKbJ+9MCgYEA4hQl
+1W/7nDI2SR4Q3UapQnaUjmDVxX5OD+E4RpKuRF6xF7Ao2CLZusMVo8WN8YiSQP2c
+RnzQNKgAVy/1zlhaaQDTs2TmSy9iStbuNZ8P+Gh6kmQXuHxwPyURSmwdpgZdL3+D
+8tt6pQNQ0vsLjA9VwHmzIT+rsxPmTxKNvBdNK/UCgYByP6zqyioJMDtYAfRkiAn7
+NIQLW0Z4ztvn2zgAyNoowPjNqgpgg/8t/xEm8tjzKg0y4bSwAnbSqa3s8JCrznKQ
+QU1qpt8bXl6TenNeiYWIstA2zYvEbnbkz3b9cT7FSLrse7RsgR0bOQyc3QcKWl+5
+ZJEsrpxbCVV/cUXIObi8awKBgQDOI8rfk+0bXhlrkBOWf/CjnpYUQK2LF4C8MALt
+Lp/hzWmyjLihYx2eknUv0Fl966ZXxidxiisaaDlvRlbeIGfHqK5fu9fUpE7+qH2p
+vPCF81YYF1YdrLF4kiby8iQSl2juf1nj3kY1IhHXXnsH6Y+qIg24emLntXRhkyxT
+XffK5QKBgGbzEvVgDkerw1SiefAaZnLumJJXBlKjJ00Sq8YLeViyFC/sr4EfG/cV
+7VYRhBw3e7RcYSBAA7uv8i3iIeCFjFooIZUARqXk4+yW753tY5nSJTWfkR7Bp5Pa
+9jKloxckbZKMjH23a+ABOxomY3l93KOBvjLvMYqccuREOwaT12cn
+-----END RSA PRIVATE KEY-----
+]];
+
+ alt_rsa_public_pem = [[
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4bt6kor2TomqRXfjCFe6
+T42ibatloyHntZCUdlDDAkUh4oJ/4jDCXAUMYqmEsZKCPXxUGQgrmSmNnJPEDMTq
+3XLDsjhyN4stxEi0UVAiqqBkcEnkqbQIJSc9v5gpQF8IuJFWRvSNic0uClFL5W9R
+2s5AHcOhdFYKeDuitqHT5r+dC7cyWZs5YleKaESxmK6i6wMVhL9adAilTuETyMH0
+yLSh+aXsPYhjns4AbjGmiKOjqd5wsPwllEg6rGcIUi/o79z9HN8yLMXq3XNFCCA8
+RI4Zh3cADI1I5fe6wk1ETN+30cDwdGQ+uQbaQrzqmKVRNjZcorMwBjsOX5AMQBFx
+7wIDAQAB
+-----END PUBLIC KEY-----
+]];
+};
+
+return test_keys;
diff --git a/spec/net_resolvers_service_spec.lua b/spec/net_resolvers_service_spec.lua
new file mode 100644
index 00000000..53ce4754
--- /dev/null
+++ b/spec/net_resolvers_service_spec.lua
@@ -0,0 +1,241 @@
+local set = require "util.set";
+
+insulate("net.resolvers.service", function ()
+ local adns = {
+ resolver = function ()
+ return {
+ lookup = function (_, cb, qname, qtype, qclass)
+ if qname == "_xmpp-server._tcp.example.com"
+ and (qtype or "SRV") == "SRV"
+ and (qclass or "IN") == "IN" then
+ cb({
+ { -- 60+35+60
+ srv = { target = "xmpp0-a.example.com", port = 5228, priority = 0, weight = 60 };
+ };
+ {
+ srv = { target = "xmpp0-b.example.com", port = 5216, priority = 0, weight = 35 };
+ };
+ {
+ srv = { target = "xmpp0-c.example.com", port = 5200, priority = 0, weight = 0 };
+ };
+ {
+ srv = { target = "xmpp0-d.example.com", port = 5256, priority = 0, weight = 120 };
+ };
+
+ {
+ srv = { target = "xmpp1-a.example.com", port = 5273, priority = 1, weight = 30 };
+ };
+ {
+ srv = { target = "xmpp1-b.example.com", port = 5274, priority = 1, weight = 30 };
+ };
+
+ {
+ srv = { target = "xmpp2.example.com", port = 5275, priority = 2, weight = 0 };
+ };
+ });
+ elseif qname == "_xmpp-server._tcp.single.example.com"
+ and (qtype or "SRV") == "SRV"
+ and (qclass or "IN") == "IN" then
+ cb({
+ {
+ srv = { target = "xmpp0-a.example.com", port = 5269, priority = 0, weight = 0 };
+ };
+ });
+ elseif qname == "_xmpp-server._tcp.half.example.com"
+ and (qtype or "SRV") == "SRV"
+ and (qclass or "IN") == "IN" then
+ cb({
+ {
+ srv = { target = "xmpp0-a.example.com", port = 5269, priority = 0, weight = 0 };
+ };
+ {
+ srv = { target = "xmpp0-b.example.com", port = 5270, priority = 0, weight = 1 };
+ };
+ });
+ elseif qtype == "A" then
+ local l = qname:match("%-(%a)%.example.com$") or "1";
+ local d = ("%d"):format(l:byte())
+ cb({
+ {
+ a = "127.0.0."..d;
+ };
+ });
+ elseif qtype == "AAAA" then
+ local l = qname:match("%-(%a)%.example.com$") or "1";
+ local d = ("%04d"):format(l:byte())
+ cb({
+ {
+ aaaa = "fdeb:9619:649e:c7d9::"..d;
+ };
+ });
+ else
+ cb(nil);
+ end
+ end;
+ };
+ end;
+ };
+ package.loaded["net.adns"] = mock(adns);
+ local resolver = require "net.resolvers.service";
+ math.randomseed(os.time());
+ it("works for 99% of deployments", function ()
+ -- Most deployments only have a single SRV record, let's make
+ -- sure that works okay
+
+ local expected_targets = set.new({
+ -- xmpp0-a
+ "tcp4 127.0.0.97 5269";
+ "tcp6 fdeb:9619:649e:c7d9::0097 5269";
+ });
+ local received_targets = set.new({});
+
+ local r = resolver.new("single.example.com", "xmpp-server");
+ local done = false;
+ local function handle_target(...)
+ if ... == nil then
+ done = true;
+ -- No more targets
+ return;
+ end
+ received_targets:add(table.concat({ ... }, " ", 1, 3));
+ end
+ r:next(handle_target);
+ while not done do
+ r:next(handle_target);
+ end
+
+ -- We should have received all expected targets, and no unexpected
+ -- ones:
+ assert.truthy(set.xor(received_targets, expected_targets):empty());
+ end);
+
+ it("supports A/AAAA fallback", function ()
+ -- Many deployments don't have any SRV records, so we should
+ -- fall back to A/AAAA records instead when that is the case
+
+ local expected_targets = set.new({
+ -- xmpp0-a
+ "tcp4 127.0.0.97 5269";
+ "tcp6 fdeb:9619:649e:c7d9::0097 5269";
+ });
+ local received_targets = set.new({});
+
+ local r = resolver.new("xmpp0-a.example.com", "xmpp-server", "tcp", { default_port = 5269 });
+ local done = false;
+ local function handle_target(...)
+ if ... == nil then
+ done = true;
+ -- No more targets
+ return;
+ end
+ received_targets:add(table.concat({ ... }, " ", 1, 3));
+ end
+ r:next(handle_target);
+ while not done do
+ r:next(handle_target);
+ end
+
+ -- We should have received all expected targets, and no unexpected
+ -- ones:
+ assert.truthy(set.xor(received_targets, expected_targets):empty());
+ end);
+
+
+ it("works", function ()
+ local expected_targets = set.new({
+ -- xmpp0-a
+ "tcp4 127.0.0.97 5228";
+ "tcp6 fdeb:9619:649e:c7d9::0097 5228";
+ "tcp4 127.0.0.97 5273";
+ "tcp6 fdeb:9619:649e:c7d9::0097 5273";
+
+ -- xmpp0-b
+ "tcp4 127.0.0.98 5274";
+ "tcp6 fdeb:9619:649e:c7d9::0098 5274";
+ "tcp4 127.0.0.98 5216";
+ "tcp6 fdeb:9619:649e:c7d9::0098 5216";
+
+ -- xmpp0-c
+ "tcp4 127.0.0.99 5200";
+ "tcp6 fdeb:9619:649e:c7d9::0099 5200";
+
+ -- xmpp0-d
+ "tcp4 127.0.0.100 5256";
+ "tcp6 fdeb:9619:649e:c7d9::0100 5256";
+
+ -- xmpp2
+ "tcp4 127.0.0.49 5275";
+ "tcp6 fdeb:9619:649e:c7d9::0049 5275";
+
+ });
+ local received_targets = set.new({});
+
+ local r = resolver.new("example.com", "xmpp-server");
+ local done = false;
+ local function handle_target(...)
+ if ... == nil then
+ done = true;
+ -- No more targets
+ return;
+ end
+ received_targets:add(table.concat({ ... }, " ", 1, 3));
+ end
+ r:next(handle_target);
+ while not done do
+ r:next(handle_target);
+ end
+
+ -- We should have received all expected targets, and no unexpected
+ -- ones:
+ assert.truthy(set.xor(received_targets, expected_targets):empty());
+ end);
+
+ it("balances across weights correctly #slow", function ()
+ -- This mimics many repeated connections to 'example.com' (mock
+ -- records defined above), and records the port number of the
+ -- first target. Therefore it (should) only return priority
+ -- 0 records, and the input data is constructed such that the
+ -- last two digits of the port number represent the percentage
+ -- of times that record should (on average) be picked first.
+
+ -- To prevent random test failures, we test across a handful
+ -- of fixed (randomly selected) seeds.
+ for _, seed in ipairs({ 8401877, 3943829, 7830992 }) do
+ math.randomseed(seed);
+
+ local results = {};
+ local function run()
+ local run_results = {};
+ local r = resolver.new("example.com", "xmpp-server");
+ local function record_target(...)
+ if ... == nil then
+ -- No more targets
+ return;
+ end
+ run_results = { ... };
+ end
+ r:next(record_target);
+ return run_results[3];
+ end
+
+ for _ = 1, 1000 do
+ local port = run();
+ results[port] = (results[port] or 0) + 1;
+ end
+
+ local ports = {};
+ for port in pairs(results) do
+ table.insert(ports, port);
+ end
+ table.sort(ports);
+ for _, port in ipairs(ports) do
+ --print("PORT", port, tostring((results[port]/1000) * 100).."% hits (expected "..tostring(port-5200).."%)");
+ local hit_pct = (results[port]/1000) * 100;
+ local expected_pct = port - 5200;
+ --print(hit_pct, expected_pct, math.abs(hit_pct - expected_pct));
+ assert.is_true(math.abs(hit_pct - expected_pct) < 5);
+ end
+ --print("---");
+ end
+ end);
+end);
diff --git a/spec/scansion/bookmarks2.scs b/spec/scansion/bookmarks2.scs
new file mode 100644
index 00000000..0243ca54
--- /dev/null
+++ b/spec/scansion/bookmarks2.scs
@@ -0,0 +1,181 @@
+# Pubsub: Bookmarks 2.0
+
+[Client] Juliet
+ jid: admin@localhost
+ password: password
+
+// admin@localhost is assumed to have node creation privileges
+
+---------
+
+Juliet connects
+
+-- Generated with https://gitlab.com/xmpp-rs/xmpp-parsers:
+-- cargo run --example=generate-caps https://code.matthewwild.co.uk/scansion/ <<< "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' name='scansion' type='bot'/><feature var='http://jabber.org/protocol/disco#info'/><feature var='urn:xmpp:bookmarks:1+notify'/></query>"
+Juliet sends:
+ <presence id='presence0'>
+ <c xmlns='http://jabber.org/protocol/caps'
+ hash='sha-1'
+ node='https://code.matthewwild.co.uk/scansion/'
+ ver='CPuQARM1gCTq2f6/ZjHUzWL2QHg='/>
+ <c xmlns='urn:xmpp:caps'>
+ <hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>OTy9GPCvBZRvqzOHmD/ThA1WbBH3tNoeKbdqKQCRPHc=</hash>
+ <hash xmlns='urn:xmpp:hashes:2' algo='sha3-256'>f/rxDeTf6HyjQ382V3GEG/UfAs5IeclC05jBSBnVQCI=</hash>
+ <hash xmlns='urn:xmpp:hashes:2' algo='blake2b-256'>ucfqg/NrLj0omE+26hYMrbpcmxHcU4Z3hfAQIF+6tt0=</hash>
+ </c>
+ </presence>
+
+Juliet receives:
+ <iq from="${Juliet's JID}" id='disco' type='get'>
+ <query xmlns='http://jabber.org/protocol/disco#info' node='https://code.matthewwild.co.uk/scansion/#CPuQARM1gCTq2f6/ZjHUzWL2QHg='/>
+ </iq>
+
+Juliet sends:
+ <iq to="${Juliet's JID}" id='disco' type='result'>
+ <query xmlns='http://jabber.org/protocol/disco#info' node='https://code.matthewwild.co.uk/scansion/#CPuQARM1gCTq2f6/ZjHUzWL2QHg='>
+ <identity category='client' name='scansion' type='bot'/>
+ <feature var='http://jabber.org/protocol/disco#info'/>
+ <feature var='urn:xmpp:bookmarks:1+notify'/>
+ </query>
+ </iq>
+
+Juliet sends:
+ <iq type='set' id='pub0'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <publish node='urn:xmpp:bookmarks:1'>
+ <item id='theplay@conference.shakespeare.lit'>
+ <conference xmlns='urn:xmpp:bookmarks:1'
+ name='The Play&apos;s the Thing'
+ autojoin='true'>
+ <nick>JC</nick>
+ </conference>
+ </item>
+ </publish>
+ <publish-options>
+ <x xmlns='jabber:x:data' type='submit'>
+ <field var='FORM_TYPE' type='hidden'>
+ <value>http://jabber.org/protocol/pubsub#publish-options</value>
+ </field>
+ <field var='pubsub#persist_items'>
+ <value>true</value>
+ </field>
+ <field var='pubsub#max_items'>
+ <value>255</value>
+ </field>
+ <field var='pubsub#send_last_published_item'>
+ <value>never</value>
+ </field>
+ <field var='pubsub#access_model'>
+ <value>whitelist</value>
+ </field>
+ </x>
+ </publish-options>
+ </pubsub>
+ </iq>
+
+Juliet receives:
+ <message type='headline' from="${Juliet's JID}">
+ <event xmlns='http://jabber.org/protocol/pubsub#event'>
+ <items node='urn:xmpp:bookmarks:1'>
+ <item id='theplay@conference.shakespeare.lit'>
+ <conference xmlns='urn:xmpp:bookmarks:1'
+ name='The Play&apos;s the Thing'
+ autojoin='true'>
+ <nick>JC</nick>
+ </conference>
+ </item>
+ </items>
+ </event>
+ </message>
+
+Juliet receives:
+ <iq type='result' id='pub0'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <publish node='urn:xmpp:bookmarks:1'>
+ <item id='theplay@conference.shakespeare.lit'/>
+ </publish>
+ </pubsub>
+ </iq>
+
+Juliet sends:
+ <iq type='set' id='pub1'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <publish node='urn:xmpp:bookmarks:1'>
+ <item id='orchard@conference.shakespeare.lit'>
+ <conference xmlns='urn:xmpp:bookmarks:1'
+ name='The Orchard'
+ autojoin='true'>
+ <nick>JC</nick>
+ </conference>
+ </item>
+ </publish>
+ <publish-options>
+ <x xmlns='jabber:x:data' type='submit'>
+ <field var='FORM_TYPE' type='hidden'>
+ <value>http://jabber.org/protocol/pubsub#publish-options</value>
+ </field>
+ <field var='pubsub#persist_items'>
+ <value>true</value>
+ </field>
+ <field var='pubsub#max_items'>
+ <value>255</value>
+ </field>
+ <field var='pubsub#send_last_published_item'>
+ <value>never</value>
+ </field>
+ <field var='pubsub#access_model'>
+ <value>whitelist</value>
+ </field>
+ </x>
+ </publish-options>
+ </pubsub>
+ </iq>
+
+Juliet receives:
+ <message type='headline' from="${Juliet's JID}">
+ <event xmlns='http://jabber.org/protocol/pubsub#event'>
+ <items node='urn:xmpp:bookmarks:1'>
+ <item id='orchard@conference.shakespeare.lit'>
+ <conference xmlns='urn:xmpp:bookmarks:1'
+ name='The Orchard'
+ autojoin='true'>
+ <nick>JC</nick>
+ </conference>
+ </item>
+ </items>
+ </event>
+ </message>
+
+Juliet receives:
+ <iq type='result' id='pub1'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <publish node='urn:xmpp:bookmarks:1'>
+ <item id='orchard@conference.shakespeare.lit'/>
+ </publish>
+ </pubsub>
+ </iq>
+
+Juliet sends:
+ <iq type='set' id='retract0'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <retract node='urn:xmpp:bookmarks:1' notify='1'>
+ <item id='theplay@conference.shakespeare.lit'/>
+ </retract>
+ </pubsub>
+ </iq>
+
+Juliet receives:
+ <message type='headline' from="${Juliet's JID}">
+ <event xmlns='http://jabber.org/protocol/pubsub#event'>
+ <items node='urn:xmpp:bookmarks:1'>
+ <retract id='theplay@conference.shakespeare.lit'/>
+ </items>
+ </event>
+ </message>
+
+Juliet receives:
+ <iq type='result' id='retract0'/>
+
+Juliet disconnects
+
+// vim: syntax=xml:
diff --git a/spec/scansion/extdisco.scs b/spec/scansion/extdisco.scs
index f0781dc5..f2c8139f 100644
--- a/spec/scansion/extdisco.scs
+++ b/spec/scansion/extdisco.scs
@@ -17,8 +17,8 @@ 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='9876' type='turn' restricted='1' password='{scansion:any}' transport='udp' host='default.example' username='{scansion:any}'/>
+ <service port='9876' type='turn' restricted='1' password='{scansion:any}' transport='udp' host='default.example' username='{scansion:any}'/>
<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>
@@ -47,8 +47,8 @@ Romeo sends:
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'/>
+ <service port='9876' type='turn' restricted='1' password='{scansion:any}' transport='udp' host='default.example' username='{scansion:any}'/>
+ <service port='9876' type='turn' restricted='1' password='{scansion:any}' transport='udp' host='default.example' username='{scansion:any}'/>
</credentials>
</iq>
diff --git a/spec/scansion/lastactivity.scs b/spec/scansion/lastactivity.scs
index 44f4e516..4b8c8573 100644
--- a/spec/scansion/lastactivity.scs
+++ b/spec/scansion/lastactivity.scs
@@ -37,7 +37,7 @@ Romeo sends:
Romeo receives:
<iq type='result' id='a'>
- <query xmlns='jabber:iq:last' seconds='0'>Goodbye</query>
+ <query xmlns='jabber:iq:last' seconds='{scansion:any}'>Goodbye</query>
</iq>
Romeo disconnects
diff --git a/spec/scansion/mam_extended.scs b/spec/scansion/mam_extended.scs
index 2c6840df..e79eba95 100644
--- a/spec/scansion/mam_extended.scs
+++ b/spec/scansion/mam_extended.scs
@@ -45,8 +45,8 @@ Romeo sends:
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}"/>
+ <start timestamp="{scansion:capture:start}" xmlns="urn:xmpp:mam:2" id="{scansion:capture:first}"/>
+ <end timestamp="{scansion:capture:end}" xmlns="urn:xmpp:mam:2" id="{scansion:capture:last}"/>
</metadata>
</iq>
@@ -57,9 +57,9 @@ Romeo sends:
Romeo receives:
<message to="${Romeo's full JID}">
- <result xmlns="urn:xmpp:mam:2" queryid="q1" id="{scansion:any}">
+ <result xmlns="urn:xmpp:mam:2" queryid="q1" id="{scansion:capture:first}">
<forwarded xmlns="urn:xmpp:forward:0">
- <delay stamp="2008-08-22T21:09:04Z" xmlns="urn:xmpp:delay"/>
+ <delay stamp="{scansion:capture:start}" 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>
@@ -69,9 +69,9 @@ Romeo receives:
Romeo receives:
<message to="${Romeo's full JID}">
- <result xmlns="urn:xmpp:mam:2" queryid="q1" id="{scansion:any}">
+ <result xmlns="urn:xmpp:mam:2" queryid="q1" id="{scansion:capture:last}">
<forwarded xmlns="urn:xmpp:forward:0">
- <delay stamp="2008-08-22T21:09:04Z" xmlns="urn:xmpp:delay"/>
+ <delay stamp="{scansion:capture:end}" 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>
@@ -98,7 +98,7 @@ 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"/>
+ <delay stamp="{scansion:capture:start}" 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>
@@ -110,7 +110,7 @@ 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"/>
+ <delay stamp="{scansion:capture:end}" 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>
diff --git a/spec/scansion/muc_outcast_reason.scs b/spec/scansion/muc_outcast_reason.scs
new file mode 100644
index 00000000..e2725653
--- /dev/null
+++ b/spec/scansion/muc_outcast_reason.scs
@@ -0,0 +1,72 @@
+# Save ban reason
+
+[Client] Romeo
+ password: password
+ jid: user@localhost
+
+-----
+
+Romeo connects
+
+Romeo sends:
+ <presence to="muc-outcast-reason@conference.localhost/Romeo">
+ <x xmlns="http://jabber.org/protocol/muc"/>
+ </presence>
+
+Romeo receives:
+ <presence from="muc-outcast-reason@conference.localhost/Romeo">
+ <x xmlns="http://jabber.org/protocol/muc#user">
+ <status code="201"/>
+ <item jid="${Romeo's full JID}" role="moderator" affiliation="owner"/>
+ <status code="110"/>
+ </x>
+ </presence>
+
+Romeo receives:
+ <message type="groupchat" from="muc-outcast-reason@conference.localhost">
+ <subject/>
+ </message>
+
+Romeo sends:
+ <iq id="lx5" to="muc-outcast-reason@conference.localhost" type="set">
+ <query xmlns="http://jabber.org/protocol/muc#admin">
+ <item affiliation="outcast" jid="tybalt@localhost">
+ <reason>Hey calm down</reason>
+ </item>
+ </query>
+ </iq>
+
+Romeo receives:
+ <message from="muc-outcast-reason@conference.localhost">
+ <x xmlns="http://jabber.org/protocol/muc#user">
+ <status code="301"/>
+ <item jid="tybalt@localhost" affiliation="outcast">
+ <reason>Hey calm down</reason>
+ </item>
+ </x>
+ </message>
+
+Romeo receives:
+ <iq id="lx5" type="result" from="muc-outcast-reason@conference.localhost"/>
+
+Romeo sends:
+ <iq id="lx6" to="muc-outcast-reason@conference.localhost" type="get">
+ <query xmlns="http://jabber.org/protocol/muc#admin">
+ <item affiliation="outcast"/>
+ </query>
+ </iq>
+
+Romeo receives:
+ <iq id="lx6" type="result" from="muc-outcast-reason@conference.localhost">
+ <query xmlns="http://jabber.org/protocol/muc#admin">
+ <item jid="tybalt@localhost" affiliation="outcast">
+ <reason>Hey calm down</reason>
+ </item>
+ </query>
+ </iq>
+
+Romeo disconnects
+
+Romeo sends:
+ <presence type='unavailable'/>
+
diff --git a/spec/scansion/muc_subject_issue_667.scs b/spec/scansion/muc_subject_issue_667.scs
index 74980073..a4544ce4 100644
--- a/spec/scansion/muc_subject_issue_667.scs
+++ b/spec/scansion/muc_subject_issue_667.scs
@@ -42,6 +42,21 @@ Romeo receives:
<body>Hello everyone</body>
</message>
+# this should be treated as a normal message
+Romeo sends:
+ <message to="issue667@conference.localhost" type="groupchat">
+ <subject>New thread</subject>
+ <thread>498acea5-5894-473f-b4c6-c77319d11c75</thread>
+ <store xmlns="urn:xmpp:hints"/>
+ </message>
+
+Romeo receives:
+ <message type="groupchat" from="issue667@conference.localhost/Romeo">
+ <subject>New thread</subject>
+ <thread>498acea5-5894-473f-b4c6-c77319d11c75</thread>
+ <store xmlns="urn:xmpp:hints"/>
+ </message>
+
# Resync
Romeo sends:
<presence to="issue667@conference.localhost/Romeo">
@@ -63,6 +78,13 @@ Romeo receives:
<body>Hello everyone</body>
</message>
+Romeo receives:
+ <message type="groupchat" from="issue667@conference.localhost/Romeo">
+ <subject>New thread</subject>
+ <thread>498acea5-5894-473f-b4c6-c77319d11c75</thread>
+ <store xmlns="urn:xmpp:hints"/>
+ </message>
+
# the still empty subject
Romeo receives:
<message type="groupchat" from="issue667@conference.localhost">
@@ -116,6 +138,13 @@ Romeo receives:
Romeo receives:
<message type="groupchat" from="issue667@conference.localhost/Romeo">
+ <subject>New thread</subject>
+ <thread>498acea5-5894-473f-b4c6-c77319d11c75</thread>
+ <store xmlns="urn:xmpp:hints"/>
+ </message>
+
+Romeo receives:
+ <message type="groupchat" from="issue667@conference.localhost/Romeo">
<body>Lorem ipsum dolor sit amet</body>
</message>
diff --git a/spec/scansion/pep_itemreply.scs b/spec/scansion/pep_itemreply.scs
new file mode 100644
index 00000000..878b9e99
--- /dev/null
+++ b/spec/scansion/pep_itemreply.scs
@@ -0,0 +1,205 @@
+# PEP itemreply (publisher) configuration
+# This tests that itemreply == "publisher" will add the 'publisher' attribute
+# to notifications. Since this is not the default behaviour, the normal
+# publish and subscribe test cases cover testing that it is not included
+# otherwise.
+
+[Client] Romeo
+ jid: pep-test-df6zdvkv@localhost
+ password: password
+
+[Client] Juliet
+ jid: pep-test-5k90xvps@localhost
+ password: password
+
+-----
+
+Romeo connects
+
+Romeo sends:
+ <presence>
+ <c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/>
+ </presence>
+
+Romeo receives:
+ <iq type='get' id='disco' from="${Romeo's JID}">
+ <query node='http://code.matthewwild.co.uk/verse/#PDH7CGVPRERS2WUqBD18PHGEzaY=' xmlns='http://jabber.org/protocol/disco#info'/>
+ </iq>
+
+Romeo receives:
+ <presence from="${Romeo's full JID}">
+ <c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/>
+ </presence>
+
+Romeo sends:
+ <iq type='get' id='6'>
+ <query ver='' xmlns='jabber:iq:roster'/>
+ </iq>
+
+Romeo receives:
+ <iq type='result' id='6'>
+ <query ver='1' xmlns='jabber:iq:roster'/>
+ </iq>
+
+Juliet connects
+
+Juliet sends:
+ <presence>
+ <c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/>
+ </presence>
+
+Juliet receives:
+ <iq type='get' id='disco' from="${Juliet's JID}">
+ <query node='http://code.matthewwild.co.uk/verse/#PDH7CGVPRERS2WUqBD18PHGEzaY=' xmlns='http://jabber.org/protocol/disco#info'/>
+ </iq>
+
+Juliet receives:
+ <presence from="${Juliet's full JID}">
+ <c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/>
+ </presence>
+
+Juliet sends:
+ <iq type='get' id='6'>
+ <query ver='' xmlns='jabber:iq:roster'/>
+ </iq>
+
+Juliet receives:
+ <iq type='result' id='6'>
+ <query ver='1' xmlns='jabber:iq:roster'/>
+ </iq>
+
+Romeo sends:
+ <iq type='result' id='disco' to='pep-test-df6zdvkv@localhost'><query xmlns='http://jabber.org/protocol/disco#info' node='http://code.matthewwild.co.uk/verse/#PDH7CGVPRERS2WUqBD18PHGEzaY='><identity type='pc' name='Verse' category='client'/><feature var='http://jabber.org/protocol/disco#info'/><feature var='http://jabber.org/protocol/disco#items'/><feature var='http://jabber.org/protocol/caps'/></query></iq>
+
+Romeo sends:
+ <presence type='subscribe' to="${Juliet's JID}"><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/></presence>
+
+Romeo receives:
+ <iq type='set' id='{scansion:any}'><query ver='1' xmlns='jabber:iq:roster'><item ask='subscribe' jid='pep-test-5k90xvps@localhost' subscription='none'/></query></iq>
+
+Romeo receives:
+ <presence type='unavailable' to='pep-test-df6zdvkv@localhost' from='pep-test-5k90xvps@localhost'/>
+
+Juliet receives:
+ <presence type='subscribe' from='pep-test-df6zdvkv@localhost' to='pep-test-5k90xvps@localhost'><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/></presence>
+
+Juliet sends:
+ <iq type='result' id='disco' to='pep-test-5k90xvps@localhost'><query xmlns='http://jabber.org/protocol/disco#info' node='http://code.matthewwild.co.uk/verse/#PDH7CGVPRERS2WUqBD18PHGEzaY='><identity type='pc' name='Verse' category='client'/><feature var='http://jabber.org/protocol/disco#info'/><feature var='http://jabber.org/protocol/disco#items'/><feature var='http://jabber.org/protocol/caps'/></query></iq>
+
+Juliet sends:
+ <presence type='subscribe' to="${Romeo's JID}"><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/></presence>
+
+Juliet receives:
+ <iq type='set' id='{scansion:any}'><query ver='2' xmlns='jabber:iq:roster'><item ask='subscribe' jid='pep-test-df6zdvkv@localhost' subscription='none'/></query></iq>
+
+Juliet receives:
+ <presence type='unavailable' to='pep-test-5k90xvps@localhost' from='pep-test-df6zdvkv@localhost'/>
+
+Romeo receives:
+ <presence type='subscribe' from='pep-test-5k90xvps@localhost' to='pep-test-df6zdvkv@localhost'><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/></presence>
+
+Romeo sends:
+ <iq type='result' id='fixme'/>
+
+Romeo sends:
+ <presence type='subscribed' to='pep-test-5k90xvps@localhost'><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/></presence>
+
+Romeo receives:
+ <iq type='set' id='{scansion:any}'><query ver='3' xmlns='jabber:iq:roster'><item ask='subscribe' jid='pep-test-5k90xvps@localhost' subscription='from'/></query></iq>
+
+Juliet receives:
+ <presence type='subscribed' from='pep-test-df6zdvkv@localhost' to='pep-test-5k90xvps@localhost'><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/></presence>
+
+Juliet receives:
+ <iq type='set' id='{scansion:any}'><query ver='3' xmlns='jabber:iq:roster'><item jid='pep-test-df6zdvkv@localhost' subscription='to'/></query></iq>
+
+Juliet receives:
+ <presence to='pep-test-5k90xvps@localhost' from="${Romeo's full JID}"><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/><delay xmlns='urn:xmpp:delay' stamp='{scansion:any}' from='localhost'/></presence>
+
+Juliet sends:
+ <presence type='subscribed' to='pep-test-df6zdvkv@localhost'><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/></presence>
+
+Juliet receives:
+ <iq type='set' id='{scansion:any}'><query ver='4' xmlns='jabber:iq:roster'><item jid='pep-test-df6zdvkv@localhost' subscription='both'/></query></iq>
+
+Juliet receives:
+ <presence to='pep-test-5k90xvps@localhost' from="${Romeo's full JID}"><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/><delay xmlns='urn:xmpp:delay' stamp='{scansion:any}' from='localhost'/></presence>
+
+Romeo receives:
+ <presence type='subscribed' from='pep-test-5k90xvps@localhost' to='pep-test-df6zdvkv@localhost'><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/></presence>
+
+Romeo receives:
+ <iq type='set' id='{scansion:any}'><query ver='4' xmlns='jabber:iq:roster'><item jid='pep-test-5k90xvps@localhost' subscription='both'/></query></iq>
+
+Romeo receives:
+ <presence to='pep-test-df6zdvkv@localhost' from="${Juliet's full JID}"><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/><delay xmlns='urn:xmpp:delay' stamp='{scansion:any}' from='localhost'/></presence>
+
+Juliet sends:
+ <iq type='result' id='fixme'/>
+
+Romeo sends:
+ <iq type='result' id='fixme'/>
+
+Romeo sends:
+ <iq type='result' id='fixme'/>
+
+Romeo sends:
+ <presence><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='m/sIsyfzKk8X1okZMtStR43nQQg=' node='http://code.matthewwild.co.uk/verse/'/></presence>
+
+Romeo receives:
+ <iq type='get' id='disco' from='pep-test-df6zdvkv@localhost'><query node='http://code.matthewwild.co.uk/verse/#m/sIsyfzKk8X1okZMtStR43nQQg=' xmlns='http://jabber.org/protocol/disco#info'/></iq>
+
+Romeo receives:
+ <presence from="${Romeo's full JID}"><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='m/sIsyfzKk8X1okZMtStR43nQQg=' node='http://code.matthewwild.co.uk/verse/'/></presence>
+
+Romeo receives:
+ <iq type='get' id='disco' from='pep-test-5k90xvps@localhost'><query node='http://code.matthewwild.co.uk/verse/#m/sIsyfzKk8X1okZMtStR43nQQg=' xmlns='http://jabber.org/protocol/disco#info'/></iq>
+
+Juliet receives:
+ <presence from="${Romeo's full JID}"><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='m/sIsyfzKk8X1okZMtStR43nQQg=' node='http://code.matthewwild.co.uk/verse/'/></presence>
+
+Romeo sends:
+ <presence><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='IfQwbaaDB4LEP5tkGArEaB/3Y+s=' node='http://code.matthewwild.co.uk/verse/'/></presence>
+
+Romeo receives:
+ <iq type='get' id='disco' from='pep-test-df6zdvkv@localhost'><query node='http://code.matthewwild.co.uk/verse/#IfQwbaaDB4LEP5tkGArEaB/3Y+s=' xmlns='http://jabber.org/protocol/disco#info'/></iq>
+
+Romeo receives:
+ <presence from="${Romeo's full JID}"><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='IfQwbaaDB4LEP5tkGArEaB/3Y+s=' node='http://code.matthewwild.co.uk/verse/'/></presence>
+
+Romeo receives:
+ <iq type='get' id='disco' from='pep-test-5k90xvps@localhost'><query node='http://code.matthewwild.co.uk/verse/#IfQwbaaDB4LEP5tkGArEaB/3Y+s=' xmlns='http://jabber.org/protocol/disco#info'/></iq>
+
+Romeo sends:
+ <iq type='result' id='disco' to='pep-test-df6zdvkv@localhost'><query xmlns='http://jabber.org/protocol/disco#info' node='http://code.matthewwild.co.uk/verse/#m/sIsyfzKk8X1okZMtStR43nQQg='/></iq>
+
+Romeo sends:
+ <iq type='result' id='disco' to='pep-test-5k90xvps@localhost'><query xmlns='http://jabber.org/protocol/disco#info' node='http://code.matthewwild.co.uk/verse/#m/sIsyfzKk8X1okZMtStR43nQQg='/></iq>
+
+Romeo sends:
+ <iq type='result' id='disco' to='pep-test-df6zdvkv@localhost'><query xmlns='http://jabber.org/protocol/disco#info' node='http://code.matthewwild.co.uk/verse/#IfQwbaaDB4LEP5tkGArEaB/3Y+s='><identity type='pc' name='Verse' category='client'/><feature var='http://jabber.org/protocol/tune+notify'/><feature var='http://jabber.org/protocol/disco#info'/><feature var='http://jabber.org/protocol/disco#items'/><feature var='http://jabber.org/protocol/caps'/><feature var='http://jabber.org/protocol/mood+notify'/></query></iq>
+
+Juliet receives:
+ <presence from="${Romeo's full JID}"><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='IfQwbaaDB4LEP5tkGArEaB/3Y+s=' node='http://code.matthewwild.co.uk/verse/'/></presence>
+
+Juliet sends:
+ <iq type='result' id='fixme'/>
+
+Juliet sends:
+ <iq type='set' id='7'><pubsub xmlns='http://jabber.org/protocol/pubsub'><publish node='http://jabber.org/protocol/tune'><item id='current' publisher="${Juliet's JID}"><tune xmlns='http://jabber.org/protocol/tune'><title>Beautiful Cedars</title><artist>The Spinners</artist><source>Not Quite Folk</source><track>4</track></tune></item></publish><publish-options><x type='submit' xmlns='jabber:x:data'><field type='hidden' var='FORM_TYPE'><value>http://jabber.org/protocol/pubsub#publish-options</value></field><field var='pubsub#persist_items'><value>true</value></field><field var='pubsub#itemreply'><value>publisher</value></field></x></publish-options></pubsub></iq>
+
+Juliet receives:
+ <iq type='result' id='7' ><pubsub xmlns='http://jabber.org/protocol/pubsub'><publish node='http://jabber.org/protocol/tune'><item id='current'/></publish></pubsub></iq>
+
+Romeo receives:
+ <message type='headline' from='pep-test-5k90xvps@localhost'><event xmlns='http://jabber.org/protocol/pubsub#event'><items node='http://jabber.org/protocol/tune'><item id='current' publisher="${Juliet's JID}"><tune xmlns='http://jabber.org/protocol/tune'><title>Beautiful Cedars</title><artist>The Spinners</artist><source>Not Quite Folk</source><track>4</track></tune></item></items></event></message>
+
+Romeo sends:
+ <iq type='result' id='disco' to='pep-test-5k90xvps@localhost'><query xmlns='http://jabber.org/protocol/disco#info' node='http://code.matthewwild.co.uk/verse/#IfQwbaaDB4LEP5tkGArEaB/3Y+s='><identity type='pc' name='Verse' category='client'/><feature var='http://jabber.org/protocol/tune+notify'/><feature var='http://jabber.org/protocol/disco#info'/><feature var='http://jabber.org/protocol/disco#items'/><feature var='http://jabber.org/protocol/caps'/><feature var='http://jabber.org/protocol/mood+notify'/></query></iq>
+
+Romeo receives:
+ <message type='headline' from='pep-test-5k90xvps@localhost'><event xmlns='http://jabber.org/protocol/pubsub#event'><items node='http://jabber.org/protocol/tune'><item id='current' publisher="${Juliet's JID}"><tune xmlns='http://jabber.org/protocol/tune'><title>Beautiful Cedars</title><artist>The Spinners</artist><source>Not Quite Folk</source><track>4</track></tune></item></items></event></message>
+
+Juliet disconnects
+
+Romeo disconnects
diff --git a/spec/scansion/pep_nickname.scs b/spec/scansion/pep_nickname.scs
index aaf53c87..1e39415b 100644
--- a/spec/scansion/pep_nickname.scs
+++ b/spec/scansion/pep_nickname.scs
@@ -58,7 +58,7 @@ Romeo receives:
<message type="headline" from="romeo@localhost">
<event xmlns="http://jabber.org/protocol/pubsub#event">
<items node="http://jabber.org/protocol/nick">
- <item id="current" publisher="${Romeo's JID}">
+ <item id="current">
<nickname xmlns="http://jabber.org/protocol/nick"/>
</item>
</items>
diff --git a/spec/scansion/pep_publish_subscribe.scs b/spec/scansion/pep_publish_subscribe.scs
index 6d33ffeb..e8080134 100644
--- a/spec/scansion/pep_publish_subscribe.scs
+++ b/spec/scansion/pep_publish_subscribe.scs
@@ -182,7 +182,7 @@ Juliet sends:
<iq type='result' id='fixme'/>
Juliet sends:
- <iq type='set' id='7'><pubsub xmlns='http://jabber.org/protocol/pubsub'><publish node='http://jabber.org/protocol/tune'><item id='current' publisher="${Juliet's JID}"><tune xmlns='http://jabber.org/protocol/tune'><title>Beautiful Cedars</title><artist>The Spinners</artist><source>Not Quite Folk</source><track>4</track></tune></item></publish></pubsub></iq>
+ <iq type='set' id='7'><pubsub xmlns='http://jabber.org/protocol/pubsub'><publish node='http://jabber.org/protocol/tune'><item id='current'><tune xmlns='http://jabber.org/protocol/tune'><title>Beautiful Cedars</title><artist>The Spinners</artist><source>Not Quite Folk</source><track>4</track></tune></item></publish></pubsub></iq>
Juliet receives:
<iq type='result' id='7' ><pubsub xmlns='http://jabber.org/protocol/pubsub'><publish node='http://jabber.org/protocol/tune'><item id='current'/></publish></pubsub></iq>
@@ -197,13 +197,13 @@ Juliet sends:
<iq type='result' id='{scansion:any}'/>
Romeo receives:
- <message type='headline' from='pep-test-tqvqu_pv@localhost'><event xmlns='http://jabber.org/protocol/pubsub#event'><items node='http://jabber.org/protocol/tune'><item id='current' publisher="${Juliet's JID}"><tune xmlns='http://jabber.org/protocol/tune'><title>Beautiful Cedars</title><artist>The Spinners</artist><source>Not Quite Folk</source><track>4</track></tune></item></items></event></message>
+ <message type='headline' from='pep-test-tqvqu_pv@localhost'><event xmlns='http://jabber.org/protocol/pubsub#event'><items node='http://jabber.org/protocol/tune'><item id='current'><tune xmlns='http://jabber.org/protocol/tune'><title>Beautiful Cedars</title><artist>The Spinners</artist><source>Not Quite Folk</source><track>4</track></tune></item></items></event></message>
Romeo sends:
<iq type='result' id='disco' to='pep-test-tqvqu_pv@localhost'><query xmlns='http://jabber.org/protocol/disco#info' node='http://code.matthewwild.co.uk/verse/#IfQwbaaDB4LEP5tkGArEaB/3Y+s='><identity type='pc' name='Verse' category='client'/><feature var='http://jabber.org/protocol/tune+notify'/><feature var='http://jabber.org/protocol/disco#info'/><feature var='http://jabber.org/protocol/disco#items'/><feature var='http://jabber.org/protocol/caps'/><feature var='http://jabber.org/protocol/mood+notify'/></query></iq>
Romeo receives:
- <message type='headline' from='pep-test-tqvqu_pv@localhost'><event xmlns='http://jabber.org/protocol/pubsub#event'><items node='http://jabber.org/protocol/tune'><item id='current' publisher="${Juliet's JID}"><tune xmlns='http://jabber.org/protocol/tune'><title>Beautiful Cedars</title><artist>The Spinners</artist><source>Not Quite Folk</source><track>4</track></tune></item></items></event></message>
+ <message type='headline' from='pep-test-tqvqu_pv@localhost'><event xmlns='http://jabber.org/protocol/pubsub#event'><items node='http://jabber.org/protocol/tune'><item id='current'><tune xmlns='http://jabber.org/protocol/tune'><title>Beautiful Cedars</title><artist>The Spinners</artist><source>Not Quite Folk</source><track>4</track></tune></item></items></event></message>
Juliet disconnects
diff --git a/spec/scansion/prosody.cfg.lua b/spec/scansion/prosody.cfg.lua
index 75f55ae0..58889fd7 100644
--- a/spec/scansion/prosody.cfg.lua
+++ b/spec/scansion/prosody.cfg.lua
@@ -1,20 +1,9 @@
--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" }
network_backend = ENV_PROSODY_NETWORK_BACKEND or "epoll"
-network_settings = require"util.json".decode(ENV_PROSODY_NETWORK_SETTINGS or "{}")
+network_settings = Lua.require"prosody.util.json".decode(ENV_PROSODY_NETWORK_SETTINGS or "{}")
modules_enabled = {
-- Generally required
@@ -66,6 +55,9 @@ modules_enabled = {
"tombstones";
"user_account_management";
+ -- Required for integration testing
+ "debug_reset";
+
-- Useful for testing
--"scansion_record"; -- Records things that happen in scansion test case format
}
diff --git a/spec/scansion/pubsub_config.scs b/spec/scansion/pubsub_config.scs
index d06d864e..a2b2ab68 100644
--- a/spec/scansion/pubsub_config.scs
+++ b/spec/scansion/pubsub_config.scs
@@ -74,6 +74,7 @@ Romeo receives:
</option>
<value>presence</value>
</field>
+ <field type="list-multi" var="pubsub#roster_groups_allowed" label="Roster groups allowed to subscribe"/>
<field var="pubsub#publish_model" label="Specify the publisher model" type="list-single">
<option label="publishers">
<value>publishers</value>
@@ -119,6 +120,18 @@ Romeo receives:
<field var="pubsub#notify_retract" label="Whether to notify subscribers when items are removed from the node" type="boolean">
<value>1</value>
</field>
+ <field label="Specify whose JID to include as the publisher of items" var="pubsub#itemreply" type="list-single">
+ <option label="Include the node owner's JID">
+ <value>owner</value>
+ </option>
+ <option label="Include the item publisher's JID">
+ <value>publisher</value>
+ </option>
+ <option label="Don't include any JID with items">
+ <value>none</value>
+ </option>
+ <value>none</value>
+ </field>
</x>
</configure>
</pubsub>
@@ -164,6 +177,7 @@ Romeo sends:
</option>
<value>presence</value>
</field>
+ <field type="list-multi" var="pubsub#roster_groups_allowed" label="Roster groups allowed to subscribe"/>
<field var="pubsub#publish_model" type="list-single" label="Specify the publisher model">
<option label="publishers">
<value>publishers</value>
diff --git a/spec/scansion/pubsub_max_items.scs b/spec/scansion/pubsub_max_items.scs
index c5525bd3..b0ead5bf 100644
--- a/spec/scansion/pubsub_max_items.scs
+++ b/spec/scansion/pubsub_max_items.scs
@@ -69,6 +69,7 @@ Alice receives:
</option>
<value>open</value>
</field>
+ <field type="list-multi" var="pubsub#roster_groups_allowed" label="Roster groups allowed to subscribe"/>
<field var="pubsub#publish_model" label="Specify the publisher model" type="list-single">
<option label="publishers">
<value>publishers</value>
@@ -114,6 +115,18 @@ Alice receives:
<field var="pubsub#notify_retract" label="Whether to notify subscribers when items are removed from the node" type="boolean">
<value>1</value>
</field>
+ <field label="Specify whose JID to include as the publisher of items" var="pubsub#itemreply" type="list-single">
+ <option label="Include the node owner's JID">
+ <value>owner</value>
+ </option>
+ <option label="Include the item publisher's JID">
+ <value>publisher</value>
+ </option>
+ <option label="Don't include any JID with items">
+ <value>none</value>
+ </option>
+ <value>none</value>
+ </field>
</x>
</configure>
</pubsub>
diff --git a/spec/scansion/pubsub_multi_items.scs b/spec/scansion/pubsub_multi_items.scs
index e43bc839..8c6872e4 100644
--- a/spec/scansion/pubsub_multi_items.scs
+++ b/spec/scansion/pubsub_multi_items.scs
@@ -69,6 +69,7 @@ Alice receives:
</option>
<value>open</value>
</field>
+ <field type="list-multi" var="pubsub#roster_groups_allowed" label="Roster groups allowed to subscribe"/>
<field var="pubsub#publish_model" label="Specify the publisher model" type="list-single">
<option label="publishers">
<value>publishers</value>
@@ -114,6 +115,18 @@ Alice receives:
<field var="pubsub#notify_retract" label="Whether to notify subscribers when items are removed from the node" type="boolean">
<value>1</value>
</field>
+ <field label="Specify whose JID to include as the publisher of items" var="pubsub#itemreply" type="list-single">
+ <option label="Include the node owner's JID">
+ <value>owner</value>
+ </option>
+ <option label="Include the item publisher's JID">
+ <value>publisher</value>
+ </option>
+ <option label="Don't include any JID with items">
+ <value>none</value>
+ </option>
+ <value>none</value>
+ </field>
</x>
</configure>
</pubsub>
diff --git a/spec/scansion/pubsub_preconditions.scs b/spec/scansion/pubsub_preconditions.scs
index 5c0c2569..fe056fc6 100644
--- a/spec/scansion/pubsub_preconditions.scs
+++ b/spec/scansion/pubsub_preconditions.scs
@@ -73,6 +73,7 @@ Romeo receives:
</option>
<value>presence</value>
</field>
+ <field type="list-multi" var="pubsub#roster_groups_allowed" label="Roster groups allowed to subscribe"/>
<field var="pubsub#publish_model" label="Specify the publisher model" type="list-single">
<option label="publishers">
<value>publishers</value>
@@ -118,6 +119,18 @@ Romeo receives:
<field var="pubsub#notify_retract" label="Whether to notify subscribers when items are removed from the node" type="boolean">
<value>1</value>
</field>
+ <field label="Specify whose JID to include as the publisher of items" var="pubsub#itemreply" type="list-single">
+ <option label="Include the node owner's JID">
+ <value>owner</value>
+ </option>
+ <option label="Include the item publisher's JID">
+ <value>publisher</value>
+ </option>
+ <option label="Don't include any JID with items">
+ <value>none</value>
+ </option>
+ <value>none</value>
+ </field>
</x>
</configure>
</pubsub>
@@ -163,6 +176,7 @@ Romeo sends:
</option>
<value>presence</value>
</field>
+ <field type="list-multi" var="pubsub#roster_groups_allowed" label="Roster groups allowed to subscribe"/>
<field var="pubsub#publish_model" type="list-single" label="Specify the publisher model">
<option label="publishers">
<value>publishers</value>
@@ -199,6 +213,9 @@ Romeo sends:
<field var="pubsub#notify_retract" type="boolean" label="Whether to notify subscribers when items are removed from the node">
<value>1</value>
</field>
+ <field var="pubsub#itemreply" type="boolean">
+ <value>none</value>
+ </field>
</x>
</configure>
</pubsub>
diff --git a/spec/scansion/uptime.scs b/spec/scansion/uptime.scs
index 188b9eb5..ed83e51b 100644
--- a/spec/scansion/uptime.scs
+++ b/spec/scansion/uptime.scs
@@ -15,7 +15,7 @@ Romeo sends:
Romeo receives:
<iq type='result' id='a' from='localhost'>
- <query xmlns='jabber:iq:last' seconds='0'/>
+ <query xmlns='jabber:iq:last' seconds='{scansion:any}'/>
</iq>
Romeo disconnects
diff --git a/spec/scansion/vcard_temp.scs b/spec/scansion/vcard_temp.scs
index 38c6f755..240ea1f3 100644
--- a/spec/scansion/vcard_temp.scs
+++ b/spec/scansion/vcard_temp.scs
@@ -51,8 +51,6 @@ Romeo receives:
</vCard>
</iq>
-Romeo disconnects
-
Juliet connects
Juliet sends:
@@ -77,4 +75,6 @@ Juliet receives:
Juliet disconnects
+Romeo disconnects
+
# recording ended on 2018-10-20T15:02:14Z
diff --git a/spec/util_argparse_spec.lua b/spec/util_argparse_spec.lua
index 0f2430b7..40f647c9 100644
--- a/spec/util_argparse_spec.lua
+++ b/spec/util_argparse_spec.lua
@@ -50,4 +50,9 @@ describe("parse", function()
assert.equal("-h", where, "returned where");
end);
+ it("supports array arguments", function ()
+ local opts, err = parse({ "--item"; "foo"; "--item"; "bar" }, { array_params = { item = true } });
+ assert.falsy(err);
+ assert.same({"foo","bar"}, opts.item);
+ end)
end);
diff --git a/spec/util_bitcompat_spec.lua b/spec/util_bitcompat_spec.lua
index 34a87f5b..99642821 100644
--- a/spec/util_bitcompat_spec.lua
+++ b/spec/util_bitcompat_spec.lua
@@ -24,4 +24,8 @@ describe("util.bitcompat", function ()
it("lshift works", function ()
assert.equal(0xFF00, bit.lshift(0xFF, 8));
end);
+
+ it("bnot works", function ()
+ assert.equal(0x0000FF00, bit.band(0xFFFFFFFF, bit.bnot(0xFFFF00FF)));
+ end);
end);
diff --git a/spec/util_cache_spec.lua b/spec/util_cache_spec.lua
index d57e25ac..ae7b1936 100644
--- a/spec/util_cache_spec.lua
+++ b/spec/util_cache_spec.lua
@@ -4,6 +4,20 @@ local cache = require "util.cache";
describe("util.cache", function()
describe("#new()", function()
it("should work", function()
+ do
+ local c = cache.new(1);
+ assert.is_not_nil(c);
+
+ assert.has_error(function ()
+ cache.new(0);
+ end);
+ assert.has_error(function ()
+ cache.new(-1);
+ end);
+ assert.has_error(function ()
+ cache.new("foo");
+ end);
+ end
local c = cache.new(5);
@@ -314,7 +328,7 @@ describe("util.cache", function()
end);
- (_VERSION=="Lua 5.1" and pending or it)(":table works", function ()
+ it(":table works", function ()
local t = cache.new(3):table();
assert.is.table(t);
t["a"] = "1";
@@ -336,5 +350,63 @@ describe("util.cache", function()
assert.spy(i).was_called_with("c", "3");
assert.spy(i).was_called_with("d", "4");
end);
+
+ local function vs(t)
+ local vs_ = {};
+ for v in t:values() do
+ vs_[#vs_+1] = v;
+ end
+ return vs_;
+ end
+
+ it(":values works", function ()
+ local t = cache.new(3);
+ t:set("k1", "v1");
+ t:set("k2", "v2");
+ assert.same({"v2", "v1"}, vs(t));
+ t:set("k3", "v3");
+ assert.same({"v3", "v2", "v1"}, vs(t));
+ t:set("k4", "v4");
+ assert.same({"v4", "v3", "v2"}, vs(t));
+ end);
+
+ it(":resize works", function ()
+ local c = cache.new(5);
+ for i = 1, 5 do
+ c:set(("k%d"):format(i), ("v%d"):format(i));
+ end
+ assert.same({"v5", "v4", "v3", "v2", "v1"}, vs(c));
+ assert.has_error(function ()
+ c:resize(-1);
+ end);
+ assert.has_error(function ()
+ c:resize(0);
+ end);
+ assert.has_error(function ()
+ c:resize("foo");
+ end);
+ c:resize(3);
+ assert.same({"v5", "v4", "v3"}, vs(c));
+ end);
+
+ it("eviction stuff", function ()
+ local c = cache.new(4, function(_k,_v,c)
+ if c.size < 10 then
+ c:resize(c.size*2);
+ end
+ end)
+ for i = 1,20 do
+ c:set(i,i)
+ end
+ assert.equal(16, c.size);
+ assert.is_nil(c:get(1))
+ assert.is_nil(c:get(4))
+ assert.equal(5, c:get(5))
+ assert.equal(20, c:get(20))
+ c:resize(4)
+ assert.equal(20, c:get(20))
+ assert.equal(17, c:get(17))
+ assert.is_nil(c:get(10))
+ end)
end);
end);
diff --git a/spec/util_crypto_spec.lua b/spec/util_crypto_spec.lua
new file mode 100644
index 00000000..4a62e0bc
--- /dev/null
+++ b/spec/util_crypto_spec.lua
@@ -0,0 +1,205 @@
+local test_keys = require "spec.inputs.test_keys";
+
+describe("util.crypto", function ()
+ local crypto = require "util.crypto";
+ local random = require "util.random";
+ local encodings = require "util.encodings";
+
+ describe("generate_ed25519_keypair", function ()
+ local keypair = crypto.generate_ed25519_keypair();
+ assert.is_not_nil(keypair);
+ assert.equal("ED25519", keypair:get_type());
+ end)
+
+ describe("generate_p256_keypair", function ()
+ local keypair = crypto.generate_p256_keypair();
+ assert.is_not_nil(keypair);
+ assert.equal("id-ecPublicKey", keypair:get_type());
+ end)
+
+ describe("export/import raw", function ()
+ local keypair = crypto.generate_p256_keypair();
+ assert.is_not_nil(keypair);
+ local raw = keypair:public_raw()
+ local imported = crypto.import_public_ec_raw(raw, "P-256")
+ assert.equal(keypair:public_pem(), imported:public_pem());
+ end)
+
+ describe("derive", function ()
+ local key = crypto.import_private_pem(test_keys.ecdsa_private_pem);
+ local peer_key = crypto.import_public_pem(test_keys.ecdsa_public_pem);
+ assert.equal("n1v4KeKmOVwjC67fiKtjJnqcEaasbpZa2fLPNHW51co=", encodings.base64.encode(key:derive(peer_key)))
+ end)
+
+ describe("import_private_pem", function ()
+ it("can import ECDSA keys", function ()
+ local ecdsa_key = crypto.import_private_pem(test_keys.ecdsa_private_pem);
+ assert.equal("id-ecPublicKey", ecdsa_key:get_type());
+ end);
+
+ it("can import EdDSA (Ed25519) keys", function ()
+ local ed25519_key = crypto.import_private_pem(crypto.generate_ed25519_keypair():private_pem());
+ assert.equal("ED25519", ed25519_key:get_type());
+ end);
+
+ it("can import RSA keys", function ()
+ -- TODO
+ end);
+
+ it("rejects invalid keys", function ()
+ assert.is_nil(crypto.import_private_pem(test_keys.eddsa_public_pem));
+ assert.is_nil(crypto.import_private_pem(test_keys.ecdsa_public_pem));
+ assert.is_nil(crypto.import_private_pem("foo"));
+ assert.is_nil(crypto.import_private_pem(""));
+ end);
+ end);
+
+ describe("import_public_pem", function ()
+ it("can import ECDSA public keys", function ()
+ local ecdsa_key = crypto.import_public_pem(test_keys.ecdsa_public_pem);
+ assert.equal("id-ecPublicKey", ecdsa_key:get_type());
+ end);
+
+ it("can import EdDSA (Ed25519) public keys", function ()
+ local ed25519_key = crypto.import_public_pem(test_keys.eddsa_public_pem);
+ assert.equal("ED25519", ed25519_key:get_type());
+ end);
+
+ it("can import RSA public keys", function ()
+ -- TODO
+ end);
+ end);
+
+ describe("PEM export", function ()
+ it("works", function ()
+ local ecdsa_key = crypto.import_public_pem(test_keys.ecdsa_public_pem);
+ assert.equal("id-ecPublicKey", ecdsa_key:get_type());
+ assert.equal(test_keys.ecdsa_public_pem, ecdsa_key:public_pem());
+
+ assert.has_error(function ()
+ -- Fails because private key is not available
+ ecdsa_key:private_pem();
+ end);
+
+ local ecdsa_private_key = crypto.import_private_pem(test_keys.ecdsa_private_pem);
+ assert.equal(test_keys.ecdsa_private_pem, ecdsa_private_key:private_pem());
+ end);
+ end);
+
+ describe("sign/verify with", function ()
+ local test_cases = {
+ ed25519 = {
+ crypto.ed25519_sign, crypto.ed25519_verify;
+ key = crypto.import_private_pem(test_keys.eddsa_private_pem);
+ sig_length = 64;
+ };
+ ecdsa = {
+ crypto.ecdsa_sha256_sign, crypto.ecdsa_sha256_verify;
+ key = crypto.import_private_pem(test_keys.ecdsa_private_pem);
+ };
+ };
+ for test_name, test in pairs(test_cases) do
+ local key = test.key;
+ describe(test_name, function ()
+ it("works", function ()
+ local sign, verify = test[1], test[2];
+ local sig = assert(sign(key, "Hello world"));
+ assert.is_string(sig);
+ if test.sig_length then
+ assert.equal(test.sig_length, #sig);
+ end
+
+ do
+ local ok = verify(key, "Hello world", sig);
+ assert.is_truthy(ok);
+ end
+ do -- Incorrect signature
+ local ok = verify(key, "Hello world", sig:sub(1, -2)..string.char((sig:byte(-1)+1)%255));
+ assert.is_falsy(ok);
+ end
+ do -- Incorrect message
+ local ok = verify(key, "Hello earth", sig);
+ assert.is_falsy(ok);
+ end
+ do -- Incorrect message (embedded NUL)
+ local ok = verify(key, "Hello world\0foo", sig);
+ assert.is_falsy(ok);
+ end
+ end);
+ end);
+ end
+ end);
+
+ describe("ECDSA signatures", function ()
+ local hex = require "util.hex";
+ local sig = hex.decode((([[
+ 304402203e936e7b0bc62887e0e9d675afd08531a930384cfcf301
+ f25d13053a2ebf141d02205a5a7c7b7ac5878d004cb79b17b39346
+ 6b0cd1043718ffc31c153b971d213a8e
+ ]]):gsub("%s+", "")));
+ it("can be parsed", function ()
+ local r, s = crypto.parse_ecdsa_signature(sig, 32);
+ assert.is_string(r);
+ assert.is_string(s);
+ assert.equal(32, #r);
+ assert.equal(32, #s);
+ end);
+ it("fails to parse invalid signatures", function ()
+ local invalid_sigs = {
+ "";
+ "\000";
+ string.rep("\000", 64);
+ string.rep("\000", 72);
+ string.rep("\000", 256);
+ string.rep("\255", 72);
+ string.rep("\255", 3);
+ };
+ for _, invalid_sig in ipairs(invalid_sigs) do
+ local r, s = crypto.parse_ecdsa_signature(invalid_sig, 32);
+ assert.is_nil(r);
+ assert.is_nil(s);
+ end
+ end);
+ it("can be built", function ()
+ local r, s = crypto.parse_ecdsa_signature(sig, 32);
+ local rebuilt_sig = crypto.build_ecdsa_signature(r, s);
+ assert.equal(sig, rebuilt_sig);
+ end);
+ end);
+
+ describe("AES-GCM encryption", function ()
+ it("works", function ()
+ local message = "foo\0bar";
+ local key_128_bit = random.bytes(16);
+ local key_256_bit = random.bytes(32);
+ local test_cases = {
+ { crypto.aes_128_gcm_encrypt, crypto.aes_128_gcm_decrypt, key = key_128_bit };
+ { crypto.aes_256_gcm_encrypt, crypto.aes_256_gcm_decrypt, key = key_256_bit };
+ };
+ for _, params in pairs(test_cases) do
+ local iv = params.iv or random.bytes(12);
+ local encrypted = params[1](params.key, iv, message);
+ assert.not_equal(message, encrypted);
+ local decrypted = params[2](params.key, iv, encrypted);
+ assert.equal(message, decrypted);
+ end
+ end);
+ end);
+
+ describe("AES-CTR encryption", function ()
+ it("works", function ()
+ local message = "foo\0bar hello world";
+ local key_256_bit = random.bytes(32);
+ local test_cases = {
+ { crypto.aes_256_ctr_decrypt, crypto.aes_256_ctr_decrypt, key = key_256_bit };
+ };
+ for _, params in pairs(test_cases) do
+ local iv = params.iv or random.bytes(16);
+ local encrypted = params[1](params.key, iv, message);
+ assert.not_equal(message, encrypted);
+ local decrypted = params[2](params.key, iv, encrypted);
+ assert.equal(message, decrypted);
+ end
+ end);
+ end);
+end);
diff --git a/spec/util_dataforms_spec.lua b/spec/util_dataforms_spec.lua
index 5293238a..ab402fdb 100644
--- a/spec/util_dataforms_spec.lua
+++ b/spec/util_dataforms_spec.lua
@@ -130,7 +130,7 @@ describe("util.dataforms", function ()
assert.truthy(st.is_stanza(xform));
assert.equal("x", xform.name);
assert.equal("jabber:x:data", xform.attr.xmlns);
- assert.equal("FORM_TYPE", xform:find("field@var"));
+ assert.equal("FORM_TYPE", xform:get_child_attr("field", nil, "var"));
assert.equal("xmpp:prosody.im/spec/util.dataforms#1", xform:find("field/value#"));
local allowed_direct_children = {
title = true,
diff --git a/spec/util_datamapper_spec.lua b/spec/util_datamapper_spec.lua
index 51ccf127..cc0e80e1 100644
--- a/spec/util_datamapper_spec.lua
+++ b/spec/util_datamapper_spec.lua
@@ -15,22 +15,22 @@ describe("util.datamapper", function()
setup(function()
-- a convenience function for simple attributes, there's a few of them
- local function attr() return {["$ref"]="#/$defs/attr"} end
+ local attr = {["$ref"]="#/$defs/attr"};
s = {
["$defs"] = { attr = { type = "string"; xml = { attribute = true } } };
type = "object";
xml = {name = "message"; namespace = "jabber:client"};
properties = {
- to = attr();
- from = attr();
- type = attr();
- id = attr();
+ to = attr;
+ from = attr;
+ type = attr;
+ id = attr;
body = true; -- should be assumed to be a 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}}};
+ properties = {stamp = attr; from = attr; reason = {type = "string"; xml = {text = true}}};
};
state = {
type = "string";
@@ -66,8 +66,8 @@ describe("util.datamapper", function()
xml = {name = "stanza-id"; namespace = "urn:xmpp:sid:0"};
type = "object";
properties = {
- id = attr();
- by = attr();
+ id = attr;
+ by = attr;
};
};
};
@@ -120,10 +120,10 @@ describe("util.datamapper", function()
namespace = "jabber:client"
};
properties = {
- to = attr();
- from = attr();
- type = attr();
- id = attr();
+ to = attr;
+ from = attr;
+ type = attr;
+ id = attr;
disco = {
type = "object";
xml = {
diff --git a/spec/util_datetime_spec.lua b/spec/util_datetime_spec.lua
index 497ab7d3..960e8aef 100644
--- a/spec/util_datetime_spec.lua
+++ b/spec/util_datetime_spec.lua
@@ -16,7 +16,10 @@ describe("util.datetime", function ()
assert.truthy(string.find(date(), "^%d%d%d%d%-%d%d%-%d%d$"));
end);
it("should work", function ()
- assert.equals(date(1136239445), "2006-01-02");
+ assert.equals("2006-01-02", date(1136239445));
+ end);
+ it("should ignore fractional parts", function ()
+ assert.equals("2006-01-02", date(1136239445.5));
end);
end);
describe("#time", function ()
@@ -32,8 +35,14 @@ describe("util.datetime", function ()
assert.truthy(string.find(time(), "^%d%d:%d%d:%d%d"));
end);
it("should work", function ()
- assert.equals(time(1136239445), "22:04:05");
- end);
+ assert.equals("22:04:05", time(1136239445));
+ end);
+ it("should handle precision", function ()
+ assert.equal("14:46:31.158200", time(1660488391.1582))
+ assert.equal("14:46:32.158200", time(1660488392.1582))
+ assert.equal("14:46:33.158200", time(1660488393.1582))
+ assert.equal("14:46:33.999900", time(1660488393.9999))
+ end)
end);
describe("#datetime", function ()
local datetime = util_datetime.datetime;
@@ -48,14 +57,23 @@ describe("util.datetime", function ()
assert.truthy(string.find(datetime(), "^%d%d%d%d%-%d%d%-%d%dT%d%d:%d%d:%d%d"));
end);
it("should work", function ()
- assert.equals(datetime(1136239445), "2006-01-02T22:04:05Z");
- end);
+ assert.equals("2006-01-02T22:04:05Z", datetime(1136239445));
+ end);
+ it("should handle precision", function ()
+ assert.equal("2022-08-14T14:46:31.158200Z", datetime(1660488391.1582))
+ assert.equal("2022-08-14T14:46:32.158200Z", datetime(1660488392.1582))
+ assert.equal("2022-08-14T14:46:33.158200Z", datetime(1660488393.1582))
+ assert.equal("2022-08-14T14:46:33.999900Z", datetime(1660488393.9999))
+ end)
end);
describe("#legacy", function ()
local legacy = util_datetime.legacy;
it("should exist", function ()
assert.is_function(legacy);
end);
+ it("should not add precision", function ()
+ assert.equal("20220814T14:46:31", legacy(1660488391.1582));
+ end);
end);
describe("#parse", function ()
local parse = util_datetime.parse;
@@ -64,13 +82,23 @@ describe("util.datetime", function ()
end);
it("should work", function ()
-- Timestamp used by Go
- assert.equals(parse("2017-11-19T17:58:13Z"), 1511114293);
- assert.equals(parse("2017-11-19T18:58:50+0100"), 1511114330);
- assert.equals(parse("2006-01-02T15:04:05-0700"), 1136239445);
+ assert.equals(1511114293, parse("2017-11-19T17:58:13Z"));
+ assert.equals(1511114330, parse("2017-11-19T18:58:50+0100"));
+ assert.equals(1136239445, parse("2006-01-02T15:04:05-0700"));
+ assert.equals(1136239445, parse("2006-01-02T15:04:05-07"));
end);
it("should handle timezones", function ()
-- https://xmpp.org/extensions/xep-0082.html#example-2 and 3
assert.equals(parse("1969-07-21T02:56:15Z"), parse("1969-07-20T21:56:15-05:00"));
end);
+ it("should handle precision", function ()
+ -- floating point comparison is not an exact science
+ assert.truthy(math.abs(1660488392.1582 - parse("2022-08-14T14:46:32.158200Z")) < 0.001)
+ end)
+ it("should return nil when given invalid inputs", function ()
+ assert.is_nil(parse(nil));
+ assert.is_nil(parse("hello world"));
+ assert.is_nil(parse("2017-11-19T18:58:50$0100"));
+ end);
end);
end);
diff --git a/spec/util_dbuffer_spec.lua b/spec/util_dbuffer_spec.lua
index 83ca1823..0072a59d 100644
--- a/spec/util_dbuffer_spec.lua
+++ b/spec/util_dbuffer_spec.lua
@@ -6,6 +6,8 @@ describe("util.dbuffer", function ()
end);
it("can be created", function ()
assert.truthy(dbuffer.new());
+ assert.truthy(dbuffer.new(1));
+ assert.truthy(dbuffer.new(1024));
end);
it("won't create an empty buffer", function ()
assert.falsy(dbuffer.new(0));
@@ -15,10 +17,21 @@ describe("util.dbuffer", function ()
end);
end);
describe(":write", function ()
- local b = dbuffer.new();
+ local b = dbuffer.new(10, 3);
it("works", function ()
assert.truthy(b:write("hi"));
end);
+ it("fails when the buffer is full", function ()
+ local ret = b:write(" there world, this is a long piece of data");
+ assert.is_falsy(ret);
+ end);
+ it("works when max_chunks is reached", function ()
+ -- Chunks are an optimization, dbuffer should collapse chunks when needed
+ for _ = 1, 8 do
+ assert.truthy(b:write("!"));
+ end
+ assert.falsy(b:write("!")); -- Length reached
+ end);
end);
describe(":read", function ()
@@ -34,6 +47,14 @@ describe("util.dbuffer", function ()
assert.equal(" ", b:read());
assert.equal("world", b:read());
end);
+ it("fails when there is not enough data in the buffer", function ()
+ local b = dbuffer.new(12);
+ b:write("hello");
+ b:write(" ");
+ b:write("world");
+ assert.is_falsy(b:read(12));
+ assert.is_falsy(b:read(13));
+ end);
end);
describe(":read_until", function ()
@@ -68,9 +89,46 @@ describe("util.dbuffer", function ()
assert.equal(5, b:len());
assert.equal("world", b:read(5));
end);
+ it("works across chunks", function ()
+ assert.truthy(b:write("hello"));
+ assert.truthy(b:write(" "));
+ assert.truthy(b:write("world"));
+ assert.truthy(b:discard(3));
+ assert.equal(8, b:length());
+ assert.truthy(b:discard(3));
+ assert.equal(5, b:length());
+ assert.equal("world", b:read(5));
+ end);
+ it("can discard the entire buffer", function ()
+ assert.equal(b:len(), 0);
+ assert.truthy(b:write("hello world"));
+ assert.truthy(b:discard(11));
+ assert.equal(0, b:len());
+ assert.truthy(b:write("hello world"));
+ assert.truthy(b:discard(12));
+ assert.equal(0, b:len());
+ assert.truthy(b:write("hello world"));
+ assert.truthy(b:discard(128));
+ assert.equal(0, b:len());
+ end);
+ it("works on an empty buffer", function ()
+ assert.truthy(dbuffer.new():discard());
+ assert.truthy(dbuffer.new():discard(0));
+ assert.truthy(dbuffer.new():discard(1));
+ end);
end);
describe(":collapse()", function ()
+ it("works", function ()
+ local b = dbuffer.new();
+ b:write("hello");
+ b:write(" ");
+ b:write("world");
+ b:collapse(6);
+ local ret, bytes = b:read_chunk();
+ assert.equal("hello ", ret);
+ assert.equal(6, bytes);
+ end);
it("works on an empty buffer", function ()
local b = dbuffer.new();
b:collapse();
@@ -115,6 +173,11 @@ describe("util.dbuffer", function ()
end
end
end);
+
+ it("works on an empty buffer", function ()
+ local b = dbuffer.new();
+ assert.equal("", b:sub(1, 12));
+ end);
end);
describe(":byte", function ()
@@ -122,7 +185,11 @@ describe("util.dbuffer", function ()
local s = "hello world"
local function test_byte(b, x, y)
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"));
+ assert.same(
+ string_result,
+ buffer_result,
+ ("buffer:byte(%s, %s) does not match string:byte()"):format(x and ("%d"):format(x) or "nil", y and ("%d"):format(y) or "nil")
+ );
end
it("is equivalent to string:byte", function ()
@@ -132,6 +199,7 @@ describe("util.dbuffer", function ()
test_byte(b, 3);
test_byte(b, -1);
test_byte(b, -3);
+ test_byte(b, nil, 5);
for i = -13, 13 do
for j = -13, 13 do
test_byte(b, i, j);
diff --git a/spec/util_error_spec.lua b/spec/util_error_spec.lua
index be176635..cd30ee94 100644
--- a/spec/util_error_spec.lua
+++ b/spec/util_error_spec.lua
@@ -29,10 +29,10 @@ describe("util.error", function ()
end);
- describe("is_err()", function ()
+ describe("is_error()", function ()
it("works", function ()
- assert.truthy(errors.is_err(errors.new()));
- assert.falsy(errors.is_err("not an error"));
+ assert.truthy(errors.is_error(errors.new()));
+ assert.falsy(errors.is_error("not an error"));
end);
end);
@@ -40,7 +40,7 @@ describe("util.error", function ()
it("works", function ()
local ok, err = errors.coerce(nil, "it dun goofed");
assert.is_nil(ok);
- assert.truthy(errors.is_err(err))
+ assert.truthy(errors.is_error(err))
end);
end);
@@ -50,12 +50,15 @@ describe("util.error", function ()
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.truthy(errors.is_error(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);
+ assert.not_has_error(function ()
+ errors.from_stanza(st.message())
+ end);
end);
end);
@@ -184,7 +187,7 @@ describe("util.error", function ()
end
local ok, err = reg.coerce(test());
assert.is_nil(ok);
- assert.is_truthy(errors.is_err(err));
+ assert.is_truthy(errors.is_error(err));
assert.equal("forbidden", err.condition);
end);
@@ -205,7 +208,7 @@ describe("util.error", function ()
end
local ok, err = reg.coerce(test());
assert.is_nil(ok);
- assert.is_truthy(errors.is_err(err));
+ assert.is_truthy(errors.is_error(err));
assert.equal("internal-server-error", err.condition);
assert.equal("Oh no", err.text);
end);
diff --git a/spec/util_format_spec.lua b/spec/util_format_spec.lua
index cb473b47..5ea0bdcf 100644
--- a/spec/util_format_spec.lua
+++ b/spec/util_format_spec.lua
@@ -13,7 +13,7 @@ describe("util.format", function()
assert.equal("true", format("%s", true));
assert.equal("[true]", format("%d", true));
assert.equal("% [true]", format("%%", true));
- assert.equal("{ }", format("%q", { }));
+ assert.equal("{}", format("%q", {}));
assert.equal("[1.5]", format("%d", 1.5));
assert.equal("[7.3786976294838e+19]", format("%d", 73786976294838206464));
end);
@@ -333,29 +333,27 @@ describe("util.format", function()
end);
end);
- if _VERSION > "Lua 5.1" then -- COMPAT no %a or %A in Lua 5.1
- describe("to %a", function ()
- it("works", function ()
- assert.equal("0x1.84p+6", format("%a", 97))
- assert.equal("-0x1.81c8p+13", format("%a", -12345))
- assert.equal("0x1.8p+0", format("%a", 1.5))
- assert.equal("0x1p+66", format("%a", 73786976294838206464))
- assert.equal("inf", format("%a", math.huge))
- assert.equal("0x1.fffffffcp+30", format("%a", 2147483647))
- end);
+ describe("to %a", function ()
+ it("works", function ()
+ assert.equal("0x1.84p+6", format("%a", 97))
+ assert.equal("-0x1.81c8p+13", format("%a", -12345))
+ assert.equal("0x1.8p+0", format("%a", 1.5))
+ assert.equal("0x1p+66", format("%a", 73786976294838206464))
+ assert.equal("inf", format("%a", math.huge))
+ assert.equal("0x1.fffffffcp+30", format("%a", 2147483647))
end);
+ end);
- describe("to %A", function ()
- it("works", function ()
- assert.equal("0X1.84P+6", format("%A", 97))
- assert.equal("-0X1.81C8P+13", format("%A", -12345))
- assert.equal("0X1.8P+0", format("%A", 1.5))
- assert.equal("0X1P+66", format("%A", 73786976294838206464))
- assert.equal("INF", format("%A", math.huge))
- assert.equal("0X1.FFFFFFFCP+30", format("%A", 2147483647))
- end);
+ describe("to %A", function ()
+ it("works", function ()
+ assert.equal("0X1.84P+6", format("%A", 97))
+ assert.equal("-0X1.81C8P+13", format("%A", -12345))
+ assert.equal("0X1.8P+0", format("%A", 1.5))
+ assert.equal("0X1P+66", format("%A", 73786976294838206464))
+ assert.equal("INF", format("%A", math.huge))
+ assert.equal("0X1.FFFFFFFCP+30", format("%A", 2147483647))
end);
- end
+ end);
describe("to %e", function ()
it("works", function ()
@@ -670,7 +668,7 @@ describe("util.format", function()
describe("to %q", function ()
it("works", function ()
- assert.matches('{__type="function",__error="fail"}', format("%q", function() end))
+ assert.matches('%[%[function: 0[xX]%x+]]', format("%q", function() end))
end);
end);
@@ -769,7 +767,7 @@ describe("util.format", function()
describe("to %q", function ()
it("works", function ()
- assert.matches('{__type="thread",__error="fail"}', format("%q", coroutine.create(function() end)))
+ assert.matches('_%[%[thread: 0[xX]%x+]]', format("%q", coroutine.create(function() end)))
end);
end);
@@ -882,8 +880,8 @@ describe("util.format", function()
describe("to %q", function ()
it("works", function ()
- assert.matches("{ }", format("%q", { }))
- assert.equal("{ }", format("%q", setmetatable({},{__tostring=function ()return "foo \1\2\3 bar"end})))
+ assert.matches("{}", format("%q", { }))
+ assert.equal("{}", format("%q", setmetatable({},{__tostring=function ()return "foo \1\2\3 bar"end})))
end);
end);
diff --git a/spec/util_fsm_spec.lua b/spec/util_fsm_spec.lua
new file mode 100644
index 00000000..9aff7ca6
--- /dev/null
+++ b/spec/util_fsm_spec.lua
@@ -0,0 +1,250 @@
+describe("util.fsm", function ()
+ local new_fsm = require "util.fsm".new;
+
+ do
+ local fsm = new_fsm({
+ transitions = {
+ { name = "melt", from = "solid", to = "liquid" };
+ { name = "freeze", from = "liquid", to = "solid" };
+ };
+ });
+
+ it("works", function ()
+ local water = fsm:init("liquid");
+ water:freeze();
+ assert.equal("solid", water.state);
+ water:melt();
+ assert.equal("liquid", water.state);
+ end);
+
+ it("does not allow invalid transitions", function ()
+ local water = fsm:init("liquid");
+ assert.has_errors(function ()
+ water:melt();
+ end, "Invalid state transition: liquid cannot melt");
+
+ water:freeze();
+ assert.equal("solid", water.state);
+
+ water:melt();
+ assert.equal("liquid", water.state);
+
+ assert.has_errors(function ()
+ water:melt();
+ end, "Invalid state transition: liquid cannot melt");
+ end);
+ end
+
+ it("notifies observers", function ()
+ local n = 0;
+ local has_become_solid = spy.new(function (transition)
+ assert.is_table(transition);
+ assert.equal("solid", transition.to);
+ assert.is_not_nil(transition.instance);
+ n = n + 1;
+ if n == 1 then
+ assert.is_nil(transition.from);
+ assert.is_nil(transition.from_attr);
+ elseif n == 2 then
+ assert.equal("liquid", transition.from);
+ assert.is_nil(transition.from_attr);
+ assert.equal("freeze", transition.name);
+ end
+ end);
+ local is_melting = spy.new(function (transition)
+ assert.is_table(transition);
+ assert.equal("melt", transition.name);
+ assert.is_not_nil(transition.instance);
+ end);
+ local fsm = new_fsm({
+ transitions = {
+ { name = "melt", from = "solid", to = "liquid" };
+ { name = "freeze", from = "liquid", to = "solid" };
+ };
+ state_handlers = {
+ solid = has_become_solid;
+ };
+
+ transition_handlers = {
+ melt = is_melting;
+ };
+ });
+
+ local water = fsm:init("liquid");
+ assert.spy(has_become_solid).was_not_called();
+
+ local ice = fsm:init("solid"); --luacheck: ignore 211/ice
+ assert.spy(has_become_solid).was_called(1);
+
+ water:freeze();
+
+ assert.spy(is_melting).was_not_called();
+ water:melt();
+ assert.spy(is_melting).was_called(1);
+ end);
+
+ local function test_machine(fsm_spec, expected_transitions, test_func)
+ fsm_spec.handlers = fsm_spec.handlers or {};
+ fsm_spec.handlers.transitioned = function (transition)
+ local expected_transition = table.remove(expected_transitions, 1);
+ assert.same(expected_transition, {
+ name = transition.name;
+ to = transition.to;
+ to_attr = transition.to_attr;
+ from = transition.from;
+ from_attr = transition.from_attr;
+ });
+ end;
+ local fsm = new_fsm(fsm_spec);
+ test_func(fsm);
+ assert.equal(0, #expected_transitions);
+ end
+
+
+ it("handles transitions with the same name", function ()
+ local expected_transitions = {
+ { name = nil , from = "none", to = "A" };
+ { name = "step", from = "A", to = "B" };
+ { name = "step", from = "B", to = "C" };
+ { name = "step", from = "C", to = "D" };
+ };
+
+ test_machine({
+ default_state = "none";
+ transitions = {
+ { name = "step", from = "A", to = "B" };
+ { name = "step", from = "B", to = "C" };
+ { name = "step", from = "C", to = "D" };
+ };
+ }, expected_transitions, function (fsm)
+ local i = fsm:init("A");
+ i:step(); -- B
+ i:step(); -- C
+ i:step(); -- D
+ assert.has_errors(function ()
+ i:step();
+ end, "Invalid state transition: D cannot step");
+ end);
+ end);
+
+ it("handles supports wildcard transitions", function ()
+ local expected_transitions = {
+ { name = nil , from = "none", to = "A" };
+ { name = "step", from = "A", to = "B" };
+ { name = "step", from = "B", to = "C" };
+ { name = "reset", from = "C", to = "A" };
+ { name = "step", from = "A", to = "B" };
+ { name = "step", from = "B", to = "C" };
+ { name = "step", from = "C", to = "D" };
+ };
+
+ test_machine({
+ default_state = "none";
+ transitions = {
+ { name = "step", from = "A", to = "B" };
+ { name = "step", from = "B", to = "C" };
+ { name = "step", from = "C", to = "D" };
+ { name = "reset", from = "*", to = "A" };
+ };
+ }, expected_transitions, function (fsm)
+ local i = fsm:init("A");
+ i:step(); -- B
+ i:step(); -- C
+ i:reset(); -- A
+ i:step(); -- B
+ i:step(); -- C
+ i:step(); -- D
+ assert.has_errors(function ()
+ i:step();
+ end, "Invalid state transition: D cannot step");
+ end);
+ end);
+
+ it("supports specifying multiple from states", function ()
+ local expected_transitions = {
+ { name = nil , from = "none", to = "A" };
+ { name = "step", from = "A", to = "B" };
+ { name = "step", from = "B", to = "C" };
+ { name = "reset", from = "C", to = "A" };
+ { name = "step", from = "A", to = "B" };
+ { name = "step", from = "B", to = "C" };
+ { name = "step", from = "C", to = "D" };
+ };
+
+ test_machine({
+ default_state = "none";
+ transitions = {
+ { name = "step", from = "A", to = "B" };
+ { name = "step", from = "B", to = "C" };
+ { name = "step", from = "C", to = "D" };
+ { name = "reset", from = {"B", "C", "D"}, to = "A" };
+ };
+ }, expected_transitions, function (fsm)
+ local i = fsm:init("A");
+ i:step(); -- B
+ i:step(); -- C
+ i:reset(); -- A
+ assert.has_errors(function ()
+ i:reset();
+ end, "Invalid state transition: A cannot reset");
+ i:step(); -- B
+ i:step(); -- C
+ i:step(); -- D
+ assert.has_errors(function ()
+ i:step();
+ end, "Invalid state transition: D cannot step");
+ end);
+ end);
+
+ it("handles transitions with the same start/end state", function ()
+ local expected_transitions = {
+ { name = nil , from = "none", to = "A" };
+ { name = "step", from = "A", to = "B" };
+ { name = "step", from = "B", to = "B" };
+ { name = "step", from = "B", to = "B" };
+ };
+
+ test_machine({
+ default_state = "none";
+ transitions = {
+ { name = "step", from = "A", to = "B" };
+ { name = "step", from = "B", to = "B" };
+ };
+ }, expected_transitions, function (fsm)
+ local i = fsm:init("A");
+ i:step(); -- B
+ i:step(); -- B
+ i:step(); -- B
+ end);
+ end);
+
+ it("can identify instances of a specific fsm", function ()
+ local fsm1 = new_fsm({ default_state = "a" });
+ local fsm2 = new_fsm({ default_state = "a" });
+
+ local i1 = fsm1:init();
+ local i2 = fsm2:init();
+
+ assert.truthy(fsm1:is_instance(i1));
+ assert.truthy(fsm2:is_instance(i2));
+
+ assert.falsy(fsm1:is_instance(i2));
+ assert.falsy(fsm2:is_instance(i1));
+ end);
+
+ it("errors when an invalid initial state is passed", function ()
+ local fsm1 = new_fsm({
+ transitions = {
+ { name = "", from = "A", to = "B" };
+ };
+ });
+
+ assert.has_no_errors(function ()
+ fsm1:init("A");
+ end);
+
+ assert.has_errors(function ()
+ fsm1:init("C");
+ end);
+ end);
+end);
diff --git a/spec/util_hashes_spec.lua b/spec/util_hashes_spec.lua
index 51a4a79c..c4b1841e 100644
--- a/spec/util_hashes_spec.lua
+++ b/spec/util_hashes_spec.lua
@@ -4,6 +4,8 @@ local hex = require "util.hex";
-- Also see spec for util.hmac where HMAC test cases reside
+--luacheck: ignore 631
+
describe("PBKDF2-HMAC-SHA1", function ()
it("test vector 1", function ()
local P = "password"
@@ -53,3 +55,56 @@ describe("PBKDF2-HMAC-SHA256", function ()
end);
+describe("SHA-3", function ()
+ describe("256", function ()
+ it("works", function ()
+ local expected = "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a"
+ assert.equal(expected, hashes.sha3_256("", true));
+ end);
+ end);
+ describe("512", function ()
+ it("works", function ()
+ local expected = "a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26"
+ assert.equal(expected, hashes.sha3_512("", true));
+ end);
+ end);
+end);
+
+describe("HKDF", function ()
+ describe("HMAC-SHA256", function ()
+ describe("RFC 5869", function ()
+ it("test vector A.1", function ()
+ local ikm = hex.decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
+ local salt = hex.decode("000102030405060708090a0b0c");
+ local info = hex.decode("f0f1f2f3f4f5f6f7f8f9");
+
+ local expected = "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865";
+
+ local ret = hashes.hkdf_hmac_sha256(42, ikm, salt, info);
+ assert.equal(expected, hex.encode(ret));
+ end);
+
+ it("test vector A.2", function ()
+ local ikm = hex.decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f");
+ local salt = hex.decode("606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf");
+ local info = hex.decode("b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff");
+
+ local expected = "b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87";
+
+ local ret = hashes.hkdf_hmac_sha256(82, ikm, salt, info);
+ assert.equal(expected, hex.encode(ret));
+ end);
+
+ it("test vector A.3", function ()
+ local ikm = hex.decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
+ local salt = "";
+ local info = "";
+
+ local expected = "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8";
+
+ local ret = hashes.hkdf_hmac_sha256(42, ikm, salt, info);
+ assert.equal(expected, hex.encode(ret));
+ end);
+ end);
+ end);
+end);
diff --git a/spec/util_hashring_spec.lua b/spec/util_hashring_spec.lua
index d8801774..4f6ec3a3 100644
--- a/spec/util_hashring_spec.lua
+++ b/spec/util_hashring_spec.lua
@@ -1,6 +1,7 @@
local hashring = require "util.hashring";
describe("util.hashring", function ()
+ randomize(false);
local sha256 = require "util.hashes".sha256;
@@ -82,4 +83,11 @@ describe("util.hashring", function ()
end
end);
+ it("should support values associated with nodes", function ()
+ local r = hashring.new(128, sha256);
+ r:add_node("node1", { a = 1 });
+ local node, value = r:get_node("foo");
+ assert.is_equal("node1", node);
+ assert.same({ a = 1 }, value);
+ end);
end);
diff --git a/spec/util_http_spec.lua b/spec/util_http_spec.lua
index c6087450..3e2f5a6a 100644
--- a/spec/util_http_spec.lua
+++ b/spec/util_http_spec.lua
@@ -41,6 +41,7 @@ describe("util.http", function()
end);
it("should encode special characters with escaping", function()
+ -- luacheck: ignore 631
assert.are.equal(http.formencode({ { name = "one two", value = "1"}, { name = "two one&", value = "2" } }), "one+two=1&two+one%26=2", "Form encoded");
end);
end);
@@ -108,4 +109,25 @@ describe("util.http", function()
assert.is_(http.contains_token("fo o", "foo"));
end);
end);
+
+do
+ describe("parse_forwarded", function()
+ it("works", function()
+ assert.same({ { ["for"] = "[2001:db8:cafe::17]:4711" } }, http.parse_forwarded('For="[2001:db8:cafe::17]:4711"'), "case insensitive");
+
+ assert.same({ { ["for"] = "192.0.2.60"; proto = "http"; by = "203.0.113.43" } }, http.parse_forwarded('for=192.0.2.60;proto=http;by=203.0.113.43'),
+ "separated by semicolon");
+
+ assert.same({ { ["for"] = "192.0.2.43" }; { ["for"] = "198.51.100.17" } }, http.parse_forwarded('for=192.0.2.43, for=198.51.100.17'),
+ "Values from multiple proxy servers can be appended using a comma");
+
+ end)
+ it("rejects quoted quotes", function ()
+ assert.falsy(http.parse_forwarded('foo="bar\"bar'), "quoted quotes");
+ end)
+ pending("deals with quoted quotes", function ()
+ assert.same({ { foo = 'bar"baz' } }, http.parse_forwarded('foo="bar\"bar'), "quoted quotes");
+ end)
+ end)
+end
end);
diff --git a/spec/util_human_io_spec.lua b/spec/util_human_io_spec.lua
index f1b28883..91a87b39 100644
--- a/spec/util_human_io_spec.lua
+++ b/spec/util_human_io_spec.lua
@@ -42,6 +42,64 @@ describe("util.human.io", function ()
assert.equal("räksmörgås", human_io.ellipsis("räksmörgås", 10));
end);
end);
+
+ describe("parse_duration", function ()
+ local function test(expected, duration)
+ return assert.equal(expected, human_io.parse_duration(duration), ("%q -> %d"):format(duration, expected));
+ end
+ local function should_fail(duration)
+ assert.is_nil(human_io.parse_duration(duration), "invalid duration should fail: %q");
+ end
+ it("works", function ()
+ test(1, "1s");
+ test(60, "1min");
+ test(60, "1 min");
+ test(60, "1 minute");
+ test(120, "2min");
+ test(7200, "2h");
+ test(7200, "2 hours");
+ test(86400, "1d");
+ test(604800, "1w");
+ test(604800, "1week");
+ test(1814400, "3 weeks");
+ test(2678400, "1month");
+ test(2678400, "1 month");
+ test(31536000, "365 days");
+ test(31556952, "1 year");
+
+ should_fail("two weeks");
+ should_fail("1m");
+ should_fail("1mi");
+ should_fail("1mo");
+ end);
+ end);
+
+ describe("parse_duration_lax", function ()
+ local function test(expected, duration)
+ return assert.equal(expected, human_io.parse_duration_lax(duration), ("%q -> %d"):format(duration, expected));
+ end
+ it("works", function ()
+ test(1, "1s");
+ test(60, "1mi");
+ test(60, "1min");
+ test(60, "1 min");
+ test(60, "1 minute");
+ test(120, "2min");
+ test(7200, "2h");
+ test(7200, "2 hours");
+ test(86400, "1d");
+ test(604800, "1w");
+ test(604800, "1week");
+ test(1814400, "3 weeks");
+ test(2678400, "1m");
+ test(2678400, "1mo");
+ test(2678400, "1month");
+ test(2678400, "1 month");
+ test(31536000, "365 days");
+ test(31556952, "1 year");
+ return assert.is_nil(human_io.parse_duration_lax("two weeks"), "\"2 weeks\" -> nil");
+ end);
+ end);
end);
diff --git a/spec/util_ip_spec.lua b/spec/util_ip_spec.lua
index be5e4cff..a0287ee7 100644
--- a/spec/util_ip_spec.lua
+++ b/spec/util_ip_spec.lua
@@ -36,6 +36,8 @@ describe("util.ip", function()
assert.are.equal(match(_"8.8.8.8", _"8.8.0.0", 16), true);
assert.are.equal(match(_"8.8.4.4", _"8.8.0.0", 16), true);
+
+ assert.are.equal(match(_"fe80::1", _"fec0::", 10), false);
end);
end);
@@ -98,6 +100,30 @@ describe("util.ip", function()
assert_cpl6("abcd::1", "abcd::1", 128);
assert_cpl6("abcd::abcd", "abcd::", 112);
assert_cpl6("abcd::abcd", "abcd::abcd:abcd", 96);
+
+ assert_cpl6("fe80::1", "fec0::", 9);
+ end);
+ end);
+
+ describe("#truncate()", function ()
+ it("should work for IPv4", function ()
+ local ip1 = ip.new_ip("192.168.0.1");
+ local ip2 = ip.truncate(ip1, 16);
+ assert.truthy(ip.is_ip(ip2));
+ assert.equal("192.168.0.0", ip2.normal);
+ assert.equal("192.168.0.1", ip1.normal); -- original unmodified
+ end);
+
+ it("should work for IPv6", function ()
+ local ip1 = ip.new_ip("2001:db8::ff00:42:8329");
+ local ip2 = ip.truncate(ip1, 24);
+ assert.truthy(ip.is_ip(ip2));
+ assert.equal("2001:d00::", ip2.normal);
+ assert.equal("2001:db8::ff00:42:8329", ip1.normal); -- original unmodified
+ end);
+
+ it("accepts a string", function ()
+ assert.equal("127.0.0.0", ip.truncate("127.0.0.1", 8).normal);
end);
end);
end);
diff --git a/spec/util_iterators_spec.lua b/spec/util_iterators_spec.lua
index 4cf6f19d..a4b85884 100644
--- a/spec/util_iterators_spec.lua
+++ b/spec/util_iterators_spec.lua
@@ -10,6 +10,14 @@ describe("util.iterators", function ()
end
assert.same(output, expect);
end);
+ it("should work with only a single iterator", function ()
+ local expect = { "a", "b", "c" };
+ local output = {};
+ for x in iter.join(iter.values({"a", "b", "c"})) do
+ table.insert(output, x);
+ end
+ assert.same(output, expect);
+ end);
end);
describe("sorted_pairs", function ()
diff --git a/spec/util_jid_spec.lua b/spec/util_jid_spec.lua
index b92ca06c..4f5fe356 100644
--- a/spec/util_jid_spec.lua
+++ b/spec/util_jid_spec.lua
@@ -48,6 +48,47 @@ describe("util.jid", function()
end)
end);
+ describe("#prepped_split()", function()
+ local function test(input_jid, expected_node, expected_server, expected_resource)
+ local rnode, rserver, rresource = jid.prepped_split(input_jid);
+ assert.are.equal(expected_node, rnode, "split("..tostring(input_jid)..") failed");
+ assert.are.equal(expected_server, rserver, "split("..tostring(input_jid)..") failed");
+ assert.are.equal(expected_resource, rresource, "split("..tostring(input_jid)..") failed");
+ end
+
+ it("should work", function()
+ -- Valid JIDs
+ test("node@server", "node", "server", nil );
+ test("node@server/resource", "node", "server", "resource" );
+ test("server", nil, "server", nil );
+ test("server/resource", nil, "server", "resource" );
+ test("server/resource@foo", nil, "server", "resource@foo" );
+ test("server/resource@foo/bar", nil, "server", "resource@foo/bar");
+
+ -- Always invalid JIDs
+ test(nil, nil, nil, nil);
+ test("node@/server", nil, nil, nil);
+ test("@server", nil, nil, nil);
+ test("@server/resource", nil, nil, nil);
+ test("@/resource", nil, nil, nil);
+ test("@server/", nil, nil, nil);
+ test("server/", nil, nil, nil);
+ test("/resource", nil, nil, nil);
+ end);
+ it("should reject invalid arguments", function ()
+ assert.has_error(function () jid.prepped_split(false) end)
+ end)
+ it("should strip empty root label", function ()
+ test("node@server.", "node", "server", nil);
+ end);
+ it("should fail for JIDs that fail stringprep", function ()
+ test("node@invalid-\128-server", nil, nil, nil);
+ test("node@invalid-\194\128-server", nil, nil, nil);
+ test("<invalid node>@server", nil, nil, nil);
+ test("node@server/invalid-\000-resource", nil, nil, nil);
+ end);
+ end);
+
describe("#bare()", function()
it("should work", function()
diff --git a/spec/util_jsonpointer_spec.lua b/spec/util_jsonpointer_spec.lua
index ce07c7a1..75122296 100644
--- a/spec/util_jsonpointer_spec.lua
+++ b/spec/util_jsonpointer_spec.lua
@@ -21,9 +21,11 @@ describe("util.jsonpointer", function()
}]])
end)
it("works", function()
+ assert.is_nil(jp.resolve("string", "/string"))
assert.same(example, jp.resolve(example, ""));
assert.same({ "bar", "baz" }, jp.resolve(example, "/foo"));
assert.same("bar", jp.resolve(example, "/foo/0"));
+ assert.same(nil, jp.resolve(example, "/foo/-"));
assert.same(0, jp.resolve(example, "/"));
assert.same(1, jp.resolve(example, "/a~1b"));
assert.same(2, jp.resolve(example, "/c%d"));
diff --git a/spec/util_jsonschema_spec.lua b/spec/util_jsonschema_spec.lua
index 968acaf1..cd4bcadd 100644
--- a/spec/util_jsonschema_spec.lua
+++ b/spec/util_jsonschema_spec.lua
@@ -2,7 +2,7 @@ local js = require "util.jsonschema";
local json = require "util.json";
local lfs = require "lfs";
--- https://github.com/json-schema-org/JSON-Schema-Test-Suite.git 2.0.0-550-g88d6948
+-- https://github.com/json-schema-org/JSON-Schema-Test-Suite.git 2.0.0-755-g7950d9e
local test_suite_dir = "spec/JSON-Schema-Test-Suite/tests/draft2020-12"
if lfs.attributes(test_suite_dir, "mode") ~= "directory" then return end
@@ -13,30 +13,25 @@ local skip = {
["additionalProperties.json:1:0"] = "NYI",
["anchor.json"] = "$anchor NYI",
["const.json:1"] = "deepcompare",
- ["const.json:13:2"] = "IEEE 754 equality",
["const.json:2"] = "deepcompare",
["const.json:8"] = "deepcompare",
["const.json:9"] = "deepcompare",
["contains.json:0:5"] = "distinguishing objects from arrays",
["defs.json"] = "need built-in meta-schema",
- ["dependentRequired.json"] = "NYI",
- ["dependentSchemas.json"] = "NYI",
+ ["dependentSchemas.json:2:2"] = "NYI", -- minProperties
["dynamicRef.json"] = "NYI",
["enum.json:1:3"] = "deepcompare",
["id.json"] = "NYI",
- ["maxContains.json"] = "NYI",
- ["maxLength.json:0:4"] = "UTF-16",
["maxProperties.json"] = "NYI",
- ["minContains.json"] = "NYI",
- ["minLength.json:0:4"] = "UTF-16",
["minProperties.json"] = "NYI",
["multipleOf.json:1"] = "multiples of IEEE 754 fractions",
["multipleOf.json:2"] = "multiples of IEEE 754 fractions",
+ ["multipleOf.json:4"] = "multiples of IEEE 754 fractions",
["pattern.json"] = "NYI",
["patternProperties.json"] = "NYI",
["properties.json:1:2"] = "NYI",
["properties.json:1:3"] = "NYI",
- ["ref.json:0:3"] = "NYI additionalProperties",
+ ["ref.json:0:3"] = "util.jsonpointer recursive issue?",
["ref.json:11"] = "NYI",
["ref.json:12:1"] = "FIXME",
["ref.json:13"] = "NYI",
@@ -54,8 +49,14 @@ local skip = {
["ref.json:6:1"] = "NYI",
["ref.json:20"] = "NYI",
["ref.json:25"] = "NYI",
+ ["ref.json:29"] = "NYI",
+ ["ref.json:30"] = "NYI",
+ ["ref.json:31"] = "NYI",
+ ["ref.json:32"] = "NYI",
+ ["not.json:6"] = "NYI",
["refRemote.json"] = "DEFINITELY NYI",
["required.json:0:2"] = "distinguishing objects from arrays",
+ ["type.json:0:1"] = "1.0 is not an integer!",
["type.json:3:4"] = "distinguishing objects from arrays",
["type.json:3:6"] = "null is weird",
["type.json:4:3"] = "distinguishing objects from arrays",
@@ -64,11 +65,12 @@ local skip = {
["type.json:9:6"] = "null is weird",
["unevaluatedItems.json"] = "NYI",
["unevaluatedProperties.json"] = "NYI",
- ["uniqueItems.json:0:11"] = "deepcompare",
- ["uniqueItems.json:0:13"] = "deepcompare",
+ ["uniqueItems.json:0:10"] = "deepcompare",
+ ["uniqueItems.json:0:12"] = "deepcompare",
["uniqueItems.json:0:14"] = "deepcompare",
- ["uniqueItems.json:0:22"] = "deepcompare",
- ["uniqueItems.json:0:24"] = "deepcompare",
+ ["uniqueItems.json:0:15"] = "deepcompare",
+ ["uniqueItems.json:0:23"] = "deepcompare",
+ ["uniqueItems.json:0:25"] = "deepcompare",
["uniqueItems.json:0:9"] = "deepcompare",
["unknownKeyword.json"] = "NYI",
["vocabulary.json"] = "NYI",
diff --git a/spec/util_jwt_spec.lua b/spec/util_jwt_spec.lua
index b391a870..6946bdd3 100644
--- a/spec/util_jwt_spec.lua
+++ b/spec/util_jwt_spec.lua
@@ -1,4 +1,12 @@
local jwt = require "util.jwt";
+local test_keys = require "spec.inputs.test_keys";
+
+local array = require "util.array";
+local iter = require "util.iterators";
+local set = require "util.set";
+
+-- Ignore long lines. We have some long tokens embedded here.
+--luacheck: ignore 631
describe("util.jwt", function ()
it("validates", function ()
@@ -8,6 +16,9 @@ describe("util.jwt", function ()
local ok, parsed = jwt.verify(key, token);
assert.truthy(ok)
assert.same({ payload = "this" }, parsed);
+
+
+
end);
it("rejects invalid", function ()
local key = "secret";
@@ -16,5 +27,233 @@ describe("util.jwt", function ()
local ok = jwt.verify(key, token);
assert.falsy(ok)
end);
+
+ local function jwt_reference_token(token)
+ return {
+ name = "jwt.io reference";
+ token;
+ { -- payload
+ sub = "1234567890";
+ name = "John Doe";
+ admin = true;
+ iat = 1516239022;
+ };
+ };
+ end
+
+ local untested_algorithms = set.new(array.collect(iter.keys(jwt._algorithms)));
+
+ local test_cases = {
+ {
+ algorithm = "HS256";
+ keys = {
+ { "your-256-bit-secret", "your-256-bit-secret" };
+ { "another-secret", "another-secret" };
+ };
+
+ jwt_reference_token [[eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJhZG1pbiI6dHJ1ZX0.F-cvL2RcfQhUtCavIM7q7zYE8drmj2LJk0JRkrS6He4]];
+ };
+ {
+ algorithm = "HS384";
+ keys = {
+ { "your-384-bit-secret", "your-384-bit-secret" };
+ { "another-secret", "another-secret" };
+ };
+
+ jwt_reference_token [[eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.bQTnz6AuMJvmXXQsVPrxeQNvzDkimo7VNXxHeSBfClLufmCVZRUuyTwJF311JHuh]];
+ };
+ {
+ algorithm = "HS512";
+ keys = {
+ { "your-512-bit-secret", "your-512-bit-secret" };
+ { "another-secret", "another-secret" };
+ };
+
+ jwt_reference_token [[eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.VFb0qJ1LRg_4ujbZoRMXnVkUgiuKq5KxWqNdbKq_G9Vvz-S1zZa9LPxtHWKa64zDl2ofkT8F6jBt_K4riU-fPg]];
+ };
+ {
+ algorithm = "ES256";
+ keys = {
+ { test_keys.ecdsa_private_pem, test_keys.ecdsa_public_pem };
+ { test_keys.alt_ecdsa_private_pem, test_keys.alt_ecdsa_public_pem };
+ };
+ {
+ name = "jwt.io reference";
+ [[eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.tyh-VfuzIxCyGYDlkBA7DfyjrqmSHu6pQ2hoZuFqUSLPNY2N0mpHb3nk5K17HWP_3cYHBw7AhHale5wky6-sVA]];
+ { -- payload
+ sub = "1234567890";
+ name = "John Doe";
+ admin = true;
+ iat = 1516239022;
+ };
+ };
+ };
+ {
+ algorithm = "ES512";
+ keys = {
+ { test_keys.ecdsa_521_private_pem, test_keys.ecdsa_521_public_pem };
+ { test_keys.alt_ecdsa_521_private_pem, test_keys.alt_ecdsa_521_public_pem };
+ };
+ {
+ name = "jwt.io reference";
+ [[eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.AbVUinMiT3J_03je8WTOIl-VdggzvoFgnOsdouAs-DLOtQzau9valrq-S6pETyi9Q18HH-EuwX49Q7m3KC0GuNBJAc9Tksulgsdq8GqwIqZqDKmG7hNmDzaQG1Dpdezn2qzv-otf3ZZe-qNOXUMRImGekfQFIuH_MjD2e8RZyww6lbZk]];
+ { -- payload
+ sub = "1234567890";
+ name = "John Doe";
+ admin = true;
+ iat = 1516239022;
+ };
+ };
+ };
+ {
+ algorithm = "RS256";
+ keys = {
+ { test_keys.rsa_private_pem, test_keys.rsa_public_pem };
+ { test_keys.alt_rsa_private_pem, test_keys.alt_rsa_public_pem };
+ };
+ {
+ name = "jwt.io reference";
+ [[eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygTqVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9RiwrV7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ]];
+ { -- payload
+ sub = "1234567890";
+ name = "John Doe";
+ admin = true;
+ iat = 1516239022;
+ };
+ };
+ };
+ {
+ algorithm = "RS384";
+ keys = {
+ { test_keys.rsa_private_pem, test_keys.rsa_public_pem };
+ { test_keys.alt_rsa_private_pem, test_keys.alt_rsa_public_pem };
+ };
+
+ jwt_reference_token [[eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.o1hC1xYbJolSyh0-bOY230w22zEQSk5TiBfc-OCvtpI2JtYlW-23-8B48NpATozzMHn0j3rE0xVUldxShzy0xeJ7vYAccVXu2Gs9rnTVqouc-UZu_wJHkZiKBL67j8_61L6SXswzPAQu4kVDwAefGf5hyYBUM-80vYZwWPEpLI8K4yCBsF6I9N1yQaZAJmkMp_Iw371Menae4Mp4JusvBJS-s6LrmG2QbiZaFaxVJiW8KlUkWyUCns8-qFl5OMeYlgGFsyvvSHvXCzQrsEXqyCdS4tQJd73ayYA4SPtCb9clz76N1zE5WsV4Z0BYrxeb77oA7jJhh994RAPzCG0hmQ]];
+ };
+ {
+ algorithm = "RS512";
+ keys = {
+ { test_keys.rsa_private_pem, test_keys.rsa_public_pem };
+ { test_keys.alt_rsa_private_pem, test_keys.alt_rsa_public_pem };
+ };
+
+ jwt_reference_token [[eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.jYW04zLDHfR1v7xdrW3lCGZrMIsVe0vWCfVkN2DRns2c3MN-mcp_-RE6TN9umSBYoNV-mnb31wFf8iun3fB6aDS6m_OXAiURVEKrPFNGlR38JSHUtsFzqTOj-wFrJZN4RwvZnNGSMvK3wzzUriZqmiNLsG8lktlEn6KA4kYVaM61_NpmPHWAjGExWv7cjHYupcjMSmR8uMTwN5UuAwgW6FRstCJEfoxwb0WKiyoaSlDuIiHZJ0cyGhhEmmAPiCwtPAwGeaL1yZMcp0p82cpTQ5Qb-7CtRov3N4DcOHgWYk6LomPR5j5cCkePAz87duqyzSMpCB0mCOuE3CU2VMtGeQ]];
+ };
+ {
+ algorithm = "PS256";
+ keys = {
+ { test_keys.rsa_private_pem, test_keys.rsa_public_pem };
+ { test_keys.alt_rsa_private_pem, test_keys.alt_rsa_public_pem };
+ };
+
+ jwt_reference_token [[eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.iOeNU4dAFFeBwNj6qdhdvm-IvDQrTa6R22lQVJVuWJxorJfeQww5Nwsra0PjaOYhAMj9jNMO5YLmud8U7iQ5gJK2zYyepeSuXhfSi8yjFZfRiSkelqSkU19I-Ja8aQBDbqXf2SAWA8mHF8VS3F08rgEaLCyv98fLLH4vSvsJGf6ueZSLKDVXz24rZRXGWtYYk_OYYTVgR1cg0BLCsuCvqZvHleImJKiWmtS0-CymMO4MMjCy_FIl6I56NqLE9C87tUVpo1mT-kbg5cHDD8I7MjCW5Iii5dethB4Vid3mZ6emKjVYgXrtkOQ-JyGMh6fnQxEFN1ft33GX2eRHluK9eg]];
+ };
+ {
+ algorithm = "PS384";
+ keys = {
+ { test_keys.rsa_private_pem, test_keys.rsa_public_pem };
+ { test_keys.alt_rsa_private_pem, test_keys.alt_rsa_public_pem };
+ };
+
+ jwt_reference_token [[eyJhbGciOiJQUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.Lfe_aCQme_gQpUk9-6l9qesu0QYZtfdzfy08w8uqqPH_gnw-IVyQwyGLBHPFBJHMbifdSMxPjJjkCD0laIclhnBhowILu6k66_5Y2z78GHg8YjKocAvB-wSUiBhuV6hXVxE5emSjhfVz2OwiCk2bfk2hziRpkdMvfcITkCx9dmxHU6qcEIsTTHuH020UcGayB1-IoimnjTdCsV1y4CMr_ECDjBrqMdnontkqKRIM1dtmgYFsJM6xm7ewi_ksG_qZHhaoBkxQ9wq9OVQRGiSZYowCp73d2BF3jYMhdmv2JiaUz5jRvv6lVU7Quq6ylVAlSPxeov9voYHO1mgZFCY1kQ]];
+ };
+ {
+ algorithm = "PS512";
+ keys = {
+ { test_keys.rsa_private_pem, test_keys.rsa_public_pem };
+ { test_keys.alt_rsa_private_pem, test_keys.alt_rsa_public_pem };
+ };
+
+ jwt_reference_token [[eyJhbGciOiJQUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.J5W09-rNx0pt5_HBiydR-vOluS6oD-RpYNa8PVWwMcBDQSXiw6-EPW8iSsalXPspGj3ouQjAnOP_4-zrlUUlvUIt2T79XyNeiKuooyIFvka3Y5NnGiOUBHWvWcWp4RcQFMBrZkHtJM23sB5D7Wxjx0-HFeNk-Y3UJgeJVhg5NaWXypLkC4y0ADrUBfGAxhvGdRdULZivfvzuVtv6AzW6NRuEE6DM9xpoWX_4here-yvLS2YPiBTZ8xbB3axdM99LhES-n52lVkiX5AWg2JJkEROZzLMpaacA_xlbUz_zbIaOaoqk8gB5oO7kI6sZej3QAdGigQy-hXiRnW_L98d4GQ]];
+ };
+ };
+
+ local function do_verify_test(algorithm, verifying_key, token, expect_payload)
+ local verify = jwt.new_verifier(algorithm, verifying_key);
+
+ assert.is_string(token);
+ local result = {verify(token)};
+ if expect_payload then
+ assert.same({
+ true; -- success
+ expect_payload; -- payload
+ }, result);
+ else
+ assert.same({
+ false;
+ "signature-mismatch";
+ }, result);
+ end
+ end
+
+ local function do_sign_verify_test(algorithm, signing_key, verifying_key, expect_success, expect_token)
+ local sign = jwt.new_signer(algorithm, signing_key);
+
+ local test_payload = {
+ sub = "1234567890";
+ name = "John Doe";
+ admin = true;
+ iat = 1516239022;
+ };
+
+ local token = sign(test_payload);
+
+ if expect_token then
+ assert.equal(expect_token, token);
+ end
+
+ do_verify_test(algorithm, verifying_key, token, expect_success and test_payload or false);
+ end
+
+
+ for _, algorithm_tests in ipairs(test_cases) do
+ local algorithm = algorithm_tests.algorithm;
+ local keypairs = algorithm_tests.keys;
+
+ untested_algorithms:remove(algorithm);
+
+ describe(algorithm, function ()
+ describe("can do basic sign and verify", function ()
+ for keypair_n, keypair in ipairs(keypairs) do
+ local signing_key, verifying_key = keypair[1], keypair[2];
+ it(("(test key pair %d)"):format(keypair_n), function ()
+ do_sign_verify_test(algorithm, signing_key, verifying_key, true);
+ end);
+ end
+ end);
+
+ if #keypairs >= 2 then
+ it("rejects invalid tokens", function ()
+ do_sign_verify_test(algorithm, keypairs[1][1], keypairs[2][2], false);
+ end);
+ else
+ pending("rejects invalid tokens", function ()
+ error("Needs at least 2 key pairs");
+ end);
+ end
+
+ if #algorithm_tests > 0 then
+ for test_n, test_case in ipairs(algorithm_tests) do
+ it("can verify "..(test_case.name or (("test case %d"):format(test_n))), function ()
+ do_verify_test(
+ algorithm,
+ test_case.verifying_key or keypairs[1][2],
+ test_case[1],
+ test_case[2]
+ );
+ end);
+ end
+ else
+ pending("can verify reference tokens", function ()
+ error("No test tokens provided");
+ end);
+ end
+ end);
+ end
+
+ for algorithm in untested_algorithms do
+ pending(algorithm.." tests", function () end);
+ end
end);
diff --git a/spec/util_paseto_spec.lua b/spec/util_paseto_spec.lua
new file mode 100644
index 00000000..417278b1
--- /dev/null
+++ b/spec/util_paseto_spec.lua
@@ -0,0 +1,292 @@
+-- Ignore long lines in this file
+--luacheck: ignore 631
+
+describe("util.paseto", function ()
+ local paseto = require "util.paseto";
+ local json = require "util.json";
+ local hex = require "util.hex";
+
+ describe("v3.local", function ()
+ local function parse_test_cases(json_test_cases)
+ local input_cases = json.decode(json_test_cases);
+ local output_cases = {};
+ for _, case in ipairs(input_cases) do
+ assert.is_string(case.name, "Bad test case: expected name");
+ assert.is_nil(output_cases[case.name], "Bad test case: duplicate name");
+ output_cases[case.name] = function ()
+ local key = hex.decode(case.key);
+ local payload, err = paseto.v3_local.decrypt(case.token, key, case.footer, case["implicit-assertion"]);
+ if case["expect-fail"] then
+ assert.is_nil(payload);
+ else
+ assert.is_nil(err);
+ assert.same(json.decode(case.payload), payload);
+ end
+ end;
+ end
+ return output_cases;
+ end
+
+ local test_cases = parse_test_cases [=[[
+ {
+ "name": "3-E-1",
+ "expect-fail": false,
+ "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f",
+ "nonce": "0000000000000000000000000000000000000000000000000000000000000000",
+ "token": "v3.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADbfcIURX_0pVZVU1mAESUzrKZAsRm2EsD6yBoZYn6cpVZNzSJOhSDN-sRaWjfLU-yn9OJH1J_B8GKtOQ9gSQlb8yk9Iza7teRdkiR89ZFyvPPsVjjFiepFUVcMa-LP18zV77f_crJrVXWa5PDNRkCSeHfBBeg",
+ "payload": "{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}",
+ "footer": "",
+ "implicit-assertion": ""
+ },
+ {
+ "name": "3-E-2",
+ "expect-fail": false,
+ "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f",
+ "nonce": "0000000000000000000000000000000000000000000000000000000000000000",
+ "token": "v3.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADbfcIURX_0pVZVU1mAESUzrKZAqhWxBMDgyBoZYn6cpVZNzSJOhSDN-sRaWjfLU-yn9OJH1J_B8GKtOQ9gSQlb8yk9IzZfaZpReVpHlDSwfuygx1riVXYVs-UjcrG_apl9oz3jCVmmJbRuKn5ZfD8mHz2db0A",
+ "payload": "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}",
+ "footer": "",
+ "implicit-assertion": ""
+ },
+ {
+ "name": "3-E-3",
+ "expect-fail": false,
+ "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2",
+ "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f",
+ "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0ROIIykcrGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJlxnt5xyhQjFJomwnt7WW_7r2VT0G704ifult011-TgLCyQ2X8imQhniG_hAQ4BydM",
+ "payload": "{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}",
+ "footer": "",
+ "implicit-assertion": ""
+ },
+ {
+ "name": "3-E-4",
+ "expect-fail": false,
+ "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2",
+ "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f",
+ "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0X-4P3EcxGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJlBZa_gOpVj4gv0M9lV6Pwjp8JS_MmaZaTA1LLTULXybOBZ2S4xMbYqYmDRhh3IgEk",
+ "payload": "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}",
+ "footer": "",
+ "implicit-assertion": ""
+ },
+ {
+ "name": "3-E-5",
+ "expect-fail": false,
+ "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2",
+ "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f",
+ "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0ROIIykcrGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJlkYSIbXOgVuIQL65UMdW9WcjOpmqvjqD40NNzed-XPqn1T3w-bJvitYpUJL_rmihc.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9",
+ "payload": "{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}",
+ "footer": "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}",
+ "implicit-assertion": ""
+ },
+ {
+ "name": "3-E-6",
+ "expect-fail": false,
+ "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2",
+ "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f",
+ "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0X-4P3EcxGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJmSeEMphEWHiwtDKJftg41O1F8Hat-8kQ82ZIAMFqkx9q5VkWlxZke9ZzMBbb3Znfo.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9",
+ "payload": "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}",
+ "footer": "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}",
+ "implicit-assertion": ""
+ },
+ {
+ "name": "3-E-7",
+ "expect-fail": false,
+ "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2",
+ "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f",
+ "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0ROIIykcrGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJkzWACWAIoVa0bz7EWSBoTEnS8MvGBYHHo6t6mJunPrFR9JKXFCc0obwz5N-pxFLOc.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9",
+ "payload": "{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}",
+ "footer": "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}",
+ "implicit-assertion": "{\"test-vector\":\"3-E-7\"}"
+ },
+ {
+ "name": "3-E-8",
+ "expect-fail": false,
+ "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2",
+ "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f",
+ "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0X-4P3EcxGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJmZHSSKYR6AnPYJV6gpHtx6dLakIG_AOPhu8vKexNyrv5_1qoom6_NaPGecoiz6fR8.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9",
+ "payload": "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}",
+ "footer": "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}",
+ "implicit-assertion": "{\"test-vector\":\"3-E-8\"}"
+ },
+ {
+ "name": "3-E-9",
+ "expect-fail": false,
+ "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2",
+ "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f",
+ "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0X-4P3EcxGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJlk1nli0_wijTH_vCuRwckEDc82QWK8-lG2fT9wQF271sgbVRVPjm0LwMQZkvvamqU.YXJiaXRyYXJ5LXN0cmluZy10aGF0LWlzbid0LWpzb24",
+ "payload": "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}",
+ "footer": "arbitrary-string-that-isn't-json",
+ "implicit-assertion": "{\"test-vector\":\"3-E-9\"}"
+ },
+ {
+ "name": "3-F-3",
+ "expect-fail": true,
+ "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2",
+ "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f",
+ "token": "v4.local.1JgN1UG8TFAYS49qsx8rxlwh-9E4ONUm3slJXYi5EibmzxpF0Q-du6gakjuyKCBX8TvnSLOKqCPu8Yh3WSa5yJWigPy33z9XZTJF2HQ9wlLDPtVn_Mu1pPxkTU50ZaBKblJBufRA.YXJiaXRyYXJ5LXN0cmluZy10aGF0LWlzbid0LWpzb24",
+ "payload": null,
+ "footer": "arbitrary-string-that-isn't-json",
+ "implicit-assertion": "{\"test-vector\":\"3-F-3\"}"
+ },
+ {
+ "name": "3-F-4",
+ "expect-fail": true,
+ "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f",
+ "nonce": "0000000000000000000000000000000000000000000000000000000000000000",
+ "token": "v3.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADbfcIURX_0pVZVU1mAESUzrKZAsRm2EsD6yBoZYn6cpVZNzSJOhSDN-sRaWjfLU-yn9OJH1J_B8GKtOQ9gSQlb8yk9Iza7teRdkiR89ZFyvPPsVjjFiepFUVcMa-LP18zV77f_crJrVXWa5PDNRkCSeHfBBeh",
+ "payload": null,
+ "footer": "",
+ "implicit-assertion": ""
+ },
+ {
+ "name": "3-F-5",
+ "expect-fail": true,
+ "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2",
+ "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f",
+ "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0ROIIykcrGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJlkYSIbXOgVuIQL65UMdW9WcjOpmqvjqD40NNzed-XPqn1T3w-bJvitYpUJL_rmihc=.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9",
+ "payload": null,
+ "footer": "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}",
+ "implicit-assertion": ""
+ }
+ ]]=];
+ for name, test in pairs(test_cases) do
+ it("test case "..name, test);
+ end
+
+ describe("basic sign/verify", function ()
+ local key = paseto.v3_local.new_key();
+ local sign, verify = paseto.v3_local.init(key);
+
+ --luacheck: ignore 211/sign2
+ local key2 = paseto.v3_local.new_key();
+ local sign2, verify2 = paseto.v3_local.init(key2);
+
+ it("works", function ()
+ local payload = { foo = "hello world", b = { 1, 2, 3 } };
+
+ local tok = sign(payload);
+ assert.same(payload, verify(tok));
+ assert.is_nil(verify2(tok));
+ end);
+
+ it("rejects tokens if implicit assertion fails", function ()
+ local payload = { foo = "hello world", b = { 1, 2, 3 } };
+ local tok = sign(payload, nil, "my-custom-assertion");
+ assert.is_nil(verify(tok, nil, "my-incorrect-assertion"));
+ assert.is_nil(verify(tok, nil, nil));
+ assert.same(payload, verify(tok, nil, "my-custom-assertion"));
+ end);
+ end);
+ end);
+
+ describe("v4.public", function ()
+ local function parse_test_cases(json_test_cases)
+ local input_cases = json.decode(json_test_cases);
+ local output_cases = {};
+ for _, case in ipairs(input_cases) do
+ assert.is_string(case.name, "Bad test case: expected name");
+ assert.is_nil(output_cases[case.name], "Bad test case: duplicate name");
+ output_cases[case.name] = function ()
+ local verify_key = paseto.v4_public.import_public_key(case["public-key-pem"]);
+ local payload, err = paseto.v4_public.verify(case.token, verify_key, case.footer, case["implicit-assertion"]);
+ if case["expect-fail"] then
+ assert.is_nil(payload);
+ else
+ assert.is_nil(err);
+ assert.same(json.decode(case.payload), payload);
+ end
+ end;
+ end
+ return output_cases;
+ end
+
+ local test_cases = parse_test_cases [=[[
+ {
+ "name": "4-S-1",
+ "expect-fail": false,
+ "public-key": "1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2",
+ "secret-key": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a37741eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2",
+ "secret-key-seed": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a3774",
+ "secret-key-pem": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----",
+ "public-key-pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----",
+ "token": "v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9bg_XBBzds8lTZShVlwwKSgeKpLT3yukTw6JUz3W4h_ExsQV-P0V54zemZDcAxFaSeef1QlXEFtkqxT1ciiQEDA",
+ "payload": "{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}",
+ "footer": "",
+ "implicit-assertion": ""
+ },
+ {
+ "name": "4-S-2",
+ "expect-fail": false,
+ "public-key": "1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2",
+ "secret-key": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a37741eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2",
+ "secret-key-seed": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a3774",
+ "secret-key-pem": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----",
+ "public-key-pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----",
+ "token": "v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9v3Jt8mx_TdM2ceTGoqwrh4yDFn0XsHvvV_D0DtwQxVrJEBMl0F2caAdgnpKlt4p7xBnx1HcO-SPo8FPp214HDw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9",
+ "payload": "{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}",
+ "footer": "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}",
+ "implicit-assertion": ""
+ },
+ {
+ "name": "4-S-3",
+ "expect-fail": false,
+ "public-key": "1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2",
+ "secret-key": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a37741eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2",
+ "secret-key-seed": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a3774",
+ "secret-key-pem": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----",
+ "public-key-pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----",
+ "token": "v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9NPWciuD3d0o5eXJXG5pJy-DiVEoyPYWs1YSTwWHNJq6DZD3je5gf-0M4JR9ipdUSJbIovzmBECeaWmaqcaP0DQ.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9",
+ "payload": "{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}",
+ "footer": "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}",
+ "implicit-assertion": "{\"test-vector\":\"4-S-3\"}"
+ }]]=];
+ for name, test in pairs(test_cases) do
+ it("test case "..name, test);
+ end
+
+ describe("basic sign/verify", function ()
+ local function new_keypair()
+ local kp = paseto.v4_public.new_keypair();
+ return kp:private_pem(), kp:public_pem();
+ end
+
+ local privkey1, pubkey1 = new_keypair();
+ local privkey2, pubkey2 = new_keypair();
+ local sign1, verify1 = paseto.v4_public.init(privkey1, pubkey1);
+ local sign2, verify2 = paseto.v4_public.init(privkey2, pubkey2);
+
+ it("works", function ()
+ local payload = { foo = "hello world", b = { 1, 2, 3 } };
+
+ local tok1 = sign1(payload);
+ assert.same(payload, verify1(tok1));
+ assert.is_nil(verify2(tok1));
+
+ local tok2 = sign2(payload);
+ assert.same(payload, verify2(tok2));
+ assert.is_nil(verify1(tok2));
+ end);
+
+ it("rejects tokens if implicit assertion fails", function ()
+ local payload = { foo = "hello world", b = { 1, 2, 3 } };
+ local tok = sign1(payload, nil, "my-custom-assertion");
+ assert.is_nil(verify1(tok, nil, "my-incorrect-assertion"));
+ assert.is_nil(verify1(tok, nil, nil));
+ assert.same(payload, verify1(tok, nil, "my-custom-assertion"));
+ end);
+ end);
+ end);
+
+ describe("pae", function ()
+ it("encodes correctly", function ()
+ -- These test cases are taken from the PASETO docs
+ -- https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Common.md
+ assert.equal("\x00\x00\x00\x00\x00\x00\x00\x00", paseto.pae{});
+ assert.equal("\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", paseto.pae{''});
+ assert.equal("\x01\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00test", paseto.pae{'test'});
+ assert.has_errors(function ()
+ paseto.pae("test");
+ end);
+ end);
+ end);
+end);
diff --git a/spec/util_poll_spec.lua b/spec/util_poll_spec.lua
index a763be90..05318453 100644
--- a/spec/util_poll_spec.lua
+++ b/spec/util_poll_spec.lua
@@ -1,6 +1,35 @@
-describe("util.poll", function ()
- it("loads", function ()
- require "util.poll"
+describe("util.poll", function()
+ local poll;
+ setup(function()
+ poll = require "util.poll";
end);
+ it("loads", function()
+ assert.is_table(poll);
+ assert.is_function(poll.new);
+ assert.is_string(poll.api);
+ end);
+ describe("new", function()
+ local p;
+ setup(function()
+ p = poll.new();
+ end)
+ it("times out", function ()
+ local fd, err = p:wait(0);
+ assert.falsy(fd);
+ assert.equal("timeout", err);
+ end);
+ it("works", function()
+ -- stdout should be writable, right?
+ assert.truthy(p:add(1, false, true));
+ local fd, r, w = p:wait(1);
+ assert.is_number(fd);
+ assert.is_boolean(r);
+ assert.is_boolean(w);
+ assert.equal(1, fd);
+ assert.falsy(r);
+ assert.truthy(w);
+ assert.truthy(p:del(1));
+ end);
+ end)
end);
diff --git a/spec/util_promise_spec.lua b/spec/util_promise_spec.lua
index 597b56f8..91b3d2d1 100644
--- a/spec/util_promise_spec.lua
+++ b/spec/util_promise_spec.lua
@@ -7,6 +7,11 @@ describe("util.promise", function ()
assert(promise.new());
end);
end);
+ it("supplies a sensible tostring()", function ()
+ local s = tostring(promise.new());
+ assert.truthy(s:find("promise", 1, true));
+ assert.truthy(s:find("pending", 1, true));
+ end);
it("notifies immediately for fulfilled promises", function ()
local p = promise.new(function (resolve)
resolve("foo");
@@ -30,6 +35,27 @@ describe("util.promise", function ()
r("foo");
assert.spy(cb).was_called(1);
end);
+ it("ignores resolve/reject of settled promises", function ()
+ local res, rej;
+ local p = promise.new(function (resolve, reject)
+ res, rej = resolve, reject;
+ end);
+ local cb = spy.new(function (v)
+ assert.equal("foo", v);
+ end);
+ p:next(cb, cb);
+ assert.spy(cb).was_called(0);
+ res("foo");
+ assert.spy(cb).was_called(1);
+ rej("bar");
+ assert.spy(cb).was_called(1);
+ rej(promise.resolve("bar"));
+ assert.spy(cb).was_called(1);
+ res(promise.reject("bar"));
+ assert.spy(cb).was_called(1);
+ res(promise.resolve("bar"));
+ assert.spy(cb).was_called(1);
+ end);
it("allows chaining :next() calls", function ()
local r;
local result;
@@ -438,6 +464,26 @@ describe("util.promise", function ()
{ status = "rejected", reason = "this fails" };
}, result);
end);
+ it("works when all promises reject", function ()
+ local r1, r2;
+ local p1, p2 = promise.new(function (_, reject) r1 = reject 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 fails too");
+ assert.spy(cb).was_called(1);
+ assert.same({
+ { status = "rejected", reason = "this fails too" };
+ { 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);
diff --git a/spec/util_pubsub_spec.lua b/spec/util_pubsub_spec.lua
index 45a612a0..a03ffa64 100644
--- a/spec/util_pubsub_spec.lua
+++ b/spec/util_pubsub_spec.lua
@@ -108,7 +108,7 @@ describe("util.pubsub", function ()
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.pubsub_condition);
+ assert.equals("precondition-not-met", err);
end);
it("allows to publish to a node with differing config when only defaults are suggested", function ()
@@ -605,4 +605,14 @@ describe("util.pubsub", function ()
end);
end)
+
+ describe("metadata", function()
+ it("works", function()
+ local service = pubsub.new { metadata_subset = { "title" } };
+ assert.truthy(service:create("node", true, { title = "Hello", secret = "hidden" }))
+ local ok, meta = service:get_node_metadata("node", "nobody");
+ assert.truthy(ok, meta);
+ assert.same({ title = "Hello" }, meta);
+ end)
+ end);
end);
diff --git a/spec/util_rfc6724_spec.lua b/spec/util_rfc6724_spec.lua
deleted file mode 100644
index 30e935b6..00000000
--- a/spec/util_rfc6724_spec.lua
+++ /dev/null
@@ -1,97 +0,0 @@
-
-local rfc6724 = require "util.rfc6724";
-local new_ip = require"util.ip".new_ip;
-
-describe("util.rfc6724", function()
- describe("#source()", function()
- it("should work", function()
- assert.are.equal(rfc6724.source(new_ip("2001:db8:1::1", "IPv6"),
- {new_ip("2001:db8:3::1", "IPv6"), new_ip("fe80::1", "IPv6")}).addr,
- "2001:db8:3::1",
- "prefer appropriate scope");
- assert.are.equal(rfc6724.source(new_ip("ff05::1", "IPv6"),
- {new_ip("2001:db8:3::1", "IPv6"), new_ip("fe80::1", "IPv6")}).addr,
- "2001:db8:3::1",
- "prefer appropriate scope");
- assert.are.equal(rfc6724.source(new_ip("2001:db8:1::1", "IPv6"),
- {new_ip("2001:db8:1::1", "IPv6"), new_ip("2001:db8:2::1", "IPv6")}).addr,
- "2001:db8:1::1",
- "prefer same address"); -- "2001:db8:1::1" should be marked "deprecated" here, we don't handle that right now
- assert.are.equal(rfc6724.source(new_ip("fe80::1", "IPv6"),
- {new_ip("fe80::2", "IPv6"), new_ip("2001:db8:1::1", "IPv6")}).addr,
- "fe80::2",
- "prefer appropriate scope"); -- "fe80::2" should be marked "deprecated" here, we don't handle that right now
- assert.are.equal(rfc6724.source(new_ip("2001:db8:1::1", "IPv6"),
- {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3::2", "IPv6")}).addr,
- "2001:db8:1::2",
- "longest matching prefix");
- --[[ "2001:db8:1::2" should be a care-of address and "2001:db8:3::2" a home address, we can't handle this and would fail
- assert.are.equal(rfc6724.source(new_ip("2001:db8:1::1", "IPv6"),
- {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3::2", "IPv6")}).addr,
- "2001:db8:3::2",
- "prefer home address");
- ]]
- assert.are.equal(rfc6724.source(new_ip("2002:c633:6401::1", "IPv6"),
- {new_ip("2002:c633:6401::d5e3:7953:13eb:22e8", "IPv6"), new_ip("2001:db8:1::2", "IPv6")}).addr,
- "2002:c633:6401::d5e3:7953:13eb:22e8",
- "prefer matching label"); -- "2002:c633:6401::d5e3:7953:13eb:22e8" should be marked "temporary" here, we don't handle that right now
- assert.are.equal(rfc6724.source(new_ip("2001:db8:1::d5e3:0:0:1", "IPv6"),
- {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:1::d5e3:7953:13eb:22e8", "IPv6")}).addr,
- "2001:db8:1::d5e3:7953:13eb:22e8",
- "prefer temporary address") -- "2001:db8:1::2" should be marked "public" and "2001:db8:1::d5e3:7953:13eb:22e8" should be marked "temporary" here, we don't handle that right now
- end);
- end);
- describe("#destination()", function()
- it("should work", function()
- local order;
- order = rfc6724.destination({new_ip("2001:db8:1::1", "IPv6"), new_ip("198.51.100.121", "IPv4")},
- {new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::1", "IPv6"), new_ip("169.254.13.78", "IPv4")})
- assert.are.equal(order[1].addr, "2001:db8:1::1", "prefer matching scope");
- assert.are.equal(order[2].addr, "198.51.100.121", "prefer matching scope");
-
- order = rfc6724.destination({new_ip("2001:db8:1::1", "IPv6"), new_ip("198.51.100.121", "IPv4")},
- {new_ip("fe80::1", "IPv6"), new_ip("198.51.100.117", "IPv4")})
- assert.are.equal(order[1].addr, "198.51.100.121", "prefer matching scope");
- assert.are.equal(order[2].addr, "2001:db8:1::1", "prefer matching scope");
-
- order = rfc6724.destination({new_ip("2001:db8:1::1", "IPv6"), new_ip("10.1.2.3", "IPv4")},
- {new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::1", "IPv6"), new_ip("10.1.2.4", "IPv4")})
- assert.are.equal(order[1].addr, "2001:db8:1::1", "prefer higher precedence");
- assert.are.equal(order[2].addr, "10.1.2.3", "prefer higher precedence");
-
- order = rfc6724.destination({new_ip("2001:db8:1::1", "IPv6"), new_ip("fe80::1", "IPv6")},
- {new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::2", "IPv6")})
- assert.are.equal(order[1].addr, "fe80::1", "prefer smaller scope");
- assert.are.equal(order[2].addr, "2001:db8:1::1", "prefer smaller scope");
-
- --[[ "2001:db8:1::2" and "fe80::2" should be marked "care-of address", while "2001:db8:3::1" should be marked "home address", we can't currently handle this and would fail the test
- order = rfc6724.destination({new_ip("2001:db8:1::1", "IPv6"), new_ip("fe80::1", "IPv6")},
- {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3::1", "IPv6"), new_ip("fe80::2", "IPv6")})
- assert.are.equal(order[1].addr, "2001:db8:1::1", "prefer home address");
- assert.are.equal(order[2].addr, "fe80::1", "prefer home address");
- ]]
-
- --[[ "fe80::2" should be marked "deprecated", we can't currently handle this and would fail the test
- order = rfc6724.destination({new_ip("2001:db8:1::1", "IPv6"), new_ip("fe80::1", "IPv6")},
- {new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::2", "IPv6")})
- assert.are.equal(order[1].addr, "2001:db8:1::1", "avoid deprecated addresses");
- assert.are.equal(order[2].addr, "fe80::1", "avoid deprecated addresses");
- ]]
-
- order = rfc6724.destination({new_ip("2001:db8:1::1", "IPv6"), new_ip("2001:db8:3ffe::1", "IPv6")},
- {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3f44::2", "IPv6"), new_ip("fe80::2", "IPv6")})
- assert.are.equal(order[1].addr, "2001:db8:1::1", "longest matching prefix");
- assert.are.equal(order[2].addr, "2001:db8:3ffe::1", "longest matching prefix");
-
- order = rfc6724.destination({new_ip("2002:c633:6401::1", "IPv6"), new_ip("2001:db8:1::1", "IPv6")},
- {new_ip("2002:c633:6401::2", "IPv6"), new_ip("fe80::2", "IPv6")})
- assert.are.equal(order[1].addr, "2002:c633:6401::1", "prefer matching label");
- assert.are.equal(order[2].addr, "2001:db8:1::1", "prefer matching label");
-
- order = rfc6724.destination({new_ip("2002:c633:6401::1", "IPv6"), new_ip("2001:db8:1::1", "IPv6")},
- {new_ip("2002:c633:6401::2", "IPv6"), new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::2", "IPv6")})
- assert.are.equal(order[1].addr, "2001:db8:1::1", "prefer higher precedence");
- assert.are.equal(order[2].addr, "2002:c633:6401::1", "prefer higher precedence");
- end);
- end);
-end);
diff --git a/spec/util_roles_spec.lua b/spec/util_roles_spec.lua
new file mode 100644
index 00000000..44d2a977
--- /dev/null
+++ b/spec/util_roles_spec.lua
@@ -0,0 +1,134 @@
+describe("util.roles", function ()
+ randomize(false);
+ local roles;
+ it("can be loaded", function ()
+ roles = require "util.roles";
+ end);
+ local test_role;
+ it("can create a new role", function ()
+ test_role = roles.new();
+ assert.is_not_nil(test_role);
+ assert.is_truthy(roles.is_role(test_role));
+ end);
+ describe("role object", function ()
+ it("can be initialized with permissions", function ()
+ local test_role_2 = roles.new({
+ permissions = {
+ perm1 = true;
+ perm2 = false;
+ };
+ });
+ assert.truthy(test_role_2:may("perm1"));
+ assert.falsy(test_role_2:may("perm2"));
+ end);
+ it("has a sensible tostring", function ()
+ local test_role_2 = roles.new({
+ id = "test-role-2";
+ name = "Test Role 2";
+ });
+ assert.truthy(tostring(test_role_2):find(test_role_2.id, 1, true));
+ assert.truthy(tostring(test_role_2):find("Test Role 2", 1, true));
+ end);
+ it("is restrictive by default", function ()
+ assert.falsy(test_role:may("my-permission"));
+ end);
+ it("allows you to set permissions", function ()
+ test_role:set_permission("my-permission", true);
+ assert.truthy(test_role:may("my-permission"));
+ end);
+ it("allows you to set negative permissions", function ()
+ test_role:set_permission("my-other-permission", false);
+ assert.falsy(test_role:may("my-other-permission"));
+ end);
+ it("does not allows you to override previously set permissions by default", function ()
+ local ok, err = test_role:set_permission("my-permission", false);
+ assert.falsy(ok);
+ assert.is_equal("policy-already-exists", err);
+ -- Confirm old permission still in place
+ assert.truthy(test_role:may("my-permission"));
+ end);
+ it("allows you to explicitly override previously set permissions", function ()
+ assert.truthy(test_role:set_permission("my-permission", false, true));
+ assert.falsy(test_role:may("my-permission"));
+ end);
+ describe("inheritance", function ()
+ local child_role;
+ it("works", function ()
+ test_role:set_permission("inherited-permission", true);
+ child_role = roles.new({
+ inherits = { test_role };
+ });
+ assert.truthy(child_role:may("inherited-permission"));
+ assert.falsy(child_role:may("my-permission"));
+ end);
+ it("allows listing policies", function ()
+ local expected = {
+ ["my-permission"] = false;
+ ["my-other-permission"] = false;
+ ["inherited-permission"] = true;
+ };
+ local received = {};
+ for permission_name, permission_policy in child_role:policies() do
+ received[permission_name] = permission_policy;
+ end
+ assert.same(expected, received);
+ end);
+ it("supports multiple depths of inheritance", function ()
+ local grandchild_role = roles.new({
+ inherits = { child_role };
+ });
+ assert.truthy(grandchild_role:may("inherited-permission"));
+ end);
+ describe("supports ordered inheritance from multiple roles", function ()
+ local parent_role = roles.new();
+ local final_role = roles.new({
+ -- Yes, the names are getting confusing.
+ -- btw, test_role is inherited through child_role.
+ inherits = { parent_role, child_role };
+ });
+
+ local test_cases = {
+ -- { <final_role policy>, <parent_role policy>, <test_role policy> }
+ { true, nil, false, result = true };
+ { nil, false, true, result = false };
+ { nil, true, false, result = true };
+ { nil, nil, false, result = false };
+ { nil, nil, true, result = true };
+ };
+
+ for n, test_case in ipairs(test_cases) do
+ it("(case "..n..")", function ()
+ local perm_name = ("multi-inheritance-perm-%d"):format(n);
+ assert.truthy(final_role:set_permission(perm_name, test_case[1]));
+ assert.truthy(parent_role:set_permission(perm_name, test_case[2]));
+ assert.truthy(test_role:set_permission(perm_name, test_case[3]));
+ assert.equal(test_case.result, final_role:may(perm_name));
+ end);
+ end
+ end);
+ it("updates child roles when parent roles change", function ()
+ assert.truthy(child_role:may("inherited-permission"));
+ assert.truthy(test_role:set_permission("inherited-permission", false, true));
+ assert.falsy(child_role:may("inherited-permission"));
+ end);
+ end);
+ describe("cloning", function ()
+ local cloned_role;
+ it("works", function ()
+ assert.truthy(test_role:set_permission("perm-1", true));
+ cloned_role = test_role:clone();
+ assert.truthy(cloned_role:may("perm-1"));
+ end);
+ it("isolates changes", function ()
+ -- After cloning, changes in either the original or the clone
+ -- should not appear in the other.
+ assert.truthy(test_role:set_permission("perm-1", false, true));
+ assert.truthy(test_role:set_permission("perm-2", true));
+ assert.truthy(cloned_role:set_permission("perm-3", true));
+ assert.truthy(cloned_role:may("perm-1"));
+ assert.falsy(cloned_role:may("perm-2"));
+ assert.falsy(test_role:may("perm-3"));
+ end);
+ end);
+ end);
+end);
diff --git a/spec/util_sasl_spec.lua b/spec/util_sasl_spec.lua
index 368291b3..67b60758 100644
--- a/spec/util_sasl_spec.lua
+++ b/spec/util_sasl_spec.lua
@@ -39,5 +39,37 @@ describe("util.sasl", function ()
-- TODO SCRAM
end);
+
+ describe("oauthbearer profile", function()
+ local profile = {
+ oauthbearer = function(_, token, _realm, _authzid)
+ if token == "example-bearer-token" then
+ return "user", true, {};
+ else
+ return nil, nil, {}
+ end
+ end;
+ }
+
+ it("works with OAUTHBEARER", function()
+ local bearer = sasl.new("sasl.test", profile);
+
+ assert.truthy(bearer:select("OAUTHBEARER"));
+ assert.equals("success", bearer:process("n,,\1auth=Bearer example-bearer-token\1\1"));
+ assert.equals("user", bearer.username);
+ end)
+
+
+ it("returns extras with OAUTHBEARER", function()
+ local bearer = sasl.new("sasl.test", profile);
+
+ assert.truthy(bearer:select("OAUTHBEARER"));
+ local status, extra = bearer:process("n,,\1auth=Bearer unknown\1\1");
+ assert.equals("challenge", status);
+ assert.equals("{\"status\":\"invalid_token\"}", extra);
+ assert.equals("failure", bearer:process("\1"));
+ end)
+
+ end)
end);
diff --git a/spec/util_smqueue_spec.lua b/spec/util_smqueue_spec.lua
index 0a02a60b..858b99c0 100644
--- a/spec/util_smqueue_spec.lua
+++ b/spec/util_smqueue_spec.lua
@@ -5,6 +5,9 @@ describe("util.smqueue", function()
describe("#new()", function()
it("should work", function()
+ assert.has_error(function () smqueue.new(-1) end);
+ assert.has_error(function () smqueue.new(0) end);
+ assert.not_has_error(function () smqueue.new(1) end);
local q = smqueue.new(10);
assert.truthy(q);
end)
diff --git a/spec/util_stanza_spec.lua b/spec/util_stanza_spec.lua
index 8732a111..e11c70dd 100644
--- a/spec/util_stanza_spec.lua
+++ b/spec/util_stanza_spec.lua
@@ -314,6 +314,20 @@ describe("util.stanza", function()
end)
end)
+ describe("#add_error()", function ()
+ describe("basics", function ()
+ local s = st.stanza("custom", { xmlns = "urn:example:foo" });
+ local e = s:add_error("cancel", "not-acceptable", "UNACCEPTABLE!!!! ONE MILLION YEARS DUNGEON!")
+ :tag("dungeon", { xmlns = "urn:uuid:c9026187-5b05-4e70-b265-c3b6338a7d0f", period="1000000years"});
+ assert.equal(s, e);
+ 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.is_nil(extra);
+ end)
+ end)
+
describe("should reject #invalid", function ()
local invalid_names = {
["empty string"] = "", ["characters"] = "<>";
diff --git a/spec/util_strbitop.lua b/spec/util_strbitop_spec.lua
index 58a13772..963c9516 100644
--- a/spec/util_strbitop.lua
+++ b/spec/util_strbitop_spec.lua
@@ -38,4 +38,48 @@ describe("util.strbitop", function ()
assert.equal("hello", strbitop.sxor("hello", ""));
end);
end);
+
+ describe("common_prefix_bits()", function ()
+ local function B(s)
+ assert(#s%8==0, "Invalid test input: B(s): s should be a multiple of 8 bits in length");
+ local byte = 0;
+ local out_str = {};
+ for i = 1, #s do
+ local bit_ascii = s:byte(i);
+ if bit_ascii == 49 then -- '1'
+ byte = byte + 2^((7-(i-1))%8);
+ elseif bit_ascii ~= 48 then
+ error("Invalid test input: B(s): s should contain only '0' or '1' characters");
+ end
+ if (i-1)%8 == 7 then
+ table.insert(out_str, string.char(byte));
+ byte = 0;
+ end
+ end
+ return table.concat(out_str);
+ end
+
+ local _cpb = strbitop.common_prefix_bits;
+ local function test(a, b)
+ local Ba, Bb = B(a), B(b);
+ local ret1 = _cpb(Ba, Bb);
+ local ret2 = _cpb(Bb, Ba);
+ assert(ret1 == ret2, ("parameter order should not make a difference to the result (%s, %s) = %d, reversed = %d"):format(a, b, ret1, ret2));
+ return ret1;
+ end
+
+ it("works on single bytes", function ()
+ assert.equal(0, test("00000000", "11111111"));
+ assert.equal(1, test("10000000", "11111111"));
+ assert.equal(0, test("01000000", "11111111"));
+ assert.equal(0, test("01000000", "11111111"));
+ assert.equal(8, test("11111111", "11111111"));
+ end);
+
+ it("works on multiple bytes", function ()
+ for i = 0, 16 do
+ assert.equal(i, test(string.rep("1", i)..string.rep("0", 16-i), "1111111111111111"));
+ end
+ end);
+ end);
end);
diff --git a/spec/util_table_spec.lua b/spec/util_table_spec.lua
index 76f54b69..574967f3 100644
--- a/spec/util_table_spec.lua
+++ b/spec/util_table_spec.lua
@@ -12,6 +12,48 @@ describe("util.table", function ()
assert.same({ "lorem", "ipsum", "dolor", "sit", "amet", n = 5 }, u_table.pack("lorem", "ipsum", "dolor", "sit", "amet"));
end);
end);
+
+ describe("move()", function ()
+ it("works", function ()
+ local t1 = { "apple", "banana", "carrot" };
+ local t2 = { "cat", "donkey", "elephant" };
+ local t3 = {};
+ u_table.move(t1, 1, 3, 1, t3);
+ u_table.move(t2, 1, 3, 3, t3);
+ assert.same({ "apple", "banana", "cat", "donkey", "elephant" }, t3);
+ end);
+ it("supports overlapping regions", function ()
+ do
+ local t1 = { "apple", "banana", "carrot", "date", "endive", "fig", "grapefruit" };
+ u_table.move(t1, 1, 3, 3);
+ assert.same({ "apple", "banana", "apple", "banana", "carrot", "fig", "grapefruit" }, t1);
+ end
+
+ do
+ local t1 = { "apple", "banana", "carrot", "date", "endive", "fig", "grapefruit" };
+ u_table.move(t1, 1, 3, 2);
+ assert.same({ "apple", "apple", "banana", "carrot", "endive", "fig", "grapefruit" }, t1);
+ end
+
+ do
+ local t1 = { "apple", "banana", "carrot", "date", "endive", "fig", "grapefruit" };
+ u_table.move(t1, 3, 5, 2);
+ assert.same({ "apple", "carrot", "date", "endive", "endive", "fig", "grapefruit" }, t1);
+ end
+
+ do
+ local t1 = { "apple", "banana", "carrot", "date", "endive", "fig", "grapefruit" };
+ u_table.move(t1, 3, 5, 6);
+ assert.same({ "apple", "banana", "carrot", "date", "endive", "carrot", "date", "endive" }, t1);
+ end
+
+ do
+ local t1 = { "apple", "banana", "carrot", "date", "endive", "fig", "grapefruit" };
+ u_table.move(t1, 3, 1, 3);
+ assert.same({ "apple", "banana", "carrot", "date", "endive", "fig", "grapefruit" }, t1);
+ end
+ end);
+ end);
end);
diff --git a/spec/util_throttle_spec.lua b/spec/util_throttle_spec.lua
index 985afae8..077f3d40 100644
--- a/spec/util_throttle_spec.lua
+++ b/spec/util_throttle_spec.lua
@@ -1,4 +1,5 @@
+-- luacheck: ignore 411/a
-- Mock util.time
local now = 0; -- wibbly-wobbly... timey-wimey... stuff
diff --git a/spec/util_uuid_spec.lua b/spec/util_uuid_spec.lua
index 95ae0a20..7157d0ec 100644
--- a/spec/util_uuid_spec.lua
+++ b/spec/util_uuid_spec.lua
@@ -5,7 +5,7 @@ local uuid = require "util.uuid";
describe("util.uuid", function()
describe("#generate()", function()
it("should work follow the UUID pattern", function()
- -- https://tools.ietf.org/html/rfc4122#section-4.4
+ -- https://www.rfc-editor.org/rfc/rfc4122.html#section-4.4
local pattern = "^" .. table.concat({
string.rep("%x", 8),
@@ -20,6 +20,28 @@ describe("util.uuid", function()
for _ = 1, 100 do
assert.is_string(uuid.generate():match(pattern));
end
+
+ assert.truthy(uuid.generate() ~= uuid.generate(), "does not generate the same UUIDv4 twice")
+ end);
+ end);
+ describe("#v7", function()
+ it("should also follow the UUID pattern", function()
+ local pattern = "^" .. table.concat({
+ string.rep("%x", 8),
+ string.rep("%x", 4),
+ "7" .. -- version
+ string.rep("%x", 3),
+ "[89ab]" .. -- reserved bits of 1 and 0
+ string.rep("%x", 3),
+ string.rep("%x", 12),
+ }, "%-") .. "$";
+
+ local one = uuid.v7(); -- one before the loop to ensure some time passes
+ for _ = 1, 100 do
+ assert.is_string(uuid.v7():match(pattern));
+ end
+ -- one after the loop when some time should have passed
+ assert.truthy(one < uuid.v7(), "should be ordererd")
end);
end);
end);
diff --git a/spec/util_xtemplate_spec.lua b/spec/util_xtemplate_spec.lua
new file mode 100644
index 00000000..421be43f
--- /dev/null
+++ b/spec/util_xtemplate_spec.lua
@@ -0,0 +1,47 @@
+local st = require "prosody.util.stanza";
+local xtemplate = require "prosody.util.xtemplate";
+
+describe("util.xtemplate", function ()
+ describe("render()", function ()
+ it("works", function ()
+ assert.same("Hello", xtemplate.render("{greeting}", st.stanza("root"):text_tag("greeting", "Hello")), "regular text content")
+ assert.same("Hello", xtemplate.render("{#}", st.stanza("root"):text("Hello")), "top tag text content")
+ assert.same("Hello", xtemplate.render("{greeting/@en}", st.stanza("root"):tag("greeting", { en = "Hello" })), "attribute")
+ end)
+ it("supports conditionals", function ()
+ local atom_tmpl = "{@pubsub:title|and{*{@pubsub:title}*\n\n}}{summary|or{{author/name|and{{author/name} posted }}{title}}}";
+ local atom_data = st.stanza("entry", { xmlns = "http://www.w3.org/2005/Atom" });
+ assert.same("", xtemplate.render(atom_tmpl, atom_data));
+
+ atom_data:text_tag("title", "an Entry")
+ assert.same("an Entry", xtemplate.render(atom_tmpl, atom_data));
+
+ atom_data:tag("author"):text_tag("name","Juliet"):up();
+ assert.same("Juliet posted an Entry", xtemplate.render(atom_tmpl, atom_data));
+
+ atom_data:text_tag("summary", "Juliet just posted a new entry");
+ assert.same("Juliet just posted a new entry", xtemplate.render(atom_tmpl, atom_data));
+
+ atom_data.attr["xmlns:pubsub"] = "http://jabber.org/protocol/pubsub";
+ atom_data.attr["pubsub:title"] = "Juliets musings";
+ assert.same("*Juliets musings*\n\nJuliet just posted a new entry", xtemplate.render(atom_tmpl, atom_data));
+ end)
+ it("can strip surrounding whitespace", function ()
+ assert.same("Hello ", xtemplate.render(" {-greeting} ", st.stanza("root"):text_tag("greeting", "Hello")))
+ assert.same(" Hello", xtemplate.render(" {greeting-} ", st.stanza("root"):text_tag("greeting", "Hello")))
+ assert.same("Hello", xtemplate.render(" {-greeting-} ", st.stanza("root"):text_tag("greeting", "Hello")))
+ end)
+ describe("each", function ()
+ it("makes sense", function ()
+ local x = st.stanza("root"):tag("foo"):tag("bar")
+ for i = 1, 5 do x:text_tag("i", tostring(i)); end
+ x:reset();
+ assert.same("12345", xtemplate.render("{foo/bar|each(i){{#}}}", x));
+ end)
+ it("handles missing inputs", function ()
+ local x = st.stanza("root");
+ assert.same("", xtemplate.render("{foo/bar|each(i){{#}}}", x));
+ end)
+ end)
+ end)
+end)