mirror of
https://github.com/lux-tel/libsession-util.git
synced 2026-10-30 20:25:11 +00:00
Session clients required the list of hashes from a merge() to perform various operations based on the messages that were accepted. This updates `merge()` to return a vector of accepted hashes. For the C API, this returns a config_string_list* pointer that needs to be freed, but contains all the good hashes (the same way current_hashes() works).
649 lines
26 KiB
C++
649 lines
26 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 <session/config/user_groups.hpp>
|
|
#include <string_view>
|
|
#include <variant>
|
|
|
|
#include "utils.hpp"
|
|
|
|
using namespace std::literals;
|
|
using namespace oxenc::literals;
|
|
|
|
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{ustring_view{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;
|
|
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 closed groups (rather it
|
|
// uses X25519 keys) but for this test the distinction doesn't matter.
|
|
c.enc_pubkey.assign(lg_pk.data(), lg_pk.size());
|
|
c.enc_seckey.assign(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 open_group_pubkey =
|
|
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hexbytes;
|
|
|
|
auto og = groups.get_or_construct_community(
|
|
"http://Example.ORG:5678", "SudokuRoom", open_group_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 [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.current_hashes() == std::vector{{"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.current_hashes() == std::vector{{"fakehash1"s}});
|
|
|
|
CHECK(g2.size() == 2);
|
|
CHECK(g2.size_communities() == 1);
|
|
CHECK(g2.size_legacy_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: " + lg->name + ", " + std::to_string(admins) + " admins, " +
|
|
std::to_string(members) + " members");
|
|
} else if (auto* og = std::get_if<session::config::community_info>(&group)) {
|
|
seen.push_back("community: " + og->base_url() + "/r/" + og->room());
|
|
} else {
|
|
seen.push_back("unknown");
|
|
}
|
|
}
|
|
|
|
CHECK(seen == std::vector<std::string>{
|
|
"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(open_group_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.current_hashes().empty());
|
|
std::tie(seqno, to_push, obs) = g2.push();
|
|
CHECK(g2.current_hashes().empty());
|
|
auto to_push2 = to_push;
|
|
CHECK(seqno == 2);
|
|
g2.confirm_pushed(seqno, "fakehash2");
|
|
CHECK(g2.current_hashes() == std::vector{{"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());
|
|
|
|
std::vector<std::pair<std::string, ustring>> to_merge;
|
|
to_merge.emplace_back("fakehash2", to_push);
|
|
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() == 2);
|
|
CHECK(groups.size_communities() == 1);
|
|
CHECK(groups.size_legacy_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.current_hashes() == std::vector{{"fakehash3"s}});
|
|
|
|
to_merge.clear();
|
|
to_merge.emplace_back("fakehash3", to_push);
|
|
groups.merge(to_merge);
|
|
CHECK(groups.size() == 1);
|
|
CHECK(groups.size_communities() == 0);
|
|
CHECK(groups.size_legacy_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() == 5);
|
|
CHECK(groups.size_communities() == 4);
|
|
CHECK(groups.size_legacy_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"));
|
|
|
|
to_merge.clear();
|
|
// Load some obsolete ones in just to check that they get immediately obsoleted
|
|
to_merge.emplace_back("fakehash10", to_push3);
|
|
to_merge.emplace_back("fakehash11", to_push1);
|
|
to_merge.emplace_back("fakehash12", to_push2);
|
|
to_merge.emplace_back("fakehash4", to_push);
|
|
g2.merge(to_merge);
|
|
CHECK(g2.needs_dump());
|
|
CHECK_FALSE(g2.needs_push());
|
|
CHECK(g2.current_hashes() == std::vector{{"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: " + lg->name + ", " + std::to_string(admins) + " admins, " +
|
|
std::to_string(members) + " members");
|
|
} else if (auto* og = std::get_if<session::config::community_info>(&group)) {
|
|
seen.push_back("community: " + og->base_url() + "/r/" + og->room());
|
|
} else {
|
|
seen.push_back("unknown");
|
|
}
|
|
}
|
|
|
|
CHECK(seen == std::vector<std::string>{
|
|
"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 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_current_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_current_hashes(conf);
|
|
REQUIRE(hashes);
|
|
CHECK(hashes->len == 0);
|
|
free(hashes);
|
|
|
|
config_confirm_pushed(conf, to_push->seqno, "fakehash1");
|
|
|
|
hashes = config_current_hashes(conf);
|
|
REQUIRE(hashes);
|
|
REQUIRE(hashes->len == 1);
|
|
CHECK(hashes->value[0] == "fakehash1"sv);
|
|
free(hashes);
|
|
|
|
session::config::UserGroups c2{ustring_view{seed}, std::nullopt};
|
|
|
|
std::vector<std::pair<std::string, ustring_view>> to_merge;
|
|
to_merge.emplace_back("fakehash1", ustring_view{to_push->config, to_push->config_len});
|
|
CHECK(c2.merge(to_merge) == std::vector<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{ustring_view{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);
|
|
}
|
|
}
|
|
|
|
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
|