blob: 500a1a8089f5b1ce77f681e59aee82f0414417ea [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 <cstdint>
#include <limits>
#include <string>
#include <unordered_set>
#include <utility>
#include <vector>
#include <gtest/gtest.h>
#include "lib.rs.h"
#include "tests/e2e/test_helpers.hpp"
class LowLevelE2E_Topic : public E2ETestFixture {};
TEST_F(LowLevelE2E_Topic, CreateTopicWithAllOptionCombinations) {
RecordProperty("description",
"Creates topics across supported option combinations and verifies they are all returned.");
const std::string stream_name = GetRandomName();
iggy::ffi::Client *client = GetLoggedInClient();
ASSERT_NO_THROW(client->create_stream(stream_name));
TrackStream(stream_name);
const std::vector<std::string> compression_algorithms = {"none", "gzip"};
const std::vector<std::uint8_t> replication_factors = {0, 1, 27};
struct ExpiryOption {
std::string kind;
std::uint64_t value;
};
const std::vector<ExpiryOption> expiry_options = {
{"server_default", 0},
{"never_expire", 0},
{"duration", 1000},
};
const std::vector<std::string> max_topic_sizes = {"server_default", "unlimited", "1GiB"};
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 replication_factor : replication_factors) {
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 + ", replication=" + std::to_string(replication_factor) +
", expiry_kind=" + expiry_option.kind +
", expiry_value=" + std::to_string(expiry_option.value) + ", max_topic_size=" + max_topic_size);
ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1,
compression_algorithm, replication_factor, expiry_option.kind,
expiry_option.value, max_topic_size));
++expected_topics_count;
expected_topic_names.insert(topic_name);
}
}
}
}
ASSERT_NO_THROW({
const auto stream_details = client->get_stream(make_string_identifier(stream_name));
EXPECT_EQ(stream_details.name, stream_name);
EXPECT_EQ(stream_details.topics_count, expected_topics_count);
ASSERT_EQ(stream_details.topics.size(), expected_topics_count);
for (const auto &topic : stream_details.topics) {
const std::string topic_name = static_cast<std::string>(topic.name);
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(LowLevelE2E_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();
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), zero_partitions_topic_name, 0, "none", 0,
"server_default", 0, "server_default"));
ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), max_partitions_topic_name, 1000, "none",
0, "server_default", 0, "server_default"));
ASSERT_THROW(client->create_topic(make_string_identifier(stream_name), overflow_topic_name, 1001, "none", 0,
"server_default", 0, "server_default"),
std::exception);
const auto stream_details = client->get_stream(make_string_identifier(stream_name));
EXPECT_EQ(stream_details.topics_count, 2u);
std::unordered_map<std::string, std::uint32_t> topic_partitions;
for (const auto &topic : stream_details.topics) {
const std::string topic_name = static_cast<std::string>(topic.name);
topic_partitions[topic_name] = topic.partitions_count;
}
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(LowLevelE2E_Topic, CreateTopicWithInvalidNamesThrows) {
RecordProperty("description", "Rejects invalid topic names and accepts the maximum allowed name length.");
const std::string stream_name = GetRandomName();
iggy::ffi::Client *client = GetLoggedInClient();
ASSERT_NO_THROW(client->create_stream(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->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", 0,
"server_default", 0, "server_default"),
std::exception);
}
const std::string max_length_name(255, 'a');
ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), max_length_name, 1, "none", 0,
"server_default", 0, "server_default"));
}
TEST_F(LowLevelE2E_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();
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", 0,
"server_default", 0, "server_default"));
ASSERT_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", 0, "server_default",
0, "server_default"),
std::exception);
}
TEST_F(LowLevelE2E_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();
iggy::ffi::Client *client = GetLoggedInClient();
ASSERT_NO_THROW(client->create_stream(first_stream_name));
TrackStream(first_stream_name);
ASSERT_NO_THROW(client->create_stream(second_stream_name));
TrackStream(second_stream_name);
ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), topic_name, 1, "none", 0,
"server_default", 0, "server_default"));
ASSERT_NO_THROW(client->create_topic(make_string_identifier(second_stream_name), topic_name, 1, "none", 0,
"server_default", 0, "server_default"));
}
TEST_F(LowLevelE2E_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", 0, "server_default", 0, "server_default"),
std::exception);
ASSERT_THROW(client->create_topic(make_string_identifier(stream_name), invalid_expiry_topic_name, 1, "none", 0,
"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", 0,
"server_default", 0, "not-a-size"),
std::exception);
}
TEST_F(LowLevelE2E_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();
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), topic_name, 1, "none", 0, "server_default",
0, "1024"),
std::exception);
}
TEST_F(LowLevelE2E_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();
iggy::ffi::Client *client = GetLoggedInClient();
ASSERT_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", 0, "server_default",
0, "server_default"),
std::exception);
}
TEST_F(LowLevelE2E_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();
iggy::ffi::Client *client = GetLoggedInClient();
ASSERT_NO_THROW(client->create_stream(stream_name));
TrackStream(stream_name);
ASSERT_NO_THROW(client->delete_stream(make_string_identifier(stream_name)));
ForgetTrackedStream(stream_name);
ASSERT_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", 0, "server_default",
0, "server_default"),
std::exception);
}
TEST_F(LowLevelE2E_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", 0, "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", 0, "server_default",
0, "server_default"),
std::exception);
}
TEST_F(LowLevelE2E_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();
iggy::ffi::Client *client = GetLoggedInClient();
ASSERT_NO_THROW(client->create_stream(stream_name));
TrackStream(stream_name);
iggy::ffi::Client *unauthenticated_client = GetLoggedOutClient();
ASSERT_NO_THROW(unauthenticated_client->connect());
ASSERT_THROW(unauthenticated_client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", 0,
"server_default", 0, "server_default"),
std::exception);
ASSERT_NO_THROW(unauthenticated_client->login_user("iggy", "iggy"));
ASSERT_NO_THROW(unauthenticated_client->disconnect());
ASSERT_THROW(unauthenticated_client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", 0,
"server_default", 0, "server_default"),
std::exception);
}
TEST_F(LowLevelE2E_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", 0,
"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(LowLevelE2E_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(LowLevelE2E_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(LowLevelE2E_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", 0,
"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(LowLevelE2E_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", 0,
"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(LowLevelE2E_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", 0,
"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(LowLevelE2E_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", 0,
"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(LowLevelE2E_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", 0,
"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(LowLevelE2E_Topic, GetTopicReturnsTopicForExistingTopic) {
RecordProperty("description", "Returns topic details for an existing topic.");
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, 3, "gzip", 1, "duration", 1000, "1GiB"));
ASSERT_NO_THROW({
const auto topic_details =
client->get_topic(make_string_identifier(stream_name), make_string_identifier(topic_name));
EXPECT_EQ(topic_details.name, topic_name);
EXPECT_EQ(topic_details.partitions_count, 3u);
EXPECT_EQ(topic_details.partitions.size(), 3u);
EXPECT_EQ(topic_details.compression_algorithm, "gzip");
EXPECT_EQ(topic_details.replication_factor, 1u);
EXPECT_EQ(topic_details.message_expiry, 1000u);
EXPECT_EQ(topic_details.max_topic_size, 1024ULL * 1024ULL * 1024ULL);
});
}
TEST_F(LowLevelE2E_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();
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", 0,
"server_default", 0, "server_default"));
iggy::ffi::Client *unauthenticated_client = GetLoggedOutClient();
ASSERT_THROW(
unauthenticated_client->get_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)),
std::exception);
ASSERT_NO_THROW(unauthenticated_client->connect());
ASSERT_THROW(
unauthenticated_client->get_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->get_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)),
std::exception);
}
TEST_F(LowLevelE2E_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();
iggy::ffi::Client *client = GetLoggedInClient();
ASSERT_NO_THROW(client->create_stream(first_stream_name));
TrackStream(first_stream_name);
ASSERT_NO_THROW(client->create_stream(second_stream_name));
TrackStream(second_stream_name);
ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), topic_name, 1, "none", 0,
"server_default", 0, "server_default"));
ASSERT_THROW(client->get_topic(make_string_identifier(second_stream_name), make_string_identifier(topic_name)),
std::exception);
}
TEST_F(LowLevelE2E_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();
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", 0,
"server_default", 0, "server_default"));
ASSERT_THROW(client->get_topic(make_string_identifier(stream_name), make_string_identifier(wrong_topic_name)),
std::exception);
}
TEST_F(LowLevelE2E_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();
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", 0,
"server_default", 0, "server_default"));
ASSERT_NO_THROW(client->delete_stream(make_string_identifier(stream_name)));
ForgetTrackedStream(stream_name);
ASSERT_THROW(client->get_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)),
std::exception);
}
TEST_F(LowLevelE2E_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();
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", 0,
"server_default", 0, "server_default"));
ASSERT_NO_THROW(client->delete_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)));
ASSERT_THROW(client->get_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)),
std::exception);
}
TEST_F(LowLevelE2E_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();
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, 0, "none", 0,
"server_default", 0, "server_default"));
ASSERT_NO_THROW({
const auto topic_details =
client->get_topic(make_string_identifier(stream_name), make_string_identifier(topic_name));
EXPECT_EQ(topic_details.name, topic_name);
EXPECT_EQ(topic_details.partitions_count, 0u);
EXPECT_TRUE(topic_details.partitions.empty());
});
}
TEST_F(LowLevelE2E_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();
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, 1000, "none", 0,
"server_default", 0, "server_default"));
ASSERT_NO_THROW({
const auto topic_details =
client->get_topic(make_string_identifier(stream_name), make_string_identifier(topic_name));
EXPECT_EQ(topic_details.name, topic_name);
EXPECT_EQ(topic_details.partitions_count, 1000u);
EXPECT_EQ(topic_details.partitions.size(), 1000u);
});
}
TEST_F(LowLevelE2E_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();
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, 3, "gzip", 1, "duration", 1000, "1GiB"));
iggy::ffi::TopicDetails first_topic{};
iggy::ffi::TopicDetails second_topic{};
ASSERT_NO_THROW({
first_topic = client->get_topic(make_string_identifier(stream_name), make_string_identifier(topic_name));
second_topic = client->get_topic(make_string_identifier(stream_name), make_string_identifier(topic_name));
});
EXPECT_EQ(static_cast<std::string>(second_topic.name), static_cast<std::string>(first_topic.name));
EXPECT_EQ(second_topic.message_expiry, first_topic.message_expiry);
EXPECT_EQ(static_cast<std::string>(second_topic.compression_algorithm),
static_cast<std::string>(first_topic.compression_algorithm));
EXPECT_EQ(second_topic.max_topic_size, first_topic.max_topic_size);
EXPECT_EQ(second_topic.replication_factor, first_topic.replication_factor);
EXPECT_EQ(second_topic.partitions_count, first_topic.partitions_count);
EXPECT_EQ(second_topic.partitions.size(), first_topic.partitions.size());
}
TEST_F(LowLevelE2E_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();
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, 3, "gzip", 1, "duration", 1000, "1GiB"));
ASSERT_NO_THROW({
const auto stream_details = client->get_stream(make_string_identifier(stream_name));
ASSERT_EQ(stream_details.topics.size(), 1u);
const auto topic_summary = stream_details.topics.front();
const auto topic_details =
client->get_topic(make_string_identifier(stream_name), make_string_identifier(topic_name));
EXPECT_EQ(static_cast<std::string>(topic_details.name), static_cast<std::string>(topic_summary.name));
EXPECT_EQ(topic_details.message_expiry, topic_summary.message_expiry);
EXPECT_EQ(static_cast<std::string>(topic_details.compression_algorithm),
static_cast<std::string>(topic_summary.compression_algorithm));
EXPECT_EQ(topic_details.max_topic_size, topic_summary.max_topic_size);
EXPECT_EQ(topic_details.replication_factor, topic_summary.replication_factor);
EXPECT_EQ(topic_details.partitions_count, topic_summary.partitions_count);
EXPECT_EQ(topic_details.partitions.size(), topic_summary.partitions_count);
});
}
TEST_F(LowLevelE2E_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;
std::uint8_t replication_factor;
};
const std::unordered_map<std::string, ExpectedTopic> expected_topics = {
{first_topic_name, {2, "gzip", 1000, 1024ULL * 1024ULL * 1024ULL, 1}},
{second_topic_name,
{0, "none", std::numeric_limits<std::uint64_t>::max(), std::numeric_limits<std::uint64_t>::max(), 1}},
};
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), first_topic_name, 2, "gzip", 1,
"duration", 1000, "1GiB"));
ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), second_topic_name, 0, "none", 1,
"never_expire", 0, "unlimited"));
ASSERT_NO_THROW({
const auto topics = client->get_topics(make_string_identifier(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 std::string topic_name = static_cast<std::string>(topic.name);
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.partitions_count, expected->second.partitions_count);
EXPECT_EQ(topic.compression_algorithm, expected->second.compression_algorithm);
EXPECT_EQ(topic.message_expiry, expected->second.message_expiry);
EXPECT_EQ(topic.max_topic_size, expected->second.max_topic_size);
EXPECT_EQ(topic.replication_factor, expected->second.replication_factor);
found_topic_names.insert(topic_name);
}
EXPECT_EQ(found_topic_names.size(), expected_topics.size());
});
}
TEST_F(LowLevelE2E_Topic, GetTopicsBeforeLoginThrows) {
RecordProperty("description", "Rejects get_topics before connect, and after connect but before login.");
const std::string stream_name = GetRandomName();
iggy::ffi::Client *setup_client = GetLoggedInClient();
ASSERT_NO_THROW(setup_client->create_stream(stream_name));
TrackStream(stream_name);
iggy::ffi::Client *unauthenticated_client = GetLoggedOutClient();
ASSERT_THROW(unauthenticated_client->get_topics(make_string_identifier(stream_name)), std::exception);
ASSERT_NO_THROW(unauthenticated_client->connect());
ASSERT_THROW(unauthenticated_client->get_topics(make_string_identifier(stream_name)), std::exception);
ASSERT_NO_THROW(unauthenticated_client->login_user("iggy", "iggy"));
ASSERT_NO_THROW(unauthenticated_client->disconnect());
ASSERT_THROW(unauthenticated_client->get_topics(make_string_identifier(stream_name)), std::exception);
}
TEST_F(LowLevelE2E_Topic, GetTopicsReturnsEmptyForStreamWithoutTopics) {
RecordProperty("description", "Returns an empty topic list for an existing stream that has no topics.");
const std::string stream_name = GetRandomName();
iggy::ffi::Client *client = GetLoggedInClient();
ASSERT_NO_THROW(client->create_stream(stream_name));
TrackStream(stream_name);
ASSERT_NO_THROW({
const auto topics = client->get_topics(make_string_identifier(stream_name));
EXPECT_TRUE(topics.empty());
});
}
TEST_F(LowLevelE2E_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();
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), deleted_topic, 1, "none", 0,
"server_default", 0, "server_default"));
ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), remaining_topic, 1, "none", 0,
"server_default", 0, "server_default"));
ASSERT_NO_THROW(client->delete_topic(make_string_identifier(stream_name), make_string_identifier(deleted_topic)));
ASSERT_NO_THROW({
const auto topics = client->get_topics(make_string_identifier(stream_name));
ASSERT_EQ(topics.size(), 1u);
EXPECT_EQ(topics.front().name, remaining_topic);
});
}
TEST_F(LowLevelE2E_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();
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), original_topic, 2, "none", 0,
"server_default", 0, "server_default"));
ASSERT_NO_THROW(client->update_topic(make_string_identifier(stream_name), make_string_identifier(original_topic),
updated_topic_name, "gzip", 1, "duration", 1000, "1GiB"));
ASSERT_NO_THROW({
const auto topics = client->get_topics(make_string_identifier(stream_name));
ASSERT_EQ(topics.size(), 1u);
EXPECT_EQ(topics.front().name, updated_topic_name);
EXPECT_EQ(topics.front().partitions_count, 2u);
EXPECT_EQ(topics.front().compression_algorithm, "gzip");
EXPECT_EQ(topics.front().replication_factor, 1u);
EXPECT_EQ(topics.front().message_expiry, 1000u);
EXPECT_EQ(topics.front().max_topic_size, 1024ULL * 1024ULL * 1024ULL);
});
}
TEST_F(LowLevelE2E_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();
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), original_topic, 2, "none", 1,
"server_default", 0, "server_default"));
ASSERT_NO_THROW(client->update_topic(make_string_identifier(stream_name), make_string_identifier(original_topic),
updated_topic_name, "gzip", 27, "duration", 1000, "1GiB"));
ASSERT_NO_THROW({
const auto topic_details =
client->get_topic(make_string_identifier(stream_name), make_string_identifier(updated_topic_name));
const auto topics = client->get_topics(make_string_identifier(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.created_at, topic_details.created_at);
EXPECT_EQ(topic_summary.name, topic_details.name);
EXPECT_EQ(topic_summary.size_bytes, topic_details.size_bytes);
EXPECT_EQ(topic_summary.message_expiry, topic_details.message_expiry);
EXPECT_EQ(topic_summary.compression_algorithm, topic_details.compression_algorithm);
EXPECT_EQ(topic_summary.max_topic_size, topic_details.max_topic_size);
EXPECT_EQ(topic_summary.replication_factor, topic_details.replication_factor);
EXPECT_EQ(topic_summary.messages_count, topic_details.messages_count);
EXPECT_EQ(topic_summary.partitions_count, topic_details.partitions_count);
});
}
TEST_F(LowLevelE2E_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;
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), original_topic, partitions_count, "none",
1, "server_default", 0, "server_default"));
ASSERT_NO_THROW(client->update_topic(make_string_identifier(stream_name), make_string_identifier(original_topic),
updated_topic_name, "gzip", 27, "duration", 1000, "1GiB"));
ASSERT_NO_THROW({
const auto topic_details =
client->get_topic(make_string_identifier(stream_name), make_string_identifier(updated_topic_name));
EXPECT_EQ(topic_details.partitions_count, partitions_count);
EXPECT_EQ(topic_details.partitions.size(), partitions_count);
});
}
TEST_F(LowLevelE2E_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 *client = GetLoggedInClient();
ASSERT_NO_THROW(client->create_stream(stream_name));
TrackStream(stream_name);
ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), original_topic, 1, "none", 1,
"server_default", 0, "server_default"));
const auto created_stream = client->get_stream(make_string_identifier(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(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->update_topic(make_string_identifier(stream_name), make_string_identifier(original_topic),
updated_topic_name, "gzip", 27, "duration", 1000, "1GiB"));
ASSERT_NO_THROW({
const auto polled = 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(LowLevelE2E_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();
const std::vector<std::string> compression_algorithms = {"none", "gzip"};
const std::vector<std::uint8_t> replication_factors = {0, 1, 27};
struct ExpiryOption {
std::string kind;
std::uint64_t value;
};
const std::vector<ExpiryOption> expiry_options = {
{"server_default", 0},
{"never_expire", 0},
{"duration", 1000},
};
const std::vector<std::string> max_topic_sizes = {"server_default", "unlimited", "1GiB"};
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, "none", 1,
"server_default", 0, "server_default"));
for (const auto &compression_algorithm : compression_algorithms) {
for (const auto replication_factor : replication_factors) {
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 + ", replication=" + std::to_string(replication_factor) +
", expiry_kind=" + expiry_option.kind +
", expiry_value=" + std::to_string(expiry_option.value) + ", max_topic_size=" + max_topic_size);
ASSERT_NO_THROW(client->update_topic(make_string_identifier(stream_name),
make_string_identifier(topic_name), updated_topic_name,
compression_algorithm, replication_factor, expiry_option.kind,
expiry_option.value, max_topic_size));
topic_name = updated_topic_name;
}
}
}
}
}
TEST_F(LowLevelE2E_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();
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), original_topic, 2, "none", 1,
"server_default", 0, "server_default"));
const auto created_topic =
client->get_topic(make_string_identifier(stream_name), make_string_identifier(original_topic));
ASSERT_NO_THROW(client->update_topic(make_string_identifier(stream_name), make_numeric_identifier(created_topic.id),
updated_topic_name, "gzip", 1, "duration", 1000, "1GiB"));
const auto first_update =
client->get_topic(make_string_identifier(stream_name), make_numeric_identifier(created_topic.id));
ASSERT_NO_THROW(client->update_topic(make_string_identifier(stream_name), make_numeric_identifier(created_topic.id),
updated_topic_name, "gzip", 1, "duration", 1000, "1GiB"));
const auto second_update =
client->get_topic(make_string_identifier(stream_name), make_numeric_identifier(created_topic.id));
EXPECT_EQ(second_update.id, first_update.id);
EXPECT_EQ(second_update.name, first_update.name);
EXPECT_EQ(second_update.partitions_count, first_update.partitions_count);
EXPECT_EQ(second_update.compression_algorithm, first_update.compression_algorithm);
EXPECT_EQ(second_update.replication_factor, first_update.replication_factor);
EXPECT_EQ(second_update.message_expiry, first_update.message_expiry);
EXPECT_EQ(second_update.max_topic_size, first_update.max_topic_size);
}
TEST_F(LowLevelE2E_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();
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), first_topic_name, 1, "none", 1,
"server_default", 0, "server_default"));
ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), second_topic_name, 1, "none", 1,
"server_default", 0, "server_default"));
ASSERT_THROW(client->update_topic(make_string_identifier(stream_name), make_string_identifier(first_topic_name),
second_topic_name, "gzip", 1, "duration", 1000, "1GiB"),
std::exception);
}
TEST_F(LowLevelE2E_Topic, UpdateTopicWithInvalidNamesThrows) {
RecordProperty("description", "Rejects invalid topic names when updating a topic.");
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", 1,
"server_default", 0, "server_default"));
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->update_topic(make_string_identifier(stream_name), make_string_identifier(topic_name),
invalid_topic_name, "gzip", 1, "duration", 1000, "1GiB"),
std::exception);
}
}
TEST_F(LowLevelE2E_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", 1, "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", 1, "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.replication_factor, topic_before_update.replication_factor);
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(LowLevelE2E_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();
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", 1,
"server_default", 0, "server_default"));
iggy::ffi::Client *unauthenticated_client = GetLoggedOutClient();
ASSERT_THROW(
unauthenticated_client->update_topic(make_string_identifier(stream_name), make_string_identifier(topic_name),
updated_topic_name, "gzip", 1, "duration", 1000, "1GiB"),
std::exception);
ASSERT_NO_THROW(unauthenticated_client->connect());
ASSERT_THROW(
unauthenticated_client->update_topic(make_string_identifier(stream_name), make_string_identifier(topic_name),
updated_topic_name, "gzip", 1, "duration", 1000, "1GiB"),
std::exception);
ASSERT_NO_THROW(unauthenticated_client->login_user("iggy", "iggy"));
ASSERT_NO_THROW(unauthenticated_client->disconnect());
ASSERT_THROW(
unauthenticated_client->update_topic(make_string_identifier(stream_name), make_string_identifier(topic_name),
updated_topic_name, "gzip", 1, "duration", 1000, "1GiB"),
std::exception);
}
TEST_F(LowLevelE2E_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();
iggy::ffi::Client *client = GetLoggedInClient();
ASSERT_THROW(client->update_topic(make_string_identifier(stream_name), make_string_identifier(topic_name),
updated_topic_name, "gzip", 1, "duration", 1000, "1GiB"),
std::exception);
}
TEST_F(LowLevelE2E_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();
iggy::ffi::Client *client = GetLoggedInClient();
ASSERT_NO_THROW(client->create_stream(stream_name));
TrackStream(stream_name);
ASSERT_THROW(client->update_topic(make_string_identifier(stream_name), make_string_identifier(topic_name),
updated_topic_name, "gzip", 1, "duration", 1000, "1GiB"),
std::exception);
}
TEST_F(LowLevelE2E_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();
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", 0,
"server_default", 0, "server_default"));
ASSERT_NO_THROW(client->delete_stream(make_string_identifier(stream_name)));
ForgetTrackedStream(stream_name);
ASSERT_NO_THROW({
const auto topics = client->get_topics(make_string_identifier(stream_name));
EXPECT_TRUE(topics.empty());
});
}
TEST_F(LowLevelE2E_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();
iggy::ffi::Client *client = GetLoggedInClient();
ASSERT_THROW(client->purge_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)),
std::exception);
}
TEST_F(LowLevelE2E_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();
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", 0,
"server_default", 0, "server_default"));
ASSERT_NO_THROW(client->delete_stream(make_string_identifier(stream_name)));
ForgetTrackedStream(stream_name);
ASSERT_THROW(client->purge_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)),
std::exception);
}
TEST_F(LowLevelE2E_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();
iggy::ffi::Client *client = GetLoggedInClient();
ASSERT_NO_THROW(client->create_stream(stream_name));
TrackStream(stream_name);
ASSERT_THROW(client->purge_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)),
std::exception);
}
TEST_F(LowLevelE2E_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", 0,
"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(LowLevelE2E_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", 0,
"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(LowLevelE2E_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 *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, 3, "gzip", 1, "duration", 1000, "1GiB"));
auto stream_before_purge = client->get_stream(make_string_identifier(stream_name));
ASSERT_EQ(stream_before_purge.topics.size(), 1u);
const auto topic_before_messages = stream_before_purge.topics.front();
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(client->send_messages(make_numeric_identifier(stream_before_purge.id),
make_numeric_identifier(topic_before_messages.id), "partition_id",
partition_id_bytes(1), std::move(messages)));
stream_before_purge = client->get_stream(make_string_identifier(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.messages_count, 0u);
EXPECT_GT(topic_with_messages.size_bytes, 0u);
ASSERT_NO_THROW(client->purge_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)));
const auto stream_after_purge = client->get_stream(make_string_identifier(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.created_at, topic_with_messages.created_at);
EXPECT_EQ(topic_after_purge.name, topic_with_messages.name);
EXPECT_EQ(topic_after_purge.message_expiry, topic_with_messages.message_expiry);
EXPECT_EQ(topic_after_purge.compression_algorithm, topic_with_messages.compression_algorithm);
EXPECT_EQ(topic_after_purge.max_topic_size, topic_with_messages.max_topic_size);
EXPECT_EQ(topic_after_purge.replication_factor, topic_with_messages.replication_factor);
EXPECT_EQ(topic_after_purge.partitions_count, topic_with_messages.partitions_count);
}
TEST_F(LowLevelE2E_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 *client = GetLoggedInClient();
ASSERT_NO_THROW(client->create_stream(stream_name));
TrackStream(stream_name);
ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), first_topic_name, 1, "none", 0,
"server_default", 0, "server_default"));
ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), second_topic_name, 1, "none", 0,
"server_default", 0, "server_default"));
const auto created_stream = client->get_stream(make_string_identifier(stream_name));
ASSERT_EQ(created_stream.topics.size(), 2u);
std::uint32_t first_topic_id = 0;
std::uint32_t second_topic_id = 0;
bool first_topic_found = false;
bool second_topic_found = false;
for (const auto &topic : created_stream.topics) {
const std::string topic_name = static_cast<std::string>(topic.name);
if (topic_name == first_topic_name) {
first_topic_id = topic.id;
first_topic_found = true;
} else if (topic_name == second_topic_name) {
second_topic_id = topic.id;
second_topic_found = true;
}
}
ASSERT_TRUE(first_topic_found);
ASSERT_TRUE(second_topic_found);
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(client->send_messages(make_numeric_identifier(created_stream.id),
make_numeric_identifier(first_topic_id), "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(client->send_messages(make_numeric_identifier(created_stream.id),
make_numeric_identifier(second_topic_id), "partition_id",
partition_id_bytes(0), std::move(second_topic_messages)));
const auto stream_before_purge = client->get_stream(make_string_identifier(stream_name));
EXPECT_EQ(stream_before_purge.messages_count, 5u);
EXPECT_GT(stream_before_purge.size_bytes, 0u);
std::unordered_map<std::string, std::uint64_t> messages_before_purge;
for (const auto &topic : stream_before_purge.topics) {
messages_before_purge[static_cast<std::string>(topic.name)] = topic.messages_count;
}
EXPECT_EQ(messages_before_purge[first_topic_name], 3u);
EXPECT_EQ(messages_before_purge[second_topic_name], 2u);
ASSERT_NO_THROW(client->purge_topic(make_string_identifier(stream_name), make_string_identifier(first_topic_name)));
const auto stream_after_purge = client->get_stream(make_string_identifier(stream_name));
EXPECT_EQ(stream_after_purge.topics_count, 2u);
EXPECT_EQ(stream_after_purge.messages_count, 2u);
EXPECT_GT(stream_after_purge.size_bytes, 0u);
EXPECT_LT(stream_after_purge.size_bytes, stream_before_purge.size_bytes);
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) {
const std::string topic_name = static_cast<std::string>(topic.name);
messages_after_purge[topic_name] = topic.messages_count;
sizes_after_purge[topic_name] = topic.size_bytes;
}
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(LowLevelE2E_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 *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, 3, "none", 0,
"server_default", 0, "server_default"));
const auto created_stream = client->get_stream(make_string_identifier(stream_name));
ASSERT_EQ(created_stream.topics.size(), 1u);
const std::uint32_t topic_id = created_stream.topics.front().id;
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(client->send_messages(make_numeric_identifier(created_stream.id),
make_numeric_identifier(topic_id), "partition_id",
partition_id_bytes(partition_id), std::move(messages)));
}
const auto stream_before_purge = client->get_stream(make_string_identifier(stream_name));
EXPECT_EQ(stream_before_purge.messages_count, 6u);
ASSERT_EQ(stream_before_purge.topics.size(), 1u);
EXPECT_EQ(stream_before_purge.topics.front().partitions_count, 3u);
EXPECT_EQ(stream_before_purge.topics.front().messages_count, 6u);
ASSERT_NO_THROW(client->purge_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)));
const auto stream_after_purge = client->get_stream(make_string_identifier(stream_name));
EXPECT_EQ(stream_after_purge.messages_count, 0u);
ASSERT_EQ(stream_after_purge.topics.size(), 1u);
EXPECT_EQ(stream_after_purge.topics.front().partitions_count, 3u);
EXPECT_EQ(stream_after_purge.topics.front().messages_count, 0u);
EXPECT_EQ(stream_after_purge.topics.front().size_bytes, 0u);
}
TEST_F(LowLevelE2E_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 *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", 0,
"server_default", 0, "server_default"));
const auto created_stream = client->get_stream(make_string_identifier(stream_name));
ASSERT_EQ(created_stream.topics.size(), 1u);
const std::uint32_t topic_id = created_stream.topics.front().id;
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(client->send_messages(make_numeric_identifier(created_stream.id), make_numeric_identifier(topic_id),
"partition_id", partition_id_bytes(0), std::move(first_batch)));
ASSERT_NO_THROW(client->purge_topic(make_string_identifier(stream_name), make_string_identifier(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(client->send_messages(make_numeric_identifier(created_stream.id), make_numeric_identifier(topic_id),
"partition_id", partition_id_bytes(0), std::move(second_batch)));
const auto stream_after_resend = client->get_stream(make_string_identifier(stream_name));
EXPECT_EQ(stream_after_resend.messages_count, 2u);
ASSERT_EQ(stream_after_resend.topics.size(), 1u);
EXPECT_EQ(stream_after_resend.topics.front().messages_count, 2u);
EXPECT_GT(stream_after_resend.topics.front().size_bytes, 0u);
}
TEST_F(LowLevelE2E_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 *client = GetLoggedInClient();
ASSERT_NO_THROW(client->create_stream(stream_name));
TrackStream(stream_name);
ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), first_topic_name, 1, "none", 0,
"server_default", 0, "server_default"));
ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), second_topic_name, 1, "none", 0,
"server_default", 0, "server_default"));
const auto created_stream = client->get_stream(make_string_identifier(stream_name));
ASSERT_EQ(created_stream.topics.size(), 2u);
std::uint32_t first_topic_id = 0;
std::uint32_t second_topic_id = 0;
bool first_topic_found = false;
bool second_topic_found = false;
for (const auto &topic : created_stream.topics) {
const std::string topic_name = static_cast<std::string>(topic.name);
if (topic_name == first_topic_name) {
first_topic_id = topic.id;
first_topic_found = true;
} else if (topic_name == second_topic_name) {
second_topic_id = topic.id;
second_topic_found = true;
}
}
ASSERT_TRUE(first_topic_found);
ASSERT_TRUE(second_topic_found);
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(client->send_messages(make_numeric_identifier(created_stream.id),
make_numeric_identifier(first_topic_id), "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(client->send_messages(make_numeric_identifier(created_stream.id),
make_numeric_identifier(second_topic_id), "partition_id",
partition_id_bytes(0), std::move(second_topic_messages)));
ASSERT_NO_THROW(client->purge_topic(make_string_identifier(stream_name), make_string_identifier(first_topic_name)));
const auto stream_after_first_purge = client->get_stream(make_string_identifier(stream_name));
EXPECT_EQ(stream_after_first_purge.topics_count, 2u);
EXPECT_EQ(stream_after_first_purge.messages_count, 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) {
const std::string topic_name = static_cast<std::string>(topic.name);
messages_after_first_purge[topic_name] = topic.messages_count;
sizes_after_first_purge[topic_name] = topic.size_bytes;
}
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->purge_topic(make_string_identifier(stream_name), make_string_identifier(first_topic_name)));
const auto stream_after_second_purge = client->get_stream(make_string_identifier(stream_name));
EXPECT_EQ(stream_after_second_purge.topics_count, 2u);
EXPECT_EQ(stream_after_second_purge.messages_count, 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) {
const std::string topic_name = static_cast<std::string>(topic.name);
messages_after_second_purge[topic_name] = topic.messages_count;
sizes_after_second_purge[topic_name] = topic.size_bytes;
}
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(LowLevelE2E_Topic, PurgeTopicBeforeLoginThrows) {
RecordProperty("description", "Throws when topic purge is attempted before authentication.");
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", 0,
"server_default", 0, "server_default"));
iggy::ffi::Client *unauthenticated_client = GetLoggedOutClient();
ASSERT_THROW(
unauthenticated_client->purge_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)),
std::exception);
ASSERT_NO_THROW(unauthenticated_client->connect());
ASSERT_THROW(
unauthenticated_client->purge_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->purge_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)),
std::exception);
}