//! Integration tests for the admin module (Phase 3). //! //! Tests: //! - Access control (admin vs non-admin vs unauthenticated) //! - Provider CRUD (list, create, update, delete) //! - Rate limit management (list, update) //! - User management (list, promote, demote, self-demotion guard) //! - Public config endpoint (only enabled providers) //! - Audit logging (provider creation, role changes) //! //! Requires a running Postgres instance. Set `TEST_DATABASE_URL` to run. mod common; use axum::http::StatusCode; fn require_test_db() -> bool { std::env::var("TEST_DATABASE_URL").is_ok() } // ═══════════════════════════════════════════════════════════════════════════ // Access Control // ═══════════════════════════════════════════════════════════════════════════ #[tokio::test] async fn non_admin_cannot_get_providers() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (_user_id, session) = app.create_authenticated_user("regular@example.com").await; let (status, body) = app .get_with_session("/api/v1/admin/providers", &session) .await; assert_eq!(status, StatusCode::FORBIDDEN, "Non-admin GET /admin/providers should return 403"); assert_eq!(body["error"], "forbidden"); } #[tokio::test] async fn non_admin_cannot_post_providers() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (_user_id, session) = app.create_authenticated_user("regular2@example.com").await; let body = serde_json::json!({ "provider_name": "gemini", "display_name": "Test", "models": [{"model_id": "m1", "display_name": "Model 1"}] }); let (status, resp) = app .post_with_session("/api/v1/admin/providers", &body, &session) .await; assert_eq!(status, StatusCode::FORBIDDEN, "Non-admin POST /admin/providers should return 403"); assert_eq!(resp["error"], "forbidden"); } #[tokio::test] async fn non_admin_cannot_get_users() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (_user_id, session) = app.create_authenticated_user("regular3@example.com").await; let (status, body) = app .get_with_session("/api/v1/admin/users", &session) .await; assert_eq!(status, StatusCode::FORBIDDEN, "Non-admin GET /admin/users should return 403"); assert_eq!(body["error"], "forbidden"); } #[tokio::test] async fn non_admin_cannot_update_user_role() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (user_id, session) = app.create_authenticated_user("regular4@example.com").await; let body = serde_json::json!({ "role": "admin" }); let uri = format!("/api/v1/admin/users/{}/role", user_id); let (status, resp) = app.put_with_session(&uri, &body, &session).await; assert_eq!(status, StatusCode::FORBIDDEN, "Non-admin PUT /admin/users/:id/role should return 403"); assert_eq!(resp["error"], "forbidden"); } #[tokio::test] async fn unauthenticated_admin_endpoint_returns_401() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (status, body) = app.get("/api/v1/admin/providers").await; assert_eq!(status, StatusCode::UNAUTHORIZED, "Unauthenticated GET /admin/providers should return 401"); assert_eq!(body["error"], "unauthorized"); let (status2, body2) = app.get("/api/v1/admin/users").await; assert_eq!(status2, StatusCode::UNAUTHORIZED, "Unauthenticated GET /admin/users should return 401"); assert_eq!(body2["error"], "unauthorized"); let (status3, body3) = app.get("/api/v1/admin/rate-limits").await; assert_eq!(status3, StatusCode::UNAUTHORIZED, "Unauthenticated GET /admin/rate-limits should return 401"); assert_eq!(body3["error"], "unauthorized"); } #[tokio::test] async fn non_admin_cannot_get_rate_limits() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (_user_id, session) = app.create_authenticated_user("regular5@example.com").await; let (status, body) = app .get_with_session("/api/v1/admin/rate-limits", &session) .await; assert_eq!(status, StatusCode::FORBIDDEN, "Non-admin GET /admin/rate-limits should return 403"); assert_eq!(body["error"], "forbidden"); } // ═══════════════════════════════════════════════════════════════════════════ // Provider CRUD // ═══════════════════════════════════════════════════════════════════════════ #[tokio::test] async fn list_providers_returns_seeded_providers() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (_admin_id, session) = app.create_admin_user("admin-list@example.com").await; let (status, body) = app .get_with_session("/api/v1/admin/providers", &session) .await; assert_eq!(status, StatusCode::OK); let providers = body.as_array().expect("Response should be an array"); assert!(providers.len() >= 3, "Should have at least 3 seeded providers (gemini, openai, anthropic)"); let names: Vec<&str> = providers .iter() .filter_map(|p| p["provider_name"].as_str()) .collect(); assert!(names.contains(&"gemini"), "Should contain gemini"); assert!(names.contains(&"openai"), "Should contain openai"); assert!(names.contains(&"anthropic"), "Should contain anthropic"); // Each provider should have an id, models, and is_enabled for p in providers { assert!(p["id"].is_string(), "Provider should have an id"); assert!(p["models"].is_array(), "Provider should have models array"); assert!(p["is_enabled"].is_boolean(), "Provider should have is_enabled flag"); } } #[tokio::test] async fn create_provider_with_invalid_name_returns_422() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (_admin_id, session) = app.create_admin_user("admin-invalid@example.com").await; let body = serde_json::json!({ "provider_name": "unknown_provider", "display_name": "Unknown", "models": [{"model_id": "m1", "display_name": "Model 1"}] }); let (status, resp) = app .post_with_session("/api/v1/admin/providers", &body, &session) .await; assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY, "Invalid provider_name should return 422"); assert_eq!(resp["error"], "validation_error"); } #[tokio::test] async fn create_provider_with_duplicate_name_returns_error() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (_admin_id, session) = app.create_admin_user("admin-dup@example.com").await; // "gemini" already exists from seed data let body = serde_json::json!({ "provider_name": "gemini", "display_name": "Google Gemini Duplicate", "models": [{"model_id": "gemini-test", "display_name": "Gemini Test"}] }); let (status, resp) = app .post_with_session("/api/v1/admin/providers", &body, &session) .await; assert_eq!(status, StatusCode::BAD_REQUEST, "Duplicate provider_name should return 400"); assert!( resp["message"].as_str().unwrap_or("").contains("already exists"), "Error message should mention provider already exists" ); } #[tokio::test] async fn update_provider_changes_display_name_and_models() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (_admin_id, session) = app.create_admin_user("admin-update@example.com").await; // Get the gemini provider ID let (status, providers) = app .get_with_session("/api/v1/admin/providers", &session) .await; assert_eq!(status, StatusCode::OK); let gemini = providers .as_array() .unwrap() .iter() .find(|p| p["provider_name"].as_str() == Some("gemini")) .expect("Should find gemini provider"); let gemini_id = gemini["id"].as_str().unwrap(); // Update display_name and models let update_body = serde_json::json!({ "display_name": "Google Gemini Updated", "models": [ {"model_id": "gemini-3.0-pro", "display_name": "Gemini 3.0 Pro", "is_default": true} ] }); let uri = format!("/api/v1/admin/providers/{}", gemini_id); let (status, resp) = app .put_with_session(&uri, &update_body, &session) .await; assert_eq!(status, StatusCode::OK, "Update should return 200"); assert_eq!(resp["display_name"], "Google Gemini Updated"); assert_eq!(resp["models"].as_array().unwrap().len(), 1); assert_eq!(resp["models"][0]["model_id"], "gemini-3.0-pro"); } #[tokio::test] async fn update_provider_with_invalid_data_returns_422() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (_admin_id, session) = app.create_admin_user("admin-updinvalid@example.com").await; // Get the gemini provider ID let (status, providers) = app .get_with_session("/api/v1/admin/providers", &session) .await; assert_eq!(status, StatusCode::OK); let gemini = providers .as_array() .unwrap() .iter() .find(|p| p["provider_name"].as_str() == Some("gemini")) .expect("Should find gemini provider"); let gemini_id = gemini["id"].as_str().unwrap(); // Empty display_name should be rejected let update_body = serde_json::json!({ "display_name": "" }); let uri = format!("/api/v1/admin/providers/{}", gemini_id); let (status, resp) = app .put_with_session(&uri, &update_body, &session) .await; assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY, "Empty display_name should return 422"); assert_eq!(resp["error"], "validation_error"); } #[tokio::test] async fn delete_provider_returns_204() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (_admin_id, session) = app.create_admin_user("admin-delete@example.com").await; // Get the anthropic provider ID let (status, providers) = app .get_with_session("/api/v1/admin/providers", &session) .await; assert_eq!(status, StatusCode::OK); let anthropic = providers .as_array() .unwrap() .iter() .find(|p| p["provider_name"].as_str() == Some("anthropic")) .expect("Should find anthropic provider"); let anthropic_id = anthropic["id"].as_str().unwrap(); // Delete let uri = format!("/api/v1/admin/providers/{}", anthropic_id); let (status, _) = app.delete_with_session(&uri, &session).await; assert_eq!(status, StatusCode::NO_CONTENT, "Delete should return 204"); // Verify it's gone let (status2, providers2) = app .get_with_session("/api/v1/admin/providers", &session) .await; assert_eq!(status2, StatusCode::OK); let names: Vec<&str> = providers2 .as_array() .unwrap() .iter() .filter_map(|p| p["provider_name"].as_str()) .collect(); assert!(!names.contains(&"anthropic"), "Anthropic should be deleted"); } #[tokio::test] async fn delete_nonexistent_provider_returns_404() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (_admin_id, session) = app.create_admin_user("admin-del404@example.com").await; let fake_id = uuid::Uuid::new_v4(); let uri = format!("/api/v1/admin/providers/{}", fake_id); let (status, body) = app.delete_with_session(&uri, &session).await; assert_eq!(status, StatusCode::NOT_FOUND, "Delete non-existent provider should return 404"); assert_eq!(body["error"], "not_found"); } #[tokio::test] async fn create_provider_with_empty_models_returns_422() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (_admin_id, session) = app.create_admin_user("admin-empty-models@example.com").await; // First delete openai so we can re-create it let (_, providers) = app .get_with_session("/api/v1/admin/providers", &session) .await; let openai = providers .as_array() .unwrap() .iter() .find(|p| p["provider_name"].as_str() == Some("openai")) .expect("Should find openai provider"); let openai_id = openai["id"].as_str().unwrap(); app.delete_with_session(&format!("/api/v1/admin/providers/{}", openai_id), &session) .await; // Now try to create openai with empty models let body = serde_json::json!({ "provider_name": "openai", "display_name": "OpenAI", "models": [] }); let (status, resp) = app .post_with_session("/api/v1/admin/providers", &body, &session) .await; assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY, "Empty models should return 422"); assert_eq!(resp["error"], "validation_error"); } #[tokio::test] async fn create_provider_returns_with_id() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (_admin_id, session) = app.create_admin_user("admin-create-ok@example.com").await; // First delete openai so we can re-create it let (_, providers) = app .get_with_session("/api/v1/admin/providers", &session) .await; let openai = providers .as_array() .unwrap() .iter() .find(|p| p["provider_name"].as_str() == Some("openai")) .expect("Should find openai provider"); let openai_id = openai["id"].as_str().unwrap(); app.delete_with_session(&format!("/api/v1/admin/providers/{}", openai_id), &session) .await; // Create openai with fresh data let body = serde_json::json!({ "provider_name": "openai", "display_name": "OpenAI Fresh", "models": [ {"model_id": "gpt-4o", "display_name": "GPT-4o", "is_default": true} ] }); let (status, resp) = app .post_with_session("/api/v1/admin/providers", &body, &session) .await; assert_eq!(status, StatusCode::CREATED, "Create should return 201"); assert!(resp["id"].is_string(), "Response should have an id"); assert_eq!(resp["provider_name"], "openai"); assert_eq!(resp["display_name"], "OpenAI Fresh"); assert_eq!(resp["models"].as_array().unwrap().len(), 1); assert_eq!(resp["is_enabled"], true); } // ═══════════════════════════════════════════════════════════════════════════ // Rate Limits // ═══════════════════════════════════════════════════════════════════════════ #[tokio::test] async fn list_rate_limits_returns_seeded_limits() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (_admin_id, session) = app.create_admin_user("admin-rl-list@example.com").await; let (status, body) = app .get_with_session("/api/v1/admin/rate-limits", &session) .await; assert_eq!(status, StatusCode::OK); let limits = body.as_array().expect("Response should be an array"); assert!(limits.len() >= 3, "Should have at least 3 seeded rate limits"); let names: Vec<&str> = limits .iter() .filter_map(|l| l["provider_name"].as_str()) .collect(); assert!(names.contains(&"gemini"), "Should contain gemini rate limit"); assert!(names.contains(&"openai"), "Should contain openai rate limit"); assert!(names.contains(&"anthropic"), "Should contain anthropic rate limit"); // Check gemini has the expected seeded values let gemini_limit = limits .iter() .find(|l| l["provider_name"].as_str() == Some("gemini")) .unwrap(); assert_eq!(gemini_limit["max_requests"], 29); assert_eq!(gemini_limit["time_window_seconds"], 60); } #[tokio::test] async fn update_rate_limit_succeeds() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (_admin_id, session) = app.create_admin_user("admin-rl-update@example.com").await; let body = serde_json::json!({ "max_requests": 100, "time_window_seconds": 120 }); let (status, resp) = app .put_with_session("/api/v1/admin/rate-limits/gemini", &body, &session) .await; assert_eq!(status, StatusCode::OK, "Update rate limit should return 200"); assert_eq!(resp["provider_name"], "gemini"); assert_eq!(resp["max_requests"], 100); assert_eq!(resp["time_window_seconds"], 120); } #[tokio::test] async fn update_rate_limit_with_invalid_values_returns_422() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (_admin_id, session) = app.create_admin_user("admin-rl-invalid@example.com").await; // max_requests = 0 is invalid (must be 1..=1000) let body = serde_json::json!({ "max_requests": 0, "time_window_seconds": 60 }); let (status, resp) = app .put_with_session("/api/v1/admin/rate-limits/gemini", &body, &session) .await; assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY, "Invalid max_requests should return 422"); assert_eq!(resp["error"], "validation_error"); // time_window_seconds > 3600 is invalid let body2 = serde_json::json!({ "max_requests": 30, "time_window_seconds": 5000 }); let (status2, resp2) = app .put_with_session("/api/v1/admin/rate-limits/gemini", &body2, &session) .await; assert_eq!(status2, StatusCode::UNPROCESSABLE_ENTITY, "Invalid time_window_seconds should return 422"); assert_eq!(resp2["error"], "validation_error"); } #[tokio::test] async fn updated_rate_limits_reflected_in_get() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (_admin_id, session) = app.create_admin_user("admin-rl-reflect@example.com").await; // Update openai rate limit let body = serde_json::json!({ "max_requests": 75, "time_window_seconds": 300 }); let (status, _) = app .put_with_session("/api/v1/admin/rate-limits/openai", &body, &session) .await; assert_eq!(status, StatusCode::OK); // Now list and verify let (status2, limits) = app .get_with_session("/api/v1/admin/rate-limits", &session) .await; assert_eq!(status2, StatusCode::OK); let openai_limit = limits .as_array() .unwrap() .iter() .find(|l| l["provider_name"].as_str() == Some("openai")) .expect("Should find openai rate limit"); assert_eq!(openai_limit["max_requests"], 75); assert_eq!(openai_limit["time_window_seconds"], 300); } // ═══════════════════════════════════════════════════════════════════════════ // User Management // ═══════════════════════════════════════════════════════════════════════════ #[tokio::test] async fn list_users_returns_user_list() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (_admin_id, session) = app.create_admin_user("admin-users@example.com").await; // Create a few regular users app.create_test_user("user-a@example.com").await; app.create_test_user("user-b@example.com").await; let (status, body) = app .get_with_session("/api/v1/admin/users", &session) .await; assert_eq!(status, StatusCode::OK); let users = body.as_array().expect("Response should be an array"); assert!(users.len() >= 3, "Should have at least 3 users (1 admin + 2 regular)"); let emails: Vec<&str> = users .iter() .filter_map(|u| u["email"].as_str()) .collect(); assert!(emails.contains(&"admin-users@example.com")); assert!(emails.contains(&"user-a@example.com")); assert!(emails.contains(&"user-b@example.com")); // Each user should have the expected fields for u in users { assert!(u["id"].is_string(), "User should have an id"); assert!(u["email"].is_string(), "User should have an email"); assert!(u["role"].is_string(), "User should have a role"); } } #[tokio::test] async fn promote_user_to_admin() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (_admin_id, admin_session) = app.create_admin_user("admin-promote@example.com").await; let (target_user_id, _) = app.create_authenticated_user("target-promote@example.com").await; let body = serde_json::json!({ "role": "admin" }); let uri = format!("/api/v1/admin/users/{}/role", target_user_id); let (status, resp) = app .put_with_session(&uri, &body, &admin_session) .await; assert_eq!(status, StatusCode::OK, "Promote to admin should return 200"); assert_eq!(resp["role"], "admin"); assert_eq!(resp["id"], target_user_id.to_string()); } #[tokio::test] async fn demote_admin_to_user() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (_admin_id, admin_session) = app.create_admin_user("admin-demote@example.com").await; let (target_admin_id, _) = app.create_admin_user("target-demote@example.com").await; let body = serde_json::json!({ "role": "user" }); let uri = format!("/api/v1/admin/users/{}/role", target_admin_id); let (status, resp) = app .put_with_session(&uri, &body, &admin_session) .await; assert_eq!(status, StatusCode::OK, "Demote to user should return 200"); assert_eq!(resp["role"], "user"); } #[tokio::test] async fn cannot_demote_self() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (admin_id, admin_session) = app.create_admin_user("admin-selfdemote@example.com").await; let body = serde_json::json!({ "role": "user" }); let uri = format!("/api/v1/admin/users/{}/role", admin_id); let (status, resp) = app .put_with_session(&uri, &body, &admin_session) .await; assert_eq!(status, StatusCode::BAD_REQUEST, "Self-demotion should return 400"); assert!( resp["message"].as_str().unwrap_or("").contains("Cannot demote yourself"), "Error message should mention self-demotion" ); } #[tokio::test] async fn update_user_role_with_invalid_role_returns_422() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (_admin_id, admin_session) = app.create_admin_user("admin-badrole@example.com").await; let (target_id, _) = app.create_authenticated_user("target-badrole@example.com").await; let body = serde_json::json!({ "role": "superadmin" }); let uri = format!("/api/v1/admin/users/{}/role", target_id); let (status, resp) = app .put_with_session(&uri, &body, &admin_session) .await; assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY, "Invalid role should return 422"); assert_eq!(resp["error"], "validation_error"); } // ═══════════════════════════════════════════════════════════════════════════ // Public Config // ═══════════════════════════════════════════════════════════════════════════ #[tokio::test] async fn config_providers_without_auth_returns_401() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (status, body) = app.get("/api/v1/config/providers").await; assert_eq!(status, StatusCode::UNAUTHORIZED, "GET /config/providers without auth should return 401"); assert_eq!(body["error"], "unauthorized"); } #[tokio::test] async fn config_providers_with_auth_returns_enabled() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (_user_id, session) = app.create_authenticated_user("config-user@example.com").await; let (status, body) = app .get_with_session("/api/v1/config/providers", &session) .await; assert_eq!(status, StatusCode::OK); let providers = body.as_array().expect("Response should be an array"); // All seeded providers are enabled by default assert!(providers.len() >= 3, "Should have at least 3 enabled providers"); // Verify the structure: should NOT have admin-only fields like `id` at root // (actually this endpoint returns provider_name, display_name, models) for p in providers { assert!(p["provider_name"].is_string()); assert!(p["display_name"].is_string()); assert!(p["models"].is_array()); } } #[tokio::test] async fn config_providers_excludes_disabled() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (_admin_id, admin_session) = app.create_admin_user("admin-disable@example.com").await; let (_user_id, user_session) = app.create_authenticated_user("config-disable-user@example.com").await; // Get the openai provider ID and disable it let (_, providers) = app .get_with_session("/api/v1/admin/providers", &admin_session) .await; let openai = providers .as_array() .unwrap() .iter() .find(|p| p["provider_name"].as_str() == Some("openai")) .expect("Should find openai provider"); let openai_id = openai["id"].as_str().unwrap(); let disable_body = serde_json::json!({ "is_enabled": false }); let uri = format!("/api/v1/admin/providers/{}", openai_id); let (status, _) = app .put_with_session(&uri, &disable_body, &admin_session) .await; assert_eq!(status, StatusCode::OK); // Now check public config as a regular user let (status2, body2) = app .get_with_session("/api/v1/config/providers", &user_session) .await; assert_eq!(status2, StatusCode::OK); let provider_names: Vec<&str> = body2 .as_array() .unwrap() .iter() .filter_map(|p| p["provider_name"].as_str()) .collect(); assert!( !provider_names.contains(&"openai"), "Disabled provider 'openai' should NOT appear in public config" ); assert!( provider_names.contains(&"gemini"), "Enabled provider 'gemini' should still appear" ); } #[tokio::test] async fn config_providers_admin_user_also_works() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (_admin_id, session) = app.create_admin_user("admin-config@example.com").await; let (status, body) = app .get_with_session("/api/v1/config/providers", &session) .await; assert_eq!(status, StatusCode::OK, "Admin should also be able to access public config"); assert!(body.as_array().unwrap().len() >= 3); } // ═══════════════════════════════════════════════════════════════════════════ // Audit Log // ═══════════════════════════════════════════════════════════════════════════ #[tokio::test] async fn audit_log_created_on_provider_creation() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (admin_id, session) = app.create_admin_user("admin-audit1@example.com").await; // Delete openai so we can create it fresh let (_, providers) = app .get_with_session("/api/v1/admin/providers", &session) .await; let openai = providers .as_array() .unwrap() .iter() .find(|p| p["provider_name"].as_str() == Some("openai")) .expect("Should find openai"); let openai_id = openai["id"].as_str().unwrap(); app.delete_with_session(&format!("/api/v1/admin/providers/{}", openai_id), &session) .await; // Create a new provider let body = serde_json::json!({ "provider_name": "openai", "display_name": "OpenAI Audit Test", "models": [{"model_id": "gpt-4o", "display_name": "GPT-4o"}] }); let (status, _) = app .post_with_session("/api/v1/admin/providers", &body, &session) .await; assert_eq!(status, StatusCode::CREATED); // Query the audit log directly from the database let audit_entries: Vec<(String, Option, Option)> = sqlx::query_as( "SELECT action, admin_user_id, target_type FROM audit_log WHERE action = 'create_provider' ORDER BY created_at DESC LIMIT 1" ) .fetch_all(&app.pool) .await .expect("Failed to query audit log"); assert!(!audit_entries.is_empty(), "Should have at least one create_provider audit entry"); let (action, audit_admin_id, target_type) = &audit_entries[0]; assert_eq!(action, "create_provider"); assert_eq!(*audit_admin_id, Some(admin_id)); assert_eq!(target_type.as_deref(), Some("provider")); } #[tokio::test] async fn audit_log_created_on_role_change() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (admin_id, admin_session) = app.create_admin_user("admin-audit2@example.com").await; let (target_id, _) = app.create_authenticated_user("target-audit2@example.com").await; // Promote user to admin let body = serde_json::json!({ "role": "admin" }); let uri = format!("/api/v1/admin/users/{}/role", target_id); let (status, _) = app .put_with_session(&uri, &body, &admin_session) .await; assert_eq!(status, StatusCode::OK); // Query the audit log let audit_entries: Vec<(String, Option, Option, Option)> = sqlx::query_as( "SELECT action, admin_user_id, target_type, target_id FROM audit_log WHERE action = 'update_user_role' ORDER BY created_at DESC LIMIT 1" ) .fetch_all(&app.pool) .await .expect("Failed to query audit log"); assert!(!audit_entries.is_empty(), "Should have at least one update_user_role audit entry"); let (action, audit_admin_id, target_type, target_id_str) = &audit_entries[0]; assert_eq!(action, "update_user_role"); assert_eq!(*audit_admin_id, Some(admin_id)); assert_eq!(target_type.as_deref(), Some("user")); assert_eq!(target_id_str.as_deref(), Some(target_id.to_string().as_str())); }