""" pagination 通用分页器单元测试 覆盖: - PaginationParams: 默认值/边界/校验/offset/limit - PaginationMeta: from_params 各种边界场景 - PaginatedResponse: create 工厂方法 - paginate: 内存分页函数 """ import pytest from pydantic import ValidationError from packages.application.common.pagination import ( PaginatedResponse, PaginationMeta, PaginationParams, 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