26db4ac44d
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
389 lines
13 KiB
Python
Executable File
389 lines
13 KiB
Python
Executable File
"""通用分页器单元测试."""
|
||
|
||
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
|