use std::fmt;
pub fn login_command() -> String {
"aristo auth login --server https://<org>.aretta.ai".to_string()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthError {
NoToken,
Invalid {
server: Option<String>,
checkout: Option<String>,
},
Malformed(String),
SeveralServers {
path: String,
entries: Vec<EntrySummary>,
},
Unreachable {
server: String,
reason: String,
},
RepoNotInOrg {
github_repo: String,
server: String,
},
EnvTokenWithoutServer,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EntrySummary {
pub server: String,
pub user_login: Option<String>,
}
impl fmt::Display for EntrySummary {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "server: {}", self.server)?;
if let Some(user) = &self.user_login {
write!(f, " user: {user}")?;
}
Ok(())
}
}
impl AuthError {
pub fn rejected() -> Self {
AuthError::Invalid {
server: None,
checkout: None,
}
}
pub fn at_server(self, server: &str) -> Self {
match self {
AuthError::Invalid {
server: None,
checkout,
} => AuthError::Invalid {
server: Some(server.to_string()),
checkout,
},
other => other,
}
}
pub fn for_checkout(self, checkout: &str) -> Self {
match self {
AuthError::Invalid {
server,
checkout: None,
} => AuthError::Invalid {
server,
checkout: Some(checkout.to_string()),
},
other => other,
}
}
}
impl fmt::Display for AuthError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AuthError::NoToken => write!(
f,
"not signed in: run `{}` (the org host is your Aretta dashboard's hostname), \
or set ARETTA_TOKEN + ARETTA_API_URL",
login_command()
),
AuthError::Invalid { server, checkout } => match (server, checkout) {
(Some(s), Some(c)) => write!(
f,
"the credential for {s} was rejected by that server (expired or revoked). \
This checkout is {c}; if it belongs to another org, run \
`aristo auth login --server https://<org>.aretta.ai`"
),
(Some(s), None) => write!(
f,
"the credential for {s} was rejected by that server (expired or revoked); \
run `aristo auth login --server {s}`"
),
(None, _) => write!(f, "auth token rejected by server (expired or revoked)"),
},
AuthError::Malformed(msg) => write!(f, "credentials malformed: {msg}"),
AuthError::Unreachable { server, reason } => {
write!(f, "could not reach {server}: {reason}")
}
AuthError::RepoNotInOrg {
github_repo,
server,
} => write!(
f,
"{github_repo} is not a repo of {server} — run from a checkout of one of the org's \
repos, or set ARETTA_API_URL to the org that has it"
),
AuthError::EnvTokenWithoutServer => write!(
f,
"ARETTA_TOKEN is set but ARETTA_API_URL is not — set \
ARETTA_API_URL=https://<org>.aretta.ai (the server the token was minted against)"
),
AuthError::SeveralServers { path, entries } => {
writeln!(
f,
"signed in to several servers; say which: pass `--server <url>` or set ARETTA_API_URL."
)?;
writeln!(f, " on file ({path}):")?;
for e in entries {
writeln!(f, " • {e}")?;
}
write!(
f,
" (`aristo auth logout --server <url>` drops one you no longer use.)"
)
}
}
}
}
impl std::error::Error for AuthError {}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error;
#[test]
fn display_no_token_mentions_login_command() {
let s = AuthError::NoToken.to_string();
assert!(
s.contains("aristo auth login --server https://<org>.aretta.ai"),
"got: {s}"
);
assert!(
s.contains("ARETTA_TOKEN") && s.contains("ARETTA_API_URL"),
"got: {s}"
);
assert!(!s.contains("tier") && !s.contains("trial"), "got: {s}");
}
#[test]
fn display_invalid_mentions_revoked_or_expired() {
let s = AuthError::rejected().to_string();
assert!(s.contains("expired") || s.contains("revoked"), "got: {s}");
}
#[test]
fn display_invalid_names_the_server_and_the_checkout_when_known() {
let s = AuthError::rejected()
.at_server("https://acme.aretta.ai")
.for_checkout("acme/widgets")
.to_string();
assert!(s.contains("for https://acme.aretta.ai"), "got: {s}");
assert!(s.contains("This checkout is acme/widgets"), "got: {s}");
assert!(s.contains("--server https://<org>.aretta.ai"), "got: {s}");
let s = AuthError::rejected()
.at_server("https://acme.aretta.ai")
.to_string();
assert!(s.contains("--server https://acme.aretta.ai"), "got: {s}");
assert!(!s.contains("checkout"), "got: {s}");
}
#[test]
fn at_server_and_for_checkout_fill_only_once_and_only_invalid() {
let e = AuthError::rejected()
.at_server("https://a.example")
.at_server("https://b.example");
assert_eq!(
e,
AuthError::Invalid {
server: Some("https://a.example".into()),
checkout: None
}
);
assert_eq!(AuthError::NoToken.at_server("x"), AuthError::NoToken);
assert_eq!(AuthError::NoToken.for_checkout("x"), AuthError::NoToken);
}
#[test]
fn display_malformed_includes_underlying_reason() {
let s = AuthError::Malformed("parse error at line 3".into()).to_string();
assert!(s.contains("parse error at line 3"), "got: {s}");
}
fn two_entries() -> Vec<EntrySummary> {
vec![
EntrySummary {
server: "https://acme.aretta.ai".into(),
user_login: Some("alice".into()),
},
EntrySummary {
server: "https://other.aretta.ai".into(),
user_login: None,
},
]
}
#[test]
fn display_several_servers_lists_servers_and_both_selectors() {
let s = AuthError::SeveralServers {
path: "/home/alice/.config/aristo/credentials".into(),
entries: two_entries(),
}
.to_string();
assert!(
s.contains("--server <url>") && s.contains("ARETTA_API_URL"),
"got: {s}"
);
assert!(
s.contains("/home/alice/.config/aristo/credentials"),
"got: {s}"
);
assert!(
s.contains("server: https://acme.aretta.ai user: alice"),
"got: {s}"
);
assert!(s.contains("server: https://other.aretta.ai"), "got: {s}");
assert!(!s.contains("tier") && !s.contains("trial"), "got: {s}");
}
#[test]
fn display_env_token_without_server_names_both_variables() {
let s = AuthError::EnvTokenWithoutServer.to_string();
assert!(
s.contains("ARETTA_TOKEN") && s.contains("ARETTA_API_URL"),
"got: {s}"
);
}
#[test]
fn is_std_error() {
fn _assert_is_error<T: Error>(_: &T) {}
let e = AuthError::NoToken;
_assert_is_error(&e);
}
}