diff --git a/tests/unit/test_pagination.py b/tests/unit/test_pagination.py new file mode 100755 index 000000000..ae2b2fd14 --- /dev/null +++ b/tests/unit/test_pagination.py @@ -0,0 +1,338 @@ +""" +pagination 通用分页器单元测试 + +覆盖: +- PaginationParams: 默认值/边界/校验/offset/limit +- PaginationMeta: from_params 各种边界场景 +- PaginatedResponse: create 工厂方法 +- paginate: 内存分页函数 +""" + +import pytest +from pydantic import ValidationError + +from packages.application.common.pagination import ( + PaginationParams, + PaginationMeta, + PaginatedResponse, + paginate, +) + + +# ============================================================ +# PaginationParams +# ============================================================ + + +class TestPaginationParamsDefaults: + """默认值测试""" + + def test_default_page_is_1(self): + params = PaginationParams() + assert params.page == 1 + + def test_default_page_size_is_20(self): + params = PaginationParams() + assert params.page_size == 20 + + def test_default_offset_is_0(self): + params = PaginationParams() + assert params.offset == 0 + + def test_default_limit_is_20(self): + params = PaginationParams() + assert params.limit == 20 + + +class TestPaginationParamsValidation: + """参数校验""" + + @pytest.mark.parametrize("page", [1, 2, 100, 9999]) + def test_valid_page_values(self, page): + params = PaginationParams(page=page) + assert params.page == page + + def test_page_zero_raises(self): + with pytest.raises(ValidationError): + PaginationParams(page=0) + + def test_page_negative_raises(self): + with pytest.raises(ValidationError): + PaginationParams(page=-1) + + @pytest.mark.parametrize("page_size", [1, 20, 50, 100]) + def test_valid_page_size_values(self, page_size): + params = PaginationParams(page_size=page_size) + assert params.page_size == page_size + + def test_page_size_zero_raises(self): + with pytest.raises(ValidationError): + PaginationParams(page_size=0) + + def test_page_size_negative_raises(self): + with pytest.raises(ValidationError): + PaginationParams(page_size=-5) + + def test_page_size_over_100_raises(self): + with pytest.raises(ValidationError): + PaginationParams(page_size=101) + + def test_invalid_page_type_raises(self): + with pytest.raises(ValidationError): + PaginationParams(page="abc") + + def test_invalid_page_size_type_raises(self): + with pytest.raises(ValidationError): + PaginationParams(page_size="abc") + + +class TestPaginationParamsOffset: + """offset 属性计算""" + + def test_page_1_offset_0(self): + params = PaginationParams(page=1, page_size=20) + assert params.offset == 0 + + def test_page_2_offset_page_size(self): + params = PaginationParams(page=2, page_size=20) + assert params.offset == 20 + + def test_page_3_offset_2x_page_size(self): + params = PaginationParams(page=3, page_size=20) + assert params.offset == 40 + + def test_page_5_page_size_10_offset_40(self): + params = PaginationParams(page=5, page_size=10) + assert params.offset == 40 + + def test_page_1_page_size_100_offset_0(self): + params = PaginationParams(page=1, page_size=100) + assert params.offset == 0 + + +class TestPaginationParamsLimit: + """limit 属性""" + + def test_limit_equals_page_size(self): + params = PaginationParams(page_size=20) + assert params.limit == 20 + + def test_limit_1(self): + params = PaginationParams(page_size=1) + assert params.limit == 1 + + def test_limit_100(self): + params = PaginationParams(page_size=100) + assert params.limit == 100 + + +# ============================================================ +# PaginationMeta.from_params +# ============================================================ + + +class TestPaginationMetaFromParams: + """from_params 工厂方法""" + + def test_empty_total_zero(self): + params = PaginationParams(page=1, page_size=20) + meta = PaginationMeta.from_params(params, total=0) + assert meta.total == 0 + assert meta.total_pages == 0 + assert meta.has_next is False + assert meta.has_prev is False + + def test_exactly_one_page(self): + params = PaginationParams(page=1, page_size=20) + meta = PaginationMeta.from_params(params, total=20) + assert meta.total_pages == 1 + assert meta.has_next is False + assert meta.has_prev is False + + def test_less_than_one_page(self): + params = PaginationParams(page=1, page_size=20) + meta = PaginationMeta.from_params(params, total=15) + assert meta.total_pages == 1 + assert meta.has_next is False + assert meta.has_prev is False + + def test_multiple_pages_first_page(self): + params = PaginationParams(page=1, page_size=20) + meta = PaginationMeta.from_params(params, total=50) + assert meta.total_pages == 3 + assert meta.has_next is True + assert meta.has_prev is False + + def test_multiple_pages_middle_page(self): + params = PaginationParams(page=2, page_size=20) + meta = PaginationMeta.from_params(params, total=50) + assert meta.total_pages == 3 + assert meta.has_next is True + assert meta.has_prev is True + + def test_multiple_pages_last_page(self): + params = PaginationParams(page=3, page_size=20) + meta = PaginationMeta.from_params(params, total=50) + assert meta.total_pages == 3 + assert meta.has_next is False + assert meta.has_prev is True + + def test_exact_division(self): + params = PaginationParams(page=2, page_size=20) + meta = PaginationMeta.from_params(params, total=40) + assert meta.total_pages == 2 + assert meta.has_next is False + assert meta.has_prev is True + + def test_non_exact_division_ceil(self): + params = PaginationParams(page=1, page_size=20) + meta = PaginationMeta.from_params(params, total=41) + assert meta.total_pages == 3 + + def test_total_1_page_size_20(self): + params = PaginationParams(page=1, page_size=20) + meta = PaginationMeta.from_params(params, total=1) + assert meta.total_pages == 1 + assert meta.has_next is False + assert meta.has_prev is False + + def test_page_beyond_total_pages(self): + params = PaginationParams(page=10, page_size=20) + meta = PaginationMeta.from_params(params, total=50) + assert meta.total_pages == 3 + assert meta.has_next is False + assert meta.has_prev is True + + def test_preserves_params_values(self): + params = PaginationParams(page=3, page_size=15) + meta = PaginationMeta.from_params(params, total=100) + assert meta.page == 3 + assert meta.page_size == 15 + assert meta.total == 100 + + +# ============================================================ +# PaginatedResponse.create +# ============================================================ + + +class TestPaginatedResponseCreate: + """create 工厂方法""" + + def test_create_with_data(self): + params = PaginationParams(page=1, page_size=20) + data = [1, 2, 3] + response = PaginatedResponse.create(data, params, total=100) + assert response.data == data + assert response.pagination.total == 100 + assert response.pagination.page == 1 + assert response.pagination.page_size == 20 + + def test_create_with_empty_data(self): + params = PaginationParams(page=1, page_size=20) + response = PaginatedResponse.create([], params, total=0) + assert response.data == [] + assert response.pagination.total == 0 + assert response.pagination.total_pages == 0 + + def test_create_preserves_list_type(self): + params = PaginationParams(page=1, page_size=20) + data = ["a", "b", "c"] + response = PaginatedResponse.create(data, params, total=10) + assert response.data == ["a", "b", "c"] + assert len(response.data) == 3 + + +# ============================================================ +# paginate 函数 +# ============================================================ + + +class TestPaginateFunction: + """内存分页函数""" + + def test_empty_list(self): + params = PaginationParams(page=1, page_size=20) + result = paginate([], params) + assert result.data == [] + assert result.pagination.total == 0 + assert result.pagination.total_pages == 0 + + def test_first_page(self): + items = list(range(50)) + params = PaginationParams(page=1, page_size=20) + result = paginate(items, params) + assert result.data == list(range(20)) + assert result.pagination.total == 50 + assert result.pagination.total_pages == 3 + assert result.pagination.has_next is True + assert result.pagination.has_prev is False + + def test_middle_page(self): + items = list(range(50)) + params = PaginationParams(page=2, page_size=20) + result = paginate(items, params) + assert result.data == list(range(20, 40)) + assert result.pagination.has_next is True + assert result.pagination.has_prev is True + + def test_last_page(self): + items = list(range(50)) + params = PaginationParams(page=3, page_size=20) + result = paginate(items, params) + assert result.data == list(range(40, 50)) + assert len(result.data) == 10 + assert result.pagination.has_next is False + assert result.pagination.has_prev is True + + def test_page_beyond_total(self): + items = list(range(25)) + params = PaginationParams(page=10, page_size=20) + result = paginate(items, params) + assert result.data == [] + assert result.pagination.total == 25 + assert result.pagination.total_pages == 2 + + def test_page_size_larger_than_total(self): + items = list(range(5)) + params = PaginationParams(page=1, page_size=20) + result = paginate(items, params) + assert result.data == items + assert result.pagination.total_pages == 1 + assert result.pagination.has_next is False + + def test_single_item(self): + items = [42] + params = PaginationParams(page=1, page_size=20) + result = paginate(items, params) + assert result.data == [42] + assert result.pagination.total == 1 + + def test_page_size_1(self): + items = list(range(5)) + params = PaginationParams(page=3, page_size=1) + result = paginate(items, params) + assert result.data == [2] + assert result.pagination.total_pages == 5 + + def test_exact_page_size(self): + items = list(range(40)) + params = PaginationParams(page=2, page_size=20) + result = paginate(items, params) + assert result.data == list(range(20, 40)) + assert result.pagination.total_pages == 2 + assert result.pagination.has_next is False + + def test_string_items(self): + items = ["a", "b", "c", "d", "e"] + params = PaginationParams(page=2, page_size=2) + result = paginate(items, params) + assert result.data == ["c", "d"] + assert result.pagination.total == 5 + + def test_does_not_mutate_original_list(self): + items = list(range(10)) + original = items.copy() + params = PaginationParams(page=1, page_size=3) + paginate(items, params) + assert items == original