blob: 808e9725cca6d257d8c024c503659ecdb0bdea07 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// TODO(slbotbm): Add tests for update_permissions after creating create_user, get_user, etc. functions
#include <algorithm>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <string>
#include <thread>
#include <unordered_set>
#include <vector>
#include <gtest/gtest.h>
#include "lib.rs.h"
#include "tests/e2e/test_helpers.hpp"
class LowLevelE2E_Client : public E2ETestFixture {};
class E2E_Client : public E2ETestFixture {};
TEST_F(LowLevelE2E_Client, ConnectAndLogin) {
RecordProperty("description",
"Connects and returns matching login information using binary connection string formats.");
constexpr std::uint32_t root_user_id = 0;
const std::string username = "iggy";
const std::string password = "iggy";
const std::string connection_strings[] = {
"iggy://iggy:iggy@127.0.0.1:8090",
"iggy+tcp://iggy:iggy@127.0.0.1:8090",
"",
};
for (const std::string &connection_string : connection_strings) {
SCOPED_TRACE(connection_string);
iggy::ffi::Client *client = nullptr;
ASSERT_NO_THROW({
client = connection_string.empty() ? iggy::ffi::new_connection({})
: iggy::ffi::from_connection_string(connection_string);
});
ASSERT_NE(client, nullptr);
TrackClient(client);
iggy::ffi::LoginInfo login_info{};
iggy::ffi::ClientInfoDetails me{};
ASSERT_NO_THROW(client->connect());
ASSERT_NO_THROW({ login_info = client->login_user(username, password); });
ASSERT_NO_THROW({ me = client->get_me(); });
EXPECT_EQ(login_info.user_id, root_user_id);
EXPECT_TRUE(me.has_user_id);
EXPECT_EQ(login_info.user_id, me.user_id);
EXPECT_FALSE(login_info.has_access_token);
EXPECT_TRUE(static_cast<std::string>(login_info.access_token).empty());
EXPECT_EQ(login_info.access_token_expiry, 0u);
}
}
TEST_F(LowLevelE2E_Client, HttpLoginReturnsAccessToken) {
RecordProperty("description", "Returns root user information and an access token after HTTP login.");
constexpr std::uint32_t root_user_id = 0;
iggy::ffi::Client *client = nullptr;
ASSERT_NO_THROW({ client = iggy::ffi::from_connection_string("iggy+http://iggy:iggy@127.0.0.1:3000"); });
ASSERT_NE(client, nullptr);
TrackClient(client);
iggy::ffi::LoginInfo login_info{};
ASSERT_NO_THROW(client->connect());
ASSERT_NO_THROW({ login_info = client->login_user("iggy", "iggy"); });
EXPECT_EQ(login_info.user_id, root_user_id);
EXPECT_TRUE(login_info.has_access_token);
EXPECT_FALSE(static_cast<std::string>(login_info.access_token).empty());
EXPECT_NE(login_info.access_token_expiry, 0u);
}
TEST_F(LowLevelE2E_Client, NewConnectionWithMalformedConnectionStringsThrow) {
RecordProperty("description", "Rejects malformed connection strings when creating a new client connection.");
const std::string malformed_connection_strings[] = {
"iggy+invalid://iggy:iggy@127.0.0.1:8090", "iggy+tcp://iggy:iggy@:8090", "iggy+tcp://iggy:iggy@127.0.0.1",
"iggy+tcp://iggy:iggy@127.0.0.1:abc", "iggy+tcp://:iggy@127.0.0.1:8090", "iggy+tcp://iggy:@127.0.0.1:8090",
"iggy+tcp://iggy:iggy127.0.0.1:8090", "not-a-connection-string", "iggy://iggy:iggy@",
};
for (const std::string &connection_string : malformed_connection_strings) {
SCOPED_TRACE(connection_string);
ASSERT_THROW({ iggy::ffi::from_connection_string(connection_string); }, std::exception);
}
}
TEST_F(LowLevelE2E_Client, LoginWithInvalidCredentialsThrows) {
RecordProperty("description", "Throws when authentication uses invalid credentials after connecting.");
iggy::ffi::Client *client = nullptr;
ASSERT_NO_THROW({ client = iggy::ffi::new_connection({}); });
ASSERT_NE(client, nullptr);
TrackClient(client);
ASSERT_NO_THROW(client->connect());
ASSERT_THROW(client->login_user("biggy", "biggy"), std::exception);
}
TEST_F(LowLevelE2E_Client, LoginTwiceWithDifferentCredentials) {
RecordProperty("description", "Rejects a second login attempt that switches to invalid credentials.");
iggy::ffi::Client *client = nullptr;
ASSERT_NO_THROW({ client = iggy::ffi::new_connection({}); });
ASSERT_NE(client, nullptr);
TrackClient(client);
ASSERT_NO_THROW(client->connect());
ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
ASSERT_THROW(client->login_user("biggy", "biggy"), std::exception);
}
TEST_F(LowLevelE2E_Client, LogoutWithoutLogin) {
RecordProperty("description",
"Rejects logout before authentication, both before and after connect, then succeeds after login.");
iggy::ffi::Client *client = nullptr;
ASSERT_NO_THROW({ client = iggy::ffi::new_connection({}); });
ASSERT_NE(client, nullptr);
TrackClient(client);
ASSERT_THROW(client->logout_user(), std::exception);
ASSERT_NO_THROW(client->connect());
ASSERT_THROW(client->logout_user(), std::exception);
ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
ASSERT_NO_THROW(client->logout_user());
}
TEST_F(LowLevelE2E_Client, ReloginOnSameClientAfterLogout) {
RecordProperty("description", "Returns the same user identity when reauthenticating after a successful logout.");
iggy::ffi::Client *client = nullptr;
ASSERT_NO_THROW({ client = iggy::ffi::new_connection({}); });
ASSERT_NE(client, nullptr);
TrackClient(client);
iggy::ffi::LoginInfo first_login{};
iggy::ffi::LoginInfo second_login{};
iggy::ffi::ClientInfoDetails first_me{};
iggy::ffi::ClientInfoDetails second_me{};
ASSERT_NO_THROW(client->connect());
ASSERT_NO_THROW({ first_login = client->login_user("iggy", "iggy"); });
ASSERT_NO_THROW({ first_me = client->get_me(); });
ASSERT_NO_THROW(client->logout_user());
ASSERT_NO_THROW({ second_login = client->login_user("iggy", "iggy"); });
ASSERT_NO_THROW({ second_me = client->get_me(); });
EXPECT_EQ(first_login.user_id, first_me.user_id);
EXPECT_EQ(second_login.user_id, second_me.user_id);
EXPECT_EQ(second_login.user_id, first_login.user_id);
EXPECT_EQ(second_me.client_id, first_me.client_id);
EXPECT_EQ(second_me.user_id, first_me.user_id);
}
TEST_F(LowLevelE2E_Client, LogoutErrorsWhenCalledMoreThanOnce) {
RecordProperty("description",
"Rejects repeated logout calls once the authenticated session has already logged out.");
iggy::ffi::Client *client = nullptr;
ASSERT_NO_THROW({ client = iggy::ffi::new_connection({}); });
ASSERT_NE(client, nullptr);
TrackClient(client);
ASSERT_NO_THROW(client->connect());
ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
ASSERT_NO_THROW(client->logout_user());
ASSERT_THROW(client->logout_user(), std::exception);
}
TEST_F(E2E_Client, CreateUserWithUsernameOutsideLengthBoundsThrows) {
RecordProperty("description", "Rejects 2-byte and 51-byte usernames over TCP without creating users.");
auto client = GetLoggedInHighLevelClient();
const std::string too_short_username(2, 'a');
const std::string too_long_username(51, 'a');
const std::string usernames[] = {too_short_username, too_long_username};
ASSERT_EQ(too_short_username.size(), 2u);
ASSERT_EQ(too_long_username.size(), 51u);
for (const auto &username : usernames) {
SCOPED_TRACE(username.size());
ASSERT_THROW(client.CreateUser(username, "secret123", iggy::UserStatus::Active), std::exception);
ASSERT_THROW(client.GetUser(iggy::Identifier::String(username)), std::exception);
}
}
TEST_F(E2E_Client, CreateUserAcceptsNonAsciiAndNonAlphabeticUsernames) {
RecordProperty("description",
"Creates and retrieves usernames containing punctuation, multilingual UTF-8, and emoji over TCP.");
auto client = GetLoggedInHighLevelClient();
const std::string suffix = GetRandomName(12);
const std::string usernames[] = {
"!@#_" + suffix, "ユーザー_" + suffix, "用户_" + suffix, "नाम_" + suffix, "사용자_" + suffix, "😀🚀_" + suffix,
};
for (const auto &username : usernames) {
SCOPED_TRACE(username);
ASSERT_LE(username.size(), 50u);
const auto created_user = CreateUser(client, username, "secret123", iggy::UserStatus::Active);
const auto fetched_user = client.GetUser(iggy::Identifier::String(username));
EXPECT_EQ(fetched_user.Id(), created_user.Id());
EXPECT_EQ(created_user.Username(), username);
EXPECT_EQ(fetched_user.Username(), username);
}
}
TEST_F(E2E_Client, CreateUserBeforeLoginThrows) {
RecordProperty("description", "Rejects user creation without an active authenticated session.");
auto client = GetLoggedOutHighLevelClient();
auto root = GetLoggedInHighLevelClient();
const std::string before_login_username = GetRandomName(50);
const std::string logged_out_username = GetRandomName(50);
const std::string disconnected_username = GetRandomName(50);
ASSERT_THROW(client.CreateUser(before_login_username, "secret123", iggy::UserStatus::Active), std::exception);
ASSERT_NO_THROW(client.Connect());
ASSERT_THROW(client.CreateUser(before_login_username, "secret123", iggy::UserStatus::Active), std::exception);
ASSERT_NO_THROW(client.Login("iggy", "iggy"));
ASSERT_NO_THROW(client.Logout());
ASSERT_THROW(client.CreateUser(logged_out_username, "secret123", iggy::UserStatus::Active), std::exception);
ASSERT_NO_THROW(client.Login("iggy", "iggy"));
ASSERT_NO_THROW(client.Disconnect());
ASSERT_THROW(client.CreateUser(disconnected_username, "secret123", iggy::UserStatus::Active), std::exception);
ASSERT_THROW(root.GetUser(iggy::Identifier::String(before_login_username)), std::exception);
ASSERT_THROW(root.GetUser(iggy::Identifier::String(logged_out_username)), std::exception);
ASSERT_THROW(root.GetUser(iggy::Identifier::String(disconnected_username)), std::exception);
}
TEST_F(E2E_Client, CreateUserAcceptsUsernameAndPasswordLengthBounds) {
RecordProperty("description",
"Creates users with shortest and longest ASCII usernames and passwords that can authenticate.");
auto root_client = GetLoggedInHighLevelClient();
auto shortest_client = GetLoggedOutHighLevelClient();
auto longest_client = GetLoggedOutHighLevelClient();
std::string shortest_username = GetRandomName(3);
std::string longest_username = GetRandomName(50);
const std::string shortest_password(3, 'a');
const std::string longest_password(100, 'a');
longest_username.resize(50, 'a');
ASSERT_EQ(shortest_username.size(), 3u);
ASSERT_EQ(longest_username.size(), 50u);
ASSERT_EQ(shortest_password.size(), 3u);
ASSERT_EQ(longest_password.size(), 100u);
const auto shortest_user = CreateUser(root_client, shortest_username, shortest_password, iggy::UserStatus::Active);
const auto longest_user = CreateUser(root_client, longest_username, longest_password, iggy::UserStatus::Active);
const auto fetched_shortest = root_client.GetUser(iggy::Identifier::String(shortest_username));
const auto fetched_longest = root_client.GetUser(iggy::Identifier::String(longest_username));
ASSERT_NO_THROW(shortest_client.Connect());
ASSERT_NO_THROW(longest_client.Connect());
ASSERT_NO_THROW(shortest_client.Login(shortest_username, shortest_password));
ASSERT_NO_THROW(longest_client.Login(longest_username, longest_password));
EXPECT_EQ(shortest_user.Username(), shortest_username);
EXPECT_EQ(longest_user.Username(), longest_username);
EXPECT_EQ(fetched_shortest.Id(), shortest_user.Id());
EXPECT_EQ(fetched_longest.Id(), longest_user.Id());
}
TEST_F(E2E_Client, CreateUserWithPasswordOutsideLengthBoundsThrows) {
RecordProperty("description", "Rejects 2-byte and 101-byte passwords without creating users.");
auto client = GetLoggedInHighLevelClient();
const std::string short_username = GetRandomName(50);
const std::string long_username = GetRandomName(50);
const std::string short_password(2, 'a');
const std::string long_password(101, 'a');
ASSERT_EQ(short_password.size(), 2u);
ASSERT_EQ(long_password.size(), 101u);
ASSERT_THROW(client.CreateUser(short_username, short_password, iggy::UserStatus::Active), std::exception);
ASSERT_THROW(client.CreateUser(long_username, long_password, iggy::UserStatus::Active), std::exception);
ASSERT_THROW(client.GetUser(iggy::Identifier::String(short_username)), std::exception);
ASSERT_THROW(client.GetUser(iggy::Identifier::String(long_username)), std::exception);
}
TEST_F(E2E_Client, CreateUserWithInvalidStatusThrows) {
RecordProperty("description", "Rejects invalid status codes before creating users.");
auto client = GetLoggedInHighLevelClient();
const iggy::UserStatus statuses[] = {
static_cast<iggy::UserStatus>(0),
static_cast<iggy::UserStatus>(3),
static_cast<iggy::UserStatus>(std::numeric_limits<std::uint8_t>::max()),
};
for (const auto status : statuses) {
const std::string username = GetRandomName(50);
SCOPED_TRACE(static_cast<std::uint8_t>(status));
ASSERT_THROW(client.CreateUser(username, "secret123", status), std::exception);
ASSERT_THROW(client.GetUser(iggy::Identifier::String(username)), std::exception);
}
}
TEST_F(E2E_Client, CreateUserReturnsCreatedActiveUserDetails) {
RecordProperty("description", "Returns and persists active user details.");
auto client = GetLoggedInHighLevelClient();
const std::string username = GetRandomName(50);
ASSERT_NO_THROW({
const auto created_user = CreateUser(client, username, "secret123", iggy::UserStatus::Active);
const auto fetched_user = client.GetUser(iggy::Identifier::String(username));
EXPECT_EQ(fetched_user.Id(), created_user.Id());
EXPECT_EQ(created_user.Username(), username);
EXPECT_EQ(fetched_user.Username(), username);
EXPECT_EQ(created_user.Status(), iggy::UserStatus::Active);
EXPECT_EQ(fetched_user.Status(), iggy::UserStatus::Active);
});
}
TEST_F(E2E_Client, CreateUserRejectsDuplicateUsernameWithoutChangingOriginal) {
RecordProperty("description", "Rejects duplicate usernames without changing the existing user.");
auto root_client = GetLoggedInHighLevelClient();
auto user_client = GetLoggedOutHighLevelClient();
const std::string username = GetRandomName(50);
const std::string password = "original-secret";
ASSERT_NO_THROW({
const auto original = CreateUser(root_client, username, password, iggy::UserStatus::Active);
ASSERT_THROW(root_client.CreateUser(username, "replacement-secret", iggy::UserStatus::Inactive),
std::exception);
const auto fetched = root_client.GetUser(iggy::Identifier::String(username));
EXPECT_EQ(fetched.Id(), original.Id());
EXPECT_EQ(fetched.Status(), iggy::UserStatus::Active);
});
ASSERT_NO_THROW(user_client.Connect());
ASSERT_NO_THROW(user_client.Login(username, password));
auto replacement_client = GetLoggedOutHighLevelClient();
ASSERT_NO_THROW(replacement_client.Connect());
ASSERT_THROW(replacement_client.Login(username, "replacement-secret"), std::exception);
}
TEST_F(E2E_Client, CreateUserPreservesNestedPermissionsInCreateAndGetResponses) {
RecordProperty("description",
"Creates a user with global and per-resource permissions, then verifies create_user and get_user "
"return the same flags and numeric stream/topic IDs.");
auto client = GetLoggedInHighLevelClient();
const std::string username = GetRandomName(50);
iggy::GlobalPermissions global;
global.SetManageServers(true).SetReadUsers(true).SetManageStreams(true).SetReadTopics(true).SetSendMessages(true);
iggy::TopicPermissions first_topic;
first_topic.SetManageTopic(true).SetPollMessages(true);
iggy::TopicPermissions second_topic;
second_topic.SetReadTopic(true).SetSendMessages(true);
iggy::StreamPermissions first_stream;
first_stream.SetManageStream(true).SetReadTopics(true).SetSendMessages(true).SetTopics(
{{7, first_topic}, {9, second_topic}});
iggy::TopicPermissions third_topic;
third_topic.SetReadTopic(true);
iggy::StreamPermissions second_stream;
second_stream.SetReadStream(true).SetManageTopics(true).SetPollMessages(true).SetTopics({{3, third_topic}});
iggy::Permissions permissions;
permissions.SetGlobal(global).SetStreams({{42, first_stream}, {84, second_stream}});
ASSERT_NO_THROW({
const auto created = CreateUser(client, username, "secret123", iggy::UserStatus::Active, permissions);
const auto fetched = client.GetUser(iggy::Identifier::String(username));
for (const auto *user : {&created, &fetched}) {
ASSERT_TRUE(user->Permissions().has_value());
const auto &user_permissions = user->Permissions().value();
EXPECT_TRUE(user_permissions.Global().ManageServers());
EXPECT_FALSE(user_permissions.Global().ReadServers());
EXPECT_FALSE(user_permissions.Global().ManageUsers());
EXPECT_TRUE(user_permissions.Global().ReadUsers());
EXPECT_TRUE(user_permissions.Global().ManageStreams());
EXPECT_FALSE(user_permissions.Global().ReadStreams());
EXPECT_FALSE(user_permissions.Global().ManageTopics());
EXPECT_TRUE(user_permissions.Global().ReadTopics());
EXPECT_FALSE(user_permissions.Global().PollMessages());
EXPECT_TRUE(user_permissions.Global().SendMessages());
ASSERT_EQ(user_permissions.Streams().size(), 2u);
const auto stream_42 = user_permissions.Streams().find(42);
const auto stream_84 = user_permissions.Streams().find(84);
ASSERT_NE(stream_42, user_permissions.Streams().end());
ASSERT_NE(stream_84, user_permissions.Streams().end());
EXPECT_TRUE(stream_42->second.ManageStream());
EXPECT_FALSE(stream_42->second.ReadStream());
EXPECT_FALSE(stream_42->second.ManageTopics());
EXPECT_TRUE(stream_42->second.ReadTopics());
EXPECT_FALSE(stream_42->second.PollMessages());
EXPECT_TRUE(stream_42->second.SendMessages());
ASSERT_EQ(stream_42->second.Topics().size(), 2u);
const auto topic_7 = stream_42->second.Topics().find(7);
const auto topic_9 = stream_42->second.Topics().find(9);
ASSERT_NE(topic_7, stream_42->second.Topics().end());
ASSERT_NE(topic_9, stream_42->second.Topics().end());
EXPECT_TRUE(topic_7->second.ManageTopic());
EXPECT_FALSE(topic_7->second.ReadTopic());
EXPECT_TRUE(topic_7->second.PollMessages());
EXPECT_FALSE(topic_7->second.SendMessages());
EXPECT_FALSE(topic_9->second.ManageTopic());
EXPECT_TRUE(topic_9->second.ReadTopic());
EXPECT_FALSE(topic_9->second.PollMessages());
EXPECT_TRUE(topic_9->second.SendMessages());
EXPECT_FALSE(stream_84->second.ManageStream());
EXPECT_TRUE(stream_84->second.ReadStream());
EXPECT_TRUE(stream_84->second.ManageTopics());
EXPECT_FALSE(stream_84->second.ReadTopics());
EXPECT_TRUE(stream_84->second.PollMessages());
EXPECT_FALSE(stream_84->second.SendMessages());
ASSERT_EQ(stream_84->second.Topics().size(), 1u);
const auto topic_3 = stream_84->second.Topics().find(3);
ASSERT_NE(topic_3, stream_84->second.Topics().end());
EXPECT_EQ(topic_3->first, 3u);
EXPECT_FALSE(topic_3->second.ManageTopic());
EXPECT_TRUE(topic_3->second.ReadTopic());
EXPECT_FALSE(topic_3->second.PollMessages());
EXPECT_FALSE(topic_3->second.SendMessages());
}
});
}
TEST_F(LowLevelE2E_Client, CreatedUserCanReadOnlyTopicGrantedByPermissions) {
RecordProperty("description",
"Creates a user with read access to one topic, then verifies that topic can be fetched and a topic "
"in another stream is denied.");
iggy::ffi::Client *root_client = GetLoggedInClient();
iggy::ffi::Client *user_client = GetLoggedOutClient();
const std::string allowed_stream_name = GetRandomName();
const std::string denied_stream_name = GetRandomName();
const std::string allowed_topic_name = GetRandomName();
const std::string denied_topic_name = GetRandomName();
const std::string username = GetRandomName(50);
iggy::ffi::StreamDetails allowed_stream{};
iggy::ffi::StreamDetails denied_stream{};
ASSERT_NO_THROW({ allowed_stream = root_client->create_stream(allowed_stream_name); });
TrackStream(allowed_stream_name);
ASSERT_NO_THROW({ denied_stream = root_client->create_stream(denied_stream_name); });
TrackStream(denied_stream_name);
iggy::ffi::TopicDetails allowed_topic{};
iggy::ffi::TopicDetails denied_topic{};
ASSERT_NO_THROW({
allowed_topic =
root_client->create_topic(make_numeric_identifier(allowed_stream.id), allowed_topic_name,
make_topic_create_options(1, "none", "server_default", 0, "server_default"));
denied_topic =
root_client->create_topic(make_numeric_identifier(denied_stream.id), denied_topic_name,
make_topic_create_options(1, "none", "server_default", 0, "server_default"));
});
iggy::ffi::Permissions permissions{};
iggy::ffi::StreamPermissionEntry stream_permissions{};
stream_permissions.stream_id = allowed_stream.id;
iggy::ffi::TopicPermissionEntry topic_permissions{};
topic_permissions.topic_id = allowed_topic.id;
topic_permissions.permissions.read_topic = true;
stream_permissions.permissions.topics.push_back(std::move(topic_permissions));
permissions.streams.push_back(std::move(stream_permissions));
ASSERT_NO_THROW({
CreateUser(root_client, username, "secret123", iggy::ffi::UserStatus::Active, true, std::move(permissions));
});
ASSERT_NO_THROW(user_client->connect());
ASSERT_NO_THROW(user_client->login_user(username, "secret123"));
iggy::ffi::TopicDetails fetched_topic{};
ASSERT_NO_THROW({
fetched_topic = user_client->get_topic(make_numeric_identifier(allowed_stream.id),
make_numeric_identifier(allowed_topic.id));
});
EXPECT_EQ(fetched_topic.id, allowed_topic.id);
EXPECT_EQ(static_cast<std::string>(fetched_topic.name), allowed_topic_name);
ASSERT_THROW(
user_client->get_topic(make_numeric_identifier(denied_stream.id), make_numeric_identifier(denied_topic.id)),
std::exception);
}
TEST_F(LowLevelE2E_Client, CreateUserRejectsDuplicateStreamPermissionIdsWithoutCreatingUser) {
RecordProperty("description",
"Attempts to create a user with two permission entries for stream ID 42, then verifies creation "
"fails and no user is stored.");
iggy::ffi::Client *client = GetLoggedInClient();
const std::string username = GetRandomName(50);
iggy::ffi::Permissions permissions{};
iggy::ffi::StreamPermissionEntry first_stream{};
iggy::ffi::StreamPermissionEntry second_stream{};
first_stream.stream_id = 42;
second_stream.stream_id = 42;
permissions.streams.push_back(std::move(first_stream));
permissions.streams.push_back(std::move(second_stream));
ASSERT_THROW(
client->create_user(username, "secret123", iggy::ffi::UserStatus::Active, true, std::move(permissions)),
std::exception);
ASSERT_THROW(client->get_user(make_string_identifier(username)), std::exception);
}
TEST_F(LowLevelE2E_Client, CreateUserRejectsDuplicateTopicPermissionIdsWithoutCreatingUser) {
RecordProperty("description",
"Attempts to create a user with two permission entries for topic ID 7 in the same stream, then "
"verifies creation fails and no user is stored.");
iggy::ffi::Client *client = GetLoggedInClient();
const std::string username = GetRandomName(50);
iggy::ffi::Permissions permissions{};
iggy::ffi::StreamPermissionEntry stream{};
stream.stream_id = 42;
iggy::ffi::TopicPermissionEntry first_topic{};
iggy::ffi::TopicPermissionEntry second_topic{};
first_topic.topic_id = 7;
second_topic.topic_id = 7;
stream.permissions.topics.push_back(std::move(first_topic));
stream.permissions.topics.push_back(std::move(second_topic));
permissions.streams.push_back(std::move(stream));
ASSERT_THROW(
client->create_user(username, "secret123", iggy::ffi::UserStatus::Active, true, std::move(permissions)),
std::exception);
ASSERT_THROW(client->get_user(make_string_identifier(username)), std::exception);
}
TEST_F(LowLevelE2E_Client, ReadUsersPermissionDoesNotAllowCreateUser) {
RecordProperty("description", "Rejects user creation by a user with read_users but not manage_users.");
iggy::ffi::Client *root_client = GetLoggedInClient();
iggy::ffi::Client *user_client = GetLoggedOutClient();
const std::string username = GetRandomName(50);
const std::string target = GetRandomName(50);
iggy::ffi::Permissions permissions{};
permissions.global.read_users = true;
ASSERT_NO_THROW({
CreateUser(root_client, username, "secret123", iggy::ffi::UserStatus::Active, true, std::move(permissions));
});
ASSERT_NO_THROW(user_client->connect());
ASSERT_NO_THROW(user_client->login_user(username, "secret123"));
ASSERT_THROW(
user_client->create_user(target, "secret123", iggy::ffi::UserStatus::Active, false, iggy::ffi::Permissions{}),
std::exception);
ASSERT_THROW(root_client->get_user(make_string_identifier(target)), std::exception);
}
TEST_F(LowLevelE2E_Client, ManageUsersPermissionAllowsGrantingAdditionalPermissions) {
RecordProperty("description", "Allows a user manager to grant a child a permission the manager does not have.");
iggy::ffi::Client *root_client = GetLoggedInClient();
iggy::ffi::Client *manager_client = GetLoggedOutClient();
iggy::ffi::Client *child_client = GetLoggedOutClient();
const std::string manager_username = GetRandomName(50);
const std::string child_username = GetRandomName(50);
const std::string denied_stream = GetRandomName();
const std::string child_stream = GetRandomName();
iggy::ffi::Permissions manager_permissions{};
manager_permissions.global.manage_users = true;
manager_permissions.global.manage_streams = false;
ASSERT_NO_THROW({
CreateUser(root_client, manager_username, "secret123", iggy::ffi::UserStatus::Active, true,
std::move(manager_permissions));
});
ASSERT_NO_THROW(manager_client->connect());
ASSERT_NO_THROW(manager_client->login_user(manager_username, "secret123"));
ASSERT_THROW(manager_client->create_stream(denied_stream), std::exception);
iggy::ffi::Permissions child_permissions{};
child_permissions.global.manage_streams = true;
iggy::ffi::UserInfoDetails child{};
ASSERT_NO_THROW({
child = CreateUser(manager_client, child_username, "child-secret", iggy::ffi::UserStatus::Active, true,
std::move(child_permissions));
});
EXPECT_EQ(static_cast<std::string>(child.username), child_username);
EXPECT_EQ(child.status, iggy::ffi::UserStatus::Active);
iggy::ffi::UserInfoDetails fetched{};
ASSERT_NO_THROW({ fetched = root_client->get_user(make_string_identifier(child_username)); });
EXPECT_EQ(fetched.id, child.id);
EXPECT_TRUE(fetched.permissions.global.manage_streams);
ASSERT_NO_THROW(child_client->connect());
ASSERT_NO_THROW(child_client->login_user(child_username, "child-secret"));
ASSERT_NO_THROW(child_client->create_stream(child_stream));
TrackStream(child_stream);
}
TEST_F(LowLevelE2E_Client, CreatedActiveUserAuthenticatesOnlyWithSuppliedPassword) {
RecordProperty("description", "Authenticates an active user only with its supplied password.");
iggy::ffi::Client *root_client = GetLoggedInClient();
iggy::ffi::Client *valid_client = GetLoggedOutClient();
iggy::ffi::Client *wrong_client = GetLoggedOutClient();
const std::string username = GetRandomName(50);
const std::string password = "known-secret";
ASSERT_NO_THROW({ CreateUser(root_client, username, password, iggy::ffi::UserStatus::Active); });
ASSERT_NO_THROW(valid_client->connect());
ASSERT_NO_THROW(wrong_client->connect());
ASSERT_NO_THROW(valid_client->login_user(username, password));
ASSERT_THROW(wrong_client->login_user(username, "other-secret"), std::exception);
}
TEST_F(LowLevelE2E_Client, CreatedInactiveUserCannotAuthenticate) {
RecordProperty("description", "Persists inactive users but rejects authentication for them.");
iggy::ffi::Client *root_client = GetLoggedInClient();
iggy::ffi::Client *user_client = GetLoggedOutClient();
const std::string username = GetRandomName(50);
const std::string password = "inactive-secret";
iggy::ffi::UserInfoDetails created{};
iggy::ffi::UserInfoDetails fetched{};
ASSERT_NO_THROW({ created = CreateUser(root_client, username, password, iggy::ffi::UserStatus::Inactive); });
ASSERT_NO_THROW({ fetched = root_client->get_user(make_string_identifier(username)); });
EXPECT_EQ(created.status, iggy::ffi::UserStatus::Inactive);
EXPECT_EQ(fetched.status, iggy::ffi::UserStatus::Inactive);
ASSERT_NO_THROW(user_client->connect());
ASSERT_THROW(user_client->login_user(username, password), std::exception);
}
TEST_F(E2E_Client, UpdateUserRejectsUnauthenticatedClientWithoutChangingTarget) {
RecordProperty("description", "Rejects user updates without an active authenticated session.");
auto root_client = GetLoggedInHighLevelClient();
const std::string username = GetRandomName(50);
const std::string replacement = GetRandomName(50);
ASSERT_NO_THROW({
const auto created = CreateUser(root_client, username, "secret123", iggy::UserStatus::Active);
auto client = GetLoggedOutHighLevelClient();
ASSERT_THROW(client.UpdateUser(iggy::Identifier::String(username), replacement, iggy::UserStatus::Inactive,
iggy::UserUpdateOptions{}),
std::exception);
ASSERT_NO_THROW(client.Connect());
ASSERT_THROW(client.UpdateUser(iggy::Identifier::String(username), replacement, iggy::UserStatus::Inactive,
iggy::UserUpdateOptions{}),
std::exception);
ASSERT_NO_THROW(client.Login("iggy", "iggy"));
ASSERT_NO_THROW(client.Logout());
ASSERT_THROW(client.UpdateUser(iggy::Identifier::String(username), replacement, iggy::UserStatus::Inactive,
iggy::UserUpdateOptions{}),
std::exception);
ASSERT_NO_THROW(client.Login("iggy", "iggy"));
ASSERT_NO_THROW(client.Disconnect());
ASSERT_THROW(client.UpdateUser(iggy::Identifier::String(username), replacement, iggy::UserStatus::Inactive,
iggy::UserUpdateOptions{}),
std::exception);
const auto fetched = root_client.GetUser(iggy::Identifier::String(username));
EXPECT_EQ(fetched.Id(), created.Id());
EXPECT_EQ(fetched.Username(), username);
EXPECT_EQ(fetched.Status(), iggy::UserStatus::Active);
});
}
TEST_F(E2E_Client, UpdateUserRejectsUnknownUsernameAndNumericId) {
RecordProperty("description", "Rejects updates for unknown username and numeric identifiers.");
auto client = GetLoggedInHighLevelClient();
const std::string unknown_username = GetRandomName(50);
const std::string proposed_username = GetRandomName(50);
const auto unknown_id = std::numeric_limits<std::uint32_t>::max();
ASSERT_THROW(client.UpdateUser(iggy::Identifier::String(unknown_username), proposed_username,
iggy::UserStatus::Inactive, iggy::UserUpdateOptions{}),
std::exception);
ASSERT_THROW(client.UpdateUser(iggy::Identifier::Numeric(unknown_id), GetRandomName(50), iggy::UserStatus::Inactive,
iggy::UserUpdateOptions{}),
std::exception);
ASSERT_THROW(client.GetUser(iggy::Identifier::String(proposed_username)), std::exception);
}
TEST_F(E2E_Client, UpdateUserByUsernameChangesUsernameAndStatus) {
RecordProperty("description", "Updates a user by username and changes both username and status.");
auto client = GetLoggedInHighLevelClient();
const std::string username = GetRandomName(50);
const std::string replacement = GetRandomName(50);
ASSERT_NO_THROW({
const auto created = CreateUser(client, username, "secret123", iggy::UserStatus::Active);
ASSERT_NO_THROW(client.UpdateUser(iggy::Identifier::String(username), replacement, iggy::UserStatus::Inactive,
iggy::UserUpdateOptions{}));
RenameTrackedUser(username, replacement);
ASSERT_THROW(client.GetUser(iggy::Identifier::String(username)), std::exception);
const auto fetched = client.GetUser(iggy::Identifier::String(replacement));
EXPECT_EQ(fetched.Id(), created.Id());
EXPECT_EQ(fetched.Username(), replacement);
EXPECT_EQ(fetched.Status(), iggy::UserStatus::Inactive);
});
}
TEST_F(E2E_Client, UpdateUserByNumericIdChangesUsernameAndStatus) {
RecordProperty("description", "Updates a user by numeric ID and changes both username and status.");
auto client = GetLoggedInHighLevelClient();
const std::string username = GetRandomName(50);
const std::string replacement = GetRandomName(50);
ASSERT_NO_THROW({
const auto created = CreateUser(client, username, "secret123", iggy::UserStatus::Inactive);
ASSERT_NO_THROW(client.UpdateUser(iggy::Identifier::Numeric(created.Id()), replacement,
iggy::UserStatus::Active, iggy::UserUpdateOptions{}));
RenameTrackedUser(username, replacement);
const auto by_id = client.GetUser(iggy::Identifier::Numeric(created.Id()));
const auto by_name = client.GetUser(iggy::Identifier::String(replacement));
EXPECT_EQ(by_id.Id(), created.Id());
EXPECT_EQ(by_name.Id(), created.Id());
EXPECT_EQ(by_id.Username(), replacement);
EXPECT_EQ(by_name.Username(), replacement);
EXPECT_EQ(by_id.Status(), iggy::UserStatus::Active);
EXPECT_EQ(by_name.Status(), iggy::UserStatus::Active);
});
}
TEST_F(E2E_Client, UpdateUserAllowsUsernameAndStatusToBeUpdatedIndependently) {
RecordProperty("description", "Updates either username or status without changing the other field.");
auto client = GetLoggedInHighLevelClient();
const std::string username = GetRandomName(50);
const std::string replacement = GetRandomName(50);
ASSERT_NO_THROW({
const auto created = CreateUser(client, username, "secret123", iggy::UserStatus::Active);
ASSERT_NO_THROW(client.UpdateUser(iggy::Identifier::Numeric(created.Id()), std::nullopt,
iggy::UserStatus::Inactive, iggy::UserUpdateOptions{}));
const auto status_updated = client.GetUser(iggy::Identifier::Numeric(created.Id()));
EXPECT_EQ(status_updated.Username(), username);
EXPECT_EQ(status_updated.Status(), iggy::UserStatus::Inactive);
ASSERT_NO_THROW(client.UpdateUser(iggy::Identifier::Numeric(created.Id()), replacement, std::nullopt,
iggy::UserUpdateOptions{}));
RenameTrackedUser(username, replacement);
const auto username_updated = client.GetUser(iggy::Identifier::Numeric(created.Id()));
EXPECT_EQ(username_updated.Username(), replacement);
EXPECT_EQ(username_updated.Status(), iggy::UserStatus::Inactive);
});
}
TEST_F(E2E_Client, UpdateUserAcceptsUsernameLengthBounds) {
RecordProperty("description", "Accepts exact three-byte and fifty-byte username boundaries.");
auto client = GetLoggedInHighLevelClient();
const std::string first_username = GetRandomName(50);
const std::string second_username = GetRandomName(50);
const std::string first_replacement = GetRandomName(3);
std::string second_replacement = GetRandomName(50);
second_replacement.resize(50, 'a');
ASSERT_EQ(second_replacement.size(), 50u);
ASSERT_NO_THROW({
const auto first = CreateUser(client, first_username, "secret123", iggy::UserStatus::Active);
const auto second = CreateUser(client, second_username, "secret123", iggy::UserStatus::Active);
ASSERT_NO_THROW(client.UpdateUser(iggy::Identifier::String(first_username), first_replacement,
iggy::UserStatus::Active, iggy::UserUpdateOptions{}));
RenameTrackedUser(first_username, first_replacement);
ASSERT_NO_THROW(client.UpdateUser(iggy::Identifier::String(second_username), second_replacement,
iggy::UserStatus::Active, iggy::UserUpdateOptions{}));
RenameTrackedUser(second_username, second_replacement);
const auto fetched_first = client.GetUser(iggy::Identifier::String(first_replacement));
const auto fetched_second = client.GetUser(iggy::Identifier::String(second_replacement));
EXPECT_EQ(fetched_first.Id(), first.Id());
EXPECT_EQ(fetched_second.Id(), second.Id());
EXPECT_EQ(fetched_first.Username(), first_replacement);
EXPECT_EQ(fetched_second.Username(), second_replacement);
});
}
TEST_F(E2E_Client, UpdateUserRejectsUsernameOutsideLengthBounds) {
RecordProperty("description", "Rejects username sizes outside the SDK and server limits.");
auto client = GetLoggedInHighLevelClient();
const std::string source = GetRandomName(50);
const std::string invalid_usernames[] = {
"", "a", "aa", std::string(51, 'c'), std::string(255, 'd'), std::string(256, 'e')};
ASSERT_NO_THROW({
const auto created = CreateUser(client, source, "secret123", iggy::UserStatus::Active);
for (const auto &replacement : invalid_usernames) {
ASSERT_THROW(client.UpdateUser(iggy::Identifier::String(source), replacement, iggy::UserStatus::Active,
iggy::UserUpdateOptions{}),
std::exception);
}
const auto fetched = client.GetUser(iggy::Identifier::String(source));
EXPECT_EQ(fetched.Id(), created.Id());
EXPECT_EQ(fetched.Status(), iggy::UserStatus::Active);
EXPECT_EQ(fetched.Username(), source);
});
}
TEST_F(E2E_Client, UpdateUserAcceptsNonAsciiAndNonAlphabeticUsername) {
RecordProperty("description", "Accepts representative non-ASCII and non-alphabetic usernames.");
auto client = GetLoggedInHighLevelClient();
const std::string suffix = GetRandomName(8);
const std::string sources[] = {GetRandomName(50), GetRandomName(50), GetRandomName(50)};
const std::string replacements[] = {"!@#_" + suffix, "ユーザー_" + suffix, "😀🚀_" + suffix};
ASSERT_LE(replacements[0].size(), 50u);
ASSERT_LE(replacements[1].size(), 50u);
ASSERT_LE(replacements[2].size(), 50u);
for (std::size_t index = 0; index < 3; ++index) {
ASSERT_NO_THROW({
const auto created = CreateUser(client, sources[index], "secret123", iggy::UserStatus::Active);
ASSERT_NO_THROW(client.UpdateUser(iggy::Identifier::String(sources[index]), replacements[index],
iggy::UserStatus::Active, iggy::UserUpdateOptions{}));
RenameTrackedUser(sources[index], replacements[index]);
const auto fetched = client.GetUser(iggy::Identifier::String(replacements[index]));
EXPECT_EQ(fetched.Id(), created.Id());
EXPECT_EQ(fetched.Username(), replacements[index]);
});
}
}
TEST_F(E2E_Client, UpdateUserRejectsInvalidStatusWithoutRenamingTarget) {
RecordProperty("description", "Rejects invalid status codes atomically with username changes.");
auto client = GetLoggedInHighLevelClient();
const std::string username = GetRandomName(50);
const iggy::UserStatus statuses[] = {
static_cast<iggy::UserStatus>(0),
static_cast<iggy::UserStatus>(3),
static_cast<iggy::UserStatus>(std::numeric_limits<std::uint8_t>::max()),
};
ASSERT_NO_THROW({
const auto created = CreateUser(client, username, "secret123", iggy::UserStatus::Active);
for (const auto status : statuses) {
const std::string replacement = GetRandomName(50);
ASSERT_THROW(
client.UpdateUser(iggy::Identifier::String(username), replacement, status, iggy::UserUpdateOptions{}),
std::exception);
ASSERT_THROW(client.GetUser(iggy::Identifier::String(replacement)), std::exception);
}
const auto fetched = client.GetUser(iggy::Identifier::String(username));
EXPECT_EQ(fetched.Id(), created.Id());
EXPECT_EQ(fetched.Status(), iggy::UserStatus::Active);
EXPECT_EQ(fetched.Username(), username);
});
}
TEST_F(E2E_Client, UpdateUserRejectsDuplicateUsernameWithoutChangingStatus) {
RecordProperty("description", "Rejects duplicate usernames without partially applying the status update.");
auto client = GetLoggedInHighLevelClient();
const std::string target_username = GetRandomName(50);
const std::string conflict_username = GetRandomName(50);
ASSERT_NO_THROW({
const auto target = CreateUser(client, target_username, "secret123", iggy::UserStatus::Active);
const auto conflict = CreateUser(client, conflict_username, "secret123", iggy::UserStatus::Inactive);
ASSERT_THROW(client.UpdateUser(iggy::Identifier::String(target_username), conflict_username,
iggy::UserStatus::Inactive, iggy::UserUpdateOptions{}),
std::exception);
const auto fetched_target = client.GetUser(iggy::Identifier::String(target_username));
const auto fetched_conflict = client.GetUser(iggy::Identifier::String(conflict_username));
EXPECT_EQ(fetched_target.Id(), target.Id());
EXPECT_EQ(fetched_conflict.Id(), conflict.Id());
EXPECT_EQ(fetched_target.Status(), iggy::UserStatus::Active);
EXPECT_EQ(fetched_conflict.Status(), iggy::UserStatus::Inactive);
});
}
TEST_F(E2E_Client, UpdateUserAllowsCurrentUsernameWhileChangingStatus) {
RecordProperty("description", "Allows a same-username update that changes status.");
auto client = GetLoggedInHighLevelClient();
const std::string username = GetRandomName(50);
ASSERT_NO_THROW({
const auto created = CreateUser(client, username, "secret123", iggy::UserStatus::Active);
ASSERT_NO_THROW(client.UpdateUser(iggy::Identifier::String(username), username, iggy::UserStatus::Inactive,
iggy::UserUpdateOptions{}));
const auto fetched = client.GetUser(iggy::Identifier::String(username));
EXPECT_EQ(fetched.Id(), created.Id());
EXPECT_EQ(fetched.Status(), iggy::UserStatus::Inactive);
EXPECT_EQ(fetched.Username(), username);
});
}
TEST_F(E2E_Client, UpdateUserPreservesPasswordPermissionsAndCreationData) {
RecordProperty("description", "Preserves password, permissions, ID, and creation timestamp after a rename.");
auto root_client = GetLoggedInHighLevelClient();
auto valid_client = GetLoggedOutHighLevelClient();
auto old_client = GetLoggedOutHighLevelClient();
const std::string username = GetRandomName(50);
const std::string replacement = GetRandomName(50);
iggy::GlobalPermissions global;
global.SetReadUsers(true).SetReadStreams(true).SetSendMessages(true);
iggy::Permissions permissions;
permissions.SetGlobal(global);
ASSERT_NO_THROW({
const auto created = CreateUser(root_client, username, "known-secret", iggy::UserStatus::Active, permissions);
ASSERT_NO_THROW(root_client.UpdateUser(iggy::Identifier::String(username), replacement,
iggy::UserStatus::Active, iggy::UserUpdateOptions{}));
RenameTrackedUser(username, replacement);
const auto fetched = root_client.GetUser(iggy::Identifier::String(replacement));
EXPECT_EQ(fetched.Id(), created.Id());
EXPECT_EQ(fetched.CreatedAt(), created.CreatedAt());
ASSERT_TRUE(fetched.Permissions().has_value());
EXPECT_TRUE(fetched.Permissions()->Global().ReadUsers());
EXPECT_TRUE(fetched.Permissions()->Global().ReadStreams());
EXPECT_TRUE(fetched.Permissions()->Global().SendMessages());
});
ASSERT_NO_THROW(valid_client.Connect());
ASSERT_NO_THROW(valid_client.Login(replacement, "known-secret"));
ASSERT_NO_THROW(old_client.Connect());
ASSERT_THROW(old_client.Login(username, "known-secret"), std::exception);
}
TEST_F(E2E_Client, UpdateUserToInactiveBlocksFreshLoginUntilReactivated) {
RecordProperty("description", "Blocks fresh login while inactive and restores it when active.");
auto root_client = GetLoggedInHighLevelClient();
auto user_client = GetLoggedOutHighLevelClient();
const std::string username = GetRandomName(50);
ASSERT_NO_THROW({ CreateUser(root_client, username, "known-secret", iggy::UserStatus::Active); });
ASSERT_NO_THROW(root_client.UpdateUser(iggy::Identifier::String(username), username, iggy::UserStatus::Inactive,
iggy::UserUpdateOptions{}));
ASSERT_NO_THROW(user_client.Connect());
ASSERT_THROW(user_client.Login(username, "known-secret"), std::exception);
ASSERT_NO_THROW(root_client.UpdateUser(iggy::Identifier::String(username), username, iggy::UserStatus::Active,
iggy::UserUpdateOptions{}));
ASSERT_NO_THROW(user_client.Login(username, "known-secret"));
}
TEST_F(E2E_Client, UpdateUserRenameMakesOldUsernameReusable) {
RecordProperty("description", "Releases the old username for a new user after a successful rename.");
auto client = GetLoggedInHighLevelClient();
const std::string old_username = GetRandomName(50);
const std::string new_username = GetRandomName(50);
ASSERT_NO_THROW({
const auto first = CreateUser(client, old_username, "secret123", iggy::UserStatus::Active);
ASSERT_NO_THROW(client.UpdateUser(iggy::Identifier::String(old_username), new_username,
iggy::UserStatus::Active, iggy::UserUpdateOptions{}));
RenameTrackedUser(old_username, new_username);
const auto second = CreateUser(client, old_username, "secret123", iggy::UserStatus::Active);
EXPECT_EQ(client.GetUser(iggy::Identifier::String(new_username)).Username(), new_username);
EXPECT_EQ(client.GetUser(iggy::Identifier::String(old_username)).Username(), old_username);
EXPECT_NE(first.Id(), second.Id());
});
}
TEST_F(E2E_Client, UpdateUserRejectsRenameToRootUsername) {
RecordProperty("description", "Rejects changing a non-root user's username to root's username.");
auto client = GetLoggedInHighLevelClient();
const std::string username = GetRandomName(50);
ASSERT_NO_THROW({
const auto target = CreateUser(client, username, "secret123", iggy::UserStatus::Active);
ASSERT_THROW(client.UpdateUser(iggy::Identifier::String(username), "iggy", iggy::UserStatus::Inactive,
iggy::UserUpdateOptions{}),
std::exception);
const auto root = client.GetUser(iggy::Identifier::String("iggy"));
const auto fetched = client.GetUser(iggy::Identifier::String(username));
EXPECT_EQ(root.Id(), 0u);
EXPECT_EQ(root.Username(), "iggy");
EXPECT_EQ(root.Status(), iggy::UserStatus::Active);
EXPECT_EQ(fetched.Id(), target.Id());
EXPECT_EQ(fetched.Status(), iggy::UserStatus::Active);
EXPECT_EQ(fetched.Username(), username);
});
}
TEST_F(LowLevelE2E_Client, ReadUsersPermissionDoesNotAllowUpdateUser) {
RecordProperty("description", "Rejects updates from a user with read_users but not manage_users.");
iggy::ffi::Client *root_client = GetLoggedInClient();
iggy::ffi::Client *caller_client = GetLoggedOutClient();
const std::string caller_username = GetRandomName(50);
const std::string target_username = GetRandomName(50);
iggy::ffi::Permissions permissions{};
permissions.global.read_users = true;
ASSERT_NO_THROW({
CreateUser(root_client, caller_username, "secret123", iggy::ffi::UserStatus::Active, true,
std::move(permissions));
});
iggy::ffi::UserInfoDetails target{};
ASSERT_NO_THROW({ target = CreateUser(root_client, target_username, "secret123", iggy::ffi::UserStatus::Active); });
ASSERT_NO_THROW(caller_client->connect());
ASSERT_NO_THROW(caller_client->login_user(caller_username, "secret123"));
ASSERT_THROW(caller_client->update_user(make_string_identifier(target_username), true, GetRandomName(50), true,
iggy::ffi::UserStatus::Inactive, {}),
std::exception);
ASSERT_THROW(caller_client->update_user(make_string_identifier(caller_username), true, GetRandomName(50), true,
iggy::ffi::UserStatus::Inactive, {}),
std::exception);
iggy::ffi::UserInfoDetails fetched{};
ASSERT_NO_THROW({ fetched = root_client->get_user(make_string_identifier(target_username)); });
EXPECT_EQ(fetched.id, target.id);
EXPECT_EQ(fetched.status, iggy::ffi::UserStatus::Active);
}
TEST_F(LowLevelE2E_Client, ManageUsersPermissionAllowsUpdateWithoutReadUsers) {
RecordProperty("description", "Allows updates from a manager without read_users permission.");
iggy::ffi::Client *root_client = GetLoggedInClient();
iggy::ffi::Client *manager_client = GetLoggedOutClient();
const std::string manager_username = GetRandomName(50);
const std::string target_username = GetRandomName(50);
const std::string replacement = GetRandomName(50);
iggy::ffi::Permissions permissions{};
permissions.global.manage_users = true;
ASSERT_NO_THROW({
CreateUser(root_client, manager_username, "secret123", iggy::ffi::UserStatus::Active, true,
std::move(permissions));
});
iggy::ffi::UserInfoDetails target{};
ASSERT_NO_THROW({ target = CreateUser(root_client, target_username, "secret123", iggy::ffi::UserStatus::Active); });
ASSERT_NO_THROW(manager_client->connect());
ASSERT_NO_THROW(manager_client->login_user(manager_username, "secret123"));
ASSERT_NO_THROW(manager_client->update_user(make_string_identifier(target_username), true, replacement, true,
iggy::ffi::UserStatus::Inactive, {}));
RenameTrackedUser(target_username, replacement);
iggy::ffi::UserInfoDetails fetched{};
ASSERT_NO_THROW({ fetched = root_client->get_user(make_string_identifier(replacement)); });
EXPECT_EQ(fetched.id, target.id);
EXPECT_EQ(fetched.status, iggy::ffi::UserStatus::Inactive);
EXPECT_EQ(static_cast<std::string>(fetched.username), replacement);
}
TEST_F(E2E_Client, GetUserBeforeLoginThrows) {
RecordProperty("description", "Rejects user lookup without an active authenticated session.");
auto client = GetLoggedOutHighLevelClient();
ASSERT_THROW(client.GetUser(iggy::Identifier::String("iggy")), std::exception);
ASSERT_NO_THROW(client.Connect());
ASSERT_THROW(client.GetUser(iggy::Identifier::String("iggy")), std::exception);
ASSERT_NO_THROW(client.Login("iggy", "iggy"));
ASSERT_NO_THROW(client.Logout());
ASSERT_THROW(client.GetUser(iggy::Identifier::String("iggy")), std::exception);
ASSERT_NO_THROW(client.Login("iggy", "iggy"));
ASSERT_NO_THROW(client.Disconnect());
ASSERT_THROW(client.GetUser(iggy::Identifier::String("iggy")), std::exception);
}
TEST_F(E2E_Client, GetUserByUsernameReturnsRootDetails) {
RecordProperty("description", "Returns deterministic root details for a username lookup.");
auto client = GetLoggedInHighLevelClient();
const auto user = client.GetUser(iggy::Identifier::String("iggy"));
EXPECT_EQ(user.Id(), 0u);
EXPECT_EQ(user.Username(), "iggy");
EXPECT_EQ(user.Status(), iggy::UserStatus::Active);
}
TEST_F(E2E_Client, GetUserByNumericIdMatchesUsernameLookup) {
RecordProperty("description", "Returns equivalent root details for username and numeric identifiers.");
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW({
const auto by_username = client.GetUser(iggy::Identifier::String("iggy"));
const auto by_id = client.GetUser(iggy::Identifier::Numeric(0));
EXPECT_EQ(by_id.Id(), by_username.Id());
EXPECT_EQ(by_id.Status(), by_username.Status());
EXPECT_EQ(by_id.Username(), by_username.Username());
ASSERT_TRUE(by_id.Permissions().has_value());
ASSERT_TRUE(by_username.Permissions().has_value());
const auto &by_id_permissions = by_id.Permissions().value();
const auto &by_username_permissions = by_username.Permissions().value();
EXPECT_EQ(by_id_permissions.Global().ManageServers(), by_username_permissions.Global().ManageServers());
EXPECT_EQ(by_id_permissions.Global().ReadServers(), by_username_permissions.Global().ReadServers());
EXPECT_EQ(by_id_permissions.Global().ManageUsers(), by_username_permissions.Global().ManageUsers());
EXPECT_EQ(by_id_permissions.Global().ReadUsers(), by_username_permissions.Global().ReadUsers());
EXPECT_EQ(by_id_permissions.Global().ManageStreams(), by_username_permissions.Global().ManageStreams());
EXPECT_EQ(by_id_permissions.Global().ReadStreams(), by_username_permissions.Global().ReadStreams());
EXPECT_EQ(by_id_permissions.Global().ManageTopics(), by_username_permissions.Global().ManageTopics());
EXPECT_EQ(by_id_permissions.Global().ReadTopics(), by_username_permissions.Global().ReadTopics());
EXPECT_EQ(by_id_permissions.Global().PollMessages(), by_username_permissions.Global().PollMessages());
EXPECT_EQ(by_id_permissions.Global().SendMessages(), by_username_permissions.Global().SendMessages());
EXPECT_EQ(by_id_permissions.Streams().size(), by_username_permissions.Streams().size());
});
}
TEST_F(E2E_Client, GetUserWithUnknownIdentifierThrows) {
RecordProperty("description", "Rejects lookups for unknown username and numeric identifiers.");
auto client = GetLoggedInHighLevelClient();
const std::string unknown_user = GetRandomName(50);
ASSERT_THROW(client.GetUser(iggy::Identifier::String(unknown_user)), std::exception);
ASSERT_THROW(client.GetUser(iggy::Identifier::Numeric(std::numeric_limits<std::uint32_t>::max())), std::exception);
}
TEST_F(E2E_Client, GetUsersBeforeLoginThrows) {
RecordProperty("description", "Rejects listing users without an active authenticated session.");
auto client = GetLoggedOutHighLevelClient();
ASSERT_THROW(client.GetUsers(), std::exception);
ASSERT_NO_THROW(client.Connect());
ASSERT_THROW(client.GetUsers(), std::exception);
ASSERT_NO_THROW(client.Login("iggy", "iggy"));
ASSERT_NO_THROW(client.Logout());
ASSERT_THROW(client.GetUsers(), std::exception);
ASSERT_NO_THROW(client.Login("iggy", "iggy"));
ASSERT_NO_THROW(client.Disconnect());
ASSERT_THROW(client.GetUsers(), std::exception);
}
TEST_F(E2E_Client, GetUserAllowsSelfLookupWithoutReadUsersPermission) {
RecordProperty("description", "Allows a user without read_users permission to read its own details.");
auto root_client = GetLoggedInHighLevelClient();
const std::string username = GetRandomName(50);
iggy::GlobalPermissions global;
global.SetReadUsers(false);
iggy::Permissions permissions;
permissions.SetGlobal(global);
ASSERT_NO_THROW({
const auto created_user = CreateUser(root_client, username, "secret123", iggy::UserStatus::Active, permissions);
auto user_client = GetLoggedOutHighLevelClient();
ASSERT_NO_THROW(user_client.Connect());
ASSERT_NO_THROW(user_client.Login(username, "secret123"));
const auto by_username = user_client.GetUser(iggy::Identifier::String(username));
const auto by_id = user_client.GetUser(iggy::Identifier::Numeric(created_user.Id()));
EXPECT_EQ(by_username.Id(), created_user.Id());
EXPECT_EQ(by_id.Id(), created_user.Id());
EXPECT_EQ(by_username.Username(), username);
EXPECT_EQ(by_id.Username(), username);
EXPECT_EQ(by_username.Status(), iggy::UserStatus::Active);
EXPECT_EQ(by_id.Status(), iggy::UserStatus::Active);
ASSERT_TRUE(by_username.Permissions().has_value());
ASSERT_TRUE(by_id.Permissions().has_value());
EXPECT_FALSE(by_username.Permissions()->Global().ReadUsers());
EXPECT_FALSE(by_id.Permissions()->Global().ReadUsers());
});
}
TEST_F(LowLevelE2E_Client, UserWithoutReadUsersPermissionCannotQueryOtherUsers) {
RecordProperty("description", "Rejects other-user lookup and listing without read_users permission.");
iggy::ffi::Client *root_client = GetLoggedInClient();
const std::string username = GetRandomName(50);
iggy::ffi::Permissions permissions{};
permissions.global.read_users = false;
ASSERT_NO_THROW({
CreateUser(root_client, username, "secret123", iggy::ffi::UserStatus::Active, true, std::move(permissions));
});
iggy::ffi::Client *user_client = GetLoggedOutClient();
ASSERT_NO_THROW(user_client->connect());
ASSERT_NO_THROW(user_client->login_user(username, "secret123"));
ASSERT_THROW(user_client->get_user(make_numeric_identifier(0)), std::exception);
ASSERT_THROW(user_client->get_users(), std::exception);
}
TEST_F(LowLevelE2E_Client, ReadUsersPermissionAllowsUserQueries) {
RecordProperty("description", "Allows user lookup and listing with only read_users permission.");
iggy::ffi::Client *root_client = GetLoggedInClient();
const std::string username = GetRandomName(50);
iggy::ffi::Permissions permissions{};
permissions.global.read_users = true;
ASSERT_NO_THROW({
CreateUser(root_client, username, "secret123", iggy::ffi::UserStatus::Active, true, std::move(permissions));
});
iggy::ffi::Client *user_client = GetLoggedOutClient();
ASSERT_NO_THROW(user_client->connect());
ASSERT_NO_THROW(user_client->login_user(username, "secret123"));
iggy::ffi::UserInfoDetails root{};
rust::Vec<iggy::ffi::UserInfo> users;
ASSERT_NO_THROW({ root = user_client->get_user(make_numeric_identifier(0)); });
ASSERT_NO_THROW({ users = user_client->get_users(); });
EXPECT_EQ(root.id, 0u);
EXPECT_EQ(static_cast<std::string>(root.username), "iggy");
bool found_self = false;
for (const auto &user : users) {
if (static_cast<std::string>(user.username) == username) {
found_self = true;
}
}
EXPECT_TRUE(found_self);
}
TEST_F(LowLevelE2E_Client, ManageUsersPermissionImpliesReadUsers) {
RecordProperty("description", "Allows user lookup and listing when manage_users is set without read_users.");
iggy::ffi::Client *root_client = GetLoggedInClient();
const std::string username = GetRandomName(50);
iggy::ffi::Permissions permissions{};
permissions.global.manage_users = true;
permissions.global.read_users = false;
ASSERT_NO_THROW({
CreateUser(root_client, username, "secret123", iggy::ffi::UserStatus::Active, true, std::move(permissions));
});
iggy::ffi::Client *user_client = GetLoggedOutClient();
ASSERT_NO_THROW(user_client->connect());
ASSERT_NO_THROW(user_client->login_user(username, "secret123"));
iggy::ffi::UserInfoDetails root{};
rust::Vec<iggy::ffi::UserInfo> users;
ASSERT_NO_THROW({ root = user_client->get_user(make_numeric_identifier(0)); });
ASSERT_NO_THROW({ users = user_client->get_users(); });
EXPECT_EQ(root.id, 0u);
EXPECT_EQ(static_cast<std::string>(root.username), "iggy");
bool found_self = false;
for (const auto &user : users) {
if (static_cast<std::string>(user.username) == username) {
found_self = true;
}
}
EXPECT_TRUE(found_self);
}
TEST_F(E2E_Client, GetUsersContainsCreatedUser) {
RecordProperty("description", "Lists a created user with the input username and status.");
auto client = GetLoggedInHighLevelClient();
const std::string username = GetRandomName(50);
ASSERT_NO_THROW({
const auto created_user = CreateUser(client, username, "secret123", iggy::UserStatus::Active);
const auto users = client.GetUsers();
bool found_user = false;
for (const auto &user : users) {
if (user.Id() == created_user.Id()) {
found_user = true;
EXPECT_EQ(user.Username(), username);
EXPECT_EQ(user.Status(), iggy::UserStatus::Active);
}
}
EXPECT_TRUE(found_user);
});
}
TEST_F(E2E_Client, GetUsersReturnsAllCreatedUsersOrderedById) {
RecordProperty("description", "Returns every created user without pagination, ordered by ID over TCP.");
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW({
const auto users_before = client.GetUsers();
const std::string first_username = GetRandomName(50);
const std::string second_username = GetRandomName(50);
const std::string third_username = GetRandomName(50);
const auto first_user = CreateUser(client, first_username, "secret123", iggy::UserStatus::Active);
const auto second_user = CreateUser(client, second_username, "secret123", iggy::UserStatus::Active);
const auto third_user = CreateUser(client, third_username, "secret123", iggy::UserStatus::Active);
const auto users_after = client.GetUsers();
ASSERT_EQ(users_after.size(), users_before.size() + 3);
bool found_first = false;
bool found_second = false;
bool found_third = false;
for (std::size_t index = 0; index < users_after.size(); ++index) {
if (index > 0) {
EXPECT_LT(users_after[index - 1].Id(), users_after[index].Id());
}
const auto &user = users_after[index];
const std::string username = user.Username();
if (user.Id() == first_user.Id() && username == first_username) {
found_first = true;
}
if (user.Id() == second_user.Id() && username == second_username) {
found_second = true;
}
if (user.Id() == third_user.Id() && username == third_username) {
found_third = true;
}
}
EXPECT_TRUE(found_first);
EXPECT_TRUE(found_second);
EXPECT_TRUE(found_third);
});
}
TEST_F(E2E_Client, GetUsersMatchesGetUserDetails) {
RecordProperty("description", "Returns consistent ID, username, and status from user query APIs.");
auto client = GetLoggedInHighLevelClient();
const std::string username = GetRandomName(50);
ASSERT_NO_THROW({
const auto created_user = CreateUser(client, username, "secret123", iggy::UserStatus::Active);
const auto user_details = client.GetUser(iggy::Identifier::String(username));
const auto users = client.GetUsers();
EXPECT_EQ(user_details.Id(), created_user.Id());
EXPECT_EQ(user_details.Username(), username);
EXPECT_EQ(user_details.Status(), iggy::UserStatus::Active);
bool found_user = false;
for (const auto &user : users) {
if (user.Id() == user_details.Id()) {
found_user = true;
EXPECT_EQ(user.Username(), username);
EXPECT_EQ(user.Status(), iggy::UserStatus::Active);
}
}
EXPECT_TRUE(found_user);
});
}
TEST_F(LowLevelE2E_Client, DeletedUserDisappearsFromGetUserAndGetUsers) {
RecordProperty("description", "Removes a deleted user from detail and list queries.");
iggy::ffi::Client *client = GetLoggedInClient();
const std::string username = GetRandomName(50);
iggy::ffi::UserInfoDetails created_user{};
ASSERT_NO_THROW({ created_user = CreateUser(client, username, "secret123", iggy::ffi::UserStatus::Active); });
ASSERT_NO_THROW(client->delete_user(make_string_identifier(username)));
ForgetUser(username);
ASSERT_THROW(client->get_user(make_string_identifier(username)), std::exception);
rust::Vec<iggy::ffi::UserInfo> users;
ASSERT_NO_THROW({ users = client->get_users(); });
for (const auto &user : users) {
EXPECT_FALSE(user.id == created_user.id && static_cast<std::string>(user.username) == username);
}
}
TEST_F(E2E_Client, DeleteUserRejectsUnauthenticatedClientWithoutDeletingTarget) {
RecordProperty("description", "Rejects user deletion without an active authenticated session.");
auto root_client = GetLoggedInHighLevelClient();
auto client = GetLoggedOutHighLevelClient();
const std::string username = GetRandomName(50);
ASSERT_NO_THROW({
const auto created_user = CreateUser(root_client, username, "secret123", iggy::UserStatus::Active);
ASSERT_THROW(client.DeleteUser(iggy::Identifier::String(username)), std::exception);
ASSERT_NO_THROW(client.Connect());
ASSERT_THROW(client.DeleteUser(iggy::Identifier::String(username)), std::exception);
ASSERT_NO_THROW(client.Login("iggy", "iggy"));
ASSERT_NO_THROW(client.Logout());
ASSERT_THROW(client.DeleteUser(iggy::Identifier::String(username)), std::exception);
ASSERT_NO_THROW(client.Login("iggy", "iggy"));
ASSERT_NO_THROW(client.Disconnect());
ASSERT_THROW(client.DeleteUser(iggy::Identifier::String(username)), std::exception);
const auto fetched_user = root_client.GetUser(iggy::Identifier::String(username));
EXPECT_EQ(fetched_user.Id(), created_user.Id());
});
}
TEST_F(E2E_Client, DeleteUserRejectsUnknownUsernameAndNumericId) {
RecordProperty("description", "Rejects deletion of unknown string and numeric user identifiers.");
auto client = GetLoggedInHighLevelClient();
const std::string unknown_user = GetRandomName(50);
constexpr std::uint32_t unknown_id = std::numeric_limits<std::uint32_t>::max();
ASSERT_THROW(client.DeleteUser(iggy::Identifier::String(unknown_user)), std::exception);
ASSERT_THROW(client.DeleteUser(iggy::Identifier::Numeric(unknown_id)), std::exception);
}
TEST_F(E2E_Client, DeleteUserRejectsRootWithoutChangingRoot) {
RecordProperty("description", "Rejects root deletion by username and numeric ID without changing root.");
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW({
const auto before_by_username = client.GetUser(iggy::Identifier::String("iggy"));
const auto before_by_id = client.GetUser(iggy::Identifier::Numeric(0));
ASSERT_EQ(before_by_username.Id(), 0u);
ASSERT_EQ(before_by_id.Id(), 0u);
ASSERT_THROW(client.DeleteUser(iggy::Identifier::String("iggy")), std::exception);
ASSERT_THROW(client.DeleteUser(iggy::Identifier::Numeric(0)), std::exception);
const auto after_by_username = client.GetUser(iggy::Identifier::String("iggy"));
const auto after_by_id = client.GetUser(iggy::Identifier::Numeric(0));
for (const auto *root : {&after_by_username, &after_by_id}) {
EXPECT_EQ(root->Id(), 0u);
EXPECT_EQ(root->Username(), "iggy");
EXPECT_EQ(root->Status(), iggy::UserStatus::Active);
}
});
}
TEST_F(E2E_Client, DeleteUserByNumericIdRemovesTarget) {
RecordProperty("description", "Deletes a user selected by its numeric ID.");
auto client = GetLoggedInHighLevelClient();
const std::string username = GetRandomName(50);
ASSERT_NO_THROW({
const auto created_user = CreateUser(client, username, "secret123", iggy::UserStatus::Active);
ASSERT_NO_THROW(client.DeleteUser(iggy::Identifier::Numeric(created_user.Id())));
ForgetUser(username);
ASSERT_THROW(client.GetUser(iggy::Identifier::String(username)), std::exception);
const auto users = client.GetUsers();
for (const auto &user : users) {
EXPECT_FALSE(user.Id() == created_user.Id() && user.Username() == username);
}
});
}
TEST_F(E2E_Client, DeleteUserRejectsRepeatedDeletion) {
RecordProperty("description", "Rejects deleting the same user twice.");
auto client = GetLoggedInHighLevelClient();
const std::string username = GetRandomName(50);
ASSERT_NO_THROW({
const auto created_user = CreateUser(client, username, "secret123", iggy::UserStatus::Active);
ASSERT_NO_THROW(client.DeleteUser(iggy::Identifier::String(username)));
ForgetUser(username);
ASSERT_THROW(client.DeleteUser(iggy::Identifier::String(username)), std::exception);
const auto users = client.GetUsers();
for (const auto &user : users) {
EXPECT_FALSE(user.Id() == created_user.Id() && user.Username() == username);
}
});
}
TEST_F(LowLevelE2E_Client, ReadUsersPermissionDoesNotAllowDeleteUser) {
RecordProperty("description", "Rejects other-user and self deletion with read_users but not manage_users.");
iggy::ffi::Client *root_client = GetLoggedInClient();
iggy::ffi::Client *caller_client = GetLoggedOutClient();
const std::string caller_name = GetRandomName(50);
const std::string target_name = GetRandomName(50);
iggy::ffi::Permissions permissions{};
permissions.global.read_users = true;
permissions.global.manage_users = false;
iggy::ffi::UserInfoDetails caller{};
iggy::ffi::UserInfoDetails target{};
ASSERT_NO_THROW({
caller = CreateUser(root_client, caller_name, "caller-secret", iggy::ffi::UserStatus::Active, true,
std::move(permissions));
});
ASSERT_NO_THROW({ target = CreateUser(root_client, target_name, "target-secret", iggy::ffi::UserStatus::Active); });
ASSERT_NO_THROW(caller_client->connect());
ASSERT_NO_THROW(caller_client->login_user(caller_name, "caller-secret"));
ASSERT_THROW(caller_client->delete_user(make_string_identifier(target_name)), std::exception);
ASSERT_THROW(caller_client->delete_user(make_string_identifier(caller_name)), std::exception);
iggy::ffi::UserInfoDetails fetched_caller{};
iggy::ffi::UserInfoDetails fetched_target{};
ASSERT_NO_THROW({ fetched_caller = root_client->get_user(make_string_identifier(caller_name)); });
ASSERT_NO_THROW({ fetched_target = root_client->get_user(make_string_identifier(target_name)); });
EXPECT_EQ(fetched_caller.id, caller.id);
EXPECT_EQ(fetched_target.id, target.id);
}
TEST_F(LowLevelE2E_Client, ManageUsersPermissionAllowsDeleteUser) {
RecordProperty("description", "Allows deletion with manage_users even when read_users is false.");
iggy::ffi::Client *root_client = GetLoggedInClient();
iggy::ffi::Client *manager_client = GetLoggedOutClient();
const std::string manager_name = GetRandomName(50);
const std::string target_name = GetRandomName(50);
iggy::ffi::Permissions permissions{};
permissions.global.manage_users = true;
permissions.global.read_users = false;
iggy::ffi::UserInfoDetails target{};
ASSERT_NO_THROW({
CreateUser(root_client, manager_name, "manager-secret", iggy::ffi::UserStatus::Active, true,
std::move(permissions));
});
ASSERT_NO_THROW({ target = CreateUser(root_client, target_name, "target-secret", iggy::ffi::UserStatus::Active); });
ASSERT_NO_THROW(manager_client->connect());
ASSERT_NO_THROW(manager_client->login_user(manager_name, "manager-secret"));
ASSERT_NO_THROW(manager_client->delete_user(make_string_identifier(target_name)));
ForgetUser(target_name);
ASSERT_THROW(root_client->get_user(make_string_identifier(target_name)), std::exception);
rust::Vec<iggy::ffi::UserInfo> users;
ASSERT_NO_THROW({ users = root_client->get_users(); });
for (const auto &user : users) {
EXPECT_FALSE(user.id == target.id && static_cast<std::string>(user.username) == target_name);
}
}
TEST_F(E2E_Client, DeleteUserRemovesOnlyTheTarget) {
RecordProperty("description", "Deletes only the selected user and leaves another user usable.");
auto root_client = GetLoggedInHighLevelClient();
auto survivor_client = GetLoggedOutHighLevelClient();
const std::string target_name = GetRandomName(50);
const std::string survivor_name = GetRandomName(50);
ASSERT_NO_THROW({
const auto target = CreateUser(root_client, target_name, "target-secret", iggy::UserStatus::Active);
const auto survivor = CreateUser(root_client, survivor_name, "survivor-secret", iggy::UserStatus::Active);
ASSERT_NO_THROW(root_client.DeleteUser(iggy::Identifier::Numeric(target.Id())));
ForgetUser(target_name);
ASSERT_THROW(root_client.GetUser(iggy::Identifier::String(target_name)), std::exception);
const auto users = root_client.GetUsers();
for (const auto &user : users) {
EXPECT_FALSE(user.Id() == target.Id() && user.Username() == target_name);
}
const auto fetched_survivor = root_client.GetUser(iggy::Identifier::String(survivor_name));
EXPECT_EQ(fetched_survivor.Id(), survivor.Id());
EXPECT_EQ(fetched_survivor.Username(), survivor_name);
EXPECT_EQ(fetched_survivor.Status(), survivor.Status());
});
ASSERT_NO_THROW(survivor_client.Connect());
ASSERT_NO_THROW(survivor_client.Login(survivor_name, "survivor-secret"));
}
TEST_F(LowLevelE2E_Client, DeleteInactiveUserSucceeds) {
RecordProperty("description", "Deletes an inactive user by username.");
iggy::ffi::Client *client = GetLoggedInClient();
const std::string username = GetRandomName(50);
iggy::ffi::UserInfoDetails created_user{};
ASSERT_NO_THROW({ created_user = CreateUser(client, username, "secret123", iggy::ffi::UserStatus::Inactive); });
ASSERT_NO_THROW(client->delete_user(make_string_identifier(username)));
ForgetUser(username);
ASSERT_THROW(client->get_user(make_string_identifier(username)), std::exception);
rust::Vec<iggy::ffi::UserInfo> users;
ASSERT_NO_THROW({ users = client->get_users(); });
for (const auto &user : users) {
EXPECT_FALSE(user.id == created_user.id && static_cast<std::string>(user.username) == username);
}
}
TEST_F(LowLevelE2E_Client, DeleteUserRevokesExistingSessions) {
RecordProperty("description", "Revokes an existing session and prevents fresh login after user deletion.");
iggy::ffi::Client *root_client = GetLoggedInClient();
iggy::ffi::Client *target_client = GetLoggedOutClient();
iggy::ffi::Client *fresh_client = GetLoggedOutClient();
const std::string username = GetRandomName(50);
const std::string password = "target-secret";
iggy::ffi::Permissions permissions{};
permissions.global.read_servers = true;
ASSERT_NO_THROW(
{ CreateUser(root_client, username, password, iggy::ffi::UserStatus::Active, true, std::move(permissions)); });
ASSERT_NO_THROW(target_client->connect());
ASSERT_NO_THROW(target_client->login_user(username, password));
iggy::ffi::Stats stats{};
ASSERT_NO_THROW({ stats = target_client->get_stats(); });
ASSERT_NO_THROW(root_client->delete_user(make_string_identifier(username)));
ForgetUser(username);
ASSERT_THROW(target_client->get_stats(), std::exception);
ASSERT_NO_THROW(fresh_client->connect());
ASSERT_THROW(fresh_client->login_user(username, password), std::exception);
}
TEST_F(LowLevelE2E_Client, DeletedUsernameCanBeRecreatedWithoutOldCredentialsOrPermissions) {
RecordProperty("description", "Recreates a deleted username without retaining old credentials or permissions.");
iggy::ffi::Client *root_client = GetLoggedInClient();
iggy::ffi::Client *new_password_client = GetLoggedOutClient();
iggy::ffi::Client *old_password_client = GetLoggedOutClient();
const std::string username = GetRandomName(50);
const std::string old_password = "old-secret";
const std::string new_password = "new-secret";
iggy::ffi::Permissions permissions{};
permissions.global.read_servers = true;
ASSERT_NO_THROW({
CreateUser(root_client, username, old_password, iggy::ffi::UserStatus::Active, true, std::move(permissions));
});
ASSERT_NO_THROW(root_client->delete_user(make_string_identifier(username)));
ForgetUser(username);
iggy::ffi::UserInfoDetails replacement{};
ASSERT_NO_THROW({ replacement = CreateUser(root_client, username, new_password, iggy::ffi::UserStatus::Active); });
EXPECT_EQ(static_cast<std::string>(replacement.username), username);
EXPECT_FALSE(replacement.has_permissions);
EXPECT_FALSE(replacement.permissions.global.read_servers);
EXPECT_TRUE(replacement.permissions.streams.empty());
iggy::ffi::UserInfoDetails fetched_replacement{};
ASSERT_NO_THROW({ fetched_replacement = root_client->get_user(make_string_identifier(username)); });
EXPECT_EQ(fetched_replacement.id, replacement.id);
ASSERT_NO_THROW(new_password_client->connect());
ASSERT_NO_THROW(new_password_client->login_user(username, new_password));
ASSERT_NO_THROW(old_password_client->connect());
ASSERT_THROW(old_password_client->login_user(username, old_password), std::exception);
}
TEST_F(LowLevelE2E_Client, ChangePasswordBeforeLoginThrows) {
RecordProperty("description",
"Rejects change_password before connect, after connect but before login, and after disconnect.");
iggy::ffi::Client *client = GetLoggedOutClient();
const auto user_id = make_string_identifier("iggy");
const std::string old_password = "iggy";
const std::string new_password = "iggy-updated-secret";
ASSERT_THROW(client->change_password(user_id, old_password, new_password), std::exception);
ASSERT_NO_THROW(client->connect());
ASSERT_THROW(client->change_password(user_id, old_password, new_password), std::exception);
ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
ASSERT_NO_THROW(client->disconnect());
ASSERT_THROW(client->change_password(user_id, old_password, new_password), std::exception);
}
TEST_F(LowLevelE2E_Client, ChangePasswordWithInvalidCurrentPasswordThrows) {
RecordProperty("description", "Rejects change_password when the provided current password is incorrect.");
iggy::ffi::Client *client = GetLoggedInClient();
const auto user_id = make_string_identifier("iggy");
const std::string wrong_password = "not-the-current-password";
const std::string new_password = "iggy-updated-secret";
ASSERT_THROW(client->change_password(user_id, wrong_password, new_password), std::exception);
ASSERT_NO_THROW(client->logout_user());
ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
}
TEST_F(LowLevelE2E_Client, ChangePasswordWithInvalidNewPasswordThrows) {
RecordProperty("description",
"Rejects change_password when the replacement password violates client-side length bounds.");
iggy::ffi::Client *client = GetLoggedInClient();
const auto user_id = make_string_identifier("iggy");
const std::string old_password = "iggy";
const std::string too_short;
const std::string too_long(256, 'a');
ASSERT_THROW(client->change_password(user_id, old_password, too_short), std::exception);
ASSERT_THROW(client->change_password(user_id, old_password, too_long), std::exception);
ASSERT_NO_THROW(client->logout_user());
ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
}
TEST_F(LowLevelE2E_Client, ChangePasswordForWrongUserThrows) {
RecordProperty("description", "Rejects change_password when targeting a user that does not exist.");
iggy::ffi::Client *client = GetLoggedInClient();
const auto wrong_user_id = make_string_identifier(GetRandomName());
const std::string current_password = "iggy";
const std::string replacement_secret = "iggy-updated-secret";
ASSERT_THROW(client->change_password(wrong_user_id, current_password, replacement_secret), std::exception);
ASSERT_NO_THROW(client->logout_user());
ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
}
TEST_F(LowLevelE2E_Client, UserWithoutManageUsersCannotChangeAnotherUsersPassword) {
RecordProperty("description",
"Rejects another user's password change without manage_users and preserves the old credentials.");
iggy::ffi::Client *root_client = GetLoggedInClient();
iggy::ffi::Client *actor_client = GetLoggedOutClient();
iggy::ffi::Client *target_client = GetLoggedOutClient();
const std::string actor_name = GetRandomName(50);
const std::string target_name = GetRandomName(50);
const std::string target_password = "target-secret";
const std::string new_password = "replacement-secret";
ASSERT_NO_THROW({ CreateUser(root_client, actor_name, "actor-secret", iggy::ffi::UserStatus::Active); });
iggy::ffi::UserInfoDetails target{};
ASSERT_NO_THROW({ target = CreateUser(root_client, target_name, target_password, iggy::ffi::UserStatus::Active); });
ASSERT_NO_THROW(actor_client->connect());
ASSERT_NO_THROW(actor_client->login_user(actor_name, "actor-secret"));
ASSERT_THROW(actor_client->change_password(make_numeric_identifier(target.id), target_password, new_password),
std::exception);
ASSERT_NO_THROW(target_client->connect());
ASSERT_NO_THROW(target_client->login_user(target_name, target_password));
}
TEST_F(LowLevelE2E_Client, ManageUsersPermissionAllowsChangingAnotherUsersPassword) {
RecordProperty("description", "Allows a user with manage_users to change another user's password.");
iggy::ffi::Client *root_client = GetLoggedInClient();
iggy::ffi::Client *manager_client = GetLoggedOutClient();
iggy::ffi::Client *old_password_client = GetLoggedOutClient();
iggy::ffi::Client *new_password_client = GetLoggedOutClient();
const std::string manager_name = GetRandomName(50);
const std::string target_name = GetRandomName(50);
const std::string target_password = "target-secret";
const std::string new_password = "replacement-secret";
iggy::ffi::Permissions permissions{};
permissions.global.manage_users = true;
ASSERT_NO_THROW({
CreateUser(root_client, manager_name, "manager-secret", iggy::ffi::UserStatus::Active, true,
std::move(permissions));
});
iggy::ffi::UserInfoDetails target{};
ASSERT_NO_THROW({ target = CreateUser(root_client, target_name, target_password, iggy::ffi::UserStatus::Active); });
ASSERT_NO_THROW(manager_client->connect());
ASSERT_NO_THROW(manager_client->login_user(manager_name, "manager-secret"));
ASSERT_NO_THROW(manager_client->change_password(make_numeric_identifier(target.id), target_password, new_password));
ASSERT_NO_THROW(old_password_client->connect());
ASSERT_THROW(old_password_client->login_user(target_name, target_password), std::exception);
ASSERT_NO_THROW(new_password_client->connect());
ASSERT_NO_THROW(new_password_client->login_user(target_name, new_password));
}
TEST_F(LowLevelE2E_Client, ChangePasswordUpdatesCredentialsAndCanBeRestored) {
RecordProperty("description",
"Changes the password for the current user, updates login behavior, and restores the original "
"password before the test exits.");
iggy::ffi::Client *client = GetLoggedInClient();
iggy::ffi::Client *second_client = GetLoggedOutClient();
iggy::ffi::Client *third_client = GetLoggedOutClient();
const auto user_id = make_string_identifier("iggy");
const std::string old_password = "iggy";
const std::string new_password = "iggy-updated-secret";
bool password_changed = false;
ASSERT_NO_THROW(client->change_password(user_id, old_password, new_password));
password_changed = true;
EXPECT_THROW(second_client->login_user("iggy", old_password), std::exception);
EXPECT_NO_THROW(second_client->login_user("iggy", new_password));
if (password_changed) {
EXPECT_NO_THROW(client->change_password(user_id, new_password, old_password));
}
EXPECT_NO_THROW(third_client->login_user("iggy", old_password));
}
TEST_F(LowLevelE2E_Client, DeleteWhileUnauthenticatedAfterFailedLogin) {
RecordProperty("description", "Allows client cleanup after a failed login leaves the connection unauthenticated.");
iggy::ffi::Client *client = nullptr;
ASSERT_NO_THROW({ client = iggy::ffi::new_connection({}); });
ASSERT_NE(client, nullptr);
ASSERT_NO_THROW(client->connect());
ASSERT_THROW(client->login_user("biggy", "biggy"), std::exception);
iggy::ffi::delete_client(client);
client = nullptr;
}
TEST_F(E2E_Client, ConnectLoginThenDisconnect) {
RecordProperty("description",
"Connects, logs in, disconnects successfully, and rejects authenticated operations afterward.");
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.Disconnect());
ASSERT_THROW(client.GetMe(), std::exception);
}
TEST_F(E2E_Client, DisconnectWithoutConnect) {
RecordProperty("description", "Allows disconnect to be called on a client that was never explicitly connected.");
auto client = GetLoggedOutHighLevelClient();
ASSERT_NO_THROW(client.Disconnect());
}
TEST_F(E2E_Client, DisconnectWithoutLogin) {
RecordProperty("description", "Allows disconnect after connect even when no user has authenticated.");
auto client = GetLoggedOutHighLevelClient();
ASSERT_NO_THROW(client.Connect());
ASSERT_NO_THROW(client.Disconnect());
ASSERT_THROW(client.GetMe(), std::exception);
}
TEST_F(E2E_Client, DisconnectThenReconnectWithoutRelogin) {
RecordProperty("description",
"Requires logging in again after a disconnect and reconnect before authenticated operations work.");
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.Disconnect());
ASSERT_NO_THROW(client.Connect());
ASSERT_THROW(client.GetMe(), std::exception);
}
TEST_F(E2E_Client, DisconnectAfterFailedLogin) {
RecordProperty("description", "Allows disconnect after a failed login attempt leaves the client unauthenticated.");
auto client = GetLoggedOutHighLevelClient();
ASSERT_NO_THROW(client.Connect());
ASSERT_THROW(client.Login("biggy", "biggy"), std::exception);
ASSERT_NO_THROW(client.Disconnect());
ASSERT_THROW(client.GetMe(), std::exception);
}
TEST_F(E2E_Client, ConnectLoginThenShutdown) {
RecordProperty("description",
"Connects, logs in, shuts down successfully, and rejects further operations afterward.");
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.GetMe());
ASSERT_NO_THROW(client.Shutdown());
ASSERT_THROW(client.GetMe(), std::exception);
ASSERT_THROW(client.GetClients(), std::exception);
}
TEST_F(E2E_Client, ShutdownWithoutConnect) {
RecordProperty("description", "Allows shutdown to be called on a client that was never explicitly connected.");
auto client = GetLoggedOutHighLevelClient();
ASSERT_NO_THROW(client.Shutdown());
}
TEST_F(E2E_Client, ShutdownWithoutLogin) {
RecordProperty("description", "Allows shutdown after connect even when no user has authenticated.");
auto client = GetLoggedOutHighLevelClient();
ASSERT_NO_THROW(client.Connect());
ASSERT_NO_THROW(client.Shutdown());
ASSERT_THROW(client.GetMe(), std::exception);
}
TEST_F(E2E_Client, ShutdownAfterFailedLogin) {
RecordProperty("description", "Allows shutdown after a failed login attempt leaves the client unauthenticated.");
auto client = GetLoggedOutHighLevelClient();
ASSERT_NO_THROW(client.Connect());
ASSERT_THROW(client.Login("biggy", "biggy"), std::exception);
ASSERT_NO_THROW(client.Shutdown());
ASSERT_THROW(client.GetMe(), std::exception);
}
TEST_F(E2E_Client, RepeatedShutdownCallsHaveStableBehavior) {
RecordProperty("description", "Keeps repeated shutdown calls stable across duplicate invocations.");
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.Shutdown());
ASSERT_NO_THROW(client.Shutdown());
ASSERT_THROW(client.GetMe(), std::exception);
}
TEST_F(E2E_Client, ShutdownThenConnectThrows) {
RecordProperty("description", "Rejects reconnecting a client after shutdown transitions it to a terminal state.");
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.Shutdown());
ASSERT_THROW(client.Connect(), std::exception);
}
TEST_F(E2E_Client, ShutdownThenLoginThrows) {
RecordProperty("description",
"Rejects logging in again after shutdown, even when login would normally auto-connect.");
auto client = GetLoggedOutHighLevelClient();
ASSERT_NO_THROW(client.Shutdown());
ASSERT_THROW(client.Login("iggy", "iggy"), std::exception);
}
TEST_F(E2E_Client, GetClientsReflectsSessionRemovalAfterShutdown) {
RecordProperty("description",
"Removes a shut down authenticated session from subsequent GetClients and GetClient results.");
auto first_client = GetLoggedInHighLevelClient();
auto second_client = GetLoggedInHighLevelClient();
const auto first_client_id = first_client.GetMe().ClientId();
ASSERT_NO_THROW(first_client.Shutdown());
constexpr auto removal_timeout = std::chrono::seconds(5);
constexpr auto removal_poll_interval = std::chrono::milliseconds(10);
const auto deadline = std::chrono::steady_clock::now() + removal_timeout;
bool removed = false;
do {
const auto clients = second_client.GetClients();
removed = std::none_of(clients.begin(), clients.end(),
[first_client_id](const auto &client) { return client.ClientId() == first_client_id; });
if (removed) {
break;
}
std::this_thread::sleep_for(removal_poll_interval);
} while (std::chrono::steady_clock::now() < deadline);
ASSERT_TRUE(removed);
ASSERT_THROW(second_client.GetClient(first_client_id), iggy::IggyException);
}
TEST_F(E2E_Client, GetClientsReflectsSessionRemovalAfterDisconnect) {
RecordProperty("description",
"Removes a disconnected authenticated session from subsequent GetClients and GetClient results.");
auto first_client = GetLoggedInHighLevelClient();
auto second_client = GetLoggedInHighLevelClient();
const auto first_client_id = first_client.GetMe().ClientId();
ASSERT_NO_THROW(first_client.Disconnect());
constexpr auto removal_timeout = std::chrono::seconds(5);
constexpr auto removal_poll_interval = std::chrono::milliseconds(10);
const auto deadline = std::chrono::steady_clock::now() + removal_timeout;
bool removed = false;
do {
const auto clients = second_client.GetClients();
removed = std::none_of(clients.begin(), clients.end(),
[first_client_id](const auto &client) { return client.ClientId() == first_client_id; });
if (removed) {
break;
}
std::this_thread::sleep_for(removal_poll_interval);
} while (std::chrono::steady_clock::now() < deadline);
ASSERT_TRUE(removed);
ASSERT_THROW(second_client.GetClient(first_client_id), iggy::IggyException);
}
TEST_F(E2E_Client, GetClientsReflectsLoggedOutSessionAsUnauthenticated) {
RecordProperty("description", "Drops a logged out session from GetClients and reports it missing in GetClient.");
auto first_client = GetLoggedInHighLevelClient();
auto second_client = GetLoggedInHighLevelClient();
const auto first_client_id = first_client.GetMe().ClientId();
// The VSR server drops the client-table entry on logout (an unauthenticated
// session is not tracked), unlike the legacy server which kept it visible
// without a user id.
ASSERT_NO_THROW(first_client.Logout());
constexpr auto removal_timeout = std::chrono::seconds(5);
constexpr auto removal_poll_interval = std::chrono::milliseconds(10);
const auto deadline = std::chrono::steady_clock::now() + removal_timeout;
bool removed = false;
do {
const auto clients = second_client.GetClients();
removed = std::none_of(clients.begin(), clients.end(),
[first_client_id](const auto &client) { return client.ClientId() == first_client_id; });
if (removed) {
break;
}
std::this_thread::sleep_for(removal_poll_interval);
} while (std::chrono::steady_clock::now() < deadline);
ASSERT_TRUE(removed);
ASSERT_THROW(second_client.GetClient(first_client_id), iggy::IggyException);
}
TEST_F(LowLevelE2E_Client, LoginWithoutConnect) {
RecordProperty("description", "Supports login without an explicit prior connect call.");
iggy::ffi::Client *client = nullptr;
ASSERT_NO_THROW({ client = iggy::ffi::new_connection({}); });
ASSERT_NE(client, nullptr);
TrackClient(client);
ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
}
TEST_F(LowLevelE2E_Client, ConnectWithoutLoginThenDelete) {
RecordProperty("description", "Allows connecting without logging in and then deleting the client.");
iggy::ffi::Client *client = nullptr;
ASSERT_NO_THROW({ client = iggy::ffi::new_connection({}); });
ASSERT_NE(client, nullptr);
ASSERT_NO_THROW(client->connect());
iggy::ffi::delete_client(client);
client = nullptr;
}
TEST_F(LowLevelE2E_Client, DeleteWithoutDisconnect) {
RecordProperty("description", "Allows deleting a connected and authenticated client without disconnecting first.");
iggy::ffi::Client *client = nullptr;
ASSERT_NO_THROW({ client = iggy::ffi::new_connection({}); });
ASSERT_NE(client, nullptr);
ASSERT_NO_THROW(client->connect());
ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
iggy::ffi::delete_client(client);
client = nullptr;
}
TEST_F(LowLevelE2E_Client, RepeatedClientMethodCallsHaveStableBehavior) {
RecordProperty("description",
"Keeps repeated connect, login, and delete calls stable across duplicate invocations.");
iggy::ffi::Client *client = nullptr;
ASSERT_NO_THROW({ client = iggy::ffi::new_connection({}); });
ASSERT_NE(client, nullptr);
ASSERT_NO_THROW(client->connect());
ASSERT_NO_THROW(client->connect());
ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
iggy::ffi::delete_client(client);
client = nullptr;
iggy::ffi::delete_client(client);
}
TEST_F(LowLevelE2E_Client, RepeatedDisconnectCallsHaveStableBehavior) {
RecordProperty("description", "Keeps repeated disconnect calls stable across duplicate invocations.");
iggy::ffi::Client *client = nullptr;
ASSERT_NO_THROW({ client = iggy::ffi::new_connection({}); });
ASSERT_NE(client, nullptr);
TrackClient(client);
ASSERT_NO_THROW(client->connect());
ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
ASSERT_NO_THROW(client->disconnect());
ASSERT_NO_THROW(client->disconnect());
ASSERT_THROW(client->get_me(), std::exception);
}
TEST_F(LowLevelE2E_Client, DeleteNullConnectionIsNoop) {
RecordProperty("description", "Treats deleting a null client pointer as a no-op.");
iggy::ffi::Client *client = nullptr;
iggy::ffi::delete_client(client);
}
TEST_F(E2E_Client, GetStatsBeforeLoginThrows) {
RecordProperty("description",
"Rejects GetStats before connect, after connect but before login, and after disconnect.");
auto client = GetLoggedOutHighLevelClient();
ASSERT_THROW((void)client.GetStats(), iggy::IggyException);
ASSERT_NO_THROW(client.Connect());
ASSERT_THROW((void)client.GetStats(), iggy::IggyException);
ASSERT_NO_THROW(client.Login("iggy", "iggy"));
ASSERT_NO_THROW(client.Disconnect());
ASSERT_THROW((void)client.GetStats(), iggy::IggyException);
}
// The VSR server has no unsaved-buffer primitive (writes are journaled at
// commit); FLUSH_UNSAVED_BUFFER denies typed with FeatureUnavailable even for
// resolvable targets.
TEST_F(E2E_Client, FlushUnsavedBufferThrowsForExistingPartition) {
RecordProperty("description",
"Rejects FlushUnsavedBuffer with the feature-unavailable error for an existing partition.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.CreateStream(stream_name));
TrackStream(stream_name);
const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name));
const auto topic_details = client.CreateTopic(iggy::Identifier::Numeric(stream_details.Id()), topic_name,
iggy::TopicCreateOptions().SetPartitionsCount(1));
iggy::ffi::Client *sender = GetLoggedInClient();
rust::Vec<iggy::ffi::IggyMessageToSend> messages;
messages.push_back(iggy::ffi::make_message(to_payload("flush-me"), rust::Vec<iggy::ffi::HeaderEntry>()));
ASSERT_NO_THROW(sender->send_messages(make_numeric_identifier(stream_details.Id()),
make_numeric_identifier(topic_details.Id()), "partition_id",
partition_id_bytes(0), std::move(messages)));
ASSERT_THROW(client.FlushUnsavedBuffer(iggy::Identifier::Numeric(stream_details.Id()),
iggy::Identifier::Numeric(topic_details.Id()), 0, true),
iggy::IggyException);
}
TEST_F(E2E_Client, FlushUnsavedBufferThrowsForExistingEmptyPartition) {
RecordProperty(
"description",
"Rejects FlushUnsavedBuffer with the feature-unavailable error for a partition with no unsaved messages.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.CreateStream(stream_name));
TrackStream(stream_name);
const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name));
const auto topic_details = client.CreateTopic(iggy::Identifier::Numeric(stream_details.Id()), topic_name,
iggy::TopicCreateOptions().SetPartitionsCount(1));
ASSERT_THROW(client.FlushUnsavedBuffer(iggy::Identifier::Numeric(stream_details.Id()),
iggy::Identifier::Numeric(topic_details.Id()), 0, true),
iggy::IggyException);
}
TEST_F(E2E_Client, FlushUnsavedBufferBeforeLoginThrows) {
RecordProperty("description",
"Throws when FlushUnsavedBuffer is called before connect, after connect but before login, and "
"after disconnect.");
auto client = GetLoggedOutHighLevelClient();
ASSERT_THROW(client.FlushUnsavedBuffer(iggy::Identifier::Numeric(1), iggy::Identifier::Numeric(1), 0, true),
iggy::IggyException);
ASSERT_NO_THROW(client.Connect());
ASSERT_THROW(client.FlushUnsavedBuffer(iggy::Identifier::Numeric(1), iggy::Identifier::Numeric(1), 0, true),
iggy::IggyException);
ASSERT_NO_THROW(client.Login("iggy", "iggy"));
ASSERT_NO_THROW(client.Disconnect());
ASSERT_THROW(client.FlushUnsavedBuffer(iggy::Identifier::Numeric(1), iggy::Identifier::Numeric(1), 0, true),
iggy::IggyException);
}
TEST_F(E2E_Client, FlushUnsavedBufferOnNonExistentStreamThrows) {
RecordProperty("description", "Throws when FlushUnsavedBuffer is called for a stream that does not exist.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.CreateStream(stream_name));
TrackStream(stream_name);
const auto topic_details = client.CreateTopic(iggy::Identifier::String(stream_name), topic_name,
iggy::TopicCreateOptions().SetPartitionsCount(1));
ASSERT_THROW(client.FlushUnsavedBuffer(iggy::Identifier::String(GetRandomName()),
iggy::Identifier::Numeric(topic_details.Id()), 0, true),
iggy::IggyException);
}
TEST_F(E2E_Client, FlushUnsavedBufferOnNonExistentTopicThrows) {
RecordProperty("description", "Throws when FlushUnsavedBuffer is called for a topic that does not exist.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.CreateStream(stream_name));
TrackStream(stream_name);
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name,
iggy::TopicCreateOptions().SetPartitionsCount(1)));
ASSERT_THROW(client.FlushUnsavedBuffer(iggy::Identifier::String(stream_name),
iggy::Identifier::String(GetRandomName()), 0, true),
iggy::IggyException);
}
TEST_F(E2E_Client, FlushUnsavedBufferAfterStreamDeletedThrows) {
RecordProperty("description", "Throws when FlushUnsavedBuffer is called after the stream has been deleted.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.CreateStream(stream_name));
const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name));
const auto topic_details = client.CreateTopic(iggy::Identifier::Numeric(stream_details.Id()), topic_name,
iggy::TopicCreateOptions().SetPartitionsCount(1));
const std::uint32_t saved_stream_id = stream_details.Id();
const std::uint32_t saved_topic_id = topic_details.Id();
ASSERT_NO_THROW(client.DeleteStream(iggy::Identifier::Numeric(saved_stream_id)));
ForgetTrackedStream(stream_name);
ASSERT_THROW(client.FlushUnsavedBuffer(iggy::Identifier::Numeric(saved_stream_id),
iggy::Identifier::Numeric(saved_topic_id), 0, true),
iggy::IggyException);
}
TEST_F(E2E_Client, FlushUnsavedBufferAfterTopicDeletedThrows) {
RecordProperty("description", "Throws when FlushUnsavedBuffer is called after the topic has been deleted.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.CreateStream(stream_name));
TrackStream(stream_name);
const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name));
const auto topic_details = client.CreateTopic(iggy::Identifier::Numeric(stream_details.Id()), topic_name,
iggy::TopicCreateOptions().SetPartitionsCount(1));
ASSERT_NO_THROW(
client.DeleteTopic(iggy::Identifier::Numeric(stream_details.Id()), iggy::Identifier::String(topic_name)));
ASSERT_THROW(client.FlushUnsavedBuffer(iggy::Identifier::Numeric(stream_details.Id()),
iggy::Identifier::Numeric(topic_details.Id()), 0, true),
iggy::IggyException);
}
TEST_F(E2E_Client, FlushUnsavedBufferTwiceThrows) {
RecordProperty("description",
"Rejects FlushUnsavedBuffer with the feature-unavailable error consistently across repeat calls.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.CreateStream(stream_name));
TrackStream(stream_name);
const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name));
const auto topic_details = client.CreateTopic(iggy::Identifier::Numeric(stream_details.Id()), topic_name,
iggy::TopicCreateOptions().SetPartitionsCount(1));
iggy::ffi::Client *sender = GetLoggedInClient();
rust::Vec<iggy::ffi::IggyMessageToSend> messages;
messages.push_back(iggy::ffi::make_message(to_payload("flush-twice"), rust::Vec<iggy::ffi::HeaderEntry>()));
ASSERT_NO_THROW(sender->send_messages(make_numeric_identifier(stream_details.Id()),
make_numeric_identifier(topic_details.Id()), "partition_id",
partition_id_bytes(0), std::move(messages)));
ASSERT_THROW(client.FlushUnsavedBuffer(iggy::Identifier::Numeric(stream_details.Id()),
iggy::Identifier::Numeric(topic_details.Id()), 0, true),
iggy::IggyException);
ASSERT_THROW(client.FlushUnsavedBuffer(iggy::Identifier::Numeric(stream_details.Id()),
iggy::Identifier::Numeric(topic_details.Id()), 0, true),
iggy::IggyException);
}
TEST_F(E2E_Client, FlushUnsavedBufferWithInvalidPartitionIdsThrows) {
RecordProperty("description", "Throws when FlushUnsavedBuffer is called for non-existent partition ids.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.CreateStream(stream_name));
TrackStream(stream_name);
const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name));
const auto topic_details = client.CreateTopic(iggy::Identifier::Numeric(stream_details.Id()), topic_name,
iggy::TopicCreateOptions().SetPartitionsCount(1));
const std::uint32_t invalid_partition_ids[] = {1u, 9999u, static_cast<std::uint32_t>(-1)};
for (const std::uint32_t invalid_partition_id : invalid_partition_ids) {
SCOPED_TRACE(invalid_partition_id);
ASSERT_THROW(
client.FlushUnsavedBuffer(iggy::Identifier::Numeric(stream_details.Id()),
iggy::Identifier::Numeric(topic_details.Id()), invalid_partition_id, true),
iggy::IggyException);
}
}
TEST_F(E2E_Client, DeleteSegmentsBeforeLoginThrows) {
RecordProperty("description",
"Rejects DeleteSegments before connect, after connect but before login, and after disconnect.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
auto setup_client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(setup_client.CreateStream(stream_name));
TrackStream(stream_name);
ASSERT_NO_THROW(setup_client.CreateTopic(iggy::Identifier::String(stream_name), topic_name,
iggy::TopicCreateOptions().SetPartitionsCount(1)));
auto unauthenticated_client = GetLoggedOutHighLevelClient();
ASSERT_THROW(unauthenticated_client.DeleteSegments(iggy::Identifier::String(stream_name),
iggy::Identifier::String(topic_name), 0, 1),
iggy::IggyException);
ASSERT_NO_THROW(unauthenticated_client.Connect());
ASSERT_THROW(unauthenticated_client.DeleteSegments(iggy::Identifier::String(stream_name),
iggy::Identifier::String(topic_name), 0, 1),
iggy::IggyException);
ASSERT_NO_THROW(unauthenticated_client.Login("iggy", "iggy"));
ASSERT_NO_THROW(unauthenticated_client.Disconnect());
ASSERT_THROW(unauthenticated_client.DeleteSegments(iggy::Identifier::String(stream_name),
iggy::Identifier::String(topic_name), 0, 1),
iggy::IggyException);
}
TEST_F(E2E_Client, DeleteSegmentsOnNonExistentStreamThrows) {
RecordProperty("description", "Throws when deleting segments from a stream that does not exist.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
const std::string missing_stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.CreateStream(stream_name));
TrackStream(stream_name);
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name,
iggy::TopicCreateOptions().SetPartitionsCount(1)));
ASSERT_THROW(client.DeleteSegments(iggy::Identifier::String(missing_stream_name),
iggy::Identifier::String(topic_name), 0, 1),
iggy::IggyException);
}
TEST_F(E2E_Client, DeleteSegmentsOnNonExistentTopicThrows) {
RecordProperty("description", "Throws when deleting segments from a topic that does not exist.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
const std::string missing_topic_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.CreateStream(stream_name));
TrackStream(stream_name);
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name,
iggy::TopicCreateOptions().SetPartitionsCount(1)));
ASSERT_THROW(client.DeleteSegments(iggy::Identifier::String(stream_name),
iggy::Identifier::String(missing_topic_name), 0, 1),
iggy::IggyException);
}
TEST_F(E2E_Client, DeleteSegmentsOnNonExistentPartitionThrows) {
RecordProperty("description", "Throws when deleting segments from a partition that does not exist.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.CreateStream(stream_name));
TrackStream(stream_name);
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name,
iggy::TopicCreateOptions().SetPartitionsCount(1)));
ASSERT_THROW(
client.DeleteSegments(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name), 999, 1),
iggy::IggyException);
}
TEST_F(E2E_Client, DeleteSegmentsWithZeroCountIsNoOp) {
RecordProperty("description", "Treats DeleteSegments with count 0 as a no-op.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.CreateStream(stream_name));
TrackStream(stream_name);
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name,
iggy::TopicCreateOptions().SetPartitionsCount(1)));
const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name));
ASSERT_EQ(stream_details.Topics().size(), 1u);
const std::uint32_t stream_id = stream_details.Id();
const std::uint32_t topic_id = stream_details.Topics().front().Id();
iggy::ffi::Client *sender = GetLoggedInClient();
rust::Vec<iggy::ffi::IggyMessageToSend> messages;
for (std::uint32_t i = 0; i < 5; ++i) {
messages.push_back(iggy::ffi::make_message(to_payload("zero-count-" + std::to_string(i)),
rust::Vec<iggy::ffi::HeaderEntry>()));
}
ASSERT_NO_THROW(sender->send_messages(make_numeric_identifier(stream_id), make_numeric_identifier(topic_id),
"partition_id", partition_id_bytes(0), std::move(messages)));
const auto find_partition = [&client, stream_id, topic_id](std::uint32_t partition_id) {
const auto topic_details =
client.GetTopic(iggy::Identifier::Numeric(stream_id), iggy::Identifier::Numeric(topic_id));
for (const auto &partition : topic_details.Partitions()) {
if (partition.Id() == partition_id) {
return partition;
}
}
throw iggy::IggyException("Partition was not found");
};
const auto partition_before_delete = find_partition(0);
iggy::ffi::PolledMessages polled_before_delete{};
ASSERT_NO_THROW({
polled_before_delete =
sender->poll_messages(make_numeric_identifier(stream_id), make_numeric_identifier(topic_id), 0, "consumer",
make_numeric_identifier(1005), "offset", 0, 1000, false);
});
ASSERT_NO_THROW(
client.DeleteSegments(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name), 0, 0));
const auto partition_after_delete = find_partition(0);
iggy::ffi::PolledMessages polled_after_delete{};
ASSERT_NO_THROW({
polled_after_delete =
sender->poll_messages(make_numeric_identifier(stream_id), make_numeric_identifier(topic_id), 0, "consumer",
make_numeric_identifier(1006), "offset", 0, 1000, false);
});
EXPECT_EQ(partition_after_delete.SegmentsCount(), partition_before_delete.SegmentsCount());
EXPECT_EQ(partition_after_delete.CurrentOffset(), partition_before_delete.CurrentOffset());
EXPECT_EQ(partition_after_delete.MessagesCount(), partition_before_delete.MessagesCount());
EXPECT_EQ(partition_after_delete.SizeBytes(), partition_before_delete.SizeBytes());
EXPECT_EQ(polled_after_delete.count, polled_before_delete.count);
ASSERT_EQ(polled_after_delete.messages.size(), polled_before_delete.messages.size());
for (std::size_t i = 0; i < polled_before_delete.messages.size(); ++i) {
EXPECT_EQ(polled_after_delete.messages[i].offset, polled_before_delete.messages[i].offset);
}
}
TEST_F(E2E_Client, DeleteSegmentsWhenOnlyActiveSegmentRemainsIsNoOp) {
RecordProperty("description",
"Keeps the partition unchanged when DeleteSegments is called with only the active segment.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.CreateStream(stream_name));
TrackStream(stream_name);
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name,
iggy::TopicCreateOptions().SetPartitionsCount(1)));
const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name));
ASSERT_EQ(stream_details.Topics().size(), 1u);
const std::uint32_t stream_id = stream_details.Id();
const std::uint32_t topic_id = stream_details.Topics().front().Id();
iggy::ffi::Client *sender = GetLoggedInClient();
rust::Vec<iggy::ffi::IggyMessageToSend> messages;
for (std::uint32_t i = 0; i < 5; ++i) {
messages.push_back(iggy::ffi::make_message(to_payload("active-only-" + std::to_string(i)),
rust::Vec<iggy::ffi::HeaderEntry>()));
}
ASSERT_NO_THROW(sender->send_messages(make_numeric_identifier(stream_id), make_numeric_identifier(topic_id),
"partition_id", partition_id_bytes(0), std::move(messages)));
const auto find_partition = [&client, stream_id, topic_id](std::uint32_t partition_id) {
const auto topic_details =
client.GetTopic(iggy::Identifier::Numeric(stream_id), iggy::Identifier::Numeric(topic_id));
for (const auto &partition : topic_details.Partitions()) {
if (partition.Id() == partition_id) {
return partition;
}
}
throw iggy::IggyException("Partition was not found");
};
const auto partition_before_delete = find_partition(0);
ASSERT_EQ(partition_before_delete.SegmentsCount(), 1u);
iggy::ffi::PolledMessages polled_before_delete{};
ASSERT_NO_THROW({
polled_before_delete =
sender->poll_messages(make_numeric_identifier(stream_id), make_numeric_identifier(topic_id), 0, "consumer",
make_numeric_identifier(1007), "offset", 0, 1000, false);
});
ASSERT_NO_THROW(
client.DeleteSegments(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name), 0, 1));
const auto partition_after_delete = find_partition(0);
iggy::ffi::PolledMessages polled_after_delete{};
ASSERT_NO_THROW({
polled_after_delete =
sender->poll_messages(make_numeric_identifier(stream_id), make_numeric_identifier(topic_id), 0, "consumer",
make_numeric_identifier(1008), "offset", 0, 1000, false);
});
EXPECT_EQ(partition_after_delete.SegmentsCount(), partition_before_delete.SegmentsCount());
EXPECT_EQ(partition_after_delete.CurrentOffset(), partition_before_delete.CurrentOffset());
EXPECT_EQ(partition_after_delete.MessagesCount(), partition_before_delete.MessagesCount());
EXPECT_EQ(partition_after_delete.SizeBytes(), partition_before_delete.SizeBytes());
EXPECT_EQ(polled_after_delete.count, polled_before_delete.count);
ASSERT_EQ(polled_after_delete.messages.size(), polled_before_delete.messages.size());
for (std::size_t i = 0; i < polled_before_delete.messages.size(); ++i) {
EXPECT_EQ(polled_after_delete.messages[i].offset, polled_before_delete.messages[i].offset);
}
}
// TODO(slbotbm): add a test to create some streams, topics, partitions, and segments, send messages, and create
// consumer groups and verify it.
TEST_F(E2E_Client, GetStatsReturnsServerStats) {
RecordProperty("description",
"Returns empty resource counts first, then reflects aggregated streams, topics, partitions, "
"consumer groups, and clients.");
const std::string first_stream_name = GetRandomName();
const std::string second_stream_name = GetRandomName();
const std::string first_topic_name = GetRandomName();
const std::string second_topic_name = GetRandomName();
const std::string third_topic_name = GetRandomName();
const std::string first_group_name = GetRandomName();
const std::string second_group_name = GetRandomName();
const std::string third_group_name = GetRandomName();
constexpr std::uint32_t additional_partitions_count = 2;
auto client = GetLoggedInHighLevelClient();
const auto empty_stats = client.GetStats();
EXPECT_NE(empty_stats.ProcessId(), 0u);
EXPECT_GT(empty_stats.ThreadsCount(), 0u);
EXPECT_GT(empty_stats.TotalMemory(), 0u);
EXPECT_LE(empty_stats.AvailableMemory(), empty_stats.TotalMemory());
EXPECT_GE(empty_stats.TotalDiskSpace(), empty_stats.FreeDiskSpace());
EXPECT_FALSE(empty_stats.Hostname().empty());
EXPECT_FALSE(empty_stats.OsName().empty());
EXPECT_FALSE(empty_stats.OsVersion().empty());
EXPECT_FALSE(empty_stats.KernelVersion().empty());
EXPECT_FALSE(empty_stats.IggyServerVersion().empty());
ASSERT_NO_THROW(client.CreateStream(first_stream_name));
TrackStream(first_stream_name);
ASSERT_NO_THROW(client.CreateStream(second_stream_name));
TrackStream(second_stream_name);
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(first_stream_name), first_topic_name,
iggy::TopicCreateOptions().SetPartitionsCount(1)));
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(first_stream_name), second_topic_name,
iggy::TopicCreateOptions().SetPartitionsCount(2)));
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(second_stream_name), third_topic_name,
iggy::TopicCreateOptions().SetPartitionsCount(3)));
ASSERT_NO_THROW(client.CreatePartitions(iggy::Identifier::String(first_stream_name),
iggy::Identifier::String(first_topic_name), additional_partitions_count));
const auto first_group = client.CreateConsumerGroup(iggy::Identifier::String(first_stream_name),
iggy::Identifier::String(first_topic_name), first_group_name);
const auto second_group = client.CreateConsumerGroup(
iggy::Identifier::String(first_stream_name), iggy::Identifier::String(second_topic_name), second_group_name);
const auto third_group = client.CreateConsumerGroup(iggy::Identifier::String(second_stream_name),
iggy::Identifier::String(third_topic_name), third_group_name);
auto second_client = GetLoggedInHighLevelClient();
auto third_client = GetLoggedInHighLevelClient();
(void)second_client;
(void)third_client;
const auto first_stream_details = client.GetStream(iggy::Identifier::String(first_stream_name));
const auto second_stream_details = client.GetStream(iggy::Identifier::String(second_stream_name));
const std::uint32_t expected_topics_count =
first_stream_details.TopicsCount() + second_stream_details.TopicsCount();
std::uint32_t first_topic_partitions = 0;
std::uint32_t second_topic_partitions = 0;
std::uint32_t third_topic_partitions = 0;
for (const auto &topic : first_stream_details.Topics()) {
if (topic.Name() == first_topic_name) {
first_topic_partitions = topic.PartitionsCount();
}
if (topic.Name() == second_topic_name) {
second_topic_partitions = topic.PartitionsCount();
}
}
for (const auto &topic : second_stream_details.Topics()) {
if (topic.Name() == third_topic_name) {
third_topic_partitions = topic.PartitionsCount();
}
}
const std::uint32_t expected_partitions_count =
first_topic_partitions + second_topic_partitions + third_topic_partitions;
const auto stats_after_create = client.GetStats();
EXPECT_GE(stats_after_create.StreamsCount(), empty_stats.StreamsCount() + 2u);
EXPECT_GE(stats_after_create.TopicsCount(), empty_stats.TopicsCount() + expected_topics_count);
EXPECT_GE(stats_after_create.PartitionsCount(), empty_stats.PartitionsCount() + expected_partitions_count);
EXPECT_GE(stats_after_create.SegmentsCount(), empty_stats.SegmentsCount() + expected_partitions_count);
EXPECT_GE(stats_after_create.ConsumerGroupsCount(), empty_stats.ConsumerGroupsCount() + 3u);
EXPECT_GE(stats_after_create.ClientsCount(), empty_stats.ClientsCount() + 2u);
EXPECT_EQ(first_group.PartitionsCount(), first_topic_partitions);
EXPECT_EQ(second_group.PartitionsCount(), second_topic_partitions);
EXPECT_EQ(third_group.PartitionsCount(), third_topic_partitions);
ASSERT_NO_THROW(client.DeleteStream(iggy::Identifier::String(second_stream_name)));
ForgetTrackedStream(second_stream_name);
ASSERT_NO_THROW(client.DeleteStream(iggy::Identifier::String(first_stream_name)));
ForgetTrackedStream(first_stream_name);
const auto stats = client.GetStats();
EXPECT_LE(stats.StreamsCount(), stats_after_create.StreamsCount());
EXPECT_LE(stats.TopicsCount(), stats_after_create.TopicsCount());
EXPECT_LE(stats.PartitionsCount(), stats_after_create.PartitionsCount());
EXPECT_LE(stats.SegmentsCount(), stats_after_create.SegmentsCount());
EXPECT_LE(stats.ConsumerGroupsCount(), stats_after_create.ConsumerGroupsCount());
EXPECT_LE(stats.ClientsCount(), stats_after_create.ClientsCount());
}
TEST_F(E2E_Client, GetStatsIsStableAcrossBackToBackCalls) {
RecordProperty("description",
"Returns sane invariant fields across back-to-back GetStats calls on an idle authenticated client.");
auto client = GetLoggedInHighLevelClient();
const auto first_stats = client.GetStats();
const auto second_stats = client.GetStats();
EXPECT_NE(first_stats.ProcessId(), 0u);
EXPECT_NE(second_stats.ProcessId(), 0u);
EXPECT_EQ(second_stats.ProcessId(), first_stats.ProcessId());
EXPECT_GT(first_stats.ThreadsCount(), 0u);
EXPECT_GT(second_stats.ThreadsCount(), 0u);
EXPECT_GT(first_stats.TotalMemory(), 0u);
EXPECT_GT(second_stats.TotalMemory(), 0u);
EXPECT_FALSE(first_stats.Hostname().empty());
EXPECT_FALSE(second_stats.Hostname().empty());
EXPECT_FALSE(first_stats.OsName().empty());
EXPECT_FALSE(second_stats.OsName().empty());
EXPECT_FALSE(first_stats.OsVersion().empty());
EXPECT_FALSE(second_stats.OsVersion().empty());
EXPECT_FALSE(first_stats.KernelVersion().empty());
EXPECT_FALSE(second_stats.KernelVersion().empty());
EXPECT_FALSE(first_stats.IggyServerVersion().empty());
EXPECT_FALSE(second_stats.IggyServerVersion().empty());
EXPECT_EQ(second_stats.Hostname(), first_stats.Hostname());
EXPECT_EQ(second_stats.OsName(), first_stats.OsName());
EXPECT_EQ(second_stats.OsVersion(), first_stats.OsVersion());
EXPECT_EQ(second_stats.KernelVersion(), first_stats.KernelVersion());
EXPECT_EQ(second_stats.IggyServerVersion(), first_stats.IggyServerVersion());
EXPECT_EQ(second_stats.ServerSemver(), first_stats.ServerSemver());
EXPECT_GE(first_stats.ClientsCount(), 1u);
EXPECT_GE(second_stats.ClientsCount(), 1u);
}
TEST_F(E2E_Client, GetMeBeforeLoginThrows) {
RecordProperty("description",
"Rejects GetMe before connect, after connect but before login, and after disconnect.");
auto client = GetLoggedOutHighLevelClient();
ASSERT_THROW(client.GetMe(), iggy::IggyException);
ASSERT_NO_THROW(client.Connect());
ASSERT_THROW(client.GetMe(), iggy::IggyException);
ASSERT_NO_THROW(client.Login("iggy", "iggy"));
ASSERT_NO_THROW(client.Disconnect());
ASSERT_THROW(client.GetMe(), iggy::IggyException);
}
TEST_F(E2E_Client, GetMeReturnsCurrentClientDetails) {
RecordProperty("description", "Returns the current authenticated client details.");
auto client = GetLoggedInHighLevelClient();
const auto me = client.GetMe();
EXPECT_NE(me.ClientId(), 0u);
EXPECT_TRUE(me.UserId().has_value());
EXPECT_FALSE(me.Address().empty());
EXPECT_EQ(me.Transport(), "TCP");
EXPECT_EQ(me.ConsumerGroupsCount(), 0u);
EXPECT_TRUE(me.ConsumerGroups().empty());
}
TEST_F(E2E_Client, GetMeReflectsConsumerGroupMembershipChanges) {
RecordProperty("description", "Reflects joined consumer groups in GetMe and removes them again after leaving.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
const std::string group_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.CreateStream(stream_name));
TrackStream(stream_name);
const auto topic_details = client.CreateTopic(iggy::Identifier::String(stream_name), topic_name,
iggy::TopicCreateOptions().SetPartitionsCount(1));
const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name));
const auto created_group = client.CreateConsumerGroup(iggy::Identifier::String(stream_name),
iggy::Identifier::String(topic_name), group_name);
const auto baseline_me = client.GetMe();
const auto baseline_groups_count = baseline_me.ConsumerGroupsCount();
const auto baseline_groups_size = baseline_me.ConsumerGroups().size();
ASSERT_NO_THROW(client.JoinConsumerGroup(iggy::Identifier::Numeric(stream_details.Id()),
iggy::Identifier::Numeric(topic_details.Id()),
iggy::Identifier::Numeric(created_group.Id())));
{
const auto me = client.GetMe();
EXPECT_GT(me.ConsumerGroupsCount(), baseline_groups_count);
EXPECT_GT(me.ConsumerGroups().size(), baseline_groups_size);
bool found_group = false;
for (const auto &group : me.ConsumerGroups()) {
if (group.StreamId() != stream_details.Id() || group.TopicId() != topic_details.Id() ||
group.GroupId() != created_group.Id()) {
continue;
}
found_group = true;
break;
}
EXPECT_TRUE(found_group);
}
ASSERT_NO_THROW(client.LeaveConsumerGroup(iggy::Identifier::Numeric(stream_details.Id()),
iggy::Identifier::Numeric(topic_details.Id()),
iggy::Identifier::Numeric(created_group.Id())));
{
const auto me = client.GetMe();
EXPECT_GE(me.ConsumerGroupsCount(), baseline_groups_count);
EXPECT_GE(me.ConsumerGroups().size(), baseline_groups_size);
bool found_group = false;
for (const auto &group : me.ConsumerGroups()) {
if (group.StreamId() != stream_details.Id() || group.TopicId() != topic_details.Id() ||
group.GroupId() != created_group.Id()) {
continue;
}
found_group = true;
break;
}
EXPECT_FALSE(found_group);
}
}
TEST_F(E2E_Client, GetMeIsStableAcrossBackToBackCalls) {
RecordProperty("description", "Returns stable current-client details across back-to-back GetMe calls.");
auto client = GetLoggedInHighLevelClient();
const auto first_me = client.GetMe();
const auto second_me = client.GetMe();
EXPECT_NE(first_me.ClientId(), 0u);
EXPECT_TRUE(first_me.UserId().has_value());
EXPECT_TRUE(second_me.UserId().has_value());
EXPECT_EQ(second_me.ClientId(), first_me.ClientId());
EXPECT_EQ(second_me.UserId(), first_me.UserId());
EXPECT_EQ(second_me.Address(), first_me.Address());
EXPECT_EQ(first_me.Transport(), "TCP");
EXPECT_EQ(second_me.Transport(), "TCP");
EXPECT_EQ(second_me.Transport(), first_me.Transport());
EXPECT_EQ(second_me.ConsumerGroupsCount(), first_me.ConsumerGroupsCount());
EXPECT_EQ(second_me.ConsumerGroups().size(), first_me.ConsumerGroups().size());
}
TEST_F(E2E_Client, GetMeReturnsDistinctClientIdsForDifferentSessions) {
RecordProperty(
"description",
"Returns different client ids for separate authenticated sessions while keeping the same user identity.");
auto first_client = GetLoggedInHighLevelClient();
auto second_client = GetLoggedInHighLevelClient();
const auto first_me = first_client.GetMe();
const auto second_me = second_client.GetMe();
EXPECT_NE(first_me.ClientId(), 0u);
EXPECT_NE(second_me.ClientId(), 0u);
EXPECT_TRUE(first_me.UserId().has_value());
EXPECT_TRUE(second_me.UserId().has_value());
EXPECT_NE(second_me.ClientId(), first_me.ClientId());
EXPECT_EQ(second_me.UserId(), first_me.UserId());
EXPECT_EQ(first_me.Transport(), "TCP");
EXPECT_EQ(second_me.Transport(), "TCP");
}
TEST_F(E2E_Client, GetMeReturnsValidDetailsAfterReconnect) {
RecordProperty("description",
"Returns valid current-client details after reconnecting with a fresh authenticated session.");
const auto first_me = [this] {
auto first_client = GetLoggedInHighLevelClient();
return first_client.GetMe();
}();
EXPECT_NE(first_me.ClientId(), 0u);
EXPECT_TRUE(first_me.UserId().has_value());
auto second_client = GetLoggedInHighLevelClient();
const auto second_me = second_client.GetMe();
EXPECT_NE(second_me.ClientId(), 0u);
EXPECT_TRUE(second_me.UserId().has_value());
EXPECT_EQ(second_me.UserId(), first_me.UserId());
EXPECT_EQ(first_me.Transport(), "TCP");
EXPECT_EQ(second_me.Transport(), "TCP");
EXPECT_EQ(second_me.Transport(), first_me.Transport());
EXPECT_FALSE(second_me.Address().empty());
EXPECT_EQ(second_me.ConsumerGroupsCount(), 0u);
EXPECT_TRUE(second_me.ConsumerGroups().empty());
}
TEST_F(E2E_Client, GetClientBeforeLoginThrows) {
RecordProperty("description",
"Rejects GetClient before connect, after connect but before login, and after disconnect.");
auto client = GetLoggedOutHighLevelClient();
ASSERT_THROW(client.GetClient(1), iggy::IggyException);
ASSERT_NO_THROW(client.Connect());
ASSERT_THROW(client.GetClient(1), iggy::IggyException);
ASSERT_NO_THROW(client.Login("iggy", "iggy"));
ASSERT_NO_THROW(client.Disconnect());
ASSERT_THROW(client.GetClient(1), iggy::IggyException);
}
TEST_F(E2E_Client, GetClientWithWrongClientIdThrows) {
RecordProperty("description", "Rejects querying invalid or non-existent client ids.");
auto client = GetLoggedInHighLevelClient();
std::uint32_t non_existent_client_id = 1u;
const auto clients = client.GetClients();
std::unordered_set<std::uint32_t> client_ids;
for (const auto &entry : clients) {
client_ids.insert(entry.ClientId());
}
while (client_ids.find(non_existent_client_id) != client_ids.end()) {
++non_existent_client_id;
}
const std::uint32_t wrong_client_ids[] = {0u, non_existent_client_id};
for (const std::uint32_t wrong_client_id : wrong_client_ids) {
SCOPED_TRACE(wrong_client_id);
ASSERT_THROW(client.GetClient(wrong_client_id), iggy::IggyException);
}
}
TEST_F(E2E_Client, GetClientReturnsDetailsForMatchingClientId) {
RecordProperty("description", "Returns current client details when querying with the authenticated client id.");
auto client = GetLoggedInHighLevelClient();
const auto current_client = client.GetMe();
const auto looked_up_client = client.GetClient(current_client.ClientId());
EXPECT_NE(current_client.ClientId(), 0u);
EXPECT_TRUE(current_client.UserId().has_value());
EXPECT_TRUE(looked_up_client.UserId().has_value());
EXPECT_EQ(looked_up_client.ClientId(), current_client.ClientId());
EXPECT_EQ(looked_up_client.UserId(), current_client.UserId());
EXPECT_EQ(looked_up_client.Address(), current_client.Address());
EXPECT_EQ(looked_up_client.Transport(), "TCP");
EXPECT_EQ(looked_up_client.Transport(), current_client.Transport());
EXPECT_EQ(looked_up_client.ConsumerGroupsCount(), current_client.ConsumerGroupsCount());
EXPECT_EQ(looked_up_client.ConsumerGroups().size(), current_client.ConsumerGroups().size());
}
TEST_F(E2E_Client, GetClientIsStableAcrossBackToBackCalls) {
RecordProperty("description", "Returns stable client details across back-to-back GetClient calls.");
auto client = GetLoggedInHighLevelClient();
const auto current_client = client.GetMe();
const auto first_lookup = client.GetClient(current_client.ClientId());
const auto second_lookup = client.GetClient(current_client.ClientId());
EXPECT_NE(current_client.ClientId(), 0u);
EXPECT_TRUE(current_client.UserId().has_value());
EXPECT_TRUE(first_lookup.UserId().has_value());
EXPECT_TRUE(second_lookup.UserId().has_value());
EXPECT_EQ(first_lookup.ClientId(), current_client.ClientId());
EXPECT_EQ(second_lookup.ClientId(), first_lookup.ClientId());
EXPECT_EQ(second_lookup.UserId(), first_lookup.UserId());
EXPECT_EQ(second_lookup.Address(), first_lookup.Address());
EXPECT_EQ(first_lookup.Transport(), "TCP");
EXPECT_EQ(second_lookup.Transport(), "TCP");
EXPECT_EQ(second_lookup.Transport(), first_lookup.Transport());
EXPECT_EQ(second_lookup.ConsumerGroupsCount(), first_lookup.ConsumerGroupsCount());
EXPECT_EQ(second_lookup.ConsumerGroups().size(), first_lookup.ConsumerGroups().size());
}
TEST_F(E2E_Client, GetClientsBeforeLoginThrows) {
RecordProperty("description",
"Rejects GetClients before connect, after connect but before login, and after disconnect.");
auto client = GetLoggedOutHighLevelClient();
ASSERT_THROW(client.GetClients(), iggy::IggyException);
ASSERT_NO_THROW(client.Connect());
ASSERT_THROW(client.GetClients(), iggy::IggyException);
ASSERT_NO_THROW(client.Login("iggy", "iggy"));
ASSERT_NO_THROW(client.Disconnect());
ASSERT_THROW(client.GetClients(), iggy::IggyException);
}
TEST_F(E2E_Client, GetClientsReturnsActiveClientSessions) {
RecordProperty("description", "Returns the currently active authenticated client sessions.");
auto first_client = GetLoggedInHighLevelClient();
auto second_client = GetLoggedInHighLevelClient();
const auto first_me = first_client.GetMe();
const auto second_me = second_client.GetMe();
const auto clients = first_client.GetClients();
ASSERT_GE(clients.size(), 2u);
bool found_first = false;
bool found_second = false;
for (const auto &client : clients) {
EXPECT_NE(client.ClientId(), 0u);
EXPECT_EQ(client.Transport(), "TCP");
if (client.ClientId() == first_me.ClientId()) {
found_first = true;
EXPECT_EQ(client.UserId(), first_me.UserId());
EXPECT_EQ(client.Address(), first_me.Address());
EXPECT_EQ(client.ConsumerGroupsCount(), first_me.ConsumerGroupsCount());
}
if (client.ClientId() == second_me.ClientId()) {
found_second = true;
EXPECT_EQ(client.UserId(), second_me.UserId());
EXPECT_EQ(client.Address(), second_me.Address());
EXPECT_EQ(client.ConsumerGroupsCount(), second_me.ConsumerGroupsCount());
}
}
EXPECT_TRUE(found_first);
EXPECT_TRUE(found_second);
}
TEST_F(E2E_Client, GetClientsIsStableAcrossBackToBackCalls) {
RecordProperty("description", "Returns stable client lists across back-to-back GetClients calls.");
auto first_client = GetLoggedInHighLevelClient();
auto second_client = GetLoggedInHighLevelClient();
const auto first_me = first_client.GetMe();
const auto second_me = second_client.GetMe();
const auto first_clients = first_client.GetClients();
const auto second_clients = first_client.GetClients();
ASSERT_GE(first_clients.size(), 2u);
ASSERT_GE(second_clients.size(), 2u);
const auto expect_entry_matches = [](const std::vector<iggy::ClientInfo> &clients,
const iggy::ClientInfoDetails &expected) {
bool found = false;
for (const auto &entry : clients) {
if (entry.ClientId() != expected.ClientId()) {
continue;
}
found = true;
EXPECT_EQ(entry.UserId(), expected.UserId());
EXPECT_EQ(entry.Address(), expected.Address());
EXPECT_EQ(entry.Transport(), expected.Transport());
EXPECT_EQ(entry.ConsumerGroupsCount(), expected.ConsumerGroupsCount());
break;
}
EXPECT_TRUE(found);
};
expect_entry_matches(first_clients, first_me);
expect_entry_matches(first_clients, second_me);
expect_entry_matches(second_clients, first_me);
expect_entry_matches(second_clients, second_me);
}
TEST_F(E2E_Client, GetClientsMatchesGetClientForReturnedIds) {
RecordProperty("description", "Returns list entries that agree with GetClient for each returned client id.");
auto first_client = GetLoggedInHighLevelClient();
auto second_client = GetLoggedInHighLevelClient();
const auto clients = first_client.GetClients();
ASSERT_GE(clients.size(), 2u);
for (const auto &client : clients) {
SCOPED_TRACE(client.ClientId());
const auto details = first_client.GetClient(client.ClientId());
EXPECT_EQ(details.ClientId(), client.ClientId());
EXPECT_EQ(details.UserId(), client.UserId());
EXPECT_EQ(details.Address(), client.Address());
EXPECT_EQ(details.Transport(), client.Transport());
EXPECT_EQ(details.ConsumerGroupsCount(), client.ConsumerGroupsCount());
}
}
TEST_F(E2E_Client, GetClientsReflectsAdditionalSession) {
RecordProperty("description", "Reflects a newly added authenticated session in subsequent GetClients results.");
auto first_client = GetLoggedInHighLevelClient();
const auto clients_before = first_client.GetClients();
auto second_client = GetLoggedInHighLevelClient();
const auto second_me = second_client.GetMe();
const auto clients_after = first_client.GetClients();
bool found_before = false;
for (const auto &client : clients_before) {
if (client.ClientId() == second_me.ClientId()) {
found_before = true;
break;
}
}
EXPECT_FALSE(found_before);
bool found_after = false;
for (const auto &client : clients_after) {
if (client.ClientId() != second_me.ClientId()) {
continue;
}
found_after = true;
EXPECT_EQ(client.UserId(), second_me.UserId());
EXPECT_EQ(client.Address(), second_me.Address());
EXPECT_EQ(client.Transport(), "TCP");
EXPECT_EQ(client.ConsumerGroupsCount(), second_me.ConsumerGroupsCount());
break;
}
EXPECT_TRUE(found_after);
}
TEST_F(LowLevelE2E_Client, GetClusterMetadataBeforeLoginThrows) {
RecordProperty("description",
"Rejects get_cluster_metadata before connect, after connect but before login, and after disconnect, "
"and serves it once authenticated.");
iggy::ffi::Client *client = GetLoggedOutClient();
ASSERT_THROW(client->get_cluster_metadata(), std::exception);
ASSERT_NO_THROW(client->connect());
// The roster is private, so the read is auth-gated. No pre-login read is
// needed: a client that dialed a backup logs in there and the server
// forwards the register to the primary.
ASSERT_THROW(client->get_cluster_metadata(), std::exception);
ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
iggy::ffi::ClusterMetadata metadata{};
ASSERT_NO_THROW({ metadata = client->get_cluster_metadata(); });
ASSERT_GE(metadata.nodes.size(), 1u);
ASSERT_NO_THROW(client->disconnect());
ASSERT_THROW(client->get_cluster_metadata(), std::exception);
}
TEST_F(LowLevelE2E_Client, GetClusterMetadataReturnsSingleNodeMetadata) {
RecordProperty("description",
"Returns the expected single-node cluster metadata shape from the default test server.");
iggy::ffi::Client *client = GetLoggedInClient();
iggy::ffi::ClusterMetadata metadata{};
ASSERT_NO_THROW({ metadata = client->get_cluster_metadata(); });
EXPECT_EQ(static_cast<std::string>(metadata.name), "single-node");
ASSERT_EQ(metadata.nodes.size(), 1u);
const auto &node = metadata.nodes[0];
EXPECT_FALSE(static_cast<std::string>(node.name).empty());
EXPECT_FALSE(static_cast<std::string>(node.ip).empty());
EXPECT_EQ(static_cast<std::string>(node.role), "leader");
EXPECT_EQ(static_cast<std::string>(node.status), "healthy");
EXPECT_NE(node.endpoints.tcp, 0u);
EXPECT_NE(node.endpoints.http, 0u);
}
TEST_F(LowLevelE2E_Client, GetClusterMetadataIsStableAcrossBackToBackCalls) {
RecordProperty("description", "Returns stable single-node cluster metadata across back-to-back calls.");
iggy::ffi::Client *client = GetLoggedInClient();
iggy::ffi::ClusterMetadata first_metadata{};
iggy::ffi::ClusterMetadata second_metadata{};
ASSERT_NO_THROW({
first_metadata = client->get_cluster_metadata();
second_metadata = client->get_cluster_metadata();
});
EXPECT_EQ(static_cast<std::string>(first_metadata.name), static_cast<std::string>(second_metadata.name));
ASSERT_EQ(first_metadata.nodes.size(), 1u);
ASSERT_EQ(second_metadata.nodes.size(), 1u);
const auto &first_node = first_metadata.nodes[0];
const auto &second_node = second_metadata.nodes[0];
EXPECT_EQ(static_cast<std::string>(first_node.name), static_cast<std::string>(second_node.name));
EXPECT_EQ(static_cast<std::string>(first_node.ip), static_cast<std::string>(second_node.ip));
EXPECT_EQ(static_cast<std::string>(first_node.role), static_cast<std::string>(second_node.role));
EXPECT_EQ(static_cast<std::string>(first_node.status), static_cast<std::string>(second_node.status));
EXPECT_EQ(first_node.endpoints.tcp, second_node.endpoints.tcp);
EXPECT_EQ(first_node.endpoints.quic, second_node.endpoints.quic);
EXPECT_EQ(first_node.endpoints.http, second_node.endpoints.http);
EXPECT_EQ(first_node.endpoints.websocket, second_node.endpoints.websocket);
}
TEST_F(LowLevelE2E_Client, PingSucceedsForNewConnection) {
RecordProperty("description", "Successfully pings the server from a fresh unauthenticated client session.");
iggy::ffi::Client *client = GetLoggedOutClient();
// The VSR client has no lazy connect; ping still needs no authentication.
ASSERT_NO_THROW(client->connect());
ASSERT_NO_THROW(client->ping());
}
TEST_F(LowLevelE2E_Client, HeartbeatIntervalReturnsDefaultValueForNewConnection) {
RecordProperty("description",
"Returns the default heartbeat interval in microseconds for a fresh unauthenticated client.");
constexpr std::uint64_t default_heartbeat_micros = 5'000'000ull;
iggy::ffi::Client *client = GetLoggedOutClient();
const auto heartbeat_interval = client->heartbeat_interval();
EXPECT_EQ(heartbeat_interval, default_heartbeat_micros);
}
TEST_F(LowLevelE2E_Client, HeartbeatIntervalReturnsConfiguredValueFromConnectionString) {
RecordProperty("description",
"Returns the configured heartbeat interval in microseconds from the connection string.");
constexpr std::uint64_t configured_heartbeat_micros = 10'000'000ull;
iggy::ffi::Client *client = nullptr;
ASSERT_NO_THROW(
{ client = iggy::ffi::from_connection_string("iggy://iggy:iggy@127.0.0.1:8090?heartbeat_interval=10s"); });
ASSERT_NE(client, nullptr);
TrackClient(client);
const auto heartbeat_interval = client->heartbeat_interval();
EXPECT_EQ(heartbeat_interval, configured_heartbeat_micros);
}
TEST_F(LowLevelE2E_Client, SnapshotBeforeLoginThrows) {
RecordProperty("description",
"Rejects snapshot before connect, after connect but before login, and after disconnect.");
iggy::ffi::Client *client = GetLoggedOutClient();
ASSERT_THROW(client->snapshot("deflated", make_snapshot_types({"test"})), std::exception);
ASSERT_NO_THROW(client->connect());
ASSERT_THROW(client->snapshot("deflated", make_snapshot_types({"test"})), std::exception);
ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
ASSERT_NO_THROW(client->disconnect());
ASSERT_THROW(client->snapshot("deflated", make_snapshot_types({"test"})), std::exception);
}
TEST_F(LowLevelE2E_Client, SnapshotAllCombinedWithOtherTypeThrows) {
RecordProperty("description", "Rejects combining the all snapshot type with any other snapshot type.");
iggy::ffi::Client *client = GetLoggedInClient();
ASSERT_THROW(client->snapshot("deflated", make_snapshot_types({"all", "test"})), std::exception);
}
TEST_F(LowLevelE2E_Client, SnapshotWithEmptySnapshotTypesThrows) {
RecordProperty("description", "Rejects an empty snapshot type list in the wrapper before sending.");
iggy::ffi::Client *client = GetLoggedInClient();
rust::Vec<rust::String> snapshot_types;
ASSERT_THROW(client->snapshot("deflated", snapshot_types), std::exception);
}
TEST_F(LowLevelE2E_Client, SnapshotReturnsNonEmptyBytes) {
RecordProperty("description", "Returns a non-empty snapshot for a valid compression and snapshot type.");
iggy::ffi::Client *client = GetLoggedInClient();
rust::Vec<std::uint8_t> snapshot_bytes;
ASSERT_NO_THROW({ snapshot_bytes = client->snapshot("deflated", make_snapshot_types({"test"})); });
EXPECT_FALSE(snapshot_bytes.empty());
}
TEST_F(LowLevelE2E_Client, SnapshotWithInvalidCompressionThrows) {
RecordProperty("description",
"Rejects empty or invalid snapshot compression values in the wrapper before sending.");
iggy::ffi::Client *client = GetLoggedInClient();
ASSERT_THROW(client->snapshot("", make_snapshot_types({"test"})), std::exception);
ASSERT_THROW(client->snapshot("invalid-compression", make_snapshot_types({"test"})), std::exception);
}
TEST_F(LowLevelE2E_Client, SnapshotWithInvalidSnapshotTypeThrows) {
RecordProperty("description", "Rejects invalid snapshot type values in the wrapper before sending.");
iggy::ffi::Client *client = GetLoggedInClient();
ASSERT_THROW(client->snapshot("deflated", make_snapshot_types({"not-a-real-type"})), std::exception);
}
TEST_F(LowLevelE2E_Client, SendBinaryRequestPingReturnsEmptyBytes) {
RecordProperty("description", "Returns an empty response body for a raw ping command with an empty payload.");
constexpr std::uint32_t ping_command_code = 1;
iggy::ffi::Client *client = GetLoggedInClient();
rust::Vec<std::uint8_t> empty_payload;
rust::Vec<std::uint8_t> response;
ASSERT_NO_THROW({ response = client->send_binary_request(ping_command_code, empty_payload); });
EXPECT_TRUE(response.empty());
}
TEST_F(LowLevelE2E_Client, SendBinaryRequestGetStatsReturnsNonEmptyBytes) {
RecordProperty("description",
"Returns a non-empty response body for a raw get-stats command with an empty payload.");
constexpr std::uint32_t get_stats_command_code = 10;
iggy::ffi::Client *client = GetLoggedInClient();
rust::Vec<std::uint8_t> empty_payload;
rust::Vec<std::uint8_t> response;
ASSERT_NO_THROW({ response = client->send_binary_request(get_stats_command_code, empty_payload); });
EXPECT_FALSE(response.empty());
}
TEST_F(LowLevelE2E_Client, SendBinaryRequestLoginUserCodeThrows) {
RecordProperty("description",
"Rejects the login-user session-control code client-side before it reaches the server.");
constexpr std::uint32_t login_user_command_code = 38;
iggy::ffi::Client *client = GetLoggedInClient();
rust::Vec<std::uint8_t> empty_payload;
ASSERT_THROW(client->send_binary_request(login_user_command_code, empty_payload), std::exception);
}
TEST_F(LowLevelE2E_Client, SendBinaryRequestUnknownCommandCodeThrows) {
RecordProperty("description", "Rejects an unknown command code with an invalid-command error from the server.");
constexpr std::uint32_t unknown_command_code = 60000;
iggy::ffi::Client *client = GetLoggedInClient();
rust::Vec<std::uint8_t> empty_payload;
ASSERT_THROW(client->send_binary_request(unknown_command_code, empty_payload), std::exception);
}