"""通用分页器单元测试.""" from __future__ import annotations import pytest from pydantic import ValidationError from packages.application.common.pagination import ( PaginatedResponse, PaginationMeta, PaginationParams, paginate, ) class TestPaginationParams: """PaginationParams 测试""" def test_default_values(self): """默认值正确""" params = PaginationParams() assert params.page == 1 assert params.page_size == 20 def test_offset_first_page(self): """第一页 offset 为 0""" params = PaginationParams(page=1, page_size=20) assert params.offset == 0 def test_offset_second_page(self): """第二页 offset 计算正确""" params = PaginationParams(page=2, page_size=20) assert params.offset == 20 def test_offset_custom_page_size(self): """自定义 page_size 的 offset""" params = PaginationParams(page=3, page_size=10) assert params.offset == 20 def test_limit_equals_page_size(self): """limit 等于 page_size""" params = PaginationParams(page_size=50) assert params.limit == 50 def test_page_must_be_at_least_1(self): """page 不能小于 1""" with pytest.raises(ValidationError): PaginationParams(page=0) def test_page_negative_raises(self): """page 不能为负数""" with pytest.raises(ValidationError): PaginationParams(page=-1) def test_page_size_must_be_at_least_1(self): """page_size 不能小于 1""" with pytest.raises(ValidationError): PaginationParams(page_size=0) def test_page_size_max_100(self): """page_size 最大 100""" with pytest.raises(ValidationError): PaginationParams(page_size=101) def test_page_size_100_is_valid(self): """page_size=100 是合法的""" params = PaginationParams(page_size=100) assert params.page_size == 100 class TestPaginationMeta: """PaginationMeta 测试""" def test_from_params_first_page(self): """第一页元数据""" params = PaginationParams(page=1, page_size=10) meta = PaginationMeta.from_params(params, total=25) assert meta.page == 1 assert meta.page_size == 10 assert meta.total == 25 assert meta.total_pages == 3 assert meta.has_next is True assert meta.has_prev is False def test_from_params_last_page(self): """最后一页元数据""" params = PaginationParams(page=3, page_size=10) meta = PaginationMeta.from_params(params, total=25) assert meta.page == 3 assert meta.total_pages == 3 assert meta.has_next is False assert meta.has_prev is True def test_from_params_middle_page(self): """中间页元数据""" params = PaginationParams(page=2, page_size=10) meta = PaginationMeta.from_params(params, total=50) assert meta.page == 2 assert meta.total_pages == 5 assert meta.has_next is True assert meta.has_prev is True def test_from_params_zero_total(self): """总数为 0 时""" 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_from_params_exact_multiple(self): """总数刚好是 page_size 的整数倍""" params = PaginationParams(page=1, page_size=10) meta = PaginationMeta.from_params(params, total=30) assert meta.total_pages == 3 def test_from_params_single_page(self): """单页即可放下所有数据""" params = PaginationParams(page=1, page_size=100) meta = PaginationMeta.from_params(params, total=50) assert meta.total_pages == 1 assert meta.has_next is False assert meta.has_prev is False class TestPaginatedResponse: """PaginatedResponse 测试""" def test_create_success(self): """创建分页响应""" params = PaginationParams(page=1, page_size=10) data = [1, 2, 3] response = PaginatedResponse.create(data, params, total=25) assert response.data == [1, 2, 3] assert response.pagination.page == 1 assert response.pagination.total == 25 assert response.pagination.total_pages == 3 def test_create_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 class TestPaginateFunction: """paginate 函数测试(内存分页)""" def test_first_page(self): """第一页分页""" items = list(range(30)) params = PaginationParams(page=1, page_size=10) result = paginate(items, params) assert result.data == list(range(10)) assert result.pagination.total == 30 assert result.pagination.total_pages == 3 assert result.pagination.has_next is True assert result.pagination.has_prev is False def test_second_page(self): """第二页分页""" items = list(range(30)) params = PaginationParams(page=2, page_size=10) result = paginate(items, params) assert result.data == list(range(10, 20)) assert result.pagination.page == 2 def test_last_page(self): """最后一页分页""" items = list(range(25)) params = PaginationParams(page=3, page_size=10) result = paginate(items, params) assert result.data == list(range(20, 25)) assert len(result.data) == 5 assert result.pagination.has_next is False 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_page_beyond_total(self): """页码超出总数""" items = list(range(5)) params = PaginationParams(page=10, page_size=10) result = paginate(items, params) assert result.data == [] assert result.pagination.total == 5 assert result.pagination.total_pages == 1 def test_custom_page_size(self): """自定义每页数量""" items = list(range(100)) params = PaginationParams(page=1, page_size=50) result = paginate(items, params) assert len(result.data) == 50 assert result.pagination.total_pages == 2 def test_single_item(self): """单条数据""" items = ["only_one"] params = PaginationParams(page=1, page_size=10) result = paginate(items, params) assert result.data == ["only_one"] assert result.pagination.total == 1 assert result.pagination.total_pages == 1 def test_generic_type_preserved(self): """泛型类型数据正确""" items = [{"id": 1, "name": "a"}, {"id": 2, "name": "b"}] params = PaginationParams(page=1, page_size=10) result = paginate(items, params) assert len(result.data) == 2 assert result.data[0]["id"] == 1 # ── PaginationParams 补充边界 ─────────────────────────────────────────────── class TestPaginationParamsEdgeCases: """PaginationParams 补充边界场景.""" def test_page_size_1_minimum(self): """page_size=1 是允许的最小值.""" params = PaginationParams(page_size=1) assert params.page_size == 1 assert params.limit == 1 def test_page_size_100_maximum(self): """page_size=100 是允许的最大值.""" params = PaginationParams(page_size=100) assert params.page_size == 100 def test_offset_page_1_size_100(self): """第1页每页100条 offset=0.""" params = PaginationParams(page=1, page_size=100) assert params.offset == 0 def test_offset_page_100_size_100(self): """第100页每页100条 offset=9900.""" params = PaginationParams(page=100, page_size=100) assert params.offset == 9900 def test_large_page_number_accepted(self): """极大页码(超过实际页数)允许.""" params = PaginationParams(page=999999, page_size=20) assert params.page == 999999 assert params.offset == (999999 - 1) * 20 # ── PaginationMeta 补充边界 ───────────────────────────────────────────────── class TestPaginationMetaEdgeCases: """PaginationMeta 补充边界场景.""" def test_total_0_page_1(self): """total=0, page=1 时 total_pages=0, 无上下页.""" params = PaginationParams(page=1, page_size=20) meta = PaginationMeta.from_params(params, total=0) assert meta.total_pages == 0 assert meta.has_next is False assert meta.has_prev is False def test_total_0_page_beyond(self): """total=0, page>1 时 has_prev=True(因为page>1).""" params = PaginationParams(page=3, page_size=20) meta = PaginationMeta.from_params(params, total=0) assert meta.total_pages == 0 assert meta.has_next is False assert meta.has_prev is True def test_exact_last_page(self): """刚好是最后一页时 has_next=False.""" params = PaginationParams(page=5, page_size=10) meta = PaginationMeta.from_params(params, total=50) assert meta.total_pages == 5 assert meta.has_next is False assert meta.has_prev is True def test_one_more_than_exact(self): """比整数页多1条时总页数+1.""" params = PaginationParams(page=1, page_size=10) meta = PaginationMeta.from_params(params, total=51) assert meta.total_pages == 6 def test_page_exactly_total_pages(self): """page == total_pages 时 has_next=False.""" params = PaginationParams(page=3, page_size=10) meta = PaginationMeta.from_params(params, total=30) assert meta.has_next is False def test_total_1_page_1_size_1(self): """1条数据1页.""" params = PaginationParams(page=1, page_size=1) meta = PaginationMeta.from_params(params, total=1) assert meta.total_pages == 1 assert meta.has_next is False assert meta.has_prev is False # ── paginate 补充边界 ────────────────────────────────────────────────────── class TestPaginateEdgeCases: """paginate 补充边界场景.""" def test_single_item_list(self): """单元素列表.""" result = paginate([42], PaginationParams(page=1, page_size=10)) assert result.data == [42] assert result.pagination.total == 1 assert result.pagination.total_pages == 1 def test_page_exactly_last(self): """刚好在最后一页.""" items = list(range(25)) result = paginate(items, PaginationParams(page=3, page_size=10)) assert result.data == list(range(20, 25)) assert result.pagination.has_next is False def test_page_past_end_returns_empty(self): """页码超过总数返回空.""" items = list(range(5)) result = paginate(items, PaginationParams(page=10, page_size=10)) assert result.data == [] assert result.pagination.total == 5 def test_empty_list_page_1(self): """空列表第1页.""" result = paginate([], PaginationParams(page=1, page_size=10)) assert result.data == [] assert result.pagination.total == 0 assert result.pagination.total_pages == 0 def test_page_size_1_iterates_all(self): """page_size=1 时每页1条.""" items = ["a", "b", "c"] r1 = paginate(items, PaginationParams(page=1, page_size=1)) r2 = paginate(items, PaginationParams(page=2, page_size=1)) r3 = paginate(items, PaginationParams(page=3, page_size=1)) assert r1.data == ["a"] assert r2.data == ["b"] assert r3.data == ["c"] def test_does_not_mutate_input(self): """不修改输入列表.""" items = [1, 2, 3, 4, 5] original = items[:] paginate(items, PaginationParams(page=1, page_size=2)) assert items == original def test_page_size_greater_than_total(self): """每页条数大于总数.""" items = list(range(5)) result = paginate(items, PaginationParams(page=1, page_size=100)) assert result.data == items assert result.pagination.total_pages == 1