177 lines
9.0 KiB
Python
177 lines
9.0 KiB
Python
"""История правок вики, чтение 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
|