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()