4141 logs=cloud_base + "/v1",
4242 )
4343
44+ token_read_delay = os.environ.get("RSCONNECT_E2E_TOKEN_READ_DELAY")
45+ if token_read_delay:
46+ import http.client
47+ import time
48+
49+ original_read = http.client.HTTPResponse.read
50+
51+ def read_completed_token(self, *args, **kwargs):
52+ body = original_read(self, *args, **kwargs)
53+ if self.status == 200 and b'"access_token"' in body:
54+ time.sleep(float(token_read_delay))
55+ return body
56+
57+ http.client.HTTPResponse.read = read_completed_token
58+
4459runpy.run_module("rsconnect.main", run_name="__main__")
4560"""
4661
@@ -360,6 +375,7 @@ def _cli_environment(home: Path) -> dict[str, str]:
360375 "CONNECT_CLOUD_OAUTH_CLIENT_ID" ,
361376 "SHINYAPPS_ACCOUNT" ,
362377 "RSCONNECT_E2E_CLOUD_BASE_URL" ,
378+ "RSCONNECT_E2E_TOKEN_READ_DELAY" ,
363379 "HTTP_PROXY" ,
364380 "http_proxy" ,
365381 "HTTPS_PROXY" ,
@@ -1327,6 +1343,59 @@ def test_cloud_finish_deadline_covers_delayed_refresh_and_keeps_checkpoint(
13271343 assert saved ["connect_cloud_refresh_token" ] == "refresh-token"
13281344
13291345
1346+ @pytest .mark .parametrize ("kind" , ["connect" , "cloud" ])
1347+ def test_completed_token_response_is_checkpointed_after_deadline_and_device_expiry (
1348+ tmp_path : Path , local_http_server : _LocalHTTPServer , kind : str
1349+ ) -> None :
1350+ home = tmp_path / "home"
1351+ environment = _cli_environment (home )
1352+ name = "completed-token-" + kind
1353+ if kind == "cloud" :
1354+ start_args = ["add" , "--connect-cloud" , "--account" , "team" , "--name" , name , "--no-wait" ]
1355+ finish_args = ["add" , "--connect-cloud" , "--name" , name , "--finish" ]
1356+ cloud_base = local_http_server .base_url
1357+ else :
1358+ start_args = ["login" , "--server" , local_http_server .base_url , "--name" , name , "--no-wait" ]
1359+ finish_args = ["login" , "--name" , name , "--finish" ]
1360+ cloud_base = None
1361+
1362+ started = _run_cli (start_args , environment , cloud_base_url = cloud_base )
1363+ assert started .returncode == 0 , _output (started )
1364+ local_http_server .approved = True
1365+ time .sleep (1.1 )
1366+ delayed_environment = {** environment , "RSCONNECT_E2E_TOKEN_READ_DELAY" : "1.2" }
1367+ pending = _run_cli ([* finish_args , "--timeout" , "1" ], delayed_environment , cloud_base_url = cloud_base )
1368+
1369+ assert pending .returncode == 0 , _output (pending )
1370+ assert _json_output (pending )["status" ] == "pending"
1371+ state_path = _device_states (home , kind )[0 ]
1372+ state = json .loads (state_path .read_text (encoding = "utf-8" ))
1373+ assert state ["tokens" ]["access_token" ] == "access-token"
1374+ assert state ["tokens" ]["refresh_token" ] == "refresh-token"
1375+ state ["expires_at" ] = time .time () - 1
1376+ state_path .write_text (json .dumps (state ), encoding = "utf-8" )
1377+ local_http_server .device_error = "invalid_grant"
1378+
1379+ restarted = _run_cli (start_args , environment , cloud_base_url = cloud_base )
1380+ assert restarted .returncode == 0 , _output (restarted )
1381+ assert _json_output (restarted )["expires_in" ] == 0
1382+ assert json .loads (state_path .read_text (encoding = "utf-8" ))["tokens" ] == state ["tokens" ]
1383+ assert len (_requests (local_http_server , "/oauth/device/authorize" )) == 1
1384+
1385+ finished = _run_cli ([* finish_args , "--timeout" , "5" ], environment , cloud_base_url = cloud_base )
1386+ assert finished .returncode == 0 , _output (finished )
1387+ assert _json_output (finished )["status" ] == "done"
1388+ assert not _device_states (home , kind )
1389+ assert len (_requests (local_http_server , "/oauth/token" )) == 1
1390+ saved = _saved_servers (home )[name ]
1391+ token_field = "connect_cloud_access_token" if kind == "cloud" else "oauth_access_token"
1392+ assert saved [token_field ] == "access-token"
1393+ for result in (started , pending , restarted , finished ):
1394+ assert "access-token" not in _output (result )
1395+ assert "refresh-token" not in _output (result )
1396+ assert "local-device-code" not in _output (result )
1397+
1398+
13301399@pytest .mark .parametrize ("phase" , ["headers" , "body" ])
13311400@pytest .mark .parametrize ("kind" , ["connect" , "cloud" ])
13321401def test_finish_deadline_interrupts_slow_responses_and_can_resume (
@@ -1780,7 +1849,7 @@ def test_oversized_valid_deployment_record_is_unknown_and_explicit_id_bypasses_i
17801849@pytest .mark .parametrize (
17811850 "contents, warning_text" ,
17821851 [
1783- pytest .param ("[" * 10000 + "0" + "]" * 10000 , "recursion " , id = "nested-json" ),
1852+ pytest .param ("[" * 10000 + "0" + "]" * 10000 , "deployment metadata " , id = "nested-json" ),
17841853 pytest .param (
17851854 '{"app_id":' + "9" * 5000 + "}" ,
17861855 "integer string conversion" ,
@@ -1792,7 +1861,7 @@ def test_oversized_valid_deployment_record_is_unknown_and_explicit_id_bypasses_i
17921861 ),
17931862 ],
17941863)
1795- def test_parser_rejected_deployment_record_is_unknown_without_fixing (
1864+ def test_unusable_deployment_record_is_unknown_without_fixing (
17961865 tmp_path : Path , local_http_server : _LocalHTTPServer , runtime : str , contents : str , warning_text : str
17971866) -> None :
17981867 environment = _cli_environment (tmp_path / "home" )
@@ -1818,6 +1887,7 @@ def test_parser_rejected_deployment_record_is_unknown_without_fixing(
18181887 assert report ["changed_files" ] == []
18191888 assert report ["actions" ]
18201889 assert any (warning_text in warning .lower () for warning in report ["warnings" ])
1890+ assert record .read_text (encoding = "utf-8" ) == contents
18211891 assert not (project / ".python-version" ).exists ()
18221892 assert not _requests (local_http_server , "/__api__/v1/content/" + _CONTENT_GUID )
18231893
@@ -1876,8 +1946,9 @@ def test_resumable_reauthentication_preserves_legacy_url_and_content_history(
18761946 assert not (project / ".python-version" ).exists ()
18771947
18781948
1949+ @pytest .mark .parametrize ("filename" , [None , "app.py" , "manifest.json" ])
18791950def test_safe_deployment_history_still_selects_its_server_before_the_default (
1880- tmp_path : Path , local_http_server : _LocalHTTPServer
1951+ tmp_path : Path , local_http_server : _LocalHTTPServer , filename : str | None
18811952) -> None :
18821953 home = tmp_path / "home"
18831954 environment = _cli_environment (home )
@@ -1893,7 +1964,9 @@ def test_safe_deployment_history_still_selects_its_server_before_the_default(
18931964 next (home .rglob ("servers.json" )).write_text (json .dumps (servers ), encoding = "utf-8" )
18941965 project = tmp_path / "history-target-project"
18951966 project .mkdir ()
1896- module_file = project / (project .name + ".py" )
1967+ module_file = project / (filename or project .name + ".py" )
1968+ if filename :
1969+ module_file .write_text ("{}" if filename .endswith (".json" ) else "app = object()\n " , encoding = "utf-8" )
18971970 AppStore (str (module_file )).set (
18981971 local_http_server .base_url ,
18991972 str (module_file ),
@@ -1904,7 +1977,8 @@ def test_safe_deployment_history_still_selects_its_server_before_the_default(
19041977 "python-api" ,
19051978 )
19061979
1907- result = _run_cli (["preflight" , str (project ), "--fix" ], environment )
1980+ target = module_file if filename else project
1981+ result = _run_cli (["preflight" , str (target ), "--fix" ], environment )
19081982
19091983 assert result .returncode == 0 , _output (result )
19101984 report = _json_output (result )
0 commit comments