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
5 changes: 4 additions & 1 deletion docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Servers can now be marked as the default with `rsconnect add --set-default`.
When neither `-n/--name` nor `-s/--server` is provided, the default server is
used automatically. `rsconnect login` sets the server as default unless
`--no-set-default` is passed. `CONNECT_SERVER` still takes precedence.
`--no-set-default` is passed. `CONNECT_SERVER` still takes precedence. An
already-saved server can be made the default without re-adding it using
`rsconnect server set-default -n <name>`. The new `rsconnect server` command
group also aliases `add`, `list`, `remove`, `details`, and `bootstrap`.
- New `environment` subcommand for managing execution environments on Connect.
- New `integration` subcommand for managing OAuth integrations on Connect.

Expand Down
4 changes: 4 additions & 0 deletions docs/commands/set-default.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
::: mkdocs-click
:module: rsconnect.main
:command: set_default
:prog_name: rsconnect server set-default
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ nav:
- logout: commands/logout.md
- quickstart: commands/quickstart.md
- remove: commands/remove.md
- set-default: commands/set-default.md
- system: commands/system.md
- version: commands/version.md
- write-manifest: commands/write-manifest.md
Expand Down
58 changes: 57 additions & 1 deletion rsconnect/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -947,7 +947,63 @@ def remove(
if message:
click.echo(message)
if removed_was_default:
click.echo("Note: the removed server was the default. Use `rsconnect add --set-default` to set a new one.")
click.echo("Note: the removed server was the default. Use `rsconnect server set-default` to set a new one.")


@click.command(
"set-default",
short_help="Set an already-saved server as the default.",
help=(
"Mark an already-saved server as the default. The default is used when "
"neither -n/--name nor -s/--server is given to other commands. This only "
"updates local metadata; the server is not contacted. Prefer -n/--name; "
"-s/--server must match the stored URL exactly."
),
no_args_is_help=True,
)
@click.option("--name", "-n", help="The nickname of the server to set as the default.")
@click.option("--server", "-s", help="The URL of the server to set as the default.")
@click.option("--verbose", "-v", count=True, help="Enable verbose output. Use -vv for very verbose (debug) output.")
@click.pass_context
def set_default(
ctx: click.Context,
name: Optional[str],
server: Optional[str],
verbose: int,
):
set_verbosity(verbose)
output_params(ctx, locals().items())

with cli_feedback("Checking arguments"):
if name and server:
raise RSConnectException("You must specify only one of -n/--name or -s/--server.")
if not name and not server:
raise RSConnectException("You must specify one of -n/--name or -s/--server.")

if server:
entry = server_store.get_by_url(server)
if entry is None:
raise RSConnectException('URL "%s" was not found.' % server)
name = entry["name"]

server_store.set_default(name)

click.echo('Server "%s" is now the default.' % name)


@cli.group("server", no_args_is_help=True)
def server_group():
"""
Manage connections with Posit Connect and shinyapps.io servers.
"""


server_group.add_command(add)
server_group.add_command(list_servers)
server_group.add_command(remove)
server_group.add_command(details)
server_group.add_command(set_default)
server_group.add_command(bootstrap)


def _resolve_identity_token(identity_token: Optional[str], identity_token_file: Optional[str]) -> Optional[str]:
Expand Down
155 changes: 155 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1773,6 +1773,161 @@ def test_add_set_default(self, tmp_path):
if original_server_value:
os.environ["CONNECT_SERVER"] = original_server_value

def test_server_set_default_by_name(self, tmp_path):
from rsconnect.metadata import ServerStore

store = ServerStore(base_dir=str(tmp_path))
store.set("s1", "http://s1.local", api_key="key1", set_as_default=True)
store.set("s2", "http://s2.local", api_key="key2")
runner = CliRunner()
with mock.patch("rsconnect.main.server_store", store):
result = runner.invoke(cli, ["server", "set-default", "-n", "s2"])
assert result.exit_code == 0, result.output
assert 'Server "s2" is now the default.' in result.output
assert store.get_default()["name"] == "s2"

def test_server_set_default_by_url_reports_nickname(self, tmp_path):
from rsconnect.metadata import ServerStore

store = ServerStore(base_dir=str(tmp_path))
store.set("s1", "http://s1.local", api_key="key1")
runner = CliRunner()
with mock.patch("rsconnect.main.server_store", store):
result = runner.invoke(cli, ["server", "set-default", "-s", "http://s1.local"])
assert result.exit_code == 0, result.output
assert 'Server "s1" is now the default.' in result.output
assert store.get_default()["name"] == "s1"

def test_server_set_default_unknown_name(self, tmp_path):
from rsconnect.metadata import ServerStore

store = ServerStore(base_dir=str(tmp_path))
store.set("s1", "http://s1.local", api_key="key1")
runner = CliRunner()
with mock.patch("rsconnect.main.server_store", store):
result = runner.invoke(cli, ["server", "set-default", "-n", "nope"])
assert result.exit_code != 0
assert "does not exist" in result.output

def test_server_set_default_unknown_url(self, tmp_path):
from rsconnect.metadata import ServerStore

store = ServerStore(base_dir=str(tmp_path))
store.set("s1", "http://s1.local", api_key="key1")
runner = CliRunner()
with mock.patch("rsconnect.main.server_store", store):
result = runner.invoke(cli, ["server", "set-default", "-s", "http://absent.local"])
assert result.exit_code != 0
assert "was not found" in result.output

def test_server_set_default_both_options_error(self, tmp_path):
from rsconnect.metadata import ServerStore

store = ServerStore(base_dir=str(tmp_path))
store.set("s1", "http://s1.local", api_key="key1")
runner = CliRunner()
with mock.patch("rsconnect.main.server_store", store):
result = runner.invoke(cli, ["server", "set-default", "-n", "s1", "-s", "http://s1.local"])
assert result.exit_code != 0
assert "only one of" in result.output

def test_server_set_default_no_args_shows_help(self):
runner = CliRunner()
result = runner.invoke(cli, ["server", "set-default"])
assert result.exit_code == 0, result.output
assert "Usage:" in result.output

def test_server_set_default_idempotent(self, tmp_path):
from rsconnect.metadata import ServerStore

store = ServerStore(base_dir=str(tmp_path))
store.set("s1", "http://s1.local", api_key="key1", set_as_default=True)
runner = CliRunner()
with mock.patch("rsconnect.main.server_store", store):
result = runner.invoke(cli, ["server", "set-default", "-n", "s1"])
assert result.exit_code == 0, result.output
assert 'Server "s1" is now the default.' in result.output
assert store.get_default()["name"] == "s1"

def test_server_set_default_shinyapps_entry(self, tmp_path):
from rsconnect.metadata import ServerStore

store = ServerStore(base_dir=str(tmp_path))
store.set("shiny", "https://api.shinyapps.io", account_name="acct", token="tok", secret="sec")
runner = CliRunner()
with mock.patch("rsconnect.main.server_store", store):
result = runner.invoke(cli, ["server", "set-default", "-n", "shiny"])
assert result.exit_code == 0, result.output
assert store.get_default()["name"] == "shiny"

def test_no_top_level_set_default(self):
runner = CliRunner()
result = runner.invoke(cli, ["set-default", "-n", "s1"])
assert result.exit_code != 0
assert "No such command" in result.output or "Usage:" in result.output

def test_set_default_moves_list_marker(self, tmp_path):
from rsconnect.metadata import ServerStore

store = ServerStore(base_dir=str(tmp_path))
store.set("s1", "http://s1.local", api_key="key1", set_as_default=True)
store.set("s2", "http://s2.local", api_key="key2")
runner = CliRunner()
with mock.patch("rsconnect.main.server_store", store):
runner.invoke(cli, ["server", "set-default", "-n", "s2"])
result = runner.invoke(cli, ["list"])
assert result.exit_code == 0, result.output
s2_line = next(line for line in result.output.splitlines() if '"s2"' in line)
assert "[default]" in s2_line


class TestServerGroup:
def test_server_group_help_lists_subcommands(self):
runner = CliRunner()
result = runner.invoke(cli, ["server", "--help"])
assert result.exit_code == 0, result.output
for sub in ("add", "list", "remove", "details", "set-default", "bootstrap"):
assert sub in result.output

def test_server_bootstrap_alias_help(self):
runner = CliRunner()
result = runner.invoke(cli, ["server", "bootstrap", "--help"])
assert result.exit_code == 0, result.output
assert "bootstrap" in result.output

def test_top_level_bootstrap_still_works(self):
runner = CliRunner()
result = runner.invoke(cli, ["bootstrap", "--help"])
assert result.exit_code == 0, result.output

def test_server_group_no_args_shows_help(self):
runner = CliRunner()
result = runner.invoke(cli, ["server"])
assert result.exit_code == 0, result.output
assert "Usage:" in result.output

def test_server_list_alias(self, tmp_path):
from rsconnect.metadata import ServerStore

store = ServerStore(base_dir=str(tmp_path))
store.set("s1", "http://s1.local", api_key="key1")
runner = CliRunner()
with mock.patch("rsconnect.main.server_store", store):
result = runner.invoke(cli, ["server", "list"])
assert result.exit_code == 0, result.output
assert "s1" in result.output

def test_top_level_list_still_works(self, tmp_path):
from rsconnect.metadata import ServerStore

store = ServerStore(base_dir=str(tmp_path))
store.set("s1", "http://s1.local", api_key="key1")
runner = CliRunner()
with mock.patch("rsconnect.main.server_store", store):
result = runner.invoke(cli, ["list"])
assert result.exit_code == 0, result.output
assert "s1" in result.output


class TestDeployGit(TestCase):
"""Tests for deploy git CLI command."""
Expand Down
Loading