Files
libsession-util/tests/test_config_user_groups.cpp

923 lines
38 KiB
C++

#include <oxenc/hex.h>
#include <session/config/user_groups.h>
#include <sodium/crypto_sign_ed25519.h>
#include <catch2/catch_test_macros.hpp>
#include <chrono>
#include <iostream>
#include <session/config/user_groups.hpp>
#include <string_view>
#include <variant>
#include "session/config/notify.hpp"
#include "utils.hpp"
static constexpr int64_t created_ts = 1680064059;
TEST_CASE("Open Group URLs", "[config][community_urls]") {
using namespace session::config;
auto [base1, room1, pk1] = community::parse_full_url(
"https://example.com/"
"SomeRoom?public_key=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef");
auto [base2, room2, pk2] = community::parse_full_url(
"HTTPS://EXAMPLE.COM/"
"sOMErOOM?public_key=0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF");
auto [base3, room3, pk3] = community::parse_full_url(
"HTTPS://EXAMPLE.COM/r/"
"someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF");
auto [base4, room4, pk4] = community::parse_full_url(
"http://example.com/r/"
"someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF");
auto [base5, room5, pk5] = community::parse_full_url(
"HTTPS://EXAMPLE.com:443/r/"
"someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF");
auto [base6, room6, pk6] = community::parse_full_url(
"HTTP://EXAMPLE.com:80/r/"
"someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF");
auto [base7, room7, pk7] = community::parse_full_url(
"http://example.com:80/r/"
"someroom?public_key=ASNFZ4mrze8BI0VniavN7wEjRWeJq83vASNFZ4mrze8");
auto [base8, room8, pk8] = community::parse_full_url(
"http://example.com:80/r/"
"someroom?public_key=yrtwk3hjixg66yjdeiuauk6p7hy1gtm8tgih55abrpnsxnpm3zzo");
CHECK(base1 == "https://example.com");
CHECK(base1 == base2);
CHECK(base1 == base3);
CHECK(base1 != base4);
CHECK(base4 == "http://example.com");
CHECK(base1 == base5);
CHECK(base4 == base6);
CHECK(base4 == base7);
CHECK(base4 == base8);
CHECK(room1 == "SomeRoom");
CHECK(room2 == "sOMErOOM");
CHECK(room3 == "someroom");
CHECK(room4 == "someroom");
CHECK(room5 == "someroom");
CHECK(room6 == "someroom");
CHECK(room7 == "someroom");
CHECK(room8 == "someroom");
CHECK(community::canonical_room(room1) == "someroom");
CHECK(community::canonical_room(room2) == "someroom");
CHECK(community::canonical_room(room3) == "someroom");
CHECK(community::canonical_room(room4) == "someroom");
CHECK(community::canonical_room(room5) == "someroom");
CHECK(community::canonical_room(room6) == "someroom");
CHECK(community::canonical_room(room7) == "someroom");
CHECK(community::canonical_room(room8) == "someroom");
CHECK(to_hex(pk1) == "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef");
CHECK(to_hex(pk2) == "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef");
CHECK(to_hex(pk3) == "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef");
CHECK(to_hex(pk4) == "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef");
CHECK(to_hex(pk5) == "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef");
CHECK(to_hex(pk6) == "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef");
CHECK(to_hex(pk7) == "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef");
CHECK(to_hex(pk8) == "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef");
}
TEST_CASE("User Groups", "[config][groups]") {
const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes;
std::array<unsigned char, 32> ed_pk, curve_pk;
std::array<unsigned char, 64> ed_sk;
crypto_sign_ed25519_seed_keypair(
ed_pk.data(), ed_sk.data(), reinterpret_cast<const unsigned char*>(seed.data()));
int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data());
REQUIRE(rc == 0);
REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) ==
"4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7");
REQUIRE(oxenc::to_hex(curve_pk.begin(), curve_pk.end()) ==
"d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72");
CHECK(oxenc::to_hex(seed.begin(), seed.end()) ==
oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32));
session::config::UserGroups groups{std::span<const unsigned char>{seed}, std::nullopt};
constexpr auto definitely_real_id =
"055000000000000000000000000000000000000000000000000000000000000000"sv;
int64_t now = std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
CHECK_FALSE(groups.get_legacy_group(definitely_real_id));
CHECK(groups.empty());
CHECK(groups.size() == 0);
auto c = groups.get_or_construct_legacy_group(definitely_real_id);
CHECK(c.session_id == definitely_real_id);
CHECK(c.disappearing_timer == 0min);
CHECK(c.enc_pubkey.empty());
CHECK(c.enc_seckey.empty());
CHECK(c.priority == 0);
CHECK(c.name == "");
CHECK(c.members().empty());
CHECK(c.joined_at == 0);
CHECK(c.notifications == session::config::notify_mode::defaulted);
CHECK(c.mute_until == 0);
CHECK_FALSE(groups.needs_push());
CHECK_FALSE(groups.needs_dump());
CHECK(std::get<seqno_t>(groups.push()) == 0);
std::vector<std::string> users = {
"050000000000000000000000000000000000000000000000000000000000000000"s,
"051111111111111111111111111111111111111111111111111111111111111111"s,
"052222222222222222222222222222222222222222222222222222222222222222"s,
"053333333333333333333333333333333333333333333333333333333333333333"s,
"054444444444444444444444444444444444444444444444444444444444444444"s,
"055555555555555555555555555555555555555555555555555555555555555555"s,
"056666666666666666666666666666666666666666666666666666666666666666"s};
c.name = "Englishmen";
c.disappearing_timer = 60min;
c.joined_at = created_ts * 1000; // milliseconds
c.notifications = session::config::notify_mode::mentions_only;
c.mute_until = now + 3600;
CHECK(c.insert(users[0], false));
CHECK(c.insert(users[1], true));
CHECK(c.insert(users[2], false));
CHECK(c.insert(users[4], true));
CHECK(c.insert(users[5], false));
CHECK_FALSE(c.insert(users[2], false));
CHECK(c.insert(users[2], true)); // Flip to admin
CHECK(c.insert(users[1], false)); // Flip to non-admin
CHECK_THROWS_AS(c.insert("0505050505", false), std::invalid_argument);
CHECK_THROWS_AS(
c.insert("020000000000000000000000000000000000000000000000000000000000000000", true),
std::invalid_argument);
CHECK(c.erase(users[5]));
CHECK(c.erase(users[4]));
std::map<std::string, bool> expected_members{
{users[0], false}, {users[1], false}, {users[2], true}};
CHECK(c.members() == expected_members);
const auto lgroup_seed =
"00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes;
std::array<unsigned char, 32> lg_pk;
std::array<unsigned char, 64> lg_sk;
crypto_sign_ed25519_seed_keypair(
lg_pk.data(), lg_sk.data(), reinterpret_cast<const unsigned char*>(lgroup_seed.data()));
// Note: this isn't exactly what Session actually does here for legacy groups (rather it
// uses X25519 keys) but for this test the distinction doesn't matter.
c.enc_pubkey.assign(lg_pk.data(), lg_pk.data() + lg_pk.size());
c.enc_seckey.assign(lg_sk.data(), lg_sk.data() + 32);
c.priority = 3;
CHECK(to_hex(c.enc_pubkey) == oxenc::to_hex(lg_pk.begin(), lg_pk.end()));
CHECK(to_hex(c.enc_seckey) == oxenc::to_hex(lg_sk.begin(), lg_sk.begin() + 32));
// The new data doesn't get stored until we call this:
groups.set(c);
REQUIRE(groups.get_legacy_group(definitely_real_id).has_value());
CHECK(groups.needs_push());
CHECK(groups.needs_dump());
const auto community_pubkey =
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hexbytes;
auto og = groups.get_or_construct_community(
"http://Example.ORG:5678", "SudokuRoom", community_pubkey);
CHECK(og.base_url() == "http://example.org:5678"); // Note: lower-case
CHECK(og.room() == "SudokuRoom"); // Note: case-preserving
CHECK(og.room_norm() == "sudokuroom");
CHECK(og.pubkey().size() == 32);
CHECK(og.pubkey_hex() == "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef");
og.priority = 14;
// The new data doesn't get stored until we call this:
groups.set(og);
auto fake_group_id = "030101010101010101010101010101010101010101010101010101010101010101"s;
auto ggg = groups.get_or_construct_group(fake_group_id);
groups.set(ggg);
auto [seqno, to_push, obs] = groups.push();
auto to_push1 = to_push;
CHECK(seqno == 1);
CHECK(obs.empty());
// Pretend we uploaded it
groups.confirm_pushed(seqno, {"fakehash1"});
CHECK(groups.needs_dump());
CHECK_FALSE(groups.needs_push());
session::config::UserGroups g2{seed, groups.dump()};
CHECK_FALSE(groups.needs_push());
CHECK_FALSE(groups.needs_dump());
std::tie(seqno, to_push, obs) = groups.push();
CHECK(seqno == 1);
CHECK(obs.empty());
CHECK(groups.curr_hashes() == std::unordered_set{{"fakehash1"s}});
CHECK_FALSE(groups.needs_dump()); // Because we just called dump() above, to load up g2
CHECK_FALSE(g2.needs_push());
CHECK_FALSE(g2.needs_dump());
std::tie(seqno, to_push, obs) = g2.push();
CHECK(seqno == 1);
CHECK_FALSE(g2.needs_dump());
CHECK(obs.empty());
CHECK(g2.curr_hashes() == std::unordered_set{{"fakehash1"s}});
CHECK(g2.size() == 3);
CHECK(g2.size_communities() == 1);
CHECK(g2.size_legacy_groups() == 1);
CHECK(g2.size_groups() == 1);
auto x1 = g2.get_legacy_group(definitely_real_id);
REQUIRE(x1);
auto& c1 = *x1;
CHECK(to_hex(c1.enc_pubkey) == oxenc::to_hex(lg_pk.begin(), lg_pk.end()));
CHECK(to_hex(c1.enc_seckey) == oxenc::to_hex(lg_sk.begin(), lg_sk.begin() + 32));
CHECK(c1.disappearing_timer == 60min);
CHECK(c1.session_id == definitely_real_id);
CHECK(c1.priority == 3);
CHECK(c1.members() == expected_members);
CHECK(c1.name == "Englishmen");
CHECK(c1.joined_at == created_ts);
CHECK(c1.notifications == session::config::notify_mode::mentions_only);
CHECK(c1.mute_until == now + 3600);
CHECK_FALSE(g2.needs_push());
CHECK_FALSE(g2.needs_dump());
std::tie(seqno, to_push, obs) = g2.push();
CHECK(seqno == 1);
CHECK_FALSE(g2.needs_dump());
for (auto* g : {&groups, &g2}) {
std::vector<std::string> seen;
for (const auto& group : *g) {
if (auto* lg = std::get_if<session::config::legacy_group_info>(&group)) {
auto [admins, members] = lg->counts();
seen.push_back(
"legacy: {}, {} admins, {} members"_format(lg->name, admins, members));
} else if (auto* og = std::get_if<session::config::community_info>(&group)) {
seen.push_back("community: " + og->base_url() + "/r/" + og->room());
} else if (auto* g = std::get_if<session::config::group_info>(&group)) {
seen.push_back("group: " + g->id);
} else {
seen.push_back("unknown");
}
}
CHECK(seen == std::vector<std::string>{
"group: " + fake_group_id,
"community: http://example.org:5678/r/SudokuRoom",
"legacy: Englishmen, 1 admins, 2 members",
});
}
auto x2 = g2.get_community("http://EXAMPLE.org:5678", "sudokuRoom");
REQUIRE(x2);
CHECK(x2->base_url() == "http://example.org:5678");
CHECK(x2->room() == "SudokuRoom"); // Case preserved from the stored value, not the input value
CHECK(x2->room_norm() == "sudokuroom");
CHECK(x2->pubkey_hex() == to_hex(community_pubkey));
CHECK(x2->priority == 14);
CHECK_FALSE(g2.needs_push());
CHECK_FALSE(g2.needs_dump());
CHECK(std::get<seqno_t>(g2.push()) == 1);
CHECK_FALSE(g2.needs_dump());
x2->set_room("sudokuRoom"); // Change capitalization
g2.set(*x2);
CHECK(g2.needs_push());
CHECK(g2.needs_dump());
CHECK(g2.curr_hashes().empty());
std::tie(seqno, to_push, obs) = g2.push();
CHECK(g2.curr_hashes().empty());
auto to_push2 = to_push;
CHECK(seqno == 2);
g2.confirm_pushed(seqno, {"fakehash2"});
CHECK(g2.curr_hashes() == std::unordered_set{{"fakehash2"s}});
CHECK(as_set(obs) == make_set("fakehash1"s));
g2.dump();
CHECK_FALSE(g2.needs_push());
CHECK_FALSE(g2.needs_dump());
CHECK(std::get<seqno_t>(g2.push()) == 2);
CHECK_FALSE(g2.needs_dump());
REQUIRE(to_push.size() == 1);
std::vector<std::pair<std::string, std::vector<unsigned char>>> to_merge;
to_merge.emplace_back("fakehash2", to_push[0]);
groups.merge(to_merge);
auto x3 = groups.get_community("http://example.org:5678", "SudokuRoom");
REQUIRE(x3.has_value());
CHECK(x3->room() == "sudokuRoom"); // We picked up the capitalization change
CHECK(groups.size() == 3);
CHECK(groups.size_communities() == 1);
CHECK(groups.size_legacy_groups() == 1);
CHECK(groups.size_groups() == 1);
CHECK(c1.insert(users[4], false));
CHECK(c1.insert(users[5], true));
CHECK(c1.insert(users[6], true));
CHECK(c1.erase(users[1]));
expected_members.emplace(users[4], false);
expected_members.emplace(users[5], true);
expected_members.emplace(users[6], true);
expected_members.erase(users[1]);
CHECK_FALSE(g2.needs_push());
CHECK_FALSE(g2.needs_dump());
std::tie(seqno, to_push, obs) = g2.push();
CHECK(seqno == 2);
CHECK_FALSE(g2.needs_dump());
g2.set(c1);
CHECK(g2.needs_push());
CHECK(g2.needs_dump());
g2.erase_community("http://exAMple.ORG:5678/", "sudokuROOM");
std::tie(seqno, to_push, obs) = g2.push();
g2.confirm_pushed(seqno, {"fakehash3"});
auto to_push3 = to_push;
CHECK(seqno == 3);
CHECK(as_set(obs) == make_set("fakehash2"s));
CHECK(g2.curr_hashes() == std::unordered_set{{"fakehash3"s}});
to_merge.clear();
REQUIRE(to_push.size() == 1);
to_merge.emplace_back("fakehash3", to_push[0]);
groups.merge(to_merge);
CHECK(groups.size() == 2);
CHECK(groups.size_communities() == 0);
CHECK(groups.size_legacy_groups() == 1);
CHECK(groups.size_groups() == 1);
int prio = 0;
auto beanstalk_pubkey = "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff";
for (auto room : {"fee", "fi", "fo", "fum"}) {
auto g = groups.get_or_construct_community(
"http://jacksbeanstalk.org", room, beanstalk_pubkey);
g.priority = prio++;
groups.set(g);
}
CHECK(groups.size() == 6);
CHECK(groups.size_communities() == 4);
CHECK(groups.size_legacy_groups() == 1);
CHECK(groups.size_groups() == 1);
std::tie(seqno, to_push, obs) = groups.push();
groups.confirm_pushed(seqno, {"fakehash4"});
CHECK(seqno == 4);
CHECK(as_set(obs) == make_set("fakehash1"s, "fakehash2", "fakehash3"));
REQUIRE(to_push.size() == 1);
REQUIRE(to_push1.size() == 1);
REQUIRE(to_push2.size() == 1);
REQUIRE(to_push3.size() == 1);
to_merge.clear();
// Load some obsolete ones in just to check that they get immediately obsoleted
to_merge.emplace_back("fakehash10", to_push3[0]);
to_merge.emplace_back("fakehash11", to_push1[0]);
to_merge.emplace_back("fakehash12", to_push2[0]);
to_merge.emplace_back("fakehash4", to_push[0]);
g2.merge(to_merge);
CHECK(g2.needs_dump());
CHECK_FALSE(g2.needs_push());
CHECK(g2.curr_hashes() == std::unordered_set{{"fakehash4"s}});
std::tie(seqno, to_push, obs) = g2.push();
CHECK(seqno == 4);
CHECK(as_set(obs) == make_set("fakehash10"s, "fakehash11", "fakehash12", "fakehash3"));
for (auto* g : {&groups, &g2}) {
std::vector<std::string> seen;
for (const auto& group : *g) {
if (auto* lg = std::get_if<session::config::legacy_group_info>(&group)) {
auto [admins, members] = lg->counts();
seen.push_back(
"legacy: {}, {} admins, {} members"_format(lg->name, admins, members));
} else if (auto* og = std::get_if<session::config::community_info>(&group)) {
seen.push_back("community: " + og->base_url() + "/r/" + og->room());
} else if (auto* g = std::get_if<session::config::group_info>(&group)) {
seen.push_back("group: " + g->id);
} else {
seen.push_back("unknown");
}
}
CHECK(seen == std::vector<std::string>{
"group: " + fake_group_id,
"community: http://jacksbeanstalk.org/r/fee",
"community: http://jacksbeanstalk.org/r/fi",
"community: http://jacksbeanstalk.org/r/fo",
"community: http://jacksbeanstalk.org/r/fum",
"legacy: Englishmen, 3 admins, 2 members",
});
}
}
TEST_CASE("User Groups -- (non-legacy) groups", "[config][groups][new]") {
const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes;
std::array<unsigned char, 32> ed_pk, curve_pk;
std::array<unsigned char, 64> ed_sk;
crypto_sign_ed25519_seed_keypair(
ed_pk.data(), ed_sk.data(), reinterpret_cast<const unsigned char*>(seed.data()));
int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data());
REQUIRE(rc == 0);
REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) ==
"4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7");
REQUIRE(oxenc::to_hex(curve_pk.begin(), curve_pk.end()) ==
"d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72");
CHECK(oxenc::to_hex(seed.begin(), seed.end()) ==
oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32));
session::config::UserGroups groups{std::span<const unsigned char>{seed}, std::nullopt};
constexpr auto definitely_real_id =
"035000000000000000000000000000000000000000000000000000000000000000"sv;
CHECK_FALSE(groups.get_group(definitely_real_id));
CHECK(groups.empty());
CHECK(groups.size() == 0);
auto c = groups.get_or_construct_group(definitely_real_id);
CHECK(c.secretkey.empty());
CHECK(c.id == definitely_real_id);
CHECK(c.priority == 0);
CHECK(c.joined_at == 0);
CHECK(c.notifications == session::config::notify_mode::defaulted);
CHECK(c.mute_until == 0);
c.secretkey = session::to_vector(ed_sk); // This *isn't* the right secret key for the group, so
// won't propagate, and so auth data will:
c.auth_data =
"01020304050000000000000000000000000000000000000000000000000000000000000000000000000000"
"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
"0000000000000000000000000000"_hexbytes;
groups.set(c);
CHECK(groups.needs_push());
CHECK(groups.needs_dump());
auto [seqno, to_push, obs] = groups.push();
groups.confirm_pushed(seqno, {"fakehash1"});
auto d1 = groups.dump();
session::config::UserGroups g2{std::span<const unsigned char>{seed}, d1};
auto c2 = g2.get_group(definitely_real_id);
REQUIRE(c2.has_value());
CHECK(c2->id == definitely_real_id);
CHECK(c2->priority == 0);
CHECK(c2->joined_at == 0);
CHECK(c2->notifications == session::config::notify_mode::defaulted);
CHECK(c2->mute_until == 0);
CHECK_FALSE(c2->invited);
CHECK(c2->name == "");
c2->priority = 123;
c2->joined_at = (int64_t)1'234'567'890 * 1'000;
c2->notifications = session::config::notify_mode::mentions_only;
c2->mute_until = (int64_t)456'789'012 * 1'000'000;
c2->invited = true;
c2->name = "Magic Special Room";
g2.set(*c2);
auto c2b = g2.get_or_construct_group("03" + oxenc::to_hex(ed_pk.begin(), ed_pk.end()));
c2b.secretkey =
session::to_vector(ed_sk); // This one does match the group ID, so should propagate
c2b.auth_data = // should get ignored, since we have a valid secret key set:
"01020304050000000000000000000000000000000000000000000000000000000000000000000000000000"
"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
"0000000000000000000000000000"_hexbytes;
g2.set(c2b);
std::tie(seqno, to_push, obs) = g2.push();
g2.confirm_pushed(seqno, {"fakehash2"});
REQUIRE(to_push.size() == 1);
std::vector<std::pair<std::string, std::vector<unsigned char>>> to_merge;
to_merge.emplace_back("fakehash2", to_push[0]);
groups.merge(to_merge);
auto c3 = groups.get_group(definitely_real_id);
REQUIRE(c3.has_value());
CHECK(c3->secretkey.empty());
CHECK(to_hex(c3->auth_data) ==
"01020304050000000000000000000000000000000000000000000000000000000000000000000000000000"
"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
"0000000000000000000000000000");
CHECK(c3->id == definitely_real_id);
CHECK(c3->priority == 123);
CHECK(c3->joined_at == 1234567890);
CHECK(c3->notifications == session::config::notify_mode::mentions_only);
CHECK(c3->mute_until == 456789012);
CHECK(c3->invited);
CHECK(c3->name == "Magic Special Room");
groups.erase(*c3);
auto c3b = groups.get_group("03" + oxenc::to_hex(ed_pk.begin(), ed_pk.end()));
REQUIRE(c3b);
CHECK(c3b->auth_data.empty());
CHECK(to_hex(c3b->secretkey) == to_hex(seed) + oxenc::to_hex(ed_pk.begin(), ed_pk.end()));
CHECK_FALSE(c3b->kicked());
c3b->auth_data.resize(100);
CHECK_FALSE(c3b->kicked());
// mark ourselves as kicked
c3b->mark_kicked();
CHECK(c3b->kicked());
CHECK(c3b->secretkey.empty());
CHECK(c3b->auth_data.empty());
// add a non empty auth_data, and reset the removed_status: we shouldn't be kicked anymore
c3b->auth_data.resize(100);
c3b->mark_invited();
CHECK_FALSE(c3b->kicked());
// we are not kicked, mark the group as destroyed
c3b->mark_destroyed();
CHECK(c3b->is_destroyed());
// the group was destroyed, so we are not `kicked` from it.
// We keep the states separate as `kicked` is not permanent but `destroyed` is.
CHECK_FALSE(c3b->kicked());
// reset the state to test that the transition kicked->destroyed works only in that direction
c3b->auth_data.resize(100);
c3b->removed_status = 0;
CHECK_FALSE(c3b->kicked());
// kicked->destroyed works
c3b->mark_kicked();
CHECK(c3b->kicked());
c3b->mark_destroyed();
CHECK(c3b->is_destroyed());
// destroyed->kicked doesn't work
c3b->mark_kicked();
CHECK_FALSE(c3b->kicked());
CHECK(c3b->is_destroyed());
auto gg = groups.get_or_construct_group(
"030303030303030303030303030303030303030303030303030303030303030303");
groups.set(gg);
CHECK(groups.erase_group("030303030303030303030303030303030303030303030303030303030303030303"));
CHECK_FALSE(
groups.erase_group("03030303030303030303030303030303030303030303030303030303030303030"
"3"));
}
TEST_CASE("User Groups members C API", "[config][groups][c]") {
const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes;
std::array<unsigned char, 32> ed_pk, curve_pk;
std::array<unsigned char, 64> ed_sk;
crypto_sign_ed25519_seed_keypair(
ed_pk.data(), ed_sk.data(), reinterpret_cast<const unsigned char*>(seed.data()));
int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data());
REQUIRE(rc == 0);
REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) ==
"4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7");
REQUIRE(oxenc::to_hex(curve_pk.begin(), curve_pk.end()) ==
"d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72");
CHECK(oxenc::to_hex(seed.begin(), seed.end()) ==
oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32));
char err[256];
config_object* conf;
rc = user_groups_init(&conf, ed_sk.data(), NULL, 0, err);
REQUIRE(rc == 0);
constexpr auto definitely_real_id =
"055000000000000000000000000000000000000000000000000000000000000000";
ugroups_legacy_group_info* group =
user_groups_get_or_construct_legacy_group(conf, definitely_real_id);
CHECK(group->joined_at == 0);
group->joined_at = created_ts;
std::vector<std::string> users = {
"050000000000000000000000000000000000000000000000000000000000000000"s,
"051111111111111111111111111111111111111111111111111111111111111111"s,
"052222222222222222222222222222222222222222222222222222222222222222"s,
"053333333333333333333333333333333333333333333333333333333333333333"s,
"054444444444444444444444444444444444444444444444444444444444444444"s,
"055555555555555555555555555555555555555555555555555555555555555555"s,
"056666666666666666666666666666666666666666666666666666666666666666"s};
CHECK(ugroups_legacy_member_add(group, users[0].c_str(), false));
CHECK(ugroups_legacy_member_add(group, users[1].c_str(), true));
CHECK(ugroups_legacy_member_add(group, users[2].c_str(), false));
CHECK(ugroups_legacy_member_add(group, users[4].c_str(), true));
CHECK(ugroups_legacy_member_add(group, users[5].c_str(), false));
CHECK_FALSE(ugroups_legacy_member_add(group, users[2].c_str(), false));
CHECK(ugroups_legacy_member_add(group, users[2].c_str(), true)); // Flip to admin
CHECK(ugroups_legacy_member_add(group, users[1].c_str(), false)); // Flip to non-admin
CHECK_FALSE(ugroups_legacy_member_add(group, "0505050505", false)); // bad id
CHECK_FALSE(ugroups_legacy_member_add(
group,
"020000000000000000000000000000000000000000000000000000000000000000",
false)); // bad id
CHECK(ugroups_legacy_member_remove(group, users[5].c_str()));
CHECK(ugroups_legacy_member_remove(group, users[4].c_str()));
std::map<std::string, bool> expected_members{
{users[0], false}, {users[1], false}, {users[2], true}};
std::map<std::string, bool> found_members;
const char* session_id;
bool admin;
ugroups_legacy_members_iterator* it = ugroups_legacy_members_begin(group);
while (ugroups_legacy_members_next(it, &session_id, &admin)) {
found_members[session_id] = admin;
}
ugroups_legacy_members_free(it);
CHECK(found_members == expected_members);
CHECK(ugroups_legacy_members_count(group, NULL, NULL) == 3);
size_t members, admins;
CHECK(ugroups_legacy_members_count(group, &members, &admins) == 3);
CHECK(members == 2);
CHECK(admins == 1);
members = 0;
admins = 0;
CHECK(ugroups_legacy_members_count(group, &members, NULL) == 3);
CHECK(members == 2);
CHECK(ugroups_legacy_members_count(group, NULL, &admins) == 3);
CHECK(admins == 1);
it = ugroups_legacy_members_begin(group);
members = 0;
admins = 0;
while (ugroups_legacy_members_next(it, &session_id, &admin)) {
if (session_id == users[1]) {
ugroups_legacy_members_erase(it);
// Adding while iterating is allowed (if you add ones that come after the current point,
// you'll iterate into them; if they come before you won't).
ugroups_legacy_member_add(group, users[3].c_str(), true);
ugroups_legacy_member_add(group, users[4].c_str(), true);
ugroups_legacy_member_add(group, users[5].c_str(), true);
} else if (admin)
admins++;
else
members++;
}
CHECK(admins == 4);
CHECK(members == 1);
ugroups_legacy_members_free(it);
CHECK(ugroups_legacy_members_count(group, NULL, NULL) == 5);
expected_members.erase(users[1]);
for (auto i : {3, 4, 5})
expected_members.emplace(users[i], true);
// Non-freeing, so we can keep using `group`; this is less common:
user_groups_set_legacy_group(conf, group);
group->session_id[2] = 'e';
// The "normal" way to set a group when you're done with it (also properly frees `group`).
user_groups_set_free_legacy_group(conf, group);
config_string_list* hashes = config_curr_hashes(conf);
REQUIRE(hashes);
CHECK(hashes->len == 0);
free(hashes);
config_push_data* to_push = config_push(conf);
CHECK(to_push->seqno == 1);
hashes = config_curr_hashes(conf);
REQUIRE(hashes);
CHECK(hashes->len == 0);
free(hashes);
const char* tmphash; // test suite cheat: &(tmphash = "asdf") to fake a length-1 array.
config_confirm_pushed(conf, to_push->seqno, &(tmphash = "fakehash1"), 1);
hashes = config_curr_hashes(conf);
REQUIRE(hashes);
REQUIRE(hashes->len == 1);
CHECK(hashes->value[0] == "fakehash1"sv);
free(hashes);
size_t key_len;
unsigned char* keys = config_get_keys(conf, &key_len);
REQUIRE(keys);
REQUIRE(key_len == 1);
session::config::UserGroups c2{std::span<const unsigned char>{seed}, std::nullopt};
REQUIRE(to_push->n_configs == 1);
std::vector<std::pair<std::string, std::span<const unsigned char>>> to_merge;
to_merge.emplace_back(
"fakehash1",
std::span<const unsigned char>{to_push->config[0], to_push->config_lens[0]});
CHECK(c2.merge(to_merge) == std::unordered_set<std::string>{{"fakehash1"}});
auto grp = c2.get_legacy_group(definitely_real_id);
REQUIRE(grp);
CHECK(grp->members() == expected_members);
CHECK(grp->joined_at == created_ts);
}
TEST_CASE("User groups empty member bug", "[config][groups][bug]") {
// Tests a bug where setting legacy group with empty members (or empty admin) list would dirty
// the config, even when the current members (or admin) list is empty. (This isn't strictly
// specific to user groups, but that's where the bug is easily encountered).
const auto seed = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa000000000000000000000000000000000"_hexbytes;
std::array<unsigned char, 32> ed_pk, curve_pk;
std::array<unsigned char, 64> ed_sk;
crypto_sign_ed25519_seed_keypair(
ed_pk.data(), ed_sk.data(), reinterpret_cast<const unsigned char*>(seed.data()));
int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data());
REQUIRE(rc == 0);
CHECK(oxenc::to_hex(seed.begin(), seed.end()) ==
oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32));
session::config::UserGroups c{std::span<const unsigned char>{seed}, std::nullopt};
CHECK_FALSE(c.needs_push());
{
auto lg = c.get_or_construct_legacy_group(
"051234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef");
lg.insert("050000000000000000000000000000000000000000000000000000000000000000", true);
lg.insert("051111111111111111111111111111111111111111111111111111111111111111", true);
lg.insert("052222222222222222222222222222222222222222222222222222222222222222", true);
c.set(lg);
}
CHECK(c.needs_push());
auto [seqno, data, obs] = c.push();
CHECK(seqno == 1);
auto d = c.dump();
c.confirm_pushed(seqno, {"fakehash1"});
CHECK_FALSE(c.needs_push());
{
auto lg = c.get_or_construct_legacy_group(
"051234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef");
lg.insert("050000000000000000000000000000000000000000000000000000000000000000", true);
lg.insert("051111111111111111111111111111111111111111111111111111111111111111", true);
lg.insert("052222222222222222222222222222222222222222222222222222222222222222", true);
// The bug was here: because *members* is empty, we would assign an empty set, which would
// set the dirty flag, even though an empty set gets pruned away, so end up with an empty
// diff.
c.set(lg);
}
{
auto lg = c.get_or_construct_legacy_group(
"051234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef");
// Now add an actual member:
lg.insert("053333333333333333333333333333333333333333333333333333333333333333", false);
c.set(lg);
}
CHECK(c.needs_push());
c.push();
{
auto lg = c.get_or_construct_legacy_group(
"051234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef");
// Remove them again, so that members is empty again (to make sure it gets emptied out in
// storage):
lg.erase("053333333333333333333333333333333333333333333333333333333333333333");
c.set(lg);
}
CHECK(c.needs_push());
c.push();
{
auto lg = c.get_or_construct_legacy_group(
"051234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef");
auto [admins, members] = lg.counts();
CHECK(admins == 3);
CHECK(members == 0);
}
}
TEST_CASE("User groups mute_until & joined_at are always seconds", "[config][groups][bug]") {
// Tests a bug where setting legacy group with empty members (or empty admin) list would dirty
// the config, even when the current members (or admin) list is empty. (This isn't strictly
// specific to user groups, but that's where the bug is easily encountered).
const auto seed = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa000000000000000000000000000000000"_hexbytes;
std::array<unsigned char, 32> ed_pk, curve_pk;
std::array<unsigned char, 64> ed_sk;
crypto_sign_ed25519_seed_keypair(
ed_pk.data(), ed_sk.data(), reinterpret_cast<const unsigned char*>(seed.data()));
int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data());
REQUIRE(rc == 0);
CHECK(oxenc::to_hex(seed.begin(), seed.end()) ==
oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32));
session::config::UserGroups c{std::span<const unsigned char>{seed}, std::nullopt};
CHECK_FALSE(c.needs_push());
{
auto lg = c.get_or_construct_legacy_group(
"051234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef");
int64_t joined_at = get_timestamp_us();
int64_t mute_until = get_timestamp_s();
lg.joined_at = joined_at;
lg.mute_until = mute_until;
c.set(lg);
auto lg2 = c.get_or_construct_legacy_group(
"051234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef");
CHECK(lg2.joined_at == joined_at / 1'000'000); // joined_at was given in microseconds
CHECK(lg2.mute_until == mute_until); // mute_until was given in seconds
c.erase_legacy_group("051234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef");
}
{
auto gr = c.get_or_construct_group(
"031234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef");
int64_t joined_at = get_timestamp_ms();
int64_t mute_until = get_timestamp_us();
gr.joined_at = joined_at;
gr.mute_until = mute_until;
c.set(gr);
auto gr2 = c.get_or_construct_group(
"031234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef");
CHECK(gr2.joined_at == joined_at / 1'000); // joined_at was given in milliseconds
CHECK(gr2.mute_until == mute_until / 1'000'000); // mute_until was given in microseconds
c.erase_group("031234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef");
}
{
const auto community_pubkey =
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hexbytes;
const auto url = "http://example.org:5678";
const auto room = "sudoku_room";
auto comm = c.get_or_construct_community(url, room, community_pubkey);
int64_t joined_at = get_timestamp_ms();
int64_t mute_until = get_timestamp_ms();
comm.joined_at = joined_at;
comm.mute_until = mute_until;
c.set(comm);
auto comm2 = c.get_or_construct_community(url, room, community_pubkey);
CHECK(comm2.joined_at == joined_at / 1'000); // joined_at was given in milliseconds
CHECK(comm2.mute_until == mute_until / 1'000); // mute_until was given in milliseconds
c.erase_community(url, room);
}
{
// this dump has:
// - an invalid joined_at (1'733'979'503'520) and
// - an invalid mute_until (1'733'979'503'520'780) values
const auto dump_with_not_seconds =
"64313a21693165313a243231303a64313a23693165313a2664313a676433333a031234567890abcdef"
"1234"
"567890abcdef1234567890abcdef1234567890abcdef64313a21693137333339373935303335323037"
"3830"
"65313a4b303a313a6a693137333339373935303335323065656565313a3c6c6c69306533323aea173b"
"57be"
"ca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c96564656565313a3d64313a67643333"
"3a03"
"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef64313a21303a313a4b"
"303a"
"313a6a303a65656565313a28303a313a296c6565"_hexbytes;
session::config::UserGroups c2{std::span<const unsigned char>{seed}, dump_with_not_seconds};
auto gr = c2.get_or_construct_group(
"031234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef");
CHECK(gr.joined_at == 1'733'979'503'520 / 1'000);
CHECK(gr.mute_until == 1'733'979'503'520'780 / 1'000'000);
}
}
namespace Catch {
template <>
struct StringMaker<std::pair<const std::string, bool>> {
static std::string convert(const std::pair<const std::string, bool>& value) {
return value.first + "[" + (value.second ? "true" : "false") + "]";
}
};
} // namespace Catch