use reqwest::header::HeaderValue;
use crate::ClientError;
use crate::auth::{AuthProvider, Basic, Bearer};
pub const ENV_TOKEN: &str = "AVISO_TOKEN";
pub const ENV_USERNAME: &str = "AVISO_USERNAME";
pub const ENV_PASSWORD: &str = "AVISO_PASSWORD";
#[derive(Debug)]
pub struct Env {
inner: EnvSource,
}
#[derive(Debug)]
enum EnvSource {
Bearer(Bearer),
Basic(Basic),
}
impl Env {
pub fn from_process_env() -> crate::Result<Self> {
let bearer = read_env_var(ENV_TOKEN)?;
let user = read_env_var(ENV_USERNAME)?;
let pass = read_env_var(ENV_PASSWORD)?;
Self::from_credentials(bearer.as_deref(), user.as_deref(), pass.as_deref())
}
pub fn from_credentials(
bearer_token: Option<&str>,
username: Option<&str>,
password: Option<&str>,
) -> crate::Result<Self> {
if let Some(token) = bearer_token.filter(|t| !t.is_empty()) {
return Ok(Self {
inner: EnvSource::Bearer(Bearer::new(token)?),
});
}
match (username, password) {
(Some(u), Some(p)) if !u.is_empty() => Ok(Self {
inner: EnvSource::Basic(Basic::new(u, p)?),
}),
_ => Err(ClientError::Auth(format!(
"no credentials available: set {ENV_TOKEN} or both {ENV_USERNAME} and {ENV_PASSWORD}"
))),
}
}
}
#[async_trait::async_trait]
impl AuthProvider for Env {
async fn authorization_header(&self) -> crate::Result<HeaderValue> {
match &self.inner {
EnvSource::Bearer(b) => b.authorization_header().await,
EnvSource::Basic(b) => b.authorization_header().await,
}
}
}
fn read_env_var(name: &str) -> crate::Result<Option<String>> {
match std::env::var(name) {
Ok(value) => Ok(Some(value)),
Err(std::env::VarError::NotPresent) => Ok(None),
Err(std::env::VarError::NotUnicode(_)) => Err(ClientError::Config(format!(
"env var {name} is set but its value is not valid UTF-8"
))),
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
reason = "test code: unwrap on constructor success is the expected diagnostic"
)]
mod tests {
use super::{AuthProvider, ClientError, Env};
#[tokio::test]
async fn bearer_token_takes_precedence_over_basic() {
let env = Env::from_credentials(Some("jwt-xyz"), Some("alice"), Some("pw")).unwrap();
let header = env.authorization_header().await.unwrap();
assert_eq!(header, "Bearer jwt-xyz");
}
#[tokio::test]
async fn falls_back_to_basic_when_no_bearer() {
let env = Env::from_credentials(None, Some("alice"), Some("wonderland")).unwrap();
let header = env.authorization_header().await.unwrap();
assert_eq!(header, "Basic YWxpY2U6d29uZGVybGFuZA==");
}
#[test]
fn errors_when_nothing_is_set() {
let err = Env::from_credentials(None, None, None).unwrap_err();
assert!(matches!(err, ClientError::Auth(_)), "got {err:?}");
}
#[test]
fn errors_when_only_username_set() {
let err = Env::from_credentials(None, Some("alice"), None).unwrap_err();
assert!(matches!(err, ClientError::Auth(_)), "got {err:?}");
}
#[test]
fn empty_bearer_token_is_treated_as_unset() {
let env = Env::from_credentials(Some(""), Some("alice"), Some("pw")).unwrap();
let header = futures_executor_block_on(env.authorization_header());
assert!(header.unwrap().to_str().unwrap().starts_with("Basic "));
}
fn futures_executor_block_on<F: std::future::Future>(future: F) -> F::Output {
tokio::runtime::Builder::new_current_thread()
.build()
.unwrap()
.block_on(future)
}
}