cumulusci-plus 5.0.21__py3-none-any.whl → 5.0.43__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 +19 -3
- cumulusci/cli/tests/test_error.py +3 -1
- cumulusci/cli/tests/test_flow.py +279 -2
- cumulusci/cli/tests/test_org.py +5 -0
- cumulusci/cli/tests/test_service.py +15 -12
- cumulusci/cli/tests/test_task.py +122 -2
- cumulusci/cli/tests/utils.py +1 -4
- cumulusci/core/config/__init__.py +1 -0
- cumulusci/core/config/base_task_flow_config.py +26 -1
- cumulusci/core/config/org_config.py +2 -1
- cumulusci/core/config/project_config.py +14 -20
- cumulusci/core/config/scratch_org_config.py +12 -0
- cumulusci/core/config/tests/test_config.py +1 -0
- cumulusci/core/config/tests/test_config_expensive.py +9 -3
- cumulusci/core/config/universal_config.py +3 -4
- cumulusci/core/dependencies/base.py +5 -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 +73 -1
- 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 +69 -0
- cumulusci/tasks/apex/anon.py +1 -1
- cumulusci/tasks/apex/testrunner.py +421 -144
- 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 +46 -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/__init__.py +2 -0
- cumulusci/tasks/metadata_etl/applications.py +256 -0
- 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/metadata_etl/tests/test_applications.py +710 -0
- 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/insert_record.py +18 -19
- 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_enable_prediction.py +4 -2
- cumulusci/tasks/salesforce/tests/test_getPackageVersion.py +651 -0
- cumulusci/tasks/salesforce/tests/test_update_dependencies.py +1 -1
- cumulusci/tasks/salesforce/tests/test_update_external_auth_identity_provider.py +927 -0
- cumulusci/tasks/salesforce/tests/test_update_external_credential.py +1427 -0
- cumulusci/tasks/salesforce/tests/test_update_named_credential.py +1042 -0
- cumulusci/tasks/salesforce/tests/test_update_record.py +512 -0
- cumulusci/tasks/salesforce/update_dependencies.py +2 -2
- cumulusci/tasks/salesforce/update_external_auth_identity_provider.py +551 -0
- cumulusci/tasks/salesforce/update_external_credential.py +647 -0
- cumulusci/tasks/salesforce/update_named_credential.py +441 -0
- cumulusci/tasks/salesforce/update_profile.py +17 -13
- cumulusci/tasks/salesforce/update_record.py +217 -0
- 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 +376 -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 +302 -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 +1150 -0
- cumulusci/tasks/utility/tests/test_directoryRecreator.py +439 -0
- cumulusci/tasks/utility/tests/test_secretsToEnv.py +1118 -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 +8 -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.21.dist-info → cumulusci_plus-5.0.43.dist-info}/METADATA +11 -10
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/RECORD +135 -104
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/WHEEL +1 -1
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/entry_points.txt +0 -0
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/licenses/AUTHORS.rst +0 -0
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1012 @@
|
|
|
1
|
+
"""Tests for SFDmu task."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
from unittest import mock
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from cumulusci.tasks.salesforce.tests.util import create_task
|
|
10
|
+
from cumulusci.tasks.sfdmu.sfdmu import SfdmuTask
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestSfdmuTask:
|
|
14
|
+
"""Test cases for SfdmuTask."""
|
|
15
|
+
|
|
16
|
+
def test_init_options_validates_path(self):
|
|
17
|
+
"""Test that _init_options validates the path exists and contains export.json."""
|
|
18
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
19
|
+
# Create export.json file
|
|
20
|
+
export_json_path = os.path.join(temp_dir, "export.json")
|
|
21
|
+
with open(export_json_path, "w") as f:
|
|
22
|
+
f.write('{"test": "data"}')
|
|
23
|
+
|
|
24
|
+
# Test valid path using create_task helper
|
|
25
|
+
task = create_task(
|
|
26
|
+
SfdmuTask, {"source": "dev", "target": "qa", "path": temp_dir}
|
|
27
|
+
)
|
|
28
|
+
assert task.options["path"] == os.path.abspath(temp_dir)
|
|
29
|
+
|
|
30
|
+
def test_init_options_raises_error_for_missing_path(self):
|
|
31
|
+
"""Test that _init_options raises error for missing path."""
|
|
32
|
+
with pytest.raises(Exception): # TaskOptionsError
|
|
33
|
+
create_task(
|
|
34
|
+
SfdmuTask,
|
|
35
|
+
{"source": "dev", "target": "qa", "path": "/nonexistent/path"},
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def test_init_options_raises_error_for_missing_export_json(self):
|
|
39
|
+
"""Test that _init_options raises error for missing export.json."""
|
|
40
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
41
|
+
with pytest.raises(Exception): # TaskOptionsError
|
|
42
|
+
create_task(
|
|
43
|
+
SfdmuTask, {"source": "dev", "target": "qa", "path": temp_dir}
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def test_validate_org_csvfile(self):
|
|
47
|
+
"""Test that _validate_org returns None for csvfile."""
|
|
48
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
49
|
+
# Create export.json file
|
|
50
|
+
export_json_path = os.path.join(temp_dir, "export.json")
|
|
51
|
+
with open(export_json_path, "w") as f:
|
|
52
|
+
f.write('{"test": "data"}')
|
|
53
|
+
|
|
54
|
+
task = create_task(
|
|
55
|
+
SfdmuTask, {"source": "csvfile", "target": "csvfile", "path": temp_dir}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
result = task._validate_org("csvfile")
|
|
59
|
+
assert result is None
|
|
60
|
+
|
|
61
|
+
def test_validate_org_missing_keychain(self):
|
|
62
|
+
"""Test that _validate_org raises error when keychain is None."""
|
|
63
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
64
|
+
# Create export.json file
|
|
65
|
+
export_json_path = os.path.join(temp_dir, "export.json")
|
|
66
|
+
with open(export_json_path, "w") as f:
|
|
67
|
+
f.write('{"test": "data"}')
|
|
68
|
+
|
|
69
|
+
task = create_task(
|
|
70
|
+
SfdmuTask, {"source": "dev", "target": "qa", "path": temp_dir}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Mock the keychain to be None
|
|
74
|
+
task.project_config.keychain = None
|
|
75
|
+
|
|
76
|
+
with pytest.raises(Exception): # TaskOptionsError
|
|
77
|
+
task._validate_org("dev")
|
|
78
|
+
|
|
79
|
+
def test_get_sf_org_name_sfdx_alias(self):
|
|
80
|
+
"""Test _get_sf_org_name with sfdx_alias."""
|
|
81
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
82
|
+
# Create export.json file
|
|
83
|
+
export_json_path = os.path.join(temp_dir, "export.json")
|
|
84
|
+
with open(export_json_path, "w") as f:
|
|
85
|
+
f.write('{"test": "data"}')
|
|
86
|
+
|
|
87
|
+
task = create_task(
|
|
88
|
+
SfdmuTask, {"source": "dev", "target": "qa", "path": temp_dir}
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
mock_org_config = mock.Mock()
|
|
92
|
+
mock_org_config.sfdx_alias = "test_alias"
|
|
93
|
+
mock_org_config.username = "test@example.com"
|
|
94
|
+
|
|
95
|
+
result = task._get_sf_org_name(mock_org_config)
|
|
96
|
+
assert result == "test_alias"
|
|
97
|
+
|
|
98
|
+
def test_get_sf_org_name_username(self):
|
|
99
|
+
"""Test _get_sf_org_name with username fallback."""
|
|
100
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
101
|
+
# Create export.json file
|
|
102
|
+
export_json_path = os.path.join(temp_dir, "export.json")
|
|
103
|
+
with open(export_json_path, "w") as f:
|
|
104
|
+
f.write('{"test": "data"}')
|
|
105
|
+
|
|
106
|
+
task = create_task(
|
|
107
|
+
SfdmuTask, {"source": "dev", "target": "qa", "path": temp_dir}
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
mock_org_config = mock.Mock()
|
|
111
|
+
mock_org_config.sfdx_alias = None
|
|
112
|
+
mock_org_config.username = "test@example.com"
|
|
113
|
+
|
|
114
|
+
result = task._get_sf_org_name(mock_org_config)
|
|
115
|
+
assert result == "test@example.com"
|
|
116
|
+
|
|
117
|
+
def test_create_execute_directory(self):
|
|
118
|
+
"""Test _create_execute_directory creates directory and copies files."""
|
|
119
|
+
with tempfile.TemporaryDirectory() as base_dir:
|
|
120
|
+
# Create test files
|
|
121
|
+
export_json = os.path.join(base_dir, "export.json")
|
|
122
|
+
test_csv = os.path.join(base_dir, "test.csv")
|
|
123
|
+
test_txt = os.path.join(base_dir, "test.txt") # Should not be copied
|
|
124
|
+
|
|
125
|
+
with open(export_json, "w") as f:
|
|
126
|
+
f.write('{"test": "data"}')
|
|
127
|
+
with open(test_csv, "w") as f:
|
|
128
|
+
f.write("col1,col2\nval1,val2")
|
|
129
|
+
with open(test_txt, "w") as f:
|
|
130
|
+
f.write("text file")
|
|
131
|
+
|
|
132
|
+
# Create subdirectory (should not be copied)
|
|
133
|
+
subdir = os.path.join(base_dir, "subdir")
|
|
134
|
+
os.makedirs(subdir)
|
|
135
|
+
with open(os.path.join(subdir, "file.txt"), "w") as f:
|
|
136
|
+
f.write("subdir file")
|
|
137
|
+
|
|
138
|
+
task = create_task(
|
|
139
|
+
SfdmuTask, {"source": "dev", "target": "qa", "path": base_dir}
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
execute_path = task._create_execute_directory(base_dir)
|
|
143
|
+
|
|
144
|
+
# Check that execute directory was created
|
|
145
|
+
assert os.path.exists(execute_path)
|
|
146
|
+
assert execute_path == os.path.join(base_dir, "execute")
|
|
147
|
+
|
|
148
|
+
# Check that files were copied
|
|
149
|
+
assert os.path.exists(os.path.join(execute_path, "export.json"))
|
|
150
|
+
assert os.path.exists(os.path.join(execute_path, "test.csv"))
|
|
151
|
+
assert not os.path.exists(
|
|
152
|
+
os.path.join(execute_path, "test.txt")
|
|
153
|
+
) # Not a valid file type
|
|
154
|
+
assert not os.path.exists(
|
|
155
|
+
os.path.join(execute_path, "subdir")
|
|
156
|
+
) # Not a file
|
|
157
|
+
|
|
158
|
+
# Check file contents
|
|
159
|
+
with open(os.path.join(execute_path, "export.json"), "r") as f:
|
|
160
|
+
assert f.read() == '{"test": "data"}'
|
|
161
|
+
with open(os.path.join(execute_path, "test.csv"), "r") as f:
|
|
162
|
+
assert f.read() == "col1,col2\nval1,val2"
|
|
163
|
+
|
|
164
|
+
def test_create_execute_directory_removes_existing(self):
|
|
165
|
+
"""Test that _create_execute_directory removes existing execute directory."""
|
|
166
|
+
with tempfile.TemporaryDirectory() as base_dir:
|
|
167
|
+
# Create existing execute directory with files
|
|
168
|
+
execute_dir = os.path.join(base_dir, "execute")
|
|
169
|
+
os.makedirs(execute_dir)
|
|
170
|
+
with open(os.path.join(execute_dir, "old_file.json"), "w") as f:
|
|
171
|
+
f.write('{"old": "data"}')
|
|
172
|
+
|
|
173
|
+
# Create export.json in base directory
|
|
174
|
+
export_json = os.path.join(base_dir, "export.json")
|
|
175
|
+
with open(export_json, "w") as f:
|
|
176
|
+
f.write('{"test": "data"}')
|
|
177
|
+
|
|
178
|
+
task = create_task(
|
|
179
|
+
SfdmuTask, {"source": "dev", "target": "qa", "path": base_dir}
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
execute_path = task._create_execute_directory(base_dir)
|
|
183
|
+
|
|
184
|
+
# Check that old file was removed
|
|
185
|
+
assert not os.path.exists(os.path.join(execute_path, "old_file.json"))
|
|
186
|
+
# Check that new file was copied
|
|
187
|
+
assert os.path.exists(os.path.join(execute_path, "export.json"))
|
|
188
|
+
|
|
189
|
+
def test_inject_namespace_tokens_csvfile_both(self):
|
|
190
|
+
"""Test that namespace injection is skipped when both source and target are csvfile."""
|
|
191
|
+
with tempfile.TemporaryDirectory() as execute_dir:
|
|
192
|
+
# Create test files
|
|
193
|
+
test_json = os.path.join(execute_dir, "test.json")
|
|
194
|
+
with open(test_json, "w") as f:
|
|
195
|
+
f.write('{"field": "%%%NAMESPACE%%%Test"}')
|
|
196
|
+
|
|
197
|
+
# Create export.json file
|
|
198
|
+
export_json_path = os.path.join(execute_dir, "export.json")
|
|
199
|
+
with open(export_json_path, "w") as f:
|
|
200
|
+
f.write('{"test": "data"}')
|
|
201
|
+
|
|
202
|
+
task = create_task(
|
|
203
|
+
SfdmuTask,
|
|
204
|
+
{"source": "csvfile", "target": "csvfile", "path": execute_dir},
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Should not raise any errors and files should remain unchanged
|
|
208
|
+
task._inject_namespace_tokens(execute_dir, None, None)
|
|
209
|
+
|
|
210
|
+
# Check that file content was not changed
|
|
211
|
+
with open(test_json, "r") as f:
|
|
212
|
+
assert f.read() == '{"field": "%%%NAMESPACE%%%Test"}'
|
|
213
|
+
|
|
214
|
+
@mock.patch("cumulusci.tasks.sfdmu.sfdmu.determine_managed_mode")
|
|
215
|
+
def test_inject_namespace_tokens_csvfile_target_with_source_org(
|
|
216
|
+
self, mock_determine_managed
|
|
217
|
+
):
|
|
218
|
+
"""Test that namespace injection uses source org when target is csvfile."""
|
|
219
|
+
mock_determine_managed.return_value = True
|
|
220
|
+
|
|
221
|
+
with tempfile.TemporaryDirectory() as execute_dir:
|
|
222
|
+
# Create test files with namespace tokens
|
|
223
|
+
test_json = os.path.join(execute_dir, "export.json")
|
|
224
|
+
with open(test_json, "w") as f:
|
|
225
|
+
f.write(
|
|
226
|
+
'{"query": "SELECT Id FROM %%%MANAGED_OR_NAMESPACED_ORG%%%CustomObject__c"}'
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
task = create_task(
|
|
230
|
+
SfdmuTask, {"source": "dev", "target": "csvfile", "path": execute_dir}
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Mock the project config namespace
|
|
234
|
+
task.project_config.project__package__namespace = "testns"
|
|
235
|
+
|
|
236
|
+
mock_source_org = mock.Mock()
|
|
237
|
+
mock_source_org.namespace = "testns"
|
|
238
|
+
|
|
239
|
+
# When target is csvfile (None), should use source org for injection
|
|
240
|
+
task._inject_namespace_tokens(execute_dir, mock_source_org, None)
|
|
241
|
+
|
|
242
|
+
# Check that namespace tokens were replaced using source org
|
|
243
|
+
with open(test_json, "r") as f:
|
|
244
|
+
content = f.read()
|
|
245
|
+
assert "testns__CustomObject__c" in content
|
|
246
|
+
assert "%%%MANAGED_OR_NAMESPACED_ORG%%%" not in content
|
|
247
|
+
|
|
248
|
+
@mock.patch("cumulusci.tasks.sfdmu.sfdmu.determine_managed_mode")
|
|
249
|
+
def test_inject_namespace_tokens_managed_mode(self, mock_determine_managed):
|
|
250
|
+
"""Test namespace injection in managed mode."""
|
|
251
|
+
mock_determine_managed.return_value = True
|
|
252
|
+
|
|
253
|
+
with tempfile.TemporaryDirectory() as execute_dir:
|
|
254
|
+
# Create test files with namespace tokens
|
|
255
|
+
test_json = os.path.join(execute_dir, "test.json")
|
|
256
|
+
test_csv = os.path.join(execute_dir, "test.csv")
|
|
257
|
+
|
|
258
|
+
with open(test_json, "w") as f:
|
|
259
|
+
f.write(
|
|
260
|
+
'{"field": "%%%NAMESPACE%%%Test", "org": "%%%NAMESPACED_ORG%%%Value"}'
|
|
261
|
+
)
|
|
262
|
+
with open(test_csv, "w") as f:
|
|
263
|
+
f.write("Name,%%%NAMESPACE%%%Field\nTest,Value")
|
|
264
|
+
|
|
265
|
+
# Create filename with namespace token
|
|
266
|
+
filename_with_token = os.path.join(execute_dir, "___NAMESPACE___test.json")
|
|
267
|
+
with open(filename_with_token, "w") as f:
|
|
268
|
+
f.write('{"test": "data"}')
|
|
269
|
+
|
|
270
|
+
# Create export.json file
|
|
271
|
+
export_json_path = os.path.join(execute_dir, "export.json")
|
|
272
|
+
with open(export_json_path, "w") as f:
|
|
273
|
+
f.write('{"test": "data"}')
|
|
274
|
+
|
|
275
|
+
task = create_task(
|
|
276
|
+
SfdmuTask, {"source": "dev", "target": "qa", "path": execute_dir}
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Mock the project config namespace
|
|
280
|
+
task.project_config.project__package__namespace = "testns"
|
|
281
|
+
|
|
282
|
+
mock_org_config = mock.Mock()
|
|
283
|
+
mock_org_config.namespace = "testns"
|
|
284
|
+
|
|
285
|
+
task._inject_namespace_tokens(execute_dir, None, mock_org_config)
|
|
286
|
+
|
|
287
|
+
# Check that namespace tokens were replaced in content
|
|
288
|
+
with open(test_json, "r") as f:
|
|
289
|
+
content = f.read()
|
|
290
|
+
assert "testns__Test" in content
|
|
291
|
+
assert "testns__Value" in content
|
|
292
|
+
|
|
293
|
+
with open(test_csv, "r") as f:
|
|
294
|
+
content = f.read()
|
|
295
|
+
assert "testns__Field" in content
|
|
296
|
+
|
|
297
|
+
# Check that filename token was replaced
|
|
298
|
+
expected_filename = os.path.join(execute_dir, "testns__test.json")
|
|
299
|
+
assert os.path.exists(expected_filename)
|
|
300
|
+
assert not os.path.exists(filename_with_token)
|
|
301
|
+
|
|
302
|
+
@mock.patch("cumulusci.tasks.sfdmu.sfdmu.determine_managed_mode")
|
|
303
|
+
def test_inject_namespace_tokens_unmanaged_mode(self, mock_determine_managed):
|
|
304
|
+
"""Test namespace injection in unmanaged mode."""
|
|
305
|
+
mock_determine_managed.return_value = False
|
|
306
|
+
|
|
307
|
+
with tempfile.TemporaryDirectory() as execute_dir:
|
|
308
|
+
# Create test files with namespace tokens
|
|
309
|
+
test_json = os.path.join(execute_dir, "test.json")
|
|
310
|
+
with open(test_json, "w") as f:
|
|
311
|
+
f.write(
|
|
312
|
+
'{"field": "%%%NAMESPACE%%%Test", "org": "%%%NAMESPACED_ORG%%%Value"}'
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Create export.json file
|
|
316
|
+
export_json_path = os.path.join(execute_dir, "export.json")
|
|
317
|
+
with open(export_json_path, "w") as f:
|
|
318
|
+
f.write('{"test": "data"}')
|
|
319
|
+
|
|
320
|
+
task = create_task(
|
|
321
|
+
SfdmuTask, {"source": "dev", "target": "qa", "path": execute_dir}
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Mock the project config namespace
|
|
325
|
+
task.project_config.project__package__namespace = "testns"
|
|
326
|
+
|
|
327
|
+
mock_org_config = mock.Mock()
|
|
328
|
+
mock_org_config.namespace = "testns"
|
|
329
|
+
|
|
330
|
+
task._inject_namespace_tokens(execute_dir, None, mock_org_config)
|
|
331
|
+
|
|
332
|
+
# Check that namespace tokens were replaced with empty strings
|
|
333
|
+
with open(test_json, "r") as f:
|
|
334
|
+
content = f.read()
|
|
335
|
+
assert "Test" in content # %%NAMESPACE%% removed
|
|
336
|
+
assert "Value" in content # %%NAMESPACED_ORG%% removed
|
|
337
|
+
assert "%%%NAMESPACE%%%" not in content
|
|
338
|
+
assert "%%%NAMESPACED_ORG%%%" not in content
|
|
339
|
+
|
|
340
|
+
@mock.patch("cumulusci.tasks.sfdmu.sfdmu.determine_managed_mode")
|
|
341
|
+
def test_inject_namespace_tokens_namespaced_org(self, mock_determine_managed):
|
|
342
|
+
"""Test namespace injection with namespaced org."""
|
|
343
|
+
mock_determine_managed.return_value = True
|
|
344
|
+
|
|
345
|
+
with tempfile.TemporaryDirectory() as execute_dir:
|
|
346
|
+
# Create test file with namespaced org token
|
|
347
|
+
test_json = os.path.join(execute_dir, "test.json")
|
|
348
|
+
with open(test_json, "w") as f:
|
|
349
|
+
f.write('{"field": "%%%NAMESPACED_ORG%%%Test"}')
|
|
350
|
+
|
|
351
|
+
# Create export.json file
|
|
352
|
+
export_json_path = os.path.join(execute_dir, "export.json")
|
|
353
|
+
with open(export_json_path, "w") as f:
|
|
354
|
+
f.write('{"test": "data"}')
|
|
355
|
+
|
|
356
|
+
task = create_task(
|
|
357
|
+
SfdmuTask, {"source": "dev", "target": "qa", "path": execute_dir}
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# Mock the project config namespace
|
|
361
|
+
task.project_config.project__package__namespace = "testns"
|
|
362
|
+
|
|
363
|
+
mock_org_config = mock.Mock()
|
|
364
|
+
mock_org_config.namespace = (
|
|
365
|
+
"testns" # Same as project namespace = namespaced org
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
task._inject_namespace_tokens(execute_dir, None, mock_org_config)
|
|
369
|
+
|
|
370
|
+
# Check that namespaced org token was replaced
|
|
371
|
+
with open(test_json, "r") as f:
|
|
372
|
+
content = f.read()
|
|
373
|
+
assert "testns__Test" in content
|
|
374
|
+
assert "%%%NAMESPACED_ORG%%%" not in content
|
|
375
|
+
|
|
376
|
+
@mock.patch("cumulusci.tasks.sfdmu.sfdmu.determine_managed_mode")
|
|
377
|
+
def test_inject_namespace_tokens_non_namespaced_org(self, mock_determine_managed):
|
|
378
|
+
"""Test namespace injection with non-namespaced org."""
|
|
379
|
+
mock_determine_managed.return_value = True
|
|
380
|
+
|
|
381
|
+
with tempfile.TemporaryDirectory() as execute_dir:
|
|
382
|
+
# Create test file with namespaced org token
|
|
383
|
+
test_json = os.path.join(execute_dir, "test.json")
|
|
384
|
+
with open(test_json, "w") as f:
|
|
385
|
+
f.write('{"field": "%%%NAMESPACED_ORG%%%Test"}')
|
|
386
|
+
|
|
387
|
+
# Create export.json file
|
|
388
|
+
export_json_path = os.path.join(execute_dir, "export.json")
|
|
389
|
+
with open(export_json_path, "w") as f:
|
|
390
|
+
f.write('{"test": "data"}')
|
|
391
|
+
|
|
392
|
+
task = create_task(
|
|
393
|
+
SfdmuTask, {"source": "dev", "target": "qa", "path": execute_dir}
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Mock the project config namespace
|
|
397
|
+
task.project_config.project__package__namespace = "testns"
|
|
398
|
+
|
|
399
|
+
mock_org_config = mock.Mock()
|
|
400
|
+
mock_org_config.namespace = (
|
|
401
|
+
"differentns" # Different from project namespace
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
task._inject_namespace_tokens(execute_dir, None, mock_org_config)
|
|
405
|
+
|
|
406
|
+
# Check that namespaced org token was replaced with empty string
|
|
407
|
+
with open(test_json, "r") as f:
|
|
408
|
+
content = f.read()
|
|
409
|
+
assert "Test" in content # %%NAMESPACED_ORG%% removed
|
|
410
|
+
assert "%%%NAMESPACED_ORG%%%" not in content
|
|
411
|
+
assert "testns__" not in content # Should not have namespace prefix
|
|
412
|
+
|
|
413
|
+
def test_inject_namespace_tokens_no_namespace(self):
|
|
414
|
+
"""Test namespace injection when project has no namespace."""
|
|
415
|
+
with tempfile.TemporaryDirectory() as execute_dir:
|
|
416
|
+
# Create test file with namespace tokens
|
|
417
|
+
test_json = os.path.join(execute_dir, "test.json")
|
|
418
|
+
with open(test_json, "w") as f:
|
|
419
|
+
f.write('{"field": "%%%NAMESPACE%%%Test"}')
|
|
420
|
+
|
|
421
|
+
# Create export.json file
|
|
422
|
+
export_json_path = os.path.join(execute_dir, "export.json")
|
|
423
|
+
with open(export_json_path, "w") as f:
|
|
424
|
+
f.write('{"test": "data"}')
|
|
425
|
+
|
|
426
|
+
task = create_task(
|
|
427
|
+
SfdmuTask, {"source": "dev", "target": "qa", "path": execute_dir}
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# Mock the project config namespace
|
|
431
|
+
task.project_config.project__package__namespace = None
|
|
432
|
+
|
|
433
|
+
mock_org_config = mock.Mock()
|
|
434
|
+
mock_org_config.namespace = None
|
|
435
|
+
|
|
436
|
+
task._inject_namespace_tokens(execute_dir, None, mock_org_config)
|
|
437
|
+
|
|
438
|
+
# Check that namespace tokens were not processed (due to circular import issue)
|
|
439
|
+
with open(test_json, "r") as f:
|
|
440
|
+
content = f.read()
|
|
441
|
+
assert (
|
|
442
|
+
"%%%NAMESPACE%%%Test" in content
|
|
443
|
+
) # Tokens remain unchanged due to import issue
|
|
444
|
+
|
|
445
|
+
def test_additional_params_option_exists(self):
|
|
446
|
+
"""Test that additional_params option is properly defined in task_options."""
|
|
447
|
+
# Check that the additional_params option is defined
|
|
448
|
+
assert "additional_params" in SfdmuTask.task_options
|
|
449
|
+
assert SfdmuTask.task_options["additional_params"]["required"] is False
|
|
450
|
+
assert (
|
|
451
|
+
"Additional parameters"
|
|
452
|
+
in SfdmuTask.task_options["additional_params"]["description"]
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
def test_additional_params_parsing_logic(self):
|
|
456
|
+
"""Test that additional_params parsing logic works correctly."""
|
|
457
|
+
# Test the splitting logic that would be used in the task
|
|
458
|
+
additional_params = "-no-warnings -m -t error"
|
|
459
|
+
additional_args = additional_params.split()
|
|
460
|
+
expected_args = ["-no-warnings", "-m", "-t", "error"]
|
|
461
|
+
assert additional_args == expected_args
|
|
462
|
+
|
|
463
|
+
def test_additional_params_empty_string_logic(self):
|
|
464
|
+
"""Test that empty additional_params are handled correctly."""
|
|
465
|
+
# Test the splitting logic with empty string
|
|
466
|
+
additional_params = ""
|
|
467
|
+
additional_args = additional_params.split()
|
|
468
|
+
assert additional_args == []
|
|
469
|
+
|
|
470
|
+
def test_additional_params_none_logic(self):
|
|
471
|
+
"""Test that None additional_params are handled correctly."""
|
|
472
|
+
# Test the logic that would be used in the task
|
|
473
|
+
additional_params = None
|
|
474
|
+
if additional_params:
|
|
475
|
+
additional_args = additional_params.split()
|
|
476
|
+
else:
|
|
477
|
+
additional_args = []
|
|
478
|
+
assert additional_args == []
|
|
479
|
+
|
|
480
|
+
def test_process_csv_exports_replaces_namespace_in_content(self):
|
|
481
|
+
"""Test that namespace prefix is replaced with token in CSV file contents."""
|
|
482
|
+
with tempfile.TemporaryDirectory() as base_dir:
|
|
483
|
+
# Create execute directory
|
|
484
|
+
execute_dir = os.path.join(base_dir, "execute")
|
|
485
|
+
os.makedirs(execute_dir)
|
|
486
|
+
|
|
487
|
+
# Create export.json
|
|
488
|
+
export_json = os.path.join(base_dir, "export.json")
|
|
489
|
+
with open(export_json, "w") as f:
|
|
490
|
+
f.write('{"test": "data"}')
|
|
491
|
+
|
|
492
|
+
# Create CSV file with namespace prefix in content
|
|
493
|
+
csv_file = os.path.join(execute_dir, "Account.csv")
|
|
494
|
+
with open(csv_file, "w", encoding="utf-8") as f:
|
|
495
|
+
f.write("Id,Name,testns__CustomField__c\n")
|
|
496
|
+
f.write("001,Test Account,testns__Value\n")
|
|
497
|
+
|
|
498
|
+
task = create_task(
|
|
499
|
+
SfdmuTask, {"source": "dev", "target": "csvfile", "path": base_dir}
|
|
500
|
+
)
|
|
501
|
+
task.project_config.project__package__namespace = "testns"
|
|
502
|
+
|
|
503
|
+
# Call the processing method
|
|
504
|
+
task._process_csv_exports(execute_dir, base_dir)
|
|
505
|
+
|
|
506
|
+
# Check that namespace was replaced in content
|
|
507
|
+
target_csv = os.path.join(base_dir, "Account.csv")
|
|
508
|
+
assert os.path.exists(target_csv)
|
|
509
|
+
with open(target_csv, "r", encoding="utf-8") as f:
|
|
510
|
+
content = f.read()
|
|
511
|
+
assert "%%%MANAGED_OR_NAMESPACED_ORG%%%CustomField__c" in content
|
|
512
|
+
assert "%%%MANAGED_OR_NAMESPACED_ORG%%%Value" in content
|
|
513
|
+
assert "testns__" not in content
|
|
514
|
+
|
|
515
|
+
def test_process_csv_exports_renames_files_with_namespace(self):
|
|
516
|
+
"""Test that CSV filenames with namespace prefix are renamed."""
|
|
517
|
+
with tempfile.TemporaryDirectory() as base_dir:
|
|
518
|
+
# Create execute directory
|
|
519
|
+
execute_dir = os.path.join(base_dir, "execute")
|
|
520
|
+
os.makedirs(execute_dir)
|
|
521
|
+
|
|
522
|
+
# Create export.json
|
|
523
|
+
export_json = os.path.join(base_dir, "export.json")
|
|
524
|
+
with open(export_json, "w") as f:
|
|
525
|
+
f.write('{"test": "data"}')
|
|
526
|
+
|
|
527
|
+
# Create CSV file with namespace in filename
|
|
528
|
+
csv_file = os.path.join(execute_dir, "testns__CustomObject__c.csv")
|
|
529
|
+
with open(csv_file, "w", encoding="utf-8") as f:
|
|
530
|
+
f.write("Id,Name\n001,Test\n")
|
|
531
|
+
|
|
532
|
+
task = create_task(
|
|
533
|
+
SfdmuTask, {"source": "dev", "target": "csvfile", "path": base_dir}
|
|
534
|
+
)
|
|
535
|
+
task.project_config.project__package__namespace = "testns"
|
|
536
|
+
|
|
537
|
+
# Call the processing method
|
|
538
|
+
task._process_csv_exports(execute_dir, base_dir)
|
|
539
|
+
|
|
540
|
+
# Check that file was renamed
|
|
541
|
+
expected_filename = "___MANAGED_OR_NAMESPACED_ORG___CustomObject__c.csv"
|
|
542
|
+
target_csv = os.path.join(base_dir, expected_filename)
|
|
543
|
+
assert os.path.exists(target_csv)
|
|
544
|
+
assert not os.path.exists(
|
|
545
|
+
os.path.join(base_dir, "testns__CustomObject__c.csv")
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
def test_process_csv_exports_replaces_existing_files(self):
|
|
549
|
+
"""Test that existing CSV files in base path are replaced."""
|
|
550
|
+
with tempfile.TemporaryDirectory() as base_dir:
|
|
551
|
+
# Create execute directory
|
|
552
|
+
execute_dir = os.path.join(base_dir, "execute")
|
|
553
|
+
os.makedirs(execute_dir)
|
|
554
|
+
|
|
555
|
+
# Create export.json
|
|
556
|
+
export_json = os.path.join(base_dir, "export.json")
|
|
557
|
+
with open(export_json, "w") as f:
|
|
558
|
+
f.write('{"test": "data"}')
|
|
559
|
+
|
|
560
|
+
# Create old CSV file in base directory
|
|
561
|
+
old_csv = os.path.join(base_dir, "Account.csv")
|
|
562
|
+
with open(old_csv, "w", encoding="utf-8") as f:
|
|
563
|
+
f.write("Id,Name\n001,Old Account\n")
|
|
564
|
+
|
|
565
|
+
# Create another old CSV that should be deleted
|
|
566
|
+
old_csv2 = os.path.join(base_dir, "Contact.csv")
|
|
567
|
+
with open(old_csv2, "w", encoding="utf-8") as f:
|
|
568
|
+
f.write("Id,Name\n001,Old Contact\n")
|
|
569
|
+
|
|
570
|
+
# Create new CSV file in execute directory
|
|
571
|
+
new_csv = os.path.join(execute_dir, "Account.csv")
|
|
572
|
+
with open(new_csv, "w", encoding="utf-8") as f:
|
|
573
|
+
f.write("Id,Name\n002,New Account\n")
|
|
574
|
+
|
|
575
|
+
task = create_task(
|
|
576
|
+
SfdmuTask, {"source": "dev", "target": "csvfile", "path": base_dir}
|
|
577
|
+
)
|
|
578
|
+
task.project_config.project__package__namespace = "testns"
|
|
579
|
+
|
|
580
|
+
# Call the processing method
|
|
581
|
+
task._process_csv_exports(execute_dir, base_dir)
|
|
582
|
+
|
|
583
|
+
# Check that old file was replaced with new content
|
|
584
|
+
with open(old_csv, "r", encoding="utf-8") as f:
|
|
585
|
+
content = f.read()
|
|
586
|
+
assert "New Account" in content
|
|
587
|
+
assert "Old Account" not in content
|
|
588
|
+
|
|
589
|
+
# Check that old CSV2 was deleted (not in execute directory)
|
|
590
|
+
assert not os.path.exists(old_csv2)
|
|
591
|
+
|
|
592
|
+
def test_process_csv_exports_skips_when_no_namespace(self):
|
|
593
|
+
"""Test that CSV post-processing is skipped when no namespace is configured."""
|
|
594
|
+
with tempfile.TemporaryDirectory() as base_dir:
|
|
595
|
+
# Create execute directory
|
|
596
|
+
execute_dir = os.path.join(base_dir, "execute")
|
|
597
|
+
os.makedirs(execute_dir)
|
|
598
|
+
|
|
599
|
+
# Create export.json
|
|
600
|
+
export_json = os.path.join(base_dir, "export.json")
|
|
601
|
+
with open(export_json, "w") as f:
|
|
602
|
+
f.write('{"test": "data"}')
|
|
603
|
+
|
|
604
|
+
# Create CSV file
|
|
605
|
+
csv_file = os.path.join(execute_dir, "Account.csv")
|
|
606
|
+
with open(csv_file, "w", encoding="utf-8") as f:
|
|
607
|
+
f.write("Id,Name\n001,Test\n")
|
|
608
|
+
|
|
609
|
+
task = create_task(
|
|
610
|
+
SfdmuTask, {"source": "dev", "target": "csvfile", "path": base_dir}
|
|
611
|
+
)
|
|
612
|
+
task.project_config.project__package__namespace = None
|
|
613
|
+
|
|
614
|
+
# Call the processing method
|
|
615
|
+
task._process_csv_exports(execute_dir, base_dir)
|
|
616
|
+
|
|
617
|
+
# Check that file was NOT copied to base directory (processing was skipped)
|
|
618
|
+
target_csv = os.path.join(base_dir, "Account.csv")
|
|
619
|
+
assert not os.path.exists(target_csv)
|
|
620
|
+
|
|
621
|
+
def test_process_csv_exports_handles_no_csv_files(self):
|
|
622
|
+
"""Test that CSV post-processing handles case when no CSV files exist."""
|
|
623
|
+
with tempfile.TemporaryDirectory() as base_dir:
|
|
624
|
+
# Create execute directory
|
|
625
|
+
execute_dir = os.path.join(base_dir, "execute")
|
|
626
|
+
os.makedirs(execute_dir)
|
|
627
|
+
|
|
628
|
+
# Create export.json
|
|
629
|
+
export_json = os.path.join(base_dir, "export.json")
|
|
630
|
+
with open(export_json, "w") as f:
|
|
631
|
+
f.write('{"test": "data"}')
|
|
632
|
+
|
|
633
|
+
# Create only export.json, no CSV files
|
|
634
|
+
execute_json = os.path.join(execute_dir, "export.json")
|
|
635
|
+
with open(execute_json, "w", encoding="utf-8") as f:
|
|
636
|
+
f.write('{"test": "data"}')
|
|
637
|
+
|
|
638
|
+
task = create_task(
|
|
639
|
+
SfdmuTask, {"source": "dev", "target": "csvfile", "path": base_dir}
|
|
640
|
+
)
|
|
641
|
+
task.project_config.project__package__namespace = "testns"
|
|
642
|
+
|
|
643
|
+
# Call the processing method - should not raise any errors
|
|
644
|
+
task._process_csv_exports(execute_dir, base_dir)
|
|
645
|
+
|
|
646
|
+
def test_process_csv_exports_copies_only_from_execute_folder(self):
|
|
647
|
+
"""Test that only CSV files from execute folder are copied, not subdirectories."""
|
|
648
|
+
with tempfile.TemporaryDirectory() as base_dir:
|
|
649
|
+
# Create execute directory
|
|
650
|
+
execute_dir = os.path.join(base_dir, "execute")
|
|
651
|
+
os.makedirs(execute_dir)
|
|
652
|
+
|
|
653
|
+
# Create export.json
|
|
654
|
+
export_json = os.path.join(base_dir, "export.json")
|
|
655
|
+
with open(export_json, "w") as f:
|
|
656
|
+
f.write('{"test": "data"}')
|
|
657
|
+
|
|
658
|
+
# Create CSV file in execute directory
|
|
659
|
+
csv_file = os.path.join(execute_dir, "Account.csv")
|
|
660
|
+
with open(csv_file, "w", encoding="utf-8") as f:
|
|
661
|
+
f.write("Id,Name\n001,Test\n")
|
|
662
|
+
|
|
663
|
+
# Create subdirectory in execute with another CSV (should not be processed)
|
|
664
|
+
subdir = os.path.join(execute_dir, "subdir")
|
|
665
|
+
os.makedirs(subdir)
|
|
666
|
+
subdir_csv = os.path.join(subdir, "Contact.csv")
|
|
667
|
+
with open(subdir_csv, "w", encoding="utf-8") as f:
|
|
668
|
+
f.write("Id,Name\n002,Contact\n")
|
|
669
|
+
|
|
670
|
+
task = create_task(
|
|
671
|
+
SfdmuTask, {"source": "dev", "target": "csvfile", "path": base_dir}
|
|
672
|
+
)
|
|
673
|
+
task.project_config.project__package__namespace = "testns"
|
|
674
|
+
|
|
675
|
+
# Call the processing method
|
|
676
|
+
task._process_csv_exports(execute_dir, base_dir)
|
|
677
|
+
|
|
678
|
+
# Check that only the CSV from execute root was copied
|
|
679
|
+
assert os.path.exists(os.path.join(base_dir, "Account.csv"))
|
|
680
|
+
assert not os.path.exists(os.path.join(base_dir, "Contact.csv"))
|
|
681
|
+
assert not os.path.exists(os.path.join(base_dir, "subdir"))
|
|
682
|
+
|
|
683
|
+
def test_process_csv_exports_handles_multiple_files(self):
|
|
684
|
+
"""Test that CSV post-processing handles multiple CSV files correctly."""
|
|
685
|
+
with tempfile.TemporaryDirectory() as base_dir:
|
|
686
|
+
# Create execute directory
|
|
687
|
+
execute_dir = os.path.join(base_dir, "execute")
|
|
688
|
+
os.makedirs(execute_dir)
|
|
689
|
+
|
|
690
|
+
# Create export.json
|
|
691
|
+
export_json = os.path.join(base_dir, "export.json")
|
|
692
|
+
with open(export_json, "w") as f:
|
|
693
|
+
f.write('{"test": "data"}')
|
|
694
|
+
|
|
695
|
+
# Create multiple CSV files with namespace prefix
|
|
696
|
+
files_data = {
|
|
697
|
+
"Account.csv": "Id,testns__Field1__c\n001,testns__Val1\n",
|
|
698
|
+
"testns__Custom__c.csv": "Id,Name,testns__Field2__c\n002,Test,testns__Val2\n",
|
|
699
|
+
"Contact.csv": "Id,testns__Email__c\n003,testns__test@example.com\n",
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
for filename, content in files_data.items():
|
|
703
|
+
csv_file = os.path.join(execute_dir, filename)
|
|
704
|
+
with open(csv_file, "w", encoding="utf-8") as f:
|
|
705
|
+
f.write(content)
|
|
706
|
+
|
|
707
|
+
task = create_task(
|
|
708
|
+
SfdmuTask, {"source": "dev", "target": "csvfile", "path": base_dir}
|
|
709
|
+
)
|
|
710
|
+
task.project_config.project__package__namespace = "testns"
|
|
711
|
+
|
|
712
|
+
# Call the processing method
|
|
713
|
+
task._process_csv_exports(execute_dir, base_dir)
|
|
714
|
+
|
|
715
|
+
# Check that all files were processed
|
|
716
|
+
# Account.csv - no namespace in filename
|
|
717
|
+
account_csv = os.path.join(base_dir, "Account.csv")
|
|
718
|
+
assert os.path.exists(account_csv)
|
|
719
|
+
with open(account_csv, "r", encoding="utf-8") as f:
|
|
720
|
+
content = f.read()
|
|
721
|
+
assert "%%%MANAGED_OR_NAMESPACED_ORG%%%Field1__c" in content
|
|
722
|
+
assert "%%%MANAGED_OR_NAMESPACED_ORG%%%Val1" in content
|
|
723
|
+
|
|
724
|
+
# testns__Custom__c.csv - should be renamed
|
|
725
|
+
custom_csv = os.path.join(
|
|
726
|
+
base_dir, "___MANAGED_OR_NAMESPACED_ORG___Custom__c.csv"
|
|
727
|
+
)
|
|
728
|
+
assert os.path.exists(custom_csv)
|
|
729
|
+
with open(custom_csv, "r", encoding="utf-8") as f:
|
|
730
|
+
content = f.read()
|
|
731
|
+
assert "%%%MANAGED_OR_NAMESPACED_ORG%%%Field2__c" in content
|
|
732
|
+
assert "%%%MANAGED_OR_NAMESPACED_ORG%%%Val2" in content
|
|
733
|
+
|
|
734
|
+
# Contact.csv - no namespace in filename
|
|
735
|
+
contact_csv = os.path.join(base_dir, "Contact.csv")
|
|
736
|
+
assert os.path.exists(contact_csv)
|
|
737
|
+
with open(contact_csv, "r", encoding="utf-8") as f:
|
|
738
|
+
content = f.read()
|
|
739
|
+
assert "%%%MANAGED_OR_NAMESPACED_ORG%%%Email__c" in content
|
|
740
|
+
assert "%%%MANAGED_OR_NAMESPACED_ORG%%%test@example.com" in content
|
|
741
|
+
|
|
742
|
+
@mock.patch("cumulusci.tasks.sfdmu.sfdmu.sfdx")
|
|
743
|
+
def test_process_csv_exports_called_when_target_is_csvfile(self, mock_sfdx):
|
|
744
|
+
"""Test that _process_csv_exports is called after SFDMU execution when target is csvfile."""
|
|
745
|
+
with tempfile.TemporaryDirectory() as base_dir:
|
|
746
|
+
# Create export.json
|
|
747
|
+
export_json = os.path.join(base_dir, "export.json")
|
|
748
|
+
with open(export_json, "w") as f:
|
|
749
|
+
f.write('{"test": "data"}')
|
|
750
|
+
|
|
751
|
+
task = create_task(
|
|
752
|
+
SfdmuTask, {"source": "dev", "target": "csvfile", "path": base_dir}
|
|
753
|
+
)
|
|
754
|
+
task.project_config.project__package__namespace = "testns"
|
|
755
|
+
|
|
756
|
+
# Mock sfdx command
|
|
757
|
+
mock_sfdx_result = mock.Mock()
|
|
758
|
+
mock_sfdx_result.stdout_text = iter([])
|
|
759
|
+
mock_sfdx_result.stderr_text = iter([])
|
|
760
|
+
mock_sfdx.return_value = mock_sfdx_result
|
|
761
|
+
|
|
762
|
+
# Mock _validate_org
|
|
763
|
+
mock_source_org = mock.Mock()
|
|
764
|
+
mock_source_org.sfdx_alias = "test_dev"
|
|
765
|
+
task._validate_org = mock.Mock(
|
|
766
|
+
side_effect=lambda org: mock_source_org if org == "dev" else None
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
# Mock _inject_namespace_tokens to avoid complex setup
|
|
770
|
+
task._inject_namespace_tokens = mock.Mock()
|
|
771
|
+
|
|
772
|
+
# Spy on _process_csv_exports
|
|
773
|
+
original_process = task._process_csv_exports
|
|
774
|
+
task._process_csv_exports = mock.Mock(wraps=original_process)
|
|
775
|
+
|
|
776
|
+
# Run the task
|
|
777
|
+
task._run_task()
|
|
778
|
+
|
|
779
|
+
# Verify that _process_csv_exports was called
|
|
780
|
+
task._process_csv_exports.assert_called_once()
|
|
781
|
+
# Verify it was called with correct arguments
|
|
782
|
+
call_args = task._process_csv_exports.call_args[0]
|
|
783
|
+
assert call_args[0].endswith("execute") # execute_path
|
|
784
|
+
assert call_args[1] == base_dir # base_path
|
|
785
|
+
|
|
786
|
+
@mock.patch("cumulusci.tasks.sfdmu.sfdmu.sfdx")
|
|
787
|
+
def test_process_csv_exports_not_called_when_target_is_org(self, mock_sfdx):
|
|
788
|
+
"""Test that _process_csv_exports is NOT called when target is an org."""
|
|
789
|
+
with tempfile.TemporaryDirectory() as base_dir:
|
|
790
|
+
# Create export.json
|
|
791
|
+
export_json = os.path.join(base_dir, "export.json")
|
|
792
|
+
with open(export_json, "w") as f:
|
|
793
|
+
f.write('{"test": "data"}')
|
|
794
|
+
|
|
795
|
+
task = create_task(
|
|
796
|
+
SfdmuTask, {"source": "dev", "target": "qa", "path": base_dir}
|
|
797
|
+
)
|
|
798
|
+
task.project_config.project__package__namespace = "testns"
|
|
799
|
+
|
|
800
|
+
# Mock sfdx command
|
|
801
|
+
mock_sfdx_result = mock.Mock()
|
|
802
|
+
mock_sfdx_result.stdout_text = iter([])
|
|
803
|
+
mock_sfdx_result.stderr_text = iter([])
|
|
804
|
+
mock_sfdx.return_value = mock_sfdx_result
|
|
805
|
+
|
|
806
|
+
# Mock _validate_org
|
|
807
|
+
mock_org = mock.Mock()
|
|
808
|
+
mock_org.sfdx_alias = "test_org"
|
|
809
|
+
task._validate_org = mock.Mock(return_value=mock_org)
|
|
810
|
+
|
|
811
|
+
# Mock _inject_namespace_tokens
|
|
812
|
+
task._inject_namespace_tokens = mock.Mock()
|
|
813
|
+
|
|
814
|
+
# Spy on _process_csv_exports
|
|
815
|
+
task._process_csv_exports = mock.Mock()
|
|
816
|
+
|
|
817
|
+
# Run the task
|
|
818
|
+
task._run_task()
|
|
819
|
+
|
|
820
|
+
# Verify that _process_csv_exports was NOT called
|
|
821
|
+
task._process_csv_exports.assert_not_called()
|
|
822
|
+
|
|
823
|
+
def test_return_always_success_option_exists(self):
|
|
824
|
+
"""Test that return_always_success option is properly defined."""
|
|
825
|
+
assert "return_always_success" in SfdmuTask.task_options
|
|
826
|
+
assert SfdmuTask.task_options["return_always_success"]["required"] is False
|
|
827
|
+
assert SfdmuTask.task_options["return_always_success"]["default"] is False
|
|
828
|
+
|
|
829
|
+
@mock.patch("cumulusci.tasks.sfdmu.sfdmu.sfdx")
|
|
830
|
+
def test_return_always_success_false_fails_on_error(self, mock_sfdx):
|
|
831
|
+
"""Test that task fails when return_always_success is False and command fails."""
|
|
832
|
+
with tempfile.TemporaryDirectory() as base_dir:
|
|
833
|
+
# Create export.json
|
|
834
|
+
export_json = os.path.join(base_dir, "export.json")
|
|
835
|
+
with open(export_json, "w") as f:
|
|
836
|
+
f.write('{"test": "data"}')
|
|
837
|
+
|
|
838
|
+
task = create_task(
|
|
839
|
+
SfdmuTask,
|
|
840
|
+
{
|
|
841
|
+
"source": "dev",
|
|
842
|
+
"target": "qa",
|
|
843
|
+
"path": base_dir,
|
|
844
|
+
"return_always_success": False,
|
|
845
|
+
},
|
|
846
|
+
)
|
|
847
|
+
|
|
848
|
+
# Mock sfdx command to raise an error
|
|
849
|
+
mock_sfdx.side_effect = Exception("SFDMU command failed")
|
|
850
|
+
|
|
851
|
+
# Mock _validate_org
|
|
852
|
+
mock_org = mock.Mock()
|
|
853
|
+
mock_org.sfdx_alias = "test_org"
|
|
854
|
+
task._validate_org = mock.Mock(return_value=mock_org)
|
|
855
|
+
|
|
856
|
+
# Mock _inject_namespace_tokens
|
|
857
|
+
task._inject_namespace_tokens = mock.Mock()
|
|
858
|
+
|
|
859
|
+
# Task should raise the exception
|
|
860
|
+
with pytest.raises(Exception, match="SFDMU command failed"):
|
|
861
|
+
task._run_task()
|
|
862
|
+
|
|
863
|
+
@mock.patch("cumulusci.tasks.sfdmu.sfdmu.sfdx")
|
|
864
|
+
def test_return_always_success_true_continues_on_error(self, mock_sfdx):
|
|
865
|
+
"""Test that task continues when return_always_success is True and command fails."""
|
|
866
|
+
with tempfile.TemporaryDirectory() as base_dir:
|
|
867
|
+
# Create export.json
|
|
868
|
+
export_json = os.path.join(base_dir, "export.json")
|
|
869
|
+
with open(export_json, "w") as f:
|
|
870
|
+
f.write('{"test": "data"}')
|
|
871
|
+
|
|
872
|
+
task = create_task(
|
|
873
|
+
SfdmuTask,
|
|
874
|
+
{
|
|
875
|
+
"source": "dev",
|
|
876
|
+
"target": "qa",
|
|
877
|
+
"path": base_dir,
|
|
878
|
+
"return_always_success": True,
|
|
879
|
+
},
|
|
880
|
+
)
|
|
881
|
+
|
|
882
|
+
# Mock sfdx command to raise an error
|
|
883
|
+
mock_sfdx.side_effect = Exception("SFDMU command failed")
|
|
884
|
+
|
|
885
|
+
# Mock _validate_org
|
|
886
|
+
mock_org = mock.Mock()
|
|
887
|
+
mock_org.sfdx_alias = "test_org"
|
|
888
|
+
task._validate_org = mock.Mock(return_value=mock_org)
|
|
889
|
+
|
|
890
|
+
# Mock _inject_namespace_tokens
|
|
891
|
+
task._inject_namespace_tokens = mock.Mock()
|
|
892
|
+
|
|
893
|
+
# Task should NOT raise exception - should complete successfully
|
|
894
|
+
task._run_task() # Should not raise
|
|
895
|
+
|
|
896
|
+
@mock.patch("cumulusci.tasks.sfdmu.sfdmu.sfdx")
|
|
897
|
+
def test_return_always_success_true_logs_warning_on_nonzero_exit(self, mock_sfdx):
|
|
898
|
+
"""Test that task logs warning when return_always_success is True and exit code is non-zero."""
|
|
899
|
+
with tempfile.TemporaryDirectory() as base_dir:
|
|
900
|
+
# Create export.json
|
|
901
|
+
export_json = os.path.join(base_dir, "export.json")
|
|
902
|
+
with open(export_json, "w") as f:
|
|
903
|
+
f.write('{"test": "data"}')
|
|
904
|
+
|
|
905
|
+
task = create_task(
|
|
906
|
+
SfdmuTask,
|
|
907
|
+
{
|
|
908
|
+
"source": "dev",
|
|
909
|
+
"target": "qa",
|
|
910
|
+
"path": base_dir,
|
|
911
|
+
"return_always_success": True,
|
|
912
|
+
},
|
|
913
|
+
)
|
|
914
|
+
|
|
915
|
+
# Mock sfdx command to return non-zero exit code
|
|
916
|
+
mock_sfdx_result = mock.Mock()
|
|
917
|
+
mock_sfdx_result.returncode = 1 # Failed
|
|
918
|
+
mock_sfdx_result.stdout_text = iter([])
|
|
919
|
+
mock_sfdx_result.stderr_text = iter([])
|
|
920
|
+
mock_sfdx.return_value = mock_sfdx_result
|
|
921
|
+
|
|
922
|
+
# Mock _validate_org
|
|
923
|
+
mock_org = mock.Mock()
|
|
924
|
+
mock_org.sfdx_alias = "test_org"
|
|
925
|
+
task._validate_org = mock.Mock(return_value=mock_org)
|
|
926
|
+
|
|
927
|
+
# Mock _inject_namespace_tokens
|
|
928
|
+
task._inject_namespace_tokens = mock.Mock()
|
|
929
|
+
|
|
930
|
+
# Mock logger to capture warnings
|
|
931
|
+
task.logger = mock.Mock()
|
|
932
|
+
|
|
933
|
+
# Run the task
|
|
934
|
+
task._run_task()
|
|
935
|
+
|
|
936
|
+
# Verify warning was logged
|
|
937
|
+
task.logger.warning.assert_called_once()
|
|
938
|
+
warning_message = task.logger.warning.call_args[0][0]
|
|
939
|
+
assert "failed with exit code 1" in warning_message
|
|
940
|
+
assert "return_always_success is True" in warning_message
|
|
941
|
+
|
|
942
|
+
@mock.patch("cumulusci.tasks.sfdmu.sfdmu.sfdx")
|
|
943
|
+
def test_return_always_success_default_false(self, mock_sfdx):
|
|
944
|
+
"""Test that return_always_success defaults to False."""
|
|
945
|
+
with tempfile.TemporaryDirectory() as base_dir:
|
|
946
|
+
# Create export.json
|
|
947
|
+
export_json = os.path.join(base_dir, "export.json")
|
|
948
|
+
with open(export_json, "w") as f:
|
|
949
|
+
f.write('{"test": "data"}')
|
|
950
|
+
|
|
951
|
+
# Create task without specifying return_always_success
|
|
952
|
+
task = create_task(
|
|
953
|
+
SfdmuTask,
|
|
954
|
+
{"source": "dev", "target": "qa", "path": base_dir},
|
|
955
|
+
)
|
|
956
|
+
|
|
957
|
+
# Mock sfdx command to raise an error
|
|
958
|
+
mock_sfdx.side_effect = Exception("SFDMU command failed")
|
|
959
|
+
|
|
960
|
+
# Mock _validate_org
|
|
961
|
+
mock_org = mock.Mock()
|
|
962
|
+
mock_org.sfdx_alias = "test_org"
|
|
963
|
+
task._validate_org = mock.Mock(return_value=mock_org)
|
|
964
|
+
|
|
965
|
+
# Mock _inject_namespace_tokens
|
|
966
|
+
task._inject_namespace_tokens = mock.Mock()
|
|
967
|
+
|
|
968
|
+
# Task should raise the exception (default behavior)
|
|
969
|
+
with pytest.raises(Exception, match="SFDMU command failed"):
|
|
970
|
+
task._run_task()
|
|
971
|
+
|
|
972
|
+
@mock.patch("cumulusci.tasks.sfdmu.sfdmu.sfdx")
|
|
973
|
+
def test_return_always_success_true_with_csvfile_export(self, mock_sfdx):
|
|
974
|
+
"""Test that CSV post-processing still runs when return_always_success is True and command fails."""
|
|
975
|
+
with tempfile.TemporaryDirectory() as base_dir:
|
|
976
|
+
# Create export.json
|
|
977
|
+
export_json = os.path.join(base_dir, "export.json")
|
|
978
|
+
with open(export_json, "w") as f:
|
|
979
|
+
f.write('{"test": "data"}')
|
|
980
|
+
|
|
981
|
+
task = create_task(
|
|
982
|
+
SfdmuTask,
|
|
983
|
+
{
|
|
984
|
+
"source": "dev",
|
|
985
|
+
"target": "csvfile",
|
|
986
|
+
"path": base_dir,
|
|
987
|
+
"return_always_success": True,
|
|
988
|
+
},
|
|
989
|
+
)
|
|
990
|
+
task.project_config.project__package__namespace = "testns"
|
|
991
|
+
|
|
992
|
+
# Mock sfdx command to raise an error
|
|
993
|
+
mock_sfdx.side_effect = Exception("SFDMU command failed")
|
|
994
|
+
|
|
995
|
+
# Mock _validate_org
|
|
996
|
+
mock_source_org = mock.Mock()
|
|
997
|
+
mock_source_org.sfdx_alias = "test_dev"
|
|
998
|
+
task._validate_org = mock.Mock(
|
|
999
|
+
side_effect=lambda org: mock_source_org if org == "dev" else None
|
|
1000
|
+
)
|
|
1001
|
+
|
|
1002
|
+
# Mock _inject_namespace_tokens
|
|
1003
|
+
task._inject_namespace_tokens = mock.Mock()
|
|
1004
|
+
|
|
1005
|
+
# Spy on _process_csv_exports
|
|
1006
|
+
task._process_csv_exports = mock.Mock()
|
|
1007
|
+
|
|
1008
|
+
# Run the task - should not raise
|
|
1009
|
+
task._run_task()
|
|
1010
|
+
|
|
1011
|
+
# Verify that _process_csv_exports was still called
|
|
1012
|
+
task._process_csv_exports.assert_called_once()
|