use anyhow::{anyhow, Context, Result};
use regex::Regex;
use reqwest::Client;
use super::types::{AuthRequest, AuthResponse};
pub struct AuthClient {
http_client: Client,
backend_url: String,
auth_endpoint: String,
api_key_pattern: Regex,
}
impl AuthClient {
pub fn new(backend_url: String, auth_endpoint: String, api_key_pattern: &str) -> Self {
Self {
http_client: Client::new(),
backend_url,
auth_endpoint,
api_key_pattern: Regex::new(api_key_pattern)
.expect("Invalid API key regex pattern"),
}
}
pub fn from_endpoints(endpoints: &dyn crate::traits::AuthEndpoints) -> Self {
Self::new(
endpoints.backend_url().to_string(),
endpoints.auth_endpoint(),
endpoints.api_key_pattern(),
)
}
pub fn validate_api_key_format(&self, api_key: &str) -> Result<()> {
if !self.api_key_pattern.is_match(api_key) {
return Err(anyhow!(
"Invalid API key format. Expected: bw_[env]_[32chars]"
));
}
Ok(())
}
pub async fn authenticate(&self, api_key: &str) -> Result<AuthResponse> {
self.validate_api_key_format(api_key)?;
let url = format!("{}{}", self.backend_url, self.auth_endpoint);
let request = AuthRequest {
api_key: api_key.to_string(),
};
let response = self
.http_client
.post(&url)
.json(&request)
.send()
.await
.context("Failed to send authentication request")?;
if !response.status().is_success() {
let status = response.status();
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
return Err(anyhow!(
"Authentication failed (status {}): {}",
status,
error_text
));
}
let auth_response: AuthResponse = response
.json()
.await
.context("Failed to parse authentication response")?;
Ok(auth_response)
}
pub fn backend_url(&self) -> &str {
&self.backend_url
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_client() -> AuthClient {
AuthClient::new(
"https://test.example.com".to_string(),
"/api/cli/auth".to_string(),
r"^bw_(prod|dev|test)_[a-z0-9]{32}$",
)
}
#[test]
fn test_validate_api_key_format() {
let client = make_client();
assert!(client.validate_api_key_format("bw_dev_12345678901234567890123456789012").is_ok());
assert!(client.validate_api_key_format("bw_prod_abcdefghijklmnopqrstuvwxyz123456").is_ok());
assert!(client.validate_api_key_format("bw_test_00000000000000000000000000000000").is_ok());
assert!(client.validate_api_key_format("invalid").is_err());
assert!(client.validate_api_key_format("bw_invalid_12345678901234567890123456789012").is_err());
assert!(client.validate_api_key_format("bw_dev_short").is_err());
assert!(client.validate_api_key_format("bw_dev_UPPERCASE0000000000000000000000").is_err());
}
#[test]
fn test_auth_client_new() {
let client = make_client();
assert_eq!(client.backend_url(), "https://test.example.com");
}
#[test]
fn test_validate_api_key_error_message() {
let client = make_client();
let result = client.validate_api_key_format("invalid_key");
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error.to_string().contains("Invalid API key format"));
}
#[test]
fn test_validate_api_key_edge_cases() {
let client = make_client();
assert!(client.validate_api_key_format("").is_err());
assert!(client.validate_api_key_format(" ").is_err());
assert!(client.validate_api_key_format("bw_dev_123").is_err()); assert!(client.validate_api_key_format("bw_dev_123456789012345678901234567890123").is_err()); assert!(client.validate_api_key_format("dev_12345678901234567890123456789012").is_err()); assert!(client.validate_api_key_format("bw__12345678901234567890123456789012").is_err()); assert!(client.validate_api_key_format("bw_Dev_12345678901234567890123456789012").is_err()); }
}