sdlc-hub/tests/test_wiki_history_api.py
Andrey Epifancev 060270b854 Вики: поля at/author/offset в diff и code/title в versions (#140, ревью задачи 3)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:27:09 +04:00

177 lines
9.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""История правок вики, чтение HTTP API (wiki-history, #140): лента `GET /wiki/changes`, версии, дифф, поля страницы и
состояния. Агента имитирует `run_history` из test_wiki_history; фикстуры — через api из test_wiki_sync."""
import difflib
from datetime import timedelta
import pytest
from sdlc_hub.core import db
from sdlc_hub.core.client import HubError
from test_wiki_history import MOLCH, SOSNOV, run_history, version # noqa: F401
from test_wiki_sync import CODES, TREE, api, first_sync, page # noqa: F401 — api — фикстура
DAY_SINCE, DAY_UNTIL = "2026-10-27T00:00:00+03:00", "2026-10-28T00:00:00+03:00"
def window(**extra):
return {"project": "AWPCA", "since": DAY_SINCE, "until": DAY_UNTIL} | extra
def hub_error(fn, *a, **kw):
with pytest.raises(HubError) as e:
fn(*a, **kw)
return e.value
@pytest.fixture
def history(api):
"""Версии: DOC-3 (Соснов) 17:59 и 18:02 +03:00, DOC-2 (Молчанов) 26.10 12:00 +03:00; DOC-1 без версий."""
_, done = run_history(api, TREE, [
version("DOC-3", "DOCH-30", "2026-10-27T17:59:00+03:00", "<p>статус</p>"),
version("DOC-3", "DOCH-31", "2026-10-27T18:02:00+03:00", "<p>статус</p><p>новый</p>"),
version("DOC-2", "DOCH-20", "2026-10-26T12:00:00+03:00", "<p>роли</p>", author=MOLCH),
], CODES)
assert done["status"] == "done"
return api
@pytest.fixture
def four(api):
"""Четыре версии DOC-3 Соснова за 27.10 (последняя — 23:48:02 +03:00)."""
stamps = ["2026-10-27T17:59:00+03:00", "2026-10-27T18:02:00+03:00", "2026-10-27T20:30:00+03:00",
"2026-10-27T23:48:02+03:00"]
vs = [version("DOC-3", f"DOCH-4{i}", ts, f"<p>строка {i}</p>") for i, ts in enumerate(stamps, 1)]
_, done = run_history(api, TREE, vs, CODES)
assert done["status"] == "done"
return api
# --- лента -----------------------------------------------------------------------------------------------------------
def test_changes_today(history): # сценарий «Что сделали за сегодня»
d = history.client.get("/wiki/changes", **window())
assert [c["at"] for c in d["changes"]] == ["2026-10-27T15:02:00+00:00", "2026-10-27T14:59:00+00:00"]
c = d["changes"][0]
assert c["page_code"] == "DOC-3" and c["title"] == "Статусы" and c["path"] == "АРМ КА/01_Статусы"
assert c["author"] == SOSNOV and c["version"] == "DOCH-31"
assert isinstance(c["added"], int) and isinstance(c["removed"], int)
assert d["since"] == "2026-10-26T21:00:00+00:00" and d["until"] == "2026-10-27T21:00:00+00:00"
assert d["truncated"] is False and d["synced_at"] and d["history_at"]
@pytest.mark.parametrize("author", ["соснов", "СОСНОВ", "Sosnov-DVI"])
def test_changes_author_cyrillic_case(history, author): # Review Focus 1: только две правки DOC-3
d = history.client.get("/wiki/changes", **window(author=author))
assert len(d["changes"]) == 2 and {c["page_code"] for c in d["changes"]} == {"DOC-3"}
def test_changes_default_window(history): # без since/until: окно ровно 24 часа
d = history.client.get("/wiki/changes", project="AWPCA")
assert db.parse(d["until"]) - db.parse(d["since"]) == timedelta(hours=24)
def test_changes_limit_truncated(history):
d = history.client.get("/wiki/changes", **window(limit="1"))
assert len(d["changes"]) == 1 and d["truncated"] is True
@pytest.mark.parametrize("q,param", [
({"since": "2026-10-27T00:00:00"}, "since"), ({"until": "вчера"}, "until"),
({"limit": "0"}, "limit"), ({"limit": "2001"}, "limit"), ({"limit": "x"}, "limit"),
({"since": "2026-10-28T00:00:00Z", "until": "2026-10-27T00:00:00Z"}, "since"),
])
def test_changes_bad_params(history, q, param):
e = hub_error(history.client.get, "/wiki/changes", **({"project": "AWPCA"} | q))
assert e.status == 422 and e.error == "bad_request" and e.detail.startswith(param)
def test_changes_history_not_loaded(api): # синхронизация без версий: история не загружена
first_sync(api)
d = api.client.get("/wiki/changes", **window())
assert d["history_at"] is None and d["changes"] == [] and d["synced_at"]
assert api.client.get("/wiki/status", project="AWPCA")["history_at"] is None
def test_changes_no_root_and_unknown(api):
api.client.post("/projects", {"label": "BEZ", "name": "Без корня"})
assert hub_error(api.client.get, "/wiki/changes", project="BEZ").status == 409
assert hub_error(api.client.get, "/wiki/changes", project="NOPE").status == 404
# --- версии страницы -------------------------------------------------------------------------------------------------
def test_versions_list(history): # новые первыми
d = history.client.get("/wiki/pages/DOC-3/versions", project="AWPCA")
assert d["code"] == "DOC-3" and d["title"] == "Статусы"
assert len(d["versions"]) == 2 and d["versions"][0]["at"] == "2026-10-27T15:02:00+00:00"
assert d["versions"][0]["version"] == "DOCH-31" and d["versions"][0]["author"] == SOSNOV
assert hub_error(history.client.get, "/wiki/pages/DOC-99/versions", project="AWPCA").status == 404
# --- дифф ------------------------------------------------------------------------------------------------------------
def test_diff_last(four): # сценарий «Дифф последней правки»
d = four.client.get("/wiki/pages/DOC-3/diff", project="AWPCA")
v3, v4 = "DOCH-43", "DOCH-44"
assert d["index"] == 4 and d["total"] == 4 and d["prev_version"] == v3 and d["version"] == v4
assert d["diff"].startswith(f"--- {v3}\n+++ {v4}\n") and d["next_offset"] is None
assert d["code"] == "DOC-3" and d["title"] == "Статусы" and d["path"] == "АРМ КА/01_Статусы"
assert d["at"] == "2026-10-27T20:48:02+00:00" and d["author"] == SOSNOV and d["offset"] == 0
def test_diff_first_version(history):
d = history.client.get("/wiki/pages/DOC-3/diff", project="AWPCA", version="DOCH-30")
assert d["index"] == 1 and d["prev_version"] is None and d["removed"] == 0
assert d["diff"].startswith("--- \n+++ ")
def test_diff_chunks(api): # сценарий «Длинный дифф»: склейка частей == полный дифф
html = "".join(f"<p>строка {i:04d} текста, которой много</p>" for i in range(1300))
_, done = run_history(api, TREE, [version("DOC-3", "DOCH-90", "2026-10-27T10:00:00Z", html)], CODES)
assert done["status"] == "done"
md = api.rows("SELECT markdown FROM wiki_version WHERE code = 'DOCH-90'")[0]["markdown"]
full = "".join(f"{x}\n" for x in difflib.unified_diff([], md.splitlines(), fromfile="", tofile="DOCH-90",
n=3, lineterm=""))
assert 40000 < len(full) <= 60000
def part(offset):
return api.client.get("/wiki/pages/DOC-3/diff", project="AWPCA", offset=str(offset))
p0 = part(0)
assert len(p0["diff"]) == 20000 and p0["next_offset"] == 20000 and p0["diff"] == full[:20000]
assert p0["offset"] == 0
p40 = part(40000)
assert p40["diff"] == full[40000:] and p40["next_offset"] is None and p40["offset"] == 40000
assert p0["diff"] + part(20000)["diff"] + p40["diff"] == full
for bad in ("-1", "x"):
e = hub_error(api.client.get, "/wiki/pages/DOC-3/diff", project="AWPCA", offset=bad)
assert e.status == 422 and e.error == "bad_request" and e.detail == "offset: целое ≥ 0"
def test_diff_errors(history):
e = hub_error(history.client.get, "/wiki/pages/DOC-99/diff", project="AWPCA")
assert e.status == 404 and e.error == "not_found"
e = hub_error(history.client.get, "/wiki/pages/DOC-3/diff", project="AWPCA", version="DOCH-404")
assert e.status == 404 and e.error == "no_version"
e = hub_error(history.client.get, "/wiki/pages/DOC-1/diff", project="AWPCA")
assert e.status == 404 and e.error == "no_versions"
# --- поля страницы и состояния ---------------------------------------------------------------------------------------
def test_page_last_change(four): # сценарий «Последняя правка в странице»
p = four.client.get("/wiki/pages/DOC-3", project="AWPCA")
assert p["modified_at"] == "2026-10-27T20:48:02+00:00"
assert p["modified_by"] == SOSNOV and p["versions"] == 4
def test_page_last_change_without_versions(history):
p = history.client.get("/wiki/pages/DOC-1", project="AWPCA")
assert p["modified_at"] is None and p["modified_by"] is None and p["versions"] == 0
def test_status_history_at(history):
st = history.client.get("/wiki/status", project="AWPCA")
stored = history.rows("SELECT history_at FROM wiki_history")[0]["history_at"]
assert stored and st["history_at"] == stored