cumulusci-plus 5.0.24__py3-none-any.whl → 5.0.26__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.

Potentially problematic release.


This version of cumulusci-plus might be problematic. Click here for more details.

Files changed (50) hide show
  1. cumulusci/__about__.py +1 -1
  2. cumulusci/cli/task.py +17 -0
  3. cumulusci/cli/tests/test_error.py +3 -1
  4. cumulusci/cli/tests/test_task.py +88 -2
  5. cumulusci/core/github.py +1 -1
  6. cumulusci/core/sfdx.py +3 -1
  7. cumulusci/cumulusci.yml +20 -0
  8. cumulusci/robotframework/pageobjects/ObjectManagerPageObject.py +1 -1
  9. cumulusci/salesforce_api/rest_deploy.py +1 -1
  10. cumulusci/tasks/apex/anon.py +1 -1
  11. cumulusci/tasks/apex/testrunner.py +6 -1
  12. cumulusci/tasks/bulkdata/extract.py +0 -1
  13. cumulusci/tasks/bulkdata/tests/test_load.py +0 -2
  14. cumulusci/tasks/bulkdata/tests/test_select_utils.py +6 -0
  15. cumulusci/tasks/metadata_etl/base.py +7 -3
  16. cumulusci/tasks/push/README.md +15 -17
  17. cumulusci/tasks/release_notes/README.md +13 -13
  18. cumulusci/tasks/robotframework/tests/test_robotframework.py +1 -1
  19. cumulusci/tasks/salesforce/Deploy.py +5 -1
  20. cumulusci/tasks/salesforce/composite.py +1 -1
  21. cumulusci/tasks/salesforce/custom_settings_wait.py +1 -1
  22. cumulusci/tasks/salesforce/enable_prediction.py +5 -1
  23. cumulusci/tasks/salesforce/sourcetracking.py +1 -1
  24. cumulusci/tasks/salesforce/tests/test_update_external_credential.py +912 -0
  25. cumulusci/tasks/salesforce/tests/test_update_named_credential.py +1042 -0
  26. cumulusci/tasks/salesforce/update_external_credential.py +562 -0
  27. cumulusci/tasks/salesforce/update_named_credential.py +441 -0
  28. cumulusci/tasks/salesforce/update_profile.py +17 -13
  29. cumulusci/tasks/salesforce/users/permsets.py +70 -2
  30. cumulusci/tasks/salesforce/users/tests/test_permsets.py +184 -0
  31. cumulusci/tasks/sfdmu/__init__.py +0 -0
  32. cumulusci/tasks/sfdmu/sfdmu.py +256 -0
  33. cumulusci/tasks/sfdmu/tests/__init__.py +1 -0
  34. cumulusci/tasks/sfdmu/tests/test_runner.py +212 -0
  35. cumulusci/tasks/sfdmu/tests/test_sfdmu.py +443 -0
  36. cumulusci/tasks/utility/credentialManager.py +256 -0
  37. cumulusci/tasks/utility/directoryRecreator.py +30 -0
  38. cumulusci/tasks/utility/secretsToEnv.py +130 -0
  39. cumulusci/tasks/utility/tests/test_credentialManager.py +564 -0
  40. cumulusci/tasks/utility/tests/test_directoryRecreator.py +439 -0
  41. cumulusci/tasks/utility/tests/test_secretsToEnv.py +1091 -0
  42. cumulusci/utils/__init__.py +23 -1
  43. cumulusci/utils/http/tests/cassettes/ManualEditTestCompositeParallelSalesforce.test_http_headers.yaml +31 -30
  44. cumulusci/utils/yaml/tests/test_model_parser.py +2 -2
  45. {cumulusci_plus-5.0.24.dist-info → cumulusci_plus-5.0.26.dist-info}/METADATA +7 -9
  46. {cumulusci_plus-5.0.24.dist-info → cumulusci_plus-5.0.26.dist-info}/RECORD +50 -35
  47. {cumulusci_plus-5.0.24.dist-info → cumulusci_plus-5.0.26.dist-info}/WHEEL +0 -0
  48. {cumulusci_plus-5.0.24.dist-info → cumulusci_plus-5.0.26.dist-info}/entry_points.txt +0 -0
  49. {cumulusci_plus-5.0.24.dist-info → cumulusci_plus-5.0.26.dist-info}/licenses/AUTHORS.rst +0 -0
  50. {cumulusci_plus-5.0.24.dist-info → cumulusci_plus-5.0.26.dist-info}/licenses/LICENSE +0 -0
@@ -416,6 +416,190 @@ class TestCreatePermissionSet:
416
416
  table.assert_called_once()
417
417
  assert expected_table_data in table.call_args[0]
418
418
 
419
+ @responses.activate
420
+ def test_namespace_injection_managed(self):
421
+ """Test that %%%NAMESPACE%%% token gets replaced in managed context"""
422
+ task = create_task(
423
+ AssignPermissionSets,
424
+ {
425
+ "api_names": "%%%NAMESPACE%%%PermSet1,PermSet2",
426
+ },
427
+ )
428
+ # Simulate managed context by setting the namespace in project config
429
+ task.project_config.config["project"]["package"]["namespace"] = "testns"
430
+ # Simulate that the package is installed (managed mode)
431
+ task.org_config._installed_packages = {"testns": "1.0"}
432
+ task.org_config.namespace = None # Not a packaging org
433
+
434
+ responses.add(
435
+ method="GET",
436
+ url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/?q=SELECT+Id%2C%28SELECT+PermissionSetId+FROM+PermissionSetAssignments%29+FROM+User+WHERE+Username+%3D+%27test-cci%40example.com%27",
437
+ status=200,
438
+ json={
439
+ "done": True,
440
+ "totalSize": 1,
441
+ "records": [
442
+ {
443
+ "Id": "005000000000000",
444
+ "PermissionSetAssignments": None,
445
+ }
446
+ ],
447
+ },
448
+ )
449
+ responses.add(
450
+ method="GET",
451
+ url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/?q=SELECT+Id%2CName+FROM+PermissionSet+WHERE+Name+IN+%28%27testns__PermSet1%27%2C+%27PermSet2%27%29",
452
+ status=200,
453
+ json={
454
+ "done": True,
455
+ "totalSize": 2,
456
+ "records": [
457
+ {
458
+ "Id": "0PS000000000000",
459
+ "Name": "testns__PermSet1",
460
+ },
461
+ {
462
+ "Id": "0PS000000000001",
463
+ "Name": "PermSet2",
464
+ },
465
+ ],
466
+ },
467
+ )
468
+ responses.add(
469
+ method="POST",
470
+ url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
471
+ status=200,
472
+ json=[
473
+ {"id": "0Pa000000000000", "success": True, "errors": []},
474
+ {"id": "0Pa000000000001", "success": True, "errors": []},
475
+ ],
476
+ )
477
+
478
+ task()
479
+
480
+ assert len(responses.calls) == 3
481
+ # Verify that the SOQL query contains the namespaced permission set name
482
+ assert "testns__PermSet1" in responses.calls[1].request.url
483
+
484
+ @responses.activate
485
+ def test_namespace_injection_unmanaged(self):
486
+ """Test that %%%NAMESPACE%%% token gets stripped in unmanaged context"""
487
+ task = create_task(
488
+ AssignPermissionSets,
489
+ {
490
+ "api_names": "%%%NAMESPACE%%%PermSet1",
491
+ },
492
+ )
493
+ # Simulate unmanaged context (scratch org) - no installed packages
494
+ task.project_config.config["project"]["package"]["namespace"] = "testns"
495
+ task.org_config._installed_packages = {}
496
+ task.org_config.namespace = None # Not a packaging org
497
+
498
+ responses.add(
499
+ method="GET",
500
+ url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/?q=SELECT+Id%2C%28SELECT+PermissionSetId+FROM+PermissionSetAssignments%29+FROM+User+WHERE+Username+%3D+%27test-cci%40example.com%27",
501
+ status=200,
502
+ json={
503
+ "done": True,
504
+ "totalSize": 1,
505
+ "records": [
506
+ {
507
+ "Id": "005000000000000",
508
+ "PermissionSetAssignments": None,
509
+ }
510
+ ],
511
+ },
512
+ )
513
+ responses.add(
514
+ method="GET",
515
+ url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/?q=SELECT+Id%2CName+FROM+PermissionSet+WHERE+Name+IN+%28%27PermSet1%27%29",
516
+ status=200,
517
+ json={
518
+ "done": True,
519
+ "totalSize": 1,
520
+ "records": [
521
+ {
522
+ "Id": "0PS000000000000",
523
+ "Name": "PermSet1",
524
+ },
525
+ ],
526
+ },
527
+ )
528
+ responses.add(
529
+ method="POST",
530
+ url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
531
+ status=200,
532
+ json=[
533
+ {"id": "0Pa000000000000", "success": True, "errors": []},
534
+ ],
535
+ )
536
+
537
+ task()
538
+
539
+ assert len(responses.calls) == 3
540
+ # Verify that the SOQL query does NOT contain the namespace prefix
541
+ assert "testns__" not in responses.calls[1].request.url
542
+ assert "PermSet1" in responses.calls[1].request.url
543
+
544
+ @responses.activate
545
+ def test_namespaced_org_token(self):
546
+ """Test that %%%NAMESPACED_ORG%%% token gets replaced in namespaced org context"""
547
+ task = create_task(
548
+ AssignPermissionSets,
549
+ {
550
+ "api_names": "%%%NAMESPACED_ORG%%%PermSet1",
551
+ },
552
+ )
553
+ # Simulate namespaced org context (packaging org)
554
+ task.project_config.config["project"]["package"]["namespace"] = "testns"
555
+ task.org_config._installed_packages = {}
556
+ task.org_config.namespace = "testns" # This makes it a namespaced org
557
+
558
+ responses.add(
559
+ method="GET",
560
+ url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/?q=SELECT+Id%2C%28SELECT+PermissionSetId+FROM+PermissionSetAssignments%29+FROM+User+WHERE+Username+%3D+%27test-cci%40example.com%27",
561
+ status=200,
562
+ json={
563
+ "done": True,
564
+ "totalSize": 1,
565
+ "records": [
566
+ {
567
+ "Id": "005000000000000",
568
+ "PermissionSetAssignments": None,
569
+ }
570
+ ],
571
+ },
572
+ )
573
+ responses.add(
574
+ method="GET",
575
+ url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/?q=SELECT+Id%2CName+FROM+PermissionSet+WHERE+Name+IN+%28%27testns__PermSet1%27%29",
576
+ status=200,
577
+ json={
578
+ "done": True,
579
+ "totalSize": 1,
580
+ "records": [
581
+ {
582
+ "Id": "0PS000000000000",
583
+ "Name": "testns__PermSet1",
584
+ },
585
+ ],
586
+ },
587
+ )
588
+ responses.add(
589
+ method="POST",
590
+ url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
591
+ status=200,
592
+ json=[
593
+ {"id": "0Pa000000000000", "success": True, "errors": []},
594
+ ],
595
+ )
596
+
597
+ task()
598
+
599
+ assert len(responses.calls) == 3
600
+ # Verify that the SOQL query contains the namespaced permission set name
601
+ assert "testns__PermSet1" in responses.calls[1].request.url
602
+
419
603
 
420
604
  class TestCreatePermissionSetLicense:
421
605
  @responses.activate
File without changes
@@ -0,0 +1,256 @@
1
+ """SFDmu task for CumulusCI."""
2
+
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+
7
+ from cumulusci.core.exceptions import TaskOptionsError
8
+ from cumulusci.core.tasks import BaseSalesforceTask
9
+ from cumulusci.core.utils import determine_managed_mode
10
+ from cumulusci.tasks.command import Command
11
+
12
+
13
+ class SfdmuTask(BaseSalesforceTask, Command):
14
+ """Execute SFDmu data migration with namespace injection support."""
15
+
16
+ salesforce_task = (
17
+ False # Override to False since we manage our own org requirements
18
+ )
19
+
20
+ task_options = {
21
+ "source": {
22
+ "description": "Source org name (CCI org name like dev, beta, qa, etc.) or 'csvfile'",
23
+ "required": True,
24
+ },
25
+ "target": {
26
+ "description": "Target org name (CCI org name like dev, beta, qa, etc.) or 'csvfile'",
27
+ "required": True,
28
+ },
29
+ "path": {
30
+ "description": "Path to folder containing export.json and other CSV files",
31
+ "required": True,
32
+ },
33
+ "additional_params": {
34
+ "description": "Additional parameters to append to the sf sfdmu command (e.g., '--simulation --noprompt --nowarnings')",
35
+ "required": False,
36
+ },
37
+ }
38
+
39
+ def _init_options(self, kwargs):
40
+ super()._init_options(kwargs)
41
+
42
+ # Convert path to absolute path
43
+ self.options["path"] = os.path.abspath(self.options["path"])
44
+
45
+ # Validate that the path exists and contains export.json
46
+ if not os.path.exists(self.options["path"]):
47
+ raise TaskOptionsError(f"Path {self.options['path']} does not exist")
48
+
49
+ export_json_path = os.path.join(self.options["path"], "export.json")
50
+ if not os.path.exists(export_json_path):
51
+ raise TaskOptionsError(f"export.json not found in {self.options['path']}")
52
+
53
+ def _validate_org(self, org_name):
54
+ """Validate that a CCI org exists and return the org config."""
55
+ if org_name == "csvfile":
56
+ return None
57
+
58
+ try:
59
+ if self.project_config.keychain is None:
60
+ raise TaskOptionsError("No keychain available")
61
+ org_config = self.project_config.keychain.get_org(org_name)
62
+ return org_config
63
+ except Exception as e:
64
+ raise TaskOptionsError(f"Org '{org_name}' does not exist: {str(e)}")
65
+
66
+ def _get_sf_org_name(self, org_config):
67
+ """Get the SF org name from org config."""
68
+ if hasattr(org_config, "sfdx_alias") and org_config.sfdx_alias:
69
+ return org_config.sfdx_alias
70
+ elif hasattr(org_config, "username") and org_config.username:
71
+ return org_config.username
72
+ else:
73
+ raise TaskOptionsError("Could not determine SF org name for org config")
74
+
75
+ def _create_execute_directory(self, base_path):
76
+ """Create /execute directory and copy files from base_path."""
77
+ execute_path = os.path.join(base_path, "execute")
78
+
79
+ # Remove existing execute directory if it exists
80
+ if os.path.exists(execute_path):
81
+ shutil.rmtree(execute_path)
82
+
83
+ # Create execute directory
84
+ os.makedirs(execute_path, exist_ok=True)
85
+
86
+ # Copy only files (not directories) from base_path to execute
87
+ for item in os.listdir(base_path):
88
+ item_path = os.path.join(base_path, item)
89
+ if os.path.isfile(item_path) and item.endswith((".json", ".csv")):
90
+ shutil.copy2(item_path, execute_path)
91
+
92
+ return execute_path
93
+
94
+ def _update_credentials(self):
95
+ """Override to handle cases where org_config might be None."""
96
+ # Only update credentials if we have an org_config
97
+ if self.org_config is not None:
98
+ super()._update_credentials()
99
+
100
+ def _inject_namespace_tokens(self, execute_path, target_org_config):
101
+ """Inject namespace tokens into files in execute directory using the same mechanism as Deploy task."""
102
+ if target_org_config is None: # csvfile case
103
+ return
104
+
105
+ # Get namespace information
106
+ namespace = self.project_config.project__package__namespace
107
+ managed = determine_managed_mode(
108
+ self.options, self.project_config, target_org_config
109
+ )
110
+ namespaced_org = bool(namespace) and namespace == getattr(
111
+ target_org_config, "namespace", None
112
+ )
113
+
114
+ # Create a temporary zipfile with all files from execute directory
115
+ import tempfile
116
+ import zipfile
117
+
118
+ from cumulusci.core.dependencies.utils import TaskContext
119
+ from cumulusci.core.source_transforms.transforms import (
120
+ NamespaceInjectionOptions,
121
+ NamespaceInjectionTransform,
122
+ )
123
+
124
+ with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as temp_zip:
125
+ temp_zip_path = temp_zip.name
126
+
127
+ try:
128
+ # Create zipfile with all files from execute directory
129
+ with zipfile.ZipFile(temp_zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
130
+ for root, dirs, files in os.walk(execute_path):
131
+ for file in files:
132
+ if file.endswith((".json", ".csv")):
133
+ file_path = os.path.join(root, file)
134
+ # Calculate relative path from execute_path
135
+ rel_path = os.path.relpath(file_path, execute_path)
136
+ zf.write(file_path, rel_path)
137
+
138
+ # Apply namespace injection using the same mechanism as Deploy task
139
+ with zipfile.ZipFile(temp_zip_path, "r") as zf:
140
+ # Create namespace injection options
141
+ options = NamespaceInjectionOptions(
142
+ namespace_tokenize=None,
143
+ namespace_inject=namespace,
144
+ namespace_strip=None,
145
+ unmanaged=not managed,
146
+ namespaced_org=namespaced_org,
147
+ )
148
+
149
+ # Create transform
150
+ transform = NamespaceInjectionTransform(options)
151
+
152
+ # Create task context
153
+ context = TaskContext(
154
+ target_org_config, self.project_config, self.logger
155
+ )
156
+
157
+ # Apply namespace injection
158
+ new_zf = transform.process(zf, context)
159
+
160
+ # Extract processed files back to execute directory
161
+ # First, remove all existing files
162
+ for root, dirs, files in os.walk(execute_path):
163
+ for file in files:
164
+ if file.endswith((".json", ".csv")):
165
+ os.remove(os.path.join(root, file))
166
+
167
+ # Extract processed files
168
+ for file_info in new_zf.infolist():
169
+ if file_info.filename.endswith((".json", ".csv")):
170
+ # Extract to execute directory
171
+ target_path = os.path.join(execute_path, file_info.filename)
172
+ # Ensure directory exists
173
+ os.makedirs(os.path.dirname(target_path), exist_ok=True)
174
+ with new_zf.open(file_info) as source:
175
+ with open(target_path, "wb") as target:
176
+ target.write(source.read())
177
+
178
+ self.logger.info(
179
+ f"Applied namespace injection to {file_info.filename}"
180
+ )
181
+
182
+ finally:
183
+ # Clean up temporary zipfile
184
+ if os.path.exists(temp_zip_path):
185
+ os.unlink(temp_zip_path)
186
+
187
+ def _run_task(self):
188
+ """Execute the SFDmu task."""
189
+ # Validate source and target orgs
190
+ source_org_config = self._validate_org(self.options["source"])
191
+ target_org_config = self._validate_org(self.options["target"])
192
+
193
+ # Get SF org names
194
+ if source_org_config:
195
+ source_sf_org = self._get_sf_org_name(source_org_config)
196
+ else:
197
+ source_sf_org = "csvfile"
198
+
199
+ if target_org_config:
200
+ target_sf_org = self._get_sf_org_name(target_org_config)
201
+ else:
202
+ target_sf_org = "csvfile"
203
+
204
+ # Create execute directory and copy files
205
+ execute_path = self._create_execute_directory(self.options["path"])
206
+ self.logger.info(f"Created execute directory at {execute_path}")
207
+
208
+ # Apply namespace injection
209
+ self._inject_namespace_tokens(execute_path, target_org_config)
210
+
211
+ # Build and execute SFDmu command
212
+ command = [
213
+ "sf",
214
+ "sfdmu",
215
+ "run",
216
+ "-s",
217
+ source_sf_org,
218
+ "-u",
219
+ target_sf_org,
220
+ "-p",
221
+ execute_path,
222
+ ]
223
+
224
+ # Append additional parameters if provided
225
+ if self.options.get("additional_params"):
226
+ # Split the additional_params string into individual arguments
227
+ # This handles cases like "-no-warnings -m -t error" -> ["-no-warnings", "-m", "-t", "error"]
228
+ additional_args = self.options["additional_params"].split()
229
+ command.extend(additional_args)
230
+
231
+ self.logger.info(f"Executing: {' '.join(command)}")
232
+
233
+ # Execute the command with real-time output
234
+ process = subprocess.Popen(
235
+ command,
236
+ stdout=subprocess.PIPE,
237
+ stderr=subprocess.STDOUT,
238
+ universal_newlines=True,
239
+ bufsize=1,
240
+ )
241
+
242
+ # Stream output in real-time
243
+ if process.stdout:
244
+ for line in iter(process.stdout.readline, ""):
245
+ if line:
246
+ self.logger.info(line.rstrip())
247
+
248
+ process.wait()
249
+
250
+ # Check return code
251
+ if process.returncode != 0:
252
+ raise TaskOptionsError(
253
+ f"SFDmu command failed with return code {process.returncode}"
254
+ )
255
+
256
+ self.logger.info("SFDmu task completed successfully")
@@ -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 target is 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": "dev", "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)
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, 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, 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())