1305 lines
38 KiB
Rust
1305 lines
38 KiB
Rust
use axum::{
|
|
body::Body,
|
|
http::{Request, StatusCode, header},
|
|
};
|
|
use http_body_util::BodyExt;
|
|
use serde_json::Value;
|
|
use tower::ServiceExt;
|
|
|
|
use nx9_auth::{
|
|
api,
|
|
config::{Config, SecurityConfig},
|
|
db::{
|
|
self,
|
|
models::{ApiToken, Role, Tenant, User, UserStatus},
|
|
repository::{roles as role_repo, tokens as token_repo},
|
|
},
|
|
error::AppError,
|
|
identity::{
|
|
permissions as identity_perms, roles as identity_roles_real, users as identity_users_real,
|
|
},
|
|
security::{sessions, tokens as tokens_real},
|
|
state::AppState,
|
|
};
|
|
|
|
#[allow(dead_code)]
|
|
mod identity_users {
|
|
use super::AppError;
|
|
use super::SecurityConfig;
|
|
use super::User;
|
|
use super::identity_users_real;
|
|
use sqlx::SqlitePool;
|
|
|
|
pub async fn create_user(
|
|
pool: &SqlitePool,
|
|
cfg: &SecurityConfig,
|
|
tenant_id: &str,
|
|
username: &str,
|
|
password: &str,
|
|
) -> Result<User, AppError> {
|
|
identity_users_real::create_user(pool, cfg, tenant_id, username, password, None, None, None)
|
|
.await
|
|
}
|
|
|
|
pub async fn get_user(pool: &SqlitePool, id: &str) -> Result<User, AppError> {
|
|
identity_users_real::get_user(pool, id).await
|
|
}
|
|
|
|
pub async fn get_user_by_username(pool: &SqlitePool, username: &str) -> Result<User, AppError> {
|
|
identity_users_real::get_user_by_username(pool, username).await
|
|
}
|
|
|
|
pub async fn list_users(pool: &SqlitePool, tenant_id: &str) -> Result<Vec<User>, AppError> {
|
|
identity_users_real::list_users(pool, tenant_id).await
|
|
}
|
|
|
|
pub async fn update_status(
|
|
pool: &SqlitePool,
|
|
user_id: &str,
|
|
status: i32,
|
|
) -> Result<(), AppError> {
|
|
identity_users_real::update_status(pool, user_id, status, None, None, None).await
|
|
}
|
|
|
|
pub async fn reset_password(
|
|
pool: &SqlitePool,
|
|
cfg: &SecurityConfig,
|
|
user_id: &str,
|
|
new_password: &str,
|
|
) -> Result<(), AppError> {
|
|
identity_users_real::reset_password(pool, cfg, user_id, new_password, None, None, None)
|
|
.await
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
mod identity_roles {
|
|
use super::AppError;
|
|
use super::Role;
|
|
use super::identity_roles_real;
|
|
use sqlx::SqlitePool;
|
|
|
|
pub async fn assign_role(
|
|
pool: &SqlitePool,
|
|
user_id: &str,
|
|
role_name: &str,
|
|
) -> Result<(), AppError> {
|
|
identity_roles_real::assign_role(pool, user_id, role_name, None, None, None).await
|
|
}
|
|
|
|
pub async fn list_roles(pool: &SqlitePool) -> Result<Vec<Role>, AppError> {
|
|
identity_roles_real::list_roles(pool).await
|
|
}
|
|
|
|
pub async fn list_user_roles(pool: &SqlitePool, user_id: &str) -> Result<Vec<Role>, AppError> {
|
|
identity_roles_real::list_user_roles(pool, user_id).await
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
mod tokens {
|
|
use super::ApiToken;
|
|
use super::AppError;
|
|
use super::SecurityConfig;
|
|
use super::tokens_real;
|
|
use sqlx::SqlitePool;
|
|
|
|
pub fn generate_pat() -> String {
|
|
tokens_real::generate_pat()
|
|
}
|
|
|
|
pub fn hash_token(raw: &str) -> String {
|
|
tokens_real::hash_token(raw)
|
|
}
|
|
|
|
pub async fn create_token(
|
|
pool: &SqlitePool,
|
|
user_id: &str,
|
|
name: &str,
|
|
cfg: &SecurityConfig,
|
|
) -> Result<(ApiToken, String), AppError> {
|
|
tokens_real::create_token(pool, user_id, name, cfg, None, None, None).await
|
|
}
|
|
|
|
pub async fn validate_token(
|
|
pool: &SqlitePool,
|
|
raw: &str,
|
|
) -> Result<Option<ApiToken>, AppError> {
|
|
tokens_real::validate_token(pool, raw).await
|
|
}
|
|
}
|
|
|
|
async fn setup_test_db() -> (sqlx::SqlitePool, String) {
|
|
let db_id = uuid::Uuid::new_v4().to_string();
|
|
let db_path = format!("target/test_{}.db", db_id);
|
|
let pool = db::create_pool(&db_path)
|
|
.await
|
|
.expect("Failed to create test pool");
|
|
db::run_migrations(&pool)
|
|
.await
|
|
.expect("Failed to run test migrations");
|
|
(pool, db_path)
|
|
}
|
|
|
|
async fn teardown_test_db(path: String) {
|
|
let _ = std::fs::remove_file(path);
|
|
}
|
|
|
|
fn test_security_config() -> SecurityConfig {
|
|
SecurityConfig {
|
|
session_ttl_hours: 24,
|
|
session_absolute_ttl_days: 30,
|
|
token_ttl_days: 365,
|
|
argon2_memory: 4096, // low cost for fast tests
|
|
argon2_iterations: 1,
|
|
argon2_parallelism: 1,
|
|
}
|
|
}
|
|
|
|
fn test_config(db_path: String) -> Config {
|
|
Config {
|
|
server: nx9_auth::config::ServerConfig {
|
|
host: "127.0.0.1".to_string(),
|
|
port: 8655,
|
|
},
|
|
database: nx9_auth::config::DatabaseConfig { path: db_path },
|
|
security: test_security_config(),
|
|
audit: nx9_auth::config::AuditConfig { enabled: true },
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Database & Migration Tests
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
async fn test_db_migration_creates_default_tenant() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let exists = sqlx::query("SELECT 1 FROM tenants WHERE id = ?")
|
|
.bind(Tenant::DEFAULT_ID)
|
|
.fetch_optional(&pool)
|
|
.await
|
|
.unwrap()
|
|
.is_some();
|
|
assert!(exists);
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_db_migration_seeds_admin_role() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let role = role_repo::find_by_name(&pool, "admin").await.unwrap();
|
|
assert!(role.is_some());
|
|
assert_eq!(role.unwrap().name, "admin");
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_db_migration_seeds_viewer_role() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let role = role_repo::find_by_name(&pool, "viewer").await.unwrap();
|
|
assert!(role.is_some());
|
|
assert_eq!(role.unwrap().name, "viewer");
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// User Repository Tests
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
async fn test_repo_create_user_success() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let sec_cfg = test_security_config();
|
|
let user = identity_users::create_user(
|
|
&pool,
|
|
&sec_cfg,
|
|
Tenant::DEFAULT_ID,
|
|
"repo_user_1",
|
|
"super_secure_passphrase_123",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(user.username, "repo_user_1");
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_repo_create_user_empty_username() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let sec_cfg = test_security_config();
|
|
let res = identity_users::create_user(
|
|
&pool,
|
|
&sec_cfg,
|
|
Tenant::DEFAULT_ID,
|
|
" ",
|
|
"super_secure_passphrase_123",
|
|
)
|
|
.await;
|
|
assert!(res.is_err());
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_repo_create_user_conflict() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let sec_cfg = test_security_config();
|
|
let _ = identity_users::create_user(
|
|
&pool,
|
|
&sec_cfg,
|
|
Tenant::DEFAULT_ID,
|
|
"repo_user_conflict",
|
|
"super_secure_passphrase_123",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
let res = identity_users::create_user(
|
|
&pool,
|
|
&sec_cfg,
|
|
Tenant::DEFAULT_ID,
|
|
"repo_user_conflict",
|
|
"super_secure_passphrase_123",
|
|
)
|
|
.await;
|
|
assert!(res.is_err());
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_repo_find_user_by_id() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let sec_cfg = test_security_config();
|
|
let user = identity_users::create_user(
|
|
&pool,
|
|
&sec_cfg,
|
|
Tenant::DEFAULT_ID,
|
|
"find_by_id_user",
|
|
"super_secure_passphrase_123",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
let found = identity_users::get_user(&pool, &user.id).await.unwrap();
|
|
assert_eq!(found.username, "find_by_id_user");
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_repo_find_user_by_username() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let sec_cfg = test_security_config();
|
|
let _ = identity_users::create_user(
|
|
&pool,
|
|
&sec_cfg,
|
|
Tenant::DEFAULT_ID,
|
|
"find_by_username_user",
|
|
"super_secure_passphrase_123",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
let found = identity_users::get_user_by_username(&pool, "find_by_username_user")
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(found.username, "find_by_username_user");
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_repo_update_status() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let sec_cfg = test_security_config();
|
|
let user = identity_users::create_user(
|
|
&pool,
|
|
&sec_cfg,
|
|
Tenant::DEFAULT_ID,
|
|
"status_user",
|
|
"super_secure_passphrase_123",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
identity_users::update_status(&pool, &user.id, UserStatus::Disabled as i32)
|
|
.await
|
|
.unwrap();
|
|
let updated = identity_users::get_user(&pool, &user.id).await.unwrap();
|
|
assert_eq!(updated.status, UserStatus::Disabled as i32);
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_repo_reset_password_strength_standard() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let sec_cfg = test_security_config();
|
|
let user = identity_users::create_user(
|
|
&pool,
|
|
&sec_cfg,
|
|
Tenant::DEFAULT_ID,
|
|
"pwd_reset_user",
|
|
"super_secure_passphrase_123",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Standard user password reset fails with too short
|
|
assert!(
|
|
identity_users::reset_password(&pool, &sec_cfg, &user.id, "short")
|
|
.await
|
|
.is_err()
|
|
);
|
|
// Fails with weak password
|
|
assert!(
|
|
identity_users::reset_password(&pool, &sec_cfg, &user.id, "password12345")
|
|
.await
|
|
.is_err()
|
|
);
|
|
// Succeeds with valid
|
|
assert!(
|
|
identity_users::reset_password(&pool, &sec_cfg, &user.id, "super_secure_new_phrase_123")
|
|
.await
|
|
.is_ok()
|
|
);
|
|
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_repo_reset_password_strength_admin() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let sec_cfg = test_security_config();
|
|
let user = identity_users::create_user(
|
|
&pool,
|
|
&sec_cfg,
|
|
Tenant::DEFAULT_ID,
|
|
"pwd_reset_admin",
|
|
"super_secure_admin_passphrase_123",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
identity_roles::assign_role(&pool, &user.id, "admin")
|
|
.await
|
|
.unwrap();
|
|
|
|
// Admin reset fails with 8 characters (requires 12)
|
|
assert!(
|
|
identity_users::reset_password(&pool, &sec_cfg, &user.id, "short_pwd")
|
|
.await
|
|
.is_err()
|
|
);
|
|
// Succeeds with >= 12 chars
|
|
assert!(
|
|
identity_users::reset_password(
|
|
&pool,
|
|
&sec_cfg,
|
|
&user.id,
|
|
"super_secure_admin_new_phrase_123"
|
|
)
|
|
.await
|
|
.is_ok()
|
|
);
|
|
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Role & Permission Tests
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
async fn test_role_assignment() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let sec_cfg = test_security_config();
|
|
let user = identity_users::create_user(
|
|
&pool,
|
|
&sec_cfg,
|
|
Tenant::DEFAULT_ID,
|
|
"role_user",
|
|
"super_secure_passphrase_123",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
identity_roles::assign_role(&pool, &user.id, "viewer")
|
|
.await
|
|
.unwrap();
|
|
let user_roles = role_repo::list_for_user(&pool, &user.id).await.unwrap();
|
|
assert!(user_roles.iter().any(|r| r.name == "viewer"));
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_role_removal() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let sec_cfg = test_security_config();
|
|
let user = identity_users::create_user(
|
|
&pool,
|
|
&sec_cfg,
|
|
Tenant::DEFAULT_ID,
|
|
"role_rm_user",
|
|
"super_secure_passphrase_123",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
identity_roles::assign_role(&pool, &user.id, "viewer")
|
|
.await
|
|
.unwrap();
|
|
let role = role_repo::find_by_name(&pool, "viewer")
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
let mut tx = pool.begin().await.unwrap();
|
|
role_repo::remove_from_user(&mut tx, &user.id, &role.id)
|
|
.await
|
|
.unwrap();
|
|
tx.commit().await.unwrap();
|
|
let user_roles = role_repo::list_for_user(&pool, &user.id).await.unwrap();
|
|
assert!(user_roles.is_empty());
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_permission_listing_admin() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let sec_cfg = test_security_config();
|
|
let user = identity_users::create_user(
|
|
&pool,
|
|
&sec_cfg,
|
|
Tenant::DEFAULT_ID,
|
|
"perm_admin",
|
|
"super_secure_admin_passphrase_123",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
identity_roles::assign_role(&pool, &user.id, "admin")
|
|
.await
|
|
.unwrap();
|
|
|
|
let perms = identity_perms::list_user_permissions(&pool, &user.id)
|
|
.await
|
|
.unwrap();
|
|
assert!(perms.contains(&"users:create".to_string()));
|
|
assert!(perms.contains(&"users:delete".to_string()));
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Session Lifecycle Tests
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
async fn test_session_creation_success() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let sec_cfg = test_security_config();
|
|
let user = identity_users::create_user(
|
|
&pool,
|
|
&sec_cfg,
|
|
Tenant::DEFAULT_ID,
|
|
"sess_create_user",
|
|
"super_secure_passphrase_123",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let (session, raw_token) =
|
|
sessions::create_session(&pool, &user.id, Some("127.0.0.1"), None, &sec_cfg)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(session.user_id, user.id);
|
|
assert_eq!(raw_token.len(), 64);
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_session_validation_valid_token() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let sec_cfg = test_security_config();
|
|
let user = identity_users::create_user(
|
|
&pool,
|
|
&sec_cfg,
|
|
Tenant::DEFAULT_ID,
|
|
"sess_val_user",
|
|
"super_secure_passphrase_123",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let (_, raw_token) =
|
|
sessions::create_session(&pool, &user.id, Some("127.0.0.1"), None, &sec_cfg)
|
|
.await
|
|
.unwrap();
|
|
let validated = sessions::validate_session(&pool, &raw_token, &sec_cfg)
|
|
.await
|
|
.unwrap();
|
|
assert!(validated.is_some());
|
|
assert_eq!(validated.unwrap().user_id, user.id);
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_session_validation_revoked_token() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let sec_cfg = test_security_config();
|
|
let user = identity_users::create_user(
|
|
&pool,
|
|
&sec_cfg,
|
|
Tenant::DEFAULT_ID,
|
|
"sess_rev_user",
|
|
"super_secure_passphrase_123",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let (session, raw_token) =
|
|
sessions::create_session(&pool, &user.id, Some("127.0.0.1"), None, &sec_cfg)
|
|
.await
|
|
.unwrap();
|
|
sessions::revoke_session(&pool, &session.id).await.unwrap();
|
|
let validated = sessions::validate_session(&pool, &raw_token, &sec_cfg)
|
|
.await
|
|
.unwrap();
|
|
assert!(validated.is_none());
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// PAT Token Lifecycle Tests
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
async fn test_pat_creation_and_validation() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let sec_cfg = test_security_config();
|
|
let user = identity_users::create_user(
|
|
&pool,
|
|
&sec_cfg,
|
|
Tenant::DEFAULT_ID,
|
|
"pat_user",
|
|
"super_secure_passphrase_123",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let (token, raw_pat) = tokens::create_token(&pool, &user.id, "my-token", &sec_cfg)
|
|
.await
|
|
.unwrap();
|
|
assert!(raw_pat.starts_with("nx9_pat_"));
|
|
|
|
let validated = tokens::validate_token(&pool, &raw_pat)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(validated.id, token.id);
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_pat_revocation() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let sec_cfg = test_security_config();
|
|
let user = identity_users::create_user(
|
|
&pool,
|
|
&sec_cfg,
|
|
Tenant::DEFAULT_ID,
|
|
"pat_rev_user",
|
|
"super_secure_passphrase_123",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let (token, raw_pat) = tokens::create_token(&pool, &user.id, "my-token", &sec_cfg)
|
|
.await
|
|
.unwrap();
|
|
let mut tx = pool.begin().await.unwrap();
|
|
token_repo::revoke(&mut tx, &token.id).await.unwrap();
|
|
tx.commit().await.unwrap();
|
|
|
|
let validated = tokens::validate_token(&pool, &raw_pat).await.unwrap();
|
|
assert!(validated.is_none());
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// API Endpoints Tests
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
async fn test_api_health_endpoint() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let config = test_config(db_path.clone());
|
|
let state = AppState::new(pool, config);
|
|
let app = api::router::build(state);
|
|
|
|
let req = Request::builder()
|
|
.uri("/health")
|
|
.body(Body::empty())
|
|
.unwrap();
|
|
let res = app.oneshot(req).await.unwrap();
|
|
assert_eq!(res.status(), StatusCode::OK);
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_api_version_endpoint() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let config = test_config(db_path.clone());
|
|
let state = AppState::new(pool, config);
|
|
let app = api::router::build(state);
|
|
|
|
let req = Request::builder()
|
|
.uri("/version")
|
|
.body(Body::empty())
|
|
.unwrap();
|
|
let res = app.oneshot(req).await.unwrap();
|
|
assert_eq!(res.status(), StatusCode::OK);
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_api_login_success() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let config = test_config(db_path.clone());
|
|
let state = AppState::new(pool.clone(), config.clone());
|
|
let app = api::router::build(state);
|
|
|
|
let password = "super_secure_passphrase_123";
|
|
let _ = identity_users::create_user(
|
|
&pool,
|
|
&config.security,
|
|
Tenant::DEFAULT_ID,
|
|
"login_ok_user",
|
|
password,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let req = Request::builder()
|
|
.method("POST")
|
|
.uri("/api/v1/auth/login")
|
|
.header(header::CONTENT_TYPE, "application/json")
|
|
.body(Body::from(
|
|
r#"{"username":"login_ok_user","password":"super_secure_passphrase_123"}"#,
|
|
))
|
|
.unwrap();
|
|
let res = app.oneshot(req).await.unwrap();
|
|
assert_eq!(res.status(), StatusCode::OK);
|
|
|
|
let cookie = res
|
|
.headers()
|
|
.get(header::SET_COOKIE)
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap();
|
|
assert!(cookie.contains("nx9_session="));
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_api_login_invalid_password() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let config = test_config(db_path.clone());
|
|
let state = AppState::new(pool.clone(), config.clone());
|
|
let app = api::router::build(state);
|
|
|
|
let _ = identity_users::create_user(
|
|
&pool,
|
|
&config.security,
|
|
Tenant::DEFAULT_ID,
|
|
"login_err_user",
|
|
"super_secure_passphrase_123",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let req = Request::builder()
|
|
.method("POST")
|
|
.uri("/api/v1/auth/login")
|
|
.header(header::CONTENT_TYPE, "application/json")
|
|
.body(Body::from(
|
|
r#"{"username":"login_err_user","password":"wrong_password"}"#,
|
|
))
|
|
.unwrap();
|
|
let res = app.oneshot(req).await.unwrap();
|
|
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_api_login_invalid_user() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let config = test_config(db_path.clone());
|
|
let state = AppState::new(pool.clone(), config.clone());
|
|
let app = api::router::build(state);
|
|
|
|
let req = Request::builder()
|
|
.method("POST")
|
|
.uri("/api/v1/auth/login")
|
|
.header(header::CONTENT_TYPE, "application/json")
|
|
.body(Body::from(
|
|
r#"{"username":"does_not_exist","password":"some_password"}"#,
|
|
))
|
|
.unwrap();
|
|
let res = app.oneshot(req).await.unwrap();
|
|
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_api_me_authenticated() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let config = test_config(db_path.clone());
|
|
let state = AppState::new(pool.clone(), config.clone());
|
|
let app = api::router::build(state);
|
|
|
|
let _ = identity_users::create_user(
|
|
&pool,
|
|
&config.security,
|
|
Tenant::DEFAULT_ID,
|
|
"me_user",
|
|
"super_secure_passphrase_123",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let req = Request::builder()
|
|
.method("POST")
|
|
.uri("/api/v1/auth/login")
|
|
.header(header::CONTENT_TYPE, "application/json")
|
|
.body(Body::from(
|
|
r#"{"username":"me_user","password":"super_secure_passphrase_123"}"#,
|
|
))
|
|
.unwrap();
|
|
let res = app.clone().oneshot(req).await.unwrap();
|
|
let cookie = res
|
|
.headers()
|
|
.get(header::SET_COOKIE)
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap()
|
|
.split(';')
|
|
.next()
|
|
.unwrap()
|
|
.to_string();
|
|
|
|
let req = Request::builder()
|
|
.uri("/api/v1/auth/me")
|
|
.header(header::COOKIE, cookie)
|
|
.body(Body::empty())
|
|
.unwrap();
|
|
let res = app.oneshot(req).await.unwrap();
|
|
assert_eq!(res.status(), StatusCode::OK);
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_api_me_unauthenticated() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let config = test_config(db_path.clone());
|
|
let state = AppState::new(pool, config);
|
|
let app = api::router::build(state);
|
|
|
|
let req = Request::builder()
|
|
.uri("/api/v1/auth/me")
|
|
.body(Body::empty())
|
|
.unwrap();
|
|
let res = app.oneshot(req).await.unwrap();
|
|
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_api_logout_success() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let config = test_config(db_path.clone());
|
|
let state = AppState::new(pool.clone(), config.clone());
|
|
let app = api::router::build(state);
|
|
|
|
let _ = identity_users::create_user(
|
|
&pool,
|
|
&config.security,
|
|
Tenant::DEFAULT_ID,
|
|
"logout_user",
|
|
"super_secure_passphrase_123",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let req = Request::builder()
|
|
.method("POST")
|
|
.uri("/api/v1/auth/login")
|
|
.header(header::CONTENT_TYPE, "application/json")
|
|
.body(Body::from(
|
|
r#"{"username":"logout_user","password":"super_secure_passphrase_123"}"#,
|
|
))
|
|
.unwrap();
|
|
let res = app.clone().oneshot(req).await.unwrap();
|
|
let cookie = res
|
|
.headers()
|
|
.get(header::SET_COOKIE)
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap()
|
|
.split(';')
|
|
.next()
|
|
.unwrap()
|
|
.to_string();
|
|
|
|
let req = Request::builder()
|
|
.method("POST")
|
|
.uri("/api/v1/auth/logout")
|
|
.header(header::COOKIE, &cookie)
|
|
.body(Body::empty())
|
|
.unwrap();
|
|
let res = app.clone().oneshot(req).await.unwrap();
|
|
assert_eq!(res.status(), StatusCode::OK);
|
|
|
|
// Profile should now fail
|
|
let req = Request::builder()
|
|
.uri("/api/v1/auth/me")
|
|
.header(header::COOKIE, &cookie)
|
|
.body(Body::empty())
|
|
.unwrap();
|
|
let res = app.oneshot(req).await.unwrap();
|
|
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
|
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_api_list_users_viewer_forbidden() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let config = test_config(db_path.clone());
|
|
let state = AppState::new(pool.clone(), config.clone());
|
|
let app = api::router::build(state);
|
|
|
|
// Create a viewer user
|
|
let viewer = identity_users::create_user(
|
|
&pool,
|
|
&config.security,
|
|
Tenant::DEFAULT_ID,
|
|
"api_viewer",
|
|
"super_secure_passphrase_123",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
identity_roles::assign_role(&pool, &viewer.id, "viewer")
|
|
.await
|
|
.unwrap();
|
|
|
|
// Login as viewer
|
|
let req = Request::builder()
|
|
.method("POST")
|
|
.uri("/api/v1/auth/login")
|
|
.header(header::CONTENT_TYPE, "application/json")
|
|
.body(Body::from(
|
|
r#"{"username":"api_viewer","password":"super_secure_passphrase_123"}"#,
|
|
))
|
|
.unwrap();
|
|
let res = app.clone().oneshot(req).await.unwrap();
|
|
let cookie = res
|
|
.headers()
|
|
.get(header::SET_COOKIE)
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap()
|
|
.split(';')
|
|
.next()
|
|
.unwrap()
|
|
.to_string();
|
|
|
|
// Attempt list users (requires users:create)
|
|
let req = Request::builder()
|
|
.uri("/api/v1/users")
|
|
.header(header::COOKIE, &cookie)
|
|
.body(Body::empty())
|
|
.unwrap();
|
|
let res = app.oneshot(req).await.unwrap();
|
|
assert_eq!(res.status(), StatusCode::FORBIDDEN);
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_api_list_users_admin_allowed() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let config = test_config(db_path.clone());
|
|
let state = AppState::new(pool.clone(), config.clone());
|
|
let app = api::router::build(state);
|
|
|
|
// Create an admin user
|
|
let admin = identity_users::create_user(
|
|
&pool,
|
|
&config.security,
|
|
Tenant::DEFAULT_ID,
|
|
"api_admin_list",
|
|
"super_secure_admin_passphrase_123",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
identity_roles::assign_role(&pool, &admin.id, "admin")
|
|
.await
|
|
.unwrap();
|
|
|
|
// Login as admin
|
|
let req = Request::builder()
|
|
.method("POST")
|
|
.uri("/api/v1/auth/login")
|
|
.header(header::CONTENT_TYPE, "application/json")
|
|
.body(Body::from(
|
|
r#"{"username":"api_admin_list","password":"super_secure_admin_passphrase_123"}"#,
|
|
))
|
|
.unwrap();
|
|
let res = app.clone().oneshot(req).await.unwrap();
|
|
let cookie = res
|
|
.headers()
|
|
.get(header::SET_COOKIE)
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap()
|
|
.split(';')
|
|
.next()
|
|
.unwrap()
|
|
.to_string();
|
|
|
|
// List users (requires users:create, which admin has)
|
|
let req = Request::builder()
|
|
.uri("/api/v1/users")
|
|
.header(header::COOKIE, &cookie)
|
|
.body(Body::empty())
|
|
.unwrap();
|
|
let res = app.oneshot(req).await.unwrap();
|
|
assert_eq!(res.status(), StatusCode::OK);
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_api_create_user_unauthorized() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let config = test_config(db_path.clone());
|
|
let state = AppState::new(pool, config);
|
|
let app = api::router::build(state);
|
|
|
|
// Call user creation without cookie
|
|
let req = Request::builder()
|
|
.method("POST")
|
|
.uri("/api/v1/users")
|
|
.header(header::CONTENT_TYPE, "application/json")
|
|
.body(Body::from(
|
|
r#"{"username":"new_user","password":"some_password"}"#,
|
|
))
|
|
.unwrap();
|
|
let res = app.oneshot(req).await.unwrap();
|
|
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_api_create_user_authorized() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let config = test_config(db_path.clone());
|
|
let state = AppState::new(pool.clone(), config.clone());
|
|
let app = api::router::build(state);
|
|
|
|
// Create admin
|
|
let admin = identity_users::create_user(
|
|
&pool,
|
|
&config.security,
|
|
Tenant::DEFAULT_ID,
|
|
"api_admin_creator",
|
|
"super_secure_admin_passphrase_123",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
identity_roles::assign_role(&pool, &admin.id, "admin")
|
|
.await
|
|
.unwrap();
|
|
|
|
// Login as admin
|
|
let req = Request::builder()
|
|
.method("POST")
|
|
.uri("/api/v1/auth/login")
|
|
.header(header::CONTENT_TYPE, "application/json")
|
|
.body(Body::from(
|
|
r#"{"username":"api_admin_creator","password":"super_secure_admin_passphrase_123"}"#,
|
|
))
|
|
.unwrap();
|
|
let res = app.clone().oneshot(req).await.unwrap();
|
|
let cookie = res
|
|
.headers()
|
|
.get(header::SET_COOKIE)
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap()
|
|
.split(';')
|
|
.next()
|
|
.unwrap()
|
|
.to_string();
|
|
|
|
// Create user via API
|
|
let req = Request::builder()
|
|
.method("POST")
|
|
.uri("/api/v1/users")
|
|
.header(header::COOKIE, &cookie)
|
|
.header(header::CONTENT_TYPE, "application/json")
|
|
.body(Body::from(
|
|
r#"{"username":"api_created_user","password":"super_secure_passphrase_123"}"#,
|
|
))
|
|
.unwrap();
|
|
let res = app.oneshot(req).await.unwrap();
|
|
assert_eq!(res.status(), StatusCode::OK);
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_api_delete_user_self_forbidden() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let config = test_config(db_path.clone());
|
|
let state = AppState::new(pool.clone(), config.clone());
|
|
let app = api::router::build(state);
|
|
|
|
// Create admin
|
|
let admin = identity_users::create_user(
|
|
&pool,
|
|
&config.security,
|
|
Tenant::DEFAULT_ID,
|
|
"api_admin_del_self",
|
|
"super_secure_admin_passphrase_123",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
identity_roles::assign_role(&pool, &admin.id, "admin")
|
|
.await
|
|
.unwrap();
|
|
|
|
// Login as admin
|
|
let req = Request::builder()
|
|
.method("POST")
|
|
.uri("/api/v1/auth/login")
|
|
.header(header::CONTENT_TYPE, "application/json")
|
|
.body(Body::from(
|
|
r#"{"username":"api_admin_del_self","password":"super_secure_admin_passphrase_123"}"#,
|
|
))
|
|
.unwrap();
|
|
let res = app.clone().oneshot(req).await.unwrap();
|
|
let cookie = res
|
|
.headers()
|
|
.get(header::SET_COOKIE)
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap()
|
|
.split(';')
|
|
.next()
|
|
.unwrap()
|
|
.to_string();
|
|
|
|
// Delete self (should fail)
|
|
let req = Request::builder()
|
|
.method("DELETE")
|
|
.uri(format!("/api/v1/users/{}", admin.id))
|
|
.header(header::COOKIE, &cookie)
|
|
.body(Body::empty())
|
|
.unwrap();
|
|
let res = app.oneshot(req).await.unwrap();
|
|
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_api_delete_user_success() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let config = test_config(db_path.clone());
|
|
let state = AppState::new(pool.clone(), config.clone());
|
|
let app = api::router::build(state);
|
|
|
|
// Create admin
|
|
let admin = identity_users::create_user(
|
|
&pool,
|
|
&config.security,
|
|
Tenant::DEFAULT_ID,
|
|
"api_admin_deleter",
|
|
"super_secure_admin_passphrase_123",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
identity_roles::assign_role(&pool, &admin.id, "admin")
|
|
.await
|
|
.unwrap();
|
|
|
|
// Create standard user to delete
|
|
let target = identity_users::create_user(
|
|
&pool,
|
|
&config.security,
|
|
Tenant::DEFAULT_ID,
|
|
"delete_target",
|
|
"super_secure_passphrase_123",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Login as admin
|
|
let req = Request::builder()
|
|
.method("POST")
|
|
.uri("/api/v1/auth/login")
|
|
.header(header::CONTENT_TYPE, "application/json")
|
|
.body(Body::from(
|
|
r#"{"username":"api_admin_deleter","password":"super_secure_admin_passphrase_123"}"#,
|
|
))
|
|
.unwrap();
|
|
let res = app.clone().oneshot(req).await.unwrap();
|
|
let cookie = res
|
|
.headers()
|
|
.get(header::SET_COOKIE)
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap()
|
|
.split(';')
|
|
.next()
|
|
.unwrap()
|
|
.to_string();
|
|
|
|
// Delete target user
|
|
let req = Request::builder()
|
|
.method("DELETE")
|
|
.uri(format!("/api/v1/users/{}", target.id))
|
|
.header(header::COOKIE, &cookie)
|
|
.body(Body::empty())
|
|
.unwrap();
|
|
let res = app.oneshot(req).await.unwrap();
|
|
assert_eq!(res.status(), StatusCode::OK);
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_api_token_creation_and_listing() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let config = test_config(db_path.clone());
|
|
let state = AppState::new(pool.clone(), config.clone());
|
|
let app = api::router::build(state);
|
|
|
|
// Create admin
|
|
let admin = identity_users::create_user(
|
|
&pool,
|
|
&config.security,
|
|
Tenant::DEFAULT_ID,
|
|
"api_admin_token",
|
|
"super_secure_admin_passphrase_123",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
identity_roles::assign_role(&pool, &admin.id, "admin")
|
|
.await
|
|
.unwrap();
|
|
|
|
// Login as admin
|
|
let req = Request::builder()
|
|
.method("POST")
|
|
.uri("/api/v1/auth/login")
|
|
.header(header::CONTENT_TYPE, "application/json")
|
|
.body(Body::from(
|
|
r#"{"username":"api_admin_token","password":"super_secure_admin_passphrase_123"}"#,
|
|
))
|
|
.unwrap();
|
|
let res = app.clone().oneshot(req).await.unwrap();
|
|
let cookie = res
|
|
.headers()
|
|
.get(header::SET_COOKIE)
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap()
|
|
.split(';')
|
|
.next()
|
|
.unwrap()
|
|
.to_string();
|
|
|
|
// Create token
|
|
let req = Request::builder()
|
|
.method("POST")
|
|
.uri("/api/v1/tokens")
|
|
.header(header::COOKIE, &cookie)
|
|
.header(header::CONTENT_TYPE, "application/json")
|
|
.body(Body::from(r#"{"name":"test-api-token"}"#))
|
|
.unwrap();
|
|
let res = app.clone().oneshot(req).await.unwrap();
|
|
assert_eq!(res.status(), StatusCode::OK);
|
|
|
|
// List tokens
|
|
let req = Request::builder()
|
|
.uri("/api/v1/tokens")
|
|
.header(header::COOKIE, &cookie)
|
|
.body(Body::empty())
|
|
.unwrap();
|
|
let res = app.oneshot(req).await.unwrap();
|
|
assert_eq!(res.status(), StatusCode::OK);
|
|
teardown_test_db(db_path).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_api_token_revocation() {
|
|
let (pool, db_path) = setup_test_db().await;
|
|
let config = test_config(db_path.clone());
|
|
let state = AppState::new(pool.clone(), config.clone());
|
|
let app = api::router::build(state);
|
|
|
|
// Create admin
|
|
let admin = identity_users::create_user(
|
|
&pool,
|
|
&config.security,
|
|
Tenant::DEFAULT_ID,
|
|
"api_admin_tok_rev",
|
|
"super_secure_admin_passphrase_123",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
identity_roles::assign_role(&pool, &admin.id, "admin")
|
|
.await
|
|
.unwrap();
|
|
|
|
// Login as admin
|
|
let req = Request::builder()
|
|
.method("POST")
|
|
.uri("/api/v1/auth/login")
|
|
.header(header::CONTENT_TYPE, "application/json")
|
|
.body(Body::from(
|
|
r#"{"username":"api_admin_tok_rev","password":"super_secure_admin_passphrase_123"}"#,
|
|
))
|
|
.unwrap();
|
|
let res = app.clone().oneshot(req).await.unwrap();
|
|
let cookie = res
|
|
.headers()
|
|
.get(header::SET_COOKIE)
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap()
|
|
.split(';')
|
|
.next()
|
|
.unwrap()
|
|
.to_string();
|
|
|
|
// Create token
|
|
let req = Request::builder()
|
|
.method("POST")
|
|
.uri("/api/v1/tokens")
|
|
.header(header::COOKIE, &cookie)
|
|
.header(header::CONTENT_TYPE, "application/json")
|
|
.body(Body::from(r#"{"name":"test-rev-token"}"#))
|
|
.unwrap();
|
|
let res = app.clone().oneshot(req).await.unwrap();
|
|
let body: Value =
|
|
serde_json::from_slice(&res.into_body().collect().await.unwrap().to_bytes()).unwrap();
|
|
let token_id = body
|
|
.get("token")
|
|
.unwrap()
|
|
.get("id")
|
|
.unwrap()
|
|
.as_str()
|
|
.unwrap();
|
|
|
|
// Revoke token
|
|
let req = Request::builder()
|
|
.method("DELETE")
|
|
.uri(format!("/api/v1/tokens/{}", token_id))
|
|
.header(header::COOKIE, &cookie)
|
|
.body(Body::empty())
|
|
.unwrap();
|
|
let res = app.oneshot(req).await.unwrap();
|
|
assert_eq!(res.status(), StatusCode::OK);
|
|
teardown_test_db(db_path).await;
|
|
}
|