|
1 | 1 | """Tests for AI guardrails policy loading and management.""" |
2 | 2 |
|
3 | 3 | from pathlib import Path |
4 | | -from unittest.mock import patch |
| 4 | +from typing import Optional |
| 5 | +from unittest.mock import MagicMock, patch |
| 6 | + |
| 7 | +from pyfakefs.fake_filesystem import FakeFilesystem |
5 | 8 |
|
6 | 9 | from cycode.cli.apps.ai_guardrails.scan.policy import ( |
7 | 10 | deep_merge, |
@@ -39,36 +42,33 @@ def test_deep_merge_override_with_non_dict() -> None: |
39 | 42 | assert result == {'key': 'simple_value'} |
40 | 43 |
|
41 | 44 |
|
42 | | -def test_load_yaml_file_nonexistent(tmp_path: Path) -> None: |
| 45 | +def test_load_yaml_file_nonexistent(fs: FakeFilesystem) -> None: |
43 | 46 | """Test loading a non-existent file returns None.""" |
44 | | - result = load_yaml_file(tmp_path / 'nonexistent.yaml') |
| 47 | + result = load_yaml_file(Path('/fake/nonexistent.yaml')) |
45 | 48 | assert result is None |
46 | 49 |
|
47 | 50 |
|
48 | | -def test_load_yaml_file_valid_yaml(tmp_path: Path) -> None: |
| 51 | +def test_load_yaml_file_valid_yaml(fs: FakeFilesystem) -> None: |
49 | 52 | """Test loading a valid YAML file.""" |
50 | | - yaml_file = tmp_path / 'config.yaml' |
51 | | - yaml_file.write_text('mode: block\nfail_open: true\n') |
| 53 | + fs.create_file('/fake/config.yaml', contents='mode: block\nfail_open: true\n') |
52 | 54 |
|
53 | | - result = load_yaml_file(yaml_file) |
| 55 | + result = load_yaml_file(Path('/fake/config.yaml')) |
54 | 56 | assert result == {'mode': 'block', 'fail_open': True} |
55 | 57 |
|
56 | 58 |
|
57 | | -def test_load_yaml_file_valid_json(tmp_path: Path) -> None: |
| 59 | +def test_load_yaml_file_valid_json(fs: FakeFilesystem) -> None: |
58 | 60 | """Test loading a valid JSON file.""" |
59 | | - json_file = tmp_path / 'config.json' |
60 | | - json_file.write_text('{"mode": "block", "fail_open": true}') |
| 61 | + fs.create_file('/fake/config.json', contents='{"mode": "block", "fail_open": true}') |
61 | 62 |
|
62 | | - result = load_yaml_file(json_file) |
| 63 | + result = load_yaml_file(Path('/fake/config.json')) |
63 | 64 | assert result == {'mode': 'block', 'fail_open': True} |
64 | 65 |
|
65 | 66 |
|
66 | | -def test_load_yaml_file_invalid_yaml(tmp_path: Path) -> None: |
| 67 | +def test_load_yaml_file_invalid_yaml(fs: FakeFilesystem) -> None: |
67 | 68 | """Test loading an invalid YAML file returns None.""" |
68 | | - yaml_file = tmp_path / 'invalid.yaml' |
69 | | - yaml_file.write_text('{ invalid yaml content [') |
| 69 | + fs.create_file('/fake/invalid.yaml', contents='{ invalid yaml content [') |
70 | 70 |
|
71 | | - result = load_yaml_file(yaml_file) |
| 71 | + result = load_yaml_file(Path('/fake/invalid.yaml')) |
72 | 72 | assert result is None |
73 | 73 |
|
74 | 74 |
|
@@ -123,92 +123,77 @@ def test_get_policy_value_non_dict_in_path() -> None: |
123 | 123 | assert get_policy_value(policy, 'key', 'nested', default='default') == 'default' |
124 | 124 |
|
125 | 125 |
|
126 | | -def test_load_policy_defaults_only() -> None: |
| 126 | +@patch('cycode.cli.apps.ai_guardrails.scan.policy.load_yaml_file') |
| 127 | +def test_load_policy_defaults_only(mock_load: MagicMock) -> None: |
127 | 128 | """Test loading policy with only defaults (no user or repo config).""" |
128 | | - with patch('cycode.cli.apps.ai_guardrails.scan.policy.load_yaml_file') as mock_load: |
129 | | - mock_load.return_value = None # No user or repo config |
| 129 | + mock_load.return_value = None # No user or repo config |
130 | 130 |
|
131 | | - policy = load_policy() |
| 131 | + policy = load_policy() |
132 | 132 |
|
133 | | - assert 'mode' in policy |
134 | | - assert 'fail_open' in policy |
| 133 | + assert 'mode' in policy |
| 134 | + assert 'fail_open' in policy |
135 | 135 |
|
136 | 136 |
|
137 | | -def test_load_policy_with_user_config(tmp_path: Path) -> None: |
| 137 | +@patch('pathlib.Path.home') |
| 138 | +def test_load_policy_with_user_config(mock_home: MagicMock, fs: FakeFilesystem) -> None: |
138 | 139 | """Test loading policy with user config override.""" |
139 | | - with patch('pathlib.Path.home') as mock_home: |
140 | | - mock_home.return_value = tmp_path |
| 140 | + mock_home.return_value = Path('/home/testuser') |
141 | 141 |
|
142 | | - # Create user config |
143 | | - user_config_dir = tmp_path / '.cycode' |
144 | | - user_config_dir.mkdir() |
145 | | - user_config = user_config_dir / 'ai-guardrails.yaml' |
146 | | - user_config.write_text('mode: warn\nfail_open: false\n') |
| 142 | + # Create user config in fake filesystem |
| 143 | + fs.create_file('/home/testuser/.cycode/ai-guardrails.yaml', contents='mode: warn\nfail_open: false\n') |
147 | 144 |
|
148 | | - policy = load_policy() |
| 145 | + policy = load_policy() |
149 | 146 |
|
150 | | - # User config should override defaults |
151 | | - assert policy['mode'] == 'warn' |
152 | | - assert policy['fail_open'] is False |
| 147 | + # User config should override defaults |
| 148 | + assert policy['mode'] == 'warn' |
| 149 | + assert policy['fail_open'] is False |
153 | 150 |
|
154 | 151 |
|
155 | | -def test_load_policy_with_repo_config(tmp_path: Path) -> None: |
| 152 | +@patch('cycode.cli.apps.ai_guardrails.scan.policy.load_yaml_file') |
| 153 | +def test_load_policy_with_repo_config(mock_load: MagicMock) -> None: |
156 | 154 | """Test loading policy with repo config (highest precedence).""" |
157 | | - # Create repo config |
158 | | - repo_config_dir = tmp_path / '.cycode' |
159 | | - repo_config_dir.mkdir() |
160 | | - repo_config = repo_config_dir / 'ai-guardrails.yaml' |
161 | | - repo_config.write_text('mode: block\nprompt:\n enabled: false\n') |
| 155 | + repo_path = Path('/fake/repo') |
| 156 | + repo_config = repo_path / '.cycode' / 'ai-guardrails.yaml' |
162 | 157 |
|
163 | | - with patch('cycode.cli.apps.ai_guardrails.scan.policy.load_yaml_file') as mock_load: |
| 158 | + def side_effect(path: Path) -> Optional[dict]: |
| 159 | + if path == repo_config: |
| 160 | + return {'mode': 'block', 'prompt': {'enabled': False}} |
| 161 | + return None |
164 | 162 |
|
165 | | - def side_effect(path: Path) -> dict | None: |
166 | | - if path == repo_config: |
167 | | - return {'mode': 'block', 'prompt': {'enabled': False}} |
168 | | - return None |
| 163 | + mock_load.side_effect = side_effect |
169 | 164 |
|
170 | | - mock_load.side_effect = side_effect |
| 165 | + policy = load_policy(str(repo_path)) |
171 | 166 |
|
172 | | - policy = load_policy(str(tmp_path)) |
| 167 | + # Repo config should have highest precedence |
| 168 | + assert policy['mode'] == 'block' |
| 169 | + assert policy['prompt']['enabled'] is False |
173 | 170 |
|
174 | | - # Repo config should have highest precedence |
175 | | - assert policy['mode'] == 'block' |
176 | | - assert policy['prompt']['enabled'] is False |
177 | 171 |
|
178 | | - |
179 | | -def test_load_policy_precedence(tmp_path: Path) -> None: |
| 172 | +@patch('pathlib.Path.home') |
| 173 | +def test_load_policy_precedence(mock_home: MagicMock, fs: FakeFilesystem) -> None: |
180 | 174 | """Test that policy precedence is: defaults < user < repo.""" |
181 | | - with patch('pathlib.Path.home') as mock_home: |
182 | | - mock_home.return_value = tmp_path |
| 175 | + mock_home.return_value = Path('/home/testuser') |
183 | 176 |
|
184 | | - # Create user config |
185 | | - user_config_dir = tmp_path / '.cycode' |
186 | | - user_config_dir.mkdir() |
187 | | - user_config = user_config_dir / 'ai-guardrails.yaml' |
188 | | - user_config.write_text('mode: warn\nfail_open: false\n') |
| 177 | + # Create user config |
| 178 | + fs.create_file('/home/testuser/.cycode/ai-guardrails.yaml', contents='mode: warn\nfail_open: false\n') |
189 | 179 |
|
190 | | - # Create repo config in a different location |
191 | | - repo_path = tmp_path / 'repo' |
192 | | - repo_path.mkdir() |
193 | | - repo_config_dir = repo_path / '.cycode' |
194 | | - repo_config_dir.mkdir() |
195 | | - repo_config = repo_config_dir / 'ai-guardrails.yaml' |
196 | | - repo_config.write_text('mode: block\n') # Override mode but not fail_open |
| 180 | + # Create repo config |
| 181 | + fs.create_file('/fake/repo/.cycode/ai-guardrails.yaml', contents='mode: block\n') |
197 | 182 |
|
198 | | - policy = load_policy(str(repo_path)) |
| 183 | + policy = load_policy('/fake/repo') |
199 | 184 |
|
200 | | - # mode should come from repo (highest precedence) |
201 | | - assert policy['mode'] == 'block' |
202 | | - # fail_open should come from user config (repo doesn't override it) |
203 | | - assert policy['fail_open'] is False |
| 185 | + # mode should come from repo (highest precedence) |
| 186 | + assert policy['mode'] == 'block' |
| 187 | + # fail_open should come from user config (repo doesn't override it) |
| 188 | + assert policy['fail_open'] is False |
204 | 189 |
|
205 | 190 |
|
206 | | -def test_load_policy_none_workspace_root() -> None: |
| 191 | +@patch('cycode.cli.apps.ai_guardrails.scan.policy.load_yaml_file') |
| 192 | +def test_load_policy_none_workspace_root(mock_load: MagicMock) -> None: |
207 | 193 | """Test that None workspace_root is handled correctly.""" |
208 | | - with patch('cycode.cli.apps.ai_guardrails.scan.policy.load_yaml_file') as mock_load: |
209 | | - mock_load.return_value = None |
| 194 | + mock_load.return_value = None |
210 | 195 |
|
211 | | - policy = load_policy(None) |
| 196 | + policy = load_policy(None) |
212 | 197 |
|
213 | | - # Should only load defaults (no repo config) |
214 | | - assert 'mode' in policy |
| 198 | + # Should only load defaults (no repo config) |
| 199 | + assert 'mode' in policy |
0 commit comments