Files
devx/tests/unit/test_sync_wiki.py
T
emil 9d75e408ae
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 8s
Build Images / detect-type (push) Successful in 42s
Post-merge / sync-wiki (push) Successful in 30s
Post-merge / vikunja (push) Successful in 16s
Post-merge / release (push) Successful in 44s
Post-merge / configure-repo (push) Successful in 17s
Post-merge / badges (push) Successful in 43s
Post-merge / publish (push) Successful in 20s
Build Images / build-and-push (push) Successful in 3m12s
Build Images / cleanup (push) Successful in 2m37s
DEVX-14: fix: retry wiki integrity check on transient API timeout
2026-06-30 05:33:00 +00:00

575 lines
28 KiB
Python

"""Unit tests for scripts/ci/sync_wiki.py."""
import base64
import json
from pathlib import Path
from unittest.mock import MagicMock, patch
import click
import pytest
from click.testing import CliRunner
from devx.ci.sync_wiki import (
decode_content,
encode_content,
fetch_page_content,
list_wiki_pages,
load_mapping,
main,
read_doc_content,
sync_page,
verify_wiki_integrity,
verify_wiki_page,
)
from devx.exceptions import APIError
class TestEncodeContent:
def test_encodes_utf8_to_base64(self) -> None:
result = encode_content("# Hello World")
assert result == base64.b64encode(b"# Hello World").decode("ascii")
def test_encodes_empty_string(self) -> None:
assert encode_content("") == ""
def test_encodes_unicode(self) -> None:
result = encode_content("# Café — résumé")
decoded = base64.b64decode(result).decode("utf-8")
assert decoded == "# Café — résumé"
class TestDecodeContent:
def test_decodes_base64_to_utf8(self) -> None:
encoded = base64.b64encode(b"# Hello").decode("ascii")
assert decode_content(encoded) == "# Hello"
def test_empty_string_returns_empty(self) -> None:
assert decode_content("") == ""
def test_roundtrip(self) -> None:
original = "# Wiki Page\n\nContent with **markdown**."
encoded = encode_content(original)
assert decode_content(encoded) == original
class TestLoadMapping:
def test_loads_mapping(self, tmp_path: Path) -> None:
mapping_file = tmp_path / "mapping.json"
mapping_file.write_text(json.dumps({"user/getting-started.md": "Getting-Started"}))
with patch("devx.ci.sync_wiki.MAPPING_FILE", mapping_file):
result = load_mapping()
assert result == {"user/getting-started.md": "Getting-Started"}
def test_missing_mapping_raises(self, tmp_path: Path) -> None:
with patch("devx.ci.sync_wiki.MAPPING_FILE", tmp_path / "nonexistent.json"):
with pytest.raises(FileNotFoundError):
load_mapping()
def test_non_dict_mapping_raises(self, tmp_path: Path) -> None:
"""Non-dict mapping.json should raise."""
mapping_file = tmp_path / "mapping.json"
mapping_file.write_text('["not", "a", "dict"]')
with patch("devx.ci.sync_wiki.MAPPING_FILE", mapping_file):
with pytest.raises(click.ClickException, match="must be a dict"):
load_mapping()
def test_non_string_values_raise(self, tmp_path: Path) -> None:
"""Non-string values in mapping.json should raise."""
mapping_file = tmp_path / "mapping.json"
mapping_file.write_text('{"file.md": 123}')
with patch("devx.ci.sync_wiki.MAPPING_FILE", mapping_file):
with pytest.raises(click.ClickException, match="must be strings"):
load_mapping()
class TestReadDocContent:
def test_reads_file(self, tmp_path: Path) -> None:
docs_dir = tmp_path / "docs"
docs_dir.mkdir()
(docs_dir / "test.md").write_text("# Test\n\nContent")
with patch("devx.ci.sync_wiki.DOCS_DIR", docs_dir):
content = read_doc_content("test.md")
assert content == "# Test\n\nContent"
def test_missing_file_raises(self, tmp_path: Path) -> None:
with patch("devx.ci.sync_wiki.DOCS_DIR", tmp_path):
with pytest.raises(FileNotFoundError):
read_doc_content("nonexistent.md")
class TestListWikiPages:
def test_raises_on_api_error(self) -> None:
client = MagicMock()
client._request.side_effect = APIError(404, "not found")
with pytest.raises(APIError):
list_wiki_pages(client)
def test_returns_page_dict(self) -> None:
client = MagicMock()
client._request.return_value.json.return_value = [
{"title": "Home", "sub_url": "Home"},
{"title": "Getting-Started", "sub_url": "Getting-Started.-"},
]
result = list_wiki_pages(client)
assert result == {"Home": "Home", "Getting-Started": "Getting-Started.-"}
class TestFetchPageContent:
def test_fetches_and_decodes_content(self) -> None:
client = MagicMock()
encoded = base64.b64encode(b"# Hello Wiki").decode("ascii")
client._request.return_value.json.return_value = {"content_base64": encoded}
result = fetch_page_content(client, "Home")
assert result == "# Hello Wiki"
def test_returns_empty_on_api_error(self) -> None:
from devx.exceptions import APIError
client = MagicMock()
client._request.side_effect = APIError(404, "not found")
assert fetch_page_content(client, "Missing") == ""
def test_returns_empty_for_empty_content(self) -> None:
client = MagicMock()
client._request.return_value.json.return_value = {"content_base64": ""}
assert fetch_page_content(client, "Home") == ""
class TestSyncPage:
def test_dry_run_skips(self) -> None:
client = MagicMock()
result = sync_page(client, "Test-Page", "# Content", {}, dry_run=True)
assert result == "skipped"
client._request.assert_not_called()
def test_creates_new_page_with_base64(self) -> None:
client = MagicMock()
result = sync_page(client, "New-Page", "# Content", {}, dry_run=False)
assert result == "created"
client._request.assert_called_once()
call_args = client._request.call_args
assert call_args.args[0] == "POST"
assert call_args.args[1] == "/wiki/new"
# Verify content_base64 is used, not content
payload = call_args.kwargs["json"]
assert "content_base64" in payload
assert "content" not in payload
assert base64.b64decode(payload["content_base64"]).decode("utf-8") == "# Content"
def test_updates_existing_page_with_base64(self) -> None:
client = MagicMock()
existing = {"Existing-Page": "Existing-Page.-"}
result = sync_page(client, "Existing-Page", "# Updated", existing, dry_run=False)
assert result == "updated"
client._request.assert_called_once()
call_args = client._request.call_args
assert call_args.args[0] == "PATCH"
assert "/wiki/page/Existing-Page.-" in call_args.args[1]
# Verify content_base64 is used
payload = call_args.kwargs["json"]
assert "content_base64" in payload
assert "content" not in payload
assert base64.b64decode(payload["content_base64"]).decode("utf-8") == "# Updated"
class TestVerifyWikiPage:
def test_verifies_matching_content(self) -> None:
client = MagicMock()
encoded = base64.b64encode(b"# Hello Wiki").decode("ascii")
client._request.return_value.json.return_value = {"content_base64": encoded}
existing = {"Home": "Home"}
assert verify_wiki_page(client, "Home", "# Hello Wiki", existing) is True
def test_fails_on_mismatch(self) -> None:
client = MagicMock()
encoded = base64.b64encode(b"# Old Content").decode("ascii")
client._request.return_value.json.return_value = {"content_base64": encoded}
existing = {"Home": "Home"}
assert verify_wiki_page(client, "Home", "# New Content", existing) is False
def test_fails_on_empty_wiki_content(self) -> None:
client = MagicMock()
client._request.return_value.json.return_value = {"content_base64": ""}
existing = {"Home": "Home"}
assert verify_wiki_page(client, "Home", "# Expected", existing) is False
def test_fails_when_page_not_in_existing(self) -> None:
client = MagicMock()
assert verify_wiki_page(client, "Missing", "# Content", {}) is False
class TestVerifyWikiIntegrity:
def _make_client(self, pages: dict[str, str], contents: dict[str, str]) -> MagicMock:
"""Create a mock client that returns the given pages and contents."""
client = MagicMock()
# list_wiki_pages calls GET /wiki/pages
page_list = [{"title": t, "sub_url": s} for t, s in pages.items()]
# fetch_page_content calls GET /wiki/page/{sub_url}
def mock_request(method, path, **kwargs):
resp = MagicMock()
if path == "/wiki/pages":
resp.json.return_value = page_list
elif path.startswith("/wiki/page/"):
sub_url = path.replace("/wiki/page/", "")
content = contents.get(sub_url, "")
encoded = base64.b64encode(content.encode()).decode("ascii") if content else ""
resp.json.return_value = {"content_base64": encoded}
return resp
client._request.side_effect = mock_request
return client
def test_all_good_no_failures(self) -> None:
pages = {"Home": "Home", "FAQ": "FAQ"}
contents = {"Home": "# Home", "FAQ": "# FAQ"}
client = self._make_client(pages, contents)
mapping = {"index.md": "Home", "faq.md": "FAQ"}
synced = {"Home": "# Home", "FAQ": "# FAQ"}
failures = verify_wiki_integrity(client, mapping, synced)
assert failures == []
def test_missing_page_detected(self) -> None:
pages = {"Home": "Home"} # FAQ missing from wiki
contents = {"Home": "# Home"}
client = self._make_client(pages, contents)
mapping = {"index.md": "Home", "faq.md": "FAQ"}
synced = {"Home": "# Home"}
failures = verify_wiki_integrity(client, mapping, synced)
assert any("Missing page: FAQ" in f for f in failures)
def test_stale_page_detected(self) -> None:
pages = {"Home": "Home", "Old-Page": "Old-Page"} # Old-Page not in mapping
contents = {"Home": "# Home", "Old-Page": "# Old"}
client = self._make_client(pages, contents)
mapping = {"index.md": "Home"}
synced = {"Home": "# Home"}
failures = verify_wiki_integrity(client, mapping, synced)
assert any("Stale page" in f and "Old-Page" in f for f in failures)
def test_page_count_mismatch_detected(self) -> None:
pages = {"Home": "Home", "Extra": "Extra"}
contents = {"Home": "# Home", "Extra": "# Extra"}
client = self._make_client(pages, contents)
mapping = {"index.md": "Home"}
synced = {"Home": "# Home"}
failures = verify_wiki_integrity(client, mapping, synced)
assert any("Page count mismatch" in f for f in failures)
def test_empty_content_detected(self) -> None:
pages = {"Home": "Home"}
contents = {"Home": ""} # Empty content
client = self._make_client(pages, contents)
mapping = {"index.md": "Home"}
synced = {"Home": "# Expected Content"}
failures = verify_wiki_integrity(client, mapping, synced)
assert any("Empty content: Home" in f for f in failures)
def test_content_mismatch_detected(self) -> None:
pages = {"Home": "Home"}
contents = {"Home": "# Wrong Content"}
client = self._make_client(pages, contents)
mapping = {"index.md": "Home"}
synced = {"Home": "# Correct Content"}
failures = verify_wiki_integrity(client, mapping, synced)
assert any("Content mismatch: Home" in f for f in failures)
def test_multiple_failures_all_reported(self) -> None:
pages = {"Home": "Home", "Stale": "Stale"}
contents = {"Home": "", "Stale": "# Stale"}
client = self._make_client(pages, contents)
mapping = {"index.md": "Home", "faq.md": "FAQ"} # FAQ missing
synced = {"Home": "# Home Content"}
failures = verify_wiki_integrity(client, mapping, synced)
assert len(failures) >= 3 # count mismatch, missing FAQ, stale Stale, empty Home
def test_transient_api_failure_returns_empty(self) -> None:
"""When the wiki API is unavailable after retries, integrity check
should return no failures (sync already succeeded)."""
client = MagicMock()
# _list_wiki_pages_with_retry raises APIError (retries exhausted)
with patch("devx.ci.sync_wiki._list_wiki_pages_with_retry", side_effect=APIError(0, "timeout")):
mapping = {"index.md": "Home", "faq.md": "FAQ"}
synced = {"Home": "# Home", "FAQ": "# FAQ"}
failures = verify_wiki_integrity(client, mapping, synced)
assert failures == []
def test_transient_api_failure_recovers_on_retry(self) -> None:
"""When the wiki API recovers after a retry, integrity check proceeds normally."""
client = MagicMock()
pages = {"Home": "Home", "FAQ": "FAQ"}
contents = {"Home": "# Home", "FAQ": "# FAQ"}
def mock_request(method, path, **kwargs):
resp = MagicMock()
if path == "/wiki/pages":
page_list = [{"title": t, "sub_url": s} for t, s in pages.items()]
resp.json.return_value = page_list
elif path.startswith("/wiki/page/"):
sub_url = path.replace("/wiki/page/", "")
content = contents.get(sub_url, "")
encoded = base64.b64encode(content.encode()).decode("ascii") if content else ""
resp.json.return_value = {"content_base64": encoded}
return resp
client._request.side_effect = mock_request
mapping = {"index.md": "Home", "faq.md": "FAQ"}
synced = {"Home": "# Home", "FAQ": "# FAQ"}
failures = verify_wiki_integrity(client, mapping, synced)
assert failures == []
class TestMain:
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"})
@patch("devx.ci.sync_wiki.MAPPING_FILE")
@patch("devx.ci.sync_wiki.DOCS_DIR")
@patch("devx.ci.sync_wiki.GiteaClient")
def test_dry_run(self, mock_client_cls: MagicMock, mock_docs_dir: Path, mock_mapping_file: Path) -> None:
mock_mapping_file.exists.return_value = True
mock_mapping_file.__str__ = lambda _: "/docs/mapping.json"
with patch("devx.ci.sync_wiki.load_mapping", return_value={"index.md": "Home"}):
with patch("devx.ci.sync_wiki.read_doc_content", return_value="# Home"):
with patch("devx.ci.sync_wiki.list_wiki_pages", return_value={}):
runner = CliRunner()
result = runner.invoke(main, ["--dry-run", "--repo", "owner/repo"])
assert result.exit_code == 0
assert "dry-run" in result.output
@patch.dict("os.environ", {"CI_GITEA_TOKEN": ""}, clear=True)
def test_missing_token_exits(self) -> None:
runner = CliRunner()
result = runner.invoke(main, ["--repo", "owner/repo"])
assert result.exit_code == 1
assert "CI_GITEA_TOKEN" in result.output
@patch.dict(
"os.environ", {"CI_GITEA_TOKEN": "tok", "DEVX_REPO_OWNER": "me", "DEVX_REPO_NAME": "myrepo"}, clear=True
)
@patch("devx.ci.sync_wiki.GiteaClient")
def test_auto_detect_repo(self, mock_client_cls: MagicMock) -> None:
"""Test that repo is auto-detected from env vars when --repo is not passed."""
with patch("devx.ci.sync_wiki.MAPPING_FILE") as mock_mapping:
mock_mapping.exists.return_value = True
with patch("devx.ci.sync_wiki.load_mapping", return_value={"index.md": "Home"}):
with patch("devx.ci.sync_wiki.read_doc_content", return_value="# Home"):
with patch("devx.ci.sync_wiki.list_wiki_pages", return_value={}):
runner = CliRunner()
result = runner.invoke(main, ["--dry-run"])
assert result.exit_code == 0
mock_client_cls.assert_called_once()
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.sync_wiki.GiteaClient")
def test_missing_mapping_file(self, mock_client_cls: MagicMock) -> None:
"""Test that missing mapping.json exits with error."""
with patch("devx.ci.sync_wiki.MAPPING_FILE") as mock_mapping:
mock_mapping.exists.return_value = False
runner = CliRunner()
result = runner.invoke(main, ["--repo", "owner/repo"])
assert result.exit_code == 1
assert "mapping.json" in result.output
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.sync_wiki.GiteaClient")
def test_existing_pages_message(self, mock_client_cls: MagicMock) -> None:
"""Test that existing wiki pages are reported."""
with patch("devx.ci.sync_wiki.MAPPING_FILE") as mock_mapping:
mock_mapping.exists.return_value = True
with patch("devx.ci.sync_wiki.load_mapping", return_value={"index.md": "Home"}):
with patch("devx.ci.sync_wiki.read_doc_content", return_value="# Home"):
with patch("devx.ci.sync_wiki.list_wiki_pages", return_value={"Home": "Home"}):
runner = CliRunner()
result = runner.invoke(main, ["--dry-run", "--repo", "owner/repo"])
assert result.exit_code == 0
assert "existing wiki pages" in result.output
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.sync_wiki.GiteaClient")
def test_file_not_found_fails(self, mock_client_cls: MagicMock) -> None:
"""Test that missing doc files cause an error, not a warning."""
with patch("devx.ci.sync_wiki.MAPPING_FILE") as mock_mapping:
mock_mapping.exists.return_value = True
with patch("devx.ci.sync_wiki.load_mapping", return_value={"missing.md": "Missing"}):
with patch("devx.ci.sync_wiki.read_doc_content", side_effect=FileNotFoundError):
with patch("devx.ci.sync_wiki.list_wiki_pages", return_value={}):
runner = CliRunner()
result = runner.invoke(main, ["--dry-run", "--repo", "owner/repo"])
assert result.exit_code != 0
assert "not found" in result.output
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.sync_wiki.GiteaClient")
def test_empty_doc_file_fails(self, mock_client_cls: MagicMock) -> None:
"""Test that empty doc files cause an error, not a warning."""
with patch("devx.ci.sync_wiki.MAPPING_FILE") as mock_mapping:
mock_mapping.exists.return_value = True
with patch("devx.ci.sync_wiki.load_mapping", return_value={"empty.md": "Empty-Page"}):
with patch("devx.ci.sync_wiki.read_doc_content", return_value=" \n "):
with patch("devx.ci.sync_wiki.list_wiki_pages", return_value={}):
runner = CliRunner()
result = runner.invoke(main, ["--dry-run", "--repo", "owner/repo"])
assert result.exit_code != 0
assert "empty" in result.output.lower()
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.sync_wiki.GiteaClient")
def test_create_and_update(self, mock_client_cls: MagicMock) -> None:
"""Test that pages are created and updated correctly (non-dry-run)."""
mock_client = MagicMock()
mock_client_cls.return_value = mock_client
with patch("devx.ci.sync_wiki.MAPPING_FILE") as mock_mapping:
mock_mapping.exists.return_value = True
mapping = {"new.md": "New-Page", "existing.md": "Existing-Page"}
with patch("devx.ci.sync_wiki.load_mapping", return_value=mapping):
with patch("devx.ci.sync_wiki.read_doc_content", return_value="# Content"):
with patch("devx.ci.sync_wiki.list_wiki_pages", return_value={"Existing-Page": "Existing-Page"}):
runner = CliRunner()
result = runner.invoke(main, ["--repo", "owner/repo"])
assert result.exit_code == 0
assert "Created: 1" in result.output
assert "Updated: 1" in result.output
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.sync_wiki.GiteaClient")
def test_verify_passes(self, mock_client_cls: MagicMock) -> None:
"""Test that --verify passes when content matches."""
mock_client = MagicMock()
mock_client_cls.return_value = mock_client
encoded = base64.b64encode(b"# Home Content").decode("ascii")
# list_wiki_pages returns {"Home": "Home"}, fetch returns encoded content
mock_client._request.return_value.json.return_value = {"content_base64": encoded}
with patch("devx.ci.sync_wiki.MAPPING_FILE") as mock_mapping:
mock_mapping.exists.return_value = True
with patch("devx.ci.sync_wiki.load_mapping", return_value={"index.md": "Home"}):
with patch("devx.ci.sync_wiki.read_doc_content", return_value="# Home Content"):
with patch("devx.ci.sync_wiki.list_wiki_pages", return_value={"Home": "Home"}):
with patch("devx.ci.sync_wiki.verify_wiki_page", return_value=True):
runner = CliRunner()
result = runner.invoke(main, ["--repo", "owner/repo", "--verify"])
assert result.exit_code == 0
assert "Verification passed" in result.output
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.sync_wiki.GiteaClient")
def test_verify_fails_on_empty_content(self, mock_client_cls: MagicMock) -> None:
"""Test that --verify fails when wiki pages have empty content."""
mock_client = MagicMock()
mock_client_cls.return_value = mock_client
with patch("devx.ci.sync_wiki.MAPPING_FILE") as mock_mapping:
mock_mapping.exists.return_value = True
with patch("devx.ci.sync_wiki.load_mapping", return_value={"index.md": "Home"}):
with patch("devx.ci.sync_wiki.read_doc_content", return_value="# Home Content"):
with patch("devx.ci.sync_wiki.list_wiki_pages", return_value={"Home": "Home"}):
with patch("devx.ci.sync_wiki.verify_wiki_page", return_value=False):
runner = CliRunner()
result = runner.invoke(main, ["--repo", "owner/repo", "--verify"])
assert result.exit_code == 1
assert "FAIL" in result.output
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.sync_wiki.GiteaClient")
def test_verify_skipped_in_dry_run(self, mock_client_cls: MagicMock) -> None:
"""Test that --verify is skipped during dry-run."""
with patch("devx.ci.sync_wiki.MAPPING_FILE") as mock_mapping:
mock_mapping.exists.return_value = True
with patch("devx.ci.sync_wiki.load_mapping", return_value={"index.md": "Home"}):
with patch("devx.ci.sync_wiki.read_doc_content", return_value="# Home"):
with patch("devx.ci.sync_wiki.list_wiki_pages", return_value={}):
runner = CliRunner()
result = runner.invoke(main, ["--dry-run", "--verify", "--repo", "owner/repo"])
assert result.exit_code == 0
assert "Verification" not in result.output
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.sync_wiki.GiteaClient")
def test_strict_passes(self, mock_client_cls: MagicMock) -> None:
"""Test that --strict passes when integrity check succeeds."""
mock_client = MagicMock()
mock_client_cls.return_value = mock_client
with patch("devx.ci.sync_wiki.MAPPING_FILE") as mock_mapping:
mock_mapping.exists.return_value = True
with patch("devx.ci.sync_wiki.load_mapping", return_value={"index.md": "Home"}):
with patch("devx.ci.sync_wiki.read_doc_content", return_value="# Home"):
with patch("devx.ci.sync_wiki.list_wiki_pages", return_value={"Home": "Home"}):
with patch("devx.ci.sync_wiki.verify_wiki_integrity", return_value=[]):
runner = CliRunner()
result = runner.invoke(main, ["--repo", "owner/repo", "--strict"])
assert result.exit_code == 0
assert "Integrity check passed" in result.output
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.sync_wiki.GiteaClient")
def test_strict_fails_on_integrity_issues(self, mock_client_cls: MagicMock) -> None:
"""Test that --strict fails when integrity check finds issues."""
mock_client = MagicMock()
mock_client_cls.return_value = mock_client
with patch("devx.ci.sync_wiki.MAPPING_FILE") as mock_mapping:
mock_mapping.exists.return_value = True
with patch("devx.ci.sync_wiki.load_mapping", return_value={"index.md": "Home"}):
with patch("devx.ci.sync_wiki.read_doc_content", return_value="# Home"):
with patch("devx.ci.sync_wiki.list_wiki_pages", return_value={"Home": "Home"}):
with patch(
"devx.ci.sync_wiki.verify_wiki_integrity",
return_value=["Missing page: FAQ", "Stale page: Old-Page"],
):
runner = CliRunner()
result = runner.invoke(main, ["--repo", "owner/repo", "--strict"])
assert result.exit_code == 1
assert "Integrity check FAILED" in result.output
assert "Missing page: FAQ" in result.output
assert "Stale page: Old-Page" in result.output
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.sync_wiki.GiteaClient")
def test_strict_skipped_in_dry_run(self, mock_client_cls: MagicMock) -> None:
"""Test that --strict verification is skipped during dry-run."""
with patch("devx.ci.sync_wiki.MAPPING_FILE") as mock_mapping:
mock_mapping.exists.return_value = True
with patch("devx.ci.sync_wiki.load_mapping", return_value={"index.md": "Home"}):
with patch("devx.ci.sync_wiki.read_doc_content", return_value="# Home"):
with patch("devx.ci.sync_wiki.list_wiki_pages", return_value={}):
runner = CliRunner()
result = runner.invoke(main, ["--dry-run", "--strict", "--repo", "owner/repo"])
assert result.exit_code == 0
assert "Integrity check" not in result.output
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.sync_wiki.GiteaClient")
def test_initial_list_api_error_treated_as_empty(self, mock_client_cls: MagicMock) -> None:
"""When the initial page list fails, sync proceeds treating wiki as empty."""
mock_client = MagicMock()
mock_client_cls.return_value = mock_client
with patch("devx.ci.sync_wiki.MAPPING_FILE") as mock_mapping:
mock_mapping.exists.return_value = True
with patch("devx.ci.sync_wiki.load_mapping", return_value={"index.md": "Home"}):
with patch("devx.ci.sync_wiki.read_doc_content", return_value="# Home"):
with patch("devx.ci.sync_wiki.list_wiki_pages", side_effect=APIError(0, "timeout")):
with patch("devx.ci.sync_wiki.sync_page", return_value="created"):
runner = CliRunner()
result = runner.invoke(main, ["--repo", "owner/repo"])
assert result.exit_code == 0
assert "Created: Home" in result.output
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "tok"}, clear=True)
@patch("devx.ci.sync_wiki.GiteaClient")
def test_verify_skips_when_refetch_fails(self, mock_client_cls: MagicMock) -> None:
"""When --verify re-fetch fails after retries, verification is skipped gracefully."""
mock_client = MagicMock()
mock_client_cls.return_value = mock_client
with patch("devx.ci.sync_wiki.MAPPING_FILE") as mock_mapping:
mock_mapping.exists.return_value = True
with patch("devx.ci.sync_wiki.load_mapping", return_value={"index.md": "Home"}):
with patch("devx.ci.sync_wiki.read_doc_content", return_value="# Home"):
with patch("devx.ci.sync_wiki.list_wiki_pages", return_value={"Home": "Home"}):
with patch("devx.ci.sync_wiki.sync_page", return_value="updated"):
with patch(
"devx.ci.sync_wiki._list_wiki_pages_with_retry",
side_effect=APIError(0, "timeout"),
):
runner = CliRunner()
result = runner.invoke(main, ["--repo", "owner/repo", "--verify"])
assert result.exit_code == 0
assert "Skipping content verification" in result.output