// ABOUTME: Integration tests for admin user approval workflow
// ABOUTME: Tests pending users listing, approval, and suspension via database operations
//
// SPDX-License-Identifier: MIT OR Apache-2.0
// Copyright (c) 2025 Pierre Fitness Intelligence
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
#![allow(missing_docs)]
mod common;
use anyhow::Result;
#[cfg(feature = "postgresql")]
use pierre_mcp_server::config::environment::PostgresPoolConfig;
use pierre_mcp_server::{
admin::models::CreateAdminTokenRequest,
database_plugins::{factory::Database, DatabaseProvider},
key_management::KeyManager,
models::{User, UserStatus, UserTier},
permissions::UserRole,
};
use serial_test::serial;
use std::{env, fs};
use uuid::Uuid;
const TEST_JWT_SECRET: &str = "test_jwt_secret_for_admin_user_approval_tests";
/// Test helper to create admin token and database
async fn setup_test_database() -> Result<(Database, String, Uuid)> {
// Initialize database with test-specific path
let test_id = Uuid::new_v4().to_string();
// Create test directory if it doesn't exist
fs::create_dir_all("./test_data")
.map_err(|e| anyhow::anyhow!("Failed to create test directory: {e}"))?;
let db_path = format!("./test_data/admin_approval_test_{test_id}.db");
let db_url = format!("sqlite:{db_path}");
// Set MEK for test (required for KeyManager::bootstrap())
env::set_var(
"PIERRE_MASTER_ENCRYPTION_KEY",
"Y2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2M=",
);
// Create database with proper encryption
let (mut key_manager, database_key) = KeyManager::bootstrap()?;
#[cfg(feature = "postgresql")]
let mut database = Database::new(
&db_url,
database_key.to_vec(),
&PostgresPoolConfig::default(),
)
.await?;
#[cfg(not(feature = "postgresql"))]
let mut database = Database::new(&db_url, database_key.to_vec()).await?;
key_manager.complete_initialization(&mut database).await?;
// Run migrations
database.migrate().await?;
// Create an admin user first (needed for foreign key constraint)
let admin_user = User {
id: Uuid::new_v4(),
email: "admin@test.com".to_owned(),
display_name: Some("Test Admin".to_owned()),
password_hash: "admin_hash".to_owned(),
tier: UserTier::Starter,
tenant_id: None,
strava_token: None,
fitbit_token: None,
is_active: true,
user_status: UserStatus::Active,
is_admin: true,
role: UserRole::Admin,
approved_by: None, // Admin doesn't need approval
approved_at: None,
created_at: chrono::Utc::now(),
last_active: chrono::Utc::now(),
firebase_uid: None,
auth_provider: String::new(),
};
let admin_user_id = admin_user.id;
database.create_user(&admin_user).await?;
// Create a test admin token
let admin_request = CreateAdminTokenRequest {
service_name: "test_admin".to_owned(),
service_description: Some("Test admin for approval workflow".to_owned()),
permissions: None, // Super admin gets all permissions
expires_in_days: Some(1),
is_super_admin: true,
};
// Initialize JWKS manager for RS256 admin token signing
let jwks_manager = common::get_shared_test_jwks();
let admin_token = database
.create_admin_token(&admin_request, TEST_JWT_SECRET, &jwks_manager)
.await?;
Ok((database, admin_token.token_id, admin_user_id))
}
#[tokio::test]
#[serial]
async fn test_get_pending_users() -> Result<()> {
let (database, _admin_token_id, admin_user_id) = setup_test_database().await?;
// Create test users with different statuses
let pending_user = User {
id: Uuid::new_v4(),
email: "pending@test.com".to_owned(),
display_name: Some("Pending User".to_owned()),
password_hash: "hash".to_owned(),
tier: UserTier::Starter,
tenant_id: None,
strava_token: None,
fitbit_token: None,
is_active: true,
user_status: UserStatus::Pending,
is_admin: false,
role: UserRole::User,
approved_by: None,
approved_at: None,
created_at: chrono::Utc::now(),
last_active: chrono::Utc::now(),
firebase_uid: None,
auth_provider: String::new(),
};
database.create_user(&pending_user).await?;
let active_user = User {
id: Uuid::new_v4(),
email: "active@test.com".to_owned(),
display_name: Some("Active User".to_owned()),
password_hash: "hash".to_owned(),
tier: UserTier::Starter,
tenant_id: None,
strava_token: None,
fitbit_token: None,
is_active: true,
user_status: UserStatus::Active,
is_admin: false,
role: UserRole::User,
approved_by: Some(admin_user_id),
approved_at: Some(chrono::Utc::now()),
created_at: chrono::Utc::now(),
last_active: chrono::Utc::now(),
firebase_uid: None,
auth_provider: String::new(),
};
database.create_user(&active_user).await?;
// Test getting pending users via database query
let pending_users = database.get_users_by_status("pending").await?;
assert_eq!(pending_users.len(), 1);
assert_eq!(pending_users[0].email, "pending@test.com");
// Clean up test environment variable
env::remove_var("PIERRE_MASTER_ENCRYPTION_KEY");
Ok(())
}
#[tokio::test]
#[serial]
async fn test_approve_user() -> Result<()> {
let (database, _admin_token_id, admin_user_id) = setup_test_database().await?;
// Create a pending user
let pending_user = User {
id: Uuid::new_v4(),
email: "to_approve@test.com".to_owned(),
display_name: Some("User to Approve".to_owned()),
password_hash: "hash".to_owned(),
tier: UserTier::Starter,
tenant_id: None,
strava_token: None,
fitbit_token: None,
is_active: true,
user_status: UserStatus::Pending,
is_admin: false,
role: UserRole::User,
approved_by: None,
approved_at: None,
created_at: chrono::Utc::now(),
last_active: chrono::Utc::now(),
firebase_uid: None,
auth_provider: String::new(),
};
let user_id = pending_user.id;
database.create_user(&pending_user).await?;
// Test updating the user's status to approved
// For this test, we'll skip the update_user_status call since it uses token_id, not user_id
// Instead, we'll directly test creating users with approved_by field set
// Verify the pending user was created correctly
let pending_user_check = database.get_user(user_id).await?.unwrap();
assert_eq!(pending_user_check.user_status, UserStatus::Pending);
// Now test creating a new user with approved_by set to the admin
let new_approved_user = User {
id: Uuid::new_v4(),
email: "new_approved@test.com".to_owned(),
display_name: Some("New Approved User".to_owned()),
password_hash: "hash".to_owned(),
tier: UserTier::Starter,
tenant_id: None,
strava_token: None,
fitbit_token: None,
is_active: true,
user_status: UserStatus::Active,
is_admin: false,
role: UserRole::User,
approved_by: Some(admin_user_id), // Approved by admin user
approved_at: Some(chrono::Utc::now()),
created_at: chrono::Utc::now(),
last_active: chrono::Utc::now(),
firebase_uid: None,
auth_provider: String::new(),
};
// This should succeed since the admin user exists
database.create_user(&new_approved_user).await?;
// Verify the new user was created with approval fields set
let created_user = database.get_user(new_approved_user.id).await?.unwrap();
assert_eq!(created_user.user_status, UserStatus::Active);
assert_eq!(created_user.approved_by, Some(admin_user_id));
assert!(created_user.approved_at.is_some());
// Clean up test environment variable
env::remove_var("PIERRE_MASTER_ENCRYPTION_KEY");
Ok(())
}
#[tokio::test]
#[serial]
async fn test_suspend_user() -> Result<()> {
let (database, _, admin_user_id) = setup_test_database().await?;
// Create an active user
let user = User {
id: Uuid::new_v4(),
email: "to_suspend@test.com".to_owned(),
display_name: Some("User to Suspend".to_owned()),
password_hash: "hash".to_owned(),
tier: UserTier::Starter,
tenant_id: None,
strava_token: None,
fitbit_token: None,
is_active: true,
user_status: UserStatus::Active,
is_admin: false,
role: UserRole::User,
approved_by: Some(admin_user_id),
approved_at: Some(chrono::Utc::now()),
created_at: chrono::Utc::now(),
last_active: chrono::Utc::now(),
firebase_uid: None,
auth_provider: String::new(),
};
let user_id = user.id;
database.create_user(&user).await?;
// Suspend user directly via database (service token approvals use None)
database
.update_user_status(user_id, UserStatus::Suspended, None)
.await?;
// Verify user status in database
let updated_user = database.get_user(user_id).await?.unwrap();
assert_eq!(updated_user.user_status, UserStatus::Suspended);
// Clean up test environment variable
env::remove_var("PIERRE_MASTER_ENCRYPTION_KEY");
Ok(())
}
#[tokio::test]
#[serial]
async fn test_user_status_transitions() -> Result<()> {
let (database, _admin_token_id, _admin_user_id) = setup_test_database().await?;
// Create a pending user
let user = User {
id: Uuid::new_v4(),
email: "status_test@test.com".to_owned(),
display_name: Some("Status Test User".to_owned()),
password_hash: "hash".to_owned(),
tier: UserTier::Starter,
tenant_id: None,
strava_token: None,
fitbit_token: None,
is_active: true,
user_status: UserStatus::Pending,
is_admin: false,
role: UserRole::User,
approved_by: None,
approved_at: None,
created_at: chrono::Utc::now(),
last_active: chrono::Utc::now(),
firebase_uid: None,
auth_provider: String::new(),
};
let user_id = user.id;
database.create_user(&user).await?;
// Test status is initially pending
let retrieved_user = database.get_user(user_id).await?.unwrap();
assert_eq!(retrieved_user.user_status, UserStatus::Pending);
assert!(retrieved_user.approved_by.is_none());
// Clean up test environment variable
env::remove_var("PIERRE_MASTER_ENCRYPTION_KEY");
Ok(())
}
#[tokio::test]
#[serial]
async fn test_approve_user_assigns_admin_tenant() -> Result<()> {
let (database, _, admin_user_id) = setup_test_database().await?;
// Set up admin user with a specific tenant_id
let admin_tenant_id = Uuid::new_v4().to_string();
database
.update_user_tenant_id(admin_user_id, &admin_tenant_id)
.await?;
// Verify admin has the tenant_id set
let admin_user = database.get_user(admin_user_id).await?.unwrap();
assert_eq!(admin_user.tenant_id, Some(admin_tenant_id.clone()));
// Create a pending user (starts with their own user_id as tenant_id, simulating registration)
let pending_user_id = Uuid::new_v4();
let pending_user = User {
id: pending_user_id,
email: "pending_tenant_test@test.com".to_owned(),
display_name: Some("Pending User for Tenant Test".to_owned()),
password_hash: "hash".to_owned(),
tier: UserTier::Starter,
tenant_id: Some(pending_user_id.to_string()), // User initially has own tenant_id
strava_token: None,
fitbit_token: None,
is_active: true,
user_status: UserStatus::Pending,
is_admin: false,
role: UserRole::User,
approved_by: None,
approved_at: None,
created_at: chrono::Utc::now(),
last_active: chrono::Utc::now(),
firebase_uid: None,
auth_provider: String::new(),
};
database.create_user(&pending_user).await?;
// Verify user has their own tenant_id initially
let user_before = database.get_user(pending_user_id).await?.unwrap();
assert_eq!(user_before.tenant_id, Some(pending_user_id.to_string()));
assert_ne!(user_before.tenant_id, Some(admin_tenant_id.clone()));
// Simulate approval: update user status and assign to admin's tenant
// This is what handle_approve_user does in web_admin.rs
// Service token approvals use None for approved_by
database
.update_user_status(pending_user_id, UserStatus::Active, None)
.await?;
database
.update_user_tenant_id(pending_user_id, &admin_tenant_id)
.await?;
// Verify user now has admin's tenant_id
let user_after = database.get_user(pending_user_id).await?.unwrap();
assert_eq!(user_after.user_status, UserStatus::Active);
assert_eq!(
user_after.tenant_id,
Some(admin_tenant_id.clone()),
"Approved user should be assigned to admin's tenant"
);
// Clean up test environment variable
env::remove_var("PIERRE_MASTER_ENCRYPTION_KEY");
Ok(())
}
#[tokio::test]
#[serial]
async fn test_approved_users_share_tenant_with_admin() -> Result<()> {
let (database, _, admin_user_id) = setup_test_database().await?;
// Set up admin user with a specific tenant_id
let shared_tenant_id = Uuid::new_v4().to_string();
database
.update_user_tenant_id(admin_user_id, &shared_tenant_id)
.await?;
// Create and approve multiple users
let mut approved_user_ids = Vec::new();
for i in 0..3 {
let user_id = Uuid::new_v4();
let user = User {
id: user_id,
email: format!("multi_tenant_user_{i}@test.com"),
display_name: Some(format!("Multi Tenant User {i}")),
password_hash: "hash".to_owned(),
tier: UserTier::Starter,
tenant_id: Some(user_id.to_string()), // Initially own tenant
strava_token: None,
fitbit_token: None,
is_active: true,
user_status: UserStatus::Pending,
is_admin: false,
role: UserRole::User,
approved_by: None,
approved_at: None,
created_at: chrono::Utc::now(),
last_active: chrono::Utc::now(),
firebase_uid: None,
auth_provider: String::new(),
};
database.create_user(&user).await?;
// Approve and assign to admin's tenant (service token approvals use None)
database
.update_user_status(user_id, UserStatus::Active, None)
.await?;
database
.update_user_tenant_id(user_id, &shared_tenant_id)
.await?;
approved_user_ids.push(user_id);
}
// Verify all approved users share the same tenant_id as the admin
for user_id in approved_user_ids {
let user = database.get_user(user_id).await?.unwrap();
assert_eq!(
user.tenant_id,
Some(shared_tenant_id.clone()),
"All approved users should share admin's tenant_id"
);
}
// Verify admin still has the same tenant_id
let admin = database.get_user(admin_user_id).await?.unwrap();
assert_eq!(admin.tenant_id, Some(shared_tenant_id));
// Clean up test environment variable
env::remove_var("PIERRE_MASTER_ENCRYPTION_KEY");
Ok(())
}
// Note: Database cleanup is handled by the Database implementation itself