cumulusci-plus 5.0.23__py3-none-any.whl → 5.0.25__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.
- cumulusci/__about__.py +1 -1
- cumulusci/cli/task.py +17 -0
- cumulusci/cli/tests/test_flow.py +279 -2
- cumulusci/cli/tests/test_task.py +88 -2
- cumulusci/core/flowrunner.py +86 -6
- cumulusci/cumulusci.yml +24 -0
- cumulusci/tasks/create_package_version.py +14 -6
- cumulusci/tasks/salesforce/SfPackageCommands.py +363 -0
- cumulusci/tasks/salesforce/getPackageVersion.py +89 -0
- cumulusci/tasks/salesforce/tests/test_SfPackageCommands.py +554 -0
- cumulusci/tasks/salesforce/tests/test_getPackageVersion.py +651 -0
- cumulusci/tasks/salesforce/tests/test_update_external_credential.py +912 -0
- cumulusci/tasks/salesforce/tests/test_update_named_credential.py +1042 -0
- cumulusci/tasks/salesforce/update_external_credential.py +562 -0
- cumulusci/tasks/salesforce/update_named_credential.py +441 -0
- cumulusci/tasks/salesforce/users/permsets.py +63 -2
- cumulusci/tasks/salesforce/users/tests/test_permsets.py +184 -0
- cumulusci/tasks/sfdmu/__init__.py +0 -0
- cumulusci/tasks/sfdmu/sfdmu.py +256 -0
- cumulusci/tasks/sfdmu/tests/__init__.py +1 -0
- cumulusci/tasks/sfdmu/tests/test_runner.py +212 -0
- cumulusci/tasks/sfdmu/tests/test_sfdmu.py +443 -0
- cumulusci/utils/__init__.py +24 -2
- {cumulusci_plus-5.0.23.dist-info → cumulusci_plus-5.0.25.dist-info}/METADATA +7 -5
- {cumulusci_plus-5.0.23.dist-info → cumulusci_plus-5.0.25.dist-info}/RECORD +29 -16
- {cumulusci_plus-5.0.23.dist-info → cumulusci_plus-5.0.25.dist-info}/WHEEL +0 -0
- {cumulusci_plus-5.0.23.dist-info → cumulusci_plus-5.0.25.dist-info}/entry_points.txt +0 -0
- {cumulusci_plus-5.0.23.dist-info → cumulusci_plus-5.0.25.dist-info}/licenses/AUTHORS.rst +0 -0
- {cumulusci_plus-5.0.23.dist-info → cumulusci_plus-5.0.25.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())
|