blob: 193b3cb42aea109c00e68c8b23594a9088ac44fd [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.
import asyncio
import pytest
from apache_iggy import (
IggyClient,
Permissions,
SendMessage,
StreamPermissions,
TopicPermissions,
)
from .utils import login_fresh_client, unique_credentials
async def _create_topic(iggy_client: IggyClient, unique_name):
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=2
)
return stream_name, topic_name
async def _wait_for_messages(iggy_client, stream_id, topic_id, expected):
for _ in range(100):
topic = await iggy_client.get_topic(stream_id, topic_id)
if topic is not None and topic.messages_count == expected:
return topic
await asyncio.sleep(0.01)
raise AssertionError(f"topic messages_count did not reach {expected}")
class TestPartitionManagement:
@pytest.mark.asyncio
@pytest.mark.parametrize("numeric_ids", [False, True])
async def test_create_and_delete_partitions(
self, iggy_client: IggyClient, unique_name, numeric_ids: bool
):
stream_name, topic_name = await _create_topic(iggy_client, unique_name)
stream = await iggy_client.get_stream(stream_name)
assert stream is not None
topic = await iggy_client.get_topic(stream.id, topic_name)
assert topic is not None
stream_id = stream.id if numeric_ids else stream_name
topic_id = topic.id if numeric_ids else topic_name
await iggy_client.create_partitions(stream_id, topic_id, 2)
created = await iggy_client.get_topic(stream_id, topic_id)
assert created is not None
assert created.partitions_count == 4
assert [partition.id for partition in created.partitions] == [0, 1, 2, 3]
await iggy_client.delete_partitions(stream_id, topic_id, 2)
deleted = await iggy_client.get_topic(stream_id, topic_id)
assert deleted is not None
assert deleted.partitions_count == 2
assert [partition.id for partition in deleted.partitions] == [0, 1]
@pytest.mark.asyncio
async def test_delete_partitions_rolls_back_stats(
self, iggy_client: IggyClient, unique_name
):
stream_name, topic_name = await _create_topic(iggy_client, unique_name)
await iggy_client.create_partitions(stream_name, topic_name, 2)
await iggy_client.send_messages(
stream_name, topic_name, 0, [SendMessage("retained")]
)
await iggy_client.send_messages(
stream_name, topic_name, 3, [SendMessage("deleted")]
)
await _wait_for_messages(iggy_client, stream_name, topic_name, 2)
await iggy_client.delete_partitions(stream_name, topic_name, 2)
deleted = await _wait_for_messages(iggy_client, stream_name, topic_name, 1)
assert [partition.id for partition in deleted.partitions] == [0, 1]
assert deleted.partitions[0].messages_count == 1
@pytest.mark.asyncio
@pytest.mark.parametrize("method", ["create_partitions", "delete_partitions"])
@pytest.mark.parametrize("partitions_count", [0, 1001])
async def test_partition_management_rejects_invalid_count(
self,
iggy_client: IggyClient,
unique_name,
method: str,
partitions_count: int,
):
stream_name, topic_name = await _create_topic(iggy_client, unique_name)
# Zero shares the legacy TooManyPartitions code with an over-limit count.
with pytest.raises(RuntimeError, match="Too many partitions"):
await getattr(iggy_client, method)(
stream_name, topic_name, partitions_count
)
@pytest.mark.asyncio
async def test_delete_partitions_rejects_count_larger_than_topic(
self, iggy_client: IggyClient, unique_name
):
stream_name, topic_name = await _create_topic(iggy_client, unique_name)
with pytest.raises(RuntimeError, match="Invalid partitions count"):
await iggy_client.delete_partitions(stream_name, topic_name, 3)
@pytest.mark.asyncio
@pytest.mark.parametrize("method", ["create_partitions", "delete_partitions"])
@pytest.mark.parametrize("missing", ["stream", "topic"])
async def test_partition_management_rejects_missing_stream_or_topic(
self, iggy_client: IggyClient, unique_name, method: str, missing: str
):
stream_name, topic_name = await _create_topic(iggy_client, unique_name)
missing_name = unique_name()
stream_id = missing_name if missing == "stream" else stream_name
topic_id = missing_name if missing == "topic" else topic_name
with pytest.raises(RuntimeError, match=r"was not found\."):
await getattr(iggy_client, method)(stream_id, topic_id, 1)
@pytest.mark.asyncio
@pytest.mark.parametrize("method", ["create_partitions", "delete_partitions"])
async def test_partition_management_rejects_invalid_identifier(
self, iggy_client: IggyClient, unique_name, method: str
):
_, topic_name = await _create_topic(iggy_client, unique_name)
with pytest.raises(ValueError):
await getattr(iggy_client, method)("", topic_name, 1)
@pytest.mark.asyncio
@pytest.mark.parametrize("method", ["create_partitions", "delete_partitions"])
@pytest.mark.parametrize("partitions_count", [-1, 2**32])
async def test_partition_management_rejects_out_of_range_python_integer(
self,
iggy_client: IggyClient,
unique_name,
method: str,
partitions_count: int,
):
stream_name, topic_name = await _create_topic(iggy_client, unique_name)
with pytest.raises(OverflowError):
await getattr(iggy_client, method)(
stream_name, topic_name, partitions_count
)
@pytest.mark.asyncio
async def test_delete_partitions_accepts_deleting_all_partitions(
self, iggy_client: IggyClient, unique_name
):
stream_name, topic_name = await _create_topic(iggy_client, unique_name)
await iggy_client.delete_partitions(stream_name, topic_name, 2)
topic = await iggy_client.get_topic(stream_name, topic_name)
assert topic is not None
assert topic.partitions_count == 0
assert topic.partitions == []
@pytest.mark.asyncio
async def test_partition_management_requires_scoped_manage_topic(
self, iggy_client: IggyClient, unique_name
):
stream_name, topic_name = await _create_topic(iggy_client, unique_name)
other_topic_name = unique_name()
await iggy_client.create_topic(
stream_name, other_topic_name, partitions_count=2
)
stream = await iggy_client.get_stream(stream_name)
assert stream is not None
topic = await iggy_client.get_topic(stream.id, topic_name)
other_topic = await iggy_client.get_topic(stream.id, other_topic_name)
assert topic is not None
assert other_topic is not None
denied_username, denied_password = unique_credentials(unique_name)
denied_user = await iggy_client.create_user(denied_username, denied_password)
denied = await login_fresh_client(denied_username, denied_password)
for method in ("create_partitions", "delete_partitions"):
with pytest.raises(RuntimeError, match="Unauthorized"):
await getattr(denied, method)(stream.id, topic.id, 1)
unchanged = await iggy_client.get_topic(stream.id, topic.id)
assert unchanged is not None
assert unchanged.partitions_count == 2
allowed_username, allowed_password = unique_credentials(unique_name)
allowed_user = await iggy_client.create_user(
allowed_username,
allowed_password,
permissions=Permissions(
streams={
stream.id: StreamPermissions(
topics={topic.id: TopicPermissions(manage_topic=True)}
)
}
),
)
allowed = await login_fresh_client(allowed_username, allowed_password)
await allowed.create_partitions(stream.id, topic.id, 1)
await allowed.delete_partitions(stream.id, topic.id, 1)
for method in ("create_partitions", "delete_partitions"):
with pytest.raises(RuntimeError, match="Unauthorized"):
await getattr(allowed, method)(stream.id, other_topic.id, 1)
scoped = await iggy_client.get_topic(stream.id, topic.id)
untouched = await iggy_client.get_topic(stream.id, other_topic.id)
assert scoped is not None and scoped.partitions_count == 2
assert untouched is not None and untouched.partitions_count == 2
await iggy_client.delete_user(denied_user.id)
await iggy_client.delete_user(allowed_user.id)