GRM-39: fix: use content_base64 for Gitea wiki API, add --verify flag (#33)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"""Unit tests for scripts/ci/sync_wiki.py."""
|
||||
|
||||
import base64
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
@@ -8,14 +9,46 @@ import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from scripts.ci.sync_wiki import (
|
||||
decode_content,
|
||||
encode_content,
|
||||
fetch_page_content,
|
||||
list_wiki_pages,
|
||||
load_mapping,
|
||||
main,
|
||||
read_doc_content,
|
||||
sync_page,
|
||||
verify_wiki_page,
|
||||
)
|
||||
|
||||
|
||||
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"
|
||||
@@ -64,6 +97,27 @@ class TestListWikiPages:
|
||||
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 gitea_runner_manager.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()
|
||||
@@ -71,7 +125,7 @@ class TestSyncPage:
|
||||
assert result == "skipped"
|
||||
client._request.assert_not_called()
|
||||
|
||||
def test_creates_new_page(self) -> None:
|
||||
def test_creates_new_page_with_base64(self) -> None:
|
||||
client = MagicMock()
|
||||
result = sync_page(client, "New-Page", "# Content", {}, dry_run=False)
|
||||
assert result == "created"
|
||||
@@ -79,8 +133,13 @@ class TestSyncPage:
|
||||
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(self) -> None:
|
||||
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)
|
||||
@@ -89,6 +148,37 @@ class TestSyncPage:
|
||||
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 TestMain:
|
||||
@@ -168,6 +258,21 @@ class TestMain:
|
||||
assert "not found" in result.output
|
||||
assert "Skipped: 1" in result.output
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
|
||||
@patch("scripts.ci.sync_wiki.GiteaClient")
|
||||
def test_empty_doc_file_skipped(self, mock_client_cls: MagicMock) -> None:
|
||||
"""Test that empty doc files are skipped with a warning."""
|
||||
with patch("scripts.ci.sync_wiki.MAPPING_FILE") as mock_mapping:
|
||||
mock_mapping.exists.return_value = True
|
||||
with patch("scripts.ci.sync_wiki.load_mapping", return_value={"empty.md": "Empty-Page"}):
|
||||
with patch("scripts.ci.sync_wiki.read_doc_content", return_value=" \n "):
|
||||
with patch("scripts.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()
|
||||
assert "Skipped: 1" in result.output
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
|
||||
@patch("scripts.ci.sync_wiki.GiteaClient")
|
||||
def test_create_and_update(self, mock_client_cls: MagicMock) -> None:
|
||||
@@ -185,3 +290,54 @@ class TestMain:
|
||||
assert result.exit_code == 0
|
||||
assert "Created: 1" in result.output
|
||||
assert "Updated: 1" in result.output
|
||||
|
||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True)
|
||||
@patch("scripts.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("scripts.ci.sync_wiki.MAPPING_FILE") as mock_mapping:
|
||||
mock_mapping.exists.return_value = True
|
||||
with patch("scripts.ci.sync_wiki.load_mapping", return_value={"index.md": "Home"}):
|
||||
with patch("scripts.ci.sync_wiki.read_doc_content", return_value="# Home Content"):
|
||||
with patch("scripts.ci.sync_wiki.list_wiki_pages", return_value={"Home": "Home"}):
|
||||
with patch("scripts.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", {"REPO_TOKEN": "tok"}, clear=True)
|
||||
@patch("scripts.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("scripts.ci.sync_wiki.MAPPING_FILE") as mock_mapping:
|
||||
mock_mapping.exists.return_value = True
|
||||
with patch("scripts.ci.sync_wiki.load_mapping", return_value={"index.md": "Home"}):
|
||||
with patch("scripts.ci.sync_wiki.read_doc_content", return_value="# Home Content"):
|
||||
with patch("scripts.ci.sync_wiki.list_wiki_pages", return_value={"Home": "Home"}):
|
||||
with patch("scripts.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", {"REPO_TOKEN": "tok"}, clear=True)
|
||||
@patch("scripts.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("scripts.ci.sync_wiki.MAPPING_FILE") as mock_mapping:
|
||||
mock_mapping.exists.return_value = True
|
||||
with patch("scripts.ci.sync_wiki.load_mapping", return_value={"index.md": "Home"}):
|
||||
with patch("scripts.ci.sync_wiki.read_doc_content", return_value="# Home"):
|
||||
with patch("scripts.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
|
||||
|
||||
Reference in New Issue
Block a user