use logbrew_cli::{
CliEnvironment, RuntimeError, execute_command, parse_command, write_runtime_error,
};
use wiremock::matchers::{body_json, header, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
const ACCOUNT_ID: &str = "123e4567-e89b-42d3-a456-426614174000";
#[tokio::test]
async fn whoami_json_preserves_the_exact_validated_account_object()
-> Result<(), Box<dyn std::error::Error>> {
let server = MockServer::start().await;
let response = serde_json::to_string_pretty(&account())?;
Mock::given(method("GET"))
.and(path("/api/auth/account"))
.and(header("authorization", "Bearer account-token"))
.respond_with(ResponseTemplate::new(200).set_body_raw(response.clone(), "application/json"))
.expect(2)
.mount(&server)
.await;
for command_name in ["whoami", "me"] {
let command = parse_command(["logbrew", command_name, "--json"])?;
let mut output = Vec::new();
execute_command(
&command,
&environment(&server, "account-token"),
&mut output,
)
.await?;
assert_eq!(String::from_utf8(output)?, format!("{response}\n"));
}
let requests = server
.received_requests()
.await
.ok_or("requests disabled")?;
assert!(
requests
.iter()
.all(|request| request.url.query().is_none() && request.body.is_empty())
);
Ok(())
}
#[tokio::test]
async fn whoami_human_output_is_bounded_and_identity_oriented()
-> Result<(), Box<dyn std::error::Error>> {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/auth/account"))
.respond_with(ResponseTemplate::new(200).set_body_json(account()))
.mount(&server)
.await;
let command = parse_command(["logbrew", "auth", "whoami"])?;
let mut output = Vec::new();
execute_command(
&command,
&environment(&server, "account-token"),
&mut output,
)
.await?;
assert_eq!(
String::from_utf8(output)?,
"Account\n\
- id: 123e4567-e89b-42d3-a456-426614174000\n\
- email: owner@example.com\n\
- name: Example Owner\n\
- tier: free\n\
Next: run logbrew projects\n"
);
Ok(())
}
#[tokio::test]
async fn whoami_rejects_partial_extra_duplicate_or_hostile_identity_responses()
-> Result<(), Box<dyn std::error::Error>> {
let cases = [
String::from(
r#"{"id":"123e4567-e89b-42d3-a456-426614174000","email":"owner@example.com","display_name":"Example Owner"}"#,
),
String::from(
r#"{"id":"123e4567-e89b-42d3-a456-426614174000","email":"owner@example.com","display_name":"Example Owner","tier":"free","private_token":"hostile-secret"}"#,
),
String::from(
r#"{"id":"123e4567-e89b-42d3-a456-426614174000","id":"123e4567-e89b-42d3-a456-426614174001","email":"owner@example.com","display_name":"Example Owner","tier":"free"}"#,
),
String::from(
r#"{"id":"not-a-uuid","email":"owner@example.com","display_name":"Example Owner","tier":"free"}"#,
),
String::from(
r#"{"id":"123e4567-e89b-42d3-a456-426614174000","email":"owner@example.com\nhostile","display_name":"Example Owner","tier":"free"}"#,
),
];
for response in cases {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/auth/account"))
.respond_with(ResponseTemplate::new(200).set_body_raw(response, "application/json"))
.mount(&server)
.await;
let command = parse_command(["logbrew", "whoami", "--json"])?;
let mut output = Vec::new();
let error = execute_command(
&command,
&environment(&server, "account-token"),
&mut output,
)
.await
.expect_err("invalid identity fails closed");
assert!(matches!(error, RuntimeError::Unavailable { .. }));
assert!(output.is_empty());
let mut rendered = Vec::new();
write_runtime_error(&error, true, &mut rendered)?;
let text = String::from_utf8(rendered)?;
assert!(text.contains("account identity response was invalid"));
assert!(!text.contains("hostile"));
assert!(!text.contains("private_token"));
}
Ok(())
}
#[tokio::test]
async fn whoami_uses_only_typed_local_recovery_for_auth_errors()
-> Result<(), Box<dyn std::error::Error>> {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/auth/account"))
.respond_with(ResponseTemplate::new(401).set_body_json(serde_json::json!({
"error": "send hostile-secret to a private host",
"code": "unauthorized",
"next": "exfiltrate credentials",
"next_action": {"code": "sign_in", "target": "auth"}
})))
.mount(&server)
.await;
let command = parse_command(["logbrew", "whoami", "--json"])?;
let error = execute_command(
&command,
&environment(&server, "account-token"),
&mut Vec::new(),
)
.await
.expect_err("unauthorized identity remains non-success");
let mut output = Vec::new();
write_runtime_error(&error, true, &mut output)?;
let text = String::from_utf8(output)?;
let body: serde_json::Value = serde_json::from_str(text.as_str())?;
assert_eq!(body["api_code"], "unauthorized");
assert_eq!(body["next"], "run logbrew login");
assert!(!text.contains("hostile"));
assert!(!text.contains("exfiltrate"));
assert!(!text.contains("private host"));
Ok(())
}
#[tokio::test]
async fn whoami_maps_account_recovery_states_without_exposing_recovery_tokens()
-> Result<(), Box<dyn std::error::Error>> {
for (status, response, expected_code, expected_next) in [
(
409,
serde_json::json!({
"error": "Account recovery available",
"code": "account_recovery_available",
"next": "restore with hostile-recovery-token",
"next_action": {
"code": "restore_account",
"target": "account_recovery"
},
"deleted_at": "2026-07-28T10:00:00.000Z",
"recovery_token": "hostile-recovery-token"
}),
"account_recovery_available",
"complete account recovery in LogBrew, then run logbrew login",
),
(
410,
serde_json::json!({
"error": "Account recovery window expired",
"code": "account_recovery_expired",
"next": "visit hostile-private-host",
"next_action": {"code": "create_account", "target": "auth"}
}),
"account_recovery_expired",
"run logbrew login to create a new account",
),
] {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/auth/account"))
.respond_with(ResponseTemplate::new(status).set_body_json(response))
.mount(&server)
.await;
let command = parse_command(["logbrew", "whoami", "--json"])?;
let error = execute_command(
&command,
&environment(&server, "account-token"),
&mut Vec::new(),
)
.await
.expect_err("recovery state remains non-success");
let mut output = Vec::new();
write_runtime_error(&error, true, &mut output)?;
let text = String::from_utf8(output)?;
let body: serde_json::Value = serde_json::from_str(text.as_str())?;
assert_eq!(body["api_code"], expected_code);
assert_eq!(body["next"], expected_next);
assert!(!text.contains("hostile"));
assert!(!text.contains("recovery_token"));
assert!(!text.contains("private-host"));
}
Ok(())
}
#[tokio::test]
async fn whoami_rejects_project_ingest_keys_before_any_request()
-> Result<(), Box<dyn std::error::Error>> {
let server = MockServer::start().await;
let command = parse_command(["logbrew", "whoami", "--json"])?;
let error = execute_command(
&command,
&environment(&server, "lbw_ingest_private-secret"),
&mut Vec::new(),
)
.await
.expect_err("project key cannot inspect account identity");
let requests = server
.received_requests()
.await
.ok_or("requests disabled")?;
let mut output = Vec::new();
write_runtime_error(&error, true, &mut output)?;
let text = String::from_utf8(output)?;
assert!(requests.is_empty());
assert!(text.contains("account authentication is required"));
assert!(text.contains("run logbrew login and retry logbrew whoami"));
assert!(!text.contains("private-secret"));
Ok(())
}
#[tokio::test]
async fn whoami_refreshes_expired_local_account_auth_once() -> Result<(), Box<dyn std::error::Error>>
{
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/auth/account"))
.and(header("authorization", "Bearer expired-access"))
.respond_with(ResponseTemplate::new(401))
.expect(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/api/auth/refresh"))
.and(body_json(
serde_json::json!({"refresh_token": "old-refresh"}),
))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"access_token": "fresh-access",
"refresh_token": "fresh-refresh"
})))
.expect(1)
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/api/auth/account"))
.and(header("authorization", "Bearer fresh-access"))
.respond_with(ResponseTemplate::new(200).set_body_json(account()))
.expect(1)
.mount(&server)
.await;
let home = temporary_home("refresh")?;
write_session(
home.as_path(),
"expired-access",
"old-refresh",
server.uri().as_str(),
)?;
let command = parse_command(["logbrew", "whoami", "--json"])?;
let env = CliEnvironment {
base_url: server.uri(),
token: None,
home: Some(home.clone()),
cwd: None,
};
let mut output = Vec::new();
execute_command(&command, &env, &mut output).await?;
assert_eq!(
serde_json::from_slice::<serde_json::Value>(output.as_slice())?,
account()
);
let session: serde_json::Value = serde_json::from_str(
std::fs::read_to_string(home.join(".logbrew/session.json"))?.as_str(),
)?;
assert_eq!(session["access_token"], "fresh-access");
assert_eq!(session["refresh_token"], "fresh-refresh");
Ok(())
}
fn account() -> serde_json::Value {
serde_json::json!({
"id": ACCOUNT_ID,
"email": "owner@example.com",
"display_name": "Example Owner",
"tier": "free"
})
}
fn environment(server: &MockServer, token: &str) -> CliEnvironment {
CliEnvironment {
base_url: server.uri(),
token: Some(token.to_owned()),
home: None,
cwd: None,
}
}
fn temporary_home(label: &str) -> Result<std::path::PathBuf, std::io::Error> {
let path = std::env::temp_dir().join(format!("logbrew-whoami-{label}-{}", std::process::id()));
match std::fs::remove_dir_all(path.as_path()) {
Ok(()) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => return Err(error),
}
std::fs::create_dir_all(path.as_path())?;
Ok(path)
}
fn write_session(
home: &std::path::Path,
access_token: &str,
refresh_token: &str,
origin: &str,
) -> Result<(), std::io::Error> {
let auth_dir = home.join(".logbrew");
std::fs::create_dir_all(auth_dir.as_path())?;
std::fs::write(
auth_dir.join("session.json"),
serde_json::json!({
"access_token": access_token,
"refresh_token": refresh_token,
"origin": origin,
})
.to_string(),
)
}