diff --git a/dclient/cli.py b/dclient/cli.py index 91ccec5..cf081e0 100644 --- a/dclient/cli.py +++ b/dclient/cli.py @@ -1163,6 +1163,55 @@ def login(alias_or_url, scope): auth_file.write_text(json.dumps(auths, indent=4)) click.echo(f"Login successful. Token saved for {auth_key}") + # Step 5: Set defaults if not already configured + config = _load_config(config_file) + default_alias = config.get("default_instance") + has_default_instance = default_alias is not None + has_default_db = bool( + config.get("instances", {}).get(default_alias or "", {}).get("default_database") + ) + if has_default_instance and has_default_db: + return + # Find alias for this instance, or use the auth_key (URL) as the instance key + instance_key = _instance_alias_for_url(url, config_file) or auth_key + # Ensure instance entry exists in config + if instance_key not in config.get("instances", {}): + config.setdefault("instances", {})[instance_key] = { + "url": url.rstrip("/"), + "default_database": None, + } + # Set as default instance if none configured + if not has_default_instance: + config["default_instance"] = instance_key + click.echo(f"Set default instance to {instance_key}") + # Query databases and set default database if none configured + if not has_default_db: + try: + db_response = _make_request(url, access_token, "/.json") + if db_response.status_code == 200: + db_data = db_response.json() + if isinstance(db_data, list): + databases_list = db_data + else: + databases_list = db_data.get("databases", []) + if isinstance(databases_list, dict): + databases_list = list(databases_list.values()) + db_names = [ + db["name"] if isinstance(db, dict) else db for db in databases_list + ] + if db_names: + if len(db_names) == 1: + default_db = db_names[0] + elif "data" in db_names: + default_db = "data" + else: + default_db = db_names[0] + config["instances"][instance_key]["default_database"] = default_db + click.echo(f"Set default database to {default_db}") + except Exception: + pass # Don't fail login if databases check fails + _save_config(config_file, config) + # -- v1 → v2 migration -- diff --git a/tests/test_cli_auth.py b/tests/test_cli_auth.py index ea17dd1..39ba383 100644 --- a/tests/test_cli_auth.py +++ b/tests/test_cli_auth.py @@ -64,6 +64,7 @@ def test_login_with_url(httpx_mock, mocker, tmpdir): mocker.patch("dclient.cli.time.sleep") httpx_mock.add_response(json=DEVICE_RESPONSE, status_code=200) httpx_mock.add_response(json=TOKEN_SUCCESS, status_code=200) + httpx_mock.add_response(json=[{"name": "data"}], status_code=200) runner = CliRunner() result = runner.invoke(cli, ["login", "https://example.com/"]) assert result.exit_code == 0 @@ -80,6 +81,7 @@ def test_login_adds_trailing_slash(httpx_mock, mocker, tmpdir): mocker.patch("dclient.cli.time.sleep") httpx_mock.add_response(json=DEVICE_RESPONSE, status_code=200) httpx_mock.add_response(json=TOKEN_SUCCESS, status_code=200) + httpx_mock.add_response(json=[{"name": "data"}], status_code=200) runner = CliRunner() result = runner.invoke(cli, ["login", "https://example.com"]) assert result.exit_code == 0 @@ -108,6 +110,7 @@ def test_login_with_alias(httpx_mock, mocker, tmpdir): ) httpx_mock.add_response(json=DEVICE_RESPONSE, status_code=200) httpx_mock.add_response(json=TOKEN_SUCCESS, status_code=200) + httpx_mock.add_response(json=[{"name": "data"}], status_code=200) runner = CliRunner() result = runner.invoke(cli, ["login", "prod"]) assert result.exit_code == 0 @@ -123,6 +126,7 @@ def test_login_interactive_prompt(httpx_mock, mocker, tmpdir): mocker.patch("dclient.cli.time.sleep") httpx_mock.add_response(json=DEVICE_RESPONSE, status_code=200) httpx_mock.add_response(json=TOKEN_SUCCESS, status_code=200) + httpx_mock.add_response(json=[{"name": "data"}], status_code=200) runner = CliRunner() result = runner.invoke(cli, ["login"], input="https://example.com/\n") assert result.exit_code == 0 @@ -160,6 +164,7 @@ def test_login_pending_then_success(httpx_mock, mocker, tmpdir): httpx_mock.add_response(json={"error": "authorization_pending"}, status_code=400) # Second poll: success httpx_mock.add_response(json=TOKEN_SUCCESS, status_code=200) + httpx_mock.add_response(json=[{"name": "data"}], status_code=200) runner = CliRunner() result = runner.invoke(cli, ["login", "https://example.com/"]) assert result.exit_code == 0 @@ -176,3 +181,123 @@ def test_login_device_endpoint_error(httpx_mock, mocker, tmpdir): result = runner.invoke(cli, ["login", "https://example.com/"]) assert result.exit_code == 1 assert "Failed to start login flow" in result.output + + +# -- login sets defaults -- + + +@pytest.mark.parametrize( + "databases_response,expected_db", + [ + ([{"name": "mydata", "is_mutable": True}], "mydata"), + ( + [ + {"name": "fixtures", "is_mutable": False}, + {"name": "data", "is_mutable": True}, + {"name": "extra", "is_mutable": True}, + ], + "data", + ), + ( + [ + {"name": "alpha", "is_mutable": True}, + {"name": "beta", "is_mutable": True}, + {"name": "gamma", "is_mutable": False}, + ], + "alpha", + ), + ], + ids=["single_db", "prefers_data", "first_when_no_data"], +) +def test_login_sets_defaults( + httpx_mock, mocker, tmpdir, databases_response, expected_db +): + mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir)) + mocker.patch("dclient.cli.time.sleep") + httpx_mock.add_response(json=DEVICE_RESPONSE, status_code=200) + httpx_mock.add_response(json=TOKEN_SUCCESS, status_code=200) + httpx_mock.add_response(json=databases_response, status_code=200) + runner = CliRunner() + result = runner.invoke(cli, ["login", "https://example.com/"]) + assert result.exit_code == 0, result.output + config = json.loads((pathlib.Path(tmpdir) / "config.json").read_text()) + assert config["default_instance"] == "https://example.com/" + assert ( + config["instances"]["https://example.com/"]["default_database"] == expected_db + ) + + +def test_login_does_not_override_existing_defaults(httpx_mock, mocker, tmpdir): + """When defaults are already configured, login should not change them.""" + mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir)) + mocker.patch("dclient.cli.time.sleep") + config_file = pathlib.Path(tmpdir) / "config.json" + config_file.write_text( + json.dumps( + { + "default_instance": "prod", + "instances": { + "prod": { + "url": "https://prod.example.com", + "default_database": "main", + } + }, + } + ) + ) + httpx_mock.add_response(json=DEVICE_RESPONSE, status_code=200) + httpx_mock.add_response(json=TOKEN_SUCCESS, status_code=200) + runner = CliRunner() + result = runner.invoke(cli, ["login", "https://other.example.com/"]) + assert result.exit_code == 0, result.output + config = json.loads(config_file.read_text()) + assert config["default_instance"] == "prod" + + +def test_login_with_alias_sets_defaults(httpx_mock, mocker, tmpdir): + """When logging in with an alias that has no default_database, set it.""" + mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir)) + mocker.patch("dclient.cli.time.sleep") + config_file = pathlib.Path(tmpdir) / "config.json" + config_file.write_text( + json.dumps( + { + "default_instance": None, + "instances": { + "prod": { + "url": "https://prod.example.com", + "default_database": None, + } + }, + } + ) + ) + httpx_mock.add_response(json=DEVICE_RESPONSE, status_code=200) + httpx_mock.add_response(json=TOKEN_SUCCESS, status_code=200) + httpx_mock.add_response( + json=[ + {"name": "fixtures", "is_mutable": False}, + {"name": "data", "is_mutable": True}, + {"name": "extra", "is_mutable": True}, + ], + status_code=200, + ) + runner = CliRunner() + result = runner.invoke(cli, ["login", "prod"]) + assert result.exit_code == 0, result.output + config = json.loads(config_file.read_text()) + assert config["default_instance"] == "prod" + assert config["instances"]["prod"]["default_database"] == "data" + + +def test_login_databases_error_still_succeeds(httpx_mock, mocker, tmpdir): + """If the databases check fails, login should still succeed.""" + mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir)) + mocker.patch("dclient.cli.time.sleep") + httpx_mock.add_response(json=DEVICE_RESPONSE, status_code=200) + httpx_mock.add_response(json=TOKEN_SUCCESS, status_code=200) + httpx_mock.add_response(status_code=500) + runner = CliRunner() + result = runner.invoke(cli, ["login", "https://example.com/"]) + assert result.exit_code == 0, result.output + assert "Login successful" in result.output