5f17fd5faf
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
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
338 lines
11 KiB
Python
Executable File
338 lines
11 KiB
Python
Executable File
"""
|
|
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
|