// ABOUTME: HTTP integration tests for tenant management routes
// ABOUTME: Tests all tenant endpoints with authentication, authorization, and error handling
//
// 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)]
#![allow(clippy::uninlined_format_args)]
//! Comprehensive HTTP integration tests for tenant management routes
//!
//! This test suite validates that all tenant endpoints are correctly registered
//! in the router and handle HTTP requests appropriately.
mod common;
mod helpers;
use helpers::axum_test::AxumTestRequest;
use pierre_mcp_server::{
config::environment::{
AppBehaviorConfig, BackupConfig, DatabaseConfig, DatabaseUrl, Environment, SecurityConfig,
SecurityHeadersConfig, ServerConfig,
},
database_plugins::DatabaseProvider,
mcp::resources::{ServerResources, ServerResourcesOptions},
models::{Tenant, TenantId, User},
routes::tenants::TenantRoutes,
};
use serde_json::json;
use std::sync::Arc;
/// Test setup helper for tenant route testing
struct TenantTestSetup {
resources: Arc<ServerResources>,
user_id: uuid::Uuid,
jwt_token: String,
}
impl TenantTestSetup {
async fn new() -> anyhow::Result<Self> {
common::init_server_config();
let database = common::create_test_database().await?;
let auth_manager = common::create_test_auth_manager();
let cache = common::create_test_cache().await?;
// Create test user
let (user_id, user) = common::create_test_user(&database).await?;
// Create ServerResources
let temp_dir = tempfile::tempdir()?;
let config = Arc::new(ServerConfig {
http_port: 8081,
database: DatabaseConfig {
url: DatabaseUrl::Memory,
backup: BackupConfig {
directory: temp_dir.path().to_path_buf(),
..Default::default()
},
..Default::default()
},
app_behavior: AppBehaviorConfig {
ci_mode: true,
auto_approve_users: false,
..Default::default()
},
security: SecurityConfig {
headers: SecurityHeadersConfig {
environment: Environment::Testing,
},
..Default::default()
},
..Default::default()
});
let resources = Arc::new(
ServerResources::new(
(*database).clone(),
(*auth_manager).clone(),
"test_jwt_secret",
config,
cache,
ServerResourcesOptions {
rsa_key_size_bits: Some(2048),
jwks_manager: Some(common::get_shared_test_jwks()),
llm_provider: None,
},
)
.await,
);
// Generate JWT token for the user
let jwt_token = auth_manager
.generate_token(&user, &resources.jwks_manager)
.map_err(|e| anyhow::anyhow!("Failed to generate JWT: {}", e))?;
Ok(Self {
resources,
user_id,
jwt_token,
})
}
fn routes(&self) -> axum::Router {
TenantRoutes::routes(self.resources.clone())
}
fn auth_header(&self) -> String {
format!("Bearer {}", self.jwt_token)
}
}
// ============================================================================
// POST /tenants - Create Tenant Tests
// ============================================================================
#[tokio::test]
async fn test_create_tenant_success() {
let setup = TenantTestSetup::new().await.expect("Setup failed");
let routes = setup.routes();
let request_body = json!({
"name": "Test Tenant",
"slug": "test-tenant",
"plan": "starter"
});
let response = AxumTestRequest::post("/tenants")
.header("authorization", &setup.auth_header())
.json(&request_body)
.send(routes)
.await;
assert_eq!(response.status(), 201);
let body: serde_json::Value = response.json();
assert!(body["tenant_id"].is_string());
assert_eq!(body["name"], "Test Tenant");
assert_eq!(body["slug"], "test-tenant");
}
#[tokio::test]
async fn test_create_tenant_missing_auth() {
let setup = TenantTestSetup::new().await.expect("Setup failed");
let routes = setup.routes();
let request_body = json!({
"name": "Test Tenant",
"slug": "test-tenant",
"plan": "starter"
});
let response = AxumTestRequest::post("/tenants")
.json(&request_body)
.send(routes)
.await;
assert_eq!(response.status(), 401);
}
#[tokio::test]
async fn test_create_tenant_invalid_auth() {
let setup = TenantTestSetup::new().await.expect("Setup failed");
let routes = setup.routes();
let request_body = json!({
"name": "Test Tenant",
"slug": "test-tenant",
"plan": "starter"
});
let response = AxumTestRequest::post("/tenants")
.header("authorization", "Bearer invalid_token")
.json(&request_body)
.send(routes)
.await;
assert_eq!(response.status(), 401);
}
#[tokio::test]
async fn test_create_tenant_invalid_json() {
let setup = TenantTestSetup::new().await.expect("Setup failed");
let routes = setup.routes();
let response = AxumTestRequest::post("/tenants")
.header("authorization", &setup.auth_header())
.header("content-type", "application/json")
.send(routes)
.await;
// Should fail due to missing/invalid body
assert_ne!(response.status(), 201);
}
#[tokio::test]
async fn test_create_tenant_missing_required_fields() {
let setup = TenantTestSetup::new().await.expect("Setup failed");
let routes = setup.routes();
// Missing slug
let request_body1 = json!({
"name": "Test Tenant",
"plan": "starter"
});
let response1 = AxumTestRequest::post("/tenants")
.header("authorization", &setup.auth_header())
.json(&request_body1)
.send(routes.clone())
.await;
assert_ne!(response1.status(), 201);
// Missing name
let request_body2 = json!({
"slug": "test-tenant",
"plan": "starter"
});
let response2 = AxumTestRequest::post("/tenants")
.header("authorization", &setup.auth_header())
.json(&request_body2)
.send(routes)
.await;
assert_ne!(response2.status(), 201);
}
#[tokio::test]
async fn test_create_tenant_duplicate_slug() {
let setup = TenantTestSetup::new().await.expect("Setup failed");
// Create first tenant
let tenant = Tenant {
id: TenantId::new(),
name: "First Tenant".to_owned(),
slug: "duplicate-slug".to_owned(),
domain: None,
plan: "starter".to_owned(),
owner_user_id: setup.user_id,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
setup
.resources
.database
.create_tenant(&tenant)
.await
.expect("Failed to create first tenant");
let routes = setup.routes();
// Try to create second tenant with same slug
let request_body = json!({
"name": "Second Tenant",
"slug": "duplicate-slug",
"plan": "starter"
});
let response = AxumTestRequest::post("/tenants")
.header("authorization", &setup.auth_header())
.json(&request_body)
.send(routes)
.await;
// Should fail with conflict or bad request
assert_ne!(response.status(), 201);
assert!(response.status() == 400 || response.status() == 409 || response.status() == 500);
}
// ============================================================================
// GET /tenants - List Tenants Tests
// ============================================================================
#[tokio::test]
async fn test_list_tenants_success() {
let setup = TenantTestSetup::new().await.expect("Setup failed");
// Create a tenant
let tenant = Tenant {
id: TenantId::new(),
name: "Test Tenant".to_owned(),
slug: "test-tenant".to_owned(),
domain: None,
plan: "starter".to_owned(),
owner_user_id: setup.user_id,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
setup
.resources
.database
.create_tenant(&tenant)
.await
.expect("Failed to create tenant");
let routes = setup.routes();
let response = AxumTestRequest::get("/tenants")
.header("authorization", &setup.auth_header())
.send(routes)
.await;
assert_eq!(response.status(), 200);
let body: serde_json::Value = response.json();
assert!(body["tenants"].is_array());
let tenants = body["tenants"].as_array().unwrap();
assert!(!tenants.is_empty());
// Verify tenant structure
assert!(tenants[0]["tenant_id"].is_string());
assert!(tenants[0]["name"].is_string());
assert!(tenants[0]["slug"].is_string());
}
#[tokio::test]
async fn test_list_tenants_missing_auth() {
let setup = TenantTestSetup::new().await.expect("Setup failed");
let routes = setup.routes();
let response = AxumTestRequest::get("/tenants").send(routes).await;
assert_eq!(response.status(), 401);
}
#[tokio::test]
async fn test_list_tenants_invalid_auth() {
let setup = TenantTestSetup::new().await.expect("Setup failed");
let routes = setup.routes();
let response = AxumTestRequest::get("/tenants")
.header("authorization", "Bearer invalid_token")
.send(routes)
.await;
assert_eq!(response.status(), 401);
}
#[tokio::test]
async fn test_list_tenants_empty() {
let setup = TenantTestSetup::new().await.expect("Setup failed");
let routes = setup.routes();
let response = AxumTestRequest::get("/tenants")
.header("authorization", &setup.auth_header())
.send(routes)
.await;
assert_eq!(response.status(), 200);
let body: serde_json::Value = response.json();
assert!(body["tenants"].is_array());
// Tenants array could be empty or have tenants depending on test execution order
}
// ============================================================================
// Additional Integration Tests
// ============================================================================
#[tokio::test]
async fn test_create_multiple_tenants() {
let setup = TenantTestSetup::new().await.expect("Setup failed");
let routes = setup.routes();
// Create first tenant
let request1 = json!({
"name": "First Tenant",
"slug": "first-tenant",
"plan": "starter"
});
let response1 = AxumTestRequest::post("/tenants")
.header("authorization", &setup.auth_header())
.json(&request1)
.send(routes.clone())
.await;
assert_eq!(response1.status(), 201);
// Create second tenant
let request2 = json!({
"name": "Second Tenant",
"slug": "second-tenant",
"plan": "professional"
});
let response2 = AxumTestRequest::post("/tenants")
.header("authorization", &setup.auth_header())
.json(&request2)
.send(routes.clone())
.await;
assert_eq!(response2.status(), 201);
// List tenants - should have both
let list_response = AxumTestRequest::get("/tenants")
.header("authorization", &setup.auth_header())
.send(routes)
.await;
assert_eq!(list_response.status(), 200);
let body: serde_json::Value = list_response.json();
let tenants = body["tenants"].as_array().unwrap();
assert!(tenants.len() >= 2);
}
#[tokio::test]
async fn test_tenant_ownership() {
let setup = TenantTestSetup::new().await.expect("Setup failed");
// Create a tenant owned by the test user
let tenant = Tenant {
id: TenantId::new(),
name: "Owned Tenant".to_owned(),
slug: "owned-tenant".to_owned(),
domain: None,
plan: "starter".to_owned(),
owner_user_id: setup.user_id,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
setup
.resources
.database
.create_tenant(&tenant)
.await
.expect("Failed to create tenant");
let routes = setup.routes();
// User should be able to list their tenant
let response = AxumTestRequest::get("/tenants")
.header("authorization", &setup.auth_header())
.send(routes)
.await;
assert_eq!(response.status(), 200);
let body: serde_json::Value = response.json();
let tenants = body["tenants"].as_array().unwrap();
assert!(tenants.iter().any(|t| t["slug"] == "owned-tenant"));
}
// ============================================================================
// POST /tenants/switch - Tenant Switching Tests (ASY-176)
// ============================================================================
#[tokio::test]
async fn test_switch_tenant_success() {
let setup = TenantTestSetup::new().await.expect("Setup failed");
// Create a tenant and add user as member
let tenant = Tenant {
id: TenantId::new(),
name: "Switch Target Tenant".to_owned(),
slug: "switch-target".to_owned(),
domain: None,
plan: "starter".to_owned(),
owner_user_id: setup.user_id,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
setup
.resources
.database
.create_tenant(&tenant)
.await
.expect("Failed to create tenant");
let routes = setup.routes();
// Switch to the tenant
let request_body = json!({
"tenant_id": tenant.id.to_string()
});
let response = AxumTestRequest::post("/tenants/switch")
.header("authorization", &setup.auth_header())
.json(&request_body)
.send(routes)
.await;
assert_eq!(response.status(), 200);
let body: serde_json::Value = response.json();
assert!(body["token"].is_string());
assert_eq!(body["active_tenant_id"], tenant.id.to_string());
assert_eq!(body["tenant_name"], "Switch Target Tenant");
assert!(body["role"].is_string());
assert!(body["expires_in"].is_number());
}
#[tokio::test]
async fn test_switch_tenant_no_auth() {
let setup = TenantTestSetup::new().await.expect("Setup failed");
let routes = setup.routes();
let request_body = json!({
"tenant_id": uuid::Uuid::new_v4().to_string()
});
let response = AxumTestRequest::post("/tenants/switch")
.json(&request_body)
.send(routes)
.await;
assert_eq!(response.status(), 401);
}
#[tokio::test]
async fn test_switch_tenant_invalid_auth() {
let setup = TenantTestSetup::new().await.expect("Setup failed");
let routes = setup.routes();
let request_body = json!({
"tenant_id": uuid::Uuid::new_v4().to_string()
});
let response = AxumTestRequest::post("/tenants/switch")
.header("authorization", "Bearer invalid_token_here")
.json(&request_body)
.send(routes)
.await;
assert_eq!(response.status(), 401);
}
#[tokio::test]
async fn test_switch_tenant_invalid_tenant_id_format() {
let setup = TenantTestSetup::new().await.expect("Setup failed");
let routes = setup.routes();
let request_body = json!({
"tenant_id": "not-a-valid-uuid"
});
let response = AxumTestRequest::post("/tenants/switch")
.header("authorization", &setup.auth_header())
.json(&request_body)
.send(routes)
.await;
// Should return 400 for invalid input
assert_eq!(response.status(), 400);
}
#[tokio::test]
async fn test_switch_tenant_non_member() {
let setup = TenantTestSetup::new().await.expect("Setup failed");
let routes = setup.routes();
// Create another user first (to satisfy foreign key constraint)
let other_user = User::new(
"other@example.com".to_owned(),
"password_hash".to_owned(),
Some("Other User".to_owned()),
);
setup
.resources
.database
.create_user(&other_user)
.await
.expect("Failed to create other user");
// Create a tenant owned by the other user
// create_tenant() auto-adds owner_user_id to tenant_users as owner
// Our test user is NOT added to tenant_users
let tenant = Tenant {
id: TenantId::new(),
name: "Other User Tenant".to_owned(),
slug: "other-user-tenant".to_owned(),
domain: None,
plan: "starter".to_owned(),
owner_user_id: other_user.id,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
setup
.resources
.database
.create_tenant(&tenant)
.await
.expect("Failed to create tenant");
// Try to switch to tenant that test user doesn't belong to
let request_body = json!({
"tenant_id": tenant.id.to_string()
});
let response = AxumTestRequest::post("/tenants/switch")
.header("authorization", &setup.auth_header())
.json(&request_body)
.send(routes)
.await;
// Should return 401 (auth invalid - not a member)
assert_eq!(response.status(), 401);
}
#[tokio::test]
async fn test_switch_tenant_nonexistent() {
let setup = TenantTestSetup::new().await.expect("Setup failed");
let routes = setup.routes();
// Try to switch to a tenant that doesn't exist
let request_body = json!({
"tenant_id": uuid::Uuid::new_v4().to_string()
});
let response = AxumTestRequest::post("/tenants/switch")
.header("authorization", &setup.auth_header())
.json(&request_body)
.send(routes)
.await;
// Should return 401 (user not a member of non-existent tenant)
assert_eq!(response.status(), 401);
}
#[tokio::test]
async fn test_switch_between_multiple_tenants() {
let setup = TenantTestSetup::new().await.expect("Setup failed");
// Create two tenants owned by test user
let tenant1 = Tenant {
id: TenantId::new(),
name: "Tenant One".to_owned(),
slug: "tenant-one".to_owned(),
domain: None,
plan: "starter".to_owned(),
owner_user_id: setup.user_id,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let tenant2 = Tenant {
id: TenantId::new(),
name: "Tenant Two".to_owned(),
slug: "tenant-two".to_owned(),
domain: None,
plan: "professional".to_owned(),
owner_user_id: setup.user_id,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
setup
.resources
.database
.create_tenant(&tenant1)
.await
.expect("Failed to create tenant1");
setup
.resources
.database
.create_tenant(&tenant2)
.await
.expect("Failed to create tenant2");
let routes = setup.routes();
// Switch to tenant1
let request1 = json!({ "tenant_id": tenant1.id.to_string() });
let response1 = AxumTestRequest::post("/tenants/switch")
.header("authorization", &setup.auth_header())
.json(&request1)
.send(routes.clone())
.await;
assert_eq!(response1.status(), 200);
let body1: serde_json::Value = response1.json();
assert_eq!(body1["active_tenant_id"], tenant1.id.to_string());
// Switch to tenant2
let request2 = json!({ "tenant_id": tenant2.id.to_string() });
let response2 = AxumTestRequest::post("/tenants/switch")
.header("authorization", &setup.auth_header())
.json(&request2)
.send(routes)
.await;
assert_eq!(response2.status(), 200);
let body2: serde_json::Value = response2.json();
assert_eq!(body2["active_tenant_id"], tenant2.id.to_string());
assert_eq!(body2["tenant_name"], "Tenant Two");
}