use axum::{ response::Json, routing::{get, post}, Router, }; use calendar_backend::auth::AuthService; use calendar_backend::AppState; use reqwest::Client; use serde_json::json; use std::sync::Arc; use std::time::Duration; use tokio::time::sleep; use tower_http::cors::{Any, CorsLayer}; /// Test utilities for integration testing mod test_utils { use super::*; pub struct TestServer { pub base_url: String, pub client: Client, } impl TestServer { pub async fn start() -> Self { // Create auth service let jwt_secret = "test-secret-key-for-integration-tests".to_string(); let auth_service = AuthService::new(jwt_secret); let app_state = AppState { auth_service }; // Build application with routes let app = Router::new() .route("/", get(root)) .route("/api/health", get(health_check)) .route("/api/auth/login", post(calendar_backend::handlers::login)) .route( "/api/auth/verify", get(calendar_backend::handlers::verify_token), ) .route( "/api/user/info", get(calendar_backend::handlers::get_user_info), ) .route( "/api/calendar/create", post(calendar_backend::handlers::create_calendar), ) .route( "/api/calendar/delete", post(calendar_backend::handlers::delete_calendar), ) .route( "/api/calendar/events", get(calendar_backend::handlers::get_calendar_events), ) .route( "/api/calendar/events/create", post(calendar_backend::handlers::create_event), ) .route( "/api/calendar/events/update", post(calendar_backend::handlers::update_event), ) .route( "/api/calendar/events/delete", post(calendar_backend::handlers::delete_event), ) .route( "/api/calendar/events/:uid", get(calendar_backend::handlers::refresh_event), ) // Event series-specific endpoints .route( "/api/calendar/events/series/create", post(calendar_backend::handlers::create_event_series), ) .route( "/api/calendar/events/series/update", post(calendar_backend::handlers::update_event_series), ) .route( "/api/calendar/events/series/delete", post(calendar_backend::handlers::delete_event_series), ) .layer( CorsLayer::new() .allow_origin(Any) .allow_methods(Any) .allow_headers(Any), ) .with_state(Arc::new(app_state)); // Start server on a random port let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let base_url = format!("http://127.0.0.1:{}", addr.port()); tokio::spawn(async move { axum::serve(listener, app).await.unwrap(); }); // Wait for server to start sleep(Duration::from_millis(100)).await; let client = Client::new(); TestServer { base_url, client } } pub async fn login(&self) -> String { let login_payload = json!({ "username": "test".to_string(), "password": "test".to_string(), "server_url": "https://example.com".to_string() }); let response = self .client .post(&format!("{}/api/auth/login", self.base_url)) .json(&login_payload) .send() .await .expect("Failed to send login request"); assert!( response.status().is_success(), "Login failed with status: {}", response.status() ); let login_response: serde_json::Value = response.json().await.unwrap(); login_response["token"] .as_str() .expect("Login response should contain token") .to_string() } } async fn root() -> &'static str { "Calendar Backend API v0.1.0" } async fn health_check() -> Json { Json(serde_json::json!({ "status": "healthy", "service": "calendar-backend", "version": "0.1.0" })) } } #[cfg(test)] mod tests { use super::test_utils::*; use super::*; /// Test the health endpoint #[tokio::test] async fn test_health_endpoint() { let server = TestServer::start().await; let response = server .client .get(&format!("{}/api/health", server.base_url)) .send() .await .unwrap(); assert_eq!(response.status(), 200); let health_response: serde_json::Value = response.json().await.unwrap(); assert_eq!(health_response["status"], "healthy"); assert_eq!(health_response["service"], "calendar-backend"); println!("✓ Health endpoint test passed"); } /// Test authentication login endpoint #[tokio::test] async fn test_auth_login() { let server = TestServer::start().await; // Use test credentials let username = "test".to_string(); let password = "test".to_string(); let server_url = "https://example.com".to_string(); let login_payload = json!({ "username": username, "password": password, "server_url": server_url }); let response = server .client .post(&format!("{}/api/auth/login", server.base_url)) .json(&login_payload) .send() .await .unwrap(); assert!( response.status().is_success(), "Login failed with status: {}", response.status() ); let login_response: serde_json::Value = response.json().await.unwrap(); assert!( login_response["token"].is_string(), "Login response should contain a token" ); assert!( login_response["username"].is_string(), "Login response should contain username" ); println!("✓ Authentication login test passed"); } /// Test authentication verify endpoint #[tokio::test] async fn test_auth_verify() { let server = TestServer::start().await; // First login to get a token let token = server.login().await; let response = server .client .get(&format!("{}/api/auth/verify", server.base_url)) .header("Authorization", format!("Bearer {}", token)) .send() .await .unwrap(); assert_eq!(response.status(), 200); let verify_response: serde_json::Value = response.json().await.unwrap(); assert!(verify_response["valid"].as_bool().unwrap_or(false)); println!("✓ Authentication verify test passed"); } /// Test user info endpoint #[tokio::test] async fn test_user_info() { let server = TestServer::start().await; // First login to get a token let token = server.login().await; // Load password from env for CalDAV requests dotenvy::dotenv().ok(); let password = "test".to_string(); let response = server .client .get(&format!("{}/api/user/info", server.base_url)) .header("Authorization", format!("Bearer {}", token)) .header("X-CalDAV-Password", password) .send() .await .unwrap(); // Note: This might fail if CalDAV server discovery fails, which can happen if response.status().is_success() { let user_info: serde_json::Value = response.json().await.unwrap(); assert!(user_info["username"].is_string()); println!("✓ User info test passed"); } else { println!( "⚠ User info test skipped (CalDAV server issues): {}", response.status() ); } } /// Test calendar events listing endpoint #[tokio::test] async fn test_get_calendar_events() { let server = TestServer::start().await; // First login to get a token let token = server.login().await; // Load password from env for CalDAV requests dotenvy::dotenv().ok(); let password = "test".to_string(); let response = server .client .get(&format!( "{}/api/calendar/events?year=2024&month=12", server.base_url )) .header("Authorization", format!("Bearer {}", token)) .header("X-CalDAV-Password", password) .send() .await .unwrap(); assert!( response.status().is_success(), "Get events failed with status: {}", response.status() ); let events: serde_json::Value = response.json().await.unwrap(); assert!(events.is_array()); println!( "✓ Get calendar events test passed (found {} events)", events.as_array().unwrap().len() ); } /// Test event creation endpoint #[tokio::test] async fn test_create_event() { let server = TestServer::start().await; // First login to get a token let token = server.login().await; // Load password from env for CalDAV requests dotenvy::dotenv().ok(); let password = "test".to_string(); let create_payload = json!({ "title": "Integration Test Event", "description": "Created by integration test", "start_date": "2024-12-25", "start_time": "10:00", "end_date": "2024-12-25", "end_time": "11:00", "location": "Test Location", "all_day": false, "status": "confirmed", "class": "public", "priority": 5, "organizer": "test@example.com", "attendees": "", "categories": "test", "reminder": "15min", "recurrence": "none", "recurrence_days": [false, false, false, false, false, false, false] }); let response = server .client .post(&format!("{}/api/calendar/events/create", server.base_url)) .header("Authorization", format!("Bearer {}", token)) .header("X-CalDAV-Password", password) .json(&create_payload) .send() .await .unwrap(); let status = response.status(); println!("Create event response status: {}", status); // Note: This might fail if CalDAV server is not accessible, which is expected in CI if status.is_success() { let create_response: serde_json::Value = response.json().await.unwrap(); assert!(create_response["success"].as_bool().unwrap_or(false)); println!("✓ Create event test passed"); } else { println!("⚠ Create event test skipped (CalDAV server not accessible)"); } } /// Test event refresh endpoint #[tokio::test] async fn test_refresh_event() { let server = TestServer::start().await; // First login to get a token let token = server.login().await; // Load password from env for CalDAV requests dotenvy::dotenv().ok(); let password = "test".to_string(); // Use a dummy UID for testing - this will likely return 404 but we're testing the endpoint structure let test_uid = "test-event-uid"; let response = server .client .get(&format!( "{}/api/calendar/events/{}", server.base_url, test_uid )) .header("Authorization", format!("Bearer {}", token)) .header("X-CalDAV-Password", password) .send() .await .unwrap(); // We expect either 200 (if event exists) or 404 (if not found) - both are valid responses assert!( response.status() == 200 || response.status() == 404, "Refresh event failed with unexpected status: {}", response.status() ); println!("✓ Refresh event endpoint test passed"); } /// Test invalid authentication #[tokio::test] async fn test_invalid_auth() { let server = TestServer::start().await; let response = server .client .get(&format!("{}/api/user/info", server.base_url)) .header("Authorization", "Bearer invalid-token") .send() .await .unwrap(); // Accept both 400 and 401 as valid responses for invalid tokens assert!( response.status() == 401 || response.status() == 400, "Expected 401 or 400 for invalid token, got {}", response.status() ); println!("✓ Invalid authentication test passed"); } /// Test missing authentication #[tokio::test] async fn test_missing_auth() { let server = TestServer::start().await; let response = server .client .get(&format!("{}/api/user/info", server.base_url)) .send() .await .unwrap(); assert_eq!(response.status(), 401); println!("✓ Missing authentication test passed"); } // ==================== EVENT SERIES TESTS ==================== /// Test event series creation endpoint #[tokio::test] async fn test_create_event_series() { let server = TestServer::start().await; // First login to get a token let token = server.login().await; // Load password from env for CalDAV requests dotenvy::dotenv().ok(); let password = "test".to_string(); let create_payload = json!({ "title": "Integration Test Series", "description": "Created by integration test for series", "start_date": "2024-12-25", "start_time": "10:00", "end_date": "2024-12-25", "end_time": "11:00", "location": "Test Series Location", "all_day": false, "status": "confirmed", "class": "public", "priority": 5, "organizer": "test@example.com", "attendees": "", "categories": "test-series", "reminder": "15min", "recurrence": "weekly", "recurrence_days": [false, true, false, false, false, false, false], // Monday only "recurrence_interval": 1, "recurrence_count": 4, "calendar_path": "/calendars/test/default/" // Provide explicit path to bypass discovery }); let response = server .client .post(&format!( "{}/api/calendar/events/series/create", server.base_url )) .header("Authorization", format!("Bearer {}", token)) .header("X-CalDAV-Password", password) .json(&create_payload) .send() .await .unwrap(); let status = response.status(); println!("Create series response status: {}", status); // Note: This might fail if CalDAV server is not accessible, which is expected in CI if status.is_success() { let create_response: serde_json::Value = response.json().await.unwrap(); assert!(create_response["success"].as_bool().unwrap_or(false)); assert!(create_response["series_uid"].is_string()); println!("✓ Create event series test passed"); } else { println!("⚠ Create event series test skipped (CalDAV server not accessible)"); } } /// Test event series update endpoint #[tokio::test] async fn test_update_event_series() { let server = TestServer::start().await; // First login to get a token let token = server.login().await; // Load password from env for CalDAV requests dotenvy::dotenv().ok(); let password = "test".to_string(); let update_payload = json!({ "series_uid": "test-series-uid", "title": "Updated Series Title", "description": "Updated by integration test", "start_date": "2024-12-26", "start_time": "14:00", "end_date": "2024-12-26", "end_time": "15:00", "location": "Updated Location", "all_day": false, "status": "confirmed", "class": "public", "priority": 3, "organizer": "test@example.com", "attendees": "attendee@example.com", "categories": "updated-series", "reminder": "30min", "recurrence": "daily", "recurrence_days": [false, false, false, false, false, false, false], "recurrence_interval": 2, "recurrence_count": 10, "update_scope": "all_in_series", "calendar_path": "/calendars/test/default/" // Provide explicit path to bypass discovery }); let response = server .client .post(&format!( "{}/api/calendar/events/series/update", server.base_url )) .header("Authorization", format!("Bearer {}", token)) .header("X-CalDAV-Password", password) .json(&update_payload) .send() .await .unwrap(); let status = response.status(); println!("Update series response status: {}", status); // Note: This might fail if CalDAV server is not accessible or event doesn't exist, which is expected in CI if status.is_success() { let update_response: serde_json::Value = response.json().await.unwrap(); assert!(update_response["success"].as_bool().unwrap_or(false)); assert_eq!( update_response["series_uid"].as_str().unwrap(), "test-series-uid" ); println!("✓ Update event series test passed"); } else if status == 404 { println!( "⚠ Update event series test skipped (event not found - expected for test data)" ); } else { println!("⚠ Update event series test skipped (CalDAV server not accessible)"); } } /// Test event series deletion endpoint #[tokio::test] async fn test_delete_event_series() { let server = TestServer::start().await; // First login to get a token let token = server.login().await; // Load password from env for CalDAV requests dotenvy::dotenv().ok(); let password = "test".to_string(); let delete_payload = json!({ "series_uid": "test-series-to-delete", "calendar_path": "/calendars/test/default/", "event_href": "test-series.ics", "delete_scope": "all_in_series" }); let response = server .client .post(&format!( "{}/api/calendar/events/series/delete", server.base_url )) .header("Authorization", format!("Bearer {}", token)) .header("X-CalDAV-Password", password) .json(&delete_payload) .send() .await .unwrap(); let status = response.status(); println!("Delete series response status: {}", status); // Note: This might fail if CalDAV server is not accessible or event doesn't exist, which is expected in CI if status.is_success() { let delete_response: serde_json::Value = response.json().await.unwrap(); assert!(delete_response["success"].as_bool().unwrap_or(false)); println!("✓ Delete event series test passed"); } else if status == 404 { println!( "⚠ Delete event series test skipped (event not found - expected for test data)" ); } else { println!("⚠ Delete event series test skipped (CalDAV server not accessible)"); } } /// Test invalid update scope #[tokio::test] async fn test_invalid_update_scope() { let server = TestServer::start().await; // First login to get a token let token = server.login().await; let invalid_payload = json!({ "series_uid": "test-series-uid", "title": "Test Title", "description": "Test", "start_date": "2024-12-25", "start_time": "10:00", "end_date": "2024-12-25", "end_time": "11:00", "location": "Test", "all_day": false, "status": "confirmed", "class": "public", "organizer": "test@example.com", "attendees": "", "categories": "", "reminder": "none", "recurrence": "none", "recurrence_days": [false, false, false, false, false, false, false], "update_scope": "invalid_scope" // This should cause a 400 error }); let response = server .client .post(&format!( "{}/api/calendar/events/series/update", server.base_url )) .header("Authorization", format!("Bearer {}", token)) .json(&invalid_payload) .send() .await .unwrap(); assert_eq!( response.status(), 400, "Expected 400 for invalid update scope" ); println!("✓ Invalid update scope test passed"); } /// Test non-recurring event rejection in series endpoint #[tokio::test] async fn test_non_recurring_series_rejection() { let server = TestServer::start().await; // First login to get a token let token = server.login().await; let non_recurring_payload = json!({ "title": "Non-recurring Event", "description": "This should be rejected", "start_date": "2024-12-25", "start_time": "10:00", "end_date": "2024-12-25", "end_time": "11:00", "location": "Test", "all_day": false, "status": "confirmed", "class": "public", "organizer": "test@example.com", "attendees": "", "categories": "", "reminder": "none", "recurrence": "none", // This should cause rejection "recurrence_days": [false, false, false, false, false, false, false] }); let response = server .client .post(&format!( "{}/api/calendar/events/series/create", server.base_url )) .header("Authorization", format!("Bearer {}", token)) .json(&non_recurring_payload) .send() .await .unwrap(); assert_eq!( response.status(), 400, "Expected 400 for non-recurring event in series endpoint" ); println!("✓ Non-recurring series rejection test passed"); } }