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,363 @@
1
+ """SFDmu task for CumulusCI."""
2
+
3
+ import os
4
+ import shutil
5
+
6
+ import sarge
7
+
8
+ from cumulusci.core.exceptions import TaskOptionsError
9
+ from cumulusci.core.sfdx import sfdx
10
+ from cumulusci.core.tasks import BaseSalesforceTask
11
+ from cumulusci.core.utils import determine_managed_mode
12
+ from cumulusci.tasks.command import Command
13
+
14
+
15
+ class SfdmuTask(BaseSalesforceTask, Command):
16
+ """Execute SFDmu data migration with namespace injection support."""
17
+
18
+ salesforce_task = (
19
+ False # Override to False since we manage our own org requirements
20
+ )
21
+
22
+ task_options = {
23
+ "source": {
24
+ "description": "Source org name (CCI org name like dev, beta, qa, etc.) or 'csvfile'",
25
+ "required": True,
26
+ },
27
+ "target": {
28
+ "description": "Target org name (CCI org name like dev, beta, qa, etc.) or 'csvfile'",
29
+ "required": True,
30
+ },
31
+ "path": {
32
+ "description": "Path to folder containing export.json and other CSV files",
33
+ "required": True,
34
+ },
35
+ "additional_params": {
36
+ "description": "Additional parameters to append to the sf sfdmu command (e.g., '--simulation --noprompt --nowarnings')",
37
+ "required": False,
38
+ },
39
+ "return_always_success": {
40
+ "description": "If True, the task will return success (exit code 0) even if SFDMU fails. A warning will be logged instead of raising an error.",
41
+ "required": False,
42
+ "default": False,
43
+ },
44
+ }
45
+
46
+ def _init_options(self, kwargs):
47
+ super()._init_options(kwargs)
48
+
49
+ # Convert path to absolute path
50
+ self.options["path"] = os.path.abspath(self.options["path"])
51
+
52
+ # Validate that the path exists and contains export.json
53
+ if not os.path.exists(self.options["path"]):
54
+ raise TaskOptionsError(f"Path {self.options['path']} does not exist")
55
+
56
+ export_json_path = os.path.join(self.options["path"], "export.json")
57
+ if not os.path.exists(export_json_path):
58
+ raise TaskOptionsError(f"export.json not found in {self.options['path']}")
59
+
60
+ def _validate_org(self, org_name):
61
+ """Validate that a CCI org exists and return the org config."""
62
+ if org_name == "csvfile":
63
+ return None
64
+
65
+ try:
66
+ if self.project_config.keychain is None:
67
+ raise TaskOptionsError("No keychain available")
68
+ org_config = self.project_config.keychain.get_org(org_name)
69
+ return org_config
70
+ except Exception as e:
71
+ raise TaskOptionsError(f"Org '{org_name}' does not exist: {str(e)}")
72
+
73
+ def _get_sf_org_name(self, org_config):
74
+ """Get the SF org name from org config."""
75
+ if hasattr(org_config, "sfdx_alias") and org_config.sfdx_alias:
76
+ return org_config.sfdx_alias
77
+ elif hasattr(org_config, "username") and org_config.username:
78
+ return org_config.username
79
+ else:
80
+ raise TaskOptionsError("Could not determine SF org name for org config")
81
+
82
+ def _create_execute_directory(self, base_path):
83
+ """Create /execute directory and copy files from base_path."""
84
+ execute_path = os.path.join(base_path, "execute")
85
+
86
+ # Remove existing execute directory if it exists
87
+ if os.path.exists(execute_path):
88
+ shutil.rmtree(execute_path)
89
+
90
+ # Create execute directory
91
+ os.makedirs(execute_path, exist_ok=True)
92
+
93
+ # Copy only files (not directories) from base_path to execute
94
+ for item in os.listdir(base_path):
95
+ item_path = os.path.join(base_path, item)
96
+ if os.path.isfile(item_path) and item.endswith((".json", ".csv")):
97
+ shutil.copy2(item_path, execute_path)
98
+
99
+ return execute_path
100
+
101
+ def _update_credentials(self):
102
+ """Override to handle cases where org_config might be None."""
103
+ # Only update credentials if we have an org_config
104
+ if self.org_config is not None:
105
+ super()._update_credentials()
106
+
107
+ def _inject_namespace_tokens(
108
+ self, execute_path, source_org_config, target_org_config
109
+ ):
110
+ """Inject namespace tokens into files in execute directory using the same mechanism as Deploy task."""
111
+ # Determine which org config to use for namespace injection
112
+ # When exporting (source=org, target=csvfile), use source org
113
+ # When importing (source=csvfile, target=org), use target org
114
+ # When transferring (source=org, target=org), use target org
115
+ org_config_for_injection = (
116
+ target_org_config if target_org_config is not None else source_org_config
117
+ )
118
+
119
+ if (
120
+ org_config_for_injection is None
121
+ ): # both source and target are csvfile (unlikely but handle it)
122
+ return
123
+
124
+ # Get namespace information
125
+ namespace = self.project_config.project__package__namespace
126
+ managed = determine_managed_mode(
127
+ self.options, self.project_config, org_config_for_injection
128
+ )
129
+ namespaced_org = bool(namespace) and namespace == getattr(
130
+ org_config_for_injection, "namespace", None
131
+ )
132
+
133
+ # Create a temporary zipfile with all files from execute directory
134
+ import tempfile
135
+ import zipfile
136
+
137
+ from cumulusci.core.dependencies.utils import TaskContext
138
+ from cumulusci.core.source_transforms.transforms import (
139
+ NamespaceInjectionOptions,
140
+ NamespaceInjectionTransform,
141
+ )
142
+
143
+ with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as temp_zip:
144
+ temp_zip_path = temp_zip.name
145
+
146
+ try:
147
+ # Create zipfile with all files from execute directory
148
+ with zipfile.ZipFile(temp_zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
149
+ for root, dirs, files in os.walk(execute_path):
150
+ for file in files:
151
+ if file.endswith((".json", ".csv")):
152
+ file_path = os.path.join(root, file)
153
+ # Calculate relative path from execute_path
154
+ rel_path = os.path.relpath(file_path, execute_path)
155
+ zf.write(file_path, rel_path)
156
+
157
+ # Apply namespace injection using the same mechanism as Deploy task
158
+ with zipfile.ZipFile(temp_zip_path, "r") as zf:
159
+ # Create namespace injection options
160
+ options = NamespaceInjectionOptions(
161
+ namespace_tokenize=None,
162
+ namespace_inject=namespace,
163
+ namespace_strip=None,
164
+ unmanaged=not managed,
165
+ namespaced_org=namespaced_org,
166
+ )
167
+
168
+ # Create transform
169
+ transform = NamespaceInjectionTransform(options)
170
+
171
+ # Create task context
172
+ context = TaskContext(
173
+ org_config_for_injection, self.project_config, self.logger
174
+ )
175
+
176
+ # Apply namespace injection
177
+ new_zf = transform.process(zf, context)
178
+
179
+ # Extract processed files back to execute directory
180
+ # First, remove all existing files
181
+ for root, dirs, files in os.walk(execute_path):
182
+ for file in files:
183
+ if file.endswith((".json", ".csv")):
184
+ os.remove(os.path.join(root, file))
185
+
186
+ # Extract processed files
187
+ for file_info in new_zf.infolist():
188
+ if file_info.filename.endswith((".json", ".csv")):
189
+ # Extract to execute directory
190
+ target_path = os.path.join(execute_path, file_info.filename)
191
+ # Ensure directory exists
192
+ os.makedirs(os.path.dirname(target_path), exist_ok=True)
193
+ with new_zf.open(file_info) as source:
194
+ with open(target_path, "wb") as target:
195
+ target.write(source.read())
196
+
197
+ self.logger.info(
198
+ f"Applied namespace injection to {file_info.filename}"
199
+ )
200
+
201
+ finally:
202
+ # Clean up temporary zipfile
203
+ if os.path.exists(temp_zip_path):
204
+ os.unlink(temp_zip_path)
205
+
206
+ def _process_csv_exports(self, execute_path, base_path):
207
+ """Process CSV files when target is csvfile.
208
+
209
+ This method performs the following operations:
210
+ 1. Replace namespace prefix with %%%MANAGED_OR_NAMESPACED_ORG%%% in CSV file contents
211
+ 2. Rename CSV files replacing namespace prefix with ___MANAGED_OR_NAMESPACED_ORG___
212
+ 3. Copy all CSV files from execute folder to base path, replacing existing files
213
+ """
214
+ namespace = self.project_config.project__package__namespace
215
+ if not namespace:
216
+ self.logger.info("No namespace configured, skipping CSV post-processing")
217
+ return
218
+
219
+ namespace_prefix = namespace + "__"
220
+ content_token = "%%%MANAGED_OR_NAMESPACED_ORG%%%"
221
+ filename_token = "___MANAGED_OR_NAMESPACED_ORG___"
222
+
223
+ # Get all CSV files in execute directory
224
+ csv_files = [f for f in os.listdir(execute_path) if f.endswith(".csv")]
225
+
226
+ if not csv_files:
227
+ self.logger.info("No CSV files found in execute directory")
228
+ return
229
+
230
+ self.logger.info(f"Processing {len(csv_files)} CSV file(s) for export")
231
+
232
+ # Process each CSV file
233
+ processed_files = []
234
+ for filename in csv_files:
235
+ file_path = os.path.join(execute_path, filename)
236
+
237
+ # Step 1: Replace namespace prefix in file contents
238
+ with open(file_path, "r", encoding="utf-8") as f:
239
+ content = f.read()
240
+
241
+ if namespace_prefix in content:
242
+ content = content.replace(namespace_prefix, content_token)
243
+ with open(file_path, "w", encoding="utf-8") as f:
244
+ f.write(content)
245
+ self.logger.debug(f"Replaced namespace prefix in content of {filename}")
246
+
247
+ # Step 2: Rename file if it contains namespace prefix
248
+ new_filename = filename.replace(namespace_prefix, filename_token)
249
+ if new_filename != filename:
250
+ new_file_path = os.path.join(execute_path, new_filename)
251
+ os.rename(file_path, new_file_path)
252
+ self.logger.debug(f"Renamed file: {filename} -> {new_filename}")
253
+ file_path = new_file_path
254
+ filename = new_filename
255
+
256
+ processed_files.append((file_path, filename))
257
+
258
+ # Step 3: Delete all CSV files in base_path and copy processed files
259
+ self.logger.debug(f"Copying processed CSV files to {base_path}")
260
+
261
+ # Remove existing CSV files in base_path
262
+ for item in os.listdir(base_path):
263
+ if item.endswith(".csv"):
264
+ item_path = os.path.join(base_path, item)
265
+ if os.path.isfile(item_path):
266
+ os.remove(item_path)
267
+ self.logger.debug(f"Removed existing file: {item}")
268
+
269
+ # Copy processed files to base_path
270
+ for file_path, filename in processed_files:
271
+ target_path = os.path.join(base_path, filename)
272
+ shutil.copy2(file_path, target_path)
273
+ self.logger.debug(f"Copied {filename} to {base_path}")
274
+
275
+ self.logger.info("CSV post-processing completed successfully")
276
+
277
+ def _run_task(self):
278
+ """Execute the SFDmu task."""
279
+ # Validate source and target orgs
280
+ source_org_config = self._validate_org(self.options["source"])
281
+ target_org_config = self._validate_org(self.options["target"])
282
+
283
+ # Get SF org names
284
+ if source_org_config:
285
+ source_sf_org = self._get_sf_org_name(source_org_config)
286
+ else:
287
+ source_sf_org = "csvfile"
288
+
289
+ if target_org_config:
290
+ target_sf_org = self._get_sf_org_name(target_org_config)
291
+ else:
292
+ target_sf_org = "csvfile"
293
+
294
+ # Create execute directory and copy files
295
+ execute_path = self._create_execute_directory(self.options["path"])
296
+ self.logger.info(f"Created execute directory at {execute_path}")
297
+
298
+ # Apply namespace injection
299
+ self._inject_namespace_tokens(
300
+ execute_path, source_org_config, target_org_config
301
+ )
302
+
303
+ # Build and execute SFDmu command
304
+ # Use shell_quote to properly handle paths with spaces on Windows
305
+ command_parts = [
306
+ "-s",
307
+ source_sf_org,
308
+ "-u",
309
+ target_sf_org,
310
+ "-p",
311
+ execute_path,
312
+ ]
313
+
314
+ # Append additional parameters if provided
315
+ if self.options.get("additional_params"):
316
+ # Split the additional_params string into individual arguments
317
+ # This handles cases like "-no-warnings -m -t error" -> ["-no-warnings", "-m", "-t", "error"]
318
+ additional_args = self.options["additional_params"].split()
319
+ # Quote each argument to handle spaces properly
320
+ command_parts.extend(additional_args)
321
+
322
+ # Join command parts into a single string for sarge (which uses shell=True)
323
+ command = "sf sfdmu run " + " ".join(command_parts)
324
+ self.logger.info(f"Executing: {command}")
325
+
326
+ # Determine if we should fail on error or just warn
327
+ return_always_success = self.options.get("return_always_success", False)
328
+
329
+ try:
330
+ p: sarge.Command = sfdx(
331
+ "sfdmu run",
332
+ log_note="Running SFDmu",
333
+ args=command_parts,
334
+ check_return=not return_always_success, # Don't check return if return_always_success is True
335
+ username=None,
336
+ )
337
+
338
+ for line in p.stdout_text:
339
+ self.logger.info(line)
340
+
341
+ for line in p.stderr_text:
342
+ self.logger.error(line)
343
+
344
+ # Check if command failed when return_always_success is True
345
+ if return_always_success and p.returncode != 0:
346
+ self.logger.warning(
347
+ f"SFDmu command failed with exit code {p.returncode}, but return_always_success is True. "
348
+ "Task will continue and return success."
349
+ )
350
+ else:
351
+ self.logger.info("SFDmu task completed successfully")
352
+ except Exception as e:
353
+ if return_always_success:
354
+ self.logger.warning(
355
+ f"SFDmu command failed with error: {str(e)}, but return_always_success is True. "
356
+ "Task will continue and return success."
357
+ )
358
+ else:
359
+ raise
360
+
361
+ # Post-process CSV files if target is csvfile
362
+ if self.options["target"] == "csvfile":
363
+ self._process_csv_exports(execute_path, self.options["path"])
@@ -0,0 +1 @@
1
+ """Tests for SFDmu tasks."""
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env python3
2
+ """Simple test runner for SFDmu tests."""
3
+
4
+ import os
5
+ import sys
6
+ import tempfile
7
+ from unittest import mock
8
+
9
+ # Add the project root to the path
10
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", ".."))
11
+
12
+ from cumulusci.tasks.salesforce.tests.util import create_task
13
+ from cumulusci.tasks.sfdmu.sfdmu import SfdmuTask
14
+
15
+
16
+ def test_create_execute_directory():
17
+ """Test _create_execute_directory creates directory and copies files."""
18
+ with tempfile.TemporaryDirectory() as base_dir:
19
+ # Create test files
20
+ export_json = os.path.join(base_dir, "export.json")
21
+ test_csv = os.path.join(base_dir, "test.csv")
22
+ test_txt = os.path.join(base_dir, "test.txt") # Should not be copied
23
+
24
+ with open(export_json, "w") as f:
25
+ f.write('{"test": "data"}')
26
+ with open(test_csv, "w") as f:
27
+ f.write("col1,col2\nval1,val2")
28
+ with open(test_txt, "w") as f:
29
+ f.write("text file")
30
+
31
+ # Create subdirectory (should not be copied)
32
+ subdir = os.path.join(base_dir, "subdir")
33
+ os.makedirs(subdir)
34
+ with open(os.path.join(subdir, "file.txt"), "w") as f:
35
+ f.write("subdir file")
36
+
37
+ task = create_task(
38
+ SfdmuTask, {"source": "dev", "target": "qa", "path": base_dir}
39
+ )
40
+
41
+ execute_path = task._create_execute_directory(base_dir)
42
+
43
+ # Check that execute directory was created
44
+ assert os.path.exists(execute_path)
45
+ assert execute_path == os.path.join(base_dir, "execute")
46
+
47
+ # Check that files were copied
48
+ assert os.path.exists(os.path.join(execute_path, "export.json"))
49
+ assert os.path.exists(os.path.join(execute_path, "test.csv"))
50
+ assert not os.path.exists(
51
+ os.path.join(execute_path, "test.txt")
52
+ ) # Not a valid file type
53
+ assert not os.path.exists(os.path.join(execute_path, "subdir")) # Not a file
54
+
55
+ # Check file contents
56
+ with open(os.path.join(execute_path, "export.json"), "r") as f:
57
+ assert f.read() == '{"test": "data"}'
58
+ with open(os.path.join(execute_path, "test.csv"), "r") as f:
59
+ assert f.read() == "col1,col2\nval1,val2"
60
+
61
+ print("✅ test_create_execute_directory passed")
62
+
63
+
64
+ def test_inject_namespace_tokens_csvfile_target():
65
+ """Test that namespace injection is skipped when both source and target are csvfile."""
66
+ with tempfile.TemporaryDirectory() as execute_dir:
67
+ # Create test files
68
+ test_json = os.path.join(execute_dir, "test.json")
69
+ with open(test_json, "w") as f:
70
+ f.write('{"field": "%%%NAMESPACE%%%Test"}')
71
+
72
+ # Create export.json file
73
+ export_json_path = os.path.join(execute_dir, "export.json")
74
+ with open(export_json_path, "w") as f:
75
+ f.write('{"test": "data"}')
76
+
77
+ task = create_task(
78
+ SfdmuTask, {"source": "csvfile", "target": "csvfile", "path": execute_dir}
79
+ )
80
+
81
+ # Should not raise any errors and files should remain unchanged
82
+ task._inject_namespace_tokens(execute_dir, None, None)
83
+
84
+ # Check that file content was not changed
85
+ with open(test_json, "r") as f:
86
+ assert f.read() == '{"field": "%%%NAMESPACE%%%Test"}'
87
+
88
+ print("✅ test_inject_namespace_tokens_csvfile_target passed")
89
+
90
+
91
+ def test_inject_namespace_tokens_managed_mode():
92
+ """Test namespace injection in managed mode."""
93
+ with tempfile.TemporaryDirectory() as execute_dir:
94
+ # Create test files with namespace tokens
95
+ test_json = os.path.join(execute_dir, "test.json")
96
+ test_csv = os.path.join(execute_dir, "test.csv")
97
+
98
+ with open(test_json, "w") as f:
99
+ f.write(
100
+ '{"field": "%%%NAMESPACE%%%Test", "org": "%%%NAMESPACED_ORG%%%Value"}'
101
+ )
102
+ with open(test_csv, "w") as f:
103
+ f.write("Name,%%%NAMESPACE%%%Field\nTest,Value")
104
+
105
+ # Create filename with namespace token
106
+ filename_with_token = os.path.join(execute_dir, "___NAMESPACE___test.json")
107
+ with open(filename_with_token, "w") as f:
108
+ f.write('{"test": "data"}')
109
+
110
+ # Create export.json file
111
+ export_json_path = os.path.join(execute_dir, "export.json")
112
+ with open(export_json_path, "w") as f:
113
+ f.write('{"test": "data"}')
114
+
115
+ task = create_task(
116
+ SfdmuTask, {"source": "dev", "target": "qa", "path": execute_dir}
117
+ )
118
+
119
+ # Mock the project config namespace
120
+ task.project_config.project__package__namespace = "testns"
121
+
122
+ mock_org_config = mock.Mock()
123
+ mock_org_config.namespace = "testns"
124
+
125
+ # Mock determine_managed_mode to return True
126
+ with mock.patch(
127
+ "cumulusci.tasks.sfdmu.sfdmu.determine_managed_mode", return_value=True
128
+ ):
129
+ task._inject_namespace_tokens(execute_dir, None, mock_org_config)
130
+
131
+ # Check that namespace tokens were replaced in content
132
+ with open(test_json, "r") as f:
133
+ content = f.read()
134
+ assert "testns__Test" in content
135
+ assert "testns__Value" in content
136
+
137
+ with open(test_csv, "r") as f:
138
+ content = f.read()
139
+ assert "testns__Field" in content
140
+
141
+ # Check that filename token was replaced
142
+ expected_filename = os.path.join(execute_dir, "testns__test.json")
143
+ assert os.path.exists(expected_filename)
144
+ assert not os.path.exists(filename_with_token)
145
+
146
+ print("✅ test_inject_namespace_tokens_managed_mode passed")
147
+
148
+
149
+ def test_inject_namespace_tokens_unmanaged_mode():
150
+ """Test namespace injection in unmanaged mode."""
151
+ with tempfile.TemporaryDirectory() as execute_dir:
152
+ # Create test files with namespace tokens
153
+ test_json = os.path.join(execute_dir, "test.json")
154
+ with open(test_json, "w") as f:
155
+ f.write(
156
+ '{"field": "%%%NAMESPACE%%%Test", "org": "%%%NAMESPACED_ORG%%%Value"}'
157
+ )
158
+
159
+ # Create export.json file
160
+ export_json_path = os.path.join(execute_dir, "export.json")
161
+ with open(export_json_path, "w") as f:
162
+ f.write('{"test": "data"}')
163
+
164
+ task = create_task(
165
+ SfdmuTask, {"source": "dev", "target": "qa", "path": execute_dir}
166
+ )
167
+
168
+ # Mock the project config namespace
169
+ task.project_config.project__package__namespace = "testns"
170
+
171
+ mock_org_config = mock.Mock()
172
+ mock_org_config.namespace = "testns"
173
+
174
+ # Mock determine_managed_mode to return False
175
+ with mock.patch(
176
+ "cumulusci.tasks.sfdmu.sfdmu.determine_managed_mode", return_value=False
177
+ ):
178
+ task._inject_namespace_tokens(execute_dir, None, mock_org_config)
179
+
180
+ # Check that namespace tokens were replaced with empty strings
181
+ with open(test_json, "r") as f:
182
+ content = f.read()
183
+ assert "Test" in content # %%NAMESPACE%% removed
184
+ assert "Value" in content # %%NAMESPACED_ORG%% removed
185
+ assert "%%%NAMESPACE%%%" not in content
186
+ assert "%%%NAMESPACED_ORG%%%" not in content
187
+
188
+ print("✅ test_inject_namespace_tokens_unmanaged_mode passed")
189
+
190
+
191
+ def main():
192
+ """Run all tests."""
193
+ print("Running SFDmu tests...")
194
+
195
+ try:
196
+ test_create_execute_directory()
197
+ test_inject_namespace_tokens_csvfile_target()
198
+ test_inject_namespace_tokens_managed_mode()
199
+ test_inject_namespace_tokens_unmanaged_mode()
200
+
201
+ print("\n🎉 All tests passed!")
202
+ return 0
203
+ except Exception as e:
204
+ print(f"\n❌ Test failed: {e}")
205
+ import traceback
206
+
207
+ traceback.print_exc()
208
+ return 1
209
+
210
+
211
+ if __name__ == "__main__":
212
+ sys.exit(main())