OSVauco/opax-mcp/test_propose_gitea_change.py

126 lines
5.9 KiB
Python

import unittest
import server # Følger eksisterende mønster
import hashlib
from unittest.mock import patch, AsyncMock
class TestProposeGiteaChange(unittest.IsolatedAsyncioTestCase):
def setUp(self):
"""Set up valid parameters for tests."""
self.valid_params = {
"repo": server.GITEA_REPO,
"branch": "feature/new-idea",
"path": "docs/new-file.md",
"new_content": "This is new content.",
"base_sha": "a" * 40,
"commit_message": "A valid commit message.",
}
def tearDown(self):
patch.stopall()
@patch('server.create_gitea_change_plan', new_callable=AsyncMock)
@patch('server._get_gitea_file_details', new_callable=AsyncMock)
@patch('server.resolve_branch_to_commit_sha', new_callable=AsyncMock)
async def test_rejects_main_branch_before_io(self, mock_resolve_sha, mock_get_details, mock_create_plan):
params = self.valid_params | {"branch": "main"}
with self.assertRaisesRegex(ValueError, "Direct writes to protected branch 'main' are not allowed."):
await server.propose_gitea_change(params)
mock_resolve_sha.assert_not_awaited()
mock_get_details.assert_not_awaited()
mock_create_plan.assert_not_awaited()
@patch('server.create_gitea_change_plan', new_callable=AsyncMock)
@patch('server._get_gitea_file_details', new_callable=AsyncMock)
@patch('server.resolve_branch_to_commit_sha', new_callable=AsyncMock)
async def test_rejects_master_branch_before_io(self, mock_resolve_sha, mock_get_details, mock_create_plan):
params = self.valid_params | {"branch": "master"}
with self.assertRaisesRegex(ValueError, "Direct writes to protected branch 'master' are not allowed."):
await server.propose_gitea_change(params)
mock_resolve_sha.assert_not_awaited()
mock_get_details.assert_not_awaited()
mock_create_plan.assert_not_awaited()
@patch("server.create_gitea_change_plan", new_callable=AsyncMock)
@patch("server._get_gitea_file_details", new_callable=AsyncMock)
@patch("server.resolve_branch_to_commit_sha", new_callable=AsyncMock)
@patch("server._validate_admin_gitea_path")
async def test_rejects_dot_git_path_via_new_write_policy(
self,
mock_admin_path_validator,
mock_resolve_sha,
mock_get_details,
mock_create_plan,
):
params = self.valid_params | {"path": "some/dir/.git/config"}
with self.assertRaisesRegex(
ValueError,
r"Changes within a '.git' directory are not allowed.",
):
await server.propose_gitea_change(params)
mock_admin_path_validator.assert_called_once_with(params["path"])
mock_resolve_sha.assert_not_awaited()
mock_get_details.assert_not_awaited()
mock_create_plan.assert_not_awaited()
@patch('server.create_gitea_change_plan', new_callable=AsyncMock)
@patch('server._get_gitea_file_details', new_callable=AsyncMock)
@patch('server.resolve_branch_to_commit_sha', new_callable=AsyncMock)
async def test_rejects_base_sha_mismatch(self, mock_resolve_sha, mock_get_details, mock_create_plan):
mock_resolve_sha.return_value = "c" * 40 # Mismatched SHA
with self.assertRaisesRegex(ValueError, "Branch head does not match the supplied base_sha"):
await server.propose_gitea_change(self.valid_params)
mock_resolve_sha.assert_awaited_once()
mock_get_details.assert_not_awaited()
mock_create_plan.assert_not_awaited()
@patch('server.create_gitea_change_plan', new_callable=AsyncMock)
@patch('server._get_gitea_file_details', new_callable=AsyncMock)
@patch('server.resolve_branch_to_commit_sha', new_callable=AsyncMock)
async def test_rejects_no_op_diff(self, mock_resolve_sha, mock_get_details, mock_create_plan):
mock_resolve_sha.return_value = self.valid_params["base_sha"]
mock_get_details.return_value = (self.valid_params["new_content"], "b" * 40)
with self.assertRaisesRegex(ValueError, "Proposed content produces no file change."):
await server.propose_gitea_change(self.valid_params)
mock_resolve_sha.assert_awaited_once()
mock_get_details.assert_awaited_once()
mock_create_plan.assert_not_awaited()
@patch('server.create_gitea_change_plan', new_callable=AsyncMock)
@patch('server._get_gitea_file_details', new_callable=AsyncMock, return_value=("old content", "b" * 40))
@patch('server.resolve_branch_to_commit_sha', new_callable=AsyncMock)
async def test_happy_path_creates_pending_plan(self, mock_resolve_sha, mock_get_details, mock_create_plan):
mock_resolve_sha.return_value = self.valid_params["base_sha"]
result = await server.propose_gitea_change(self.valid_params)
self.assertEqual(result["status"], "PENDING")
self.assertEqual(result["repo"], self.valid_params["repo"])
self.assertEqual(result["branch"], self.valid_params["branch"])
self.assertEqual(result["path"], self.valid_params["path"])
expected_hash = hashlib.sha256(self.valid_params["new_content"].encode("utf-8")).hexdigest()
self.assertEqual(result["content_hash"], expected_hash)
mock_create_plan.assert_awaited_once()
# Verify the object passed to create_gitea_change_plan is the real Pydantic model
call_args = mock_create_plan.call_args[0][0]
self.assertIsInstance(call_args, server.GiteaChangePlan)
self.assertEqual(call_args.repo, self.valid_params["repo"])
self.assertEqual(call_args.branch, self.valid_params["branch"])
self.assertEqual(call_args.path, self.valid_params["path"])
self.assertEqual(call_args.base_sha, self.valid_params["base_sha"])
self.assertEqual(call_args.existing_file_sha, "b" * 40)
if __name__ == "__main__":
unittest.main()