use rusqlite::Connection;
use crate::actor::Transport;
use crate::db::models::AuthUser;
use crate::db::queries;
use crate::error::LificError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedIdentity {
pub user: AuthUser,
pub transport: Transport,
}
pub fn resolve_caller_conn(
conn: &Connection,
credential_user: Option<AuthUser>,
transport: Transport,
) -> Result<Option<ResolvedIdentity>, LificError> {
let user = match credential_user {
Some(u) => u,
None => match queries::users::first_admin(conn)? {
Some(admin) => admin,
None => return Ok(None),
},
};
Ok(Some(ResolvedIdentity { user, transport }))
}
pub fn resolve_caller(
db: &crate::db::DbPool,
credential_user: Option<AuthUser>,
transport: Transport,
) -> Result<Option<ResolvedIdentity>, LificError> {
if let Some(user) = credential_user {
return Ok(Some(ResolvedIdentity { user, transport }));
}
let conn = db.read()?;
resolve_caller_conn(&conn, None, transport)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::{self, queries};
use crate::db::models::CreateUser;
fn test_db() -> db::DbPool {
db::open_memory().expect("test db")
}
fn seed_admin(conn: &Connection, username: &str) -> AuthUser {
let u = queries::users::create_user(
conn,
&CreateUser {
username: username.into(),
email: format!("{username}@local.test"),
password: "adminpass123".into(),
display_name: Some(format!("Admin {username}")),
is_admin: true,
is_bot: false,
},
)
.unwrap();
AuthUser {
id: u.id,
username: u.username,
display_name: u.display_name,
is_admin: u.is_admin,
}
}
fn seed_regular(conn: &Connection, username: &str) -> AuthUser {
let u = queries::users::create_user(
conn,
&CreateUser {
username: username.into(),
email: format!("{username}@local.test"),
password: "userpass123".into(),
display_name: None,
is_admin: false,
is_bot: false,
},
)
.unwrap();
AuthUser {
id: u.id,
username: u.username,
display_name: u.display_name,
is_admin: u.is_admin,
}
}
#[test]
fn credential_user_is_returned_unchanged_with_its_transport() {
let pool = test_db();
let conn = pool.read().unwrap();
let regular = seed_regular(&conn, "alice");
for transport in [Transport::Web, Transport::Mcp, Transport::Api, Transport::Cli] {
let id = resolve_caller_conn(&conn, Some(regular.clone()), transport)
.unwrap()
.expect("Some(credential) always resolves");
assert_eq!(id.user, regular);
assert_eq!(id.transport, transport);
}
}
#[test]
fn credential_user_resolves_with_zero_users_in_db() {
let pool = test_db(); let conn = pool.read().unwrap();
let phantom = AuthUser {
id: 999,
username: "phantom".into(),
display_name: String::new(),
is_admin: false,
};
let id = resolve_caller_conn(&conn, Some(phantom.clone()), Transport::Api)
.unwrap()
.expect("credential user resolves regardless of DB state");
assert_eq!(id.user, phantom);
}
#[test]
fn none_credential_falls_back_to_first_admin() {
let pool = test_db();
let conn = pool.write().unwrap();
let admin = seed_admin(&conn, "admin");
let later = seed_admin(&conn, "later");
assert_ne!(admin.id, later.id);
drop(conn);
let conn = pool.read().unwrap();
let id = resolve_caller_conn(&conn, None, Transport::Mcp)
.unwrap()
.expect("first_admin fallback should resolve");
assert_eq!(id.user, admin, "fallback must be the earliest admin");
assert_eq!(id.transport, Transport::Mcp);
}
#[test]
fn none_credential_with_no_admin_returns_none() {
let pool = test_db();
let conn = pool.write().unwrap();
seed_regular(&conn, "onlyuser");
drop(conn);
let conn = pool.read().unwrap();
assert!(resolve_caller_conn(&conn, None, Transport::Api)
.unwrap()
.is_none());
}
#[test]
fn none_credential_zero_users_returns_none() {
let pool = test_db();
let conn = pool.read().unwrap();
assert!(resolve_caller_conn(&conn, None, Transport::System)
.unwrap()
.is_none());
}
#[test]
fn dbpool_overload_and_conn_core_agree_on_first_admin_fallback() {
let pool = test_db();
{
let conn = pool.write().unwrap();
seed_admin(&conn, "admin");
}
let via_conn = {
let conn = pool.read().unwrap();
resolve_caller_conn(&conn, None, Transport::Api)
.unwrap()
.expect("conn fallback resolves")
};
let via_pool = resolve_caller(&pool, None, Transport::Api)
.unwrap()
.expect("pool wrapper fallback resolves");
assert_eq!(via_conn, via_pool);
}
#[test]
fn credential_user_resolves_without_opening_a_db_connection() {
let pool = test_db();
let user = AuthUser {
id: 1,
username: "cred".into(),
display_name: String::new(),
is_admin: false,
};
let id = resolve_caller(&pool, Some(user.clone()), Transport::Web)
.unwrap()
.expect("credential fast path");
assert_eq!(id.user, user);
assert_eq!(id.transport, Transport::Web);
}
}