regscale-cli 6.21.2.0__py3-none-any.whl → 6.28.2.1__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.
- regscale/_version.py +1 -1
- regscale/airflow/hierarchy.py +2 -2
- regscale/core/app/api.py +5 -2
- regscale/core/app/application.py +36 -6
- regscale/core/app/internal/control_editor.py +73 -21
- regscale/core/app/internal/evidence.py +727 -204
- regscale/core/app/internal/login.py +4 -2
- regscale/core/app/internal/model_editor.py +219 -64
- regscale/core/app/utils/app_utils.py +86 -12
- regscale/core/app/utils/catalog_utils/common.py +1 -1
- regscale/core/login.py +21 -4
- regscale/core/utils/async_graphql_client.py +363 -0
- regscale/core/utils/date.py +77 -1
- regscale/dev/cli.py +26 -0
- regscale/dev/code_gen.py +109 -24
- regscale/dev/version.py +72 -0
- regscale/integrations/commercial/__init__.py +30 -2
- regscale/integrations/commercial/aws/audit_manager_compliance.py +3908 -0
- regscale/integrations/commercial/aws/cli.py +3107 -54
- regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
- regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
- regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
- regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
- regscale/integrations/commercial/{amazon → aws}/common.py +71 -19
- regscale/integrations/commercial/aws/config_compliance.py +914 -0
- regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
- regscale/integrations/commercial/aws/control_compliance_analyzer.py +439 -0
- regscale/integrations/commercial/aws/evidence_generator.py +283 -0
- regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
- regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
- regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
- regscale/integrations/commercial/aws/iam_evidence.py +574 -0
- regscale/integrations/commercial/aws/inventory/__init__.py +338 -22
- regscale/integrations/commercial/aws/inventory/base.py +107 -5
- regscale/integrations/commercial/aws/inventory/resources/analytics.py +390 -0
- regscale/integrations/commercial/aws/inventory/resources/applications.py +234 -0
- regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
- regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
- regscale/integrations/commercial/aws/inventory/resources/compute.py +328 -9
- regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
- regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
- regscale/integrations/commercial/aws/inventory/resources/database.py +481 -31
- regscale/integrations/commercial/aws/inventory/resources/developer_tools.py +253 -0
- regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
- regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
- regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
- regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
- regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
- regscale/integrations/commercial/aws/inventory/resources/machine_learning.py +358 -0
- regscale/integrations/commercial/aws/inventory/resources/networking.py +390 -67
- regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
- regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
- regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
- regscale/integrations/commercial/aws/inventory/resources/storage.py +288 -29
- regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
- regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
- regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
- regscale/integrations/commercial/aws/kms_evidence.py +879 -0
- regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
- regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
- regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
- regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
- regscale/integrations/commercial/aws/org_evidence.py +666 -0
- regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
- regscale/integrations/commercial/aws/s3_evidence.py +632 -0
- regscale/integrations/commercial/aws/scanner.py +1072 -205
- regscale/integrations/commercial/aws/security_hub.py +319 -0
- regscale/integrations/commercial/aws/session_manager.py +282 -0
- regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
- regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
- regscale/integrations/commercial/jira.py +489 -153
- regscale/integrations/commercial/microsoft_defender/defender.py +326 -5
- regscale/integrations/commercial/microsoft_defender/defender_api.py +348 -14
- regscale/integrations/commercial/microsoft_defender/defender_constants.py +157 -0
- regscale/integrations/commercial/qualys/__init__.py +167 -68
- regscale/integrations/commercial/qualys/scanner.py +305 -39
- regscale/integrations/commercial/sarif/sairf_importer.py +432 -0
- regscale/integrations/commercial/sarif/sarif_converter.py +67 -0
- regscale/integrations/commercial/sicura/api.py +79 -42
- regscale/integrations/commercial/sicura/commands.py +8 -2
- regscale/integrations/commercial/sicura/scanner.py +83 -44
- regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
- regscale/integrations/commercial/synqly/assets.py +133 -16
- regscale/integrations/commercial/synqly/edr.py +2 -8
- regscale/integrations/commercial/synqly/query_builder.py +536 -0
- regscale/integrations/commercial/synqly/ticketing.py +27 -0
- regscale/integrations/commercial/synqly/vulnerabilities.py +165 -28
- regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
- regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
- regscale/integrations/commercial/tenablev2/commands.py +146 -5
- regscale/integrations/commercial/tenablev2/scanner.py +1 -3
- regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
- regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
- regscale/integrations/commercial/wizv2/click.py +191 -76
- regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
- regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
- regscale/integrations/commercial/wizv2/compliance_report.py +1592 -0
- regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
- regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +7 -3
- regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +92 -89
- regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
- regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
- regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +66 -9
- regscale/integrations/commercial/wizv2/file_cleanup.py +104 -0
- regscale/integrations/commercial/wizv2/issue.py +776 -28
- regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
- regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
- regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
- regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
- regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
- regscale/integrations/commercial/wizv2/reports.py +243 -0
- regscale/integrations/commercial/wizv2/sbom.py +1 -1
- regscale/integrations/commercial/wizv2/scanner.py +1031 -441
- regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
- regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
- regscale/integrations/commercial/wizv2/variables.py +89 -3
- regscale/integrations/compliance_integration.py +1036 -151
- regscale/integrations/control_matcher.py +432 -0
- regscale/integrations/due_date_handler.py +333 -0
- regscale/integrations/milestone_manager.py +291 -0
- regscale/integrations/public/__init__.py +14 -0
- regscale/integrations/public/cci_importer.py +834 -0
- regscale/integrations/public/csam/__init__.py +0 -0
- regscale/integrations/public/csam/csam.py +938 -0
- regscale/integrations/public/csam/csam_agency_defined.py +179 -0
- regscale/integrations/public/csam/csam_common.py +154 -0
- regscale/integrations/public/csam/csam_controls.py +432 -0
- regscale/integrations/public/csam/csam_poam.py +124 -0
- regscale/integrations/public/fedramp/click.py +77 -6
- regscale/integrations/public/fedramp/docx_parser.py +10 -1
- regscale/integrations/public/fedramp/fedramp_cis_crm.py +675 -289
- regscale/integrations/public/fedramp/fedramp_five.py +1 -1
- regscale/integrations/public/fedramp/poam/scanner.py +75 -7
- regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
- regscale/integrations/scanner_integration.py +1961 -430
- regscale/models/integration_models/CCI_List.xml +1 -0
- regscale/models/integration_models/aqua.py +2 -2
- regscale/models/integration_models/cisa_kev_data.json +805 -11
- regscale/models/integration_models/flat_file_importer/__init__.py +5 -8
- regscale/models/integration_models/nexpose.py +36 -10
- regscale/models/integration_models/qualys.py +3 -4
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +87 -18
- regscale/models/integration_models/synqly_models/filter_parser.py +332 -0
- regscale/models/integration_models/synqly_models/ocsf_mapper.py +124 -25
- regscale/models/integration_models/synqly_models/synqly_model.py +89 -16
- regscale/models/locking.py +12 -8
- regscale/models/platform.py +4 -2
- regscale/models/regscale_models/__init__.py +7 -0
- regscale/models/regscale_models/assessment.py +2 -1
- regscale/models/regscale_models/catalog.py +1 -1
- regscale/models/regscale_models/compliance_settings.py +251 -1
- regscale/models/regscale_models/component.py +1 -0
- regscale/models/regscale_models/control_implementation.py +236 -41
- regscale/models/regscale_models/control_objective.py +74 -5
- regscale/models/regscale_models/file.py +2 -0
- regscale/models/regscale_models/form_field_value.py +5 -3
- regscale/models/regscale_models/inheritance.py +44 -0
- regscale/models/regscale_models/issue.py +301 -102
- regscale/models/regscale_models/milestone.py +33 -14
- regscale/models/regscale_models/organization.py +3 -0
- regscale/models/regscale_models/regscale_model.py +310 -73
- regscale/models/regscale_models/security_plan.py +4 -2
- regscale/models/regscale_models/vulnerability.py +3 -3
- regscale/regscale.py +25 -4
- regscale/templates/__init__.py +0 -0
- regscale/utils/threading/threadhandler.py +20 -15
- regscale/validation/record.py +23 -1
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/METADATA +17 -33
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/RECORD +310 -111
- tests/core/__init__.py +0 -0
- tests/core/utils/__init__.py +0 -0
- tests/core/utils/test_async_graphql_client.py +472 -0
- tests/fixtures/test_fixture.py +13 -8
- tests/regscale/core/test_login.py +171 -4
- tests/regscale/integrations/commercial/__init__.py +0 -0
- tests/regscale/integrations/commercial/aws/__init__.py +0 -0
- tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
- tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
- tests/regscale/integrations/commercial/aws/test_aws_analytics_collector.py +260 -0
- tests/regscale/integrations/commercial/aws/test_aws_applications_collector.py +242 -0
- tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
- tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
- tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
- tests/regscale/integrations/commercial/aws/test_aws_developer_tools_collector.py +203 -0
- tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
- tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
- tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
- tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
- tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
- tests/regscale/integrations/commercial/aws/test_aws_machine_learning_collector.py +237 -0
- tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
- tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
- tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
- tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
- tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
- tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
- tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
- tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
- tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
- tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
- tests/regscale/integrations/commercial/aws/test_control_compliance_analyzer.py +375 -0
- tests/regscale/integrations/commercial/aws/test_datetime_parsing.py +223 -0
- tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
- tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
- tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
- tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
- tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
- tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
- tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
- tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
- tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
- tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
- tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
- tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
- tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
- tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
- tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
- tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
- tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
- tests/regscale/integrations/commercial/conftest.py +28 -0
- tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
- tests/regscale/integrations/commercial/test_aws.py +3742 -0
- tests/regscale/integrations/commercial/test_burp.py +48 -0
- tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
- tests/regscale/integrations/commercial/test_dependabot.py +341 -0
- tests/regscale/integrations/commercial/test_gcp.py +1543 -0
- tests/regscale/integrations/commercial/test_gitlab.py +549 -0
- tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
- tests/regscale/integrations/commercial/test_jira.py +2204 -0
- tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
- tests/regscale/integrations/commercial/test_okta.py +1228 -0
- tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
- tests/regscale/integrations/commercial/test_sicura.py +349 -0
- tests/regscale/integrations/commercial/test_snow.py +423 -0
- tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
- tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
- tests/regscale/integrations/commercial/test_stig.py +33 -0
- tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
- tests/regscale/integrations/commercial/test_stigv2.py +406 -0
- tests/regscale/integrations/commercial/test_wiz.py +1365 -0
- tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
- tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
- tests/regscale/integrations/commercial/wizv2/compliance/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
- tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
- tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
- tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
- tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
- tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
- tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
- tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
- tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
- tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
- tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
- tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
- tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
- tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
- tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
- tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
- tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
- tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1394 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_findings_comprehensive.py +364 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2.py +1218 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +519 -0
- tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -0
- tests/regscale/integrations/public/__init__.py +0 -0
- tests/regscale/integrations/public/fedramp/__init__.py +1 -0
- tests/regscale/integrations/public/fedramp/test_gen_asset_list.py +150 -0
- tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
- tests/regscale/integrations/public/test_alienvault.py +220 -0
- tests/regscale/integrations/public/test_cci.py +1053 -0
- tests/regscale/integrations/public/test_cisa.py +1021 -0
- tests/regscale/integrations/public/test_emass.py +518 -0
- tests/regscale/integrations/public/test_fedramp.py +1152 -0
- tests/regscale/integrations/public/test_fedramp_cis_crm.py +3661 -0
- tests/regscale/integrations/public/test_file_uploads.py +506 -0
- tests/regscale/integrations/public/test_oscal.py +453 -0
- tests/regscale/integrations/test_compliance_status_mapping.py +406 -0
- tests/regscale/integrations/test_control_matcher.py +1421 -0
- tests/regscale/integrations/test_control_matching.py +155 -0
- tests/regscale/integrations/test_milestone_manager.py +408 -0
- tests/regscale/models/test_control_implementation.py +118 -3
- tests/regscale/models/test_form_field_value_integration.py +304 -0
- tests/regscale/models/test_issue.py +378 -1
- tests/regscale/models/test_module_integration.py +582 -0
- tests/regscale/models/test_tenable_integrations.py +811 -105
- regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3057
- regscale/integrations/public/fedramp/mappings/fedramp_r4_parts.json +0 -7388
- regscale/integrations/public/fedramp/mappings/fedramp_r5_parts.json +0 -9605
- regscale/integrations/public/fedramp/parts_mapper.py +0 -107
- /regscale/integrations/commercial/{amazon → sarif}/__init__.py +0 -0
- /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/LICENSE +0 -0
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/WHEEL +0 -0
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,2204 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Test for jira integration in RegScale CLI"""
|
|
4
|
+
# standard python imports
|
|
5
|
+
import contextlib
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import tempfile
|
|
9
|
+
from unittest.mock import MagicMock, patch
|
|
10
|
+
from datetime import datetime, timedelta
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
from rich.progress import Progress
|
|
14
|
+
from jira import JIRAError
|
|
15
|
+
|
|
16
|
+
from regscale.core.app.application import Application
|
|
17
|
+
from regscale.core.app.api import Api
|
|
18
|
+
from regscale.core.app.utils.app_utils import compute_hash, create_progress_object, get_current_datetime
|
|
19
|
+
from regscale.integrations.commercial.jira import (
|
|
20
|
+
_generate_jira_comment,
|
|
21
|
+
check_and_close_tasks,
|
|
22
|
+
create_and_update_regscale_issues,
|
|
23
|
+
create_issue_in_jira,
|
|
24
|
+
create_jira_client,
|
|
25
|
+
download_regscale_attachments_to_directory,
|
|
26
|
+
fetch_jira_objects,
|
|
27
|
+
get_regscale_data_and_attachments,
|
|
28
|
+
map_jira_due_date,
|
|
29
|
+
map_jira_to_regscale_issue,
|
|
30
|
+
sync_regscale_and_jira,
|
|
31
|
+
sync_regscale_objects_to_jira,
|
|
32
|
+
sync_regscale_to_jira,
|
|
33
|
+
task_and_attachments_sync,
|
|
34
|
+
create_regscale_task_from_jira,
|
|
35
|
+
create_and_update_regscale_tasks,
|
|
36
|
+
process_tasks_for_sync,
|
|
37
|
+
upload_files_to_jira,
|
|
38
|
+
upload_files_to_regscale,
|
|
39
|
+
validate_issue_type,
|
|
40
|
+
)
|
|
41
|
+
from regscale.models import File, Issue
|
|
42
|
+
from regscale.models.regscale_models.task import Task
|
|
43
|
+
from tests import CLITestFixture
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TestJira(CLITestFixture):
|
|
47
|
+
JIRA_PROJECT = "SNES"
|
|
48
|
+
PATH = "regscale.integrations.commercial.jira"
|
|
49
|
+
security_plan = None
|
|
50
|
+
|
|
51
|
+
@pytest.fixture(autouse=True)
|
|
52
|
+
def setup_ssp(self, create_security_plan):
|
|
53
|
+
self.security_plan = create_security_plan
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def PARENT_ID(self):
|
|
57
|
+
"""Get the parent ID from the existing SSP"""
|
|
58
|
+
return self.security_plan.id
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def PARENT_MODULE(self):
|
|
62
|
+
"""Get the parent module from the existing SSP"""
|
|
63
|
+
return self.security_plan.get_module_string()
|
|
64
|
+
|
|
65
|
+
@pytest.fixture
|
|
66
|
+
def jira_client(self):
|
|
67
|
+
"""Setup jira client"""
|
|
68
|
+
return create_jira_client(
|
|
69
|
+
config=self.config,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
@pytest.fixture(params=[True, False])
|
|
74
|
+
def fetch_attachments(request):
|
|
75
|
+
"""Pytest fixture that will run twice:
|
|
76
|
+
first time with a true value, second time with a false value"""
|
|
77
|
+
return request.param
|
|
78
|
+
|
|
79
|
+
@pytest.fixture
|
|
80
|
+
def jira_issues(self, jira_client, fetch_attachments):
|
|
81
|
+
"""Fixture for fetching Jira issues and attachments"""
|
|
82
|
+
return fetch_jira_objects(jira_client=jira_client, jira_project=self.JIRA_PROJECT, jira_issue_type="Bug")
|
|
83
|
+
|
|
84
|
+
@pytest.fixture
|
|
85
|
+
def jira_tasks(self, jira_client, fetch_attachments):
|
|
86
|
+
"""Fixture for fetching Jira tasks and attachments"""
|
|
87
|
+
return fetch_jira_objects(
|
|
88
|
+
jira_client=jira_client,
|
|
89
|
+
jira_project=self.JIRA_PROJECT,
|
|
90
|
+
jira_issue_type="Task",
|
|
91
|
+
sync_tasks_only=True,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
@pytest.fixture
|
|
95
|
+
def get_jira_issue(self, jira_client):
|
|
96
|
+
"""Fixture for creating a test Issue in Jira"""
|
|
97
|
+
jira_issue = jira_client.create_issue(
|
|
98
|
+
fields={
|
|
99
|
+
"project": {"key": self.JIRA_PROJECT},
|
|
100
|
+
"summary": f"{self.title_prefix} Jira Integration Test",
|
|
101
|
+
"description": "Test issue for integration testing",
|
|
102
|
+
"issuetype": {"name": "Bug"},
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
yield jira_issue
|
|
106
|
+
jira_issue.delete() # cleanup afterwards
|
|
107
|
+
|
|
108
|
+
@pytest.fixture
|
|
109
|
+
def get_jira_task(self, jira_client):
|
|
110
|
+
"""Fixture for creating a test Task in Jira"""
|
|
111
|
+
jira_task = jira_client.create_issue(
|
|
112
|
+
fields={
|
|
113
|
+
"project": {"key": self.JIRA_PROJECT},
|
|
114
|
+
"summary": f"{self.title_prefix} Jira Integration Test",
|
|
115
|
+
"description": "Test task for integration testing",
|
|
116
|
+
"issuetype": {"name": "Task"},
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
yield jira_task
|
|
120
|
+
jira_task.delete() # cleanup afterwards
|
|
121
|
+
|
|
122
|
+
@pytest.fixture
|
|
123
|
+
def get_jira_issue_with_attachment(self, get_jira_issue, jira_client):
|
|
124
|
+
"""Fixture for creating a test Issue in Jira with an attachment"""
|
|
125
|
+
issue = get_jira_issue
|
|
126
|
+
file_path = os.path.join(self.get_tests_dir("tests"), "test_data", "jira_attachments", "attachment.txt")
|
|
127
|
+
with open(file_path, "rb") as f:
|
|
128
|
+
jira_client.add_attachment(issue=issue, filename="test_attachment.txt", attachment=f)
|
|
129
|
+
yield jira_client.issue(issue.id)
|
|
130
|
+
|
|
131
|
+
@pytest.fixture
|
|
132
|
+
def get_jira_task_with_attachment(self, get_jira_task, jira_client):
|
|
133
|
+
"""Fixture for creating a test Task in Jira with an attachment"""
|
|
134
|
+
task = get_jira_task
|
|
135
|
+
file_path = os.path.join(self.get_tests_dir("tests"), "test_data", "jira_attachments", "attachment.txt")
|
|
136
|
+
with open(file_path, "rb") as f:
|
|
137
|
+
jira_client.add_attachment(issue=task, filename="test_attachment.txt", attachment=f)
|
|
138
|
+
yield jira_client.issue(task.id)
|
|
139
|
+
|
|
140
|
+
@pytest.fixture
|
|
141
|
+
def regscale_issues_and_attachments(self, fetch_attachments, regscale_issue_and_attachment):
|
|
142
|
+
"""Fixture for fetching RegScale issues and attachments"""
|
|
143
|
+
_ = regscale_issue_and_attachment
|
|
144
|
+
|
|
145
|
+
if fetch_attachments:
|
|
146
|
+
return Issue.get_objects_and_attachments_by_parent(
|
|
147
|
+
parent_id=self.PARENT_ID,
|
|
148
|
+
parent_module=self.PARENT_MODULE,
|
|
149
|
+
)
|
|
150
|
+
else:
|
|
151
|
+
return (
|
|
152
|
+
Issue.get_all_by_parent(
|
|
153
|
+
parent_id=self.PARENT_ID,
|
|
154
|
+
parent_module=self.PARENT_MODULE,
|
|
155
|
+
),
|
|
156
|
+
[],
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
@pytest.fixture
|
|
160
|
+
def regscale_tasks_and_attachments(self, fetch_attachments, regscale_task_and_attachment):
|
|
161
|
+
"""Fixture for fetching RegScale tasks and attachments"""
|
|
162
|
+
_ = regscale_task_and_attachment
|
|
163
|
+
if fetch_attachments:
|
|
164
|
+
return Task.get_objects_and_attachments_by_parent(
|
|
165
|
+
parent_id=self.PARENT_ID,
|
|
166
|
+
parent_module=self.PARENT_MODULE,
|
|
167
|
+
)
|
|
168
|
+
else:
|
|
169
|
+
return (
|
|
170
|
+
Task.get_all_by_parent(
|
|
171
|
+
parent_id=self.PARENT_ID,
|
|
172
|
+
parent_module=self.PARENT_MODULE,
|
|
173
|
+
),
|
|
174
|
+
[],
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
@pytest.fixture
|
|
178
|
+
def regscale_issue_and_attachment(self, fetch_attachments):
|
|
179
|
+
"""Fixture for creating RegScale issue and attachment"""
|
|
180
|
+
issue = Issue(
|
|
181
|
+
title=f"{self.title_prefix} Jira Issue Integration Test",
|
|
182
|
+
description="Security plan for Jira integration testing",
|
|
183
|
+
parentId=self.PARENT_ID,
|
|
184
|
+
parentModule=self.PARENT_MODULE,
|
|
185
|
+
dueDate=get_current_datetime(),
|
|
186
|
+
identification=f"{self.title_prefix} Jira Issue Integration Test",
|
|
187
|
+
status="Open",
|
|
188
|
+
).create()
|
|
189
|
+
if fetch_attachments:
|
|
190
|
+
File.upload_file_to_regscale(
|
|
191
|
+
file_name=os.path.join(self.get_tests_dir("tests"), "test_data", "jira_attachments", "attachment.txt"),
|
|
192
|
+
parent_id=issue.id,
|
|
193
|
+
parent_module=issue.get_module_string(),
|
|
194
|
+
api=self.api,
|
|
195
|
+
)
|
|
196
|
+
return issue
|
|
197
|
+
|
|
198
|
+
@pytest.fixture
|
|
199
|
+
def regscale_task_and_attachment(self, fetch_attachments):
|
|
200
|
+
"""Fixture for creating RegScale task and attachment"""
|
|
201
|
+
task = Task(
|
|
202
|
+
status="Backlog",
|
|
203
|
+
title=f"{self.title_prefix} Jira Task Integration Test",
|
|
204
|
+
description="Task for Jira integration testing",
|
|
205
|
+
parentId=self.PARENT_ID,
|
|
206
|
+
parentModule=self.PARENT_MODULE,
|
|
207
|
+
dueDate=get_current_datetime(),
|
|
208
|
+
).create()
|
|
209
|
+
if fetch_attachments:
|
|
210
|
+
File.upload_file_to_regscale(
|
|
211
|
+
file_name=os.path.join(self.get_tests_dir("tests"), "test_data", "jira_attachments", "image.png"),
|
|
212
|
+
parent_id=task.id,
|
|
213
|
+
parent_module=task.get_module_string(),
|
|
214
|
+
api=self.api,
|
|
215
|
+
)
|
|
216
|
+
return task
|
|
217
|
+
|
|
218
|
+
@pytest.fixture
|
|
219
|
+
def mock_job_progres_object(self):
|
|
220
|
+
"""Mock job_progress object"""
|
|
221
|
+
with patch.object(self.PATH, "job_progress", new=create_progress_object()) as job_progress:
|
|
222
|
+
yield job_progress
|
|
223
|
+
|
|
224
|
+
def test_init(self):
|
|
225
|
+
"""Test init file and config"""
|
|
226
|
+
self.verify_config(
|
|
227
|
+
[
|
|
228
|
+
"jiraUrl",
|
|
229
|
+
"jiraApiToken",
|
|
230
|
+
"jiraUserName",
|
|
231
|
+
]
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
@patch(f"{PATH}.Task.get_objects_and_attachments_by_parent")
|
|
235
|
+
def test_get_regscale_data_and_attachments_sync_both(self, mock_get_objects_and_attachments_by_parent):
|
|
236
|
+
"""Test getting RegScale data and attachments"""
|
|
237
|
+
mock_get_objects_and_attachments_by_parent.return_value = (["objects"], {"attachments": ["attachment"]})
|
|
238
|
+
regscale_issues, regscale_attachments = get_regscale_data_and_attachments(
|
|
239
|
+
parent_id=self.PARENT_ID,
|
|
240
|
+
parent_module=self.PARENT_MODULE,
|
|
241
|
+
sync_attachments=True,
|
|
242
|
+
sync_tasks_only=True,
|
|
243
|
+
)
|
|
244
|
+
assert regscale_issues == ["objects"]
|
|
245
|
+
assert regscale_attachments == {"attachments": ["attachment"]}
|
|
246
|
+
|
|
247
|
+
@patch(f"{PATH}.Task.get_all_by_parent")
|
|
248
|
+
def test_get_regscale_data_and_attachments_sync_tasks_only(self, mock_get_all_by_parent):
|
|
249
|
+
"""Test getting RegScale data and no attachments"""
|
|
250
|
+
mock_get_all_by_parent.return_value = ["objects"]
|
|
251
|
+
regscale_issues, regscale_attachments = get_regscale_data_and_attachments(
|
|
252
|
+
parent_id=self.PARENT_ID,
|
|
253
|
+
parent_module=self.PARENT_MODULE,
|
|
254
|
+
sync_attachments=False,
|
|
255
|
+
sync_tasks_only=True,
|
|
256
|
+
)
|
|
257
|
+
assert regscale_issues == ["objects"]
|
|
258
|
+
assert regscale_attachments == []
|
|
259
|
+
|
|
260
|
+
@patch(f"{PATH}.Issue.get_objects_and_attachments_by_parent")
|
|
261
|
+
def test_get_regscale_data_and_attachments_sync_attachments_only(self, mock_get_objects_and_attachments_by_parent):
|
|
262
|
+
"""Test getting RegScale attachments only"""
|
|
263
|
+
mock_get_objects_and_attachments_by_parent.return_value = (["objects"], {"attachments": ["attachment"]})
|
|
264
|
+
regscale_issues, regscale_attachments = get_regscale_data_and_attachments(
|
|
265
|
+
parent_id=self.PARENT_ID,
|
|
266
|
+
parent_module=self.PARENT_MODULE,
|
|
267
|
+
sync_attachments=True,
|
|
268
|
+
sync_tasks_only=False,
|
|
269
|
+
)
|
|
270
|
+
assert regscale_issues == ["objects"]
|
|
271
|
+
assert regscale_attachments == {"attachments": ["attachment"]}
|
|
272
|
+
|
|
273
|
+
@patch(f"{PATH}.Issue.get_all_by_parent")
|
|
274
|
+
def test_get_regscale_data_and_attachments_sync_issues_only(self, mock_get_all_by_parent):
|
|
275
|
+
"""Test getting RegScale issues only"""
|
|
276
|
+
mock_get_all_by_parent.return_value = ["objects"]
|
|
277
|
+
regscale_issues, regscale_attachments = get_regscale_data_and_attachments(
|
|
278
|
+
parent_id=self.PARENT_ID,
|
|
279
|
+
parent_module=self.PARENT_MODULE,
|
|
280
|
+
sync_attachments=False,
|
|
281
|
+
sync_tasks_only=False,
|
|
282
|
+
)
|
|
283
|
+
assert regscale_issues == ["objects"]
|
|
284
|
+
assert regscale_attachments == []
|
|
285
|
+
|
|
286
|
+
@patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
|
|
287
|
+
def test_sync_regscale_and_jira(self, mock_check_license, fetch_attachments):
|
|
288
|
+
"""Test the entire Jira & RegScale sync process"""
|
|
289
|
+
mock_check_license.return_value.config = self.config
|
|
290
|
+
# Add thread_manager to the mock Application
|
|
291
|
+
mock_check_license.return_value.thread_manager = MagicMock()
|
|
292
|
+
try:
|
|
293
|
+
sync_regscale_and_jira(
|
|
294
|
+
parent_id=self.PARENT_ID,
|
|
295
|
+
parent_module=self.PARENT_MODULE,
|
|
296
|
+
jira_project=self.JIRA_PROJECT,
|
|
297
|
+
jira_issue_type="Bug",
|
|
298
|
+
sync_attachments=fetch_attachments,
|
|
299
|
+
)
|
|
300
|
+
except Exception as e:
|
|
301
|
+
pytest.fail("Jira & RegScale sync failed: {}".format(e))
|
|
302
|
+
|
|
303
|
+
@patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
|
|
304
|
+
def test_sync_regscale_and_jira_tasks(self, mock_check_license, fetch_attachments):
|
|
305
|
+
"""Test the entire Jira & RegScale task sync process"""
|
|
306
|
+
mock_check_license.return_value.config = self.config
|
|
307
|
+
# Add thread_manager to the mock Application
|
|
308
|
+
mock_check_license.return_value.thread_manager = MagicMock()
|
|
309
|
+
try:
|
|
310
|
+
sync_regscale_and_jira(
|
|
311
|
+
parent_id=self.PARENT_ID,
|
|
312
|
+
parent_module=self.PARENT_MODULE,
|
|
313
|
+
jira_project=self.JIRA_PROJECT,
|
|
314
|
+
jira_issue_type="Task",
|
|
315
|
+
sync_attachments=fetch_attachments,
|
|
316
|
+
sync_tasks_only=True,
|
|
317
|
+
)
|
|
318
|
+
except Exception as e:
|
|
319
|
+
pytest.fail("Jira & RegScale task sync failed: {}".format(e))
|
|
320
|
+
|
|
321
|
+
@patch(f"{PATH}.sync_regscale_objects_to_jira")
|
|
322
|
+
@patch(f"{PATH}.sync_regscale_to_jira", return_value=[])
|
|
323
|
+
@patch(f"{PATH}.create_jira_client")
|
|
324
|
+
@patch(f"{PATH}.fetch_jira_objects", return_value=[])
|
|
325
|
+
@patch(f"{PATH}.get_regscale_data_and_attachments", return_value=([], {}))
|
|
326
|
+
@patch(f"{PATH}.Api", return_value=MagicMock(spec=Api))
|
|
327
|
+
@patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
|
|
328
|
+
def test_sync_regscale_and_jira_no_updates(
|
|
329
|
+
self,
|
|
330
|
+
mock_check_license,
|
|
331
|
+
mock_api,
|
|
332
|
+
mock_get_regscale_data_and_attachments,
|
|
333
|
+
mock_fetch_jira_objects,
|
|
334
|
+
mock_create_jira_client,
|
|
335
|
+
mock_sync_regscale_to_jira,
|
|
336
|
+
mock_sync_regscale_objects_to_jira,
|
|
337
|
+
fetch_attachments,
|
|
338
|
+
):
|
|
339
|
+
"""Test sync_regscale_and_jira without updates from either side"""
|
|
340
|
+
# mock jira client so we can check it was correctly used later
|
|
341
|
+
mock_jira_client = MagicMock()
|
|
342
|
+
mock_create_jira_client.return_value = mock_jira_client
|
|
343
|
+
|
|
344
|
+
sync_regscale_and_jira(
|
|
345
|
+
parent_id=self.PARENT_ID,
|
|
346
|
+
parent_module=self.PARENT_MODULE,
|
|
347
|
+
jira_project=self.JIRA_PROJECT,
|
|
348
|
+
jira_issue_type="Bug",
|
|
349
|
+
sync_attachments=fetch_attachments,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# check that we get the jira client correctly
|
|
353
|
+
mock_create_jira_client.assert_called_once()
|
|
354
|
+
|
|
355
|
+
# check that we correctly fetch objects from jira and regscale
|
|
356
|
+
mock_get_regscale_data_and_attachments.assert_called_once_with(
|
|
357
|
+
parent_id=self.PARENT_ID,
|
|
358
|
+
parent_module=self.PARENT_MODULE,
|
|
359
|
+
sync_attachments=fetch_attachments,
|
|
360
|
+
sync_tasks_only=False,
|
|
361
|
+
)
|
|
362
|
+
mock_fetch_jira_objects.assert_called_once_with(
|
|
363
|
+
jira_client=mock_jira_client,
|
|
364
|
+
jira_project=self.JIRA_PROJECT,
|
|
365
|
+
jql_str="project = 'SNES'",
|
|
366
|
+
jira_issue_type="Bug",
|
|
367
|
+
sync_tasks_only=False,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# check that no updates were made because we did not find any objects from jira/regscale
|
|
371
|
+
mock_sync_regscale_to_jira.assert_not_called()
|
|
372
|
+
mock_sync_regscale_objects_to_jira.assert_not_called()
|
|
373
|
+
|
|
374
|
+
@patch(f"{PATH}.sync_regscale_objects_to_jira")
|
|
375
|
+
@patch(f"{PATH}.sync_regscale_to_jira", return_value=[])
|
|
376
|
+
@patch(f"{PATH}.create_jira_client")
|
|
377
|
+
@patch(f"{PATH}.fetch_jira_objects")
|
|
378
|
+
@patch(f"{PATH}.get_regscale_data_and_attachments")
|
|
379
|
+
@patch(f"{PATH}.Api", return_value=MagicMock(spec=Api))
|
|
380
|
+
@patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
|
|
381
|
+
def test_sync_regscale_and_jira_updates(
|
|
382
|
+
self,
|
|
383
|
+
mock_check_license,
|
|
384
|
+
mock_api,
|
|
385
|
+
mock_get_regscale_data_and_attachments,
|
|
386
|
+
mock_fetch_jira_objects,
|
|
387
|
+
mock_create_jira_client,
|
|
388
|
+
mock_sync_regscale_to_jira,
|
|
389
|
+
mock_sync_regscale_objects_to_jira,
|
|
390
|
+
fetch_attachments,
|
|
391
|
+
):
|
|
392
|
+
"""Test sync_regscale_and_jira with updates from both sides"""
|
|
393
|
+
# mock jira client so we can check it was correctly used later
|
|
394
|
+
mock_jira_client = MagicMock()
|
|
395
|
+
mock_create_jira_client.return_value = mock_jira_client
|
|
396
|
+
|
|
397
|
+
# Setup mock config with jiraCustomFields returning empty dict
|
|
398
|
+
mock_config = MagicMock()
|
|
399
|
+
mock_config.get.return_value = {}
|
|
400
|
+
mock_check_license.return_value.config = mock_config
|
|
401
|
+
|
|
402
|
+
# mock these so that we can control what objects were returned to check later
|
|
403
|
+
mock_fetch_jira_objects.return_value = MagicMock()
|
|
404
|
+
mock_get_regscale_data_and_attachments.return_value = (MagicMock(), MagicMock())
|
|
405
|
+
|
|
406
|
+
sync_regscale_and_jira(
|
|
407
|
+
parent_id=self.PARENT_ID,
|
|
408
|
+
parent_module=self.PARENT_MODULE,
|
|
409
|
+
jira_project=self.JIRA_PROJECT,
|
|
410
|
+
jira_issue_type="Bug",
|
|
411
|
+
sync_attachments=fetch_attachments,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
# check that we get the jira client correctly
|
|
415
|
+
mock_create_jira_client.assert_called_once()
|
|
416
|
+
|
|
417
|
+
# check that we correctly fetch objects from jira and regscale
|
|
418
|
+
mock_get_regscale_data_and_attachments.assert_called_once_with(
|
|
419
|
+
parent_id=self.PARENT_ID,
|
|
420
|
+
parent_module=self.PARENT_MODULE,
|
|
421
|
+
sync_attachments=fetch_attachments,
|
|
422
|
+
sync_tasks_only=False,
|
|
423
|
+
)
|
|
424
|
+
mock_fetch_jira_objects.assert_called_once_with(
|
|
425
|
+
jira_client=mock_jira_client,
|
|
426
|
+
jira_project=self.JIRA_PROJECT,
|
|
427
|
+
jql_str="project = 'SNES'",
|
|
428
|
+
jira_issue_type="Bug",
|
|
429
|
+
sync_tasks_only=False,
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
# check that updates were made with correct objects
|
|
433
|
+
mock_sync_regscale_to_jira.assert_called_once_with(
|
|
434
|
+
regscale_objects=mock_get_regscale_data_and_attachments.return_value[0],
|
|
435
|
+
jira_client=mock_jira_client,
|
|
436
|
+
jira_project=self.JIRA_PROJECT,
|
|
437
|
+
jira_issue_type="Bug",
|
|
438
|
+
api=mock_api.return_value,
|
|
439
|
+
sync_attachments=fetch_attachments,
|
|
440
|
+
attachments=mock_get_regscale_data_and_attachments.return_value[1],
|
|
441
|
+
custom_fields={},
|
|
442
|
+
)
|
|
443
|
+
mock_sync_regscale_objects_to_jira.assert_called_once_with(
|
|
444
|
+
mock_fetch_jira_objects.return_value,
|
|
445
|
+
mock_get_regscale_data_and_attachments.return_value[0],
|
|
446
|
+
fetch_attachments,
|
|
447
|
+
mock_check_license.return_value,
|
|
448
|
+
self.PARENT_ID,
|
|
449
|
+
self.PARENT_MODULE,
|
|
450
|
+
False,
|
|
451
|
+
False,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
@patch(f"{PATH}.sync_regscale_objects_to_jira")
|
|
455
|
+
@patch(f"{PATH}.sync_regscale_to_jira", return_value=[])
|
|
456
|
+
@patch(f"{PATH}.create_jira_client")
|
|
457
|
+
@patch(f"{PATH}.fetch_jira_objects")
|
|
458
|
+
@patch(f"{PATH}.get_regscale_data_and_attachments")
|
|
459
|
+
@patch(f"{PATH}.Api", return_value=MagicMock(spec=Api))
|
|
460
|
+
@patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
|
|
461
|
+
def test_sync_regscale_and_jira_custom_jql(
|
|
462
|
+
self,
|
|
463
|
+
mock_check_license,
|
|
464
|
+
mock_api,
|
|
465
|
+
mock_get_regscale_data_and_attachments,
|
|
466
|
+
mock_fetch_jira_objects,
|
|
467
|
+
mock_create_jira_client,
|
|
468
|
+
mock_sync_regscale_to_jira,
|
|
469
|
+
mock_sync_regscale_objects_to_jira,
|
|
470
|
+
fetch_attachments,
|
|
471
|
+
):
|
|
472
|
+
"""Test sync_regscale_and_jira with custom JQL query"""
|
|
473
|
+
# mock jira client so we can check it was correctly used later
|
|
474
|
+
mock_jira_client = MagicMock()
|
|
475
|
+
mock_create_jira_client.return_value = mock_jira_client
|
|
476
|
+
|
|
477
|
+
# mock these so that we can control what objects were returned to check later
|
|
478
|
+
mock_fetch_jira_objects.return_value = []
|
|
479
|
+
mock_get_regscale_data_and_attachments.return_value = ([], {})
|
|
480
|
+
|
|
481
|
+
custom_jql = "project = SNES AND assignee = currentUser() AND status != Closed"
|
|
482
|
+
|
|
483
|
+
sync_regscale_and_jira(
|
|
484
|
+
parent_id=self.PARENT_ID,
|
|
485
|
+
parent_module=self.PARENT_MODULE,
|
|
486
|
+
jira_project=self.JIRA_PROJECT,
|
|
487
|
+
jira_issue_type="Bug",
|
|
488
|
+
sync_attachments=fetch_attachments,
|
|
489
|
+
jql=custom_jql,
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
# check that we get the jira client correctly
|
|
493
|
+
mock_create_jira_client.assert_called_once()
|
|
494
|
+
|
|
495
|
+
# check that we correctly fetch objects from jira and regscale
|
|
496
|
+
mock_get_regscale_data_and_attachments.assert_called_once_with(
|
|
497
|
+
parent_id=self.PARENT_ID,
|
|
498
|
+
parent_module=self.PARENT_MODULE,
|
|
499
|
+
sync_attachments=fetch_attachments,
|
|
500
|
+
sync_tasks_only=False,
|
|
501
|
+
)
|
|
502
|
+
# Verify that the custom JQL was used instead of the default
|
|
503
|
+
mock_fetch_jira_objects.assert_called_once_with(
|
|
504
|
+
jira_client=mock_jira_client,
|
|
505
|
+
jira_project=self.JIRA_PROJECT,
|
|
506
|
+
jql_str=custom_jql,
|
|
507
|
+
jira_issue_type="Bug",
|
|
508
|
+
sync_tasks_only=False,
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
@patch(f"{PATH}.sync_regscale_objects_to_jira")
|
|
512
|
+
@patch(f"{PATH}.sync_regscale_to_jira", return_value=[])
|
|
513
|
+
@patch(f"{PATH}.create_jira_client")
|
|
514
|
+
@patch(f"{PATH}.fetch_jira_objects")
|
|
515
|
+
@patch(f"{PATH}.get_regscale_data_and_attachments")
|
|
516
|
+
@patch(f"{PATH}.Api", return_value=MagicMock(spec=Api))
|
|
517
|
+
@patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
|
|
518
|
+
def test_sync_regscale_and_jira_custom_jql_tasks(
|
|
519
|
+
self,
|
|
520
|
+
mock_check_license,
|
|
521
|
+
mock_api,
|
|
522
|
+
mock_get_regscale_data_and_attachments,
|
|
523
|
+
mock_fetch_jira_objects,
|
|
524
|
+
mock_create_jira_client,
|
|
525
|
+
mock_sync_regscale_to_jira,
|
|
526
|
+
mock_sync_regscale_objects_to_jira,
|
|
527
|
+
fetch_attachments,
|
|
528
|
+
):
|
|
529
|
+
"""Test sync_regscale_and_jira with custom JQL query for tasks"""
|
|
530
|
+
# mock jira client so we can check it was correctly used later
|
|
531
|
+
mock_jira_client = MagicMock()
|
|
532
|
+
mock_create_jira_client.return_value = mock_jira_client
|
|
533
|
+
|
|
534
|
+
# mock these so that we can control what objects were returned to check later
|
|
535
|
+
mock_fetch_jira_objects.return_value = []
|
|
536
|
+
mock_get_regscale_data_and_attachments.return_value = ([], {})
|
|
537
|
+
|
|
538
|
+
custom_jql = "project = SNES AND assignee = currentUser() AND issueType = Task"
|
|
539
|
+
|
|
540
|
+
sync_regscale_and_jira(
|
|
541
|
+
parent_id=self.PARENT_ID,
|
|
542
|
+
parent_module=self.PARENT_MODULE,
|
|
543
|
+
jira_project=self.JIRA_PROJECT,
|
|
544
|
+
jira_issue_type="Task",
|
|
545
|
+
sync_attachments=fetch_attachments,
|
|
546
|
+
sync_tasks_only=True,
|
|
547
|
+
jql=custom_jql,
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
# check that we get the jira client correctly
|
|
551
|
+
mock_create_jira_client.assert_called_once()
|
|
552
|
+
|
|
553
|
+
# check that we correctly fetch objects from jira and regscale
|
|
554
|
+
mock_get_regscale_data_and_attachments.assert_called_once_with(
|
|
555
|
+
parent_id=self.PARENT_ID,
|
|
556
|
+
parent_module=self.PARENT_MODULE,
|
|
557
|
+
sync_attachments=fetch_attachments,
|
|
558
|
+
sync_tasks_only=True,
|
|
559
|
+
)
|
|
560
|
+
# Verify that the custom JQL was used instead of the default task-specific JQL
|
|
561
|
+
mock_fetch_jira_objects.assert_called_once_with(
|
|
562
|
+
jira_client=mock_jira_client,
|
|
563
|
+
jira_project=self.JIRA_PROJECT,
|
|
564
|
+
jql_str=custom_jql,
|
|
565
|
+
jira_issue_type="Task",
|
|
566
|
+
sync_tasks_only=True,
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
@patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
|
|
570
|
+
def test_sync_regscale_objects_to_jira(
|
|
571
|
+
self, mock_check_license, fetch_attachments, get_jira_issue, regscale_issues_and_attachments
|
|
572
|
+
):
|
|
573
|
+
"""Test syncing RegScale objects to Jira"""
|
|
574
|
+
mock_check_license.return_value.config = self.config
|
|
575
|
+
try:
|
|
576
|
+
sync_regscale_objects_to_jira(
|
|
577
|
+
jira_issues=[get_jira_issue],
|
|
578
|
+
regscale_objects=regscale_issues_and_attachments[0],
|
|
579
|
+
sync_attachments=fetch_attachments,
|
|
580
|
+
app=self.app,
|
|
581
|
+
parent_id=self.PARENT_ID,
|
|
582
|
+
parent_module=self.PARENT_MODULE,
|
|
583
|
+
sync_tasks_only=False,
|
|
584
|
+
)
|
|
585
|
+
except Exception as e:
|
|
586
|
+
pytest.fail("Jira & RegScale task sync failed: {}".format(e))
|
|
587
|
+
|
|
588
|
+
@patch(f"{PATH}.create_jira_client")
|
|
589
|
+
@patch(f"{PATH}.create_and_update_regscale_issues")
|
|
590
|
+
@patch(f"{PATH}.create_and_update_regscale_tasks")
|
|
591
|
+
@patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
|
|
592
|
+
def test_sync_regscale_objects_to_jira_tasks_only(
|
|
593
|
+
self,
|
|
594
|
+
mock_check_license,
|
|
595
|
+
mock_create_and_update_regscale_tasks,
|
|
596
|
+
mock_create_and_update_regscale_issues,
|
|
597
|
+
mock_create_jira_client,
|
|
598
|
+
fetch_attachments,
|
|
599
|
+
):
|
|
600
|
+
"""Test syncing RegScale objects to Jira with sync_tasks_only=True"""
|
|
601
|
+
mock_check_license.return_value.config = self.config
|
|
602
|
+
mock_jira_client = MagicMock()
|
|
603
|
+
mock_create_jira_client.return_value = mock_jira_client
|
|
604
|
+
mock_create_and_update_regscale_tasks.return_value = (1, 0, 0)
|
|
605
|
+
|
|
606
|
+
sync_regscale_objects_to_jira(
|
|
607
|
+
MagicMock(),
|
|
608
|
+
MagicMock(),
|
|
609
|
+
fetch_attachments,
|
|
610
|
+
MagicMock(spec=Application),
|
|
611
|
+
self.PARENT_ID,
|
|
612
|
+
self.PARENT_MODULE,
|
|
613
|
+
True,
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
mock_create_and_update_regscale_tasks.assert_called_once()
|
|
617
|
+
mock_create_and_update_regscale_issues.assert_not_called()
|
|
618
|
+
|
|
619
|
+
@patch(f"{PATH}.create_jira_client")
|
|
620
|
+
@patch(f"{PATH}.create_and_update_regscale_tasks")
|
|
621
|
+
@patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
|
|
622
|
+
def test_sync_regscale_objects_to_jira_all(
|
|
623
|
+
self,
|
|
624
|
+
mock_check_license,
|
|
625
|
+
mock_create_and_update_regscale_tasks,
|
|
626
|
+
mock_create_jira_client,
|
|
627
|
+
fetch_attachments,
|
|
628
|
+
):
|
|
629
|
+
"""Test syncing RegScale objects to Jira with sync_tasks_only=False"""
|
|
630
|
+
mock_check_license.return_value.config = self.config
|
|
631
|
+
mock_jira_client = MagicMock()
|
|
632
|
+
mock_create_jira_client.return_value = mock_jira_client
|
|
633
|
+
|
|
634
|
+
# Create mock application with ThreadManager
|
|
635
|
+
mock_app = MagicMock(spec=Application)
|
|
636
|
+
mock_app.config = self.config
|
|
637
|
+
mock_thread_manager = MagicMock()
|
|
638
|
+
mock_app.thread_manager = mock_thread_manager
|
|
639
|
+
|
|
640
|
+
# Mock Jira issues
|
|
641
|
+
mock_jira_issues = [MagicMock(), MagicMock()]
|
|
642
|
+
|
|
643
|
+
sync_regscale_objects_to_jira(
|
|
644
|
+
mock_jira_issues,
|
|
645
|
+
MagicMock(),
|
|
646
|
+
fetch_attachments,
|
|
647
|
+
mock_app,
|
|
648
|
+
self.PARENT_ID,
|
|
649
|
+
self.PARENT_MODULE,
|
|
650
|
+
False,
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
mock_create_and_update_regscale_tasks.assert_not_called()
|
|
654
|
+
# Verify ThreadManager methods were called
|
|
655
|
+
mock_thread_manager.submit_tasks_from_list.assert_called_once()
|
|
656
|
+
mock_thread_manager.execute_and_verify.assert_called_once()
|
|
657
|
+
|
|
658
|
+
@patch(f"{PATH}.JIRA")
|
|
659
|
+
def test_create_jira_client_basic(self, mock_jira):
|
|
660
|
+
"""Test creating a Jira client"""
|
|
661
|
+
conf = {
|
|
662
|
+
"jiraUrl": "https://example.com",
|
|
663
|
+
"jiraApiToken": "token",
|
|
664
|
+
"jiraUserName": "user",
|
|
665
|
+
}
|
|
666
|
+
_ = create_jira_client(conf, token_auth=False)
|
|
667
|
+
mock_jira.assert_called_once_with(basic_auth=("user", "token"), options={"server": "https://example.com"})
|
|
668
|
+
|
|
669
|
+
@patch(f"{PATH}.JIRA")
|
|
670
|
+
def test_create_jira_client_token(self, mock_jira):
|
|
671
|
+
"""Test creating a Jira client with token auth"""
|
|
672
|
+
from regscale.integrations.variables import ScannerVariables
|
|
673
|
+
|
|
674
|
+
conf = {
|
|
675
|
+
"jiraUrl": "https://example.com",
|
|
676
|
+
"jiraApiToken": "token",
|
|
677
|
+
"jiraUserName": "user",
|
|
678
|
+
}
|
|
679
|
+
_ = create_jira_client(conf, token_auth=True)
|
|
680
|
+
mock_jira.assert_called_once_with(
|
|
681
|
+
token_auth="token", options={"server": "https://example.com", "verify": ScannerVariables.sslVerify}
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
@staticmethod
|
|
685
|
+
def test_jira_issues(jira_issues, fetch_attachments):
|
|
686
|
+
"""Test fetching Jira issues and creating a Jira client"""
|
|
687
|
+
has_attachments = None
|
|
688
|
+
if fetch_attachments:
|
|
689
|
+
assert jira_issues is not None
|
|
690
|
+
assert True in [True if issue.fields.attachment else False for issue in jira_issues]
|
|
691
|
+
else:
|
|
692
|
+
assert jira_issues is not None
|
|
693
|
+
try:
|
|
694
|
+
# try to access the attachment attribute
|
|
695
|
+
has_attachments = [True if issue.fields.attachment else False for issue in jira_issues]
|
|
696
|
+
assert len(has_attachments) == len(jira_issues)
|
|
697
|
+
except AttributeError:
|
|
698
|
+
# if the attribute doesn't exist, then we know there are no attachments
|
|
699
|
+
assert has_attachments is None
|
|
700
|
+
|
|
701
|
+
@staticmethod
|
|
702
|
+
def test_jira_tasks(jira_tasks, fetch_attachments):
|
|
703
|
+
"""Test fetching Jira tasks and creating a Jira client"""
|
|
704
|
+
has_attachments = None
|
|
705
|
+
if fetch_attachments:
|
|
706
|
+
assert jira_tasks is not None
|
|
707
|
+
assert True in [True if task.fields.attachment else False for task in jira_tasks]
|
|
708
|
+
else:
|
|
709
|
+
assert jira_tasks is not None
|
|
710
|
+
try:
|
|
711
|
+
# try to access the attachment attribute
|
|
712
|
+
has_attachments = [True if task.fields.attachment else False for task in jira_tasks]
|
|
713
|
+
assert len(has_attachments) == len(jira_tasks)
|
|
714
|
+
except AttributeError:
|
|
715
|
+
# if the attribute doesn't exist, then we know there are no attachments
|
|
716
|
+
assert has_attachments is None
|
|
717
|
+
|
|
718
|
+
@staticmethod
|
|
719
|
+
def test_fetch_regscale_issues_and_attachments(regscale_issues_and_attachments, fetch_attachments):
|
|
720
|
+
"""Test fetching RegScale issues and attachments"""
|
|
721
|
+
issues, attachments = regscale_issues_and_attachments
|
|
722
|
+
assert issues is not None
|
|
723
|
+
if fetch_attachments:
|
|
724
|
+
assert attachments is not None
|
|
725
|
+
else:
|
|
726
|
+
assert attachments == []
|
|
727
|
+
|
|
728
|
+
@staticmethod
|
|
729
|
+
def test_fetch_regscale_tasks_and_attachments(regscale_tasks_and_attachments, fetch_attachments):
|
|
730
|
+
"""Test fetching RegScale tasks and attachments"""
|
|
731
|
+
tasks, attachments = regscale_tasks_and_attachments
|
|
732
|
+
assert tasks is not None
|
|
733
|
+
if fetch_attachments:
|
|
734
|
+
assert attachments != []
|
|
735
|
+
else:
|
|
736
|
+
assert attachments == []
|
|
737
|
+
|
|
738
|
+
@pytest.mark.parametrize(
|
|
739
|
+
"due_date,priority,expected_days",
|
|
740
|
+
[
|
|
741
|
+
("2024-12-31", "High", 7), # Has due date
|
|
742
|
+
(None, "High", 7), # No due date, high priority
|
|
743
|
+
(None, "Medium", 14), # No due date, medium priority
|
|
744
|
+
(None, "Low", 30), # No due date, low priority
|
|
745
|
+
(None, None, 14), # No due date, no priority (defaults to medium)
|
|
746
|
+
],
|
|
747
|
+
)
|
|
748
|
+
@patch(f"{PATH}.datetime")
|
|
749
|
+
def test_map_jira_due_date(self, mock_datetime, due_date, priority, expected_days):
|
|
750
|
+
"""Test mapping Jira due dates to RegScale format"""
|
|
751
|
+
# Set up mock datetime to return a fixed date
|
|
752
|
+
fixed_date = datetime(2024, 1, 1, 12, 0, 0)
|
|
753
|
+
mock_datetime.now.return_value = fixed_date
|
|
754
|
+
mock_datetime.timedelta = timedelta # Allow timedelta to work normally
|
|
755
|
+
|
|
756
|
+
# Create mock Jira issue
|
|
757
|
+
mock_issue = MagicMock()
|
|
758
|
+
mock_issue.fields.duedate = due_date
|
|
759
|
+
if priority:
|
|
760
|
+
mock_issue.fields.priority = MagicMock()
|
|
761
|
+
mock_issue.fields.priority.name = priority
|
|
762
|
+
else:
|
|
763
|
+
mock_issue.fields.priority = None
|
|
764
|
+
|
|
765
|
+
# Create mock config
|
|
766
|
+
mock_config = {"issues": {"jira": {"high": 7, "medium": 14, "low": 30}}}
|
|
767
|
+
|
|
768
|
+
result = map_jira_due_date(mock_issue, mock_config)
|
|
769
|
+
|
|
770
|
+
if due_date:
|
|
771
|
+
assert result == due_date
|
|
772
|
+
else:
|
|
773
|
+
# Calculate expected date using the same fixed date
|
|
774
|
+
expected_date = fixed_date + timedelta(days=expected_days)
|
|
775
|
+
result_date = datetime.strptime(result, "%Y-%m-%d %H:%M:%S")
|
|
776
|
+
assert result_date.date() == expected_date.date()
|
|
777
|
+
|
|
778
|
+
@pytest.mark.parametrize(
|
|
779
|
+
"status,expected_status",
|
|
780
|
+
[
|
|
781
|
+
("Done", "Closed"),
|
|
782
|
+
("In Progress", "Open"),
|
|
783
|
+
("To Do", "Open"),
|
|
784
|
+
],
|
|
785
|
+
)
|
|
786
|
+
def test_map_jira_to_regscale_issue(self, status, expected_status):
|
|
787
|
+
"""Test mapping Jira issues to RegScale format"""
|
|
788
|
+
# Create mock Jira issue
|
|
789
|
+
issue_status = MagicMock()
|
|
790
|
+
issue_status.name = status
|
|
791
|
+
mock_issue = MagicMock(
|
|
792
|
+
key="TEST-123",
|
|
793
|
+
fields=MagicMock(
|
|
794
|
+
summary="Skipped task",
|
|
795
|
+
description="Skipped task description",
|
|
796
|
+
status=issue_status,
|
|
797
|
+
duedate=None,
|
|
798
|
+
statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
|
|
799
|
+
),
|
|
800
|
+
)
|
|
801
|
+
mock_issue.fields.priority = MagicMock()
|
|
802
|
+
mock_issue.fields.priority.name = "High"
|
|
803
|
+
|
|
804
|
+
# Create mock config
|
|
805
|
+
mock_config = {
|
|
806
|
+
"userId": "1", # Convert to string to match Optional[str] type
|
|
807
|
+
"issues": {"jira": {"status": "Open", "high": 7, "medium": 14, "low": 30}},
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
result = map_jira_to_regscale_issue(mock_issue, mock_config, 1, "issues")
|
|
811
|
+
|
|
812
|
+
# Verify the Issue object was created with correct attributes
|
|
813
|
+
assert isinstance(result, Issue)
|
|
814
|
+
assert result.title == "Skipped task"
|
|
815
|
+
assert "Skipped task description" in result.description
|
|
816
|
+
assert result.status == expected_status
|
|
817
|
+
assert result.jiraId == "TEST-123"
|
|
818
|
+
assert result.parentId == 1
|
|
819
|
+
assert result.parentModule == "issues"
|
|
820
|
+
assert result.dueDate is not None # Due date will be calculated based on priority
|
|
821
|
+
if status == "Done":
|
|
822
|
+
assert result.dateCompleted is not None
|
|
823
|
+
else:
|
|
824
|
+
assert result.dateCompleted is None
|
|
825
|
+
|
|
826
|
+
@pytest.mark.parametrize(
|
|
827
|
+
"status,expected_status,expected_percent_complete,expected_date_closed",
|
|
828
|
+
[
|
|
829
|
+
("Done", "Closed", 100, True),
|
|
830
|
+
("In Progress", "Open", None, False),
|
|
831
|
+
("To Do", "Backlog", None, False),
|
|
832
|
+
],
|
|
833
|
+
)
|
|
834
|
+
def test_create_regscale_task_from_jira(
|
|
835
|
+
self, status, expected_status, expected_percent_complete, expected_date_closed
|
|
836
|
+
):
|
|
837
|
+
"""Test creating RegScale tasks from Jira issues"""
|
|
838
|
+
# Create mock Jira issue
|
|
839
|
+
mock_issue = MagicMock()
|
|
840
|
+
mock_issue.fields.summary = "Test Task"
|
|
841
|
+
mock_issue.fields.description = "Test Description"
|
|
842
|
+
mock_issue.fields.status.name = status
|
|
843
|
+
mock_issue.fields.duedate = "2024-12-31"
|
|
844
|
+
mock_issue.fields.statuscategorychangedate = "2024-01-01T12:00:00.000Z"
|
|
845
|
+
mock_issue.key = "TEST-123"
|
|
846
|
+
|
|
847
|
+
# Create mock config
|
|
848
|
+
mock_config = {"issues": {"jira": {"medium": 14}}}
|
|
849
|
+
|
|
850
|
+
result = create_regscale_task_from_jira(mock_config, mock_issue, 1, "issues")
|
|
851
|
+
|
|
852
|
+
assert result.title == "Test Task"
|
|
853
|
+
assert result.description == "Test Description"
|
|
854
|
+
assert result.status == expected_status
|
|
855
|
+
assert result.dueDate == "2024-12-31"
|
|
856
|
+
assert result.parentId == 1
|
|
857
|
+
assert result.parentModule == "issues"
|
|
858
|
+
assert result.otherIdentifier == "TEST-123"
|
|
859
|
+
if expected_percent_complete:
|
|
860
|
+
assert result.percentComplete == expected_percent_complete
|
|
861
|
+
if expected_date_closed:
|
|
862
|
+
assert result.dateClosed is not None
|
|
863
|
+
else:
|
|
864
|
+
assert result.dateClosed is None
|
|
865
|
+
|
|
866
|
+
def test_check_and_close_tasks(self):
|
|
867
|
+
"""Test checking and closing tasks"""
|
|
868
|
+
jira_titles = {"Testing1234"}
|
|
869
|
+
tasks = [
|
|
870
|
+
Task(
|
|
871
|
+
id=3,
|
|
872
|
+
title="Different Title",
|
|
873
|
+
status="Backlog",
|
|
874
|
+
dueDate=get_current_datetime(),
|
|
875
|
+
dateClosed="",
|
|
876
|
+
percentComplete=0,
|
|
877
|
+
),
|
|
878
|
+
Task(
|
|
879
|
+
id=4,
|
|
880
|
+
title="Testing1234",
|
|
881
|
+
status="Backlog",
|
|
882
|
+
dueDate=get_current_datetime(),
|
|
883
|
+
dateClosed="",
|
|
884
|
+
percentComplete=0,
|
|
885
|
+
),
|
|
886
|
+
]
|
|
887
|
+
|
|
888
|
+
closed_tasks = check_and_close_tasks(tasks, set(jira_titles))
|
|
889
|
+
assert len(closed_tasks) == 1
|
|
890
|
+
assert closed_tasks[0].status == "Closed"
|
|
891
|
+
assert closed_tasks[0].percentComplete == 100
|
|
892
|
+
|
|
893
|
+
def test_process_tasks_for_sync(self):
|
|
894
|
+
"""Test processing tasks for sync"""
|
|
895
|
+
todo_status = MagicMock()
|
|
896
|
+
todo_status.name = "to do"
|
|
897
|
+
in_progress_status = MagicMock()
|
|
898
|
+
in_progress_status.name = "in progress"
|
|
899
|
+
done_status = MagicMock()
|
|
900
|
+
done_status.name = "done"
|
|
901
|
+
# Create mock Jira issues
|
|
902
|
+
jira_tasks = [
|
|
903
|
+
MagicMock( # should be skipped (up to date - nothing happens)
|
|
904
|
+
key="JIRA-1",
|
|
905
|
+
fields=MagicMock(
|
|
906
|
+
summary="Skipped task",
|
|
907
|
+
description="Skipped task description",
|
|
908
|
+
status=todo_status,
|
|
909
|
+
duedate=None,
|
|
910
|
+
statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
|
|
911
|
+
priority=None,
|
|
912
|
+
),
|
|
913
|
+
),
|
|
914
|
+
MagicMock( # should be inserted (task in jira but not regscale)
|
|
915
|
+
key="JIRA-2",
|
|
916
|
+
fields=MagicMock(
|
|
917
|
+
summary="New task",
|
|
918
|
+
description="New task description",
|
|
919
|
+
status=todo_status,
|
|
920
|
+
duedate=None,
|
|
921
|
+
statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
|
|
922
|
+
priority=None,
|
|
923
|
+
),
|
|
924
|
+
),
|
|
925
|
+
MagicMock( # should be updated (task in both but out of sync)
|
|
926
|
+
key="JIRA-3",
|
|
927
|
+
fields=MagicMock(
|
|
928
|
+
summary="Existing task",
|
|
929
|
+
description="Existing task description",
|
|
930
|
+
status=in_progress_status,
|
|
931
|
+
duedate=None,
|
|
932
|
+
statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
|
|
933
|
+
priority=None,
|
|
934
|
+
),
|
|
935
|
+
),
|
|
936
|
+
MagicMock( # should be updated
|
|
937
|
+
key="JIRA-4",
|
|
938
|
+
fields=MagicMock(
|
|
939
|
+
summary="Existing task",
|
|
940
|
+
description="Existing task description",
|
|
941
|
+
status=todo_status,
|
|
942
|
+
duedate=None,
|
|
943
|
+
statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
|
|
944
|
+
priority=None,
|
|
945
|
+
),
|
|
946
|
+
),
|
|
947
|
+
MagicMock( # should be closed
|
|
948
|
+
key="JIRA-5",
|
|
949
|
+
fields=MagicMock(
|
|
950
|
+
summary="Existing task",
|
|
951
|
+
description="Existing task description",
|
|
952
|
+
status=done_status,
|
|
953
|
+
duedate=None,
|
|
954
|
+
statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
|
|
955
|
+
priority=None,
|
|
956
|
+
),
|
|
957
|
+
),
|
|
958
|
+
MagicMock( # date to be updated
|
|
959
|
+
key="JIRA-6",
|
|
960
|
+
fields=MagicMock(
|
|
961
|
+
summary="Existing task",
|
|
962
|
+
description="Existing task description",
|
|
963
|
+
status=todo_status,
|
|
964
|
+
duedate="2024-12-31",
|
|
965
|
+
statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
|
|
966
|
+
priority=None,
|
|
967
|
+
),
|
|
968
|
+
),
|
|
969
|
+
]
|
|
970
|
+
regscale_tasks = [
|
|
971
|
+
Task( # matches JIRA-1 (should be skipped - up to date)
|
|
972
|
+
id=1,
|
|
973
|
+
title="Skipped task",
|
|
974
|
+
status="Backlog",
|
|
975
|
+
description="Skipped task description",
|
|
976
|
+
otherIdentifier="JIRA-1",
|
|
977
|
+
parentId=self.PARENT_ID,
|
|
978
|
+
parentModule=self.PARENT_MODULE,
|
|
979
|
+
dueDate=get_current_datetime(),
|
|
980
|
+
),
|
|
981
|
+
Task( # matches JIRA-3 (should be updated - task in both but out of sync)
|
|
982
|
+
id=3,
|
|
983
|
+
title="Existing task",
|
|
984
|
+
status="Open",
|
|
985
|
+
description="Different description", # Different description to show sync needed
|
|
986
|
+
otherIdentifier="JIRA-3",
|
|
987
|
+
parentId=self.PARENT_ID,
|
|
988
|
+
parentModule=self.PARENT_MODULE,
|
|
989
|
+
dueDate=get_current_datetime(),
|
|
990
|
+
),
|
|
991
|
+
Task( # matches JIRA-4 (should be updated - regscale closed but jira open)
|
|
992
|
+
id=4,
|
|
993
|
+
title="Existing task",
|
|
994
|
+
status="Closed", # Closed in RegScale but open in Jira
|
|
995
|
+
description="Existing task description",
|
|
996
|
+
otherIdentifier="JIRA-4",
|
|
997
|
+
parentId=self.PARENT_ID,
|
|
998
|
+
parentModule=self.PARENT_MODULE,
|
|
999
|
+
dueDate=get_current_datetime(),
|
|
1000
|
+
),
|
|
1001
|
+
Task( # matches JIRA-5 (should be closed - jira closed but regscale open)
|
|
1002
|
+
id=5,
|
|
1003
|
+
title="Existing task",
|
|
1004
|
+
status="Open", # Open in RegScale but closed in Jira
|
|
1005
|
+
description="Existing task description",
|
|
1006
|
+
otherIdentifier="JIRA-5",
|
|
1007
|
+
parentId=self.PARENT_ID,
|
|
1008
|
+
parentModule=self.PARENT_MODULE,
|
|
1009
|
+
dueDate=get_current_datetime(),
|
|
1010
|
+
),
|
|
1011
|
+
Task(
|
|
1012
|
+
id=6,
|
|
1013
|
+
title="New task",
|
|
1014
|
+
status="Backlog",
|
|
1015
|
+
description="Not in jira",
|
|
1016
|
+
otherIdentifier="JIRA-19",
|
|
1017
|
+
parentId=self.PARENT_ID,
|
|
1018
|
+
parentModule=self.PARENT_MODULE,
|
|
1019
|
+
),
|
|
1020
|
+
Task(
|
|
1021
|
+
id=7,
|
|
1022
|
+
title="Existing task",
|
|
1023
|
+
status="Backlog",
|
|
1024
|
+
description="Existing task description",
|
|
1025
|
+
otherIdentifier="JIRA-6",
|
|
1026
|
+
parentId=self.PARENT_ID,
|
|
1027
|
+
parentModule=self.PARENT_MODULE,
|
|
1028
|
+
dueDate="2023-01-01",
|
|
1029
|
+
),
|
|
1030
|
+
]
|
|
1031
|
+
|
|
1032
|
+
progress = MagicMock(spec=Progress)
|
|
1033
|
+
progress_task = MagicMock()
|
|
1034
|
+
|
|
1035
|
+
insert_tasks, update_tasks, close_tasks = process_tasks_for_sync(
|
|
1036
|
+
config=self.config,
|
|
1037
|
+
jira_issues=jira_tasks,
|
|
1038
|
+
existing_tasks=regscale_tasks,
|
|
1039
|
+
parent_id=self.PARENT_ID,
|
|
1040
|
+
parent_module=self.PARENT_MODULE,
|
|
1041
|
+
progress=progress,
|
|
1042
|
+
progress_task=progress_task,
|
|
1043
|
+
)
|
|
1044
|
+
|
|
1045
|
+
# Assertions
|
|
1046
|
+
assert len(insert_tasks) == 1 # New task should be inserted
|
|
1047
|
+
assert len(update_tasks) == 3 # No updates needed
|
|
1048
|
+
assert len(close_tasks) == 1 # Existing task should be closed
|
|
1049
|
+
|
|
1050
|
+
@patch(f"{PATH}.process_tasks_for_sync")
|
|
1051
|
+
@patch(f"{PATH}.task_and_attachments_sync")
|
|
1052
|
+
@patch("regscale.core.app.api.Api")
|
|
1053
|
+
@patch(f"{PATH}.ThreadPoolExecutor")
|
|
1054
|
+
@patch(f"{PATH}.as_completed")
|
|
1055
|
+
def test_create_and_update_regscale_tasks(
|
|
1056
|
+
self, mock_as_completed, mock_thread_pool, mock_api, mock_task_sync, mock_process_tasks_for_sync
|
|
1057
|
+
):
|
|
1058
|
+
"""Test creating and updating RegScale tasks from Jira tasks"""
|
|
1059
|
+
# Setup mock tasks
|
|
1060
|
+
insert_tasks = [
|
|
1061
|
+
Task(
|
|
1062
|
+
id=1,
|
|
1063
|
+
title="New task",
|
|
1064
|
+
status="Backlog",
|
|
1065
|
+
description="New task description",
|
|
1066
|
+
otherIdentifier="JIRA-19",
|
|
1067
|
+
parentId=self.PARENT_ID,
|
|
1068
|
+
parentModule=self.PARENT_MODULE,
|
|
1069
|
+
)
|
|
1070
|
+
]
|
|
1071
|
+
update_tasks = [
|
|
1072
|
+
Task(
|
|
1073
|
+
id=2,
|
|
1074
|
+
title="Existing task",
|
|
1075
|
+
status="Open",
|
|
1076
|
+
description="Existing task description",
|
|
1077
|
+
otherIdentifier="JIRA-3",
|
|
1078
|
+
parentId=self.PARENT_ID,
|
|
1079
|
+
parentModule=self.PARENT_MODULE,
|
|
1080
|
+
)
|
|
1081
|
+
]
|
|
1082
|
+
close_tasks = [
|
|
1083
|
+
Task(
|
|
1084
|
+
id=3,
|
|
1085
|
+
title="Existing task",
|
|
1086
|
+
status="Closed",
|
|
1087
|
+
description="Existing task description",
|
|
1088
|
+
otherIdentifier="JIRA-5",
|
|
1089
|
+
parentId=self.PARENT_ID,
|
|
1090
|
+
parentModule=self.PARENT_MODULE,
|
|
1091
|
+
)
|
|
1092
|
+
]
|
|
1093
|
+
mock_process_tasks_for_sync.return_value = (insert_tasks, update_tasks, close_tasks)
|
|
1094
|
+
|
|
1095
|
+
mock_api_instance = MagicMock()
|
|
1096
|
+
mock_api.return_value = mock_api_instance
|
|
1097
|
+
mock_api_instance.app.config = self.config
|
|
1098
|
+
|
|
1099
|
+
# Setup mock thread pool
|
|
1100
|
+
mock_executor = MagicMock()
|
|
1101
|
+
mock_thread_pool.return_value.__enter__.return_value = mock_executor
|
|
1102
|
+
|
|
1103
|
+
# Setup task_and_attachments_sync to return None
|
|
1104
|
+
mock_task_sync.return_value = None
|
|
1105
|
+
|
|
1106
|
+
# Make as_completed return an empty iterator
|
|
1107
|
+
mock_as_completed.return_value = []
|
|
1108
|
+
|
|
1109
|
+
inserted, updated, closed = create_and_update_regscale_tasks(
|
|
1110
|
+
jira_issues=[],
|
|
1111
|
+
existing_tasks=[],
|
|
1112
|
+
jira_client=MagicMock(),
|
|
1113
|
+
parent_id=self.PARENT_ID,
|
|
1114
|
+
parent_module=self.PARENT_MODULE,
|
|
1115
|
+
progress=MagicMock(spec=Progress),
|
|
1116
|
+
progress_task=MagicMock(),
|
|
1117
|
+
)
|
|
1118
|
+
|
|
1119
|
+
assert inserted == 1
|
|
1120
|
+
assert updated == 1
|
|
1121
|
+
assert closed == 1
|
|
1122
|
+
|
|
1123
|
+
mock_thread_pool.assert_called_once_with(max_workers=10)
|
|
1124
|
+
assert mock_executor.submit.call_count == 3
|
|
1125
|
+
|
|
1126
|
+
@patch(f"{PATH}.compare_files_for_dupes_and_upload")
|
|
1127
|
+
def test_tasks_and_attachments_sync_create(self, mock_compare_files_for_dupes_and_upload):
|
|
1128
|
+
"""Test performing create operation on tasks in task_and_attachments_sync"""
|
|
1129
|
+
# check if operation fails
|
|
1130
|
+
mock_task = MagicMock()
|
|
1131
|
+
mock_task.create.return_value = None
|
|
1132
|
+
task_and_attachments_sync(
|
|
1133
|
+
operation="create",
|
|
1134
|
+
task=mock_task,
|
|
1135
|
+
jira_client=MagicMock(),
|
|
1136
|
+
api=MagicMock(),
|
|
1137
|
+
)
|
|
1138
|
+
mock_task.create.assert_called_once()
|
|
1139
|
+
mock_compare_files_for_dupes_and_upload.assert_not_called()
|
|
1140
|
+
|
|
1141
|
+
# check if operation is successful
|
|
1142
|
+
mock_task.create.reset_mock()
|
|
1143
|
+
mock_task.create.return_value = MagicMock()
|
|
1144
|
+
task_and_attachments_sync(
|
|
1145
|
+
operation="create",
|
|
1146
|
+
task=mock_task,
|
|
1147
|
+
jira_client=MagicMock(),
|
|
1148
|
+
api=MagicMock(),
|
|
1149
|
+
)
|
|
1150
|
+
mock_task.create.assert_called_once()
|
|
1151
|
+
mock_compare_files_for_dupes_and_upload.assert_called_once()
|
|
1152
|
+
|
|
1153
|
+
@pytest.mark.parametrize("operation", ["update", "close"])
|
|
1154
|
+
@patch(f"{PATH}.compare_files_for_dupes_and_upload")
|
|
1155
|
+
def test_tasks_and_attachments_sync_save(self, mock_compare_files_for_dupes_and_upload, operation):
|
|
1156
|
+
"""Test performing save operation on tasks in task_and_attachments_sync"""
|
|
1157
|
+
# check if operation fails
|
|
1158
|
+
mock_task = MagicMock()
|
|
1159
|
+
mock_task.save.return_value = None
|
|
1160
|
+
task_and_attachments_sync(
|
|
1161
|
+
operation=operation,
|
|
1162
|
+
task=mock_task,
|
|
1163
|
+
jira_client=MagicMock(),
|
|
1164
|
+
api=MagicMock(),
|
|
1165
|
+
)
|
|
1166
|
+
mock_task.save.assert_called_once()
|
|
1167
|
+
mock_compare_files_for_dupes_and_upload.assert_not_called()
|
|
1168
|
+
|
|
1169
|
+
# check if operation is successful
|
|
1170
|
+
mock_task.save.reset_mock()
|
|
1171
|
+
mock_task.save.return_value = MagicMock()
|
|
1172
|
+
task_and_attachments_sync(
|
|
1173
|
+
operation=operation,
|
|
1174
|
+
task=mock_task,
|
|
1175
|
+
jira_client=MagicMock(),
|
|
1176
|
+
api=MagicMock(),
|
|
1177
|
+
)
|
|
1178
|
+
mock_task.save.assert_called_once()
|
|
1179
|
+
mock_compare_files_for_dupes_and_upload.assert_called_once()
|
|
1180
|
+
|
|
1181
|
+
@patch(f"{PATH}.compare_files_for_dupes_and_upload")
|
|
1182
|
+
@patch(f"{PATH}.map_jira_to_regscale_issue")
|
|
1183
|
+
@patch(f"{PATH}.Issue.save")
|
|
1184
|
+
@patch(f"{PATH}.job_progress", return_value=MagicMock(spec=Progress))
|
|
1185
|
+
def test_create_and_update_regscale_issues(
|
|
1186
|
+
self,
|
|
1187
|
+
mock_job_progress_object,
|
|
1188
|
+
mock_update_issue,
|
|
1189
|
+
mock_map_jira_to_regscale_issue,
|
|
1190
|
+
mock_compare_files_for_dupes_and_upload,
|
|
1191
|
+
fetch_attachments,
|
|
1192
|
+
):
|
|
1193
|
+
"""Test creating and updating RegScale issues from Jira issues"""
|
|
1194
|
+
open_status = MagicMock()
|
|
1195
|
+
open_status.name = "open"
|
|
1196
|
+
in_progress_status = MagicMock()
|
|
1197
|
+
in_progress_status.name = "in progress"
|
|
1198
|
+
closed_status = MagicMock()
|
|
1199
|
+
closed_status.name = "done"
|
|
1200
|
+
|
|
1201
|
+
highest_priority = MagicMock()
|
|
1202
|
+
highest_priority.name = "highest"
|
|
1203
|
+
high_priority = MagicMock()
|
|
1204
|
+
high_priority.name = "high"
|
|
1205
|
+
medium_priority = MagicMock()
|
|
1206
|
+
medium_priority.name = "medium"
|
|
1207
|
+
low_priority = MagicMock()
|
|
1208
|
+
low_priority.name = "low"
|
|
1209
|
+
lowest_priority = MagicMock()
|
|
1210
|
+
lowest_priority.name = "lowest"
|
|
1211
|
+
|
|
1212
|
+
# Create mock Jira issues
|
|
1213
|
+
jira_issue_1 = MagicMock( # should be updated (existing issue)
|
|
1214
|
+
key="JIRA-1",
|
|
1215
|
+
fields=MagicMock(
|
|
1216
|
+
summary="Skipped issue",
|
|
1217
|
+
description="Skipped issue description",
|
|
1218
|
+
status=open_status,
|
|
1219
|
+
duedate=None,
|
|
1220
|
+
priority=highest_priority,
|
|
1221
|
+
statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
|
|
1222
|
+
attachment=[MagicMock()], # Has attachments
|
|
1223
|
+
),
|
|
1224
|
+
)
|
|
1225
|
+
jira_issue_2 = MagicMock( # should be inserted (issue in jira but not regscale)
|
|
1226
|
+
key="JIRA-2",
|
|
1227
|
+
fields=MagicMock(
|
|
1228
|
+
summary="New issue",
|
|
1229
|
+
description="New issue description",
|
|
1230
|
+
status=open_status,
|
|
1231
|
+
duedate=None,
|
|
1232
|
+
priority=medium_priority,
|
|
1233
|
+
statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
|
|
1234
|
+
attachment=None,
|
|
1235
|
+
),
|
|
1236
|
+
)
|
|
1237
|
+
jira_issue_3 = MagicMock( # should be updated (issue in both but out of sync)
|
|
1238
|
+
key="JIRA-3",
|
|
1239
|
+
fields=MagicMock(
|
|
1240
|
+
summary="Existing issue",
|
|
1241
|
+
description="Existing issue description",
|
|
1242
|
+
status=in_progress_status,
|
|
1243
|
+
duedate=None,
|
|
1244
|
+
priority=low_priority,
|
|
1245
|
+
statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
|
|
1246
|
+
attachment=None, # No attachments
|
|
1247
|
+
),
|
|
1248
|
+
)
|
|
1249
|
+
jira_issue_4 = MagicMock( # should be closed - counts as updated
|
|
1250
|
+
key="JIRA-4",
|
|
1251
|
+
fields=MagicMock(
|
|
1252
|
+
summary="Existing issue",
|
|
1253
|
+
description="Existing issue description",
|
|
1254
|
+
status=closed_status,
|
|
1255
|
+
duedate=None,
|
|
1256
|
+
priority=lowest_priority,
|
|
1257
|
+
statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
|
|
1258
|
+
attachment=None, # No attachments
|
|
1259
|
+
),
|
|
1260
|
+
)
|
|
1261
|
+
|
|
1262
|
+
# Create RegScale issues
|
|
1263
|
+
regscale_issues = [
|
|
1264
|
+
Issue( # matches JIRA-1 (should be updated)
|
|
1265
|
+
id=1,
|
|
1266
|
+
title="Skipped issue",
|
|
1267
|
+
status="Open",
|
|
1268
|
+
description="Skipped issue description",
|
|
1269
|
+
jiraId="JIRA-1",
|
|
1270
|
+
parentId=self.PARENT_ID,
|
|
1271
|
+
parentModule=self.PARENT_MODULE,
|
|
1272
|
+
dueDate=get_current_datetime(),
|
|
1273
|
+
identification=f"{self.title_prefix} Jira Issue Integration Test",
|
|
1274
|
+
),
|
|
1275
|
+
Issue( # matches JIRA-3 (should be updated - issue in both but out of sync)
|
|
1276
|
+
id=3,
|
|
1277
|
+
title="Existing issue",
|
|
1278
|
+
status="Open",
|
|
1279
|
+
description="Different description", # Different description to show sync needed
|
|
1280
|
+
jiraId="JIRA-3",
|
|
1281
|
+
parentId=self.PARENT_ID,
|
|
1282
|
+
parentModule=self.PARENT_MODULE,
|
|
1283
|
+
dueDate=get_current_datetime(),
|
|
1284
|
+
identification=f"{self.title_prefix} Jira Issue Integration Test",
|
|
1285
|
+
),
|
|
1286
|
+
Issue( # matches JIRA-4 (should be closed - jira closed but regscale open)
|
|
1287
|
+
id=4,
|
|
1288
|
+
title="Existing issue",
|
|
1289
|
+
status="Open", # Open in RegScale but closed in Jira
|
|
1290
|
+
description="Existing issue description",
|
|
1291
|
+
jiraId="JIRA-4",
|
|
1292
|
+
parentId=self.PARENT_ID,
|
|
1293
|
+
parentModule=self.PARENT_MODULE,
|
|
1294
|
+
dueDate=get_current_datetime(),
|
|
1295
|
+
identification=f"{self.title_prefix} Jira Issue Integration Test",
|
|
1296
|
+
),
|
|
1297
|
+
]
|
|
1298
|
+
|
|
1299
|
+
# Create mock config with priority mappings from init.yaml
|
|
1300
|
+
config = {
|
|
1301
|
+
"issues": {"jira": {"highest": 7, "high": 30, "medium": 90, "low": 180, "lowest": 365, "status": "Open"}},
|
|
1302
|
+
"maxThreads": 4,
|
|
1303
|
+
"userId": "123e4567-e89b-12d3-a456-426614174000",
|
|
1304
|
+
}
|
|
1305
|
+
app = MagicMock()
|
|
1306
|
+
app.config = config
|
|
1307
|
+
|
|
1308
|
+
# Setup mock return values
|
|
1309
|
+
mock_update_issue.return_value = MagicMock()
|
|
1310
|
+
|
|
1311
|
+
# Mock the creation of a new issue
|
|
1312
|
+
created_issue_mock = MagicMock()
|
|
1313
|
+
created_issue_mock.id = 2
|
|
1314
|
+
created_issue_mock.create.return_value = created_issue_mock
|
|
1315
|
+
mock_map_jira_to_regscale_issue.return_value = created_issue_mock
|
|
1316
|
+
|
|
1317
|
+
with mock_job_progress_object as job_progress:
|
|
1318
|
+
test_task = job_progress.add_task(
|
|
1319
|
+
description="Processing issues",
|
|
1320
|
+
total=4,
|
|
1321
|
+
visible=False,
|
|
1322
|
+
)
|
|
1323
|
+
|
|
1324
|
+
# Test the function with each Jira issue individually (as ThreadManager would call it)
|
|
1325
|
+
# JIRA-1: existing issue with matching jiraId and attachments
|
|
1326
|
+
create_and_update_regscale_issues(
|
|
1327
|
+
jira_issue_1,
|
|
1328
|
+
regscale_issues,
|
|
1329
|
+
False,
|
|
1330
|
+
fetch_attachments,
|
|
1331
|
+
MagicMock(),
|
|
1332
|
+
app,
|
|
1333
|
+
self.PARENT_ID,
|
|
1334
|
+
self.PARENT_MODULE,
|
|
1335
|
+
test_task,
|
|
1336
|
+
job_progress,
|
|
1337
|
+
)
|
|
1338
|
+
|
|
1339
|
+
# JIRA-2: new issue (not in regscale_issues)
|
|
1340
|
+
create_and_update_regscale_issues(
|
|
1341
|
+
jira_issue_2,
|
|
1342
|
+
regscale_issues,
|
|
1343
|
+
False,
|
|
1344
|
+
fetch_attachments,
|
|
1345
|
+
MagicMock(),
|
|
1346
|
+
app,
|
|
1347
|
+
self.PARENT_ID,
|
|
1348
|
+
self.PARENT_MODULE,
|
|
1349
|
+
test_task,
|
|
1350
|
+
job_progress,
|
|
1351
|
+
)
|
|
1352
|
+
|
|
1353
|
+
# JIRA-3: existing issue to be updated
|
|
1354
|
+
create_and_update_regscale_issues(
|
|
1355
|
+
jira_issue_3,
|
|
1356
|
+
regscale_issues,
|
|
1357
|
+
False,
|
|
1358
|
+
fetch_attachments,
|
|
1359
|
+
MagicMock(),
|
|
1360
|
+
app,
|
|
1361
|
+
self.PARENT_ID,
|
|
1362
|
+
self.PARENT_MODULE,
|
|
1363
|
+
test_task,
|
|
1364
|
+
job_progress,
|
|
1365
|
+
)
|
|
1366
|
+
|
|
1367
|
+
# JIRA-4: existing issue to be closed
|
|
1368
|
+
create_and_update_regscale_issues(
|
|
1369
|
+
jira_issue_4,
|
|
1370
|
+
regscale_issues,
|
|
1371
|
+
False,
|
|
1372
|
+
fetch_attachments,
|
|
1373
|
+
MagicMock(),
|
|
1374
|
+
app,
|
|
1375
|
+
self.PARENT_ID,
|
|
1376
|
+
self.PARENT_MODULE,
|
|
1377
|
+
test_task,
|
|
1378
|
+
job_progress,
|
|
1379
|
+
)
|
|
1380
|
+
|
|
1381
|
+
# Verify update_issue was called 3 times (JIRA-1, JIRA-3, JIRA-4)
|
|
1382
|
+
assert mock_update_issue.call_count == 3
|
|
1383
|
+
# Verify map_jira_to_regscale_issue was called once for JIRA-2 (new issue)
|
|
1384
|
+
assert mock_map_jira_to_regscale_issue.call_count == 1
|
|
1385
|
+
# Verify attachment handling
|
|
1386
|
+
if fetch_attachments:
|
|
1387
|
+
# Only JIRA-1 has attachments in fields.attachment
|
|
1388
|
+
assert mock_compare_files_for_dupes_and_upload.call_count == 1
|
|
1389
|
+
else:
|
|
1390
|
+
assert mock_compare_files_for_dupes_and_upload.call_count == 0
|
|
1391
|
+
|
|
1392
|
+
@patch(f"{PATH}.create_issue_in_jira")
|
|
1393
|
+
def test_sync_regscale_issues_to_jira(self, mock_create_issue_in_jira, fetch_attachments):
|
|
1394
|
+
"""Test inserting Regscale issues into jira if they do not exist"""
|
|
1395
|
+
|
|
1396
|
+
# Create RegScale issues
|
|
1397
|
+
regscale_objects = [
|
|
1398
|
+
Issue(
|
|
1399
|
+
id=1,
|
|
1400
|
+
title="Test Issue",
|
|
1401
|
+
status="Open",
|
|
1402
|
+
description="This is a test issue",
|
|
1403
|
+
dueDate=get_current_datetime(),
|
|
1404
|
+
parentId=self.PARENT_ID,
|
|
1405
|
+
parentModule=self.PARENT_MODULE,
|
|
1406
|
+
identification=f"{self.title_prefix} Jira Issue Integration Test",
|
|
1407
|
+
),
|
|
1408
|
+
Issue(
|
|
1409
|
+
id=3,
|
|
1410
|
+
title="Test Issue with Jira ID",
|
|
1411
|
+
status="Open",
|
|
1412
|
+
description="This is a test issue with Jira ID",
|
|
1413
|
+
dueDate=get_current_datetime(),
|
|
1414
|
+
parentId=self.PARENT_ID,
|
|
1415
|
+
parentModule=self.PARENT_MODULE,
|
|
1416
|
+
jiraId="JIRA-3",
|
|
1417
|
+
identification=f"{self.title_prefix} Jira Issue Integration Test",
|
|
1418
|
+
),
|
|
1419
|
+
]
|
|
1420
|
+
|
|
1421
|
+
# Create mock Jira issues
|
|
1422
|
+
open_status = MagicMock()
|
|
1423
|
+
open_status.name = "open"
|
|
1424
|
+
|
|
1425
|
+
returned_jira_issues = [
|
|
1426
|
+
MagicMock(
|
|
1427
|
+
key="JIRA-1",
|
|
1428
|
+
fields=MagicMock(
|
|
1429
|
+
summary="Test Issue",
|
|
1430
|
+
description="This is a test issue",
|
|
1431
|
+
status=open_status,
|
|
1432
|
+
duedate=None,
|
|
1433
|
+
statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
|
|
1434
|
+
attachment=None,
|
|
1435
|
+
),
|
|
1436
|
+
)
|
|
1437
|
+
]
|
|
1438
|
+
|
|
1439
|
+
mock_create_issue_in_jira.side_effect = returned_jira_issues
|
|
1440
|
+
|
|
1441
|
+
new_regscale_objects = sync_regscale_to_jira(
|
|
1442
|
+
regscale_objects=regscale_objects,
|
|
1443
|
+
jira_client=MagicMock(),
|
|
1444
|
+
jira_project=self.JIRA_PROJECT,
|
|
1445
|
+
jira_issue_type="Issue", # Using Issue type for issues
|
|
1446
|
+
sync_attachments=fetch_attachments,
|
|
1447
|
+
attachments={},
|
|
1448
|
+
api=MagicMock(),
|
|
1449
|
+
)
|
|
1450
|
+
|
|
1451
|
+
assert len(new_regscale_objects) == 1 # Only one new issue should be created
|
|
1452
|
+
assert mock_create_issue_in_jira.call_count == 1 # Should only be called once for the issue without Jira ID
|
|
1453
|
+
assert new_regscale_objects[0].jiraId == "JIRA-1"
|
|
1454
|
+
|
|
1455
|
+
@patch(f"{PATH}.create_issue_in_jira")
|
|
1456
|
+
def test_sync_regscale_tasks_to_jira(self, mock_create_issue_in_jira, fetch_attachments):
|
|
1457
|
+
"""Test inserting Regscale tasks into jira if they do not exist"""
|
|
1458
|
+
|
|
1459
|
+
# Create RegScale tasks
|
|
1460
|
+
regscale_objects = [
|
|
1461
|
+
Task(
|
|
1462
|
+
id=2,
|
|
1463
|
+
title="Test Task",
|
|
1464
|
+
status="Backlog",
|
|
1465
|
+
description="This is a test task",
|
|
1466
|
+
dueDate=get_current_datetime(),
|
|
1467
|
+
parentId=self.PARENT_ID,
|
|
1468
|
+
parentModule=self.PARENT_MODULE,
|
|
1469
|
+
),
|
|
1470
|
+
Task(
|
|
1471
|
+
id=4,
|
|
1472
|
+
title="Test Task with Other ID",
|
|
1473
|
+
status="Backlog",
|
|
1474
|
+
description="This is a test task with other ID",
|
|
1475
|
+
dueDate=get_current_datetime(),
|
|
1476
|
+
parentId=self.PARENT_ID,
|
|
1477
|
+
parentModule=self.PARENT_MODULE,
|
|
1478
|
+
otherIdentifier="JIRA-4",
|
|
1479
|
+
),
|
|
1480
|
+
]
|
|
1481
|
+
|
|
1482
|
+
# Create mock Jira issues
|
|
1483
|
+
todo_status = MagicMock()
|
|
1484
|
+
todo_status.name = "to do"
|
|
1485
|
+
|
|
1486
|
+
returned_jira_issues = [
|
|
1487
|
+
MagicMock(
|
|
1488
|
+
key="JIRA-2",
|
|
1489
|
+
fields=MagicMock(
|
|
1490
|
+
summary="Test Task",
|
|
1491
|
+
description="This is a test task",
|
|
1492
|
+
status=todo_status,
|
|
1493
|
+
duedate=None,
|
|
1494
|
+
statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
|
|
1495
|
+
attachment=None,
|
|
1496
|
+
),
|
|
1497
|
+
)
|
|
1498
|
+
]
|
|
1499
|
+
|
|
1500
|
+
mock_create_issue_in_jira.side_effect = returned_jira_issues
|
|
1501
|
+
|
|
1502
|
+
new_regscale_objects = sync_regscale_to_jira(
|
|
1503
|
+
regscale_objects=regscale_objects,
|
|
1504
|
+
jira_client=MagicMock(),
|
|
1505
|
+
jira_project=self.JIRA_PROJECT,
|
|
1506
|
+
jira_issue_type="Task", # Using Task type for tasks
|
|
1507
|
+
sync_attachments=fetch_attachments,
|
|
1508
|
+
attachments={},
|
|
1509
|
+
api=MagicMock(),
|
|
1510
|
+
)
|
|
1511
|
+
|
|
1512
|
+
assert len(new_regscale_objects) == 1 # Only one new task should be created
|
|
1513
|
+
assert mock_create_issue_in_jira.call_count == 1 # Should only be called once for the task without Jira ID
|
|
1514
|
+
assert new_regscale_objects[0].otherIdentifier == "JIRA-2"
|
|
1515
|
+
|
|
1516
|
+
def test_generate_jira_comment(self):
|
|
1517
|
+
"""Test generating a Jira comment from a RegScale issue"""
|
|
1518
|
+
# Create issue with mix of included and excluded fields
|
|
1519
|
+
issue = Issue(
|
|
1520
|
+
id=1,
|
|
1521
|
+
title="Test Issue",
|
|
1522
|
+
status="Open",
|
|
1523
|
+
description="Test description",
|
|
1524
|
+
createdById="excluded-1",
|
|
1525
|
+
lastUpdatedById="excluded-2",
|
|
1526
|
+
issueOwnerId="excluded-3",
|
|
1527
|
+
assignedToId="excluded-4",
|
|
1528
|
+
uuid="excluded-5",
|
|
1529
|
+
jiraId="JIRA-123",
|
|
1530
|
+
severityLevel="High",
|
|
1531
|
+
dueDate="2023-12-31",
|
|
1532
|
+
identification=f"{self.title_prefix} Jira Issue Integration Test",
|
|
1533
|
+
)
|
|
1534
|
+
|
|
1535
|
+
comment = _generate_jira_comment(issue)
|
|
1536
|
+
|
|
1537
|
+
# Verify excluded fields are not in comment
|
|
1538
|
+
assert "createdById" not in comment
|
|
1539
|
+
assert "lastUpdatedById" not in comment
|
|
1540
|
+
assert "issueOwnerId" not in comment
|
|
1541
|
+
assert "assignedToId" not in comment
|
|
1542
|
+
assert "uuid" not in comment
|
|
1543
|
+
|
|
1544
|
+
# Verify included fields are in comment
|
|
1545
|
+
assert "**jiraId:** JIRA-123" in comment
|
|
1546
|
+
assert "**severityLevel:** High" in comment
|
|
1547
|
+
assert "**dueDate:** 2023-12-31" in comment
|
|
1548
|
+
assert "**title:** Test Issue" in comment
|
|
1549
|
+
assert "**status:** Open" in comment
|
|
1550
|
+
assert "**description:** Test description" in comment
|
|
1551
|
+
|
|
1552
|
+
def test_generate_jira_comment_task(self):
|
|
1553
|
+
"""Test generating a Jira comment from a RegScale task"""
|
|
1554
|
+
# Create task with mix of included and excluded fields
|
|
1555
|
+
task = Task(
|
|
1556
|
+
id=1,
|
|
1557
|
+
title="Test Task",
|
|
1558
|
+
status="Backlog",
|
|
1559
|
+
description="Test task description",
|
|
1560
|
+
createdById="excluded-1",
|
|
1561
|
+
lastUpdatedById="excluded-2",
|
|
1562
|
+
assignedToId="excluded-3",
|
|
1563
|
+
uuid="excluded-4",
|
|
1564
|
+
otherIdentifier="JIRA-123",
|
|
1565
|
+
percentComplete=50,
|
|
1566
|
+
dueDate="2023-12-31",
|
|
1567
|
+
identification=f"{self.title_prefix} Jira Issue Integration Test",
|
|
1568
|
+
)
|
|
1569
|
+
|
|
1570
|
+
comment = _generate_jira_comment(task)
|
|
1571
|
+
|
|
1572
|
+
# Verify excluded fields are not in comment
|
|
1573
|
+
assert "createdById" not in comment
|
|
1574
|
+
assert "lastUpdatedById" not in comment
|
|
1575
|
+
assert "assignedToId" not in comment
|
|
1576
|
+
assert "uuid" not in comment
|
|
1577
|
+
|
|
1578
|
+
# Verify included fields are in comment
|
|
1579
|
+
assert "**otherIdentifier:** JIRA-123" in comment
|
|
1580
|
+
assert "**percentComplete:** 50" in comment
|
|
1581
|
+
assert "**dueDate:** 2023-12-31" in comment
|
|
1582
|
+
assert "**title:** Test Task" in comment
|
|
1583
|
+
assert "**status:** Backlog" in comment
|
|
1584
|
+
assert "**description:** Test task description" in comment
|
|
1585
|
+
|
|
1586
|
+
def test_create_issue_in_jira(self, regscale_issues_and_attachments, jira_client, fetch_attachments):
|
|
1587
|
+
"""Test creating an issue in Jira"""
|
|
1588
|
+
issues, attachments = regscale_issues_and_attachments
|
|
1589
|
+
for issue in issues:
|
|
1590
|
+
jira_issue = create_issue_in_jira(
|
|
1591
|
+
regscale_object=issue,
|
|
1592
|
+
jira_client=jira_client,
|
|
1593
|
+
jira_project=self.JIRA_PROJECT,
|
|
1594
|
+
issue_type="Bug",
|
|
1595
|
+
add_attachments=fetch_attachments,
|
|
1596
|
+
attachments=attachments,
|
|
1597
|
+
api=self.api,
|
|
1598
|
+
)
|
|
1599
|
+
|
|
1600
|
+
assert jira_issue is not None
|
|
1601
|
+
assert jira_issue.key is not None
|
|
1602
|
+
assert jira_issue.fields.summary == issue.title
|
|
1603
|
+
assert issue.description in jira_issue.fields.description
|
|
1604
|
+
|
|
1605
|
+
jira_issue.delete() # cleanup issue in jira
|
|
1606
|
+
|
|
1607
|
+
def test_create_task_in_jira(self, regscale_tasks_and_attachments, jira_client, fetch_attachments):
|
|
1608
|
+
"""Test creating a task in Jira"""
|
|
1609
|
+
tasks, attachments = regscale_tasks_and_attachments
|
|
1610
|
+
for task in tasks:
|
|
1611
|
+
jira_task = create_issue_in_jira(
|
|
1612
|
+
regscale_object=task,
|
|
1613
|
+
jira_client=jira_client,
|
|
1614
|
+
jira_project=self.JIRA_PROJECT,
|
|
1615
|
+
issue_type="Task",
|
|
1616
|
+
add_attachments=fetch_attachments,
|
|
1617
|
+
attachments=attachments,
|
|
1618
|
+
api=self.api,
|
|
1619
|
+
)
|
|
1620
|
+
|
|
1621
|
+
assert jira_task is not None
|
|
1622
|
+
assert jira_task.key is not None
|
|
1623
|
+
assert jira_task.fields.summary == task.title
|
|
1624
|
+
assert task.description in jira_task.fields.description
|
|
1625
|
+
|
|
1626
|
+
jira_task.delete() # cleanup task in jira
|
|
1627
|
+
|
|
1628
|
+
def test_create_issue_in_jira_error(self):
|
|
1629
|
+
"""Test that we exit when Jira API call fails"""
|
|
1630
|
+
# Create a mock Jira client that will raise an error
|
|
1631
|
+
mock_jira_client = MagicMock()
|
|
1632
|
+
mock_jira_client.create_issue.side_effect = JIRAError("Test error")
|
|
1633
|
+
|
|
1634
|
+
mock_regscale = MagicMock()
|
|
1635
|
+
mock_regscale.get_module_string.return_value = "issues"
|
|
1636
|
+
mock_regscale.id = 1
|
|
1637
|
+
|
|
1638
|
+
mock_api = MagicMock()
|
|
1639
|
+
mock_api.config = {"domain": "https://test.regscale.com"}
|
|
1640
|
+
|
|
1641
|
+
with pytest.raises(SystemExit) as e:
|
|
1642
|
+
create_issue_in_jira(
|
|
1643
|
+
regscale_object=mock_regscale,
|
|
1644
|
+
jira_client=mock_jira_client,
|
|
1645
|
+
jira_project=self.JIRA_PROJECT,
|
|
1646
|
+
issue_type="Bug",
|
|
1647
|
+
add_attachments=True,
|
|
1648
|
+
attachments={},
|
|
1649
|
+
api=mock_api,
|
|
1650
|
+
)
|
|
1651
|
+
assert e.value.code == 1
|
|
1652
|
+
assert e.type == SystemExit
|
|
1653
|
+
|
|
1654
|
+
def test_upload_files_to_jira(self, jira_client):
|
|
1655
|
+
"""Test uploading files to Jira"""
|
|
1656
|
+
# create a jira issue to upload attachment to
|
|
1657
|
+
jira_issue = jira_client.create_issue(
|
|
1658
|
+
project=self.JIRA_PROJECT,
|
|
1659
|
+
summary="Test Issue",
|
|
1660
|
+
description="Test Description",
|
|
1661
|
+
issuetype={"name": "Bug"},
|
|
1662
|
+
)
|
|
1663
|
+
|
|
1664
|
+
# create regscale issue to link to
|
|
1665
|
+
reg_issue = Issue(
|
|
1666
|
+
id=1,
|
|
1667
|
+
title="Test Issue",
|
|
1668
|
+
description="Test Description",
|
|
1669
|
+
parentId=self.PARENT_ID,
|
|
1670
|
+
parentModule=self.PARENT_MODULE,
|
|
1671
|
+
identification=f"{self.title_prefix} Jira Issue Integration Test",
|
|
1672
|
+
)
|
|
1673
|
+
|
|
1674
|
+
# setup file hashes for upload
|
|
1675
|
+
file_path = os.path.join(self.get_tests_dir("tests"), "test_data", "jira_attachments", "attachment.txt")
|
|
1676
|
+
with open(file_path, "rb") as file:
|
|
1677
|
+
reg_hashes = {compute_hash(file): file_path}
|
|
1678
|
+
jira_hashes = {}
|
|
1679
|
+
uploaded_attachments = []
|
|
1680
|
+
|
|
1681
|
+
upload_files_to_jira(
|
|
1682
|
+
jira_hashes,
|
|
1683
|
+
reg_hashes,
|
|
1684
|
+
jira_issue,
|
|
1685
|
+
reg_issue,
|
|
1686
|
+
jira_client,
|
|
1687
|
+
uploaded_attachments,
|
|
1688
|
+
)
|
|
1689
|
+
assert uploaded_attachments == [file_path]
|
|
1690
|
+
check_issue = jira_client.issue(jira_issue.key)
|
|
1691
|
+
assert len(check_issue.fields.attachment) == 1
|
|
1692
|
+
assert check_issue.fields.attachment[0].size > 0
|
|
1693
|
+
assert check_issue.fields.attachment[0].created is not None
|
|
1694
|
+
|
|
1695
|
+
# Clean up
|
|
1696
|
+
jira_issue.delete()
|
|
1697
|
+
|
|
1698
|
+
def test_upload_files_to_jira_duplicates(self):
|
|
1699
|
+
"""Test that we don't upload duplicates"""
|
|
1700
|
+
jira_hashes = {"dummyhash": "dummy/path/file.txt"}
|
|
1701
|
+
reg_hashes = {"dummyhash": "dummy/path/file.txt"}
|
|
1702
|
+
jira_issue = MagicMock()
|
|
1703
|
+
reg_issue = MagicMock()
|
|
1704
|
+
jira_client = MagicMock()
|
|
1705
|
+
uploaded_attachments = []
|
|
1706
|
+
|
|
1707
|
+
upload_files_to_jira(jira_hashes, reg_hashes, jira_issue, reg_issue, jira_client, uploaded_attachments)
|
|
1708
|
+
|
|
1709
|
+
assert uploaded_attachments == []
|
|
1710
|
+
jira_client.add_attachment.assert_not_called()
|
|
1711
|
+
|
|
1712
|
+
@pytest.mark.parametrize("error_type", [JIRAError, TypeError])
|
|
1713
|
+
@patch(f"{PATH}.open")
|
|
1714
|
+
def test_upload_files_to_jira_error(self, mock_open, error_type):
|
|
1715
|
+
"""Test that uploads aren't made when errors are encountered"""
|
|
1716
|
+
# Setup mock file
|
|
1717
|
+
mock_file = MagicMock()
|
|
1718
|
+
mock_file.read.return_value = b"test content"
|
|
1719
|
+
mock_open.return_value.__enter__.return_value = mock_file
|
|
1720
|
+
|
|
1721
|
+
# Setup test data
|
|
1722
|
+
file_path = "dummy/path/file.txt"
|
|
1723
|
+
reg_hashes = {"dummyhash": file_path}
|
|
1724
|
+
jira_hashes = {}
|
|
1725
|
+
jira_issue = MagicMock()
|
|
1726
|
+
reg_issue = MagicMock()
|
|
1727
|
+
jira_client = MagicMock()
|
|
1728
|
+
jira_client.add_attachment.side_effect = error_type("Test error")
|
|
1729
|
+
uploaded_attachments = []
|
|
1730
|
+
|
|
1731
|
+
upload_files_to_jira(jira_hashes, reg_hashes, jira_issue, reg_issue, jira_client, uploaded_attachments)
|
|
1732
|
+
|
|
1733
|
+
assert uploaded_attachments == []
|
|
1734
|
+
jira_client.add_attachment.assert_called_once()
|
|
1735
|
+
mock_file.read.assert_called_once()
|
|
1736
|
+
|
|
1737
|
+
def test_upload_files_to_regscale(self):
|
|
1738
|
+
"""Test uploading files to RegScale"""
|
|
1739
|
+
tmp = Issue(
|
|
1740
|
+
id=1,
|
|
1741
|
+
title="Test Issue",
|
|
1742
|
+
description="Test Description",
|
|
1743
|
+
parentId=self.PARENT_ID,
|
|
1744
|
+
parentModule=self.PARENT_MODULE,
|
|
1745
|
+
dueDate=get_current_datetime(),
|
|
1746
|
+
status="Open",
|
|
1747
|
+
identification=f"{self.title_prefix} Jira Issue Integration Test",
|
|
1748
|
+
)
|
|
1749
|
+
reg_issue = tmp.create()
|
|
1750
|
+
|
|
1751
|
+
file_path = os.path.join(self.get_tests_dir("tests"), "test_data", "jira_attachments", "attachment.txt")
|
|
1752
|
+
with open(file_path, "rb") as file:
|
|
1753
|
+
jira_hashes = {compute_hash(file): file_path}
|
|
1754
|
+
reg_hashes = {}
|
|
1755
|
+
uploaded_attachments = []
|
|
1756
|
+
|
|
1757
|
+
upload_files_to_regscale(jira_hashes, reg_hashes, reg_issue, self.api, uploaded_attachments)
|
|
1758
|
+
|
|
1759
|
+
assert uploaded_attachments == [file_path]
|
|
1760
|
+
check_issues, attachments = Issue.get_objects_and_attachments_by_parent(
|
|
1761
|
+
parent_id=self.PARENT_ID, parent_module=self.PARENT_MODULE
|
|
1762
|
+
)
|
|
1763
|
+
assert reg_issue in check_issues
|
|
1764
|
+
assert len(attachments[reg_issue.id]) == 1
|
|
1765
|
+
|
|
1766
|
+
@patch(f"{PATH}.File.upload_file_to_regscale", return_value=None)
|
|
1767
|
+
def test_upload_files_to_regscale_duplicates(self, mock_upload_file_to_regscale):
|
|
1768
|
+
"""Test that we don't upload duplicate attachments to regscale"""
|
|
1769
|
+
jira_hashes = {"dummyhash": "dummy/path/file.txt"}
|
|
1770
|
+
reg_hashes = {"dummyhash": "dummy/path/file.txt"}
|
|
1771
|
+
reg_issue = MagicMock()
|
|
1772
|
+
uploaded_attachments = []
|
|
1773
|
+
|
|
1774
|
+
upload_files_to_regscale(jira_hashes, reg_hashes, reg_issue, MagicMock(), uploaded_attachments)
|
|
1775
|
+
|
|
1776
|
+
assert uploaded_attachments == []
|
|
1777
|
+
mock_upload_file_to_regscale.assert_not_called()
|
|
1778
|
+
|
|
1779
|
+
@patch(f"{PATH}.File.upload_file_to_regscale", return_value=None)
|
|
1780
|
+
@patch(f"{PATH}.open")
|
|
1781
|
+
def test_upload_files_to_regscale_error(self, mock_open, mock_upload_file_to_regscale):
|
|
1782
|
+
"""Test when the uploads are unsuccessful"""
|
|
1783
|
+
mock_file = MagicMock()
|
|
1784
|
+
mock_file.read.return_value = b"test content"
|
|
1785
|
+
mock_open.return_value.__enter__.return_value = mock_file
|
|
1786
|
+
|
|
1787
|
+
# Setup test data
|
|
1788
|
+
file_path = "dummy/path/file.txt"
|
|
1789
|
+
jira_hashes = {"dummyhash": file_path}
|
|
1790
|
+
reg_hashes = {}
|
|
1791
|
+
reg_issue = MagicMock()
|
|
1792
|
+
uploaded_attachments = []
|
|
1793
|
+
api = MagicMock()
|
|
1794
|
+
|
|
1795
|
+
upload_files_to_regscale(jira_hashes, reg_hashes, reg_issue, api, uploaded_attachments)
|
|
1796
|
+
|
|
1797
|
+
assert uploaded_attachments == []
|
|
1798
|
+
mock_upload_file_to_regscale.assert_called_once()
|
|
1799
|
+
|
|
1800
|
+
def test_validate_issue_type(self):
|
|
1801
|
+
"""Test validating the issue type"""
|
|
1802
|
+
jira_client = MagicMock()
|
|
1803
|
+
issue_type_bug = MagicMock()
|
|
1804
|
+
issue_type_bug.name = "Bug"
|
|
1805
|
+
issue_type_task = MagicMock()
|
|
1806
|
+
issue_type_task.name = "Task"
|
|
1807
|
+
jira_client.issue_types.return_value = [issue_type_bug, issue_type_task]
|
|
1808
|
+
assert validate_issue_type(jira_client, "Bug") is True
|
|
1809
|
+
assert validate_issue_type(jira_client, "Task") is True
|
|
1810
|
+
with pytest.raises(SystemExit) as e:
|
|
1811
|
+
validate_issue_type(jira_client, "Invalid")
|
|
1812
|
+
assert e.value.code == 1
|
|
1813
|
+
assert e.type == SystemExit
|
|
1814
|
+
|
|
1815
|
+
def test_download_issue_attachments(self, regscale_issues_and_attachments, get_jira_issue_with_attachment):
|
|
1816
|
+
"""Test downloading attachments from Jira and RegScale issues"""
|
|
1817
|
+
issues, attachments = regscale_issues_and_attachments
|
|
1818
|
+
jira_issue = get_jira_issue_with_attachment
|
|
1819
|
+
if attachments:
|
|
1820
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1821
|
+
download_regscale_attachments_to_directory(
|
|
1822
|
+
directory=tmpdir,
|
|
1823
|
+
jira_issue=jira_issue,
|
|
1824
|
+
regscale_object=issues[0],
|
|
1825
|
+
api=self.api,
|
|
1826
|
+
)
|
|
1827
|
+
assert os.path.exists(tmpdir) is True
|
|
1828
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1829
|
+
download_regscale_attachments_to_directory(
|
|
1830
|
+
directory=tmpdir,
|
|
1831
|
+
jira_issue=jira_issue,
|
|
1832
|
+
regscale_object=issues[0],
|
|
1833
|
+
api=self.api,
|
|
1834
|
+
)
|
|
1835
|
+
assert os.path.exists(tmpdir) is True
|
|
1836
|
+
|
|
1837
|
+
def test_download_task_attachments(self, regscale_tasks_and_attachments, get_jira_task_with_attachment):
|
|
1838
|
+
"""Test downloading attachments from Jira and RegScale tasks"""
|
|
1839
|
+
tasks, attachments = regscale_tasks_and_attachments
|
|
1840
|
+
jira_task = get_jira_task_with_attachment
|
|
1841
|
+
if attachments:
|
|
1842
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1843
|
+
download_regscale_attachments_to_directory(
|
|
1844
|
+
directory=tmpdir,
|
|
1845
|
+
jira_issue=jira_task,
|
|
1846
|
+
regscale_object=tasks[0],
|
|
1847
|
+
api=self.api,
|
|
1848
|
+
)
|
|
1849
|
+
assert os.path.exists(tmpdir) is True
|
|
1850
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1851
|
+
download_regscale_attachments_to_directory(
|
|
1852
|
+
directory=tmpdir,
|
|
1853
|
+
jira_issue=jira_task,
|
|
1854
|
+
regscale_object=tasks[0],
|
|
1855
|
+
api=self.api,
|
|
1856
|
+
)
|
|
1857
|
+
assert os.path.exists(tmpdir) is True
|
|
1858
|
+
|
|
1859
|
+
@patch(f"{PATH}.sync_regscale_objects_to_jira")
|
|
1860
|
+
@patch(f"{PATH}.sync_regscale_to_jira", return_value=[])
|
|
1861
|
+
@patch(f"{PATH}.create_jira_client")
|
|
1862
|
+
@patch(f"{PATH}.fetch_jira_objects")
|
|
1863
|
+
@patch(f"{PATH}.get_regscale_data_and_attachments")
|
|
1864
|
+
@patch(f"{PATH}.Api", return_value=MagicMock(spec=Api))
|
|
1865
|
+
@patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
|
|
1866
|
+
def test_sync_regscale_and_jira_with_poams_true(
|
|
1867
|
+
self,
|
|
1868
|
+
mock_check_license,
|
|
1869
|
+
mock_api,
|
|
1870
|
+
mock_get_regscale_data_and_attachments,
|
|
1871
|
+
mock_fetch_jira_objects,
|
|
1872
|
+
mock_create_jira_client,
|
|
1873
|
+
mock_sync_regscale_to_jira,
|
|
1874
|
+
mock_sync_regscale_objects_to_jira,
|
|
1875
|
+
fetch_attachments,
|
|
1876
|
+
):
|
|
1877
|
+
"""Test sync_regscale_and_jira with use_poams=True"""
|
|
1878
|
+
# Setup mocks
|
|
1879
|
+
mock_jira_client = MagicMock()
|
|
1880
|
+
mock_create_jira_client.return_value = mock_jira_client
|
|
1881
|
+
mock_fetch_jira_objects.return_value = [MagicMock()]
|
|
1882
|
+
mock_get_regscale_data_and_attachments.return_value = ([MagicMock()], MagicMock())
|
|
1883
|
+
|
|
1884
|
+
# Call function with use_poams=True
|
|
1885
|
+
sync_regscale_and_jira(
|
|
1886
|
+
parent_id=self.PARENT_ID,
|
|
1887
|
+
parent_module=self.PARENT_MODULE,
|
|
1888
|
+
jira_project=self.JIRA_PROJECT,
|
|
1889
|
+
jira_issue_type="Bug",
|
|
1890
|
+
sync_attachments=fetch_attachments,
|
|
1891
|
+
use_poams=True,
|
|
1892
|
+
)
|
|
1893
|
+
|
|
1894
|
+
# Verify sync_regscale_objects_to_jira was called with use_poams=True
|
|
1895
|
+
mock_sync_regscale_objects_to_jira.assert_called_once()
|
|
1896
|
+
call_args = mock_sync_regscale_objects_to_jira.call_args
|
|
1897
|
+
assert call_args[0][7] is True # use_poams is the 8th positional argument
|
|
1898
|
+
|
|
1899
|
+
@patch(f"{PATH}.sync_regscale_objects_to_jira")
|
|
1900
|
+
@patch(f"{PATH}.sync_regscale_to_jira", return_value=[])
|
|
1901
|
+
@patch(f"{PATH}.create_jira_client")
|
|
1902
|
+
@patch(f"{PATH}.fetch_jira_objects")
|
|
1903
|
+
@patch(f"{PATH}.get_regscale_data_and_attachments")
|
|
1904
|
+
@patch(f"{PATH}.Api", return_value=MagicMock(spec=Api))
|
|
1905
|
+
@patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
|
|
1906
|
+
def test_sync_regscale_and_jira_with_poams_false(
|
|
1907
|
+
self,
|
|
1908
|
+
mock_check_license,
|
|
1909
|
+
mock_api,
|
|
1910
|
+
mock_get_regscale_data_and_attachments,
|
|
1911
|
+
mock_fetch_jira_objects,
|
|
1912
|
+
mock_create_jira_client,
|
|
1913
|
+
mock_sync_regscale_to_jira,
|
|
1914
|
+
mock_sync_regscale_objects_to_jira,
|
|
1915
|
+
fetch_attachments,
|
|
1916
|
+
):
|
|
1917
|
+
"""Test sync_regscale_and_jira with use_poams=False (default)"""
|
|
1918
|
+
# Setup mocks
|
|
1919
|
+
mock_jira_client = MagicMock()
|
|
1920
|
+
mock_create_jira_client.return_value = mock_jira_client
|
|
1921
|
+
mock_fetch_jira_objects.return_value = [MagicMock()]
|
|
1922
|
+
mock_get_regscale_data_and_attachments.return_value = ([MagicMock()], MagicMock())
|
|
1923
|
+
|
|
1924
|
+
# Call function with use_poams=False (default)
|
|
1925
|
+
sync_regscale_and_jira(
|
|
1926
|
+
parent_id=self.PARENT_ID,
|
|
1927
|
+
parent_module=self.PARENT_MODULE,
|
|
1928
|
+
jira_project=self.JIRA_PROJECT,
|
|
1929
|
+
jira_issue_type="Bug",
|
|
1930
|
+
sync_attachments=fetch_attachments,
|
|
1931
|
+
use_poams=False,
|
|
1932
|
+
)
|
|
1933
|
+
|
|
1934
|
+
# Verify sync_regscale_objects_to_jira was called with use_poams=False
|
|
1935
|
+
mock_sync_regscale_objects_to_jira.assert_called_once()
|
|
1936
|
+
call_args = mock_sync_regscale_objects_to_jira.call_args
|
|
1937
|
+
assert call_args[0][7] is False # use_poams is the 8th positional argument
|
|
1938
|
+
|
|
1939
|
+
@patch(f"{PATH}.create_jira_client")
|
|
1940
|
+
@patch(f"{PATH}.create_and_update_regscale_issues")
|
|
1941
|
+
@patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
|
|
1942
|
+
def test_sync_regscale_objects_to_jira_with_poams(
|
|
1943
|
+
self,
|
|
1944
|
+
mock_check_license,
|
|
1945
|
+
mock_create_and_update_regscale_issues,
|
|
1946
|
+
mock_create_jira_client,
|
|
1947
|
+
fetch_attachments,
|
|
1948
|
+
):
|
|
1949
|
+
"""Test sync_regscale_objects_to_jira passes use_poams to create_and_update_regscale_issues"""
|
|
1950
|
+
mock_check_license.return_value.config = self.config
|
|
1951
|
+
mock_jira_client = MagicMock()
|
|
1952
|
+
mock_create_jira_client.return_value = mock_jira_client
|
|
1953
|
+
|
|
1954
|
+
# Create mock application with ThreadManager
|
|
1955
|
+
mock_app = MagicMock(spec=Application)
|
|
1956
|
+
mock_app.config = self.config
|
|
1957
|
+
mock_thread_manager = MagicMock()
|
|
1958
|
+
mock_app.thread_manager = mock_thread_manager
|
|
1959
|
+
|
|
1960
|
+
# Mock Jira issues
|
|
1961
|
+
mock_jira_issues = [MagicMock(), MagicMock()]
|
|
1962
|
+
mock_regscale_objects = [MagicMock()]
|
|
1963
|
+
|
|
1964
|
+
# Test with use_poams=True
|
|
1965
|
+
sync_regscale_objects_to_jira(
|
|
1966
|
+
mock_jira_issues,
|
|
1967
|
+
mock_regscale_objects,
|
|
1968
|
+
fetch_attachments,
|
|
1969
|
+
mock_app,
|
|
1970
|
+
self.PARENT_ID,
|
|
1971
|
+
self.PARENT_MODULE,
|
|
1972
|
+
False, # sync_tasks_only
|
|
1973
|
+
True, # use_poams
|
|
1974
|
+
)
|
|
1975
|
+
|
|
1976
|
+
# Verify ThreadManager was called with correct parameters
|
|
1977
|
+
mock_thread_manager.submit_tasks_from_list.assert_called_once()
|
|
1978
|
+
call_args = mock_thread_manager.submit_tasks_from_list.call_args[0]
|
|
1979
|
+
# use_poams is the 4th argument (index 3) after function, jira_issues, and regscale_objects
|
|
1980
|
+
assert call_args[3] is True
|
|
1981
|
+
|
|
1982
|
+
@patch(f"{PATH}.map_jira_to_regscale_issue")
|
|
1983
|
+
def test_map_jira_to_regscale_issue_with_poam_true(self, mock_map_jira_to_regscale_issue):
|
|
1984
|
+
"""Test map_jira_to_regscale_issue sets isPoam=True when is_poam=True"""
|
|
1985
|
+
# Create mock Jira issue
|
|
1986
|
+
mock_issue = MagicMock()
|
|
1987
|
+
mock_issue.fields.summary = "Test Issue"
|
|
1988
|
+
mock_issue.fields.description = "Test Description"
|
|
1989
|
+
mock_issue.fields.status.name = "Open"
|
|
1990
|
+
mock_issue.fields.priority.name = "High"
|
|
1991
|
+
mock_issue.fields.duedate = None
|
|
1992
|
+
mock_issue.key = "TEST-123"
|
|
1993
|
+
|
|
1994
|
+
# Create mock config
|
|
1995
|
+
mock_config = {
|
|
1996
|
+
"userId": "1",
|
|
1997
|
+
"issues": {"jira": {"status": "Open", "high": 7, "medium": 14, "low": 30}},
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
# Call the actual function with is_poam=True
|
|
2001
|
+
result = map_jira_to_regscale_issue(
|
|
2002
|
+
jira_issue=mock_issue,
|
|
2003
|
+
config=mock_config,
|
|
2004
|
+
parent_id=self.PARENT_ID,
|
|
2005
|
+
parent_module=self.PARENT_MODULE,
|
|
2006
|
+
is_poam=True,
|
|
2007
|
+
)
|
|
2008
|
+
|
|
2009
|
+
# Verify the Issue object was created with isPoam=True
|
|
2010
|
+
assert isinstance(result, Issue)
|
|
2011
|
+
assert result.isPoam is True
|
|
2012
|
+
assert result.title == "Test Issue"
|
|
2013
|
+
assert result.jiraId == "TEST-123"
|
|
2014
|
+
|
|
2015
|
+
@patch(f"{PATH}.map_jira_to_regscale_issue")
|
|
2016
|
+
def test_map_jira_to_regscale_issue_with_poam_false(self, mock_map_jira_to_regscale_issue):
|
|
2017
|
+
"""Test map_jira_to_regscale_issue sets isPoam=False when is_poam=False"""
|
|
2018
|
+
# Create mock Jira issue
|
|
2019
|
+
mock_issue = MagicMock()
|
|
2020
|
+
mock_issue.fields.summary = "Test Issue"
|
|
2021
|
+
mock_issue.fields.description = "Test Description"
|
|
2022
|
+
mock_issue.fields.status.name = "Open"
|
|
2023
|
+
mock_issue.fields.priority.name = "High"
|
|
2024
|
+
mock_issue.fields.duedate = None
|
|
2025
|
+
mock_issue.key = "TEST-123"
|
|
2026
|
+
|
|
2027
|
+
# Create mock config
|
|
2028
|
+
mock_config = {
|
|
2029
|
+
"userId": "1",
|
|
2030
|
+
"issues": {"jira": {"status": "Open", "high": 7, "medium": 14, "low": 30}},
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
# Call the actual function with is_poam=False
|
|
2034
|
+
result = map_jira_to_regscale_issue(
|
|
2035
|
+
jira_issue=mock_issue,
|
|
2036
|
+
config=mock_config,
|
|
2037
|
+
parent_id=self.PARENT_ID,
|
|
2038
|
+
parent_module=self.PARENT_MODULE,
|
|
2039
|
+
is_poam=False,
|
|
2040
|
+
)
|
|
2041
|
+
|
|
2042
|
+
# Verify the Issue object was created with isPoam=False
|
|
2043
|
+
assert isinstance(result, Issue)
|
|
2044
|
+
assert result.isPoam is False
|
|
2045
|
+
assert result.title == "Test Issue"
|
|
2046
|
+
assert result.jiraId == "TEST-123"
|
|
2047
|
+
|
|
2048
|
+
@patch(f"{PATH}.compare_files_for_dupes_and_upload")
|
|
2049
|
+
@patch(f"{PATH}.map_jira_to_regscale_issue")
|
|
2050
|
+
@patch(f"{PATH}.Issue.update_issue")
|
|
2051
|
+
@patch(f"{PATH}.job_progress", return_value=MagicMock(spec=Progress))
|
|
2052
|
+
def test_create_and_update_regscale_issues_sets_ispoam_on_new_issue(
|
|
2053
|
+
self,
|
|
2054
|
+
mock_job_progress_object,
|
|
2055
|
+
mock_update_issue,
|
|
2056
|
+
mock_map_jira_to_regscale_issue,
|
|
2057
|
+
mock_compare_files_for_dupes_and_upload,
|
|
2058
|
+
):
|
|
2059
|
+
"""Test that create_and_update_regscale_issues sets isPoam on newly created issues"""
|
|
2060
|
+
# Create mock Jira issue
|
|
2061
|
+
open_status = MagicMock()
|
|
2062
|
+
open_status.name = "open"
|
|
2063
|
+
high_priority = MagicMock()
|
|
2064
|
+
high_priority.name = "high"
|
|
2065
|
+
|
|
2066
|
+
jira_issue = MagicMock(
|
|
2067
|
+
key="JIRA-NEW",
|
|
2068
|
+
fields=MagicMock(
|
|
2069
|
+
summary="New Issue",
|
|
2070
|
+
description="New issue description",
|
|
2071
|
+
status=open_status,
|
|
2072
|
+
duedate=None,
|
|
2073
|
+
priority=high_priority,
|
|
2074
|
+
statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
|
|
2075
|
+
attachment=None,
|
|
2076
|
+
),
|
|
2077
|
+
)
|
|
2078
|
+
|
|
2079
|
+
# Create empty RegScale issues list (no existing issues)
|
|
2080
|
+
regscale_issues = []
|
|
2081
|
+
|
|
2082
|
+
# Create mock config
|
|
2083
|
+
config = {
|
|
2084
|
+
"issues": {"jira": {"highest": 7, "high": 30, "medium": 90, "low": 180, "lowest": 365, "status": "Open"}},
|
|
2085
|
+
"maxThreads": 4,
|
|
2086
|
+
"userId": "123e4567-e89b-12d3-a456-426614174000",
|
|
2087
|
+
}
|
|
2088
|
+
app = MagicMock()
|
|
2089
|
+
app.config = config
|
|
2090
|
+
|
|
2091
|
+
# Mock the creation of a new issue
|
|
2092
|
+
created_issue_mock = MagicMock()
|
|
2093
|
+
created_issue_mock.id = 999
|
|
2094
|
+
created_issue_mock.create.return_value = created_issue_mock
|
|
2095
|
+
mock_map_jira_to_regscale_issue.return_value = created_issue_mock
|
|
2096
|
+
|
|
2097
|
+
with mock_job_progress_object as job_progress:
|
|
2098
|
+
test_task = job_progress.add_task(
|
|
2099
|
+
description="Processing issues",
|
|
2100
|
+
total=1,
|
|
2101
|
+
visible=False,
|
|
2102
|
+
)
|
|
2103
|
+
|
|
2104
|
+
# Call with use_poams=True
|
|
2105
|
+
create_and_update_regscale_issues(
|
|
2106
|
+
jira_issue,
|
|
2107
|
+
regscale_issues,
|
|
2108
|
+
True, # use_poams
|
|
2109
|
+
False, # add_attachments
|
|
2110
|
+
MagicMock(),
|
|
2111
|
+
app,
|
|
2112
|
+
self.PARENT_ID,
|
|
2113
|
+
self.PARENT_MODULE,
|
|
2114
|
+
test_task,
|
|
2115
|
+
job_progress,
|
|
2116
|
+
)
|
|
2117
|
+
|
|
2118
|
+
# Verify map_jira_to_regscale_issue was called with is_poam=True
|
|
2119
|
+
mock_map_jira_to_regscale_issue.assert_called_once()
|
|
2120
|
+
call_kwargs = mock_map_jira_to_regscale_issue.call_args[1]
|
|
2121
|
+
assert call_kwargs["is_poam"] is True
|
|
2122
|
+
|
|
2123
|
+
@patch(f"{PATH}.compare_files_for_dupes_and_upload")
|
|
2124
|
+
@patch(f"{PATH}.map_jira_to_regscale_issue")
|
|
2125
|
+
@patch(f"{PATH}.Issue.save")
|
|
2126
|
+
@patch(f"{PATH}.job_progress", return_value=MagicMock(spec=Progress))
|
|
2127
|
+
def test_create_and_update_regscale_issues_sets_ispoam_on_existing_issue(
|
|
2128
|
+
self,
|
|
2129
|
+
mock_job_progress_object,
|
|
2130
|
+
mock_save,
|
|
2131
|
+
mock_map_jira_to_regscale_issue,
|
|
2132
|
+
mock_compare_files_for_dupes_and_upload,
|
|
2133
|
+
):
|
|
2134
|
+
"""Test that create_and_update_regscale_issues sets isPoam on existing issues"""
|
|
2135
|
+
# Create mock Jira issue
|
|
2136
|
+
open_status = MagicMock()
|
|
2137
|
+
open_status.name = "open"
|
|
2138
|
+
high_priority = MagicMock()
|
|
2139
|
+
high_priority.name = "high"
|
|
2140
|
+
|
|
2141
|
+
jira_issue = MagicMock(
|
|
2142
|
+
key="JIRA-1",
|
|
2143
|
+
fields=MagicMock(
|
|
2144
|
+
summary="Existing Issue",
|
|
2145
|
+
description="Existing issue description",
|
|
2146
|
+
status=open_status,
|
|
2147
|
+
duedate=None,
|
|
2148
|
+
priority=high_priority,
|
|
2149
|
+
statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
|
|
2150
|
+
attachment=None,
|
|
2151
|
+
),
|
|
2152
|
+
)
|
|
2153
|
+
|
|
2154
|
+
# Create existing RegScale issue (using MagicMock to avoid actual creation)
|
|
2155
|
+
existing_issue = MagicMock(spec=Issue)
|
|
2156
|
+
existing_issue.jiraId = "JIRA-1"
|
|
2157
|
+
existing_issue.isPoam = False # Initially not a POAM
|
|
2158
|
+
existing_issue.id = 1
|
|
2159
|
+
existing_issue.title = "Existing Issue"
|
|
2160
|
+
regscale_issues = [existing_issue]
|
|
2161
|
+
|
|
2162
|
+
# Create mock config
|
|
2163
|
+
config = {
|
|
2164
|
+
"issues": {"jira": {"highest": 7, "high": 30, "medium": 90, "low": 180, "lowest": 365, "status": "Open"}},
|
|
2165
|
+
"maxThreads": 4,
|
|
2166
|
+
"userId": "123e4567-e89b-12d3-a456-426614174000",
|
|
2167
|
+
"jiraCustomFields": {},
|
|
2168
|
+
}
|
|
2169
|
+
app = MagicMock()
|
|
2170
|
+
app.config = config
|
|
2171
|
+
|
|
2172
|
+
# Setup mock return values
|
|
2173
|
+
mock_save.return_value = MagicMock()
|
|
2174
|
+
|
|
2175
|
+
with mock_job_progress_object as job_progress:
|
|
2176
|
+
test_task = job_progress.add_task(
|
|
2177
|
+
description="Processing issues",
|
|
2178
|
+
total=1,
|
|
2179
|
+
visible=False,
|
|
2180
|
+
)
|
|
2181
|
+
|
|
2182
|
+
# Call with use_poams=True
|
|
2183
|
+
create_and_update_regscale_issues(
|
|
2184
|
+
jira_issue,
|
|
2185
|
+
regscale_issues,
|
|
2186
|
+
True, # use_poams
|
|
2187
|
+
False, # add_attachments
|
|
2188
|
+
MagicMock(),
|
|
2189
|
+
app,
|
|
2190
|
+
self.PARENT_ID,
|
|
2191
|
+
self.PARENT_MODULE,
|
|
2192
|
+
test_task,
|
|
2193
|
+
job_progress,
|
|
2194
|
+
)
|
|
2195
|
+
|
|
2196
|
+
# Verify the existing issue had isPoam set to True
|
|
2197
|
+
assert existing_issue.isPoam is True
|
|
2198
|
+
|
|
2199
|
+
@staticmethod
|
|
2200
|
+
def teardown_class(cls):
|
|
2201
|
+
"""Remove test data"""
|
|
2202
|
+
with contextlib.suppress(FileNotFoundError):
|
|
2203
|
+
shutil.rmtree("./artifacts")
|
|
2204
|
+
assert not os.path.exists("./artifacts")
|