cumulusci-plus 5.0.21__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.
Files changed (121) hide show
  1. cumulusci/__about__.py +1 -1
  2. cumulusci/cli/logger.py +2 -2
  3. cumulusci/cli/service.py +20 -0
  4. cumulusci/cli/task.py +17 -0
  5. cumulusci/cli/tests/test_error.py +3 -1
  6. cumulusci/cli/tests/test_flow.py +279 -2
  7. cumulusci/cli/tests/test_service.py +15 -12
  8. cumulusci/cli/tests/test_task.py +88 -2
  9. cumulusci/cli/tests/utils.py +1 -4
  10. cumulusci/core/config/base_task_flow_config.py +26 -1
  11. cumulusci/core/config/project_config.py +2 -20
  12. cumulusci/core/config/tests/test_config_expensive.py +9 -3
  13. cumulusci/core/config/universal_config.py +3 -4
  14. cumulusci/core/dependencies/base.py +1 -1
  15. cumulusci/core/dependencies/dependencies.py +1 -1
  16. cumulusci/core/dependencies/github.py +1 -2
  17. cumulusci/core/dependencies/resolvers.py +1 -1
  18. cumulusci/core/dependencies/tests/test_dependencies.py +1 -1
  19. cumulusci/core/dependencies/tests/test_resolvers.py +1 -1
  20. cumulusci/core/flowrunner.py +90 -6
  21. cumulusci/core/github.py +1 -1
  22. cumulusci/core/sfdx.py +3 -1
  23. cumulusci/core/source_transforms/tests/test_transforms.py +1 -1
  24. cumulusci/core/source_transforms/transforms.py +1 -1
  25. cumulusci/core/tasks.py +13 -2
  26. cumulusci/core/tests/test_flowrunner.py +100 -0
  27. cumulusci/core/tests/test_tasks.py +65 -0
  28. cumulusci/core/utils.py +3 -1
  29. cumulusci/core/versions.py +1 -1
  30. cumulusci/cumulusci.yml +55 -0
  31. cumulusci/oauth/client.py +1 -1
  32. cumulusci/plugins/plugin_base.py +5 -3
  33. cumulusci/robotframework/pageobjects/ObjectManagerPageObject.py +1 -1
  34. cumulusci/salesforce_api/rest_deploy.py +1 -1
  35. cumulusci/schema/cumulusci.jsonschema.json +64 -0
  36. cumulusci/tasks/apex/anon.py +1 -1
  37. cumulusci/tasks/apex/testrunner.py +416 -142
  38. cumulusci/tasks/apex/tests/test_apex_tasks.py +917 -1
  39. cumulusci/tasks/bulkdata/extract.py +0 -1
  40. cumulusci/tasks/bulkdata/extract_dataset_utils/extract_yml.py +1 -1
  41. cumulusci/tasks/bulkdata/extract_dataset_utils/synthesize_extract_declarations.py +1 -1
  42. cumulusci/tasks/bulkdata/extract_dataset_utils/tests/test_extract_yml.py +1 -1
  43. cumulusci/tasks/bulkdata/generate_and_load_data.py +136 -12
  44. cumulusci/tasks/bulkdata/mapping_parser.py +139 -44
  45. cumulusci/tasks/bulkdata/select_utils.py +1 -1
  46. cumulusci/tasks/bulkdata/snowfakery.py +100 -25
  47. cumulusci/tasks/bulkdata/tests/test_generate_and_load.py +159 -0
  48. cumulusci/tasks/bulkdata/tests/test_load.py +0 -2
  49. cumulusci/tasks/bulkdata/tests/test_mapping_parser.py +763 -1
  50. cumulusci/tasks/bulkdata/tests/test_select_utils.py +26 -0
  51. cumulusci/tasks/bulkdata/tests/test_snowfakery.py +133 -0
  52. cumulusci/tasks/create_package_version.py +190 -16
  53. cumulusci/tasks/datadictionary.py +1 -1
  54. cumulusci/tasks/metadata_etl/base.py +7 -3
  55. cumulusci/tasks/metadata_etl/layouts.py +1 -1
  56. cumulusci/tasks/metadata_etl/permissions.py +1 -1
  57. cumulusci/tasks/metadata_etl/remote_site_settings.py +2 -2
  58. cumulusci/tasks/push/README.md +15 -17
  59. cumulusci/tasks/release_notes/README.md +13 -13
  60. cumulusci/tasks/release_notes/generator.py +13 -8
  61. cumulusci/tasks/robotframework/tests/test_robotframework.py +6 -1
  62. cumulusci/tasks/salesforce/Deploy.py +53 -2
  63. cumulusci/tasks/salesforce/SfPackageCommands.py +363 -0
  64. cumulusci/tasks/salesforce/__init__.py +1 -0
  65. cumulusci/tasks/salesforce/assign_ps_psg.py +448 -0
  66. cumulusci/tasks/salesforce/composite.py +1 -1
  67. cumulusci/tasks/salesforce/custom_settings_wait.py +1 -1
  68. cumulusci/tasks/salesforce/enable_prediction.py +5 -1
  69. cumulusci/tasks/salesforce/getPackageVersion.py +89 -0
  70. cumulusci/tasks/salesforce/sourcetracking.py +1 -1
  71. cumulusci/tasks/salesforce/tests/test_Deploy.py +316 -1
  72. cumulusci/tasks/salesforce/tests/test_SfPackageCommands.py +554 -0
  73. cumulusci/tasks/salesforce/tests/test_assign_ps_psg.py +1055 -0
  74. cumulusci/tasks/salesforce/tests/test_getPackageVersion.py +651 -0
  75. cumulusci/tasks/salesforce/tests/test_update_dependencies.py +1 -1
  76. cumulusci/tasks/salesforce/tests/test_update_external_credential.py +912 -0
  77. cumulusci/tasks/salesforce/tests/test_update_named_credential.py +1042 -0
  78. cumulusci/tasks/salesforce/update_dependencies.py +2 -2
  79. cumulusci/tasks/salesforce/update_external_credential.py +562 -0
  80. cumulusci/tasks/salesforce/update_named_credential.py +441 -0
  81. cumulusci/tasks/salesforce/update_profile.py +17 -13
  82. cumulusci/tasks/salesforce/users/permsets.py +62 -5
  83. cumulusci/tasks/salesforce/users/tests/test_permsets.py +237 -11
  84. cumulusci/tasks/sfdmu/__init__.py +0 -0
  85. cumulusci/tasks/sfdmu/sfdmu.py +363 -0
  86. cumulusci/tasks/sfdmu/tests/__init__.py +1 -0
  87. cumulusci/tasks/sfdmu/tests/test_runner.py +212 -0
  88. cumulusci/tasks/sfdmu/tests/test_sfdmu.py +1012 -0
  89. cumulusci/tasks/tests/test_create_package_version.py +716 -1
  90. cumulusci/tasks/tests/test_util.py +42 -0
  91. cumulusci/tasks/util.py +37 -1
  92. cumulusci/tasks/utility/copyContents.py +402 -0
  93. cumulusci/tasks/utility/credentialManager.py +256 -0
  94. cumulusci/tasks/utility/directoryRecreator.py +30 -0
  95. cumulusci/tasks/utility/env_management.py +1 -1
  96. cumulusci/tasks/utility/secretsToEnv.py +135 -0
  97. cumulusci/tasks/utility/tests/test_copyContents.py +1719 -0
  98. cumulusci/tasks/utility/tests/test_credentialManager.py +564 -0
  99. cumulusci/tasks/utility/tests/test_directoryRecreator.py +439 -0
  100. cumulusci/tasks/utility/tests/test_secretsToEnv.py +1091 -0
  101. cumulusci/tests/test_integration_infrastructure.py +3 -1
  102. cumulusci/tests/test_utils.py +70 -6
  103. cumulusci/utils/__init__.py +54 -9
  104. cumulusci/utils/classutils.py +5 -2
  105. cumulusci/utils/http/tests/cassettes/ManualEditTestCompositeParallelSalesforce.test_http_headers.yaml +31 -30
  106. cumulusci/utils/options.py +23 -1
  107. cumulusci/utils/parallel/task_worker_queues/parallel_worker.py +1 -1
  108. cumulusci/utils/yaml/cumulusci_yml.py +7 -3
  109. cumulusci/utils/yaml/model_parser.py +2 -2
  110. cumulusci/utils/yaml/tests/test_cumulusci_yml.py +1 -1
  111. cumulusci/utils/yaml/tests/test_model_parser.py +3 -3
  112. cumulusci/vcs/base.py +23 -15
  113. cumulusci/vcs/bootstrap.py +5 -4
  114. cumulusci/vcs/utils/list_modified_files.py +189 -0
  115. cumulusci/vcs/utils/tests/test_list_modified_files.py +588 -0
  116. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.35.dist-info}/METADATA +12 -10
  117. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.35.dist-info}/RECORD +121 -96
  118. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.35.dist-info}/WHEEL +0 -0
  119. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.35.dist-info}/entry_points.txt +0 -0
  120. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.35.dist-info}/licenses/AUTHORS.rst +0 -0
  121. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.35.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()