Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- When client credentials can't publish to a Posit Connect Cloud account because the
account requires single sign-on (SSO), the error now says so, instead of
suggesting the publisher role.
- `rsconnect bootstrap` uses the `/__api__/v1/bootstrap` endpoint, falling back to
the deprecated `/__api__/v1/experimental/bootstrap` endpoint on Posit Connect
versions before 2024.05.0. (#594)

## [1.32.0] - 2026-10-16

Expand Down
6 changes: 5 additions & 1 deletion rsconnect/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,11 @@ def me(self) -> UserRecord:
return response

def bootstrap(self) -> BootstrapOutputDTO | HTTPResponse:
response = cast(Union[BootstrapOutputDTO, HTTPResponse], self.post("v1/experimental/bootstrap"))
response = cast(Union[BootstrapOutputDTO, HTTPResponse], self.post("v1/bootstrap"))
# Connect versions before 2024.05.0 only provide the experimental endpoint. The server version
# is not exposed to unauthenticated requests, so fall back based on the 404 instead.
if isinstance(response, HTTPResponse) and response.status == 404:
response = cast(Union[BootstrapOutputDTO, HTTPResponse], self.post("v1/experimental/bootstrap"))
# TODO: The place where bootstrap() is called expects a JSON object if the response is successfule, and a
# HTTPResponse if it is not; then it handles the error. This is different from the other methods, and probably
# should be changed in the future. For this to work, we will _not_ call .handle_bad_response() here at present.
Expand Down
46 changes: 40 additions & 6 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1756,7 +1756,8 @@ def test_env_management_callback(self):
class TestBootstrap(TestCase):
def setUp(self):
self.mock_server = "http://localhost:8080"
self.mock_uri = "http://localhost:8080/__api__/v1/experimental/bootstrap"
self.mock_uri = "http://localhost:8080/__api__/v1/bootstrap"
self.mock_experimental_uri = "http://localhost:8080/__api__/v1/experimental/bootstrap"
self.jwt_keypath = "tests/testdata/jwt/secret.key"
self.jwt_env_secret = "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU="

Expand All @@ -1769,7 +1770,9 @@ def setUp(self):
"--insecure",
]

def create_bootstrap_mock_callback(self, status, json_data):
def create_bootstrap_mock_callback(self, status, json_data, expected_uri=None):
expected_uri = expected_uri or self.mock_uri

def request_callback(request, uri, response_headers):
# verify auth header is sent correctly
authorization = request.headers.get("Authorization")
Expand All @@ -1779,7 +1782,7 @@ def request_callback(request, uri, response_headers):
self.assertTrue(has_jwt_structure(auth_split[1]))

# verify uri
self.assertEqual(uri, self.mock_uri)
self.assertEqual(uri, expected_uri)

return [status, {"Content-Type": "application/json"}, json.dumps(json_data)]

Expand Down Expand Up @@ -1808,6 +1811,34 @@ def test_bootstrap(self):
expected_output = json.loads(open("tests/testdata/initial-admin-responses/success.json", "r").read())
self.assertEqual(json_output, expected_output)

@httpretty.activate(verbose=True, allow_net_connect=False)
def test_bootstrap_experimental_fallback(self):
"""
Fall back to the experimental endpoint on Connect versions before 2024.05.0
"""

httpretty.register_uri(
httpretty.POST,
self.mock_uri,
body=self.create_bootstrap_mock_callback(404, {}),
)
httpretty.register_uri(
httpretty.POST,
self.mock_experimental_uri,
body=self.create_bootstrap_mock_callback(
200, {"api_key": "testapikey123"}, expected_uri=self.mock_experimental_uri
),
)

runner = CliRunner()
result = runner.invoke(cli, self.default_cli_args)

self.assertEqual(result.exit_code, 0, result.output)

json_output = json.loads(result.output)
expected_output = json.loads(open("tests/testdata/initial-admin-responses/success.json", "r").read())
self.assertEqual(json_output, expected_output)

@httpretty.activate(verbose=True, allow_net_connect=False)
def test_bootstrap_env_var(self):
"""
Expand Down Expand Up @@ -1870,12 +1901,15 @@ def test_bootstrap_not_found_error(self):
Fail reasonablly if response indicates 404 not found
"""

callback = self.create_bootstrap_mock_callback(404, {})

httpretty.register_uri(
httpretty.POST,
self.mock_uri,
body=callback,
body=self.create_bootstrap_mock_callback(404, {}),
)
httpretty.register_uri(
httpretty.POST,
self.mock_experimental_uri,
body=self.create_bootstrap_mock_callback(404, {}, expected_uri=self.mock_experimental_uri),
)

runner = CliRunner()
Expand Down
Loading