regscale-cli 6.23.0.0__py3-none-any.whl → 6.24.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of regscale-cli might be problematic. Click here for more details.

Files changed (44) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/application.py +2 -0
  3. regscale/integrations/commercial/__init__.py +1 -0
  4. regscale/integrations/commercial/sarif/sarif_converter.py +1 -1
  5. regscale/integrations/commercial/wizv2/click.py +109 -2
  6. regscale/integrations/commercial/wizv2/compliance_report.py +1485 -0
  7. regscale/integrations/commercial/wizv2/constants.py +72 -2
  8. regscale/integrations/commercial/wizv2/data_fetcher.py +61 -0
  9. regscale/integrations/commercial/wizv2/file_cleanup.py +104 -0
  10. regscale/integrations/commercial/wizv2/issue.py +775 -27
  11. regscale/integrations/commercial/wizv2/policy_compliance.py +599 -181
  12. regscale/integrations/commercial/wizv2/reports.py +243 -0
  13. regscale/integrations/commercial/wizv2/scanner.py +668 -245
  14. regscale/integrations/compliance_integration.py +304 -51
  15. regscale/integrations/due_date_handler.py +210 -0
  16. regscale/integrations/public/cci_importer.py +444 -0
  17. regscale/integrations/scanner_integration.py +718 -153
  18. regscale/models/integration_models/CCI_List.xml +1 -0
  19. regscale/models/integration_models/cisa_kev_data.json +61 -3
  20. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  21. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +3 -3
  22. regscale/models/regscale_models/form_field_value.py +1 -1
  23. regscale/models/regscale_models/milestone.py +1 -0
  24. regscale/models/regscale_models/regscale_model.py +225 -60
  25. regscale/models/regscale_models/security_plan.py +3 -2
  26. regscale/regscale.py +7 -0
  27. {regscale_cli-6.23.0.0.dist-info → regscale_cli-6.24.0.0.dist-info}/METADATA +9 -9
  28. {regscale_cli-6.23.0.0.dist-info → regscale_cli-6.24.0.0.dist-info}/RECORD +44 -27
  29. tests/fixtures/test_fixture.py +13 -8
  30. tests/regscale/integrations/public/__init__.py +0 -0
  31. tests/regscale/integrations/public/test_alienvault.py +220 -0
  32. tests/regscale/integrations/public/test_cci.py +458 -0
  33. tests/regscale/integrations/public/test_cisa.py +1021 -0
  34. tests/regscale/integrations/public/test_emass.py +518 -0
  35. tests/regscale/integrations/public/test_fedramp.py +851 -0
  36. tests/regscale/integrations/public/test_fedramp_cis_crm.py +3661 -0
  37. tests/regscale/integrations/public/test_file_uploads.py +506 -0
  38. tests/regscale/integrations/public/test_oscal.py +453 -0
  39. tests/regscale/models/test_form_field_value_integration.py +304 -0
  40. tests/regscale/models/test_module_integration.py +582 -0
  41. {regscale_cli-6.23.0.0.dist-info → regscale_cli-6.24.0.0.dist-info}/LICENSE +0 -0
  42. {regscale_cli-6.23.0.0.dist-info → regscale_cli-6.24.0.0.dist-info}/WHEEL +0 -0
  43. {regscale_cli-6.23.0.0.dist-info → regscale_cli-6.24.0.0.dist-info}/entry_points.txt +0 -0
  44. {regscale_cli-6.23.0.0.dist-info → regscale_cli-6.24.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,458 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Tests for CCI Importer functionality."""
4
+ import xml.etree.ElementTree as ET
5
+ from unittest.mock import MagicMock, patch
6
+
7
+ import pytest
8
+ from click.testing import CliRunner
9
+
10
+ from regscale.integrations.public.cci_importer import CCIImporter, cci_importer, _load_xml_file
11
+ from tests import CLITestFixture
12
+
13
+
14
+ class TestCCIImporter(CLITestFixture):
15
+ """Test cases for the CCIImporter class."""
16
+
17
+ @pytest.fixture
18
+ def sample_xml_data(self):
19
+ """Create sample XML data for testing."""
20
+ xml_content = """<?xml version="1.0" encoding="utf-8"?>
21
+ <cci_list xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
22
+ xmlns="http://iase.disa.mil/cci">
23
+ <cci_item id="CCI-000001">
24
+ <definition>The organization develops, documents, and disseminates access control policy.</definition>
25
+ <references>
26
+ <reference creator="NIST" title="NIST SP 800-53 Revision 5" version="5"
27
+ location="AC-1" index="AC-1 a" />
28
+ </references>
29
+ </cci_item>
30
+ <cci_item id="CCI-000002">
31
+ <definition>The organization reviews and updates access control policy.</definition>
32
+ <references>
33
+ <reference creator="NIST" title="NIST SP 800-53 Revision 5" version="5"
34
+ location="AC-1" index="AC-1 b" />
35
+ <reference creator="NIST" title="NIST SP 800-53 Revision 4" version="4"
36
+ location="AC-1" index="AC-1 b" />
37
+ </references>
38
+ </cci_item>
39
+ <cci_item id="CCI-000003">
40
+ <definition>The organization develops access control procedures.</definition>
41
+ <references>
42
+ <reference creator="NIST" title="NIST SP 800-53 Revision 5" version="5"
43
+ location="AC-2" index="AC-2 a 1" />
44
+ </references>
45
+ </cci_item>
46
+ </cci_list>"""
47
+ return ET.fromstring(xml_content)
48
+
49
+ @pytest.fixture
50
+ def cci_importer_instance(self, sample_xml_data):
51
+ """Create a CCIImporter instance for testing."""
52
+ return CCIImporter(sample_xml_data, version="5", verbose=False)
53
+
54
+ def test_init(self, sample_xml_data):
55
+ """Test CCIImporter initialization."""
56
+ importer = CCIImporter(sample_xml_data, version="4", verbose=True)
57
+
58
+ assert importer.xml_data == sample_xml_data
59
+ assert importer.reference_version == "4"
60
+ assert importer.verbose is True
61
+ assert importer.normalized_cci == {}
62
+ assert importer._user_context is None
63
+
64
+ def test_parse_control_id(self, cci_importer_instance):
65
+ """Test parsing control IDs from reference indices."""
66
+ assert cci_importer_instance._parse_control_id("AC-1 a 1 (b)") == "AC-1"
67
+ assert cci_importer_instance._parse_control_id("SC-7") == "SC-7"
68
+ assert cci_importer_instance._parse_control_id("") == ""
69
+ assert cci_importer_instance._parse_control_id(" ") == ""
70
+
71
+ def test_extract_cci_data(self, cci_importer_instance, sample_xml_data):
72
+ """Test extracting CCI ID and definition from XML elements."""
73
+ cci_item = sample_xml_data.find(".//{http://iase.disa.mil/cci}cci_item")
74
+ cci_id, definition = cci_importer_instance._extract_cci_data(cci_item)
75
+
76
+ assert cci_id == "CCI-000001"
77
+ assert "organization develops, documents, and disseminates access control policy" in definition
78
+
79
+ def test_is_valid_reference(self, cci_importer_instance, sample_xml_data):
80
+ """Test validation of reference elements based on version."""
81
+ references = sample_xml_data.findall(".//{http://iase.disa.mil/cci}reference")
82
+
83
+ # Should accept version 5 references
84
+ version_5_refs = [ref for ref in references if ref.get("version") == "5"]
85
+ assert len(version_5_refs) > 0
86
+ assert cci_importer_instance._is_valid_reference(version_5_refs[0]) is True
87
+
88
+ # Should reject version 4 references when configured for version 5
89
+ version_4_refs = [ref for ref in references if ref.get("version") == "4"]
90
+ if version_4_refs:
91
+ assert cci_importer_instance._is_valid_reference(version_4_refs[0]) is False
92
+
93
+ def test_parse_cci(self, cci_importer_instance):
94
+ """Test parsing CCI items and normalizing them."""
95
+ cci_importer_instance.parse_cci()
96
+ normalized = cci_importer_instance.get_normalized_cci()
97
+
98
+ # Should have parsed AC-1 and AC-2 controls
99
+ assert "AC-1" in normalized
100
+ assert "AC-2" in normalized
101
+
102
+ # AC-1 should have 2 CCI items (CCI-000001 and CCI-000002)
103
+ assert len(normalized["AC-1"]) == 2
104
+
105
+ # AC-2 should have 1 CCI item (CCI-000003)
106
+ assert len(normalized["AC-2"]) == 1
107
+
108
+ # Check CCI content
109
+ ac1_ccis = normalized["AC-1"]
110
+ cci_ids = [cci["cci_id"] for cci in ac1_ccis]
111
+ assert "CCI-000001" in cci_ids
112
+ assert "CCI-000002" in cci_ids
113
+
114
+ @patch("regscale.integrations.public.cci_importer.Application")
115
+ def test_get_user_context(self, mock_app_class, cci_importer_instance):
116
+ """Test getting user context from application config."""
117
+ mock_app = MagicMock()
118
+ mock_app.config.get.side_effect = lambda key, default=None: {"userId": "123", "tenantId": 456}.get(key, default)
119
+ mock_app_class.return_value = mock_app
120
+
121
+ user_id, tenant_id = cci_importer_instance._get_user_context()
122
+
123
+ assert user_id == "123" # Code keeps user_id as string (UUID)
124
+ assert tenant_id == 456
125
+
126
+ # Should cache the result
127
+ user_id2, tenant_id2 = cci_importer_instance._get_user_context()
128
+ assert user_id2 == "123"
129
+ assert tenant_id2 == 456
130
+
131
+ @patch("regscale.integrations.public.cci_importer.Application")
132
+ def test_get_user_context_none_user_id(self, mock_app_class, cci_importer_instance):
133
+ """Test handling None user ID in config."""
134
+ mock_app = MagicMock()
135
+ mock_app.config.get.side_effect = lambda key, default=None: {"userId": None, "tenantId": "456"}.get(
136
+ key, default
137
+ )
138
+ mock_app_class.return_value = mock_app
139
+
140
+ user_id, tenant_id = cci_importer_instance._get_user_context()
141
+
142
+ assert user_id is None
143
+ assert tenant_id == 456 # tenant_id converted to int
144
+
145
+ @patch("regscale.models.regscale_models.Catalog.get")
146
+ def test_get_catalog_success(self, mock_get, cci_importer_instance):
147
+ """Test successful catalog retrieval."""
148
+ mock_catalog = MagicMock()
149
+ mock_get.return_value = mock_catalog
150
+
151
+ result = cci_importer_instance._get_catalog(1)
152
+
153
+ assert result == mock_catalog
154
+ mock_get.assert_called_once_with(id=1)
155
+
156
+ @patch("regscale.integrations.public.cci_importer.error_and_exit")
157
+ @patch("regscale.models.regscale_models.Catalog.get")
158
+ def test_get_catalog_not_found(self, mock_get, mock_error_exit, cci_importer_instance):
159
+ """Test catalog not found scenario."""
160
+ mock_get.return_value = None
161
+ mock_error_exit.side_effect = SystemExit(1) # Mock the actual exit behavior
162
+
163
+ with pytest.raises(SystemExit):
164
+ cci_importer_instance._get_catalog(999)
165
+
166
+ mock_error_exit.assert_called_once_with("Catalog with id 999 not found. Please ensure the catalog exists.")
167
+
168
+ @patch("regscale.models.regscale_models.CCI.get_all_by_parent")
169
+ def test_find_existing_cci(self, mock_get_all, cci_importer_instance):
170
+ """Test finding existing CCI by ID."""
171
+ mock_cci1 = MagicMock()
172
+ mock_cci1.uuid = "CCI-000001"
173
+ mock_cci2 = MagicMock()
174
+ mock_cci2.uuid = "CCI-000002"
175
+
176
+ mock_get_all.return_value = [mock_cci1, mock_cci2]
177
+
178
+ result = cci_importer_instance._find_existing_cci(123, "CCI-000001")
179
+ assert result == mock_cci1
180
+
181
+ result = cci_importer_instance._find_existing_cci(123, "CCI-999999")
182
+ assert result is None
183
+
184
+ def test_create_cci_data(self, cci_importer_instance):
185
+ """Test creating CCI data structure."""
186
+ current_time = "2023-01-01 12:00:00"
187
+
188
+ result = cci_importer_instance._create_cci_data("CCI-000001", "Test definition", "uuid-123", 456, current_time)
189
+
190
+ expected = {
191
+ "name": "CCI-000001",
192
+ "description": "Test definition",
193
+ "controlType": "policy",
194
+ "publishDate": current_time,
195
+ "dateLastUpdated": current_time,
196
+ "lastUpdatedById": "uuid-123",
197
+ "isPublic": True,
198
+ "tenantsId": 456,
199
+ }
200
+
201
+ assert result == expected
202
+
203
+ def test_create_cci_data_no_user(self, cci_importer_instance):
204
+ """Test creating CCI data with no user ID."""
205
+ current_time = "2023-01-01 12:00:00"
206
+
207
+ result = cci_importer_instance._create_cci_data("CCI-000001", "Test definition", None, 456, current_time)
208
+
209
+ assert result["lastUpdatedById"] is None
210
+
211
+ @patch("regscale.models.regscale_models.CCI")
212
+ def test_update_existing_cci(self, mock_cci_class, cci_importer_instance):
213
+ """Test updating an existing CCI."""
214
+ mock_cci = MagicMock()
215
+ cci_data = {"name": "Updated Name", "description": "Updated Description"}
216
+
217
+ cci_importer_instance._update_existing_cci(mock_cci, cci_data)
218
+
219
+ assert mock_cci.name == "Updated Name"
220
+ assert mock_cci.description == "Updated Description"
221
+ mock_cci.create_or_update.assert_called_once()
222
+
223
+ @patch("regscale.integrations.public.cci_importer.CCI")
224
+ def test_create_new_cci(self, mock_cci_class, cci_importer_instance):
225
+ """Test creating a new CCI."""
226
+ mock_cci = MagicMock()
227
+ mock_cci_class.return_value = mock_cci
228
+ mock_cci.create.return_value = mock_cci # Mock the create method return
229
+
230
+ cci_data = {"name": "CCI-000001", "description": "Test definition"}
231
+ current_time = "2023-01-01 12:00:00"
232
+
233
+ result = cci_importer_instance._create_new_cci("CCI-000001", cci_data, 123, "uuid-456", current_time)
234
+
235
+ mock_cci_class.assert_called_once_with(
236
+ uuid="CCI-000001", securityControlId=123, createdById="uuid-456", dateCreated=current_time, **cci_data
237
+ )
238
+ mock_cci.create.assert_called_once()
239
+ assert result == mock_cci
240
+
241
+ @patch("regscale.integrations.public.cci_importer.CCIImporter._get_user_context")
242
+ @patch("regscale.integrations.public.cci_importer.CCIImporter._find_existing_cci")
243
+ @patch("regscale.integrations.public.cci_importer.CCIImporter._create_new_cci")
244
+ @patch("regscale.integrations.public.cci_importer.CCIImporter._update_existing_cci")
245
+ def test_process_cci_for_control(self, mock_update, mock_create, mock_find, mock_context, cci_importer_instance):
246
+ """Test processing CCI items for a control."""
247
+ mock_context.return_value = ("uuid-123", 456)
248
+ mock_find.side_effect = [None, MagicMock()] # First not found, second found
249
+ mock_create.return_value = MagicMock()
250
+
251
+ cci_list = [
252
+ {"cci_id": "CCI-000001", "definition": "Definition 1"},
253
+ {"cci_id": "CCI-000002", "definition": "Definition 2"},
254
+ ]
255
+
256
+ created, updated = cci_importer_instance._process_cci_for_control(789, cci_list, "uuid-123", 456)
257
+
258
+ assert created == 1
259
+ assert updated == 1
260
+ mock_create.assert_called_once()
261
+ mock_update.assert_called_once()
262
+
263
+ @patch("regscale.integrations.public.cci_importer.CCIImporter._get_catalog")
264
+ @patch("regscale.integrations.public.cci_importer.CCIImporter._get_user_context")
265
+ @patch("regscale.integrations.public.cci_importer.CCIImporter._process_cci_for_control")
266
+ @patch("regscale.models.regscale_models.SecurityControl.get_all_by_parent")
267
+ def test_map_to_security_controls(
268
+ self, mock_get_controls, mock_process, mock_context, mock_catalog, cci_importer_instance
269
+ ):
270
+ """Test mapping CCI data to security controls."""
271
+ # Setup mocks
272
+ mock_catalog_obj = MagicMock()
273
+ mock_catalog_obj.id = 1
274
+ mock_catalog.return_value = mock_catalog_obj
275
+
276
+ mock_control1 = MagicMock()
277
+ mock_control1.controlId = "AC-1"
278
+ mock_control1.id = 101
279
+ mock_control2 = MagicMock()
280
+ mock_control2.controlId = "AC-2"
281
+ mock_control2.id = 102
282
+ mock_get_controls.return_value = [mock_control1, mock_control2]
283
+
284
+ mock_context.return_value = ("uuid-123", 456)
285
+ mock_process.return_value = (2, 1) # 2 created, 1 updated
286
+
287
+ # Setup test data
288
+ cci_importer_instance.normalized_cci = {
289
+ "AC-1": [{"cci_id": "CCI-000001", "definition": "Definition 1"}],
290
+ "AC-2": [{"cci_id": "CCI-000002", "definition": "Definition 2"}],
291
+ "AC-99": [{"cci_id": "CCI-000099", "definition": "Definition 99"}], # Non-existent control
292
+ }
293
+
294
+ result = cci_importer_instance.map_to_security_controls(catalog_id=1)
295
+
296
+ assert result["created"] == 4 # 2 calls * 2 created each
297
+ assert result["updated"] == 2 # 2 calls * 1 updated each
298
+ assert result["skipped"] == 1 # AC-99 control not found
299
+ assert result["total_processed"] == 3
300
+
301
+ mock_catalog.assert_called_once_with(1)
302
+ assert mock_process.call_count == 2 # Called for AC-1 and AC-2
303
+
304
+ def test_get_normalized_cci(self, cci_importer_instance):
305
+ """Test getting normalized CCI data."""
306
+ test_data = {"AC-1": [{"cci_id": "CCI-000001", "definition": "Test"}]}
307
+ cci_importer_instance.normalized_cci = test_data
308
+
309
+ result = cci_importer_instance.get_normalized_cci()
310
+ assert result == test_data
311
+
312
+
313
+ class TestCCIImporterCLI:
314
+ """Test cases for the CCI importer CLI command."""
315
+
316
+ @patch("regscale.integrations.public.cci_importer._load_xml_file")
317
+ @patch("regscale.integrations.public.cci_importer.CCIImporter")
318
+ def test_cci_importer_command_dry_run(self, mock_importer_class, mock_load_xml):
319
+ """Test CLI command with dry run flag."""
320
+ runner = CliRunner()
321
+
322
+ # Setup mocks
323
+ mock_root = MagicMock()
324
+ mock_load_xml.return_value = mock_root
325
+
326
+ mock_importer = MagicMock()
327
+ mock_importer.get_normalized_cci.return_value = {"AC-1": []}
328
+ mock_importer_class.return_value = mock_importer
329
+
330
+ result = runner.invoke(cci_importer, ["--dry-run", "--verbose"])
331
+
332
+ # More detailed assertion with error context
333
+ if result.exit_code != 0:
334
+ print(f"Command output: {result.output}")
335
+ print(f"Exception: {result.exception}")
336
+ assert result.exit_code == 0, f"Expected exit code 0, got {result.exit_code}. Output: {result.output}"
337
+ mock_load_xml.assert_called_once()
338
+ mock_importer_class.assert_called_once_with(mock_root, version="5", verbose=True)
339
+ mock_importer.parse_cci.assert_called_once()
340
+ mock_importer.map_to_security_controls.assert_not_called()
341
+
342
+ @patch("regscale.integrations.public.cci_importer._load_xml_file")
343
+ @patch("regscale.integrations.public.cci_importer.CCIImporter")
344
+ def test_cci_importer_command_with_database(self, mock_importer_class, mock_load_xml):
345
+ """Test CLI command with database operations."""
346
+ runner = CliRunner()
347
+
348
+ # Setup mocks
349
+ mock_root = MagicMock()
350
+ mock_load_xml.return_value = mock_root
351
+
352
+ mock_importer = MagicMock()
353
+ mock_importer.get_normalized_cci.return_value = {"AC-1": []}
354
+ mock_importer.map_to_security_controls.return_value = {
355
+ "created": 5,
356
+ "updated": 3,
357
+ "skipped": 1,
358
+ "total_processed": 2,
359
+ }
360
+ mock_importer_class.return_value = mock_importer
361
+
362
+ result = runner.invoke(cci_importer, ["-n", "4", "-c", "2"])
363
+
364
+ # More detailed assertion with error context
365
+ if result.exit_code != 0:
366
+ print(f"Command output: {result.output}")
367
+ print(f"Exception: {result.exception}")
368
+ assert result.exit_code == 0, f"Expected exit code 0, got {result.exit_code}. Output: {result.output}"
369
+ mock_importer_class.assert_called_once_with(mock_root, version="4", verbose=False)
370
+ mock_importer.map_to_security_controls.assert_called_with(2)
371
+
372
+ @patch("regscale.integrations.public.cci_importer._load_xml_file")
373
+ def test_cci_importer_command_xml_error(self, mock_load_xml):
374
+ """Test CLI command with XML loading error."""
375
+ runner = CliRunner()
376
+
377
+ mock_load_xml.side_effect = ET.ParseError("Invalid XML")
378
+
379
+ result = runner.invoke(cci_importer)
380
+
381
+ assert result.exit_code != 0
382
+
383
+ def test_load_xml_file_success(self, tmp_path):
384
+ """Test successful XML file loading."""
385
+ xml_content = """<?xml version="1.0" encoding="utf-8"?>
386
+ <cci_list xmlns="http://iase.disa.mil/cci">
387
+ <cci_item id="CCI-000001">
388
+ <definition>Test definition</definition>
389
+ </cci_item>
390
+ </cci_list>"""
391
+
392
+ xml_file = tmp_path / "test.xml"
393
+ xml_file.write_text(xml_content)
394
+
395
+ root = _load_xml_file(str(xml_file))
396
+
397
+ assert root is not None
398
+ assert root.tag.endswith("cci_list")
399
+
400
+ def test_load_xml_file_parse_error(self, tmp_path):
401
+ """Test XML file loading with parse error."""
402
+ invalid_xml = "This is not valid XML"
403
+
404
+ xml_file = tmp_path / "invalid.xml"
405
+ xml_file.write_text(invalid_xml)
406
+
407
+ with pytest.raises(SystemExit):
408
+ _load_xml_file(str(xml_file))
409
+
410
+
411
+ class TestCCIImporterIntegration:
412
+ """Integration tests for CCI importer."""
413
+
414
+ def test_full_workflow_dry_run(self):
415
+ """Test full workflow in dry run mode."""
416
+ xml_content = """<?xml version="1.0" encoding="utf-8"?>
417
+ <cci_list xmlns="http://iase.disa.mil/cci">
418
+ <cci_item id="CCI-000001">
419
+ <definition>Test definition for AC-1</definition>
420
+ <references>
421
+ <reference creator="NIST" title="NIST SP 800-53 Revision 5" version="5"
422
+ location="AC-1" index="AC-1 a" />
423
+ </references>
424
+ </cci_item>
425
+ </cci_list>"""
426
+
427
+ root = ET.fromstring(xml_content)
428
+ importer = CCIImporter(root, version="5", verbose=False)
429
+
430
+ # Parse CCI data
431
+ importer.parse_cci()
432
+ normalized = importer.get_normalized_cci()
433
+
434
+ # Verify parsing worked correctly
435
+ assert "AC-1" in normalized
436
+ assert len(normalized["AC-1"]) == 1
437
+ assert normalized["AC-1"][0]["cci_id"] == "CCI-000001"
438
+ assert "Test definition for AC-1" in normalized["AC-1"][0]["definition"]
439
+
440
+ @patch("regscale.integrations.public.cci_importer.Application")
441
+ def test_user_context_caching(self, mock_app_class):
442
+ """Test that user context is properly cached."""
443
+ mock_app = MagicMock()
444
+ mock_app.config.get.side_effect = lambda key, default=None: {"userId": "uuid-123", "tenantId": "456"}.get(
445
+ key, default
446
+ )
447
+ mock_app_class.return_value = mock_app
448
+
449
+ root = ET.fromstring("<root></root>")
450
+ importer = CCIImporter(root)
451
+
452
+ # First call should create the context
453
+ context1 = importer._get_user_context()
454
+ # Second call should use cached context
455
+ context2 = importer._get_user_context()
456
+
457
+ assert context1 == context2
458
+ assert mock_app_class.call_count == 1 # Should only create Application once