GET /projects 与 GET /templates 原一次返回全部 owner 行,行数随数据线性增长。 新增可复用分页依赖 ww_api.pagination.get_page(limit∈[1,200] 默认 50、offset≥0, 越界 → 422),两端点接入,Repository.list_for_owner 加 keyword-only limit/offset 把 LIMIT/OFFSET 下推 DB。向后兼容:不传参返回第一页。补 projects 分页 + 边界 422 测试。
150 lines
5.0 KiB
Python
150 lines
5.0 KiB
Python
"""F3b GET/POST/DELETE /templates 端点(内存替身,无 DB/无网络)。
|
||
|
||
覆盖:
|
||
- POST 201 + 回显 + 端点 commit;GET 列出;DELETE 204 + commit;
|
||
- 空 title → 422、空 body → 422(schema `min_length=1`,FastAPI 422);
|
||
- 删不存在 → 404(repo.delete 返 False → AppError NOT_FOUND)。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import uuid
|
||
|
||
import httpx
|
||
import pytest
|
||
from cryptography.fernet import Fernet
|
||
from fakes_projects import FakeSession
|
||
from ww_core.domain.template_repo import TemplateCreate, TemplateView
|
||
|
||
|
||
class _FakeTemplateRepo:
|
||
def __init__(self) -> None:
|
||
self.rows: dict[uuid.UUID, TemplateView] = {}
|
||
|
||
async def create(self, owner_id: uuid.UUID, data: TemplateCreate) -> TemplateView:
|
||
view = TemplateView(
|
||
id=uuid.uuid4(),
|
||
title=data.title,
|
||
body=data.body,
|
||
category=data.category,
|
||
tool_key=data.tool_key,
|
||
)
|
||
self.rows[view.id] = view
|
||
return view
|
||
|
||
async def list_for_owner(
|
||
self, owner_id: uuid.UUID, *, limit: int | None = None, offset: int = 0
|
||
) -> list[TemplateView]:
|
||
views = list(self.rows.values())
|
||
if limit is None:
|
||
return views
|
||
return views[offset : offset + limit]
|
||
|
||
async def delete(self, owner_id: uuid.UUID, template_id: uuid.UUID) -> bool:
|
||
if template_id in self.rows:
|
||
del self.rows[template_id]
|
||
return True
|
||
return False
|
||
|
||
|
||
def _make_client() -> tuple[httpx.AsyncClient, _FakeTemplateRepo, FakeSession]:
|
||
os.environ.setdefault("CREDENTIAL_ENC_KEY", Fernet.generate_key().decode())
|
||
from ww_api.main import create_app
|
||
from ww_api.services.project_deps import get_template_repo
|
||
from ww_db import get_session
|
||
|
||
repo = _FakeTemplateRepo()
|
||
session = FakeSession()
|
||
|
||
app = create_app()
|
||
app.dependency_overrides[get_template_repo] = lambda: repo
|
||
app.dependency_overrides[get_session] = lambda: session
|
||
transport = httpx.ASGITransport(app=app)
|
||
client = httpx.AsyncClient(transport=transport, base_url="http://test")
|
||
return client, repo, session
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_create_template_returns_201_and_commits() -> None:
|
||
client, repo, session = _make_client()
|
||
async with client:
|
||
resp = await client.post(
|
||
"/templates",
|
||
json={"title": "爽文开局", "body": "主角穿越...", "tool_key": "expand"},
|
||
)
|
||
assert resp.status_code == 201
|
||
body = resp.json()
|
||
assert body["title"] == "爽文开局"
|
||
assert body["body"] == "主角穿越..."
|
||
assert body["tool_key"] == "expand"
|
||
assert session.commits == 1
|
||
assert len(repo.rows) == 1
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_list_templates_returns_created() -> None:
|
||
client, repo, _session = _make_client()
|
||
async with client:
|
||
await client.post("/templates", json={"title": "a", "body": "b"})
|
||
resp = await client.get("/templates")
|
||
assert resp.status_code == 200
|
||
items = resp.json()
|
||
assert len(items) == 1
|
||
assert items[0]["title"] == "a"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_create_template_empty_title_returns_422() -> None:
|
||
client, _repo, _session = _make_client()
|
||
async with client:
|
||
resp = await client.post("/templates", json={"title": "", "body": "b"})
|
||
assert resp.status_code == 422
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_create_template_empty_body_returns_422() -> None:
|
||
client, _repo, _session = _make_client()
|
||
async with client:
|
||
resp = await client.post("/templates", json={"title": "a", "body": ""})
|
||
assert resp.status_code == 422
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_create_template_whitespace_only_title_returns_422() -> None:
|
||
# strip 后为空 → min_length=1 不满足 → 422(防纯空白标题,审评 #2)。
|
||
client, _repo, _session = _make_client()
|
||
async with client:
|
||
resp = await client.post("/templates", json={"title": " ", "body": "b"})
|
||
assert resp.status_code == 422
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_create_template_whitespace_only_body_returns_422() -> None:
|
||
client, _repo, _session = _make_client()
|
||
async with client:
|
||
resp = await client.post("/templates", json={"title": "a", "body": " "})
|
||
assert resp.status_code == 422
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_delete_template_returns_204_and_commits() -> None:
|
||
client, repo, session = _make_client()
|
||
async with client:
|
||
created = await client.post("/templates", json={"title": "a", "body": "b"})
|
||
tid = created.json()["id"]
|
||
resp = await client.delete(f"/templates/{tid}")
|
||
assert resp.status_code == 204
|
||
assert session.commits == 2
|
||
assert repo.rows == {}
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_delete_missing_template_returns_404() -> None:
|
||
client, _repo, session = _make_client()
|
||
async with client:
|
||
resp = await client.delete(f"/templates/{uuid.uuid4()}")
|
||
assert resp.status_code == 404
|
||
assert resp.json()["error"]["code"] == "NOT_FOUND"
|
||
assert session.commits == 0
|