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: 1 addition & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,19 +152,16 @@ jobs:
organization="$(sed -n 's/^RAWTREE_LOCAL_BOOTSTRAP_ORGANIZATION=//p' ../platform/.env.local)"
cluster="$(sed -n 's/^RAWTREE_LOCAL_BOOTSTRAP_CLUSTER=//p' ../platform/.env.local)"
email="$(sed -n 's/^RAWTREE_LOCAL_BOOTSTRAP_EMAIL=//p' ../platform/.env.local)"
password="$(sed -n 's/^RAWTREE_LOCAL_BOOTSTRAP_PASSWORD=//p' ../platform/.env.local)"
if [ -z "$token" ] || [ -z "$organization" ] || [ -z "$cluster" ] || [ -z "$email" ] || [ -z "$password" ]; then
if [ -z "$token" ] || [ -z "$organization" ] || [ -z "$cluster" ] || [ -z "$email" ]; then
echo '::error::Platform local bootstrap identity is incomplete.'
exit 1
fi
echo "::add-mask::$token"
echo "::add-mask::$password"
export RAWTREE_LIVE_API_URL=http://localhost:18087
export RAWTREE_LIVE_SESSION_TOKEN="$token"
export RAWTREE_LIVE_ORGANIZATION="$organization"
export RAWTREE_LIVE_CLUSTER="$cluster"
export RAWTREE_LIVE_EMAIL="$email"
export RAWTREE_LIVE_PASSWORD="$password"
cargo test --locked --test live_platform -- --ignored

- name: Stop Platform
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rawtree-cli"
version = "0.7.4"
version = "0.7.5"
edition = "2021"
repository = "https://github.com/rawtreedb/rawtree-cli"

Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -543,8 +543,10 @@ To run the real test locally, start Platform from its checkout with
`RAWTREE_LIVE_API_URL=http://localhost:18087` and the
`RAWTREE_LIVE_SESSION_TOKEN`, `RAWTREE_LIVE_ORGANIZATION`, and
`RAWTREE_LIVE_CLUSTER` values from that checkout's ignored `.env.local`.
Also set `RAWTREE_LIVE_EMAIL` and `RAWTREE_LIVE_PASSWORD` from
`RAWTREE_LOCAL_BOOTSTRAP_EMAIL` and `RAWTREE_LOCAL_BOOTSTRAP_PASSWORD` in the same file.
Also set `RAWTREE_LIVE_EMAIL` from
`RAWTREE_LOCAL_BOOTSTRAP_EMAIL` in the same file. The login test approves the
CLI device challenge with that bootstrap session, then checks saved-session use
and logout against the real API.
Run `cargo test --locked --test live_platform -- --ignored` in this checkout.

Run locally:
Expand Down
5 changes: 0 additions & 5 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,6 @@ pub enum Command {
after_help = "API key mode:\n --api-key saves an API key directly without browser authentication.\n\nAPI key output (--json):\n {\"success\":true,\"config_path\":\"<path>\",\"database\":\"<name>\",\"organization\":\"<name>\",\"cluster\":\"<name>\"}\n\nNon-interactive login:\n JSON is automatic when stdin is not a terminal. Read the device approval link\n from a JSON event on stderr while login is running. Missing selections return\n JSON choices on stdout and exit code 2. Rerun with --org, --cluster, or\n --database; each retry requires approval again."
)]
Login {
#[arg(long, hide = true)]
email: Option<String>,
/// Password (prompted interactively if omitted)
#[arg(long, requires = "email", hide = true)]
password: Option<String>,
/// Do not try to open the browser automatically
#[arg(long, default_value_t = false)]
no_browser: bool,
Expand Down
34 changes: 12 additions & 22 deletions src/cli/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,34 +259,24 @@ fn login_with_token_flag_is_rejected() {
}

#[test]
fn login_with_password_requires_email() {
let result = Cli::try_parse_from(["rtree", "login", "--password", "secret123"]);
assert!(result.is_err(), "password without email should fail");
}

#[test]
fn login_with_api_key_conflicts_with_email() {
let cli = Cli::try_parse_from([
"rtree",
"login",
"--api-key",
"rt_abc123",
"--email",
"user@example.com",
])
.expect("global --api-key is parsed before runtime login validation");
assert_eq!(cli.api_key.as_deref(), Some("rt_abc123"));
fn password_login_flags_are_rejected() {
for flags in [
vec!["--email", "user@example.com"],
vec!["--password", "secret"],
vec!["--email", "user@example.com", "--password", "secret"],
] {
let mut args = vec!["rtree", "login"];
args.extend(flags);
assert!(Cli::try_parse_from(args).is_err());
}
}

#[test]
fn login_with_database_without_email_is_allowed_for_interactive_flow() {
fn login_with_database_is_allowed_for_interactive_flow() {
let cli = Cli::try_parse_from(["rtree", "login", "--database", "analytics"])
.expect("login with --database should parse");
match cli.command {
Command::Login {
email, database, ..
} => {
assert!(email.is_none());
Command::Login { database, .. } => {
assert_eq!(database.as_deref(), Some("analytics"));
}
_ => panic!("expected login command"),
Expand Down
67 changes: 1 addition & 66 deletions src/commands/auth.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
mod selection;

use selection::{resolve_auth_selection, resolve_browser_auth_selection, SelectionError};
use selection::{resolve_browser_auth_selection, SelectionError};

use std::io::{self, Write};
use std::process::ExitCode;
Expand Down Expand Up @@ -253,29 +253,6 @@ fn map_config_read_error(err: anyhow::Error) -> anyhow::Error {
output::coded_error("config_read_failed", format!("{:#}", err), 1)
}

fn update_and_save_config(
client: &ApiClient,
resp: &AuthResponse,
cli_org: Option<&str>,
cli_cluster: Option<&str>,
cli_database: Option<&str>,
) -> Result<AuthSelection> {
let mut cfg = config::load()?;
let env_org = std::env::var("RAWTREE_ORG").ok();
let selection = resolve_auth_selection(
&client.base_url,
&resp.token,
cli_org,
cli_cluster,
cli_database,
env_org.as_deref(),
cfg.default_organization.as_deref(),
)?;
apply_auth_config(&mut cfg, &client.base_url, resp, &selection);
config::save(&cfg)?;
Ok(selection)
}

fn update_and_save_browser_config(
client: &ApiClient,
resp: &AuthResponse,
Expand Down Expand Up @@ -322,48 +299,6 @@ fn clear_auth_config(cfg: &mut config::Config) {
*cfg = config::Config::default();
}

pub fn login(
client: &ApiClient,
email: &str,
password: &str,
organization: Option<String>,
cluster: Option<String>,
database: Option<String>,
json_mode: bool,
) -> Result<()> {
let resp: AuthResponse = client.post(
"/v1/auth/login",
&json!({"email": email, "password": password}),
)?;

let selection = update_and_save_config(
client,
&resp,
organization.as_deref(),
cluster.as_deref(),
database.as_deref(),
)?;
let selected_organization = selection.organization.clone();
let selected_cluster = selection.cluster.clone();
let selected_database = selection.database.clone();

output::print_result(
&json!({
"email": resp.email,
"status": "logged_in",
"selected_organization": selected_organization,
"selected_cluster": selected_cluster,
"selected_database": selected_database,
}),
json_mode,
|_| {
println!("Logged in as {}.", resp.email);
print_selected_context(&selection);
},
);
Ok(())
}

pub fn login_with_api_key(
client: &ApiClient,
api_key: &str,
Expand Down
Loading