cumulusci-plus 5.0.19__py3-none-any.whl → 5.0.35__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.
- cumulusci/__about__.py +1 -1
- cumulusci/cli/logger.py +2 -2
- cumulusci/cli/service.py +20 -0
- cumulusci/cli/task.py +17 -0
- cumulusci/cli/tests/test_error.py +3 -1
- cumulusci/cli/tests/test_flow.py +279 -2
- cumulusci/cli/tests/test_service.py +15 -12
- cumulusci/cli/tests/test_task.py +88 -2
- cumulusci/cli/tests/utils.py +1 -4
- cumulusci/core/config/base_task_flow_config.py +26 -1
- cumulusci/core/config/project_config.py +2 -20
- cumulusci/core/config/tests/test_config_expensive.py +9 -3
- cumulusci/core/config/universal_config.py +3 -4
- cumulusci/core/dependencies/base.py +1 -1
- cumulusci/core/dependencies/dependencies.py +1 -1
- cumulusci/core/dependencies/github.py +1 -2
- cumulusci/core/dependencies/resolvers.py +1 -1
- cumulusci/core/dependencies/tests/test_dependencies.py +1 -1
- cumulusci/core/dependencies/tests/test_resolvers.py +1 -1
- cumulusci/core/flowrunner.py +90 -6
- cumulusci/core/github.py +1 -1
- cumulusci/core/sfdx.py +3 -1
- cumulusci/core/source_transforms/tests/test_transforms.py +1 -1
- cumulusci/core/source_transforms/transforms.py +1 -1
- cumulusci/core/tasks.py +13 -2
- cumulusci/core/tests/test_flowrunner.py +100 -0
- cumulusci/core/tests/test_tasks.py +65 -0
- cumulusci/core/utils.py +3 -1
- cumulusci/core/versions.py +1 -1
- cumulusci/cumulusci.yml +55 -0
- cumulusci/oauth/client.py +1 -1
- cumulusci/plugins/plugin_base.py +5 -3
- cumulusci/robotframework/pageobjects/ObjectManagerPageObject.py +1 -1
- cumulusci/salesforce_api/rest_deploy.py +1 -1
- cumulusci/schema/cumulusci.jsonschema.json +64 -0
- cumulusci/tasks/apex/anon.py +1 -1
- cumulusci/tasks/apex/testrunner.py +416 -142
- cumulusci/tasks/apex/tests/test_apex_tasks.py +917 -1
- cumulusci/tasks/bulkdata/extract.py +0 -1
- cumulusci/tasks/bulkdata/extract_dataset_utils/extract_yml.py +1 -1
- cumulusci/tasks/bulkdata/extract_dataset_utils/synthesize_extract_declarations.py +1 -1
- cumulusci/tasks/bulkdata/extract_dataset_utils/tests/test_extract_yml.py +1 -1
- cumulusci/tasks/bulkdata/generate_and_load_data.py +136 -12
- cumulusci/tasks/bulkdata/mapping_parser.py +139 -44
- cumulusci/tasks/bulkdata/select_utils.py +1 -1
- cumulusci/tasks/bulkdata/snowfakery.py +100 -25
- cumulusci/tasks/bulkdata/tests/test_generate_and_load.py +159 -0
- cumulusci/tasks/bulkdata/tests/test_load.py +0 -2
- cumulusci/tasks/bulkdata/tests/test_mapping_parser.py +763 -1
- cumulusci/tasks/bulkdata/tests/test_select_utils.py +26 -0
- cumulusci/tasks/bulkdata/tests/test_snowfakery.py +133 -0
- cumulusci/tasks/create_package_version.py +190 -16
- cumulusci/tasks/datadictionary.py +1 -1
- cumulusci/tasks/metadata_etl/base.py +7 -3
- cumulusci/tasks/metadata_etl/layouts.py +1 -1
- cumulusci/tasks/metadata_etl/permissions.py +1 -1
- cumulusci/tasks/metadata_etl/remote_site_settings.py +2 -2
- cumulusci/tasks/push/README.md +15 -17
- cumulusci/tasks/release_notes/README.md +13 -13
- cumulusci/tasks/release_notes/generator.py +13 -8
- cumulusci/tasks/robotframework/tests/test_robotframework.py +6 -1
- cumulusci/tasks/salesforce/Deploy.py +53 -2
- cumulusci/tasks/salesforce/SfPackageCommands.py +363 -0
- cumulusci/tasks/salesforce/__init__.py +1 -0
- cumulusci/tasks/salesforce/assign_ps_psg.py +448 -0
- cumulusci/tasks/salesforce/composite.py +1 -1
- cumulusci/tasks/salesforce/custom_settings_wait.py +1 -1
- cumulusci/tasks/salesforce/enable_prediction.py +5 -1
- cumulusci/tasks/salesforce/getPackageVersion.py +89 -0
- cumulusci/tasks/salesforce/profiles.py +13 -9
- cumulusci/tasks/salesforce/sourcetracking.py +1 -1
- cumulusci/tasks/salesforce/tests/test_Deploy.py +316 -1
- cumulusci/tasks/salesforce/tests/test_SfPackageCommands.py +554 -0
- cumulusci/tasks/salesforce/tests/test_assign_ps_psg.py +1055 -0
- cumulusci/tasks/salesforce/tests/test_getPackageVersion.py +651 -0
- cumulusci/tasks/salesforce/tests/test_profiles.py +43 -3
- cumulusci/tasks/salesforce/tests/test_update_dependencies.py +1 -1
- cumulusci/tasks/salesforce/tests/test_update_external_credential.py +912 -0
- cumulusci/tasks/salesforce/tests/test_update_named_credential.py +1042 -0
- cumulusci/tasks/salesforce/update_dependencies.py +2 -2
- cumulusci/tasks/salesforce/update_external_credential.py +562 -0
- cumulusci/tasks/salesforce/update_named_credential.py +441 -0
- cumulusci/tasks/salesforce/update_profile.py +17 -13
- cumulusci/tasks/salesforce/users/permsets.py +62 -5
- cumulusci/tasks/salesforce/users/tests/test_permsets.py +237 -11
- cumulusci/tasks/sfdmu/__init__.py +0 -0
- cumulusci/tasks/sfdmu/sfdmu.py +363 -0
- cumulusci/tasks/sfdmu/tests/__init__.py +1 -0
- cumulusci/tasks/sfdmu/tests/test_runner.py +212 -0
- cumulusci/tasks/sfdmu/tests/test_sfdmu.py +1012 -0
- cumulusci/tasks/tests/test_create_package_version.py +716 -1
- cumulusci/tasks/tests/test_util.py +42 -0
- cumulusci/tasks/util.py +37 -1
- cumulusci/tasks/utility/copyContents.py +402 -0
- cumulusci/tasks/utility/credentialManager.py +256 -0
- cumulusci/tasks/utility/directoryRecreator.py +30 -0
- cumulusci/tasks/utility/env_management.py +1 -1
- cumulusci/tasks/utility/secretsToEnv.py +135 -0
- cumulusci/tasks/utility/tests/test_copyContents.py +1719 -0
- cumulusci/tasks/utility/tests/test_credentialManager.py +564 -0
- cumulusci/tasks/utility/tests/test_directoryRecreator.py +439 -0
- cumulusci/tasks/utility/tests/test_secretsToEnv.py +1091 -0
- cumulusci/tests/test_integration_infrastructure.py +3 -1
- cumulusci/tests/test_utils.py +70 -6
- cumulusci/utils/__init__.py +54 -9
- cumulusci/utils/classutils.py +5 -2
- cumulusci/utils/http/tests/cassettes/ManualEditTestCompositeParallelSalesforce.test_http_headers.yaml +31 -30
- cumulusci/utils/options.py +23 -1
- cumulusci/utils/parallel/task_worker_queues/parallel_worker.py +1 -1
- cumulusci/utils/yaml/cumulusci_yml.py +7 -3
- cumulusci/utils/yaml/model_parser.py +2 -2
- cumulusci/utils/yaml/tests/test_cumulusci_yml.py +1 -1
- cumulusci/utils/yaml/tests/test_model_parser.py +3 -3
- cumulusci/vcs/base.py +23 -15
- cumulusci/vcs/bootstrap.py +5 -4
- cumulusci/vcs/utils/list_modified_files.py +189 -0
- cumulusci/vcs/utils/tests/test_list_modified_files.py +588 -0
- {cumulusci_plus-5.0.19.dist-info → cumulusci_plus-5.0.35.dist-info}/METADATA +12 -10
- {cumulusci_plus-5.0.19.dist-info → cumulusci_plus-5.0.35.dist-info}/RECORD +123 -98
- {cumulusci_plus-5.0.19.dist-info → cumulusci_plus-5.0.35.dist-info}/WHEEL +0 -0
- {cumulusci_plus-5.0.19.dist-info → cumulusci_plus-5.0.35.dist-info}/entry_points.txt +0 -0
- {cumulusci_plus-5.0.19.dist-info → cumulusci_plus-5.0.35.dist-info}/licenses/AUTHORS.rst +0 -0
- {cumulusci_plus-5.0.19.dist-info → cumulusci_plus-5.0.35.dist-info}/licenses/LICENSE +0 -0
|
@@ -599,6 +599,7 @@ class TestMappingParser:
|
|
|
599
599
|
mock.ANY, # This is a function def
|
|
600
600
|
mock.ANY,
|
|
601
601
|
DataOperationType.INSERT,
|
|
602
|
+
None, # validation_result
|
|
602
603
|
)
|
|
603
604
|
|
|
604
605
|
ms._validate_field_dict.assert_has_calls(
|
|
@@ -612,6 +613,7 @@ class TestMappingParser:
|
|
|
612
613
|
mock.ANY, # local function def
|
|
613
614
|
False,
|
|
614
615
|
DataOperationType.INSERT,
|
|
616
|
+
None,
|
|
615
617
|
),
|
|
616
618
|
mock.call(
|
|
617
619
|
{"ns__Test__c": {"name": "ns__Test__c", "createable": True}},
|
|
@@ -620,6 +622,7 @@ class TestMappingParser:
|
|
|
620
622
|
mock.ANY, # local function def
|
|
621
623
|
False,
|
|
622
624
|
DataOperationType.INSERT,
|
|
625
|
+
None,
|
|
623
626
|
),
|
|
624
627
|
]
|
|
625
628
|
)
|
|
@@ -668,6 +671,7 @@ class TestMappingParser:
|
|
|
668
671
|
mock.ANY, # local function def
|
|
669
672
|
mock.ANY,
|
|
670
673
|
DataOperationType.INSERT,
|
|
674
|
+
None,
|
|
671
675
|
)
|
|
672
676
|
|
|
673
677
|
ms._validate_field_dict.assert_has_calls(
|
|
@@ -686,6 +690,7 @@ class TestMappingParser:
|
|
|
686
690
|
mock.ANY, # local function def.
|
|
687
691
|
False,
|
|
688
692
|
DataOperationType.INSERT,
|
|
693
|
+
None,
|
|
689
694
|
),
|
|
690
695
|
mock.call(
|
|
691
696
|
{
|
|
@@ -701,6 +706,7 @@ class TestMappingParser:
|
|
|
701
706
|
mock.ANY, # local function def.
|
|
702
707
|
False,
|
|
703
708
|
DataOperationType.INSERT,
|
|
709
|
+
None,
|
|
704
710
|
),
|
|
705
711
|
]
|
|
706
712
|
)
|
|
@@ -734,6 +740,7 @@ class TestMappingParser:
|
|
|
734
740
|
None,
|
|
735
741
|
None,
|
|
736
742
|
DataOperationType.INSERT,
|
|
743
|
+
None,
|
|
737
744
|
)
|
|
738
745
|
|
|
739
746
|
ms._validate_field_dict.assert_has_calls(
|
|
@@ -745,6 +752,7 @@ class TestMappingParser:
|
|
|
745
752
|
None,
|
|
746
753
|
False,
|
|
747
754
|
DataOperationType.INSERT,
|
|
755
|
+
None,
|
|
748
756
|
),
|
|
749
757
|
mock.call(
|
|
750
758
|
{"Field__c": {"name": "Field__c", "createable": True}},
|
|
@@ -753,6 +761,7 @@ class TestMappingParser:
|
|
|
753
761
|
None,
|
|
754
762
|
False,
|
|
755
763
|
DataOperationType.INSERT,
|
|
764
|
+
None,
|
|
756
765
|
),
|
|
757
766
|
]
|
|
758
767
|
)
|
|
@@ -788,6 +797,7 @@ class TestMappingParser:
|
|
|
788
797
|
None,
|
|
789
798
|
None,
|
|
790
799
|
DataOperationType.INSERT,
|
|
800
|
+
None,
|
|
791
801
|
)
|
|
792
802
|
|
|
793
803
|
ms._validate_field_dict.assert_not_called()
|
|
@@ -823,6 +833,7 @@ class TestMappingParser:
|
|
|
823
833
|
None,
|
|
824
834
|
None,
|
|
825
835
|
DataOperationType.INSERT,
|
|
836
|
+
None,
|
|
826
837
|
)
|
|
827
838
|
|
|
828
839
|
ms._validate_field_dict.assert_has_calls(
|
|
@@ -834,6 +845,7 @@ class TestMappingParser:
|
|
|
834
845
|
None,
|
|
835
846
|
False,
|
|
836
847
|
DataOperationType.INSERT,
|
|
848
|
+
None,
|
|
837
849
|
)
|
|
838
850
|
]
|
|
839
851
|
)
|
|
@@ -881,6 +893,7 @@ class TestMappingParser:
|
|
|
881
893
|
None,
|
|
882
894
|
None,
|
|
883
895
|
DataOperationType.INSERT,
|
|
896
|
+
None,
|
|
884
897
|
)
|
|
885
898
|
|
|
886
899
|
ms._validate_field_dict.assert_has_calls(
|
|
@@ -899,6 +912,7 @@ class TestMappingParser:
|
|
|
899
912
|
None,
|
|
900
913
|
False,
|
|
901
914
|
DataOperationType.INSERT,
|
|
915
|
+
None,
|
|
902
916
|
),
|
|
903
917
|
mock.call(
|
|
904
918
|
{
|
|
@@ -914,6 +928,7 @@ class TestMappingParser:
|
|
|
914
928
|
None,
|
|
915
929
|
False,
|
|
916
930
|
DataOperationType.INSERT,
|
|
931
|
+
None,
|
|
917
932
|
),
|
|
918
933
|
]
|
|
919
934
|
)
|
|
@@ -962,6 +977,7 @@ class TestMappingParser:
|
|
|
962
977
|
None,
|
|
963
978
|
None,
|
|
964
979
|
DataOperationType.INSERT,
|
|
980
|
+
None,
|
|
965
981
|
)
|
|
966
982
|
|
|
967
983
|
ms._validate_field_dict.assert_has_calls(
|
|
@@ -980,6 +996,7 @@ class TestMappingParser:
|
|
|
980
996
|
None,
|
|
981
997
|
False,
|
|
982
998
|
DataOperationType.INSERT,
|
|
999
|
+
None,
|
|
983
1000
|
),
|
|
984
1001
|
mock.call(
|
|
985
1002
|
{
|
|
@@ -995,6 +1012,7 @@ class TestMappingParser:
|
|
|
995
1012
|
None,
|
|
996
1013
|
False,
|
|
997
1014
|
DataOperationType.INSERT,
|
|
1015
|
+
None,
|
|
998
1016
|
),
|
|
999
1017
|
]
|
|
1000
1018
|
)
|
|
@@ -1185,15 +1203,58 @@ class TestMappingParser:
|
|
|
1185
1203
|
{"instance_url": "https://example.com", "access_token": "abc123"}, "test"
|
|
1186
1204
|
)
|
|
1187
1205
|
|
|
1206
|
+
# Should raise BulkDataException when drop_missing=False
|
|
1207
|
+
with pytest.raises(
|
|
1208
|
+
BulkDataException,
|
|
1209
|
+
match="One or more schema or permissions errors blocked the operation",
|
|
1210
|
+
):
|
|
1211
|
+
validate_and_inject_mapping(
|
|
1212
|
+
mapping=mapping,
|
|
1213
|
+
sf=org_config.salesforce_client,
|
|
1214
|
+
namespace="",
|
|
1215
|
+
data_operation=DataOperationType.INSERT,
|
|
1216
|
+
inject_namespaces=False,
|
|
1217
|
+
drop_missing=False,
|
|
1218
|
+
)
|
|
1219
|
+
|
|
1220
|
+
# Verify the error was logged
|
|
1221
|
+
expected_error_message = (
|
|
1222
|
+
"One or more required fields are missing for loading on Account :{'Name'}"
|
|
1223
|
+
)
|
|
1224
|
+
error_logs = [
|
|
1225
|
+
record.message for record in caplog.records if record.levelname == "ERROR"
|
|
1226
|
+
]
|
|
1227
|
+
assert any(expected_error_message in error_log for error_log in error_logs)
|
|
1228
|
+
|
|
1229
|
+
@responses.activate
|
|
1230
|
+
def test_validate_and_inject_mapping_allows_missing_required_fields_with_drop_missing(
|
|
1231
|
+
self, caplog
|
|
1232
|
+
):
|
|
1233
|
+
"""Test that drop_missing=True allows missing required fields (with warning)."""
|
|
1234
|
+
caplog.set_level(logging.ERROR)
|
|
1235
|
+
mock_describe_calls()
|
|
1236
|
+
mapping = parse_from_yaml(
|
|
1237
|
+
StringIO(
|
|
1238
|
+
(
|
|
1239
|
+
"Insert Accounts:\n sf_object: Account\n table: Account\n fields:\n - ns__Description__c\n"
|
|
1240
|
+
)
|
|
1241
|
+
)
|
|
1242
|
+
)
|
|
1243
|
+
org_config = DummyOrgConfig(
|
|
1244
|
+
{"instance_url": "https://example.com", "access_token": "abc123"}, "test"
|
|
1245
|
+
)
|
|
1246
|
+
|
|
1247
|
+
# Should NOT raise exception when drop_missing=True, even with missing required fields
|
|
1188
1248
|
validate_and_inject_mapping(
|
|
1189
1249
|
mapping=mapping,
|
|
1190
1250
|
sf=org_config.salesforce_client,
|
|
1191
1251
|
namespace="",
|
|
1192
1252
|
data_operation=DataOperationType.INSERT,
|
|
1193
1253
|
inject_namespaces=False,
|
|
1194
|
-
drop_missing=
|
|
1254
|
+
drop_missing=True,
|
|
1195
1255
|
)
|
|
1196
1256
|
|
|
1257
|
+
# Verify the error was still logged as a warning
|
|
1197
1258
|
expected_error_message = (
|
|
1198
1259
|
"One or more required fields are missing for loading on Account :{'Name'}"
|
|
1199
1260
|
)
|
|
@@ -1656,3 +1717,704 @@ class TestUpsertKeyValidations:
|
|
|
1656
1717
|
record.message for record in caplog.records if record.levelname == "ERROR"
|
|
1657
1718
|
]
|
|
1658
1719
|
assert any(expected_error_message in error_log for error_log in error_logs)
|
|
1720
|
+
|
|
1721
|
+
|
|
1722
|
+
class TestValidationResult:
|
|
1723
|
+
"""Tests for ValidationResult class"""
|
|
1724
|
+
|
|
1725
|
+
def test_validation_result_initialization(self):
|
|
1726
|
+
"""Test ValidationResult initializes with empty lists"""
|
|
1727
|
+
from cumulusci.tasks.bulkdata.mapping_parser import ValidationResult
|
|
1728
|
+
|
|
1729
|
+
result = ValidationResult()
|
|
1730
|
+
assert result.errors == []
|
|
1731
|
+
assert result.warnings == []
|
|
1732
|
+
assert not result.has_errors()
|
|
1733
|
+
|
|
1734
|
+
def test_validation_result_add_error(self, caplog):
|
|
1735
|
+
"""Test adding errors to ValidationResult"""
|
|
1736
|
+
from cumulusci.tasks.bulkdata.mapping_parser import ValidationResult
|
|
1737
|
+
|
|
1738
|
+
caplog.set_level(logging.ERROR)
|
|
1739
|
+
result = ValidationResult()
|
|
1740
|
+
|
|
1741
|
+
result.add_error("Test error 1")
|
|
1742
|
+
result.add_error("Test error 2")
|
|
1743
|
+
|
|
1744
|
+
assert len(result.errors) == 2
|
|
1745
|
+
assert "Test error 1" in result.errors
|
|
1746
|
+
assert "Test error 2" in result.errors
|
|
1747
|
+
assert result.has_errors()
|
|
1748
|
+
|
|
1749
|
+
# Verify errors are also logged
|
|
1750
|
+
assert "Test error 1" in caplog.text
|
|
1751
|
+
assert "Test error 2" in caplog.text
|
|
1752
|
+
|
|
1753
|
+
def test_validation_result_add_warning(self, caplog):
|
|
1754
|
+
"""Test adding warnings to ValidationResult"""
|
|
1755
|
+
from cumulusci.tasks.bulkdata.mapping_parser import ValidationResult
|
|
1756
|
+
|
|
1757
|
+
caplog.set_level(logging.WARNING)
|
|
1758
|
+
result = ValidationResult()
|
|
1759
|
+
|
|
1760
|
+
result.add_warning("Test warning 1")
|
|
1761
|
+
result.add_warning("Test warning 2")
|
|
1762
|
+
|
|
1763
|
+
assert len(result.warnings) == 2
|
|
1764
|
+
assert "Test warning 1" in result.warnings
|
|
1765
|
+
assert "Test warning 2" in result.warnings
|
|
1766
|
+
assert not result.has_errors() # Warnings don't count as errors
|
|
1767
|
+
|
|
1768
|
+
# Verify warnings are also logged
|
|
1769
|
+
assert "Test warning 1" in caplog.text
|
|
1770
|
+
assert "Test warning 2" in caplog.text
|
|
1771
|
+
|
|
1772
|
+
def test_validation_result_mixed_errors_and_warnings(self):
|
|
1773
|
+
"""Test ValidationResult with both errors and warnings"""
|
|
1774
|
+
from cumulusci.tasks.bulkdata.mapping_parser import ValidationResult
|
|
1775
|
+
|
|
1776
|
+
result = ValidationResult()
|
|
1777
|
+
|
|
1778
|
+
result.add_warning("Warning message")
|
|
1779
|
+
result.add_error("Error message")
|
|
1780
|
+
result.add_warning("Another warning")
|
|
1781
|
+
|
|
1782
|
+
assert len(result.errors) == 1
|
|
1783
|
+
assert len(result.warnings) == 2
|
|
1784
|
+
assert result.has_errors()
|
|
1785
|
+
|
|
1786
|
+
|
|
1787
|
+
class TestValidateOnlyMode:
|
|
1788
|
+
"""Tests for validate_only mode in validate_and_inject_mapping"""
|
|
1789
|
+
|
|
1790
|
+
@responses.activate
|
|
1791
|
+
def test_validate_only_returns_validation_result(self):
|
|
1792
|
+
"""Test that validate_only=True returns ValidationResult"""
|
|
1793
|
+
from cumulusci.tasks.bulkdata.mapping_parser import ValidationResult
|
|
1794
|
+
|
|
1795
|
+
mock_describe_calls()
|
|
1796
|
+
mapping = parse_from_yaml(
|
|
1797
|
+
StringIO(
|
|
1798
|
+
"Insert Accounts:\n sf_object: Account\n table: Account\n fields:\n - Name"
|
|
1799
|
+
)
|
|
1800
|
+
)
|
|
1801
|
+
org_config = DummyOrgConfig(
|
|
1802
|
+
{"instance_url": "https://example.com", "access_token": "abc123"}, "test"
|
|
1803
|
+
)
|
|
1804
|
+
|
|
1805
|
+
result = validate_and_inject_mapping(
|
|
1806
|
+
mapping=mapping,
|
|
1807
|
+
sf=org_config.salesforce_client,
|
|
1808
|
+
namespace=None,
|
|
1809
|
+
data_operation=DataOperationType.INSERT,
|
|
1810
|
+
inject_namespaces=False,
|
|
1811
|
+
drop_missing=False,
|
|
1812
|
+
validate_only=True,
|
|
1813
|
+
)
|
|
1814
|
+
|
|
1815
|
+
assert result is not None
|
|
1816
|
+
assert isinstance(result, ValidationResult)
|
|
1817
|
+
assert not result.has_errors()
|
|
1818
|
+
|
|
1819
|
+
@responses.activate
|
|
1820
|
+
def test_validate_only_false_returns_none(self):
|
|
1821
|
+
"""Test that validate_only=False returns None"""
|
|
1822
|
+
mock_describe_calls()
|
|
1823
|
+
mapping = parse_from_yaml(
|
|
1824
|
+
StringIO(
|
|
1825
|
+
"Insert Accounts:\n sf_object: Account\n table: Account\n fields:\n - Name"
|
|
1826
|
+
)
|
|
1827
|
+
)
|
|
1828
|
+
org_config = DummyOrgConfig(
|
|
1829
|
+
{"instance_url": "https://example.com", "access_token": "abc123"}, "test"
|
|
1830
|
+
)
|
|
1831
|
+
|
|
1832
|
+
result = validate_and_inject_mapping(
|
|
1833
|
+
mapping=mapping,
|
|
1834
|
+
sf=org_config.salesforce_client,
|
|
1835
|
+
namespace=None,
|
|
1836
|
+
data_operation=DataOperationType.INSERT,
|
|
1837
|
+
inject_namespaces=False,
|
|
1838
|
+
drop_missing=False,
|
|
1839
|
+
validate_only=False,
|
|
1840
|
+
)
|
|
1841
|
+
|
|
1842
|
+
assert result is None
|
|
1843
|
+
|
|
1844
|
+
@responses.activate
|
|
1845
|
+
def test_validate_only_collects_missing_field_errors(self):
|
|
1846
|
+
"""Test that validate_only collects missing field errors"""
|
|
1847
|
+
from cumulusci.tasks.bulkdata.mapping_parser import ValidationResult
|
|
1848
|
+
|
|
1849
|
+
mock_describe_calls()
|
|
1850
|
+
mapping = parse_from_yaml(
|
|
1851
|
+
StringIO(
|
|
1852
|
+
"Insert Accounts:\n sf_object: Account\n table: Account\n fields:\n - Nonsense__c"
|
|
1853
|
+
)
|
|
1854
|
+
)
|
|
1855
|
+
org_config = DummyOrgConfig(
|
|
1856
|
+
{"instance_url": "https://example.com", "access_token": "abc123"}, "test"
|
|
1857
|
+
)
|
|
1858
|
+
|
|
1859
|
+
result = validate_and_inject_mapping(
|
|
1860
|
+
mapping=mapping,
|
|
1861
|
+
sf=org_config.salesforce_client,
|
|
1862
|
+
namespace=None,
|
|
1863
|
+
data_operation=DataOperationType.INSERT,
|
|
1864
|
+
inject_namespaces=False,
|
|
1865
|
+
drop_missing=False,
|
|
1866
|
+
validate_only=True,
|
|
1867
|
+
)
|
|
1868
|
+
|
|
1869
|
+
assert result is not None
|
|
1870
|
+
assert isinstance(result, ValidationResult)
|
|
1871
|
+
# Should have warnings about missing field
|
|
1872
|
+
assert any("Nonsense__c" in warning for warning in result.warnings)
|
|
1873
|
+
|
|
1874
|
+
@responses.activate
|
|
1875
|
+
def test_validate_only_collects_missing_required_field_errors(self):
|
|
1876
|
+
"""Test that validate_only collects missing required field errors"""
|
|
1877
|
+
from cumulusci.tasks.bulkdata.mapping_parser import ValidationResult
|
|
1878
|
+
|
|
1879
|
+
mock_describe_calls()
|
|
1880
|
+
mapping = parse_from_yaml(
|
|
1881
|
+
StringIO(
|
|
1882
|
+
"Insert Accounts:\n sf_object: Account\n table: Account\n fields:\n - Description"
|
|
1883
|
+
)
|
|
1884
|
+
)
|
|
1885
|
+
org_config = DummyOrgConfig(
|
|
1886
|
+
{"instance_url": "https://example.com", "access_token": "abc123"}, "test"
|
|
1887
|
+
)
|
|
1888
|
+
|
|
1889
|
+
result = validate_and_inject_mapping(
|
|
1890
|
+
mapping=mapping,
|
|
1891
|
+
sf=org_config.salesforce_client,
|
|
1892
|
+
namespace=None,
|
|
1893
|
+
data_operation=DataOperationType.INSERT,
|
|
1894
|
+
inject_namespaces=False,
|
|
1895
|
+
drop_missing=False,
|
|
1896
|
+
validate_only=True,
|
|
1897
|
+
)
|
|
1898
|
+
|
|
1899
|
+
assert result is not None
|
|
1900
|
+
assert isinstance(result, ValidationResult)
|
|
1901
|
+
assert result.has_errors()
|
|
1902
|
+
# Should have error about missing required field 'Name'
|
|
1903
|
+
assert any("required fields" in error.lower() for error in result.errors)
|
|
1904
|
+
assert any("Name" in error for error in result.errors)
|
|
1905
|
+
|
|
1906
|
+
@responses.activate
|
|
1907
|
+
def test_validate_only_early_return_on_sobject_error(self):
|
|
1908
|
+
"""Test that validate_only returns early when sObject doesn't exist"""
|
|
1909
|
+
from cumulusci.tasks.bulkdata.mapping_parser import ValidationResult
|
|
1910
|
+
|
|
1911
|
+
mock_describe_calls()
|
|
1912
|
+
mapping = parse_from_yaml(
|
|
1913
|
+
StringIO(
|
|
1914
|
+
"Insert Invalid:\n sf_object: InvalidObject__c\n table: InvalidObject\n fields:\n - Name"
|
|
1915
|
+
)
|
|
1916
|
+
)
|
|
1917
|
+
org_config = DummyOrgConfig(
|
|
1918
|
+
{"instance_url": "https://example.com", "access_token": "abc123"}, "test"
|
|
1919
|
+
)
|
|
1920
|
+
|
|
1921
|
+
result = validate_and_inject_mapping(
|
|
1922
|
+
mapping=mapping,
|
|
1923
|
+
sf=org_config.salesforce_client,
|
|
1924
|
+
namespace=None,
|
|
1925
|
+
data_operation=DataOperationType.INSERT,
|
|
1926
|
+
inject_namespaces=False,
|
|
1927
|
+
drop_missing=False,
|
|
1928
|
+
validate_only=True,
|
|
1929
|
+
)
|
|
1930
|
+
|
|
1931
|
+
assert result is not None
|
|
1932
|
+
assert isinstance(result, ValidationResult)
|
|
1933
|
+
# Should have warning about missing object
|
|
1934
|
+
assert any("InvalidObject__c" in warning for warning in result.warnings)
|
|
1935
|
+
|
|
1936
|
+
@responses.activate
|
|
1937
|
+
def test_validate_only_collects_lookup_errors(self):
|
|
1938
|
+
"""Test that validate_only collects lookup validation errors"""
|
|
1939
|
+
from cumulusci.tasks.bulkdata.mapping_parser import ValidationResult
|
|
1940
|
+
|
|
1941
|
+
mock_describe_calls()
|
|
1942
|
+
mapping = parse_from_yaml(
|
|
1943
|
+
StringIO(
|
|
1944
|
+
(
|
|
1945
|
+
"Insert Contacts:\n sf_object: Contact\n table: Contact\n fields:\n - LastName\n lookups:\n AccountId:\n table: Account"
|
|
1946
|
+
)
|
|
1947
|
+
)
|
|
1948
|
+
)
|
|
1949
|
+
org_config = DummyOrgConfig(
|
|
1950
|
+
{"instance_url": "https://example.com", "access_token": "abc123"}, "test"
|
|
1951
|
+
)
|
|
1952
|
+
|
|
1953
|
+
result = validate_and_inject_mapping(
|
|
1954
|
+
mapping=mapping,
|
|
1955
|
+
sf=org_config.salesforce_client,
|
|
1956
|
+
namespace=None,
|
|
1957
|
+
data_operation=DataOperationType.INSERT,
|
|
1958
|
+
inject_namespaces=False,
|
|
1959
|
+
drop_missing=False,
|
|
1960
|
+
validate_only=True,
|
|
1961
|
+
)
|
|
1962
|
+
|
|
1963
|
+
assert result is not None
|
|
1964
|
+
assert isinstance(result, ValidationResult)
|
|
1965
|
+
assert result.has_errors()
|
|
1966
|
+
# Should have error about missing Account table
|
|
1967
|
+
assert any(
|
|
1968
|
+
"Account" in error and "does not exist" in error for error in result.errors
|
|
1969
|
+
)
|
|
1970
|
+
|
|
1971
|
+
@responses.activate
|
|
1972
|
+
def test_validate_only_without_load_skips_lookup_validation(self):
|
|
1973
|
+
"""Test that validate_only skips lookup validation for QUERY operations"""
|
|
1974
|
+
from cumulusci.tasks.bulkdata.mapping_parser import ValidationResult
|
|
1975
|
+
|
|
1976
|
+
mock_describe_calls()
|
|
1977
|
+
mapping = parse_from_yaml(
|
|
1978
|
+
StringIO(
|
|
1979
|
+
(
|
|
1980
|
+
"Insert Contacts:\n sf_object: Contact\n table: Contact\n fields:\n - LastName\n lookups:\n AccountId:\n table: Account"
|
|
1981
|
+
)
|
|
1982
|
+
)
|
|
1983
|
+
)
|
|
1984
|
+
org_config = DummyOrgConfig(
|
|
1985
|
+
{"instance_url": "https://example.com", "access_token": "abc123"}, "test"
|
|
1986
|
+
)
|
|
1987
|
+
|
|
1988
|
+
result = validate_and_inject_mapping(
|
|
1989
|
+
mapping=mapping,
|
|
1990
|
+
sf=org_config.salesforce_client,
|
|
1991
|
+
namespace=None,
|
|
1992
|
+
data_operation=DataOperationType.QUERY, # Not a load operation
|
|
1993
|
+
inject_namespaces=False,
|
|
1994
|
+
drop_missing=False,
|
|
1995
|
+
validate_only=True,
|
|
1996
|
+
)
|
|
1997
|
+
|
|
1998
|
+
assert result is not None
|
|
1999
|
+
assert isinstance(result, ValidationResult)
|
|
2000
|
+
# Should not have lookup validation errors since it's a QUERY
|
|
2001
|
+
assert not any(
|
|
2002
|
+
"Account" in error and "does not exist" in error for error in result.errors
|
|
2003
|
+
)
|
|
2004
|
+
|
|
2005
|
+
|
|
2006
|
+
class TestValidationResultParameter:
|
|
2007
|
+
"""Tests for optional ValidationResult parameter in validation methods"""
|
|
2008
|
+
|
|
2009
|
+
def test_check_required_with_validation_result(self):
|
|
2010
|
+
"""Test check_required adds errors to ValidationResult when provided"""
|
|
2011
|
+
from cumulusci.tasks.bulkdata.mapping_parser import ValidationResult
|
|
2012
|
+
|
|
2013
|
+
ms = MappingStep(
|
|
2014
|
+
sf_object="Account",
|
|
2015
|
+
fields=["Description"],
|
|
2016
|
+
action=DataOperationType.INSERT,
|
|
2017
|
+
)
|
|
2018
|
+
fields_describe = CaseInsensitiveDict(
|
|
2019
|
+
{
|
|
2020
|
+
"Name": {
|
|
2021
|
+
"createable": True,
|
|
2022
|
+
"nillable": False,
|
|
2023
|
+
"defaultedOnCreate": False,
|
|
2024
|
+
"defaultValue": None,
|
|
2025
|
+
},
|
|
2026
|
+
"Description": {
|
|
2027
|
+
"createable": True,
|
|
2028
|
+
"nillable": True,
|
|
2029
|
+
"defaultedOnCreate": False,
|
|
2030
|
+
"defaultValue": None,
|
|
2031
|
+
},
|
|
2032
|
+
}
|
|
2033
|
+
)
|
|
2034
|
+
|
|
2035
|
+
validation_result = ValidationResult()
|
|
2036
|
+
result = ms.check_required(fields_describe, validation_result)
|
|
2037
|
+
|
|
2038
|
+
assert not result # Should return False due to missing required field
|
|
2039
|
+
assert validation_result.has_errors()
|
|
2040
|
+
assert any(
|
|
2041
|
+
"required fields" in error.lower() for error in validation_result.errors
|
|
2042
|
+
)
|
|
2043
|
+
assert any("Name" in error for error in validation_result.errors)
|
|
2044
|
+
|
|
2045
|
+
def test_check_required_without_validation_result_logs(self, caplog):
|
|
2046
|
+
"""Test check_required logs errors when ValidationResult not provided"""
|
|
2047
|
+
caplog.set_level(logging.ERROR)
|
|
2048
|
+
ms = MappingStep(
|
|
2049
|
+
sf_object="Account",
|
|
2050
|
+
fields=["Description"],
|
|
2051
|
+
action=DataOperationType.INSERT,
|
|
2052
|
+
)
|
|
2053
|
+
fields_describe = CaseInsensitiveDict(
|
|
2054
|
+
{
|
|
2055
|
+
"Name": {
|
|
2056
|
+
"createable": True,
|
|
2057
|
+
"nillable": False,
|
|
2058
|
+
"defaultedOnCreate": False,
|
|
2059
|
+
"defaultValue": None,
|
|
2060
|
+
},
|
|
2061
|
+
}
|
|
2062
|
+
)
|
|
2063
|
+
|
|
2064
|
+
result = ms.check_required(fields_describe, None)
|
|
2065
|
+
|
|
2066
|
+
assert not result
|
|
2067
|
+
assert "required fields" in caplog.text.lower()
|
|
2068
|
+
assert "Name" in caplog.text
|
|
2069
|
+
|
|
2070
|
+
def test_validate_sobject_with_validation_result(self):
|
|
2071
|
+
"""Test _validate_sobject adds warnings to ValidationResult"""
|
|
2072
|
+
from cumulusci.tasks.bulkdata.mapping_parser import ValidationResult
|
|
2073
|
+
|
|
2074
|
+
ms = MappingStep(
|
|
2075
|
+
sf_object="InvalidObject__c",
|
|
2076
|
+
fields=["Name"],
|
|
2077
|
+
action=DataOperationType.INSERT,
|
|
2078
|
+
)
|
|
2079
|
+
|
|
2080
|
+
validation_result = ValidationResult()
|
|
2081
|
+
result = ms._validate_sobject(
|
|
2082
|
+
CaseInsensitiveDict({"Account": {"createable": True}}),
|
|
2083
|
+
None,
|
|
2084
|
+
None,
|
|
2085
|
+
DataOperationType.INSERT,
|
|
2086
|
+
validation_result,
|
|
2087
|
+
)
|
|
2088
|
+
|
|
2089
|
+
assert not result
|
|
2090
|
+
assert len(validation_result.warnings) > 0
|
|
2091
|
+
assert any(
|
|
2092
|
+
"InvalidObject__c" in warning for warning in validation_result.warnings
|
|
2093
|
+
)
|
|
2094
|
+
|
|
2095
|
+
def test_validate_field_dict_with_validation_result(self):
|
|
2096
|
+
"""Test _validate_field_dict adds warnings to ValidationResult"""
|
|
2097
|
+
from cumulusci.tasks.bulkdata.mapping_parser import ValidationResult
|
|
2098
|
+
|
|
2099
|
+
ms = MappingStep(
|
|
2100
|
+
sf_object="Account",
|
|
2101
|
+
fields=["Name", "NonexistentField__c"],
|
|
2102
|
+
action=DataOperationType.INSERT,
|
|
2103
|
+
)
|
|
2104
|
+
|
|
2105
|
+
validation_result = ValidationResult()
|
|
2106
|
+
result = ms._validate_field_dict(
|
|
2107
|
+
describe=CaseInsensitiveDict({"Name": {"createable": True}}),
|
|
2108
|
+
field_dict=ms.fields_,
|
|
2109
|
+
inject=None,
|
|
2110
|
+
strip=None,
|
|
2111
|
+
drop_missing=False,
|
|
2112
|
+
data_operation_type=DataOperationType.INSERT,
|
|
2113
|
+
validation_result=validation_result,
|
|
2114
|
+
)
|
|
2115
|
+
|
|
2116
|
+
assert not result
|
|
2117
|
+
assert len(validation_result.warnings) > 0
|
|
2118
|
+
assert any(
|
|
2119
|
+
"NonexistentField__c" in warning for warning in validation_result.warnings
|
|
2120
|
+
)
|
|
2121
|
+
|
|
2122
|
+
def test_infer_and_validate_lookups_with_validation_result(self):
|
|
2123
|
+
"""Test _infer_and_validate_lookups adds errors to ValidationResult"""
|
|
2124
|
+
from cumulusci.tasks.bulkdata.mapping_parser import ValidationResult
|
|
2125
|
+
|
|
2126
|
+
mock_sf = mock.Mock()
|
|
2127
|
+
mock_sf.Contact.describe.return_value = {
|
|
2128
|
+
"fields": [
|
|
2129
|
+
{
|
|
2130
|
+
"name": "AccountId",
|
|
2131
|
+
"referenceTo": ["Account"],
|
|
2132
|
+
}
|
|
2133
|
+
]
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
mapping = {
|
|
2137
|
+
"Insert Contacts": MappingStep(
|
|
2138
|
+
sf_object="Contact",
|
|
2139
|
+
table="Contact",
|
|
2140
|
+
fields=["LastName"],
|
|
2141
|
+
lookups={"AccountId": MappingLookup(table="Account", name="AccountId")},
|
|
2142
|
+
)
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
validation_result = ValidationResult()
|
|
2146
|
+
_infer_and_validate_lookups(mapping, mock_sf, validation_result)
|
|
2147
|
+
|
|
2148
|
+
# Should have error about missing Account table
|
|
2149
|
+
assert validation_result.has_errors()
|
|
2150
|
+
assert any(
|
|
2151
|
+
"Account" in error and "does not exist" in error
|
|
2152
|
+
for error in validation_result.errors
|
|
2153
|
+
)
|
|
2154
|
+
|
|
2155
|
+
def test_infer_and_validate_lookups_without_validation_result_raises(self):
|
|
2156
|
+
"""Test _infer_and_validate_lookups raises exception when ValidationResult not provided"""
|
|
2157
|
+
mock_sf = mock.Mock()
|
|
2158
|
+
mock_sf.Contact.describe.return_value = {
|
|
2159
|
+
"fields": [
|
|
2160
|
+
{
|
|
2161
|
+
"name": "AccountId",
|
|
2162
|
+
"referenceTo": ["Account"],
|
|
2163
|
+
}
|
|
2164
|
+
]
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
mapping = {
|
|
2168
|
+
"Insert Contacts": MappingStep(
|
|
2169
|
+
sf_object="Contact",
|
|
2170
|
+
table="Contact",
|
|
2171
|
+
fields=["LastName"],
|
|
2172
|
+
lookups={"AccountId": MappingLookup(table="Account", name="AccountId")},
|
|
2173
|
+
)
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
with pytest.raises(BulkDataException) as e:
|
|
2177
|
+
_infer_and_validate_lookups(mapping, mock_sf, None)
|
|
2178
|
+
|
|
2179
|
+
assert "relationship errors" in str(e.value).lower()
|
|
2180
|
+
|
|
2181
|
+
|
|
2182
|
+
class TestValidationResultCoverage:
|
|
2183
|
+
"""Additional tests to achieve full coverage of ValidationResult code paths"""
|
|
2184
|
+
|
|
2185
|
+
def test_validate_field_dict_duplicate_field_with_validation_result(self):
|
|
2186
|
+
"""Test _validate_field_dict with duplicate fields (injected and original) using ValidationResult"""
|
|
2187
|
+
from cumulusci.tasks.bulkdata.mapping_parser import ValidationResult
|
|
2188
|
+
|
|
2189
|
+
ms = MappingStep(
|
|
2190
|
+
sf_object="Account",
|
|
2191
|
+
fields=["Test__c"],
|
|
2192
|
+
action=DataOperationType.INSERT,
|
|
2193
|
+
)
|
|
2194
|
+
|
|
2195
|
+
validation_result = ValidationResult()
|
|
2196
|
+
# Both Test__c and ns__Test__c exist in describe
|
|
2197
|
+
result = ms._validate_field_dict(
|
|
2198
|
+
describe=CaseInsensitiveDict(
|
|
2199
|
+
{"Test__c": {"createable": True}, "ns__Test__c": {"createable": True}}
|
|
2200
|
+
),
|
|
2201
|
+
field_dict=ms.fields_,
|
|
2202
|
+
inject=lambda field: f"ns__{field}",
|
|
2203
|
+
strip=None,
|
|
2204
|
+
drop_missing=False,
|
|
2205
|
+
data_operation_type=DataOperationType.INSERT,
|
|
2206
|
+
validation_result=validation_result,
|
|
2207
|
+
)
|
|
2208
|
+
|
|
2209
|
+
assert result
|
|
2210
|
+
# Should have warning about both fields being present
|
|
2211
|
+
assert any(
|
|
2212
|
+
"Both" in warning and "Test__c" in warning
|
|
2213
|
+
for warning in validation_result.warnings
|
|
2214
|
+
)
|
|
2215
|
+
|
|
2216
|
+
def test_validate_field_dict_permission_error_with_validation_result(self):
|
|
2217
|
+
"""Test _validate_field_dict with field permission errors using ValidationResult"""
|
|
2218
|
+
from cumulusci.tasks.bulkdata.mapping_parser import ValidationResult
|
|
2219
|
+
|
|
2220
|
+
ms = MappingStep(
|
|
2221
|
+
sf_object="Account",
|
|
2222
|
+
fields=["Name"],
|
|
2223
|
+
action=DataOperationType.INSERT,
|
|
2224
|
+
)
|
|
2225
|
+
|
|
2226
|
+
validation_result = ValidationResult()
|
|
2227
|
+
result = ms._validate_field_dict(
|
|
2228
|
+
describe=CaseInsensitiveDict({"Name": {"createable": False}}),
|
|
2229
|
+
field_dict=ms.fields_,
|
|
2230
|
+
inject=None,
|
|
2231
|
+
strip=None,
|
|
2232
|
+
drop_missing=False,
|
|
2233
|
+
data_operation_type=DataOperationType.INSERT,
|
|
2234
|
+
validation_result=validation_result,
|
|
2235
|
+
)
|
|
2236
|
+
|
|
2237
|
+
assert not result
|
|
2238
|
+
# Should have warning about incorrect permissions
|
|
2239
|
+
assert any(
|
|
2240
|
+
"does not have the correct permissions" in warning
|
|
2241
|
+
for warning in validation_result.warnings
|
|
2242
|
+
)
|
|
2243
|
+
|
|
2244
|
+
def test_validate_sobject_permission_error_with_validation_result(self):
|
|
2245
|
+
"""Test _validate_sobject with permission errors using ValidationResult"""
|
|
2246
|
+
from cumulusci.tasks.bulkdata.mapping_parser import ValidationResult
|
|
2247
|
+
|
|
2248
|
+
ms = MappingStep(
|
|
2249
|
+
sf_object="Account",
|
|
2250
|
+
fields=["Name"],
|
|
2251
|
+
action=DataOperationType.INSERT,
|
|
2252
|
+
)
|
|
2253
|
+
|
|
2254
|
+
validation_result = ValidationResult()
|
|
2255
|
+
result = ms._validate_sobject(
|
|
2256
|
+
CaseInsensitiveDict({"Account": {"createable": False}}),
|
|
2257
|
+
None,
|
|
2258
|
+
None,
|
|
2259
|
+
DataOperationType.INSERT,
|
|
2260
|
+
validation_result,
|
|
2261
|
+
)
|
|
2262
|
+
|
|
2263
|
+
assert not result
|
|
2264
|
+
# Should have warning about incorrect permissions
|
|
2265
|
+
assert any(
|
|
2266
|
+
"does not have the correct permissions" in warning
|
|
2267
|
+
for warning in validation_result.warnings
|
|
2268
|
+
)
|
|
2269
|
+
|
|
2270
|
+
def test_infer_and_validate_lookups_invalid_reference_with_validation_result(self):
|
|
2271
|
+
"""Test _infer_and_validate_lookups with invalid reference using ValidationResult"""
|
|
2272
|
+
from cumulusci.tasks.bulkdata.mapping_parser import ValidationResult
|
|
2273
|
+
|
|
2274
|
+
mock_sf = mock.Mock()
|
|
2275
|
+
# Mock Event.describe
|
|
2276
|
+
mock_sf.Event.describe.return_value = {
|
|
2277
|
+
"fields": [
|
|
2278
|
+
{
|
|
2279
|
+
"name": "Description",
|
|
2280
|
+
"referenceTo": [],
|
|
2281
|
+
}
|
|
2282
|
+
]
|
|
2283
|
+
}
|
|
2284
|
+
# Mock Contact.describe
|
|
2285
|
+
mock_sf.Contact.describe.return_value = {
|
|
2286
|
+
"fields": [
|
|
2287
|
+
{
|
|
2288
|
+
"name": "AccountId",
|
|
2289
|
+
"referenceTo": ["Account"], # Only Account is valid
|
|
2290
|
+
}
|
|
2291
|
+
]
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
mapping = {
|
|
2295
|
+
"Insert Events": MappingStep(
|
|
2296
|
+
sf_object="Event",
|
|
2297
|
+
table="Event",
|
|
2298
|
+
fields=["Description"],
|
|
2299
|
+
),
|
|
2300
|
+
"Insert Contacts": MappingStep(
|
|
2301
|
+
sf_object="Contact",
|
|
2302
|
+
table="Contact",
|
|
2303
|
+
fields=["LastName"],
|
|
2304
|
+
lookups={
|
|
2305
|
+
"AccountId": MappingLookup(table="Event", name="AccountId")
|
|
2306
|
+
}, # Invalid - Event is not a valid lookup
|
|
2307
|
+
),
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
validation_result = ValidationResult()
|
|
2311
|
+
_infer_and_validate_lookups(mapping, mock_sf, validation_result)
|
|
2312
|
+
|
|
2313
|
+
# Should have error about invalid lookup
|
|
2314
|
+
assert validation_result.has_errors()
|
|
2315
|
+
assert any(
|
|
2316
|
+
"is not a valid lookup" in error for error in validation_result.errors
|
|
2317
|
+
)
|
|
2318
|
+
|
|
2319
|
+
def test_infer_and_validate_lookups_polymorphic_incorrect_order_with_validation_result(
|
|
2320
|
+
self,
|
|
2321
|
+
):
|
|
2322
|
+
"""Test _infer_and_validate_lookups with polymorphic lookups in incorrect order using ValidationResult"""
|
|
2323
|
+
from cumulusci.tasks.bulkdata.mapping_parser import ValidationResult
|
|
2324
|
+
|
|
2325
|
+
mock_sf = mock.Mock()
|
|
2326
|
+
# Mock Account.describe
|
|
2327
|
+
mock_sf.Account.describe.return_value = {
|
|
2328
|
+
"fields": [
|
|
2329
|
+
{
|
|
2330
|
+
"name": "Name",
|
|
2331
|
+
"referenceTo": [],
|
|
2332
|
+
}
|
|
2333
|
+
]
|
|
2334
|
+
}
|
|
2335
|
+
# Mock Event.describe
|
|
2336
|
+
mock_sf.Event.describe.return_value = {
|
|
2337
|
+
"fields": [
|
|
2338
|
+
{
|
|
2339
|
+
"name": "WhatId",
|
|
2340
|
+
"referenceTo": ["Account", "Opportunity"],
|
|
2341
|
+
}
|
|
2342
|
+
]
|
|
2343
|
+
}
|
|
2344
|
+
# Mock Opportunity.describe
|
|
2345
|
+
mock_sf.Opportunity.describe.return_value = {
|
|
2346
|
+
"fields": [
|
|
2347
|
+
{
|
|
2348
|
+
"name": "Name",
|
|
2349
|
+
"referenceTo": [],
|
|
2350
|
+
}
|
|
2351
|
+
]
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
# Event comes before Opportunity, but WhatId references both Account and Opportunity
|
|
2355
|
+
mapping = {
|
|
2356
|
+
"Insert Account": MappingStep(
|
|
2357
|
+
sf_object="Account",
|
|
2358
|
+
table="Account",
|
|
2359
|
+
fields=["Name"],
|
|
2360
|
+
),
|
|
2361
|
+
"Insert Events": MappingStep(
|
|
2362
|
+
sf_object="Event",
|
|
2363
|
+
table="Event",
|
|
2364
|
+
fields=["Description"],
|
|
2365
|
+
lookups={
|
|
2366
|
+
"WhatId": MappingLookup(
|
|
2367
|
+
table=["Account", "Opportunity"], name="WhatId"
|
|
2368
|
+
)
|
|
2369
|
+
},
|
|
2370
|
+
),
|
|
2371
|
+
"Insert Opportunity": MappingStep(
|
|
2372
|
+
sf_object="Opportunity",
|
|
2373
|
+
table="Opportunity",
|
|
2374
|
+
fields=["Name"],
|
|
2375
|
+
),
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
validation_result = ValidationResult()
|
|
2379
|
+
_infer_and_validate_lookups(mapping, mock_sf, validation_result)
|
|
2380
|
+
|
|
2381
|
+
# Should have error about incorrect order
|
|
2382
|
+
assert validation_result.has_errors()
|
|
2383
|
+
assert any("must precede" in error for error in validation_result.errors)
|
|
2384
|
+
|
|
2385
|
+
@responses.activate
|
|
2386
|
+
def test_validate_and_inject_mapping_required_lookup_dropped_with_validate_only(
|
|
2387
|
+
self,
|
|
2388
|
+
):
|
|
2389
|
+
"""Test validate_and_inject_mapping when a required lookup is dropped in validate_only mode"""
|
|
2390
|
+
from cumulusci.tasks.bulkdata.mapping_parser import ValidationResult
|
|
2391
|
+
|
|
2392
|
+
mock_describe_calls()
|
|
2393
|
+
# Using Id field as a required lookup (it's non-nillable)
|
|
2394
|
+
mapping = parse_from_yaml(
|
|
2395
|
+
StringIO(
|
|
2396
|
+
(
|
|
2397
|
+
"Insert Accounts:\n sf_object: NotAccount\n table: Account\n fields:\n - Nonsense__c\n"
|
|
2398
|
+
"Insert Contacts:\n sf_object: Contact\n table: Contact\n fields:\n - LastName\n lookups:\n Id:\n table: Account"
|
|
2399
|
+
)
|
|
2400
|
+
)
|
|
2401
|
+
)
|
|
2402
|
+
org_config = DummyOrgConfig(
|
|
2403
|
+
{"instance_url": "https://example.com", "access_token": "abc123"}, "test"
|
|
2404
|
+
)
|
|
2405
|
+
|
|
2406
|
+
result = validate_and_inject_mapping(
|
|
2407
|
+
mapping=mapping,
|
|
2408
|
+
sf=org_config.salesforce_client,
|
|
2409
|
+
namespace=None,
|
|
2410
|
+
data_operation=DataOperationType.INSERT,
|
|
2411
|
+
inject_namespaces=False,
|
|
2412
|
+
drop_missing=True,
|
|
2413
|
+
validate_only=True,
|
|
2414
|
+
)
|
|
2415
|
+
|
|
2416
|
+
assert result is not None
|
|
2417
|
+
assert isinstance(result, ValidationResult)
|
|
2418
|
+
assert result.has_errors()
|
|
2419
|
+
# Should have error about required field being dropped
|
|
2420
|
+
assert any("is a required field" in error for error in result.errors)
|