regscale-cli 6.23.0.1__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 (43) 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 +18 -3
  20. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  21. regscale/models/regscale_models/form_field_value.py +1 -1
  22. regscale/models/regscale_models/milestone.py +1 -0
  23. regscale/models/regscale_models/regscale_model.py +225 -60
  24. regscale/models/regscale_models/security_plan.py +3 -2
  25. regscale/regscale.py +7 -0
  26. {regscale_cli-6.23.0.1.dist-info → regscale_cli-6.24.0.0.dist-info}/METADATA +9 -9
  27. {regscale_cli-6.23.0.1.dist-info → regscale_cli-6.24.0.0.dist-info}/RECORD +43 -26
  28. tests/fixtures/test_fixture.py +13 -8
  29. tests/regscale/integrations/public/__init__.py +0 -0
  30. tests/regscale/integrations/public/test_alienvault.py +220 -0
  31. tests/regscale/integrations/public/test_cci.py +458 -0
  32. tests/regscale/integrations/public/test_cisa.py +1021 -0
  33. tests/regscale/integrations/public/test_emass.py +518 -0
  34. tests/regscale/integrations/public/test_fedramp.py +851 -0
  35. tests/regscale/integrations/public/test_fedramp_cis_crm.py +3661 -0
  36. tests/regscale/integrations/public/test_file_uploads.py +506 -0
  37. tests/regscale/integrations/public/test_oscal.py +453 -0
  38. tests/regscale/models/test_form_field_value_integration.py +304 -0
  39. tests/regscale/models/test_module_integration.py +582 -0
  40. {regscale_cli-6.23.0.1.dist-info → regscale_cli-6.24.0.0.dist-info}/LICENSE +0 -0
  41. {regscale_cli-6.23.0.1.dist-info → regscale_cli-6.24.0.0.dist-info}/WHEEL +0 -0
  42. {regscale_cli-6.23.0.1.dist-info → regscale_cli-6.24.0.0.dist-info}/entry_points.txt +0 -0
  43. {regscale_cli-6.23.0.1.dist-info → regscale_cli-6.24.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,518 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Test eMASS Integration"""
4
+
5
+ import os
6
+ import sys
7
+ import tempfile
8
+ from pathlib import Path
9
+
10
+ import pytest
11
+ import pandas as pd
12
+ from click.testing import CliRunner
13
+ from openpyxl import Workbook
14
+ from openpyxl.reader.excel import load_workbook
15
+ from openpyxl.styles import PatternFill
16
+ from openpyxl.comments import Comment
17
+ import unittest.mock as mock
18
+
19
+ import regscale.integrations.public.emass as emass_mod
20
+ from regscale.integrations.public.emass import (
21
+ populate_assessment_results,
22
+ populate_emass_workbook,
23
+ import_emass_slcm_file,
24
+ determine_assessment_result,
25
+ map_finish_date,
26
+ map_ccis,
27
+ fetch_template_from_blob,
28
+ populate_emass_workbook,
29
+ SKIP_ROWS,
30
+ )
31
+ from tests import CLITestFixture
32
+
33
+
34
+ class TestEmass(CLITestFixture):
35
+ """Integration, unit, and CLI callback tests for the eMASS CLI integration logic."""
36
+
37
+ @pytest.fixture(autouse=True)
38
+ def setup_test_environment(self, create_security_plan):
39
+ """Setup test environment with dynamic security plan creation."""
40
+ self.security_plan = create_security_plan
41
+ self.template_workbook = ""
42
+ self.output_workbook = Path()
43
+ yield
44
+ # Cleanup will be handled by create_security_plan fixture
45
+
46
+ @pytest.fixture(autouse=True)
47
+ def patch_dependencies(self, tmp_path):
48
+ """Patches I/O and external fetch logic to isolate tests from real API or filesystem."""
49
+ with mock.patch.object(
50
+ emass_mod,
51
+ "fetch_assessments_and_controls",
52
+ side_effect=lambda ssp_id, api: [
53
+ {
54
+ "ccis": ["CCI-123456"],
55
+ "assessments": [
56
+ {
57
+ "id": 1,
58
+ "actualFinish": "2024-01-01",
59
+ "assessmentResult": "Pass",
60
+ "summaryOfResults": "OK",
61
+ "leadAssessor": {"firstName": "Test", "lastName": "User"},
62
+ }
63
+ ],
64
+ }
65
+ ],
66
+ ), mock.patch.object(
67
+ emass_mod, "check_file_path", side_effect=lambda path: Path(path).mkdir(exist_ok=True)
68
+ ), mock.patch.object(
69
+ pd, "read_excel", side_effect=lambda file, skiprows: pd.DataFrame({"CCI": [123456]})
70
+ ):
71
+ yield
72
+
73
+ def create_template(self):
74
+ """Creates a temporary Excel file with mock CCI data."""
75
+ tmp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx")
76
+ wb = Workbook()
77
+ ws = wb.active
78
+ for _ in range(SKIP_ROWS - 2):
79
+ ws.append([])
80
+ ws.append(["CCI"])
81
+ ws.append([123456])
82
+ wb.save(tmp_file.name)
83
+ tmp_file.close()
84
+ self.template_workbook = tmp_file.name
85
+
86
+ def populate_controls(self):
87
+ """Populates the output workbook using mock API and template."""
88
+ self.output_workbook = populate_assessment_results(
89
+ file_name=Path(self.template_workbook),
90
+ ssp_id=self.security_plan.id,
91
+ api=self.api,
92
+ )
93
+
94
+ def check_values(self):
95
+ """Verifies that output cells M and O are filled for at least one row."""
96
+ pass_flag = False
97
+ wb = load_workbook(self.output_workbook)
98
+ sheet = wb.active
99
+ for row in range(SKIP_ROWS, sheet.max_row + 1):
100
+ if sheet[f"M{row}"].value and sheet[f"O{row}"].value:
101
+ pass_flag = True
102
+ break
103
+ assert pass_flag
104
+
105
+ def remove_files(self):
106
+ """Removes temp input and output Excel files."""
107
+ if self.template_workbook and os.path.exists(self.template_workbook):
108
+ os.remove(self.template_workbook)
109
+ if self.output_workbook and self.output_workbook.exists():
110
+ os.remove(self.output_workbook)
111
+
112
+ def test_emass_integration(self):
113
+ """Full e2e test for eMASS integration with template population."""
114
+ self.create_template()
115
+ self.populate_controls()
116
+ self.check_values()
117
+ self.remove_files()
118
+
119
+ def test_determine_assessment_result_logic(self):
120
+ """Tests correct string mapping of assessment results."""
121
+ assert determine_assessment_result({"assessmentResult": "Pass"}) == "Compliant"
122
+ assert determine_assessment_result({"assessmentResult": "Fail"}) == "Non-Compliant"
123
+ assert determine_assessment_result({"assessmentResult": "Partial Pass"}) == "Non-Compliant"
124
+ assert determine_assessment_result({"assessmentResult": "Unknown"}) == "Not Applicable"
125
+
126
+ def test_determine_assessment_result_missing_key(self):
127
+ """Raises KeyError when expected key is missing in result."""
128
+ with pytest.raises(KeyError):
129
+ determine_assessment_result({})
130
+
131
+ def test_map_finish_date_logic(self):
132
+ """Tests cell value and comment placement for valid and null finish dates."""
133
+ sheet = Workbook().active
134
+ sheet.insert_rows(5)
135
+ assessment_with_date = {"assessmentResult": "Pass", "actualFinish": "2024-01-01", "id": 1}
136
+ assessment_no_date = {"assessmentResult": "Fail", "actualFinish": None, "id": 2}
137
+ yellow_fill = PatternFill(start_color="FFFF00", end_color="FFFF00", fill_type="solid")
138
+ map_finish_date(assessment_with_date, sheet, 5, "Tester", yellow_fill)
139
+ assert sheet["M5"].value == "Compliant"
140
+ map_finish_date(assessment_no_date, sheet, 6, "Tester", yellow_fill)
141
+ assert isinstance(sheet["N6"].comment, Comment)
142
+
143
+ def test_map_ccis_success(self):
144
+ """Tests successful CCI mapping into formatted dictionary rows."""
145
+ file_data = {"CCI": {0: 111, 2: 222}}
146
+ mapped = map_ccis(file_data_dict=file_data, file_name="f.xlsx")
147
+ expected = {
148
+ "CCI-000111": {"cci": "CCI-000111", "row": SKIP_ROWS + 0},
149
+ "CCI-000222": {"cci": "CCI-000222", "row": SKIP_ROWS + 2},
150
+ }
151
+ assert mapped == expected
152
+
153
+ def test_map_ccis_keyerror(self):
154
+ """Exits when file data is missing required CCI keys."""
155
+ with pytest.raises(SystemExit):
156
+ map_ccis(file_data_dict={}, file_name="bad.xlsx")
157
+
158
+ def test_fetch_assessments_and_controls_success(self):
159
+ """Simulates valid RegScale graph and REST response for assessments and CCIs."""
160
+ result = emass_mod.fetch_assessments_and_controls(ssp_id=self.security_plan.id, api=self.api)
161
+ assert result[0].get("ccis") == ["CCI-123456"]
162
+
163
+ def test_fetch_assessments_and_controls_no_data(self):
164
+ """Raises exit if no assessment controls returned for SSP ID."""
165
+ result = emass_mod.fetch_assessments_and_controls(ssp_id=self.security_plan.id, api=self.api)
166
+ assert result[0].get("ccis") == ["CCI-123456"]
167
+
168
+ def test_fetch_assessments_and_controls_bad_count(self):
169
+ """Raises exit if REST control count fails."""
170
+ result = emass_mod.fetch_assessments_and_controls(ssp_id=self.security_plan.id, api=self.api)
171
+ assert result[0].get("ccis") == ["CCI-123456"]
172
+
173
+ @mock.patch.object(
174
+ emass_mod,
175
+ "Api",
176
+ side_effect=lambda *args, **kwargs: type(
177
+ "FakeAPI",
178
+ (),
179
+ {
180
+ "get": lambda self, url, headers: type("Resp", (), {"content": b"data"})(),
181
+ "logger": mock.Mock(),
182
+ },
183
+ )(),
184
+ )
185
+ @mock.patch.object(emass_mod, "check_file_path", side_effect=lambda path: Path(path).mkdir(exist_ok=True))
186
+ def test_fetch_template_from_blob(self, *mocks):
187
+ """Mocks RegScale blob fetch and verifies template download completes."""
188
+ fetch_template_from_blob()
189
+ assert Path("artifacts/eMASS_Template.xlsx").exists()
190
+
191
+ def test_invalid_file_type(self):
192
+ """Ensures unsupported file extensions raise SystemExit."""
193
+ with pytest.raises(SystemExit):
194
+ populate_emass_workbook(file_name=Path("invalid.txt"), regscale_id=self.security_plan.id)
195
+
196
+ @mock.patch.object(
197
+ emass_mod,
198
+ "fetch_assessments_and_controls",
199
+ side_effect=lambda ssp_id, api: [
200
+ {
201
+ "ccis": ["CCI-123456"],
202
+ "assessments": [
203
+ {
204
+ "id": 1,
205
+ "actualFinish": "2024-01-01",
206
+ "assessmentResult": "Pass",
207
+ "summaryOfResults": "OK",
208
+ "leadAssessor": {"firstName": "Test", "lastName": "User"},
209
+ }
210
+ ],
211
+ }
212
+ ],
213
+ )
214
+ def test_summary_cell_population(self, *mocks):
215
+ """Validates the 'Summary of Results' field is written correctly to Excel column P."""
216
+ self.create_template()
217
+ output = populate_assessment_results(
218
+ file_name=Path(self.template_workbook), ssp_id=self.security_plan.id, api=self.api
219
+ )
220
+ wb = load_workbook(output)
221
+ sheet = wb.active
222
+ assert sheet[f"P{SKIP_ROWS}"].value == "OK"
223
+ try:
224
+ Path("artifacts/eMASS_Template.xlsx").unlink(missing_ok=True)
225
+ except FileNotFoundError:
226
+ pass
227
+ try:
228
+ Path("artifacts").rmdir()
229
+ except OSError:
230
+ pass
231
+
232
+ def test_populate_controls_callback(self):
233
+ """Tests populate_controls command callback bindings."""
234
+ called = {}
235
+ with mock.patch.object(
236
+ emass_mod, "populate_emass_workbook", side_effect=lambda **kwargs: called.update(kwargs)
237
+ ):
238
+ emass_mod.populate_workbook.callback(file_name="f.xlsx", regscale_id=self.security_plan.id)
239
+ assert called == {"file_name": "f.xlsx", "regscale_id": self.security_plan.id}
240
+
241
+ def test_import_slcm_callback(self):
242
+ """Tests import_slcm command callback with expected args."""
243
+ called = {}
244
+ with mock.patch.object(emass_mod, "import_emass_slcm_file", side_effect=lambda **kwargs: called.update(kwargs)):
245
+ emass_mod.import_slcm.callback(
246
+ file_name="f2.xlsx", regscale_id=self.security_plan.id, catalogue_id=2, tenant_id=3
247
+ )
248
+ assert called == {
249
+ "file_name": "f2.xlsx",
250
+ "regscale_id": self.security_plan.id,
251
+ "catalogue_id": 2,
252
+ "tenant_id": 3,
253
+ }
254
+
255
+ def test_populate_emass_workbook_cli_function(self):
256
+ """Tests the main CLI function populate_emass_workbook."""
257
+ with mock.patch.object(emass_mod, "populate_assessment_results") as mock_populate:
258
+ mock_populate.return_value = Path("test_output.xlsx")
259
+ with mock.patch.object(emass_mod, "Api") as mock_api_class:
260
+ mock_api = mock.Mock()
261
+ mock_api.logger.info = mock.Mock()
262
+ mock_api_class.return_value = mock_api
263
+
264
+ populate_emass_workbook(file_name=Path("test.xlsx"), regscale_id=self.security_plan.id)
265
+
266
+ mock_populate.assert_called_once_with(
267
+ file_name=Path("test.xlsx"), ssp_id=self.security_plan.id, api=mock_api
268
+ )
269
+ mock_api.logger.info.assert_called_once()
270
+
271
+ def test_populate_emass_workbook_invalid_file_type(self):
272
+ """Tests populate_emass_workbook with invalid file type."""
273
+ with pytest.raises(SystemExit):
274
+ populate_emass_workbook(file_name=Path("test.txt"), regscale_id=self.security_plan.id)
275
+
276
+ def test_populate_assessment_results_empty_summary(self):
277
+ """Tests the case where assessment summaryOfResults is empty."""
278
+ self.create_template()
279
+
280
+ with mock.patch.object(
281
+ emass_mod,
282
+ "fetch_assessments_and_controls",
283
+ side_effect=lambda ssp_id, api: [
284
+ {
285
+ "ccis": ["CCI-123456"],
286
+ "assessments": [
287
+ {
288
+ "id": 1,
289
+ "actualFinish": "2024-01-01",
290
+ "assessmentResult": "Pass",
291
+ "summaryOfResults": "",
292
+ "leadAssessor": {"firstName": "Test", "lastName": "User"},
293
+ }
294
+ ],
295
+ }
296
+ ],
297
+ ):
298
+ output = populate_assessment_results(
299
+ file_name=Path(self.template_workbook), ssp_id=self.security_plan.id, api=self.api
300
+ )
301
+ wb = load_workbook(output)
302
+ sheet = wb.active
303
+
304
+ cell = sheet[f"P{SKIP_ROWS}"]
305
+ assert cell.comment is not None
306
+ assert "Summary of Results" in cell.comment.text
307
+ assert cell.fill.start_color.rgb == "00FFFF00"
308
+
309
+ os.remove(output)
310
+
311
+ def test_map_finish_date_no_finish_date(self):
312
+ """Tests map_finish_date when actualFinish is None."""
313
+ sheet = Workbook().active
314
+ sheet.insert_rows(5)
315
+ assessment_no_date = {"assessmentResult": "Pass", "actualFinish": None, "id": 1}
316
+ yellow_fill = PatternFill(start_color="FFFF00", end_color="FFFF00", fill_type="solid")
317
+
318
+ map_finish_date(assessment_no_date, sheet, 5, "Tester", yellow_fill)
319
+
320
+ cell = sheet["N5"]
321
+ assert cell.comment is not None
322
+ assert "finish date" in cell.comment.text
323
+ assert cell.fill.start_color.rgb == "00FFFF00"
324
+
325
+ def test_fetch_assessments_and_controls_keyerror_handling(self):
326
+ """Tests the KeyError handling in fetch_assessments_and_controls."""
327
+ result = emass_mod.fetch_assessments_and_controls(ssp_id=self.security_plan.id, api=self.api)
328
+ assert result[0].get("ccis") == ["CCI-123456"]
329
+
330
+ def test_fetch_assessments_and_controls_control_processing(self):
331
+ """Tests the control processing logic in fetch_assessments_and_controls."""
332
+ with mock.patch.object(
333
+ emass_mod,
334
+ "fetch_assessments_and_controls",
335
+ side_effect=lambda ssp_id, api: [
336
+ {"control": {"cci": [{"name": "CCI-000001"}, {"name": "CCI-000002"}]}, "assessments": [{"id": 1}]}
337
+ ],
338
+ ):
339
+ result = emass_mod.fetch_assessments_and_controls(ssp_id=self.security_plan.id, api=self.api)
340
+ assert len(result) == 1
341
+ assert "cci" in result[0]["control"]
342
+
343
+ def test_fetch_assessments_and_controls_control_processing_exception(self):
344
+ """Tests the exception handling in control processing."""
345
+ with mock.patch.object(
346
+ emass_mod,
347
+ "fetch_assessments_and_controls",
348
+ side_effect=lambda ssp_id, api: [{"control": {}, "assessments": [{"id": 1}]}],
349
+ ):
350
+ result = emass_mod.fetch_assessments_and_controls(ssp_id=self.security_plan.id, api=self.api)
351
+ assert len(result) == 1
352
+
353
+
354
+ class TestEmassIntegration(CLITestFixture):
355
+ """Integration tests for eMASS without heavy mocking to get better coverage."""
356
+
357
+ @pytest.fixture(autouse=True)
358
+ def setup_test_environment(self, create_security_plan):
359
+ """Setup test environment with dynamic security plan creation."""
360
+ self.security_plan = create_security_plan
361
+ self.template_workbook = ""
362
+ self.output_workbook = Path()
363
+ yield
364
+ # Cleanup will be handled by create_security_plan fixture
365
+
366
+ def test_real_fetch_assessments_and_controls(self):
367
+ """Test the real fetch_assessments_and_controls function with actual test data."""
368
+ from regscale.models.regscale_models import ControlImplementation, Assessment, SecurityControl
369
+
370
+ # Create test security control first
371
+ security_control = SecurityControl(
372
+ controlId="AC-1",
373
+ title=f"Test Security Control {self.title_prefix}",
374
+ description="Test security control for eMASS testing",
375
+ catalogueID=1,
376
+ weight=1,
377
+ createdById=self.config["userId"],
378
+ lastUpdatedById=self.config["userId"],
379
+ )
380
+ created_security_control = security_control.create_or_update()
381
+
382
+ # Create test control implementation
383
+ control_impl = ControlImplementation(
384
+ controlID=created_security_control.id,
385
+ title=f"Test Control {self.title_prefix}",
386
+ description="Test control implementation for eMASS testing",
387
+ status="Implemented",
388
+ parentId=self.security_plan.id,
389
+ parentModule=self.security_plan.get_module_string(),
390
+ createdById=self.config["userId"],
391
+ lastUpdatedById=self.config["userId"],
392
+ )
393
+ created_control = control_impl.create_or_update()
394
+
395
+ # Try to create a real assessment - if it fails due to API issues, we'll test the error case
396
+ try:
397
+ assessment = Assessment(
398
+ title=f"Test Assessment {self.title_prefix}",
399
+ assessmentResult="Pass",
400
+ actualFinish="2024-01-01",
401
+ summaryOfResults="Test assessment passed",
402
+ status="Complete",
403
+ leadAssessorId=self.config["userId"],
404
+ parentId=created_control.id,
405
+ parentModule="controls",
406
+ createdById=self.config["userId"],
407
+ lastUpdatedById=self.config["userId"],
408
+ )
409
+ created_assessment = assessment.create_or_update()
410
+
411
+ # Test with real assessment data
412
+ result = emass_mod.fetch_assessments_and_controls(ssp_id=self.security_plan.id, api=self.api)
413
+ assert isinstance(result, list)
414
+ assert len(result) > 0
415
+
416
+ # Verify the structure of returned data
417
+ first_result = result[0]
418
+ assert "control" in first_result
419
+ assert "assessments" in first_result
420
+ assert "ccis" in first_result
421
+
422
+ # Clean up assessment
423
+ try:
424
+ created_assessment.delete()
425
+ except Exception:
426
+ pass
427
+
428
+ except Exception:
429
+ # If assessment creation fails, test the error case (no assessments)
430
+ with pytest.raises(SystemExit):
431
+ emass_mod.fetch_assessments_and_controls(ssp_id=self.security_plan.id, api=self.api)
432
+
433
+ # Clean up test data
434
+ try:
435
+ created_control.delete()
436
+ except Exception:
437
+ pass
438
+ try:
439
+ created_security_control.delete()
440
+ except Exception:
441
+ pass
442
+
443
+ def test_real_fetch_assessments_and_controls_system_exit(self):
444
+ """Test that fetch_assessments_and_controls properly raises SystemExit when no assessments exist."""
445
+ # This test explicitly expects SystemExit to be raised when no assessments are found
446
+ with pytest.raises(SystemExit):
447
+ emass_mod.fetch_assessments_and_controls(ssp_id=self.security_plan.id, api=self.api)
448
+
449
+ def test_real_determine_assessment_result(self):
450
+ """Test the real determine_assessment_result function."""
451
+ # Test with valid assessment data
452
+ assessment = {"assessmentResult": "Pass"}
453
+ result = determine_assessment_result(assessment)
454
+ assert result == "Compliant"
455
+
456
+ # Test with different assessment results
457
+ assessment = {"assessmentResult": "Fail"}
458
+ result = determine_assessment_result(assessment)
459
+ assert result == "Non-Compliant"
460
+
461
+ # Test with missing key
462
+ with pytest.raises(KeyError):
463
+ determine_assessment_result({})
464
+
465
+ def test_real_map_ccis(self):
466
+ """Test the real map_ccis function."""
467
+ # Test with valid data
468
+ file_data = {"CCI": {0: 111, 2: 222}}
469
+ mapped = map_ccis(file_data_dict=file_data, file_name="test.xlsx")
470
+ expected = {
471
+ "CCI-000111": {"cci": "CCI-000111", "row": SKIP_ROWS + 0},
472
+ "CCI-000222": {"cci": "CCI-000222", "row": SKIP_ROWS + 2},
473
+ }
474
+ assert mapped == expected
475
+
476
+ # Test with missing CCI key
477
+ with pytest.raises(SystemExit):
478
+ map_ccis(file_data_dict={}, file_name="bad.xlsx")
479
+
480
+ def test_real_map_finish_date(self):
481
+ """Test the real map_finish_date function."""
482
+ sheet = Workbook().active
483
+ sheet.insert_rows(5)
484
+
485
+ # Test with valid finish date
486
+ assessment_with_date = {"assessmentResult": "Pass", "actualFinish": "2024-01-01", "id": 1}
487
+ yellow_fill = PatternFill(start_color="FFFF00", end_color="FFFF00", fill_type="solid")
488
+ map_finish_date(assessment_with_date, sheet, 5, "Tester", yellow_fill)
489
+ assert sheet["M5"].value == "Compliant"
490
+
491
+ # Test with no finish date
492
+ assessment_no_date = {"assessmentResult": "Fail", "actualFinish": None, "id": 2}
493
+ map_finish_date(assessment_no_date, sheet, 6, "Tester", yellow_fill)
494
+ assert isinstance(sheet["N6"].comment, Comment)
495
+
496
+ def test_real_populate_emass_workbook_invalid_file(self):
497
+ """Test populate_emass_workbook with invalid file type."""
498
+ with pytest.raises(SystemExit):
499
+ populate_emass_workbook(file_name=Path("test.txt"), regscale_id=self.security_plan.id)
500
+
501
+ def test_real_fetch_template_from_blob(self):
502
+ """Test the real fetch_template_from_blob function."""
503
+ try:
504
+ fetch_template_from_blob()
505
+ # Check if the template was downloaded
506
+ template_path = Path("artifacts/eMASS_Template.xlsx")
507
+ if template_path.exists():
508
+ assert template_path.stat().st_size > 0
509
+ # Clean up
510
+ template_path.unlink(missing_ok=True)
511
+ try:
512
+ Path("artifacts").rmdir()
513
+ except OSError:
514
+ pass
515
+ except Exception as e:
516
+ # If the download fails, that's expected in a test environment
517
+ # Just verify it's a reasonable exception
518
+ assert "connection" in str(e).lower() or "timeout" in str(e).lower() or "404" in str(e)