use crate::config::AuthConfig;
use crate::error::AuthError;
use crate::handlers::{register_user, sign_in_with_session};
use crate::jwt::JwtStrategy;
use crate::state::{set_state, AuthState};
use crate::user::AuthUser;
use doido_controller::axum::body::Body;
use doido_controller::axum::Router;
use doido_controller::session::Session;
use doido_model::password::{hash_password_with_cost, HasSecurePassword};
use doido_model::sea_orm::DatabaseConnection;
use http::{Request, StatusCode};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tower::ServiceExt;
const TEST_COST: u32 = 4;
static AUTH_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
pub struct AuthTestGuard {
_lock: std::sync::MutexGuard<'static, ()>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TestUser {
pub id: i64,
pub email: String,
pub password_digest: String,
}
impl HasSecurePassword for TestUser {
fn password_digest(&self) -> &str {
&self.password_digest
}
}
impl AuthUser for TestUser {
type Id = i64;
fn id(&self) -> Self::Id {
self.id
}
fn email(&self) -> &str {
&self.email
}
fn password_digest(&self) -> Option<&str> {
Some(&self.password_digest)
}
async fn find_by_email(
_db: &DatabaseConnection,
email: &str,
) -> doido_core::Result<Option<Self>> {
Ok(store()
.lock()
.unwrap()
.values()
.find(|u| u.email == email)
.cloned())
}
async fn find_by_id(
_db: &DatabaseConnection,
id: Self::Id,
) -> doido_core::Result<Option<Self>> {
Ok(store().lock().unwrap().get(&id).cloned())
}
}
static TEST_STORE: std::sync::OnceLock<Arc<Mutex<HashMap<i64, TestUser>>>> =
std::sync::OnceLock::new();
fn store() -> Arc<Mutex<HashMap<i64, TestUser>>> {
TEST_STORE
.get_or_init(|| Arc::new(Mutex::new(HashMap::new())))
.clone()
}
pub fn reset_store() {
store().lock().unwrap().clear();
}
pub fn test_auth_config() -> AuthConfig {
AuthConfig::default()
}
pub fn test_jwt_auth_config(secret: &str) -> AuthConfig {
AuthConfig {
strategies: vec!["cookie".into(), "jwt".into()],
jwt: Some(crate::config::JwtConfig {
secret: secret.into(),
access_ttl: 900,
refresh_ttl: 604_800,
issuer: Some("test".into()),
}),
..Default::default()
}
}
pub async fn init_test_auth(
db: DatabaseConnection,
config: AuthConfig,
) -> Result<AuthTestGuard, AuthError> {
let guard = AUTH_TEST_LOCK.lock().expect("auth test lock");
reset_store();
crate::state::reset_state();
set_state(AuthState::build(db, config)?);
Ok(AuthTestGuard { _lock: guard })
}
pub async fn create_test_user(
db: &DatabaseConnection,
email: &str,
password: &str,
) -> Result<TestUser, AuthError> {
let users = store();
register_user(db, email, password, |email, digest| async move {
let mut map = users.lock().unwrap();
let id = (map.len() as i64) + 1;
let user = TestUser {
id,
email,
password_digest: digest,
};
map.insert(id, user.clone());
Ok(user)
})
.await
}
pub fn jwt_for_user(config: &crate::config::JwtConfig, user_id: i64) -> String {
let strategy = JwtStrategy::new(config.clone()).expect("jwt config");
strategy
.issue_tokens(&serde_json::json!(user_id))
.expect("issue tokens")
.access_token
}
pub struct TestResponse {
pub status: StatusCode,
pub body: String,
pub set_cookie: Option<String>,
}
pub async fn send(router: Router, method: &str, uri: &str, body: &str) -> TestResponse {
send_with_headers(router, method, uri, body, &[]).await
}
pub async fn send_with_headers(
router: Router,
method: &str,
uri: &str,
body: &str,
headers: &[(&str, &str)],
) -> TestResponse {
let mut builder = Request::builder().method(method).uri(uri);
if !body.is_empty() && (method == "POST" || method == "PATCH") {
builder = builder.header(http::header::CONTENT_TYPE, "application/json");
}
for (k, v) in headers {
builder = builder.header(*k, *v);
}
let request = builder
.body(Body::from(body.to_string()))
.expect("valid test request");
let response = router
.oneshot(request)
.await
.expect("router handled the request");
let status = response.status();
let set_cookie = response
.headers()
.get(http::header::SET_COOKIE)
.and_then(|v| v.to_str().ok())
.map(str::to_string);
let bytes = doido_controller::axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.expect("read response body");
TestResponse {
status,
body: String::from_utf8_lossy(&bytes).to_string(),
set_cookie,
}
}
pub fn hash_test_password(password: &str) -> String {
hash_password_with_cost(password, TEST_COST).expect("hash")
}
pub fn session_for_user(user: &TestUser) -> Session {
let mut session = Session::new();
sign_in_with_session(&mut session, user);
session
}
pub use crate::jwt::JwtStrategy as TestJwtStrategy;
pub use crate::session::SessionStrategy as TestSessionStrategy;