blob: 76f60b0a44f9d9c449100b449e43de4332d29b1d [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.
from datetime import timedelta
import pytest
from apache_iggy import IggyClient
from .utils import get_server_config, wait_for_ping, wait_for_server
class TestTopicOperations:
"""Test topic creation, retrieval, and management."""
@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
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_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_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_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_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",
[
timedelta(0), # value for server default message expiry
timedelta(microseconds=1),
timedelta(seconds=1),
timedelta(minutes=10),
timedelta(days=1, seconds=2, microseconds=3),
],
)
async def test_create_topic_with_message_expiry(
self, iggy_client: IggyClient, unique_name, message_expiry: timedelta
):
"""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
@pytest.mark.asyncio
@pytest.mark.parametrize("invalid_message_expiry", [1, "1s", object()])
async def test_create_topic_invalid_message_expiry(
self, iggy_client: IggyClient, unique_name, invalid_message_expiry
):
"""Test create_topic rejects message_expiry values that are not timedeltas."""
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",
[
0, # value for server default max topic size
2**64 - 1,
2_000_000_000,
],
)
async def test_create_topic_with_valid_max_topic_size(
self, iggy_client: IggyClient, unique_name, max_topic_size: int
):
"""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
@pytest.mark.asyncio
@pytest.mark.parametrize(
("max_topic_size", "expected_exception"),
[
(4563, RuntimeError),
(-1, OverflowError),
(2e64, TypeError),
],
)
async def test_create_topic_invalid_max_topic_size(
self,
iggy_client: IggyClient,
unique_name,
max_topic_size,
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=max_topic_size,
)
@pytest.mark.asyncio
@pytest.mark.parametrize(
"replication_factor",
[
0, # value for server default replication factor
1,
42,
255,
],
)
async def test_create_topic_with_valid_replication_factor(
self, iggy_client: IggyClient, unique_name, replication_factor: int
):
"""Test create_topic accepts a supported replication factor."""
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,
replication_factor=replication_factor,
)
topic = await iggy_client.get_topic(stream_name, topic_name)
assert topic is not None
assert topic.name == topic_name
@pytest.mark.asyncio
@pytest.mark.parametrize(
("replication_factor", "expected_exception"),
[
(-1, OverflowError),
(256, OverflowError),
("1", TypeError),
(1.0, TypeError),
],
)
async def test_create_topic_invalid_replication_factor(
self,
iggy_client: IggyClient,
unique_name,
replication_factor,
expected_exception,
):
"""Test create_topic rejects invalid replication factor 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,
replication_factor=replication_factor,
)
@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_get_topic_before_connect_fails(self, unique_name):
"""Test get_topic requires an established connection."""
host, port = get_server_config()
client = IggyClient(f"{host}:{port}")
with pytest.raises(RuntimeError):
await client.get_topic(unique_name(), unique_name())
@pytest.mark.asyncio
async def test_get_topic_before_login_fails(self, unique_name):
"""Test get_topic requires authentication."""
host, port = get_server_config()
wait_for_server(host, port)
client = IggyClient(f"{host}:{port}")
await client.connect()
with pytest.raises(RuntimeError):
await client.get_topic(unique_name(), unique_name())
@pytest.mark.asyncio
async def test_create_topic_before_connect_fails(self, unique_name):
"""Test create_topic requires an established connection."""
host, port = get_server_config()
client = IggyClient(f"{host}:{port}")
with pytest.raises(RuntimeError):
await client.create_topic(
stream=unique_name(), name=unique_name(), partitions_count=1
)
@pytest.mark.asyncio
async def test_create_topic_before_login_fails(self, unique_name):
"""Test create_topic requires authentication."""
host, port = get_server_config()
wait_for_server(host, port)
client = IggyClient(f"{host}:{port}")
await client.connect()
with pytest.raises(RuntimeError):
await client.create_topic(
stream=unique_name(), name=unique_name(), partitions_count=1
)