Skip to content

Commit 9db1001

Browse files
committed
adding some new tests that defines the contract between sf cli and python cli
1 parent 6f21a6b commit 9db1001

1 file changed

Lines changed: 300 additions & 0 deletions

File tree

tests/test_sf_cli_contract.py

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
"""
2+
Contract tests: verify that the Python CLI accepts exactly the argument signatures
3+
passed by the SF CLI plugin (@salesforce/plugin-data-code-extension v0.1.4) via spawn().
4+
5+
These tests do NOT exercise business logic. They verify that:
6+
1. All flags recognised by the SF CLI plugin are still accepted by the Python CLI.
7+
2. Exit code is never 2 (Click's "bad usage" code) for valid SF CLI invocations.
8+
3. stdout patterns parsed by the SF CLI plugin are present in the Python CLI output.
9+
10+
Source of truth for expected args and stdout regex patterns:
11+
data-code-extension/src/utils/datacodeBinaryExecutor.ts
12+
"""
13+
14+
import os
15+
import re
16+
from typing import ClassVar
17+
from unittest.mock import patch
18+
19+
from click.testing import CliRunner
20+
21+
from datacustomcode.cli import (
22+
deploy,
23+
init,
24+
run,
25+
scan,
26+
zip as zip_cmd,
27+
)
28+
from datacustomcode.deploy import AccessTokenResponse
29+
30+
31+
class TestInitArgContract:
32+
"""
33+
SF CLI spawn: datacustomcode init --code-type <script|function> <packageDir>
34+
Ref: executeBinaryInit()
35+
"""
36+
37+
@patch("datacustomcode.template.copy_script_template")
38+
@patch("datacustomcode.scan.dc_config_json_from_file")
39+
@patch("datacustomcode.scan.update_config")
40+
@patch("datacustomcode.scan.write_sdk_config")
41+
def test_accepts_code_type_script(
42+
self, mock_write_sdk, mock_update, mock_scan, mock_copy
43+
):
44+
mock_scan.return_value = {}
45+
mock_update.return_value = {}
46+
runner = CliRunner()
47+
with runner.isolated_filesystem():
48+
os.makedirs(os.path.join("mydir", "payload"), exist_ok=True)
49+
result = runner.invoke(init, ["--code-type", "script", "mydir"])
50+
assert result.exit_code != 2, result.output
51+
52+
@patch("datacustomcode.template.copy_function_template")
53+
@patch("datacustomcode.scan.dc_config_json_from_file")
54+
@patch("datacustomcode.scan.update_config")
55+
@patch("datacustomcode.scan.write_sdk_config")
56+
def test_accepts_code_type_function(
57+
self, mock_write_sdk, mock_update, mock_scan, mock_copy
58+
):
59+
mock_scan.return_value = {}
60+
mock_update.return_value = {}
61+
runner = CliRunner()
62+
with runner.isolated_filesystem():
63+
os.makedirs(os.path.join("mydir", "payload"), exist_ok=True)
64+
result = runner.invoke(init, ["--code-type", "function", "mydir"])
65+
assert result.exit_code != 2, result.output
66+
67+
68+
class TestScanArgContract:
69+
"""
70+
SF CLI spawn: datacustomcode scan [--dry-run] [--no-requirements]
71+
[--config <file>] <entrypoint>
72+
Ref: executeBinaryScan()
73+
"""
74+
75+
@patch("datacustomcode.scan.update_config")
76+
def test_accepts_positional_entrypoint(self, mock_update):
77+
mock_update.return_value = {}
78+
runner = CliRunner()
79+
result = runner.invoke(scan, ["--dry-run", "payload/entrypoint.py"])
80+
assert result.exit_code != 2, result.output
81+
82+
@patch("datacustomcode.scan.update_config")
83+
def test_accepts_dry_run_flag(self, mock_update):
84+
mock_update.return_value = {}
85+
runner = CliRunner()
86+
result = runner.invoke(scan, ["--dry-run", "payload/entrypoint.py"])
87+
assert result.exit_code != 2, result.output
88+
89+
@patch("datacustomcode.scan.update_config")
90+
def test_accepts_no_requirements_flag(self, mock_update):
91+
mock_update.return_value = {}
92+
runner = CliRunner()
93+
result = runner.invoke(
94+
scan, ["--no-requirements", "--dry-run", "payload/entrypoint.py"]
95+
)
96+
assert result.exit_code != 2, result.output
97+
98+
@patch("datacustomcode.scan.update_config")
99+
def test_accepts_config_flag(self, mock_update):
100+
mock_update.return_value = {}
101+
runner = CliRunner()
102+
result = runner.invoke(
103+
scan,
104+
["--config", "custom/config.json", "--dry-run", "payload/entrypoint.py"],
105+
)
106+
assert result.exit_code != 2, result.output
107+
108+
109+
class TestZipArgContract:
110+
"""
111+
SF CLI spawn: datacustomcode zip [--network <network>] <packageDir>
112+
Ref: executeBinaryZip()
113+
"""
114+
115+
@patch("datacustomcode.deploy.zip")
116+
@patch("datacustomcode.scan.find_base_directory")
117+
@patch("datacustomcode.scan.get_package_type")
118+
def test_accepts_positional_packagedir(
119+
self, mock_pkg_type, mock_find_base, mock_zip
120+
):
121+
mock_find_base.return_value = "payload"
122+
mock_pkg_type.return_value = "script"
123+
runner = CliRunner()
124+
result = runner.invoke(zip_cmd, ["payload"])
125+
assert result.exit_code != 2, result.output
126+
127+
@patch("datacustomcode.deploy.zip")
128+
@patch("datacustomcode.scan.find_base_directory")
129+
@patch("datacustomcode.scan.get_package_type")
130+
def test_accepts_network_flag(self, mock_pkg_type, mock_find_base, mock_zip):
131+
mock_find_base.return_value = "payload"
132+
mock_pkg_type.return_value = "script"
133+
runner = CliRunner()
134+
result = runner.invoke(zip_cmd, ["--network", "custom", "payload"])
135+
assert result.exit_code != 2, result.output
136+
137+
138+
class TestDeployArgContract:
139+
"""
140+
SF CLI spawn:
141+
datacustomcode deploy
142+
--name <name> --version <ver> --description <desc>
143+
--path <dir> --sf-cli-org <org> --cpu-size <size>
144+
[--network <net>] [--function-invoke-opt <opt>]
145+
Ref: executeBinaryDeploy()
146+
"""
147+
148+
_BASE_ARGS: ClassVar[list[str]] = [
149+
"--name", "my-pkg",
150+
"--version", "1.0.0",
151+
"--description", "My description",
152+
"--path", "payload",
153+
"--sf-cli-org", "my-org",
154+
"--cpu-size", "CPU_2XL",
155+
] # fmt: skip
156+
157+
@patch("datacustomcode.deploy._retrieve_access_token_from_sf_cli")
158+
@patch("datacustomcode.deploy.deploy_full")
159+
@patch("datacustomcode.cli.find_base_directory")
160+
@patch("datacustomcode.cli.get_package_type")
161+
def test_accepts_required_flags(
162+
self, mock_pkg_type, mock_find_base, mock_deploy_full, mock_sf_cli_token
163+
):
164+
mock_find_base.return_value = "payload"
165+
mock_pkg_type.return_value = "script"
166+
mock_sf_cli_token.return_value = AccessTokenResponse(
167+
access_token="tok", instance_url="https://example.com"
168+
)
169+
runner = CliRunner()
170+
result = runner.invoke(deploy, self._BASE_ARGS)
171+
assert result.exit_code != 2, result.output
172+
173+
@patch("datacustomcode.deploy._retrieve_access_token_from_sf_cli")
174+
@patch("datacustomcode.deploy.deploy_full")
175+
@patch("datacustomcode.cli.find_base_directory")
176+
@patch("datacustomcode.cli.get_package_type")
177+
def test_accepts_network_flag(
178+
self, mock_pkg_type, mock_find_base, mock_deploy_full, mock_sf_cli_token
179+
):
180+
mock_find_base.return_value = "payload"
181+
mock_pkg_type.return_value = "script"
182+
mock_sf_cli_token.return_value = AccessTokenResponse(
183+
access_token="tok", instance_url="https://example.com"
184+
)
185+
runner = CliRunner()
186+
result = runner.invoke(deploy, [*self._BASE_ARGS, "--network", "custom"])
187+
assert result.exit_code != 2, result.output
188+
189+
@patch("datacustomcode.deploy._retrieve_access_token_from_sf_cli")
190+
@patch("datacustomcode.deploy.deploy_full")
191+
@patch("datacustomcode.cli.find_base_directory")
192+
@patch("datacustomcode.cli.get_package_type")
193+
def test_accepts_function_invoke_opt_flag(
194+
self, mock_pkg_type, mock_find_base, mock_deploy_full, mock_sf_cli_token
195+
):
196+
mock_find_base.return_value = "payload"
197+
mock_pkg_type.return_value = "function"
198+
mock_sf_cli_token.return_value = AccessTokenResponse(
199+
access_token="tok", instance_url="https://example.com"
200+
)
201+
runner = CliRunner()
202+
result = runner.invoke(
203+
deploy, [*self._BASE_ARGS, "--function-invoke-opt", "ASYNC"]
204+
)
205+
assert result.exit_code != 2, result.output
206+
207+
208+
class TestRunArgContract:
209+
"""
210+
SF CLI spawn:
211+
datacustomcode run --sf-cli-org <org>
212+
[--config-file <file>] [--dependencies <deps>] <packageDir>
213+
Ref: executeBinaryRun()
214+
215+
Known incompatibility: SF CLI passes `--dependencies` once as a single string.
216+
Python CLI declares multiple=True, so the value arrives as a 1-tuple containing
217+
the raw string rather than individual dep names.
218+
"""
219+
220+
@patch("datacustomcode.run.run_entrypoint")
221+
def test_accepts_sf_cli_org_and_positional(self, mock_run):
222+
runner = CliRunner()
223+
result = runner.invoke(run, ["--sf-cli-org", "my-org", "payload/entrypoint.py"])
224+
assert result.exit_code != 2, result.output
225+
226+
@patch("datacustomcode.run.run_entrypoint")
227+
def test_accepts_config_file_flag(self, mock_run):
228+
runner = CliRunner()
229+
result = runner.invoke(
230+
run,
231+
[
232+
"--sf-cli-org",
233+
"my-org",
234+
"--config-file",
235+
"payload/config.json",
236+
"payload/entrypoint.py",
237+
],
238+
)
239+
assert result.exit_code != 2, result.output
240+
241+
@patch("datacustomcode.run.run_entrypoint")
242+
def test_accepts_dependencies_as_single_string(self, mock_run):
243+
"""SF CLI passes --dependencies once as a comma-separated string.
244+
245+
Python CLI uses multiple=True, so run_entrypoint receives ("dep1,dep2",)
246+
not ("dep1", "dep2"). The string is NOT split on commas.
247+
"""
248+
runner = CliRunner()
249+
result = runner.invoke(
250+
run,
251+
[
252+
"--sf-cli-org",
253+
"my-org",
254+
"--dependencies",
255+
"dep1,dep2",
256+
"payload/entrypoint.py",
257+
],
258+
)
259+
assert result.exit_code != 2, result.output
260+
# Document the incompatibility: SF CLI passes a single "dep1,dep2" string,
261+
# but run_entrypoint receives ("dep1,dep2",) — not ("dep1", "dep2").
262+
assert mock_run.call_args[0][2] == ("dep1,dep2",)
263+
264+
265+
class TestSfCliOutputRegexContract:
266+
"""
267+
The SF CLI plugin parses stdout from each command with regex patterns.
268+
These tests verify that the Python CLI's actual output matches what the
269+
plugin expects (v0.1.4).
270+
271+
Ref: stdout parsing in each executeBinary*() method of datacodeBinaryExecutor.ts.
272+
"""
273+
274+
# ── init ──────────────────────────────────────────────────────────────────
275+
276+
@patch("datacustomcode.template.copy_script_template")
277+
@patch("datacustomcode.scan.dc_config_json_from_file")
278+
@patch("datacustomcode.scan.update_config")
279+
@patch("datacustomcode.scan.write_sdk_config")
280+
def test_init_copying_template_pattern(
281+
self, mock_write_sdk, mock_update, mock_scan, mock_copy
282+
):
283+
"""SF CLI parses 'Copying template to <dir>' from init stdout."""
284+
mock_scan.return_value = {}
285+
mock_update.return_value = {}
286+
runner = CliRunner()
287+
with runner.isolated_filesystem():
288+
os.makedirs(os.path.join("mydir", "payload"), exist_ok=True)
289+
result = runner.invoke(init, ["--code-type", "script", "mydir"])
290+
assert re.search(r"Copying template to .+", result.output)
291+
292+
# ── scan ──────────────────────────────────────────────────────────────────
293+
294+
@patch("datacustomcode.scan.update_config")
295+
def test_scan_scanning_file_pattern(self, mock_update):
296+
"""SF CLI parses 'Scanning <file>...' from scan stdout."""
297+
mock_update.return_value = {}
298+
runner = CliRunner()
299+
result = runner.invoke(scan, ["--dry-run", "payload/entrypoint.py"])
300+
assert re.search(r"Scanning .+\.\.\.", result.output)

0 commit comments

Comments
 (0)