| # 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. |
| |
| from datetime import timedelta |
| |
| import pytest |
| |
| from apache_iggy import HeaderValue, IggyClient, IggyExpiry, MaxTopicSize, SendMessage |
| |
| from .utils import ( |
| get_server_config, |
| wait_for_ping, |
| wait_for_server, |
| ) |
| |
| |
| class TestCreateTopic: |
| """Test topic creation via create_topic.""" |
| |
| @pytest.mark.asyncio |
| @pytest.mark.parametrize( |
| ("prefix", "min_bytes", "max_bytes"), |
| [ |
| ("a", 8, 255), |
| ("stream-name", 8, 255), |
| ("stream_name.with.mixed-CHARS123", 8, 255), |
| (" leading-space", 8, 255), |
| ("trailing-space ", 8, 255), |
| ("multiple spaces inside", 8, 255), |
| ("name/with/slash", 8, 255), |
| ("name:with:colons", 8, 255), |
| ("name.with.dots", 8, 255), |
| (" ", 8, 255), |
| ("a" * 247, 255, 255), |
| (("é" * 122) + "abc", 255, 255), |
| (("한" * 81) + "abc", 255, 255), |
| (("漢" * 81) + "abc", 255, 255), |
| (("あ" * 81) + "abc", 255, 255), |
| (("😀" * 60) + "abcdefg", 255, 255), |
| ], |
| ) |
| async def test_create_and_get_topic( |
| self, |
| iggy_client: IggyClient, |
| unique_name, |
| prefix: str, |
| min_bytes: int, |
| max_bytes: int, |
| ): |
| """Test topic creation and retrieval.""" |
| stream_name = unique_name() |
| topic_name = unique_name(prefix, min_bytes=min_bytes, max_bytes=max_bytes) |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_name, partitions_count=2 |
| ) |
| |
| topic = await iggy_client.get_topic(stream_name, topic_name) |
| assert topic is not None |
| assert topic.name == topic_name |
| assert topic.partitions_count == 2 |
| assert topic.created_at > 0 |
| assert topic.size == 0 |
| assert len(topic.partitions) == 2 |
| assert all(partition.messages_count == 0 for partition in topic.partitions) |
| |
| stream = await iggy_client.get_stream(stream_name) |
| assert stream is not None |
| assert stream.topics_count > 0 |
| |
| @pytest.mark.asyncio |
| @pytest.mark.parametrize( |
| ("prefix", "min_bytes", "max_bytes"), |
| [ |
| ("", 0, 0), |
| ("a" * 248, 256, 256), |
| ("é" * 124, 256, 256), |
| (("é" * 123) + "ab", 256, 256), |
| (("한" * 82) + "ab", 256, 256), |
| (("漢" * 82) + "ab", 256, 256), |
| (("あ" * 82) + "ab", 256, 256), |
| ("😀" * 62, 256, 256), |
| (("😀" * 61) + "abcd", 256, 256), |
| ], |
| ) |
| async def test_create_topic_invalid_names( |
| self, |
| iggy_client: IggyClient, |
| unique_name, |
| prefix: str, |
| min_bytes: int, |
| max_bytes: int, |
| ): |
| """Test create_topic enforces byte-length validation.""" |
| stream_name = unique_name() |
| topic_name = unique_name(prefix, min_bytes=min_bytes, max_bytes=max_bytes) |
| |
| await iggy_client.create_stream(stream_name) |
| |
| with pytest.raises(RuntimeError): |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_name, partitions_count=1 |
| ) |
| |
| @pytest.mark.asyncio |
| async def test_create_and_get_topic_with_numeric_stream_id( |
| self, iggy_client: IggyClient, unique_name |
| ): |
| """Test topic APIs accept a numeric stream id for create and get operations.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| stream = await iggy_client.get_stream(stream_name) |
| assert stream is not None |
| |
| await iggy_client.create_topic( |
| stream=stream.id, name=topic_name, partitions_count=2 |
| ) |
| |
| topic_by_name = await iggy_client.get_topic(stream.id, topic_name) |
| assert topic_by_name is not None |
| assert topic_by_name.name == topic_name |
| assert topic_by_name.partitions_count == 2 |
| |
| topic_by_id = await iggy_client.get_topic(stream.id, topic_by_name.id) |
| assert topic_by_id is not None |
| assert topic_by_id.id == topic_by_name.id |
| assert topic_by_id.name == topic_by_name.name |
| |
| @pytest.mark.asyncio |
| async def test_duplicate_topic_creation(self, iggy_client: IggyClient, unique_name): |
| """Test that creating duplicate topics raises appropriate errors.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_name, partitions_count=1 |
| ) |
| |
| with pytest.raises(RuntimeError) as exc_info: |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_name, partitions_count=1 |
| ) |
| |
| assert "already exists" in str(exc_info.value) |
| |
| @pytest.mark.asyncio |
| async def test_topic_names_can_repeat_across_different_streams( |
| self, iggy_client: IggyClient, unique_name |
| ): |
| """Test the same topic name can be created in different streams.""" |
| first_stream_name = unique_name() |
| second_stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(first_stream_name) |
| await iggy_client.create_stream(second_stream_name) |
| |
| await iggy_client.create_topic( |
| stream=first_stream_name, name=topic_name, partitions_count=1 |
| ) |
| await iggy_client.create_topic( |
| stream=second_stream_name, name=topic_name, partitions_count=1 |
| ) |
| |
| first_topic = await iggy_client.get_topic(first_stream_name, topic_name) |
| second_topic = await iggy_client.get_topic(second_stream_name, topic_name) |
| |
| assert first_topic is not None |
| assert second_topic is not None |
| assert first_topic.name == topic_name |
| assert second_topic.name == topic_name |
| assert first_topic.partitions_count == 1 |
| assert second_topic.partitions_count == 1 |
| |
| @pytest.mark.asyncio |
| @pytest.mark.parametrize("compression_algorithm", ["gzip", "Gzip", "none", "None"]) |
| async def test_create_topic_with_valid_compression_algorithm( |
| self, iggy_client: IggyClient, unique_name, compression_algorithm: str |
| ): |
| """Test create_topic accepts a supported compression algorithm value.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, |
| name=topic_name, |
| partitions_count=1, |
| compression_algorithm=compression_algorithm, |
| ) |
| |
| topic = await iggy_client.get_topic(stream_name, topic_name) |
| assert topic is not None |
| assert topic.name == topic_name |
| assert topic.partitions_count == 1 |
| |
| @pytest.mark.asyncio |
| @pytest.mark.parametrize( |
| "compression_algorithm", ["brotli", "deflate", "gzipp", "", " gzip "] |
| ) |
| async def test_create_topic_invalid_compression_algorithm( |
| self, iggy_client: IggyClient, unique_name, compression_algorithm: str |
| ): |
| """Test create_topic rejects unsupported compression algorithm values.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| |
| with pytest.raises(RuntimeError, match="Unknown compression type"): |
| await iggy_client.create_topic( |
| stream=stream_name, |
| name=topic_name, |
| partitions_count=1, |
| compression_algorithm=compression_algorithm, |
| ) |
| |
| @pytest.mark.asyncio |
| @pytest.mark.parametrize( |
| "message_expiry", |
| [ |
| IggyExpiry.ExpireDuration(timedelta(microseconds=1)), |
| IggyExpiry.ExpireDuration(timedelta(seconds=1)), |
| IggyExpiry.ExpireDuration(timedelta(minutes=10)), |
| IggyExpiry.ExpireDuration(timedelta(days=1, seconds=2, microseconds=3)), |
| # days * 86_400 overflows i32 (max ~24,855 days); regression test |
| # for widening the days-to-seconds conversion to i64. |
| IggyExpiry.ExpireDuration(timedelta(days=30_000)), |
| ], |
| ) |
| async def test_create_topic_with_message_expiry( |
| self, |
| iggy_client: IggyClient, |
| unique_name, |
| message_expiry: IggyExpiry.ExpireDuration, |
| ): |
| """Test create_topic accepts an explicit message expiry.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, |
| name=topic_name, |
| partitions_count=1, |
| message_expiry=message_expiry, |
| ) |
| |
| topic = await iggy_client.get_topic(stream_name, topic_name) |
| assert topic is not None |
| assert topic.name == topic_name |
| assert isinstance(topic.message_expiry, IggyExpiry.ExpireDuration) |
| assert topic.message_expiry.duration == message_expiry.duration |
| |
| @pytest.mark.asyncio |
| @pytest.mark.parametrize( |
| "invalid_duration", |
| [ |
| timedelta(seconds=-1), |
| # 0 is the wire sentinel reserved for IggyExpiry.ServerDefault(); |
| # matches MaxTopicSize.Custom(0) rejecting its own sentinel. |
| timedelta(0), |
| # u64::MAX microseconds is the wire sentinel reserved for |
| # IggyExpiry.NeverExpire(); matches MaxTopicSize.Custom(u64::MAX). |
| timedelta(microseconds=2**64 - 1), |
| ], |
| ) |
| async def test_create_topic_rejects_invalid_message_expiry_duration( |
| self, iggy_client: IggyClient, unique_name, invalid_duration: timedelta |
| ): |
| """Test create_topic rejects an ExpireDuration at a reserved boundary.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| |
| with pytest.raises(ValueError): |
| await iggy_client.create_topic( |
| stream=stream_name, |
| name=topic_name, |
| partitions_count=1, |
| message_expiry=IggyExpiry.ExpireDuration(invalid_duration), |
| ) |
| |
| @pytest.mark.asyncio |
| @pytest.mark.parametrize( |
| "invalid_message_expiry", [1, "1s", object(), timedelta(seconds=1)] |
| ) |
| async def test_create_topic_invalid_message_expiry( |
| self, iggy_client: IggyClient, unique_name, invalid_message_expiry |
| ): |
| """Test create_topic rejects non-IggyExpiry message_expiry values.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| |
| with pytest.raises(TypeError): |
| await iggy_client.create_topic( |
| stream=stream_name, |
| name=topic_name, |
| partitions_count=1, |
| message_expiry=invalid_message_expiry, |
| ) |
| |
| @pytest.mark.asyncio |
| @pytest.mark.parametrize( |
| ("max_topic_size", "expected_kind"), |
| [ |
| (MaxTopicSize.ServerDefault(), "unlimited"), # resolved by create_topic |
| (MaxTopicSize.Unlimited(), "unlimited"), |
| (MaxTopicSize.Custom(2_000_000_000), "custom"), |
| ], |
| ) |
| async def test_create_topic_with_valid_max_topic_size( |
| self, |
| iggy_client: IggyClient, |
| unique_name, |
| max_topic_size: MaxTopicSize, |
| expected_kind: str, |
| ): |
| """Test create_topic accepts supported maximum topic size values.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, |
| name=topic_name, |
| partitions_count=1, |
| max_topic_size=max_topic_size, |
| ) |
| |
| topic = await iggy_client.get_topic(stream_name, topic_name) |
| assert topic is not None |
| assert topic.name == topic_name |
| if expected_kind == "unlimited": |
| assert isinstance(topic.max_topic_size, MaxTopicSize.Unlimited) |
| else: |
| assert isinstance(topic.max_topic_size, MaxTopicSize.Custom) |
| assert isinstance(max_topic_size, MaxTopicSize.Custom) |
| assert topic.max_topic_size.bytes == max_topic_size.bytes |
| |
| @pytest.mark.asyncio |
| @pytest.mark.parametrize( |
| ("max_topic_size_bytes", "expected_exception"), |
| [ |
| (4563, RuntimeError), |
| (-1, OverflowError), |
| (2e64, TypeError), |
| (0, ValueError), |
| (2**64 - 1, ValueError), # u64::MAX is reserved for Unlimited |
| ], |
| ) |
| async def test_create_topic_invalid_max_topic_size( |
| self, |
| iggy_client: IggyClient, |
| unique_name, |
| max_topic_size_bytes, |
| expected_exception, |
| ): |
| """Test create_topic rejects invalid maximum topic size values.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| |
| with pytest.raises(expected_exception): |
| await iggy_client.create_topic( |
| stream=stream_name, |
| name=topic_name, |
| partitions_count=1, |
| max_topic_size=MaxTopicSize.Custom(max_topic_size_bytes), |
| ) |
| |
| @pytest.mark.asyncio |
| @pytest.mark.parametrize("partitions_count", [1001, 10000]) |
| async def test_create_topic_invalid_partitions_count( |
| self, iggy_client: IggyClient, unique_name, partitions_count: int |
| ): |
| """Test create_topic rejects partition counts above the supported limit.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| |
| with pytest.raises(RuntimeError) as exc_info: |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_name, partitions_count=partitions_count |
| ) |
| |
| assert "Too many partitions" in str(exc_info.value) |
| |
| @pytest.mark.asyncio |
| @pytest.mark.parametrize("partitions_count", [0, 1, 1000]) |
| async def test_create_topic_with_valid_partitions_count( |
| self, iggy_client: IggyClient, unique_name, partitions_count: int |
| ): |
| """Test create_topic accepts the maximum supported partitions count.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_name, partitions_count=partitions_count |
| ) |
| |
| topic = await iggy_client.get_topic(stream_name, topic_name) |
| assert topic is not None |
| assert topic.name == topic_name |
| assert topic.partitions_count == partitions_count |
| |
| @pytest.mark.asyncio |
| async def test_stream_topics_count_increases_after_multiple_topic_creations( |
| self, iggy_client: IggyClient, unique_name |
| ): |
| """Test a stream reports additional topics after multiple creations.""" |
| stream_name = unique_name() |
| first_topic_name = unique_name() |
| second_topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| |
| stream_before = await iggy_client.get_stream(stream_name) |
| assert stream_before is not None |
| assert stream_before.topics_count == 0 |
| |
| await iggy_client.create_topic( |
| stream=stream_name, name=first_topic_name, partitions_count=1 |
| ) |
| await iggy_client.create_topic( |
| stream=stream_name, name=second_topic_name, partitions_count=1 |
| ) |
| |
| stream_after = await iggy_client.get_stream(stream_name) |
| assert stream_after is not None |
| assert stream_after.topics_count == 2 |
| |
| @pytest.mark.asyncio |
| async def test_create_topic_then_reconnect_then_get_topic( |
| self, iggy_client: IggyClient, unique_name |
| ): |
| """Test a topic remains retrievable after reconnecting with a fresh client.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_name, partitions_count=1 |
| ) |
| |
| host, port = get_server_config() |
| wait_for_server(host, port) |
| |
| client = IggyClient(f"{host}:{port}") |
| await client.connect() |
| await wait_for_ping(client) |
| await client.login_user("iggy", "iggy") |
| |
| topic = await client.get_topic(stream_name, topic_name) |
| assert topic is not None |
| assert topic.name == topic_name |
| assert topic.partitions_count == 1 |
| |
| @pytest.mark.asyncio |
| async def test_create_topic_in_nonexistent_stream( |
| self, iggy_client: IggyClient, unique_name |
| ): |
| """Test creating a topic in a non-existent stream.""" |
| nonexistent_stream = unique_name() |
| topic_name = unique_name() |
| |
| with pytest.raises(RuntimeError): |
| await iggy_client.create_topic( |
| stream=nonexistent_stream, name=topic_name, partitions_count=1 |
| ) |
| |
| @pytest.mark.asyncio |
| async def test_create_topic_requires_connection_and_auth(self, unique_name): |
| """Test create_topic fails both before connecting and before logging in.""" |
| host, port = get_server_config() |
| wait_for_server(host, port) |
| |
| client = IggyClient(f"{host}:{port}") |
| with pytest.raises(RuntimeError): |
| await client.create_topic( |
| stream=unique_name(), name=unique_name(), partitions_count=1 |
| ) |
| |
| await client.connect() |
| with pytest.raises(RuntimeError): |
| await client.create_topic( |
| stream=unique_name(), name=unique_name(), partitions_count=1 |
| ) |
| |
| |
| class TestGetTopic: |
| """Test topic retrieval via get_topic.""" |
| |
| @pytest.mark.asyncio |
| async def test_get_topic_by_name_and_id(self, iggy_client: IggyClient, unique_name): |
| """Test repeated topic lookup works by both name and numeric id.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_name, partitions_count=1 |
| ) |
| |
| topic_by_name = await iggy_client.get_topic(stream_name, topic_name) |
| assert topic_by_name is not None |
| |
| topic_by_name_again = await iggy_client.get_topic(stream_name, topic_name) |
| assert topic_by_name_again is not None |
| assert topic_by_name_again.id == topic_by_name.id |
| assert topic_by_name_again.name == topic_by_name.name |
| |
| topic_by_id = await iggy_client.get_topic(stream_name, topic_by_name.id) |
| assert topic_by_id is not None |
| assert topic_by_id.id == topic_by_name.id |
| assert topic_by_id.name == topic_by_name.name |
| |
| @pytest.mark.asyncio |
| async def test_get_topic_partitions(self, iggy_client: IggyClient, unique_name): |
| """Test TopicDetails.partitions returns one Partition per partition.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_name, partitions_count=3 |
| ) |
| |
| topic = await iggy_client.get_topic(stream_name, topic_name) |
| assert topic is not None |
| assert len(topic.partitions) == 3 |
| assert [partition.id for partition in topic.partitions] == [0, 1, 2] |
| for partition in topic.partitions: |
| assert partition.created_at > 0 |
| assert partition.segments_count == 1 |
| assert partition.current_offset == 0 |
| assert partition.size == 0 |
| assert partition.messages_count == 0 |
| |
| @pytest.mark.asyncio |
| async def test_get_nonexistent_topic(self, iggy_client: IggyClient, unique_name): |
| """Test getting a non-existent topic by name or numeric id.""" |
| stream_name = unique_name() |
| nonexistent_topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| |
| topic_by_name = await iggy_client.get_topic(stream_name, nonexistent_topic_name) |
| assert topic_by_name is None |
| |
| topic_by_id = await iggy_client.get_topic(stream_name, 999999) |
| assert topic_by_id is None |
| |
| @pytest.mark.asyncio |
| async def test_get_topic_in_nonexistent_stream( |
| self, iggy_client: IggyClient, unique_name |
| ): |
| """Test getting a topic from a non-existent stream returns no topic.""" |
| nonexistent_stream = unique_name() |
| topic_name = unique_name() |
| |
| topic = await iggy_client.get_topic(nonexistent_stream, topic_name) |
| assert topic is None |
| |
| @pytest.mark.asyncio |
| async def test_get_topic_requires_connection_and_auth(self, unique_name): |
| """Test get_topic fails both before connecting and before logging in.""" |
| host, port = get_server_config() |
| wait_for_server(host, port) |
| |
| client = IggyClient(f"{host}:{port}") |
| with pytest.raises(RuntimeError): |
| await client.get_topic(unique_name(), unique_name()) |
| |
| await client.connect() |
| with pytest.raises(RuntimeError): |
| await client.get_topic(unique_name(), unique_name()) |
| |
| |
| class TestGetTopics: |
| """Test listing topics in a stream via get_topics.""" |
| |
| @pytest.mark.asyncio |
| async def test_get_topics_in_empty_stream_returns_empty_list( |
| self, iggy_client: IggyClient, unique_name |
| ): |
| """Test get_topics returns an empty list for a stream with no topics.""" |
| stream_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| |
| topics = await iggy_client.get_topics(stream_name) |
| assert topics == [] |
| |
| @pytest.mark.asyncio |
| @pytest.mark.parametrize("topic_count", [1, 3, 5]) |
| async def test_get_topics_returns_all_created_topics( |
| self, iggy_client: IggyClient, unique_name, topic_count: int |
| ): |
| """Test get_topics returns every created topic, ordered by id ascending.""" |
| stream_name = unique_name() |
| # Reverse-alphabetical names so that id-ascending (creation) order |
| # and name order disagree, proving the list isn't accidentally |
| # name-sorted. |
| topic_names = [unique_name() for _ in range(topic_count, 0, -1)] |
| |
| await iggy_client.create_stream(stream_name) |
| for name in topic_names: |
| await iggy_client.create_topic( |
| stream=stream_name, name=name, partitions_count=1 |
| ) |
| |
| topics = await iggy_client.get_topics(stream_name) |
| assert len(topics) == len(topic_names) |
| assert [topic.name for topic in topics] == topic_names |
| assert [topic.id for topic in topics] == sorted(topic.id for topic in topics) |
| # Only assert fields supplied at creation or deterministically set by |
| # the server: each topic keeps its partition count and a fresh topic |
| # holds no messages. The numeric id is server-assigned and not checked. |
| assert all(topic.partitions_count == 1 for topic in topics) |
| assert all(topic.messages_count == 0 for topic in topics) |
| |
| @pytest.mark.asyncio |
| async def test_get_topics_returns_same_result_when_called_repeatedly( |
| self, iggy_client: IggyClient, unique_name |
| ): |
| """Test repeated get_topics calls return an identically ordered view.""" |
| stream_name = unique_name() |
| topic_names = [unique_name() for _ in range(3)] |
| |
| await iggy_client.create_stream(stream_name) |
| for name in topic_names: |
| await iggy_client.create_topic( |
| stream=stream_name, name=name, partitions_count=1 |
| ) |
| |
| first = await iggy_client.get_topics(stream_name) |
| second = await iggy_client.get_topics(stream_name) |
| assert [topic.name for topic in first] == topic_names |
| assert [topic.id for topic in first] == [topic.id for topic in second] |
| assert [topic.name for topic in first] == [topic.name for topic in second] |
| |
| @pytest.mark.asyncio |
| async def test_get_topics_accepts_numeric_stream_id( |
| self, iggy_client: IggyClient, unique_name |
| ): |
| """Test get_topics works when the stream is referenced by numeric id.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_name, partitions_count=1 |
| ) |
| stream = await iggy_client.get_stream(stream_name) |
| assert stream is not None |
| |
| topics_by_name = await iggy_client.get_topics(stream_name) |
| topics_by_id = await iggy_client.get_topics(stream.id) |
| assert len(topics_by_name) == 1 |
| assert len(topics_by_id) == 1 |
| assert topics_by_id[0].name == topic_name |
| |
| @pytest.mark.asyncio |
| async def test_get_topics_in_nonexistent_stream_returns_empty( |
| self, iggy_client: IggyClient, unique_name |
| ): |
| """Test get_topics returns an empty list for a non-existent stream.""" |
| topics = await iggy_client.get_topics(unique_name()) |
| assert topics == [] |
| |
| @pytest.mark.asyncio |
| async def test_get_topics_requires_connection_and_auth(self, unique_name): |
| """Test get_topics fails both before connecting and before logging in.""" |
| host, port = get_server_config() |
| wait_for_server(host, port) |
| |
| client = IggyClient(f"{host}:{port}") |
| with pytest.raises(RuntimeError): |
| await client.get_topics(unique_name()) |
| |
| await client.connect() |
| with pytest.raises(RuntimeError): |
| await client.get_topics(unique_name()) |
| |
| |
| class TestUpdateTopic: |
| """Test updating topics via update_topic.""" |
| |
| @pytest.mark.asyncio |
| @pytest.mark.parametrize( |
| ("prefix", "min_bytes", "max_bytes"), |
| [ |
| ("", 0, 0), |
| ("a" * 248, 256, 256), |
| ("é" * 124, 256, 256), |
| (("é" * 123) + "ab", 256, 256), |
| (("한" * 82) + "ab", 256, 256), |
| (("漢" * 82) + "ab", 256, 256), |
| (("あ" * 82) + "ab", 256, 256), |
| ("😀" * 62, 256, 256), |
| (("😀" * 61) + "abcd", 256, 256), |
| ], |
| ) |
| async def test_update_topic_invalid_names( |
| self, |
| iggy_client: IggyClient, |
| unique_name, |
| prefix: str, |
| min_bytes: int, |
| max_bytes: int, |
| ): |
| """Test update_topic enforces byte-length validation.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| invalid_name = unique_name(prefix, min_bytes=min_bytes, max_bytes=max_bytes) |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_name, partitions_count=1 |
| ) |
| |
| with pytest.raises(RuntimeError): |
| await iggy_client.update_topic( |
| stream_id=stream_name, topic_id=topic_name, name=invalid_name |
| ) |
| |
| @pytest.mark.asyncio |
| async def test_update_topic_renames_topic( |
| self, iggy_client: IggyClient, unique_name |
| ): |
| """Test update_topic renames a topic; old name no longer resolves.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| new_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_name, partitions_count=1 |
| ) |
| |
| await iggy_client.update_topic( |
| stream_id=stream_name, topic_id=topic_name, name=new_name |
| ) |
| |
| renamed = await iggy_client.get_topic(stream_name, new_name) |
| assert renamed is not None |
| assert renamed.name == new_name |
| |
| old = await iggy_client.get_topic(stream_name, topic_name) |
| assert old is None |
| |
| @pytest.mark.asyncio |
| async def test_update_topic_preserves_id( |
| self, iggy_client: IggyClient, unique_name |
| ): |
| """Test update_topic keeps the same numeric id after a rename.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| new_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_name, partitions_count=1 |
| ) |
| before = await iggy_client.get_topic(stream_name, topic_name) |
| assert before is not None |
| |
| await iggy_client.update_topic( |
| stream_id=stream_name, topic_id=topic_name, name=new_name |
| ) |
| |
| after = await iggy_client.get_topic(stream_name, new_name) |
| assert after is not None |
| assert after.id == before.id |
| |
| @pytest.mark.asyncio |
| async def test_update_topic_by_numeric_id( |
| self, iggy_client: IggyClient, unique_name |
| ): |
| """Test update_topic accepts a numeric topic id.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| new_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_name, partitions_count=1 |
| ) |
| topic = await iggy_client.get_topic(stream_name, topic_name) |
| assert topic is not None |
| |
| await iggy_client.update_topic( |
| stream_id=stream_name, topic_id=topic.id, name=new_name |
| ) |
| |
| renamed = await iggy_client.get_topic(stream_name, new_name) |
| assert renamed is not None |
| assert renamed.name == new_name |
| |
| @pytest.mark.asyncio |
| @pytest.mark.parametrize("compression_algorithm", ["gzip", "none"]) |
| async def test_update_topic_with_valid_compression_algorithm( |
| self, iggy_client: IggyClient, unique_name, compression_algorithm: str |
| ): |
| """Test update_topic accepts a supported compression algorithm.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_name, partitions_count=1 |
| ) |
| |
| await iggy_client.update_topic( |
| stream_id=stream_name, |
| topic_id=topic_name, |
| name=topic_name, |
| compression_algorithm=compression_algorithm, |
| ) |
| |
| topic = await iggy_client.get_topic(stream_name, topic_name) |
| assert topic is not None |
| assert topic.name == topic_name |
| assert topic.compression_algorithm == compression_algorithm |
| |
| @pytest.mark.asyncio |
| @pytest.mark.parametrize("compression_algorithm", ["brotli", "gzipp", ""]) |
| async def test_update_topic_invalid_compression_algorithm( |
| self, iggy_client: IggyClient, unique_name, compression_algorithm: str |
| ): |
| """Test update_topic rejects unsupported compression algorithm values.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_name, partitions_count=1 |
| ) |
| |
| with pytest.raises(RuntimeError, match="Unknown compression type"): |
| await iggy_client.update_topic( |
| stream_id=stream_name, |
| topic_id=topic_name, |
| name=topic_name, |
| compression_algorithm=compression_algorithm, |
| ) |
| |
| @pytest.mark.asyncio |
| @pytest.mark.parametrize( |
| "invalid_message_expiry", [1, "1s", object(), timedelta(seconds=1)] |
| ) |
| async def test_update_topic_invalid_message_expiry( |
| self, iggy_client: IggyClient, unique_name, invalid_message_expiry |
| ): |
| """Test update_topic rejects non-IggyExpiry message_expiry values.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_name, partitions_count=1 |
| ) |
| |
| with pytest.raises(TypeError): |
| await iggy_client.update_topic( |
| stream_id=stream_name, |
| topic_id=topic_name, |
| name=topic_name, |
| message_expiry=invalid_message_expiry, |
| ) |
| |
| @pytest.mark.asyncio |
| @pytest.mark.parametrize( |
| ("max_topic_size_bytes", "expected_exception"), |
| [ |
| (-1, OverflowError), |
| (2e64, TypeError), |
| (0, ValueError), |
| (2**64 - 1, ValueError), # u64::MAX is reserved for Unlimited |
| ], |
| ) |
| async def test_update_topic_invalid_max_topic_size( |
| self, |
| iggy_client: IggyClient, |
| unique_name, |
| max_topic_size_bytes, |
| expected_exception, |
| ): |
| """Test update_topic rejects invalid maximum topic size values.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_name, partitions_count=1 |
| ) |
| |
| with pytest.raises(expected_exception): |
| await iggy_client.update_topic( |
| stream_id=stream_name, |
| topic_id=topic_name, |
| name=topic_name, |
| max_topic_size=MaxTopicSize.Custom(max_topic_size_bytes), |
| ) |
| |
| @pytest.mark.asyncio |
| async def test_update_topic_with_message_expiry( |
| self, iggy_client: IggyClient, unique_name |
| ): |
| """Test update_topic accepts an explicit message expiry.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_name, partitions_count=1 |
| ) |
| |
| await iggy_client.update_topic( |
| stream_id=stream_name, |
| topic_id=topic_name, |
| name=topic_name, |
| message_expiry=IggyExpiry.ExpireDuration(timedelta(minutes=10)), |
| ) |
| |
| topic = await iggy_client.get_topic(stream_name, topic_name) |
| assert topic is not None |
| assert topic.name == topic_name |
| assert isinstance(topic.message_expiry, IggyExpiry.ExpireDuration) |
| assert topic.message_expiry.duration == timedelta(minutes=10) |
| |
| @pytest.mark.asyncio |
| @pytest.mark.parametrize( |
| "invalid_duration", |
| [ |
| timedelta(seconds=-1), |
| # 0 is the wire sentinel reserved for IggyExpiry.ServerDefault(); |
| # matches MaxTopicSize.Custom(0) rejecting its own sentinel. |
| timedelta(0), |
| # u64::MAX microseconds is the wire sentinel reserved for |
| # IggyExpiry.NeverExpire(); matches MaxTopicSize.Custom(u64::MAX). |
| timedelta(microseconds=2**64 - 1), |
| ], |
| ) |
| async def test_update_topic_rejects_invalid_message_expiry_duration( |
| self, iggy_client: IggyClient, unique_name, invalid_duration: timedelta |
| ): |
| """Test update_topic rejects an ExpireDuration at a reserved boundary.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_name, partitions_count=1 |
| ) |
| |
| with pytest.raises(ValueError): |
| await iggy_client.update_topic( |
| stream_id=stream_name, |
| topic_id=topic_name, |
| name=topic_name, |
| message_expiry=IggyExpiry.ExpireDuration(invalid_duration), |
| ) |
| |
| @pytest.mark.asyncio |
| @pytest.mark.parametrize( |
| ("max_topic_size", "expected_kind"), |
| [ |
| (MaxTopicSize.ServerDefault(), "server_default"), |
| (MaxTopicSize.Custom(2_000_000_000), "custom"), |
| (MaxTopicSize.Unlimited(), "unlimited"), |
| ], |
| ) |
| async def test_update_topic_with_valid_max_topic_size( |
| self, |
| iggy_client: IggyClient, |
| unique_name, |
| max_topic_size: MaxTopicSize, |
| expected_kind: str, |
| ): |
| """Test update_topic accepts supported maximum topic size values.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_name, partitions_count=1 |
| ) |
| |
| created = await iggy_client.get_topic(stream_name, topic_name) |
| assert created is not None |
| resolved_at_creation = created.max_topic_size |
| |
| await iggy_client.update_topic( |
| stream_id=stream_name, |
| topic_id=topic_name, |
| name=topic_name, |
| max_topic_size=max_topic_size, |
| ) |
| |
| topic = await iggy_client.get_topic(stream_name, topic_name) |
| assert topic is not None |
| assert topic.name == topic_name |
| if expected_kind == "server_default": |
| # Every setting rides the options block and 0 is its "resolve the |
| # default" sentinel, so a ServerDefault update carries no key at all |
| # and the topic keeps the value admission resolved when it was |
| # created. Resetting a setting back to the node default is |
| # deliberately not expressible. |
| assert isinstance(topic.max_topic_size, type(resolved_at_creation)) |
| assert not isinstance(topic.max_topic_size, MaxTopicSize.ServerDefault) |
| elif expected_kind == "unlimited": |
| assert isinstance(topic.max_topic_size, MaxTopicSize.Unlimited) |
| else: |
| assert isinstance(topic.max_topic_size, MaxTopicSize.Custom) |
| assert isinstance(max_topic_size, MaxTopicSize.Custom) |
| assert topic.max_topic_size.bytes == max_topic_size.bytes |
| |
| @pytest.mark.asyncio |
| async def test_update_topic_applies_repeated_updates( |
| self, iggy_client: IggyClient, unique_name |
| ): |
| """Test successive update_topic calls each take effect.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| first_rename = unique_name() |
| second_rename = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_name, partitions_count=1 |
| ) |
| |
| await iggy_client.update_topic( |
| stream_id=stream_name, topic_id=topic_name, name=first_rename |
| ) |
| after_first = await iggy_client.get_topic(stream_name, first_rename) |
| assert after_first is not None |
| assert after_first.name == first_rename |
| assert await iggy_client.get_topic(stream_name, topic_name) is None |
| |
| await iggy_client.update_topic( |
| stream_id=stream_name, topic_id=first_rename, name=second_rename |
| ) |
| after_second = await iggy_client.get_topic(stream_name, second_rename) |
| assert after_second is not None |
| assert after_second.name == second_rename |
| assert await iggy_client.get_topic(stream_name, first_rename) is None |
| |
| @pytest.mark.asyncio |
| async def test_update_nonexistent_topic_fails( |
| self, iggy_client: IggyClient, unique_name |
| ): |
| """Test update_topic raises for a non-existent topic.""" |
| stream_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| |
| with pytest.raises(RuntimeError): |
| await iggy_client.update_topic( |
| stream_id=stream_name, topic_id=unique_name(), name=unique_name() |
| ) |
| |
| @pytest.mark.asyncio |
| async def test_update_topic_in_nonexistent_stream_fails( |
| self, iggy_client: IggyClient, unique_name |
| ): |
| """Test update_topic raises when the stream does not exist.""" |
| with pytest.raises(RuntimeError): |
| await iggy_client.update_topic( |
| stream_id=unique_name(), topic_id=unique_name(), name=unique_name() |
| ) |
| |
| @pytest.mark.asyncio |
| async def test_update_topic_to_existing_name_fails( |
| self, iggy_client: IggyClient, unique_name |
| ): |
| """Test update_topic rejects renaming a topic to a name already in use.""" |
| stream_name = unique_name() |
| first_topic = unique_name() |
| second_topic = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, name=first_topic, partitions_count=1 |
| ) |
| await iggy_client.create_topic( |
| stream=stream_name, name=second_topic, partitions_count=1 |
| ) |
| |
| with pytest.raises(RuntimeError): |
| await iggy_client.update_topic( |
| stream_id=stream_name, topic_id=second_topic, name=first_topic |
| ) |
| |
| @pytest.mark.asyncio |
| async def test_update_topic_requires_connection_and_auth(self, unique_name): |
| """Test update_topic fails both before connecting and before logging in.""" |
| host, port = get_server_config() |
| wait_for_server(host, port) |
| |
| client = IggyClient(f"{host}:{port}") |
| with pytest.raises(RuntimeError): |
| await client.update_topic( |
| stream_id=unique_name(), topic_id=unique_name(), name=unique_name() |
| ) |
| |
| await client.connect() |
| with pytest.raises(RuntimeError): |
| await client.update_topic( |
| stream_id=unique_name(), topic_id=unique_name(), name=unique_name() |
| ) |
| |
| |
| class TestDeleteTopic: |
| """Test deleting topics via delete_topic.""" |
| |
| @pytest.mark.asyncio |
| async def test_delete_topic_removes_topic( |
| self, iggy_client: IggyClient, unique_name |
| ): |
| """Test delete_topic removes the topic and drops the stream count.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_name, partitions_count=1 |
| ) |
| |
| await iggy_client.delete_topic(stream_name, topic_name) |
| |
| assert await iggy_client.get_topic(stream_name, topic_name) is None |
| assert await iggy_client.get_topics(stream_name) == [] |
| |
| stream = await iggy_client.get_stream(stream_name) |
| assert stream is not None |
| assert stream.topics_count == 0 |
| |
| @pytest.mark.asyncio |
| async def test_delete_topic_by_numeric_id( |
| self, iggy_client: IggyClient, unique_name |
| ): |
| """Test delete_topic accepts a numeric topic id.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_name, partitions_count=1 |
| ) |
| topic = await iggy_client.get_topic(stream_name, topic_name) |
| assert topic is not None |
| |
| await iggy_client.delete_topic(stream_name, topic.id) |
| |
| assert await iggy_client.get_topic(stream_name, topic_name) is None |
| |
| @pytest.mark.asyncio |
| async def test_delete_topic_leaves_other_topics( |
| self, iggy_client: IggyClient, unique_name |
| ): |
| """Test delete_topic removes only the targeted topic.""" |
| stream_name = unique_name() |
| topic_to_delete = unique_name() |
| topic_to_keep = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_to_delete, partitions_count=1 |
| ) |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_to_keep, partitions_count=1 |
| ) |
| |
| await iggy_client.delete_topic(stream_name, topic_to_delete) |
| |
| remaining = await iggy_client.get_topics(stream_name) |
| assert len(remaining) == 1 |
| assert remaining[0].name == topic_to_keep |
| |
| @pytest.mark.asyncio |
| async def test_delete_nonexistent_topic_fails( |
| self, iggy_client: IggyClient, unique_name |
| ): |
| """Test delete_topic raises for a non-existent topic.""" |
| stream_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| |
| with pytest.raises(RuntimeError): |
| await iggy_client.delete_topic(stream_name, unique_name()) |
| |
| @pytest.mark.asyncio |
| async def test_delete_topic_twice_fails_second_time( |
| self, iggy_client: IggyClient, unique_name |
| ): |
| """Test deleting an already-deleted topic raises on the second call.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_name, partitions_count=1 |
| ) |
| |
| await iggy_client.delete_topic(stream_name, topic_name) |
| with pytest.raises(RuntimeError): |
| await iggy_client.delete_topic(stream_name, topic_name) |
| |
| @pytest.mark.asyncio |
| async def test_delete_topic_requires_connection_and_auth(self, unique_name): |
| """Test delete_topic fails both before connecting and before logging in.""" |
| host, port = get_server_config() |
| wait_for_server(host, port) |
| |
| client = IggyClient(f"{host}:{port}") |
| with pytest.raises(RuntimeError): |
| await client.delete_topic(unique_name(), unique_name()) |
| |
| await client.connect() |
| with pytest.raises(RuntimeError): |
| await client.delete_topic(unique_name(), unique_name()) |
| |
| |
| class TestPurgeTopic: |
| """Test purging topic messages via purge_topic.""" |
| |
| @pytest.mark.asyncio |
| async def test_purge_topic_clears_messages_but_keeps_topic( |
| self, iggy_client: IggyClient, unique_name |
| ): |
| """Test purge_topic empties the topic while leaving it in place.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_name, partitions_count=1 |
| ) |
| |
| messages = [SendMessage(f"payload-{index}") for index in range(5)] |
| await iggy_client.send_messages(stream_name, topic_name, 0, messages) |
| |
| before = await iggy_client.get_topic(stream_name, topic_name) |
| assert before is not None |
| assert before.messages_count == 5 |
| |
| await iggy_client.purge_topic(stream_name, topic_name) |
| |
| after = await iggy_client.get_topic(stream_name, topic_name) |
| assert after is not None |
| assert after.messages_count == 0 |
| assert after.size == 0 |
| # Purging clears messages and size only; topic config is unchanged. |
| assert after.id == before.id |
| assert after.name == before.name |
| assert after.created_at == before.created_at |
| assert after.partitions_count == before.partitions_count |
| assert after.compression_algorithm == before.compression_algorithm |
| assert isinstance(before.message_expiry, IggyExpiry.NeverExpire) |
| assert isinstance(after.message_expiry, IggyExpiry.NeverExpire) |
| assert isinstance(before.max_topic_size, MaxTopicSize.Unlimited) |
| assert isinstance(after.max_topic_size, MaxTopicSize.Unlimited) |
| |
| @pytest.mark.asyncio |
| async def test_purge_empty_topic_succeeds( |
| self, iggy_client: IggyClient, unique_name |
| ): |
| """Test purge_topic is a no-op on a topic with no messages.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_name, partitions_count=1 |
| ) |
| |
| await iggy_client.purge_topic(stream_name, topic_name) |
| |
| topic = await iggy_client.get_topic(stream_name, topic_name) |
| assert topic is not None |
| assert topic.messages_count == 0 |
| |
| @pytest.mark.asyncio |
| async def test_purge_topic_is_idempotent_when_called_repeatedly( |
| self, iggy_client: IggyClient, unique_name |
| ): |
| """Test purge_topic succeeds when called repeatedly on the same topic.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_name, partitions_count=1 |
| ) |
| |
| messages = [SendMessage(f"payload-{index}") for index in range(5)] |
| await iggy_client.send_messages(stream_name, topic_name, 0, messages) |
| |
| await iggy_client.purge_topic(stream_name, topic_name) |
| await iggy_client.purge_topic(stream_name, topic_name) |
| |
| topic = await iggy_client.get_topic(stream_name, topic_name) |
| assert topic is not None |
| assert topic.messages_count == 0 |
| assert topic.size == 0 |
| |
| @pytest.mark.asyncio |
| async def test_purge_nonexistent_topic_fails( |
| self, iggy_client: IggyClient, unique_name |
| ): |
| """Test purge_topic raises for a non-existent topic in an existing stream.""" |
| stream_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| |
| with pytest.raises(RuntimeError): |
| await iggy_client.purge_topic(stream_name, unique_name()) |
| |
| @pytest.mark.asyncio |
| async def test_purge_topic_in_nonexistent_stream_fails( |
| self, iggy_client: IggyClient, unique_name |
| ): |
| """Test purge_topic raises when the stream does not exist.""" |
| with pytest.raises(RuntimeError): |
| await iggy_client.purge_topic(unique_name(), unique_name()) |
| |
| @pytest.mark.asyncio |
| async def test_purge_topic_requires_connection_and_auth(self, unique_name): |
| """Test purge_topic fails both before connecting and before logging in.""" |
| host, port = get_server_config() |
| wait_for_server(host, port) |
| |
| client = IggyClient(f"{host}:{port}") |
| with pytest.raises(RuntimeError): |
| await client.purge_topic(unique_name(), unique_name()) |
| |
| await client.connect() |
| with pytest.raises(RuntimeError): |
| await client.purge_topic(unique_name(), unique_name()) |
| |
| |
| class TestTopicOptions: |
| """Tests for the option catalog and the options a topic reports.""" |
| |
| @pytest.mark.asyncio |
| async def test_topic_options_round_trip(self, iggy_client: IggyClient, unique_name): |
| """Options a client sets come back readable, split by provenance.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, |
| name=topic_name, |
| partitions_count=1, |
| options={"enforce_fsync": "true", "segment_size": "128 MiB"}, |
| ) |
| |
| topic = await iggy_client.get_topic(stream_name, topic_name) |
| assert topic is not None |
| # Options come back through the same typed dict message user headers |
| # use, so the scalar helper reads them the same way. |
| explicit = topic.options.to_scalar_dict() |
| assert explicit["enforce_fsync"] is True |
| assert explicit["segment_size"] == 128 * 1024 * 1024 |
| # Keys the client left alone are resolved by admission and reported |
| # separately, so an operator can tell chosen from defaulted. |
| derived = topic.derived_options.to_scalar_dict() |
| assert "max_topic_size" in derived |
| assert "enforce_fsync" not in derived |
| |
| topics = await iggy_client.get_topics(stream_name) |
| listed = next(entry for entry in topics if entry.name == topic_name) |
| assert listed.options.to_scalar_dict()["enforce_fsync"] is True |
| |
| @pytest.mark.asyncio |
| async def test_update_topic_options_reach_the_server( |
| self, iggy_client: IggyClient, unique_name |
| ): |
| """Options passed to update_topic are applied, and gated by key.""" |
| stream_name = unique_name() |
| topic_name = unique_name() |
| |
| await iggy_client.create_stream(stream_name) |
| await iggy_client.create_topic( |
| stream=stream_name, name=topic_name, partitions_count=1 |
| ) |
| |
| await iggy_client.update_topic( |
| stream_id=stream_name, |
| topic_id=topic_name, |
| name=topic_name, |
| options={"compression_algorithm": "gzip"}, |
| ) |
| |
| topic = await iggy_client.get_topic(stream_name, topic_name) |
| assert topic is not None |
| assert topic.compression_algorithm == "gzip" |
| |
| # A create-time key is refused by name, so nothing re-pushes it to the |
| # partitions of a live topic. |
| with pytest.raises(RuntimeError): |
| await iggy_client.update_topic( |
| stream_id=stream_name, |
| topic_id=topic_name, |
| name=topic_name, |
| options={"segment_size": "2 MiB"}, |
| ) |
| |
| @pytest.mark.asyncio |
| async def test_describe_options_lists_the_topic_catalog( |
| self, iggy_client: IggyClient |
| ): |
| """The catalog is what tells a client which keys create accepts.""" |
| specs = await iggy_client.describe_options("topic") |
| |
| by_key = {spec.key: spec for spec in specs} |
| assert "segment_size" in by_key |
| assert "enforce_fsync" in by_key |
| segment_size = by_key["segment_size"] |
| assert segment_size.kind == "uint64" |
| # The default is the same HeaderValue type message headers carry, so it |
| # arrives as the variant matching the key's kind. |
| default = segment_size.default_value |
| assert isinstance(default, HeaderValue.UnsignedInt64) |
| assert default.value == 1024 * 1024 * 1024 |
| assert segment_size.description |
| |
| # Streams and users have no catalog keys yet. |
| assert await iggy_client.describe_options("stream") == [] |
| assert await iggy_client.describe_options("user") == [] |
| |
| @pytest.mark.asyncio |
| async def test_describe_options_rejects_an_unknown_scope( |
| self, iggy_client: IggyClient |
| ): |
| """Test describe_options raises ValueError for an unknown scope.""" |
| with pytest.raises(ValueError): |
| await iggy_client.describe_options("partition") |