|
1 | 1 | """Tests for the deploy module.""" |
2 | 2 |
|
| 3 | +import json |
3 | 4 | from unittest.mock import ( |
4 | 5 | MagicMock, |
5 | 6 | mock_open, |
|
27 | 28 | DeploymentsResponse, |
28 | 29 | _make_api_call, |
29 | 30 | _retrieve_access_token, |
| 31 | + _retrieve_access_token_from_sf_cli, |
30 | 32 | create_data_transform, |
31 | 33 | create_deployment, |
32 | 34 | deploy_full, |
@@ -1107,3 +1109,144 @@ def test_deploy_full_happy_path( |
1107 | 1109 | "/test/dir", access_token, metadata, data_transform_config |
1108 | 1110 | ) |
1109 | 1111 | assert result == access_token |
| 1112 | + |
| 1113 | + |
| 1114 | +class TestRetrieveAccessTokenFromSFCLI: |
| 1115 | + """Tests for _retrieve_access_token_from_sf_cli.""" |
| 1116 | + |
| 1117 | + SF_CLI_OUTPUT = json.dumps( |
| 1118 | + { |
| 1119 | + "status": 0, |
| 1120 | + "result": { |
| 1121 | + "accessToken": "sf_access_token", |
| 1122 | + "instanceUrl": "https://sf.salesforce.com", |
| 1123 | + }, |
| 1124 | + } |
| 1125 | + ) |
| 1126 | + |
| 1127 | + @patch("datacustomcode.deploy.subprocess.run") |
| 1128 | + def test_happy_path(self, mock_run): |
| 1129 | + """Successful sf org display returns AccessTokenResponse.""" |
| 1130 | + mock_run.return_value = MagicMock(stdout=self.SF_CLI_OUTPUT, returncode=0) |
| 1131 | + |
| 1132 | + result = _retrieve_access_token_from_sf_cli("my-org") |
| 1133 | + |
| 1134 | + assert result.access_token == "sf_access_token" |
| 1135 | + assert result.instance_url == "https://sf.salesforce.com" |
| 1136 | + mock_run.assert_called_once_with( |
| 1137 | + ["sf", "org", "display", "--target-org", "my-org", "--json"], |
| 1138 | + capture_output=True, |
| 1139 | + text=True, |
| 1140 | + check=True, |
| 1141 | + timeout=30, |
| 1142 | + ) |
| 1143 | + |
| 1144 | + @patch("datacustomcode.deploy.subprocess.run") |
| 1145 | + def test_file_not_found(self, mock_run): |
| 1146 | + """FileNotFoundError raised when sf CLI is not installed.""" |
| 1147 | + mock_run.side_effect = FileNotFoundError("No such file or directory: 'sf'") |
| 1148 | + |
| 1149 | + with pytest.raises(RuntimeError, match="'sf' command was not found"): |
| 1150 | + _retrieve_access_token_from_sf_cli("my-org") |
| 1151 | + |
| 1152 | + @patch("datacustomcode.deploy.subprocess.run") |
| 1153 | + def test_timeout_expired(self, mock_run): |
| 1154 | + """TimeoutExpired raised when sf CLI times out.""" |
| 1155 | + import subprocess |
| 1156 | + |
| 1157 | + mock_run.side_effect = subprocess.TimeoutExpired(cmd="sf", timeout=30) |
| 1158 | + |
| 1159 | + with pytest.raises(RuntimeError, match="timed out"): |
| 1160 | + _retrieve_access_token_from_sf_cli("my-org") |
| 1161 | + |
| 1162 | + @patch("datacustomcode.deploy.subprocess.run") |
| 1163 | + def test_called_process_error(self, mock_run): |
| 1164 | + """CalledProcessError raised when sf CLI exits non-zero.""" |
| 1165 | + import subprocess |
| 1166 | + |
| 1167 | + mock_run.side_effect = subprocess.CalledProcessError( |
| 1168 | + returncode=1, cmd="sf", stderr="Org not found" |
| 1169 | + ) |
| 1170 | + |
| 1171 | + with pytest.raises(RuntimeError, match="failed for org"): |
| 1172 | + _retrieve_access_token_from_sf_cli("my-org") |
| 1173 | + |
| 1174 | + @patch("datacustomcode.deploy.subprocess.run") |
| 1175 | + def test_json_decode_error(self, mock_run): |
| 1176 | + """RuntimeError raised when output is not valid JSON.""" |
| 1177 | + mock_run.return_value = MagicMock(stdout="not-json", returncode=0) |
| 1178 | + |
| 1179 | + with pytest.raises(RuntimeError, match="Failed to parse"): |
| 1180 | + _retrieve_access_token_from_sf_cli("my-org") |
| 1181 | + |
| 1182 | + @patch("datacustomcode.deploy.subprocess.run") |
| 1183 | + def test_nonzero_status_in_json(self, mock_run): |
| 1184 | + """RuntimeError raised when JSON status field is non-zero.""" |
| 1185 | + output = json.dumps({"status": 1, "message": "org not found"}) |
| 1186 | + mock_run.return_value = MagicMock(stdout=output, returncode=0) |
| 1187 | + |
| 1188 | + with pytest.raises(RuntimeError, match="SF CLI error"): |
| 1189 | + _retrieve_access_token_from_sf_cli("my-org") |
| 1190 | + |
| 1191 | + @patch("datacustomcode.deploy.subprocess.run") |
| 1192 | + def test_missing_access_token(self, mock_run): |
| 1193 | + """RuntimeError raised when accessToken is absent.""" |
| 1194 | + output = json.dumps( |
| 1195 | + {"status": 0, "result": {"instanceUrl": "https://sf.salesforce.com"}} |
| 1196 | + ) |
| 1197 | + mock_run.return_value = MagicMock(stdout=output, returncode=0) |
| 1198 | + |
| 1199 | + with pytest.raises(RuntimeError, match="did not return"): |
| 1200 | + _retrieve_access_token_from_sf_cli("my-org") |
| 1201 | + |
| 1202 | + @patch("datacustomcode.deploy.subprocess.run") |
| 1203 | + def test_missing_instance_url(self, mock_run): |
| 1204 | + """RuntimeError raised when instanceUrl is absent.""" |
| 1205 | + output = json.dumps({"status": 0, "result": {"accessToken": "sf_access_token"}}) |
| 1206 | + mock_run.return_value = MagicMock(stdout=output, returncode=0) |
| 1207 | + |
| 1208 | + with pytest.raises(RuntimeError, match="did not return"): |
| 1209 | + _retrieve_access_token_from_sf_cli("my-org") |
| 1210 | + |
| 1211 | + |
| 1212 | +class TestDeployFullWithAccessTokenResponse: |
| 1213 | + """Test deploy_full when passed an AccessTokenResponse directly.""" |
| 1214 | + |
| 1215 | + @patch("datacustomcode.deploy.create_data_transform") |
| 1216 | + @patch("datacustomcode.deploy.wait_for_deployment") |
| 1217 | + @patch("datacustomcode.deploy.upload_zip") |
| 1218 | + @patch("datacustomcode.deploy.zip") |
| 1219 | + @patch("datacustomcode.deploy.create_deployment") |
| 1220 | + @patch("datacustomcode.deploy.get_config") |
| 1221 | + @patch("datacustomcode.deploy._retrieve_access_token") |
| 1222 | + def test_deploy_full_with_access_token_response_skips_token_exchange( |
| 1223 | + self, |
| 1224 | + mock_retrieve_token, |
| 1225 | + mock_get_config, |
| 1226 | + mock_create_deployment, |
| 1227 | + mock_zip, |
| 1228 | + mock_upload_zip, |
| 1229 | + mock_wait, |
| 1230 | + mock_create_transform, |
| 1231 | + ): |
| 1232 | + """deploy_full skips token exchange when given an AccessTokenResponse.""" |
| 1233 | + access_token = AccessTokenResponse( |
| 1234 | + access_token="direct_token", instance_url="https://instance.example.com" |
| 1235 | + ) |
| 1236 | + metadata = CodeExtensionMetadata( |
| 1237 | + name="test", |
| 1238 | + version="1.0.0", |
| 1239 | + description="desc", |
| 1240 | + computeType="CPU_M", |
| 1241 | + codeType="script", |
| 1242 | + ) |
| 1243 | + mock_get_config.return_value = MagicMock(spec=[]) # not DataTransformConfig |
| 1244 | + mock_create_deployment.return_value = CreateDeploymentResponse( |
| 1245 | + fileUploadUrl="https://upload.example.com" |
| 1246 | + ) |
| 1247 | + |
| 1248 | + result = deploy_full("/test/dir", metadata, access_token, "default") |
| 1249 | + |
| 1250 | + mock_retrieve_token.assert_not_called() |
| 1251 | + mock_create_deployment.assert_called_once_with(access_token, metadata) |
| 1252 | + assert result == access_token |
0 commit comments