blob: 6bab34830151d8ca2d9efb9354c69ca215a41bab [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.
*/
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
#include <gtest/gtest.h>
#include "iggy.hpp"
#include "lib.rs.h"
#include "tests/e2e/test_helpers.hpp"
class E2E_Topic : public E2ETestFixture {};
TEST_F(E2E_Topic, CreateTopicWithAllOptionCombinations) {
RecordProperty("description",
"Creates topics across supported option combinations and verifies they are all returned.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.CreateStream(stream_name));
TrackStream(stream_name);
struct CompressionOption {
std::string name;
iggy::CompressionAlgorithm value;
};
const std::vector<CompressionOption> compression_algorithms = {
{"none", iggy::CompressionAlgorithm::None()},
{"gzip", iggy::CompressionAlgorithm::Gzip()},
};
struct ExpiryOption {
std::string name;
iggy::Expiry value;
};
const std::vector<ExpiryOption> expiry_options = {
{"server_default", iggy::Expiry::ServerDefault()},
{"never_expire", iggy::Expiry::NeverExpire()},
{"duration", iggy::Expiry::Duration(1000)},
};
struct MaxTopicSizeOption {
std::string name;
iggy::MaxTopicSize value;
};
const std::vector<MaxTopicSizeOption> max_topic_sizes = {
{"server_default", iggy::MaxTopicSize::ServerDefault()},
{"unlimited", iggy::MaxTopicSize::Unlimited()},
{"1GiB", iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)},
};
std::size_t expected_topics_count = 0;
std::unordered_set<std::string> expected_topic_names;
for (const auto &compression_algorithm : compression_algorithms) {
for (const auto &expiry_option : expiry_options) {
for (const auto &max_topic_size : max_topic_sizes) {
const std::string topic_name = GetRandomName();
SCOPED_TRACE("compression=" + compression_algorithm.name + ", expiry_kind=" + expiry_option.name +
", max_topic_size=" + max_topic_size.name);
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, 1,
compression_algorithm.value, expiry_option.value,
max_topic_size.value));
++expected_topics_count;
expected_topic_names.insert(topic_name);
}
}
}
ASSERT_NO_THROW({
const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name));
EXPECT_EQ(stream_details.Name(), stream_name);
EXPECT_EQ(stream_details.TopicsCount(), expected_topics_count);
ASSERT_EQ(stream_details.Topics().size(), expected_topics_count);
for (const auto &topic : stream_details.Topics()) {
const auto erased = expected_topic_names.erase(topic.Name());
EXPECT_EQ(erased, 1u) << "Unexpected topic name returned: " << topic.Name();
}
EXPECT_TRUE(expected_topic_names.empty());
});
}
TEST_F(E2E_Topic, CreateTopicWithBoundaryPartitionsCountValues) {
RecordProperty("description", "Accepts boundary partition counts and rejects values above the supported maximum.");
const std::string stream_name = GetRandomName();
const std::string zero_partitions_topic_name = GetRandomName();
const std::string max_partitions_topic_name = GetRandomName();
const std::string overflow_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), zero_partitions_topic_name, 0));
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), max_partitions_topic_name, 1000));
ASSERT_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), overflow_topic_name, 1001), std::exception);
const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name));
EXPECT_EQ(stream_details.TopicsCount(), 2u);
std::unordered_map<std::string, std::uint32_t> topic_partitions;
for (const auto &topic : stream_details.Topics()) {
topic_partitions[topic.Name()] = topic.PartitionsCount();
}
EXPECT_EQ(topic_partitions.size(), 2u);
EXPECT_EQ(topic_partitions[zero_partitions_topic_name], 0u);
EXPECT_EQ(topic_partitions[max_partitions_topic_name], 1000u);
}
TEST_F(E2E_Topic, CreateTopicWithInvalidNamesThrows) {
RecordProperty("description", "Rejects invalid topic names and accepts the maximum allowed name length.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.CreateStream(stream_name));
TrackStream(stream_name);
const std::string illegal_topic_names[] = {
"",
std::string(256, 'b'),
};
for (const auto &topic_name : illegal_topic_names) {
SCOPED_TRACE(topic_name);
ASSERT_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, 1), std::exception);
}
const std::string max_length_name(255, 'a');
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), max_length_name, 1));
}
TEST_F(E2E_Topic, CreateDuplicateTopicThrows) {
RecordProperty("description", "Rejects creating a duplicate topic within the same stream.");
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, 1));
ASSERT_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, 1), std::exception);
}
TEST_F(E2E_Topic, CreateSameTopicNameInDifferentStreamsSucceeds) {
RecordProperty("description", "Allows the same topic name to be created in different streams.");
const std::string first_stream_name = GetRandomName();
const std::string second_stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
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), topic_name, 1));
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(second_stream_name), topic_name, 1));
}
TEST_F(E2E_Topic, CreateTopicWithInvalidOptionsThrows) {
RecordProperty("description", "Rejects topic creation requests that use invalid option values.");
const std::string stream_name = GetRandomName();
const std::string invalid_compression_topic_name = GetRandomName();
const std::string invalid_expiry_topic_name = GetRandomName();
const std::string invalid_max_size_topic_name = GetRandomName();
iggy::ffi::Client *client = GetLoggedInClient();
ASSERT_NO_THROW(client->create_stream(stream_name));
TrackStream(stream_name);
ASSERT_THROW(client->create_topic(make_string_identifier(stream_name), invalid_compression_topic_name, 1,
"invalid-compression", "server_default", 0, "server_default", {}),
std::exception);
ASSERT_THROW(client->create_topic(make_string_identifier(stream_name), invalid_expiry_topic_name, 1, "none",
"invalid-expiry-kind", 0, "server_default", {}),
std::exception);
ASSERT_THROW(client->create_topic(make_string_identifier(stream_name), invalid_max_size_topic_name, 1, "none",
"server_default", 0, "not-a-size", {}),
std::exception);
}
TEST_F(E2E_Topic, CreateTopicWithOptionsReturnsCanonicalKindAndDerivedRemainder) {
RecordProperty("description",
"Returns an explicitly set option in its canonical kind, derives the keys left unset, and rejects "
"an option key outside the server catalog.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
const std::string unknown_option_topic = GetRandomName();
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.CreateStream(stream_name));
TrackStream(stream_name);
const auto options = iggy::ResourceOptions::Explicit({iggy::HeaderEntry::Create(
iggy::HeaderField::Create(iggy::HeaderKind::String,
{'e', 'n', 'f', 'o', 'r', 'c', 'e', '_', 'f', 's', 'y', 'n', 'c'}),
iggy::HeaderField::Create(iggy::HeaderKind::String, {'t', 'r', 'u', 'e'}))});
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, 1,
iggy::CompressionAlgorithm::None(), iggy::Expiry::ServerDefault(),
iggy::MaxTopicSize::ServerDefault(), options));
const auto topic_details =
client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name));
// Admission re-encodes the block from its own parse, so a value comes back
// in its key's catalog kind rather than in the kind that was sent.
const auto &explicit_options = topic_details.Options().Explicit();
ASSERT_EQ(explicit_options.size(), 1u);
EXPECT_EQ(explicit_options.front().Key().Kind(), iggy::HeaderKind::String);
EXPECT_EQ(explicit_options.front().Key().Value(),
(std::vector<std::uint8_t>{'e', 'n', 'f', 'o', 'r', 'c', 'e', '_', 'f', 's', 'y', 'n', 'c'}));
EXPECT_EQ(explicit_options.front().Value().Kind(), iggy::HeaderKind::Bool);
EXPECT_EQ(explicit_options.front().Value().Value(), (std::vector<std::uint8_t>{1}));
EXPECT_FALSE(topic_details.Options().Derived().empty());
std::unordered_set<std::string> derived_option_keys;
for (const auto &derived_option : topic_details.Options().Derived()) {
const auto &key = derived_option.Key().Value();
derived_option_keys.emplace(key.begin(), key.end());
}
EXPECT_EQ(derived_option_keys.count("max_topic_size"), 1u);
EXPECT_EQ(derived_option_keys.count("enforce_fsync"), 0u);
const auto unknown_options = iggy::ResourceOptions::Explicit({iggy::HeaderEntry::Create(
iggy::HeaderField::Create(iggy::HeaderKind::String, {'n', 'o', 't', '_', 'a', '_', 'r', 'e', 'a', 'l', '_', 'o',
'p', 't', 'i', 'o', 'n'}),
iggy::HeaderField::Create(iggy::HeaderKind::String, {'t', 'r', 'u', 'e'}))});
ASSERT_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), unknown_option_topic, 1,
iggy::CompressionAlgorithm::None(), iggy::Expiry::ServerDefault(),
iggy::MaxTopicSize::ServerDefault(), unknown_options),
std::exception);
}
TEST_F(E2E_Topic, CreateTopicWithTypedOptionHelpersReportsThemAsExplicitOptions) {
RecordProperty("description",
"Creates a topic with every typed option helper and verifies each key comes back as an explicit "
"option in its catalog kind.");
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);
constexpr std::uint64_t segment_size_bytes = 8ULL * 1024ULL * 1024ULL;
constexpr std::uint32_t messages_required_to_save = 512;
constexpr std::uint64_t size_of_messages_required_to_save = 2ULL * 1024ULL * 1024ULL;
const auto options = iggy::ResourceOptions::Explicit({
iggy::TopicOption::SegmentSize(segment_size_bytes),
iggy::TopicOption::EnforceFsync(true),
iggy::TopicOption::MessagesRequiredToSave(messages_required_to_save),
iggy::TopicOption::SizeOfMessagesRequiredToSave(size_of_messages_required_to_save),
iggy::TopicOption::PreallocateSegments(false),
});
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, 1,
iggy::CompressionAlgorithm::None(), iggy::Expiry::ServerDefault(),
iggy::MaxTopicSize::ServerDefault(), options));
const auto topic_details =
client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name));
auto expected_options = options.Explicit();
auto explicit_options = topic_details.Options().Explicit();
const auto compare_option_keys = [](const auto &left, const auto &right) {
return left.Key().Value() < right.Key().Value();
};
std::sort(expected_options.begin(), expected_options.end(), compare_option_keys);
std::sort(explicit_options.begin(), explicit_options.end(), compare_option_keys);
ASSERT_EQ(explicit_options.size(), expected_options.size());
for (std::size_t index = 0; index < explicit_options.size(); ++index) {
EXPECT_EQ(explicit_options[index].Key().Kind(), expected_options[index].Key().Kind());
EXPECT_EQ(explicit_options[index].Key().Value(), expected_options[index].Key().Value());
EXPECT_EQ(explicit_options[index].Value().Kind(), expected_options[index].Value().Kind());
EXPECT_EQ(explicit_options[index].Value().Value(), expected_options[index].Value().Value());
}
for (const auto &derived_option : topic_details.Options().Derived()) {
const auto &key = derived_option.Key().Value();
const std::string derived_key(key.begin(), key.end());
EXPECT_NE(derived_key, "segment_size") << "segment_size was set explicitly, so it cannot be derived";
}
}
TEST_F(E2E_Topic, DescribeOptionsServesTopicCatalogAndRejectsUnknownScope) {
RecordProperty("description",
"Serves the topic option catalog with each key's kind, default and description, returns an empty "
"catalog for the stream scope, and rejects an unknown scope name.");
iggy::ffi::Client *client = GetLoggedInClient();
rust::Vec<iggy::ffi::OptionSpec> topic_options;
ASSERT_NO_THROW({ topic_options = client->describe_options("topic"); });
const iggy::ffi::OptionSpec *segment_size = nullptr;
bool found_enforce_fsync = false;
for (const auto &option : topic_options) {
const std::string key = static_cast<std::string>(option.key);
if (key == "segment_size") {
segment_size = &option;
} else if (key == "enforce_fsync") {
found_enforce_fsync = true;
}
}
ASSERT_NE(segment_size, nullptr) << "Topic catalog is missing segment_size";
EXPECT_TRUE(found_enforce_fsync) << "Topic catalog is missing enforce_fsync";
EXPECT_EQ(segment_size->kind, static_cast<std::uint8_t>(iggy::ffi::HeaderKind::Uint64));
EXPECT_FALSE(segment_size->default_value.empty());
EXPECT_FALSE(segment_size->description.empty());
// Streams take no option keys yet, which is an empty catalog rather than a failure.
ASSERT_NO_THROW({
const auto stream_options = client->describe_options("stream");
EXPECT_TRUE(stream_options.empty());
});
ASSERT_THROW(client->describe_options("not_a_scope"), std::exception);
}
TEST_F(E2E_Topic, CreateTopicWithMaxTopicSizeBelowSegmentSizeThrows) {
RecordProperty("description",
"Rejects topic creation when the maximum topic size is smaller than the segment size.");
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_THROW(
client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, 1, iggy::CompressionAlgorithm::None(),
iggy::Expiry::ServerDefault(), iggy::MaxTopicSize::FromBytes(1024)),
std::exception);
}
TEST_F(E2E_Topic, CreateTopicOnNonExistentStreamThrows) {
RecordProperty("description", "Throws when creating a topic on a stream that does not exist.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
ASSERT_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, 1), std::exception);
}
TEST_F(E2E_Topic, CreateTopicAfterStreamDeletionThrows) {
RecordProperty("description", "Throws when creating a topic after its 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));
TrackStream(stream_name);
ASSERT_NO_THROW(client.DeleteStream(iggy::Identifier::String(stream_name)));
ForgetTrackedStream(stream_name);
ASSERT_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, 1), std::exception);
}
TEST_F(E2E_Topic, CreateTopicWithInvalidStreamIdentifierThrows) {
RecordProperty("description", "Rejects topic creation requests that use invalid stream identifier formats.");
const std::string stream_name = GetRandomName();
const std::string first_topic_name = GetRandomName();
const std::string second_topic_name = GetRandomName();
iggy::ffi::Client *client = GetLoggedInClient();
ASSERT_NO_THROW(client->create_stream(stream_name));
TrackStream(stream_name);
iggy::ffi::Identifier invalid_kind_id;
invalid_kind_id.kind = "invalid";
invalid_kind_id.length = 4;
invalid_kind_id.value = {1, 0, 0, 0};
ASSERT_THROW(client->create_topic(std::move(invalid_kind_id), first_topic_name, 1, "none", "server_default", 0,
"server_default", {}),
std::exception);
iggy::ffi::Identifier invalid_numeric_id;
invalid_numeric_id.kind = "numeric";
invalid_numeric_id.length = 1;
invalid_numeric_id.value.push_back(1);
ASSERT_THROW(client->create_topic(std::move(invalid_numeric_id), second_topic_name, 1, "none", "server_default", 0,
"server_default", {}),
std::exception);
}
TEST_F(E2E_Topic, CreateTopicBeforeLoginThrows) {
RecordProperty("description", "Throws when topic creation is attempted from an unauthenticated client.");
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);
auto unauthenticated_client = GetLoggedOutHighLevelClient();
ASSERT_THROW(unauthenticated_client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, 1),
std::exception);
ASSERT_NO_THROW(unauthenticated_client.Connect());
ASSERT_THROW(unauthenticated_client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, 1),
std::exception);
ASSERT_NO_THROW(unauthenticated_client.Login("iggy", "iggy"));
ASSERT_NO_THROW(unauthenticated_client.Disconnect());
ASSERT_THROW(unauthenticated_client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, 1),
std::exception);
}
TEST_F(E2E_Topic, DeleteTopicAfterCreate) {
RecordProperty("description", "Deletes an existing topic after creating it.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
iggy::ffi::Client *client = GetLoggedInClient();
ASSERT_NO_THROW(client->create_stream(stream_name));
TrackStream(stream_name);
ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default",
0, "server_default", {}));
ASSERT_NO_THROW(client->delete_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)));
ASSERT_NO_THROW({
const auto topics = client->get_topics(make_string_identifier(stream_name));
EXPECT_TRUE(topics.empty());
});
}
TEST_F(E2E_Topic, DeleteTopicOnNonExistentStreamThrows) {
RecordProperty("description", "Throws when deleting a topic from a stream that does not exist.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
iggy::ffi::Client *client = GetLoggedInClient();
ASSERT_THROW(client->delete_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)),
std::exception);
}
TEST_F(E2E_Topic, DeleteTopicOnNonExistentTopicThrows) {
RecordProperty("description", "Throws when deleting a topic that does not exist.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
iggy::ffi::Client *client = GetLoggedInClient();
ASSERT_NO_THROW(client->create_stream(stream_name));
TrackStream(stream_name);
ASSERT_THROW(client->delete_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)),
std::exception);
}
TEST_F(E2E_Topic, DeleteTopicTwiceThrows) {
RecordProperty("description", "Throws when deleting the same topic a second time.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
iggy::ffi::Client *client = GetLoggedInClient();
ASSERT_NO_THROW(client->create_stream(stream_name));
TrackStream(stream_name);
ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default",
0, "server_default", {}));
ASSERT_NO_THROW(client->delete_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)));
ASSERT_THROW(client->delete_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)),
std::exception);
}
TEST_F(E2E_Topic, DeleteTopicAfterStreamDeletionThrows) {
RecordProperty("description", "Throws when deleting a topic after its stream has been deleted.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
iggy::ffi::Client *client = GetLoggedInClient();
ASSERT_NO_THROW(client->create_stream(stream_name));
TrackStream(stream_name);
ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default",
0, "server_default", {}));
ASSERT_NO_THROW(client->delete_stream(make_string_identifier(stream_name)));
ForgetTrackedStream(stream_name);
ASSERT_THROW(client->delete_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)),
std::exception);
}
TEST_F(E2E_Topic, DeleteTopicBeforeLoginThrows) {
RecordProperty("description", "Rejects delete_topic before connect, and after connect but before login.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
iggy::ffi::Client *client = GetLoggedInClient();
ASSERT_NO_THROW(client->create_stream(stream_name));
TrackStream(stream_name);
ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default",
0, "server_default", {}));
iggy::ffi::Client *unauthenticated_client = GetLoggedOutClient();
ASSERT_THROW(
unauthenticated_client->delete_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)),
std::exception);
ASSERT_NO_THROW(unauthenticated_client->connect());
ASSERT_THROW(
unauthenticated_client->delete_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)),
std::exception);
ASSERT_NO_THROW(unauthenticated_client->login_user("iggy", "iggy"));
ASSERT_NO_THROW(unauthenticated_client->disconnect());
ASSERT_THROW(
unauthenticated_client->delete_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)),
std::exception);
}
TEST_F(E2E_Topic, DeleteTopicWithInvalidStreamIdentifierThrows) {
RecordProperty("description", "Rejects topic deletion requests that use invalid stream identifier formats.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
iggy::ffi::Client *client = GetLoggedInClient();
ASSERT_NO_THROW(client->create_stream(stream_name));
TrackStream(stream_name);
ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default",
0, "server_default", {}));
iggy::ffi::Identifier invalid_kind_id;
invalid_kind_id.kind = "invalid";
invalid_kind_id.length = 4;
invalid_kind_id.value = {1, 0, 0, 0};
ASSERT_THROW(client->delete_topic(std::move(invalid_kind_id), make_string_identifier(topic_name)), std::exception);
iggy::ffi::Identifier invalid_numeric_id;
invalid_numeric_id.kind = "numeric";
invalid_numeric_id.length = 1;
invalid_numeric_id.value.push_back(1);
ASSERT_THROW(client->delete_topic(std::move(invalid_numeric_id), make_string_identifier(topic_name)),
std::exception);
}
TEST_F(E2E_Topic, DeleteTopicWithInvalidTopicIdentifierThrows) {
RecordProperty("description", "Rejects topic deletion requests that use invalid topic identifier formats.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
iggy::ffi::Client *client = GetLoggedInClient();
ASSERT_NO_THROW(client->create_stream(stream_name));
TrackStream(stream_name);
ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default",
0, "server_default", {}));
iggy::ffi::Identifier invalid_kind_id;
invalid_kind_id.kind = "invalid";
invalid_kind_id.length = 4;
invalid_kind_id.value = {1, 0, 0, 0};
ASSERT_THROW(client->delete_topic(make_string_identifier(stream_name), std::move(invalid_kind_id)), std::exception);
iggy::ffi::Identifier invalid_numeric_id;
invalid_numeric_id.kind = "numeric";
invalid_numeric_id.length = 1;
invalid_numeric_id.value.push_back(1);
ASSERT_THROW(client->delete_topic(make_string_identifier(stream_name), std::move(invalid_numeric_id)),
std::exception);
}
TEST_F(E2E_Topic, GetTopicReturnsTopicForExistingTopic) {
RecordProperty("description", "Returns topic details for an existing topic.");
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, 3,
iggy::CompressionAlgorithm::Gzip(), iggy::Expiry::Duration(1000),
iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)));
ASSERT_NO_THROW({
const auto topic_details =
client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name));
EXPECT_EQ(topic_details.Name(), topic_name);
EXPECT_EQ(topic_details.PartitionsCount(), 3u);
EXPECT_EQ(topic_details.Partitions().size(), 3u);
EXPECT_EQ(topic_details.CompressionAlgorithm(), "gzip");
EXPECT_EQ(topic_details.MessageExpiry(), 1000u);
EXPECT_EQ(topic_details.MaxTopicSize(), 1024ULL * 1024ULL * 1024ULL);
});
}
TEST_F(E2E_Topic, GetTopicBeforeLoginThrows) {
RecordProperty("description", "Rejects get_topic before connect, and after connect but before login.");
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, 1));
auto unauthenticated_client = GetLoggedOutHighLevelClient();
ASSERT_THROW(
unauthenticated_client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)),
std::exception);
ASSERT_NO_THROW(unauthenticated_client.Connect());
ASSERT_THROW(
unauthenticated_client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)),
std::exception);
ASSERT_NO_THROW(unauthenticated_client.Login("iggy", "iggy"));
ASSERT_NO_THROW(unauthenticated_client.Disconnect());
ASSERT_THROW(
unauthenticated_client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)),
std::exception);
}
TEST_F(E2E_Topic, GetTopicWithWrongStreamIdThrows) {
RecordProperty("description", "Rejects get_topic when the topic belongs to a different stream.");
const std::string first_stream_name = GetRandomName();
const std::string second_stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
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), topic_name, 1));
ASSERT_THROW(client.GetTopic(iggy::Identifier::String(second_stream_name), iggy::Identifier::String(topic_name)),
std::exception);
}
TEST_F(E2E_Topic, GetTopicWithWrongTopicThrows) {
RecordProperty("description", "Rejects get_topic when the topic does not exist in the stream.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
const std::string wrong_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, 1));
ASSERT_THROW(client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(wrong_topic_name)),
std::exception);
}
TEST_F(E2E_Topic, GetTopicAfterStreamDeletionThrows) {
RecordProperty("description", "Rejects get_topic 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));
TrackStream(stream_name);
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, 1));
ASSERT_NO_THROW(client.DeleteStream(iggy::Identifier::String(stream_name)));
ForgetTrackedStream(stream_name);
ASSERT_THROW(client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)),
std::exception);
}
TEST_F(E2E_Topic, GetTopicAfterTopicDeletionThrows) {
RecordProperty("description", "Rejects get_topic 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);
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, 1));
ASSERT_NO_THROW(client.DeleteTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)));
ASSERT_THROW(client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)),
std::exception);
}
TEST_F(E2E_Topic, GetTopicReturnsEmptyPartitionsForZeroPartitionTopic) {
RecordProperty("description", "Returns an empty partitions vector for a topic created with zero partitions.");
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, 0));
ASSERT_NO_THROW({
const auto topic_details =
client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name));
EXPECT_EQ(topic_details.Name(), topic_name);
EXPECT_EQ(topic_details.PartitionsCount(), 0u);
EXPECT_TRUE(topic_details.Partitions().empty());
});
}
TEST_F(E2E_Topic, GetTopicReturnsMaxBoundaryPartitionCount) {
RecordProperty("description", "Returns the maximum boundary partition count for a topic created with it.");
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, 1000));
ASSERT_NO_THROW({
const auto topic_details =
client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name));
EXPECT_EQ(topic_details.Name(), topic_name);
EXPECT_EQ(topic_details.PartitionsCount(), 1000u);
EXPECT_EQ(topic_details.Partitions().size(), 1000u);
});
}
TEST_F(E2E_Topic, GetTopicIsStableAcrossBackToBackCalls) {
RecordProperty("description", "Returns stable topic details across back-to-back get_topic 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);
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, 3,
iggy::CompressionAlgorithm::Gzip(), iggy::Expiry::Duration(1000),
iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)));
ASSERT_NO_THROW({
const auto first_topic =
client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name));
const auto second_topic =
client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name));
EXPECT_EQ(second_topic.Name(), first_topic.Name());
EXPECT_EQ(second_topic.MessageExpiry(), first_topic.MessageExpiry());
EXPECT_EQ(second_topic.CompressionAlgorithm(), first_topic.CompressionAlgorithm());
EXPECT_EQ(second_topic.MaxTopicSize(), first_topic.MaxTopicSize());
EXPECT_EQ(second_topic.PartitionsCount(), first_topic.PartitionsCount());
EXPECT_EQ(second_topic.Partitions().size(), first_topic.Partitions().size());
});
}
TEST_F(E2E_Topic, GetTopicAgreesWithGetStreamTopicSummary) {
RecordProperty("description", "Returns topic details that agree with get_stream topic summary fields.");
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, 3,
iggy::CompressionAlgorithm::Gzip(), iggy::Expiry::Duration(1000),
iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)));
ASSERT_NO_THROW({
const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name));
ASSERT_EQ(stream_details.Topics().size(), 1u);
const auto &topic_summary = stream_details.Topics().front();
const auto topic_details =
client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name));
EXPECT_EQ(topic_details.Name(), topic_summary.Name());
EXPECT_EQ(topic_details.MessageExpiry(), topic_summary.MessageExpiry());
EXPECT_EQ(topic_details.CompressionAlgorithm(), topic_summary.CompressionAlgorithm());
EXPECT_EQ(topic_details.MaxTopicSize(), topic_summary.MaxTopicSize());
EXPECT_EQ(topic_details.PartitionsCount(), topic_summary.PartitionsCount());
EXPECT_EQ(topic_details.Partitions().size(), topic_summary.PartitionsCount());
});
}
TEST_F(E2E_Topic, GetTopicsReturnsCreatedTopicInputFields) {
RecordProperty("description", "Creates topics in a stream, gets them, and verifies user-provided topic fields.");
const std::string stream_name = GetRandomName();
const std::string first_topic_name = GetRandomName();
const std::string second_topic_name = GetRandomName();
struct ExpectedTopic {
std::uint32_t partitions_count;
std::string compression_algorithm;
std::uint64_t message_expiry;
std::uint64_t max_topic_size;
};
const std::unordered_map<std::string, ExpectedTopic> expected_topics = {
{first_topic_name, {2, "gzip", 1000, 1024ULL * 1024ULL * 1024ULL}},
{second_topic_name,
{0, "none", std::numeric_limits<std::uint64_t>::max(), std::numeric_limits<std::uint64_t>::max()}},
};
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.CreateStream(stream_name));
TrackStream(stream_name);
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), first_topic_name, 2,
iggy::CompressionAlgorithm::Gzip(), iggy::Expiry::Duration(1000),
iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)));
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), second_topic_name, 0,
iggy::CompressionAlgorithm::None(), iggy::Expiry::NeverExpire(),
iggy::MaxTopicSize::Unlimited()));
ASSERT_NO_THROW({
const auto topics = client.GetTopics(iggy::Identifier::String(stream_name));
ASSERT_EQ(topics.size(), expected_topics.size());
EXPECT_EQ(topics[0].Name(), first_topic_name);
EXPECT_EQ(topics[1].Name(), second_topic_name);
std::unordered_set<std::string> found_topic_names;
for (const auto &topic : topics) {
const auto expected = expected_topics.find(topic.Name());
ASSERT_NE(expected, expected_topics.end()) << "Unexpected topic name returned: " << topic.Name();
EXPECT_EQ(topic.Name(), expected->first);
EXPECT_EQ(topic.PartitionsCount(), expected->second.partitions_count);
EXPECT_EQ(topic.CompressionAlgorithm(), expected->second.compression_algorithm);
EXPECT_EQ(topic.MessageExpiry(), expected->second.message_expiry);
EXPECT_EQ(topic.MaxTopicSize(), expected->second.max_topic_size);
found_topic_names.insert(topic.Name());
}
EXPECT_EQ(found_topic_names.size(), expected_topics.size());
});
}
TEST_F(E2E_Topic, GetTopicsBeforeLoginThrows) {
RecordProperty("description", "Rejects get_topics before connect, and after connect but before login.");
const std::string stream_name = GetRandomName();
auto setup_client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(setup_client.CreateStream(stream_name));
TrackStream(stream_name);
auto unauthenticated_client = GetLoggedOutHighLevelClient();
ASSERT_THROW(unauthenticated_client.GetTopics(iggy::Identifier::String(stream_name)), std::exception);
ASSERT_NO_THROW(unauthenticated_client.Connect());
ASSERT_THROW(unauthenticated_client.GetTopics(iggy::Identifier::String(stream_name)), std::exception);
ASSERT_NO_THROW(unauthenticated_client.Login("iggy", "iggy"));
ASSERT_NO_THROW(unauthenticated_client.Disconnect());
ASSERT_THROW(unauthenticated_client.GetTopics(iggy::Identifier::String(stream_name)), std::exception);
}
TEST_F(E2E_Topic, GetTopicsReturnsEmptyForStreamWithoutTopics) {
RecordProperty("description", "Returns an empty topic list for an existing stream that has no topics.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.CreateStream(stream_name));
TrackStream(stream_name);
ASSERT_NO_THROW({
const auto topics = client.GetTopics(iggy::Identifier::String(stream_name));
EXPECT_TRUE(topics.empty());
});
}
TEST_F(E2E_Topic, GetTopicsAfterTopicDeletionReturnsRemainingTopics) {
RecordProperty("description", "Returns only non-deleted topics after a topic is deleted from the stream.");
const std::string stream_name = GetRandomName();
const std::string deleted_topic = GetRandomName();
const std::string remaining_topic = GetRandomName();
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.CreateStream(stream_name));
TrackStream(stream_name);
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), deleted_topic, 1));
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), remaining_topic, 1));
ASSERT_NO_THROW(client.DeleteTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(deleted_topic)));
ASSERT_NO_THROW({
const auto topics = client.GetTopics(iggy::Identifier::String(stream_name));
ASSERT_EQ(topics.size(), 1u);
EXPECT_EQ(topics.front().Name(), remaining_topic);
});
}
TEST_F(E2E_Topic, GetTopicsAfterTopicUpdateReturnsUpdatedInputFields) {
RecordProperty("description", "Returns updated user-provided topic fields after a topic update.");
const std::string stream_name = GetRandomName();
const std::string original_topic = GetRandomName();
const std::string updated_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), original_topic, 2));
ASSERT_NO_THROW(client.UpdateTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(original_topic),
updated_topic_name, iggy::CompressionAlgorithm::Gzip(),
iggy::Expiry::Duration(1000),
iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)));
ASSERT_NO_THROW({
const auto topics = client.GetTopics(iggy::Identifier::String(stream_name));
ASSERT_EQ(topics.size(), 1u);
EXPECT_EQ(topics.front().Name(), updated_topic_name);
EXPECT_EQ(topics.front().PartitionsCount(), 2u);
EXPECT_EQ(topics.front().CompressionAlgorithm(), "gzip");
EXPECT_EQ(topics.front().MessageExpiry(), 1000u);
EXPECT_EQ(topics.front().MaxTopicSize(), 1024ULL * 1024ULL * 1024ULL);
});
}
TEST_F(E2E_Topic, UpdateTopicWorksCorrectly) {
RecordProperty("description", "Returns a topic summary that matches topic details after updating a topic.");
const std::string stream_name = GetRandomName();
const std::string original_topic = GetRandomName();
const std::string updated_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), original_topic, 2));
ASSERT_NO_THROW(client.UpdateTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(original_topic),
updated_topic_name, iggy::CompressionAlgorithm::Gzip(),
iggy::Expiry::Duration(1000),
iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)));
ASSERT_NO_THROW({
const auto topic_details =
client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(updated_topic_name));
const auto topics = client.GetTopics(iggy::Identifier::String(stream_name));
ASSERT_EQ(topics.size(), 1u);
const auto &topic_summary = topics.front();
EXPECT_EQ(topic_summary.Id(), topic_details.Id());
EXPECT_EQ(topic_summary.CreatedAt(), topic_details.CreatedAt());
EXPECT_EQ(topic_summary.Name(), topic_details.Name());
EXPECT_EQ(topic_summary.SizeBytes(), topic_details.SizeBytes());
EXPECT_EQ(topic_summary.MessageExpiry(), topic_details.MessageExpiry());
EXPECT_EQ(topic_summary.CompressionAlgorithm(), topic_details.CompressionAlgorithm());
EXPECT_EQ(topic_summary.MaxTopicSize(), topic_details.MaxTopicSize());
EXPECT_EQ(topic_summary.MessagesCount(), topic_details.MessagesCount());
EXPECT_EQ(topic_summary.PartitionsCount(), topic_details.PartitionsCount());
});
}
TEST_F(E2E_Topic, UpdateTopicDoesNotChangePartitionsCount) {
RecordProperty("description", "Preserves the topic partition count after updating topic metadata.");
const std::string stream_name = GetRandomName();
const std::string original_topic = GetRandomName();
const std::string updated_topic_name = GetRandomName();
constexpr std::uint32_t partitions_count = 3;
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.CreateStream(stream_name));
TrackStream(stream_name);
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), original_topic, partitions_count));
ASSERT_NO_THROW(client.UpdateTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(original_topic),
updated_topic_name, iggy::CompressionAlgorithm::Gzip(),
iggy::Expiry::Duration(1000),
iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)));
ASSERT_NO_THROW({
const auto topic_details =
client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(updated_topic_name));
EXPECT_EQ(topic_details.PartitionsCount(), partitions_count);
EXPECT_EQ(topic_details.Partitions().size(), partitions_count);
});
}
TEST_F(E2E_Topic, UpdateTopicDoesNotChangeMessages) {
RecordProperty("description", "Keeps existing messages readable after updating topic metadata.");
const std::string stream_name = GetRandomName();
const std::string original_topic = GetRandomName();
const std::string updated_topic_name = GetRandomName();
iggy::ffi::Client *message_client = GetLoggedInClient();
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.CreateStream(stream_name));
TrackStream(stream_name);
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), original_topic, 1));
const auto created_stream = client.GetStream(iggy::Identifier::String(stream_name));
ASSERT_EQ(created_stream.Topics().size(), 1u);
const auto topic_id = created_stream.Topics().front().Id();
rust::Vec<iggy::ffi::IggyMessageToSend> messages;
messages.push_back(
iggy::ffi::make_message(to_payload("message-before-topic-update"), rust::Vec<iggy::ffi::HeaderEntry>()));
ASSERT_NO_THROW(message_client->send_messages(make_numeric_identifier(created_stream.Id()),
make_numeric_identifier(topic_id), "partition_id",
partition_id_bytes(0), std::move(messages)));
ASSERT_NO_THROW(client.UpdateTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(original_topic),
updated_topic_name, iggy::CompressionAlgorithm::Gzip(),
iggy::Expiry::Duration(1000),
iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)));
ASSERT_NO_THROW({
const auto polled = message_client->poll_messages(make_numeric_identifier(created_stream.Id()),
make_string_identifier(updated_topic_name), 0, "consumer",
make_numeric_identifier(1), "offset", 0, 10, false);
ASSERT_EQ(polled.count, 1u);
ASSERT_EQ(polled.messages.size(), 1u);
const std::string actual(polled.messages[0].payload.begin(), polled.messages[0].payload.end());
EXPECT_EQ(actual, "message-before-topic-update");
});
}
TEST_F(E2E_Topic, UpdateTopicWithAllOptionCombinationsUpdatesInputFields) {
RecordProperty("description",
"Updates a topic across supported option combinations and verifies deterministic updated fields.");
const std::string stream_name = GetRandomName();
std::string topic_name = GetRandomName();
struct CompressionOption {
std::string name;
iggy::CompressionAlgorithm value;
};
const std::vector<CompressionOption> compression_algorithms = {
{"none", iggy::CompressionAlgorithm::None()},
{"gzip", iggy::CompressionAlgorithm::Gzip()},
};
struct ExpiryOption {
std::string name;
iggy::Expiry value;
};
const std::vector<ExpiryOption> expiry_options = {
{"server_default", iggy::Expiry::ServerDefault()},
{"never_expire", iggy::Expiry::NeverExpire()},
{"duration", iggy::Expiry::Duration(1000)},
};
struct MaxTopicSizeOption {
std::string name;
iggy::MaxTopicSize value;
};
const std::vector<MaxTopicSizeOption> max_topic_sizes = {
{"server_default", iggy::MaxTopicSize::ServerDefault()},
{"unlimited", iggy::MaxTopicSize::Unlimited()},
{"1GiB", iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)},
};
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, 2));
for (const auto &compression_algorithm : compression_algorithms) {
for (const auto &expiry_option : expiry_options) {
for (const auto &max_topic_size : max_topic_sizes) {
const std::string updated_topic_name = GetRandomName();
SCOPED_TRACE("compression=" + compression_algorithm.name + ", expiry_kind=" + expiry_option.name +
", max_topic_size=" + max_topic_size.name);
ASSERT_NO_THROW(client.UpdateTopic(
iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name), updated_topic_name,
compression_algorithm.value, expiry_option.value, max_topic_size.value));
topic_name = updated_topic_name;
}
}
}
}
TEST_F(E2E_Topic, UpdateTopicWithSameOptionsIsIdempotent) {
RecordProperty("description", "Calling update_topic twice with the same options returns the same topic details.");
const std::string stream_name = GetRandomName();
const std::string original_topic = GetRandomName();
const std::string updated_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), original_topic, 2));
const auto created_topic =
client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(original_topic));
ASSERT_NO_THROW(client.UpdateTopic(iggy::Identifier::String(stream_name),
iggy::Identifier::Numeric(created_topic.Id()), updated_topic_name,
iggy::CompressionAlgorithm::Gzip(), iggy::Expiry::Duration(1000),
iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)));
const auto first_update =
client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::Numeric(created_topic.Id()));
ASSERT_NO_THROW(client.UpdateTopic(iggy::Identifier::String(stream_name),
iggy::Identifier::Numeric(created_topic.Id()), updated_topic_name,
iggy::CompressionAlgorithm::Gzip(), iggy::Expiry::Duration(1000),
iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)));
const auto second_update =
client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::Numeric(created_topic.Id()));
EXPECT_EQ(second_update.Id(), first_update.Id());
EXPECT_EQ(second_update.Name(), first_update.Name());
EXPECT_EQ(second_update.PartitionsCount(), first_update.PartitionsCount());
EXPECT_EQ(second_update.CompressionAlgorithm(), first_update.CompressionAlgorithm());
EXPECT_EQ(second_update.MessageExpiry(), first_update.MessageExpiry());
EXPECT_EQ(second_update.MaxTopicSize(), first_update.MaxTopicSize());
}
TEST_F(E2E_Topic, UpdateTopicWithDuplicateTopicNameThrows) {
RecordProperty("description", "Rejects renaming a topic to another topic's existing name.");
const std::string stream_name = GetRandomName();
const std::string first_topic_name = GetRandomName();
const std::string second_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), first_topic_name, 1));
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), second_topic_name, 1));
ASSERT_THROW(client.UpdateTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(first_topic_name),
second_topic_name, iggy::CompressionAlgorithm::Gzip(), iggy::Expiry::Duration(1000),
iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)),
std::exception);
}
TEST_F(E2E_Topic, UpdateTopicWithInvalidNamesThrows) {
RecordProperty("description", "Rejects invalid topic names when updating a topic.");
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, 1));
const std::vector<std::string> invalid_topic_names = {
"",
std::string(256, 'b'),
};
for (const auto &invalid_topic_name : invalid_topic_names) {
SCOPED_TRACE("invalid_topic_name_length=" + std::to_string(invalid_topic_name.size()));
ASSERT_THROW(
client.UpdateTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name),
invalid_topic_name, iggy::CompressionAlgorithm::Gzip(), iggy::Expiry::Duration(1000),
iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)),
std::exception);
}
}
TEST_F(E2E_Topic, UpdateTopicFailedValidationDoesNotMutateTopic) {
RecordProperty("description", "Keeps the topic unchanged when update_topic fails wrapper validation.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
const std::string updated_topic_name = GetRandomName();
iggy::ffi::Client *client = GetLoggedInClient();
ASSERT_NO_THROW(client->create_stream(stream_name));
TrackStream(stream_name);
ASSERT_NO_THROW(
client->create_topic(make_string_identifier(stream_name), topic_name, 2, "gzip", "duration", 1000, "1GiB", {}));
const auto topic_before_update =
client->get_topic(make_string_identifier(stream_name), make_string_identifier(topic_name));
ASSERT_THROW(client->update_topic(make_string_identifier(stream_name), make_string_identifier(topic_name),
updated_topic_name, "none", "duration", 2000, "not-a-size", {}),
std::exception);
const auto topic_after_failed_update =
client->get_topic(make_string_identifier(stream_name), make_string_identifier(topic_name));
EXPECT_EQ(topic_after_failed_update.id, topic_before_update.id);
EXPECT_EQ(topic_after_failed_update.name, topic_before_update.name);
EXPECT_EQ(topic_after_failed_update.partitions_count, topic_before_update.partitions_count);
EXPECT_EQ(topic_after_failed_update.compression_algorithm, topic_before_update.compression_algorithm);
EXPECT_EQ(topic_after_failed_update.message_expiry, topic_before_update.message_expiry);
EXPECT_EQ(topic_after_failed_update.max_topic_size, topic_before_update.max_topic_size);
EXPECT_THROW(client->get_topic(make_string_identifier(stream_name), make_string_identifier(updated_topic_name)),
std::exception);
}
TEST_F(E2E_Topic, UpdateTopicBeforeLoginThrows) {
RecordProperty("description", "Rejects update_topic before connect, and after connect but before login.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
const std::string updated_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, 1));
auto unauthenticated_client = GetLoggedOutHighLevelClient();
ASSERT_THROW(unauthenticated_client.UpdateTopic(iggy::Identifier::String(stream_name),
iggy::Identifier::String(topic_name), updated_topic_name),
std::exception);
ASSERT_NO_THROW(unauthenticated_client.Connect());
ASSERT_THROW(unauthenticated_client.UpdateTopic(iggy::Identifier::String(stream_name),
iggy::Identifier::String(topic_name), updated_topic_name),
std::exception);
ASSERT_NO_THROW(unauthenticated_client.Login("iggy", "iggy"));
ASSERT_NO_THROW(unauthenticated_client.Disconnect());
ASSERT_THROW(unauthenticated_client.UpdateTopic(iggy::Identifier::String(stream_name),
iggy::Identifier::String(topic_name), updated_topic_name),
std::exception);
}
TEST_F(E2E_Topic, UpdateTopicOnNonExistentStreamThrows) {
RecordProperty("description", "Throws when updating a topic on a stream that does not exist.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
const std::string updated_topic_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
ASSERT_THROW(
client.UpdateTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name),
updated_topic_name, iggy::CompressionAlgorithm::Gzip(), iggy::Expiry::Duration(1000),
iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)),
std::exception);
}
TEST_F(E2E_Topic, UpdateTopicOnNonExistentTopicThrows) {
RecordProperty("description", "Throws when updating a topic that does not exist.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
const std::string updated_topic_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.CreateStream(stream_name));
TrackStream(stream_name);
ASSERT_THROW(
client.UpdateTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name),
updated_topic_name, iggy::CompressionAlgorithm::Gzip(), iggy::Expiry::Duration(1000),
iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)),
std::exception);
}
TEST_F(E2E_Topic, GetTopicsAfterStreamDeletionReturnsEmpty) {
RecordProperty("description", "Returns an empty topic list after deleting the stream that owned the topic.");
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, 1));
ASSERT_NO_THROW(client.DeleteStream(iggy::Identifier::String(stream_name)));
ForgetTrackedStream(stream_name);
ASSERT_NO_THROW({
const auto topics = client.GetTopics(iggy::Identifier::String(stream_name));
EXPECT_TRUE(topics.empty());
});
}
TEST_F(E2E_Topic, PurgeTopicOnNonExistentStreamThrows) {
RecordProperty("description", "Throws when purging a topic on a stream that does not exist.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
ASSERT_THROW(client.PurgeTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)),
std::exception);
}
TEST_F(E2E_Topic, PurgeTopicAfterStreamDeletionThrows) {
RecordProperty("description", "Throws when purging a topic after its 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));
TrackStream(stream_name);
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, 1));
ASSERT_NO_THROW(client.DeleteStream(iggy::Identifier::String(stream_name)));
ForgetTrackedStream(stream_name);
ASSERT_THROW(client.PurgeTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)),
std::exception);
}
TEST_F(E2E_Topic, PurgeTopicOnNonExistentTopicThrows) {
RecordProperty("description", "Throws when purging 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_THROW(client.PurgeTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)),
std::exception);
}
TEST_F(E2E_Topic, PurgeTopicWithInvalidStreamIdentifierThrows) {
RecordProperty("description", "Rejects topic purge requests that use invalid stream identifier formats.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
iggy::ffi::Client *client = GetLoggedInClient();
ASSERT_NO_THROW(client->create_stream(stream_name));
TrackStream(stream_name);
ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default",
0, "server_default", {}));
iggy::ffi::Identifier invalid_kind_id;
invalid_kind_id.kind = "invalid";
invalid_kind_id.length = 4;
invalid_kind_id.value = {1, 0, 0, 0};
ASSERT_THROW(client->purge_topic(std::move(invalid_kind_id), make_string_identifier(topic_name)), std::exception);
iggy::ffi::Identifier invalid_numeric_id;
invalid_numeric_id.kind = "numeric";
invalid_numeric_id.length = 1;
invalid_numeric_id.value.push_back(1);
ASSERT_THROW(client->purge_topic(std::move(invalid_numeric_id), make_string_identifier(topic_name)),
std::exception);
}
TEST_F(E2E_Topic, PurgeTopicWithInvalidTopicIdentifierThrows) {
RecordProperty("description", "Rejects topic purge requests that use invalid topic identifier formats.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
iggy::ffi::Client *client = GetLoggedInClient();
ASSERT_NO_THROW(client->create_stream(stream_name));
TrackStream(stream_name);
ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default",
0, "server_default", {}));
iggy::ffi::Identifier invalid_kind_id;
invalid_kind_id.kind = "invalid";
invalid_kind_id.length = 4;
invalid_kind_id.value = {1, 0, 0, 0};
ASSERT_THROW(client->purge_topic(make_string_identifier(stream_name), std::move(invalid_kind_id)), std::exception);
iggy::ffi::Identifier invalid_numeric_id;
invalid_numeric_id.kind = "numeric";
invalid_numeric_id.length = 1;
invalid_numeric_id.value.push_back(1);
ASSERT_THROW(client->purge_topic(make_string_identifier(stream_name), std::move(invalid_numeric_id)),
std::exception);
}
TEST_F(E2E_Topic, PurgeTopicPreservesTopicMetadata) {
RecordProperty("description", "Preserves topic metadata after purging its messages.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
iggy::ffi::Client *message_client = GetLoggedInClient();
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, 3,
iggy::CompressionAlgorithm::Gzip(), iggy::Expiry::Duration(1000),
iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)));
auto stream_before_purge = client.GetStream(iggy::Identifier::String(stream_name));
ASSERT_EQ(stream_before_purge.Topics().size(), 1u);
rust::Vec<iggy::ffi::IggyMessageToSend> messages;
messages.push_back(
iggy::ffi::make_message(to_payload("preserve-topic-metadata"), rust::Vec<iggy::ffi::HeaderEntry>()));
ASSERT_NO_THROW(message_client->send_messages(make_string_identifier(stream_name),
make_string_identifier(topic_name), "partition_id",
partition_id_bytes(1), std::move(messages)));
stream_before_purge = client.GetStream(iggy::Identifier::String(stream_name));
ASSERT_EQ(stream_before_purge.Topics().size(), 1u);
const auto &topic_with_messages = stream_before_purge.Topics().front();
EXPECT_GT(topic_with_messages.MessagesCount(), 0u);
EXPECT_GT(topic_with_messages.SizeBytes(), 0u);
ASSERT_NO_THROW(client.PurgeTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)));
const auto stream_after_purge = client.GetStream(iggy::Identifier::String(stream_name));
ASSERT_EQ(stream_after_purge.Topics().size(), 1u);
const auto &topic_after_purge = stream_after_purge.Topics().front();
EXPECT_EQ(topic_after_purge.Id(), topic_with_messages.Id());
EXPECT_EQ(topic_after_purge.CreatedAt(), topic_with_messages.CreatedAt());
EXPECT_EQ(topic_after_purge.Name(), topic_with_messages.Name());
EXPECT_EQ(topic_after_purge.MessageExpiry(), topic_with_messages.MessageExpiry());
EXPECT_EQ(topic_after_purge.CompressionAlgorithm(), topic_with_messages.CompressionAlgorithm());
EXPECT_EQ(topic_after_purge.MaxTopicSize(), topic_with_messages.MaxTopicSize());
EXPECT_EQ(topic_after_purge.PartitionsCount(), topic_with_messages.PartitionsCount());
}
TEST_F(E2E_Topic, PurgeTopicRemovesOnlyTargetTopicMessages) {
RecordProperty("description", "Purges one topic's messages without affecting the other topics in the stream.");
const std::string stream_name = GetRandomName();
const std::string first_topic_name = GetRandomName();
const std::string second_topic_name = GetRandomName();
iggy::ffi::Client *message_client = GetLoggedInClient();
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.CreateStream(stream_name));
TrackStream(stream_name);
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), first_topic_name, 1));
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), second_topic_name, 1));
rust::Vec<iggy::ffi::IggyMessageToSend> first_topic_messages;
for (std::uint32_t i = 0; i < 3; ++i) {
first_topic_messages.push_back(iggy::ffi::make_message(to_payload("purge-topic-first-" + std::to_string(i)),
rust::Vec<iggy::ffi::HeaderEntry>()));
}
ASSERT_NO_THROW(message_client->send_messages(make_string_identifier(stream_name),
make_string_identifier(first_topic_name), "partition_id",
partition_id_bytes(0), std::move(first_topic_messages)));
rust::Vec<iggy::ffi::IggyMessageToSend> second_topic_messages;
for (std::uint32_t i = 0; i < 2; ++i) {
second_topic_messages.push_back(iggy::ffi::make_message(to_payload("purge-topic-second-" + std::to_string(i)),
rust::Vec<iggy::ffi::HeaderEntry>()));
}
ASSERT_NO_THROW(message_client->send_messages(make_string_identifier(stream_name),
make_string_identifier(second_topic_name), "partition_id",
partition_id_bytes(0), std::move(second_topic_messages)));
const auto stream_before_purge = client.GetStream(iggy::Identifier::String(stream_name));
EXPECT_EQ(stream_before_purge.MessagesCount(), 5u);
EXPECT_GT(stream_before_purge.SizeBytes(), 0u);
std::unordered_map<std::string, std::uint64_t> messages_before_purge;
for (const auto &topic : stream_before_purge.Topics()) {
messages_before_purge[topic.Name()] = topic.MessagesCount();
}
EXPECT_EQ(messages_before_purge[first_topic_name], 3u);
EXPECT_EQ(messages_before_purge[second_topic_name], 2u);
ASSERT_NO_THROW(
client.PurgeTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(first_topic_name)));
const auto stream_after_purge = client.GetStream(iggy::Identifier::String(stream_name));
EXPECT_EQ(stream_after_purge.TopicsCount(), 2u);
EXPECT_EQ(stream_after_purge.MessagesCount(), 2u);
EXPECT_GT(stream_after_purge.SizeBytes(), 0u);
EXPECT_LT(stream_after_purge.SizeBytes(), stream_before_purge.SizeBytes());
std::unordered_map<std::string, std::uint64_t> messages_after_purge;
std::unordered_map<std::string, std::uint64_t> sizes_after_purge;
for (const auto &topic : stream_after_purge.Topics()) {
messages_after_purge[topic.Name()] = topic.MessagesCount();
sizes_after_purge[topic.Name()] = topic.SizeBytes();
}
EXPECT_EQ(messages_after_purge[first_topic_name], 0u);
EXPECT_EQ(sizes_after_purge[first_topic_name], 0u);
EXPECT_EQ(messages_after_purge[second_topic_name], 2u);
EXPECT_GT(sizes_after_purge[second_topic_name], 0u);
}
TEST_F(E2E_Topic, PurgeTopicAcrossMultiplePartitionsClearsAllPartitions) {
RecordProperty("description", "Purges all messages from every partition in the topic.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
iggy::ffi::Client *message_client = GetLoggedInClient();
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, 3));
for (std::uint32_t partition_id = 0; partition_id < 3; ++partition_id) {
rust::Vec<iggy::ffi::IggyMessageToSend> messages;
for (std::uint32_t i = 0; i < 2; ++i) {
messages.push_back(iggy::ffi::make_message(
to_payload("purge-topic-partition-" + std::to_string(partition_id) + "-" + std::to_string(i)),
rust::Vec<iggy::ffi::HeaderEntry>()));
}
ASSERT_NO_THROW(message_client->send_messages(make_string_identifier(stream_name),
make_string_identifier(topic_name), "partition_id",
partition_id_bytes(partition_id), std::move(messages)));
}
const auto stream_before_purge = client.GetStream(iggy::Identifier::String(stream_name));
EXPECT_EQ(stream_before_purge.MessagesCount(), 6u);
ASSERT_EQ(stream_before_purge.Topics().size(), 1u);
EXPECT_EQ(stream_before_purge.Topics().front().PartitionsCount(), 3u);
EXPECT_EQ(stream_before_purge.Topics().front().MessagesCount(), 6u);
ASSERT_NO_THROW(client.PurgeTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)));
const auto stream_after_purge = client.GetStream(iggy::Identifier::String(stream_name));
EXPECT_EQ(stream_after_purge.MessagesCount(), 0u);
ASSERT_EQ(stream_after_purge.Topics().size(), 1u);
EXPECT_EQ(stream_after_purge.Topics().front().PartitionsCount(), 3u);
EXPECT_EQ(stream_after_purge.Topics().front().MessagesCount(), 0u);
EXPECT_EQ(stream_after_purge.Topics().front().SizeBytes(), 0u);
}
TEST_F(E2E_Topic, PurgeTopicThenSendMessagesAgainSucceeds) {
RecordProperty("description", "Allows sending fresh messages again after purging the topic.");
const std::string stream_name = GetRandomName();
const std::string topic_name = GetRandomName();
iggy::ffi::Client *message_client = GetLoggedInClient();
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, 1));
rust::Vec<iggy::ffi::IggyMessageToSend> first_batch;
first_batch.push_back(
iggy::ffi::make_message(to_payload("before-topic-purge"), rust::Vec<iggy::ffi::HeaderEntry>()));
ASSERT_NO_THROW(message_client->send_messages(make_string_identifier(stream_name),
make_string_identifier(topic_name), "partition_id",
partition_id_bytes(0), std::move(first_batch)));
ASSERT_NO_THROW(client.PurgeTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)));
rust::Vec<iggy::ffi::IggyMessageToSend> second_batch;
second_batch.push_back(
iggy::ffi::make_message(to_payload("after-topic-purge-0"), rust::Vec<iggy::ffi::HeaderEntry>()));
second_batch.push_back(
iggy::ffi::make_message(to_payload("after-topic-purge-1"), rust::Vec<iggy::ffi::HeaderEntry>()));
ASSERT_NO_THROW(message_client->send_messages(make_string_identifier(stream_name),
make_string_identifier(topic_name), "partition_id",
partition_id_bytes(0), std::move(second_batch)));
const auto stream_after_resend = client.GetStream(iggy::Identifier::String(stream_name));
EXPECT_EQ(stream_after_resend.MessagesCount(), 2u);
ASSERT_EQ(stream_after_resend.Topics().size(), 1u);
EXPECT_EQ(stream_after_resend.Topics().front().MessagesCount(), 2u);
EXPECT_GT(stream_after_resend.Topics().front().SizeBytes(), 0u);
}
TEST_F(E2E_Topic, PurgeTopicTwiceKeepsTargetTopicEmptyAndOtherTopicsUntouched) {
RecordProperty("description",
"Allows purging the same topic twice and keeps the target topic empty without affecting siblings.");
const std::string stream_name = GetRandomName();
const std::string first_topic_name = GetRandomName();
const std::string second_topic_name = GetRandomName();
iggy::ffi::Client *message_client = GetLoggedInClient();
auto client = GetLoggedInHighLevelClient();
ASSERT_NO_THROW(client.CreateStream(stream_name));
TrackStream(stream_name);
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), first_topic_name, 1));
ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), second_topic_name, 1));
rust::Vec<iggy::ffi::IggyMessageToSend> first_topic_messages;
for (std::uint32_t i = 0; i < 3; ++i) {
first_topic_messages.push_back(iggy::ffi::make_message(
to_payload("purge-topic-twice-first-" + std::to_string(i)), rust::Vec<iggy::ffi::HeaderEntry>()));
}
ASSERT_NO_THROW(message_client->send_messages(make_string_identifier(stream_name),
make_string_identifier(first_topic_name), "partition_id",
partition_id_bytes(0), std::move(first_topic_messages)));
rust::Vec<iggy::ffi::IggyMessageToSend> second_topic_messages;
for (std::uint32_t i = 0; i < 2; ++i) {
second_topic_messages.push_back(iggy::ffi::make_message(
to_payload("purge-topic-twice-second-" + std::to_string(i)), rust::Vec<iggy::ffi::HeaderEntry>()));
}
ASSERT_NO_THROW(message_client->send_messages(make_string_identifier(stream_name),
make_string_identifier(second_topic_name), "partition_id",
partition_id_bytes(0), std::move(second_topic_messages)));
ASSERT_NO_THROW(
client.PurgeTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(first_topic_name)));
const auto stream_after_first_purge = client.GetStream(iggy::Identifier::String(stream_name));
EXPECT_EQ(stream_after_first_purge.TopicsCount(), 2u);
EXPECT_EQ(stream_after_first_purge.MessagesCount(), 2u);
std::unordered_map<std::string, std::uint64_t> messages_after_first_purge;
std::unordered_map<std::string, std::uint64_t> sizes_after_first_purge;
for (const auto &topic : stream_after_first_purge.Topics()) {
messages_after_first_purge[topic.Name()] = topic.MessagesCount();
sizes_after_first_purge[topic.Name()] = topic.SizeBytes();
}
EXPECT_EQ(messages_after_first_purge[first_topic_name], 0u);
EXPECT_EQ(sizes_after_first_purge[first_topic_name], 0u);
EXPECT_EQ(messages_after_first_purge[second_topic_name], 2u);
EXPECT_GT(sizes_after_first_purge[second_topic_name], 0u);
ASSERT_NO_THROW(
client.PurgeTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(first_topic_name)));
const auto stream_after_second_purge = client.GetStream(iggy::Identifier::String(stream_name));
EXPECT_EQ(stream_after_second_purge.TopicsCount(), 2u);
EXPECT_EQ(stream_after_second_purge.MessagesCount(), 2u);
std::unordered_map<std::string, std::uint64_t> messages_after_second_purge;
std::unordered_map<std::string, std::uint64_t> sizes_after_second_purge;
for (const auto &topic : stream_after_second_purge.Topics()) {
messages_after_second_purge[topic.Name()] = topic.MessagesCount();
sizes_after_second_purge[topic.Name()] = topic.SizeBytes();
}
EXPECT_EQ(messages_after_second_purge[first_topic_name], 0u);
EXPECT_EQ(sizes_after_second_purge[first_topic_name], 0u);
EXPECT_EQ(messages_after_second_purge[second_topic_name], 2u);
EXPECT_GT(sizes_after_second_purge[second_topic_name], 0u);
}
TEST_F(E2E_Topic, PurgeTopicBeforeLoginThrows) {
RecordProperty("description", "Throws when topic purge is attempted before authentication.");
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, 1));
auto unauthenticated_client = GetLoggedOutHighLevelClient();
ASSERT_THROW(
unauthenticated_client.PurgeTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)),
std::exception);
ASSERT_NO_THROW(unauthenticated_client.Connect());
ASSERT_THROW(
unauthenticated_client.PurgeTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)),
std::exception);
ASSERT_NO_THROW(unauthenticated_client.Login("iggy", "iggy"));
ASSERT_NO_THROW(unauthenticated_client.Disconnect());
ASSERT_THROW(
unauthenticated_client.PurgeTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)),
std::exception);
}