Skip to content

Commit 87f5b73

Browse files
authored
Merge pull request #106 from forcedotcom/sf-cli-auth-fix
Use new SF CLI command to obtain token for --sf-cli-org usage
2 parents 64d9d12 + 6d6a47a commit 87f5b73

3 files changed

Lines changed: 118 additions & 53 deletions

File tree

src/datacustomcode/token_provider.py

Lines changed: 63 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -101,48 +101,75 @@ def get_token(self) -> "AccessTokenResponse":
101101

102102
from datacustomcode.deploy import AccessTokenResponse
103103

104-
try:
105-
result = subprocess.run(
106-
["sf", "org", "display", "--target-org", self.sf_cli_org, "--json"],
107-
capture_output=True,
108-
text=True,
109-
check=True,
110-
timeout=30,
111-
)
112-
except FileNotFoundError as exc:
113-
raise RuntimeError(
114-
"The 'sf' command was not found. "
115-
"Install Salesforce CLI: https://developer.salesforce.com/tools/salesforcecli"
116-
) from exc
117-
except subprocess.TimeoutExpired as exc:
118-
raise RuntimeError(
119-
f"'sf org display' timed out for org '{self.sf_cli_org}'"
120-
) from exc
121-
except subprocess.CalledProcessError as exc:
122-
raise RuntimeError(
123-
f"'sf org display' failed for org '{self.sf_cli_org}': {exc.stderr}"
124-
) from exc
104+
def _run_sf_command(args: list[str], description: str) -> dict:
105+
try:
106+
result = subprocess.run(
107+
args,
108+
capture_output=True,
109+
text=True,
110+
check=True,
111+
timeout=30,
112+
)
113+
except FileNotFoundError as exc:
114+
raise RuntimeError(
115+
"The 'sf' command was not found. "
116+
"Install Salesforce CLI: "
117+
"https://developer.salesforce.com/tools/salesforcecli"
118+
) from exc
119+
except subprocess.TimeoutExpired as exc:
120+
raise RuntimeError(
121+
f"'{description}' timed out for org '{self.sf_cli_org}'"
122+
) from exc
123+
except subprocess.CalledProcessError as exc:
124+
raise RuntimeError(
125+
f"'{description}' failed for org '{self.sf_cli_org}': "
126+
f"{exc.stderr}"
127+
) from exc
125128

126-
try:
127-
data = json.loads(result.stdout)
128-
except json.JSONDecodeError as exc:
129-
raise RuntimeError(
130-
f"Failed to parse JSON from 'sf org display': {result.stdout}"
131-
) from exc
129+
try:
130+
data = json.loads(result.stdout)
131+
except json.JSONDecodeError as exc:
132+
raise RuntimeError(
133+
f"Failed to parse JSON from '{description}': {result.stdout}"
134+
) from exc
132135

133-
if data.get("status") != 0:
136+
if data.get("status") != 0:
137+
raise RuntimeError(
138+
f"SF CLI error for org '{self.sf_cli_org}': "
139+
f"{data.get('message', 'unknown error')}"
140+
)
141+
return dict(data)
142+
143+
# Get instanceUrl from sf org display
144+
display_data = _run_sf_command(
145+
["sf", "org", "display", "--target-org", self.sf_cli_org, "--json"],
146+
"sf org display",
147+
)
148+
instance_url = display_data.get("result", {}).get("instanceUrl")
149+
if not instance_url:
134150
raise RuntimeError(
135-
f"SF CLI error for org '{self.sf_cli_org}': "
136-
f"{data.get('message', 'unknown error')}"
151+
f"'sf org display' did not return an instance URL "
152+
f"for org '{self.sf_cli_org}'"
137153
)
138154

139-
result_data = data.get("result", {})
140-
access_token = result_data.get("accessToken")
141-
instance_url = result_data.get("instanceUrl")
142-
143-
if not access_token or not instance_url:
155+
# Get access token via show-access-token (newer SF CLI versions
156+
# redact the token in sf org display)
157+
token_data = _run_sf_command(
158+
[
159+
"sf",
160+
"org",
161+
"auth",
162+
"show-access-token",
163+
"--target-org",
164+
self.sf_cli_org,
165+
"--json",
166+
],
167+
"sf org auth show-access-token",
168+
)
169+
access_token = token_data.get("result", {}).get("accessToken")
170+
if not access_token:
144171
raise RuntimeError(
145-
f"'sf org display' did not return an access token or instance URL "
172+
f"'sf org auth show-access-token' did not return an access token "
146173
f"for org '{self.sf_cli_org}'"
147174
)
148175

tests/io/reader/test_sf_cli.py

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -79,23 +79,43 @@ def _run_result(self, stdout: str) -> MagicMock:
7979
return result
8080

8181
def test_returns_token_and_instance_url(self, reader):
82+
display_result = self._run_result(
83+
_sf_display_output("redacted", "https://org.salesforce.com")
84+
)
85+
token_result = self._run_result(
86+
json.dumps({"status": 0, "result": {"accessToken": "mytoken"}})
87+
)
8288
with patch(
8389
"subprocess.run",
84-
return_value=self._run_result(
85-
_sf_display_output("mytoken", "https://org.salesforce.com")
86-
),
90+
side_effect=[display_result, token_result],
8791
) as mock_run:
8892
token, url = reader._get_token()
8993

9094
assert token == "mytoken"
9195
assert url == "https://org.salesforce.com"
92-
mock_run.assert_called_once_with(
96+
assert mock_run.call_count == 2
97+
mock_run.assert_any_call(
9398
["sf", "org", "display", "--target-org", "dev1", "--json"],
9499
capture_output=True,
95100
text=True,
96101
check=True,
97102
timeout=30,
98103
)
104+
mock_run.assert_any_call(
105+
[
106+
"sf",
107+
"org",
108+
"auth",
109+
"show-access-token",
110+
"--target-org",
111+
"dev1",
112+
"--json",
113+
],
114+
capture_output=True,
115+
text=True,
116+
check=True,
117+
timeout=30,
118+
)
99119

100120
def test_file_not_found_raises_runtime_error(self, reader):
101121
with patch("subprocess.run", side_effect=FileNotFoundError):
@@ -148,21 +168,21 @@ def test_nonzero_status_without_message_uses_unknown_error(self, reader):
148168
reader._get_token()
149169

150170
def test_missing_access_token_raises_runtime_error(self, reader):
151-
payload = json.dumps(
171+
display_result = MagicMock()
172+
display_result.stdout = json.dumps(
152173
{"status": 0, "result": {"instanceUrl": "https://x.salesforce.com"}}
153174
)
154-
result = MagicMock()
155-
result.stdout = payload
156-
with patch("subprocess.run", return_value=result):
157-
with pytest.raises(RuntimeError, match="access token or instance URL"):
175+
token_result = MagicMock()
176+
token_result.stdout = json.dumps({"status": 0, "result": {}})
177+
with patch("subprocess.run", side_effect=[display_result, token_result]):
178+
with pytest.raises(RuntimeError, match="did not return an access token"):
158179
reader._get_token()
159180

160181
def test_missing_instance_url_raises_runtime_error(self, reader):
161-
payload = json.dumps({"status": 0, "result": {"accessToken": "tok"}})
162-
result = MagicMock()
163-
result.stdout = payload
164-
with patch("subprocess.run", return_value=result):
165-
with pytest.raises(RuntimeError, match="access token or instance URL"):
182+
display_result = MagicMock()
183+
display_result.stdout = json.dumps({"status": 0, "result": {}})
184+
with patch("subprocess.run", return_value=display_result):
185+
with pytest.raises(RuntimeError, match="did not return an instance URL"):
166186
reader._get_token()
167187

168188

tests/test_token_provider.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,25 +160,43 @@ def test_successful_token_retrieval(self):
160160

161161
provider = SFCLITokenProvider("test_org")
162162

163-
cli_output = json.dumps(
163+
display_output = json.dumps(
164164
{
165165
"status": 0,
166166
"result": {
167-
"accessToken": "cli_access_token",
167+
"accessToken": "[REDACTED]",
168168
"instanceUrl": "https://cli.salesforce.com",
169169
},
170170
}
171171
)
172+
token_output = json.dumps(
173+
{
174+
"status": 0,
175+
"result": {
176+
"accessToken": "cli_access_token",
177+
},
178+
}
179+
)
172180

173181
with patch("subprocess.run") as mock_run:
174-
mock_run.return_value = MagicMock(stdout=cli_output)
182+
mock_run.side_effect = [
183+
MagicMock(stdout=display_output),
184+
MagicMock(stdout=token_output),
185+
]
175186

176187
result = provider.get_token()
177188

178189
assert isinstance(result, AccessTokenResponse)
179190
assert result.access_token == "cli_access_token"
180191
assert result.instance_url == "https://cli.salesforce.com"
181192

193+
# Verify both commands were called
194+
assert mock_run.call_count == 2
195+
display_call = mock_run.call_args_list[0]
196+
token_call = mock_run.call_args_list[1]
197+
assert "org" in display_call[0][0] and "display" in display_call[0][0]
198+
assert "show-access-token" in token_call[0][0]
199+
182200
def test_sf_command_not_found(self):
183201
"""Test that FileNotFoundError is wrapped in RuntimeError."""
184202
provider = SFCLITokenProvider("test_org")

0 commit comments

Comments
 (0)