"""Unit tests for scripts/sync_wiki.py.""" import json from pathlib import Path from unittest.mock import MagicMock, patch import pytest from click.testing import CliRunner from scripts.sync_wiki import ( list_wiki_pages, load_mapping, main, read_doc_content, sync_page, ) 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("scripts.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("scripts.sync_wiki.MAPPING_FILE", tmp_path / "nonexistent.json"): with pytest.raises(FileNotFoundError): 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("scripts.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("scripts.sync_wiki.DOCS_DIR", tmp_path): with pytest.raises(FileNotFoundError): read_doc_content("nonexistent.md") class TestListWikiPages: 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") result = list_wiki_pages(client) assert result == {} def test_returns_page_dict(self) -> None: client = MagicMock() client._request.return_value.json.return_value = [ {"title": "Home", "page_name": "Home"}, {"title": "Getting-Started", "page_name": "Getting-Started"}, ] result = list_wiki_pages(client) assert result == {"Home": "Home", "Getting-Started": "Getting-Started"} 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(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/page" def test_updates_existing_page(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] == "PUT" class TestMain: @patch.dict("os.environ", {"REPO_TOKEN": "tok"}) @patch("scripts.sync_wiki.MAPPING_FILE") @patch("scripts.sync_wiki.DOCS_DIR") @patch("scripts.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("scripts.sync_wiki.load_mapping", return_value={"index.md": "Home"}): with patch("scripts.sync_wiki.read_doc_content", return_value="# Home"): with patch("scripts.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", {"REPO_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 "REPO_TOKEN" in result.output @patch.dict("os.environ", {"REPO_TOKEN": "tok", "GRM_REPO_OWNER": "me", "GRM_REPO_NAME": "myrepo"}, clear=True) @patch("scripts.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("scripts.sync_wiki.MAPPING_FILE") as mock_mapping: mock_mapping.exists.return_value = True with patch("scripts.sync_wiki.load_mapping", return_value={"index.md": "Home"}): with patch("scripts.sync_wiki.read_doc_content", return_value="# Home"): with patch("scripts.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", {"REPO_TOKEN": "tok"}, clear=True) @patch("scripts.sync_wiki.GiteaClient") def test_missing_mapping_file(self, mock_client_cls: MagicMock) -> None: """Test that missing mapping.json exits with error.""" with patch("scripts.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", {"REPO_TOKEN": "tok"}, clear=True) @patch("scripts.sync_wiki.GiteaClient") def test_existing_pages_message(self, mock_client_cls: MagicMock) -> None: """Test that existing wiki pages are reported.""" with patch("scripts.sync_wiki.MAPPING_FILE") as mock_mapping: mock_mapping.exists.return_value = True with patch("scripts.sync_wiki.load_mapping", return_value={"index.md": "Home"}): with patch("scripts.sync_wiki.read_doc_content", return_value="# Home"): with patch("scripts.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", {"REPO_TOKEN": "tok"}, clear=True) @patch("scripts.sync_wiki.GiteaClient") def test_file_not_found_warning(self, mock_client_cls: MagicMock) -> None: """Test that missing doc files are skipped with a warning.""" with patch("scripts.sync_wiki.MAPPING_FILE") as mock_mapping: mock_mapping.exists.return_value = True with patch("scripts.sync_wiki.load_mapping", return_value={"missing.md": "Missing"}): with patch("scripts.sync_wiki.read_doc_content", side_effect=FileNotFoundError): with patch("scripts.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 assert "Skipped: 1" in result.output @patch.dict("os.environ", {"REPO_TOKEN": "tok"}, clear=True) @patch("scripts.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("scripts.sync_wiki.MAPPING_FILE") as mock_mapping: mock_mapping.exists.return_value = True mapping = {"new.md": "New-Page", "existing.md": "Existing-Page"} with patch("scripts.sync_wiki.load_mapping", return_value=mapping): with patch("scripts.sync_wiki.read_doc_content", return_value="# Content"): with patch("scripts.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