Fix calendar event fetching to use visible date range
Some checks failed
Build and Push Docker Image / docker (push) Failing after 1m7s

Moved event fetching logic from CalendarView to Calendar component to properly
use the visible date range instead of hardcoded current month. The Calendar
component already tracks the current visible date through navigation, so events
now load correctly for August and other months when navigating.

Changes:
- Calendar component now manages its own events state and fetching
- Event fetching responds to current_date changes from navigation
- CalendarView simplified to just render Calendar component
- Fixed cargo fmt/clippy formatting across codebase

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Connor Johnstone
2025-09-01 18:31:51 -04:00
parent e55e6bf4dd
commit 79f287ed61
38 changed files with 3922 additions and 2590 deletions

View File

@@ -2,16 +2,16 @@ use chrono::{Duration, Utc};
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use crate::models::{CalDAVLoginRequest, AuthResponse, ApiError};
use crate::config::CalDAVConfig;
use crate::calendar::CalDAVClient;
use crate::config::CalDAVConfig;
use crate::models::{ApiError, AuthResponse, CalDAVLoginRequest};
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub username: String,
pub server_url: String,
pub exp: i64, // Expiration time
pub iat: i64, // Issued at
pub exp: i64, // Expiration time
pub iat: i64, // Issued at
}
#[derive(Clone)]
@@ -33,22 +33,25 @@ impl AuthService {
// Create CalDAV config with provided credentials
let caldav_config = CalDAVConfig::new(
request.server_url.clone(),
request.username.clone(),
request.password.clone()
request.username.clone(),
request.password.clone(),
);
println!("📝 Created CalDAV config");
// Test authentication against CalDAV server
let caldav_client = CalDAVClient::new(caldav_config.clone());
println!("🔗 Created CalDAV client, attempting to discover calendars...");
// Try to discover calendars as an authentication test
match caldav_client.discover_calendars().await {
Ok(calendars) => {
println!("✅ Authentication successful! Found {} calendars", calendars.len());
println!(
"✅ Authentication successful! Found {} calendars",
calendars.len()
);
// Authentication successful, generate JWT token
let token = self.generate_token(&request.username, &request.server_url)?;
Ok(AuthResponse {
token,
username: request.username,
@@ -58,7 +61,9 @@ impl AuthService {
Err(err) => {
println!("❌ Authentication failed: {:?}", err);
// Authentication failed
Err(ApiError::Unauthorized("Invalid CalDAV credentials or server unavailable".to_string()))
Err(ApiError::Unauthorized(
"Invalid CalDAV credentials or server unavailable".to_string(),
))
}
}
}
@@ -69,13 +74,17 @@ impl AuthService {
}
/// Create CalDAV config from token
pub fn caldav_config_from_token(&self, token: &str, password: &str) -> Result<CalDAVConfig, ApiError> {
pub fn caldav_config_from_token(
&self,
token: &str,
password: &str,
) -> Result<CalDAVConfig, ApiError> {
let claims = self.verify_token(token)?;
Ok(CalDAVConfig::new(
claims.server_url,
claims.username,
password.to_string()
claims.username,
password.to_string(),
))
}
@@ -93,8 +102,11 @@ impl AuthService {
}
// Basic URL validation
if !request.server_url.starts_with("http://") && !request.server_url.starts_with("https://") {
return Err(ApiError::BadRequest("Server URL must start with http:// or https://".to_string()));
if !request.server_url.starts_with("http://") && !request.server_url.starts_with("https://")
{
return Err(ApiError::BadRequest(
"Server URL must start with http:// or https://".to_string(),
));
}
Ok(())
@@ -131,4 +143,4 @@ impl AuthService {
Ok(token_data.claims)
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,30 +1,30 @@
use base64::prelude::*;
use serde::{Deserialize, Serialize};
use std::env;
use base64::prelude::*;
/// Configuration for CalDAV server connection and authentication.
///
///
/// This struct holds all the necessary information to connect to a CalDAV server,
/// including server URL, credentials, and optional collection paths.
///
///
/// # Security Note
///
///
/// The password field contains sensitive information and should be handled carefully.
/// This struct implements `Debug` but in production, consider implementing a custom
/// `Debug` that masks the password field.
///
///
/// # Example
///
///
/// ```rust
/// # use calendar_backend::config::CalDAVConfig;
/// let config = CalDAVConfig {
/// server_url: "https://caldav.example.com".to_string(),
/// username: "user@example.com".to_string(),
/// username: "user@example.com".to_string(),
/// password: "password".to_string(),
/// calendar_path: None,
/// tasks_path: None,
/// };
///
///
/// // Use the configuration for HTTP requests
/// let auth_header = format!("Basic {}", config.get_basic_auth());
/// ```
@@ -32,17 +32,17 @@ use base64::prelude::*;
pub struct CalDAVConfig {
/// The base URL of the CalDAV server (e.g., "https://caldav.example.com/dav/")
pub server_url: String,
/// Username for authentication with the CalDAV server
pub username: String,
/// Password for authentication with the CalDAV server
///
///
/// **Security Note**: This contains sensitive information
pub password: String,
/// Optional path to the calendar collection on the server
///
///
/// If not provided, the client will discover available calendars
/// through CalDAV PROPFIND requests
pub calendar_path: Option<String>,
@@ -50,20 +50,20 @@ pub struct CalDAVConfig {
impl CalDAVConfig {
/// Creates a new CalDAVConfig with the given credentials.
///
///
/// # Arguments
///
///
/// * `server_url` - The base URL of the CalDAV server
/// * `username` - Username for authentication
/// * `password` - Password for authentication
///
///
/// # Example
///
///
/// ```rust
/// # use calendar_backend::config::CalDAVConfig;
/// let config = CalDAVConfig::new(
/// "https://caldav.example.com".to_string(),
/// "user@example.com".to_string(),
/// "user@example.com".to_string(),
/// "password".to_string()
/// );
/// ```
@@ -77,21 +77,21 @@ impl CalDAVConfig {
}
/// Generates a Base64-encoded string for HTTP Basic Authentication.
///
///
/// This method combines the username and password in the format
/// `username:password` and encodes it using Base64, which is the
/// standard format for the `Authorization: Basic` HTTP header.
///
///
/// # Returns
///
///
/// A Base64-encoded string that can be used directly in the
/// `Authorization` header: `Authorization: Basic <returned_value>`
///
///
/// # Example
///
///
/// ```rust
/// # use calendar_backend::config::CalDAVConfig;
///
///
/// let config = CalDAVConfig {
/// server_url: "https://example.com".to_string(),
/// username: "user".to_string(),
@@ -99,7 +99,7 @@ impl CalDAVConfig {
/// calendar_path: None,
/// tasks_path: None,
/// };
///
///
/// let auth_value = config.get_basic_auth();
/// let auth_header = format!("Basic {}", auth_value);
/// ```
@@ -113,15 +113,15 @@ impl CalDAVConfig {
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
/// A required environment variable is missing or cannot be read.
///
///
/// This error occurs when calling `CalDAVConfig::from_env()` and one of the
/// required environment variables (`CALDAV_SERVER_URL`, `CALDAV_USERNAME`,
/// or `CALDAV_PASSWORD`) is not set.
#[error("Missing environment variable: {0}")]
MissingVar(String),
/// The configuration contains invalid or malformed values.
///
///
/// This could include malformed URLs, invalid authentication credentials,
/// or other configuration issues that prevent proper CalDAV operation.
#[error("Invalid configuration: {0}")]
@@ -139,7 +139,6 @@ mod tests {
username: "testuser".to_string(),
password: "testpass".to_string(),
calendar_path: None,
tasks_path: None,
};
let auth = config.get_basic_auth();
@@ -148,12 +147,12 @@ mod tests {
}
/// Integration test that authenticates with the actual Baikal CalDAV server
///
///
/// This test requires a valid .env file with:
/// - CALDAV_SERVER_URL
/// - CALDAV_USERNAME
/// - CALDAV_PASSWORD
///
///
/// Run with: `cargo test test_baikal_auth`
#[tokio::test]
async fn test_baikal_auth() {
@@ -161,7 +160,7 @@ mod tests {
let config = CalDAVConfig::new(
"https://example.com".to_string(),
"test_user".to_string(),
"test_password".to_string()
"test_password".to_string(),
);
println!("Testing authentication to: {}", config.server_url);
@@ -172,7 +171,10 @@ mod tests {
// Make a simple OPTIONS request to test authentication
let response = client
.request(reqwest::Method::OPTIONS, &config.server_url)
.header("Authorization", format!("Basic {}", config.get_basic_auth()))
.header(
"Authorization",
format!("Basic {}", config.get_basic_auth()),
)
.header("User-Agent", "calendar-app/0.1.0")
.send()
.await
@@ -190,9 +192,9 @@ mod tests {
// For Baikal/CalDAV servers, we should see DAV headers
assert!(
response.headers().contains_key("dav") ||
response.headers().contains_key("DAV") ||
response.status().is_success(),
response.headers().contains_key("dav")
|| response.headers().contains_key("DAV")
|| response.status().is_success(),
"Server doesn't appear to be a CalDAV server - missing DAV headers"
);
@@ -200,17 +202,17 @@ mod tests {
}
/// Test making a PROPFIND request to discover calendars
///
///
/// This test requires a valid .env file and makes an actual CalDAV PROPFIND request
///
///
/// Run with: `cargo test test_propfind_calendars`
#[tokio::test]
async fn test_propfind_calendars() {
// Use test config - update these values to test with real server
// Use test config - update these values to test with real server
let config = CalDAVConfig::new(
"https://example.com".to_string(),
"test_user".to_string(),
"test_password".to_string()
"test_password".to_string(),
);
let client = reqwest::Client::new();
@@ -227,8 +229,14 @@ mod tests {
</d:propfind>"#;
let response = client
.request(reqwest::Method::from_bytes(b"PROPFIND").unwrap(), &config.server_url)
.header("Authorization", format!("Basic {}", config.get_basic_auth()))
.request(
reqwest::Method::from_bytes(b"PROPFIND").unwrap(),
&config.server_url,
)
.header(
"Authorization",
format!("Basic {}", config.get_basic_auth()),
)
.header("Content-Type", "application/xml")
.header("Depth", "1")
.header("User-Agent", "calendar-app/0.1.0")
@@ -239,7 +247,7 @@ mod tests {
let status = response.status();
println!("PROPFIND Response status: {}", status);
let body = response.text().await.expect("Failed to read response body");
println!("PROPFIND Response body: {}", body);
@@ -251,8 +259,11 @@ mod tests {
);
// The response should contain XML with calendar information
assert!(body.contains("calendar"), "Response should contain calendar information");
assert!(
body.contains("calendar"),
"Response should contain calendar information"
);
println!("✓ PROPFIND calendars test passed!");
}
}
}

View File

@@ -4,7 +4,7 @@ mod calendar;
mod events;
mod series;
pub use auth::{login, verify_token, get_user_info};
pub use auth::{get_user_info, login, verify_token};
pub use calendar::{create_calendar, delete_calendar};
pub use events::{get_calendar_events, refresh_event, create_event, update_event, delete_event};
pub use series::{create_event_series, update_event_series, delete_event_series};
pub use events::{create_event, delete_event, get_calendar_events, refresh_event, update_event};
pub use series::{create_event_series, delete_event_series, update_event_series};

View File

@@ -1,33 +1,38 @@
use axum::{
extract::State,
http::HeaderMap,
response::Json,
};
use axum::{extract::State, http::HeaderMap, response::Json};
use std::sync::Arc;
use crate::{AppState, models::{CalDAVLoginRequest, AuthResponse, ApiError, UserInfo, CalendarInfo}};
use crate::calendar::CalDAVClient;
use crate::config::CalDAVConfig;
use crate::{
models::{ApiError, AuthResponse, CalDAVLoginRequest, CalendarInfo, UserInfo},
AppState,
};
pub fn extract_bearer_token(headers: &HeaderMap) -> Result<String, ApiError> {
let auth_header = headers.get("authorization")
let auth_header = headers
.get("authorization")
.ok_or_else(|| ApiError::Unauthorized("Missing Authorization header".to_string()))?;
let auth_str = auth_header.to_str()
let auth_str = auth_header
.to_str()
.map_err(|_| ApiError::BadRequest("Invalid Authorization header".to_string()))?;
if let Some(token) = auth_str.strip_prefix("Bearer ") {
Ok(token.to_string())
} else {
Err(ApiError::BadRequest("Authorization header must be Bearer token".to_string()))
Err(ApiError::BadRequest(
"Authorization header must be Bearer token".to_string(),
))
}
}
pub fn extract_password_header(headers: &HeaderMap) -> Result<String, ApiError> {
let password_header = headers.get("x-caldav-password")
let password_header = headers
.get("x-caldav-password")
.ok_or_else(|| ApiError::BadRequest("Missing X-CalDAV-Password header".to_string()))?;
password_header.to_str()
password_header
.to_str()
.map(|s| s.to_string())
.map_err(|_| ApiError::BadRequest("Invalid X-CalDAV-Password header".to_string()))
}
@@ -40,32 +45,37 @@ pub async fn login(
println!(" Server URL: {}", request.server_url);
println!(" Username: {}", request.username);
println!(" Password length: {}", request.password.len());
// Basic validation
if request.username.is_empty() || request.password.is_empty() || request.server_url.is_empty() {
return Err(ApiError::BadRequest("Username, password, and server URL are required".to_string()));
return Err(ApiError::BadRequest(
"Username, password, and server URL are required".to_string(),
));
}
println!("✅ Input validation passed");
// Create a token using the auth service
println!("📝 Created CalDAV config");
// First verify the credentials are valid by attempting to discover calendars
let config = CalDAVConfig::new(
request.server_url.clone(),
request.username.clone(),
request.password.clone()
request.password.clone(),
);
let client = CalDAVClient::new(config);
client.discover_calendars()
client
.discover_calendars()
.await
.map_err(|e| ApiError::Unauthorized(format!("Authentication failed: {}", e)))?;
let token = state.auth_service.generate_token(&request.username, &request.server_url)?;
let token = state
.auth_service
.generate_token(&request.username, &request.server_url)?;
println!("🔗 Created CalDAV client, attempting to discover calendars...");
Ok(Json(AuthResponse {
token,
username: request.username,
@@ -79,7 +89,7 @@ pub async fn verify_token(
) -> Result<Json<serde_json::Value>, ApiError> {
let token = extract_bearer_token(&headers)?;
let is_valid = state.auth_service.verify_token(&token).is_ok();
Ok(Json(serde_json::json!({ "valid": is_valid })))
}
@@ -89,26 +99,33 @@ pub async fn get_user_info(
) -> Result<Json<UserInfo>, ApiError> {
let token = extract_bearer_token(&headers)?;
let password = extract_password_header(&headers)?;
// Create CalDAV config from token and password
let config = state.auth_service.caldav_config_from_token(&token, &password)?;
let config = state
.auth_service
.caldav_config_from_token(&token, &password)?;
let client = CalDAVClient::new(config.clone());
// Discover calendars
let calendar_paths = client.discover_calendars()
let calendar_paths = client
.discover_calendars()
.await
.map_err(|e| ApiError::Internal(format!("Failed to discover calendars: {}", e)))?;
println!("✅ Authentication successful! Found {} calendars", calendar_paths.len());
let calendars: Vec<CalendarInfo> = calendar_paths.iter().map(|path| {
CalendarInfo {
println!(
"✅ Authentication successful! Found {} calendars",
calendar_paths.len()
);
let calendars: Vec<CalendarInfo> = calendar_paths
.iter()
.map(|path| CalendarInfo {
path: path.clone(),
display_name: extract_calendar_name(path),
color: generate_calendar_color(path),
}
}).collect();
})
.collect();
Ok(Json(UserInfo {
username: config.username,
server_url: config.server_url,
@@ -123,15 +140,14 @@ fn generate_calendar_color(path: &str) -> String {
for byte in path.bytes() {
hash = hash.wrapping_mul(31).wrapping_add(byte as u32);
}
// Define a set of pleasant colors
let colors = [
"#3B82F6", "#10B981", "#F59E0B", "#EF4444", "#8B5CF6",
"#06B6D4", "#84CC16", "#F97316", "#EC4899", "#6366F1",
"#14B8A6", "#F3B806", "#8B5A2B", "#6B7280", "#DC2626",
"#7C3AED", "#059669", "#D97706", "#BE185D", "#4F46E5"
"#3B82F6", "#10B981", "#F59E0B", "#EF4444", "#8B5CF6", "#06B6D4", "#84CC16", "#F97316",
"#EC4899", "#6366F1", "#14B8A6", "#F3B806", "#8B5A2B", "#6B7280", "#DC2626", "#7C3AED",
"#059669", "#D97706", "#BE185D", "#4F46E5",
];
colors[(hash as usize) % colors.len()].to_string()
}
@@ -154,4 +170,4 @@ fn extract_calendar_name(path: &str) -> String {
})
.collect::<Vec<String>>()
.join(" ")
}
}

View File

@@ -1,12 +1,14 @@
use axum::{
extract::State,
http::HeaderMap,
response::Json,
};
use axum::{extract::State, http::HeaderMap, response::Json};
use std::sync::Arc;
use crate::{AppState, models::{ApiError, CreateCalendarRequest, CreateCalendarResponse, DeleteCalendarRequest, DeleteCalendarResponse}};
use crate::calendar::CalDAVClient;
use crate::{
models::{
ApiError, CreateCalendarRequest, CreateCalendarResponse, DeleteCalendarRequest,
DeleteCalendarResponse,
},
AppState,
};
use super::auth::{extract_bearer_token, extract_password_header};
@@ -20,22 +22,36 @@ pub async fn create_calendar(
// Validate request
if request.name.trim().is_empty() {
return Err(ApiError::BadRequest("Calendar name is required".to_string()));
return Err(ApiError::BadRequest(
"Calendar name is required".to_string(),
));
}
// Create CalDAV config from token and password
let config = state.auth_service.caldav_config_from_token(&token, &password)?;
let config = state
.auth_service
.caldav_config_from_token(&token, &password)?;
let client = CalDAVClient::new(config);
// Create calendar on CalDAV server
match client.create_calendar(&request.name, request.description.as_deref(), request.color.as_deref()).await {
match client
.create_calendar(
&request.name,
request.description.as_deref(),
request.color.as_deref(),
)
.await
{
Ok(_) => Ok(Json(CreateCalendarResponse {
success: true,
message: "Calendar created successfully".to_string(),
})),
Err(e) => {
eprintln!("Failed to create calendar: {}", e);
Err(ApiError::Internal(format!("Failed to create calendar: {}", e)))
Err(ApiError::Internal(format!(
"Failed to create calendar: {}",
e
)))
}
}
}
@@ -50,11 +66,15 @@ pub async fn delete_calendar(
// Validate request
if request.path.trim().is_empty() {
return Err(ApiError::BadRequest("Calendar path is required".to_string()));
return Err(ApiError::BadRequest(
"Calendar path is required".to_string(),
));
}
// Create CalDAV config from token and password
let config = state.auth_service.caldav_config_from_token(&token, &password)?;
let config = state
.auth_service
.caldav_config_from_token(&token, &password)?;
let client = CalDAVClient::new(config);
// Delete calendar on CalDAV server
@@ -65,7 +85,10 @@ pub async fn delete_calendar(
})),
Err(e) => {
eprintln!("Failed to delete calendar: {}", e);
Err(ApiError::Internal(format!("Failed to delete calendar: {}", e)))
Err(ApiError::Internal(format!(
"Failed to delete calendar: {}",
e
)))
}
}
}
}

View File

@@ -1,15 +1,23 @@
use axum::{
extract::{State, Query, Path},
extract::{Path, Query, State},
http::HeaderMap,
response::Json,
};
use chrono::Datelike;
use serde::Deserialize;
use std::sync::Arc;
use chrono::Datelike;
use calendar_models::{VEvent, EventStatus, EventClass, CalendarUser, Attendee, VAlarm, AlarmAction, AlarmTrigger};
use crate::{AppState, models::{ApiError, DeleteEventRequest, DeleteEventResponse, CreateEventRequest, CreateEventResponse, UpdateEventRequest, UpdateEventResponse}};
use crate::calendar::{CalDAVClient, CalendarEvent};
use crate::{
models::{
ApiError, CreateEventRequest, CreateEventResponse, DeleteEventRequest, DeleteEventResponse,
UpdateEventRequest, UpdateEventResponse,
},
AppState,
};
use calendar_models::{
AlarmAction, AlarmTrigger, Attendee, CalendarUser, EventClass, EventStatus, VAlarm, VEvent,
};
use super::auth::{extract_bearer_token, extract_password_header};
@@ -28,20 +36,23 @@ pub async fn get_calendar_events(
let token = extract_bearer_token(&headers)?;
let password = extract_password_header(&headers)?;
println!("🔑 API call with password length: {}", password.len());
// Create CalDAV config from token and password
let config = state.auth_service.caldav_config_from_token(&token, &password)?;
let config = state
.auth_service
.caldav_config_from_token(&token, &password)?;
let client = CalDAVClient::new(config);
// Discover calendars if needed
let calendar_paths = client.discover_calendars()
let calendar_paths = client
.discover_calendars()
.await
.map_err(|e| ApiError::Internal(format!("Failed to discover calendars: {}", e)))?;
if calendar_paths.is_empty() {
return Ok(Json(vec![])); // No calendars found
}
// Fetch events from all calendars
let mut all_events = Vec::new();
for calendar_path in &calendar_paths {
@@ -54,12 +65,15 @@ pub async fn get_calendar_events(
all_events.extend(events);
}
Err(e) => {
eprintln!("Failed to fetch events from calendar {}: {}", calendar_path, e);
eprintln!(
"Failed to fetch events from calendar {}: {}",
calendar_path, e
);
// Continue with other calendars instead of failing completely
}
}
}
// If year and month are specified, filter events
if let (Some(year), Some(month)) = (params.year, params.month) {
all_events.retain(|event| {
@@ -68,7 +82,7 @@ pub async fn get_calendar_events(
event_year == year && event_month == month
});
}
println!("📅 Returning {} events", all_events.len());
Ok(Json(all_events))
}
@@ -80,16 +94,19 @@ pub async fn refresh_event(
) -> Result<Json<Option<CalendarEvent>>, ApiError> {
let token = extract_bearer_token(&headers)?;
let password = extract_password_header(&headers)?;
// Create CalDAV config from token and password
let config = state.auth_service.caldav_config_from_token(&token, &password)?;
let config = state
.auth_service
.caldav_config_from_token(&token, &password)?;
let client = CalDAVClient::new(config);
// Discover calendars
let calendar_paths = client.discover_calendars()
let calendar_paths = client
.discover_calendars()
.await
.map_err(|e| ApiError::Internal(format!("Failed to discover calendars: {}", e)))?;
// Search for the event by UID across all calendars
for calendar_path in &calendar_paths {
if let Ok(Some(mut event)) = client.fetch_event_by_uid(calendar_path, &uid).await {
@@ -97,18 +114,25 @@ pub async fn refresh_event(
return Ok(Json(Some(event)));
}
}
Ok(Json(None))
}
async fn fetch_event_by_href(client: &CalDAVClient, calendar_path: &str, event_href: &str) -> Result<Option<CalendarEvent>, crate::calendar::CalDAVError> {
async fn fetch_event_by_href(
client: &CalDAVClient,
calendar_path: &str,
event_href: &str,
) -> Result<Option<CalendarEvent>, crate::calendar::CalDAVError> {
// This is a simplified implementation - in a real scenario, you'd want to fetch the specific event by href
// For now, we'll fetch all events and find the matching one by href (inefficient but functional)
let events = client.fetch_events(calendar_path).await?;
println!("🔍 fetch_event_by_href: looking for href='{}'", event_href);
println!("🔍 Available events with hrefs: {:?}", events.iter().map(|e| (&e.uid, &e.href)).collect::<Vec<_>>());
println!(
"🔍 Available events with hrefs: {:?}",
events.iter().map(|e| (&e.uid, &e.href)).collect::<Vec<_>>()
);
// First try to match by exact href
for event in &events {
if let Some(stored_href) = &event.href {
@@ -118,22 +142,25 @@ async fn fetch_event_by_href(client: &CalDAVClient, calendar_path: &str, event_h
}
}
}
// Fallback: try to match by UID extracted from href filename
let filename = event_href.split('/').last().unwrap_or(event_href);
let uid_from_href = filename.trim_end_matches(".ics");
println!("🔍 Fallback: trying UID match. filename='{}', uid='{}'", filename, uid_from_href);
println!(
"🔍 Fallback: trying UID match. filename='{}', uid='{}'",
filename, uid_from_href
);
for event in events {
if event.uid == uid_from_href {
println!("✅ Found matching event by UID: {}", event.uid);
return Ok(Some(event));
}
}
println!("❌ No matching event found for href: {}", event_href);
Ok(None)
}
@@ -146,41 +173,63 @@ pub async fn delete_event(
let password = extract_password_header(&headers)?;
// Create CalDAV config from token and password
let config = state.auth_service.caldav_config_from_token(&token, &password)?;
let config = state
.auth_service
.caldav_config_from_token(&token, &password)?;
let client = CalDAVClient::new(config);
// Handle different delete actions for recurring events
match request.delete_action.as_str() {
"delete_this" => {
if let Some(event) = fetch_event_by_href(&client, &request.calendar_path, &request.event_href).await
.map_err(|e| ApiError::Internal(format!("Failed to fetch event: {}", e)))? {
if let Some(event) =
fetch_event_by_href(&client, &request.calendar_path, &request.event_href)
.await
.map_err(|e| ApiError::Internal(format!("Failed to fetch event: {}", e)))?
{
// Check if this is a recurring event
if event.rrule.is_some() && !event.rrule.as_ref().unwrap().is_empty() {
// Recurring event - add EXDATE for this occurrence
if let Some(occurrence_date) = &request.occurrence_date {
let exception_utc = if let Ok(date) = chrono::DateTime::parse_from_rfc3339(occurrence_date) {
let exception_utc = if let Ok(date) =
chrono::DateTime::parse_from_rfc3339(occurrence_date)
{
// RFC3339 format (with time and timezone)
date.with_timezone(&chrono::Utc)
} else if let Ok(naive_date) = chrono::NaiveDate::parse_from_str(occurrence_date, "%Y-%m-%d") {
} else if let Ok(naive_date) =
chrono::NaiveDate::parse_from_str(occurrence_date, "%Y-%m-%d")
{
// Simple date format (YYYY-MM-DD)
naive_date.and_hms_opt(0, 0, 0).unwrap().and_utc()
} else {
return Err(ApiError::BadRequest(format!("Invalid occurrence date format: {}. Expected RFC3339 or YYYY-MM-DD", occurrence_date)));
};
let mut updated_event = event;
updated_event.exdate.push(exception_utc);
println!("🔄 Adding EXDATE {} to recurring event {}", exception_utc.format("%Y%m%dT%H%M%SZ"), updated_event.uid);
println!(
"🔄 Adding EXDATE {} to recurring event {}",
exception_utc.format("%Y%m%dT%H%M%SZ"),
updated_event.uid
);
// Update the event with the new EXDATE
client.update_event(&request.calendar_path, &updated_event, &request.event_href)
client
.update_event(
&request.calendar_path,
&updated_event,
&request.event_href,
)
.await
.map_err(|e| ApiError::Internal(format!("Failed to update event with EXDATE: {}", e)))?;
.map_err(|e| {
ApiError::Internal(format!(
"Failed to update event with EXDATE: {}",
e
))
})?;
println!("✅ Successfully updated recurring event with EXDATE");
Ok(Json(DeleteEventResponse {
success: true,
message: "Single occurrence deleted successfully".to_string(),
@@ -191,13 +240,16 @@ pub async fn delete_event(
} else {
// Non-recurring event - delete the entire event
println!("🗑️ Deleting non-recurring event: {}", event.uid);
client.delete_event(&request.calendar_path, &request.event_href)
client
.delete_event(&request.calendar_path, &request.event_href)
.await
.map_err(|e| ApiError::Internal(format!("Failed to delete event: {}", e)))?;
.map_err(|e| {
ApiError::Internal(format!("Failed to delete event: {}", e))
})?;
println!("✅ Successfully deleted non-recurring event");
Ok(Json(DeleteEventResponse {
success: true,
message: "Event deleted successfully".to_string(),
@@ -206,70 +258,99 @@ pub async fn delete_event(
} else {
Err(ApiError::NotFound("Event not found".to_string()))
}
},
}
"delete_following" => {
// For "this and following" deletion, we need to:
// 1. Fetch the recurring event
// 2. Modify the RRULE to end before this occurrence
// 3. Update the event
if let Some(mut event) = fetch_event_by_href(&client, &request.calendar_path, &request.event_href).await
.map_err(|e| ApiError::Internal(format!("Failed to fetch event: {}", e)))? {
if let Some(mut event) =
fetch_event_by_href(&client, &request.calendar_path, &request.event_href)
.await
.map_err(|e| ApiError::Internal(format!("Failed to fetch event: {}", e)))?
{
if let Some(occurrence_date) = &request.occurrence_date {
let until_date = if let Ok(date) = chrono::DateTime::parse_from_rfc3339(occurrence_date) {
let until_date = if let Ok(date) =
chrono::DateTime::parse_from_rfc3339(occurrence_date)
{
// RFC3339 format (with time and timezone)
date.with_timezone(&chrono::Utc)
} else if let Ok(naive_date) = chrono::NaiveDate::parse_from_str(occurrence_date, "%Y-%m-%d") {
} else if let Ok(naive_date) =
chrono::NaiveDate::parse_from_str(occurrence_date, "%Y-%m-%d")
{
// Simple date format (YYYY-MM-DD)
naive_date.and_hms_opt(0, 0, 0).unwrap().and_utc()
} else {
return Err(ApiError::BadRequest(format!("Invalid occurrence date format: {}. Expected RFC3339 or YYYY-MM-DD", occurrence_date)));
return Err(ApiError::BadRequest(format!(
"Invalid occurrence date format: {}. Expected RFC3339 or YYYY-MM-DD",
occurrence_date
)));
};
// Modify the RRULE to add an UNTIL clause
if let Some(rrule) = &event.rrule {
// Remove existing UNTIL if present and add new one
let parts: Vec<&str> = rrule.split(';').filter(|part| {
!part.starts_with("UNTIL=") && !part.starts_with("COUNT=")
}).collect();
let new_rrule = format!("{};UNTIL={}", parts.join(";"), until_date.format("%Y%m%dT%H%M%SZ"));
let parts: Vec<&str> = rrule
.split(';')
.filter(|part| {
!part.starts_with("UNTIL=") && !part.starts_with("COUNT=")
})
.collect();
let new_rrule = format!(
"{};UNTIL={}",
parts.join(";"),
until_date.format("%Y%m%dT%H%M%SZ")
);
event.rrule = Some(new_rrule);
// Update the event with the modified RRULE
client.update_event(&request.calendar_path, &event, &request.event_href)
client
.update_event(&request.calendar_path, &event, &request.event_href)
.await
.map_err(|e| ApiError::Internal(format!("Failed to update event with modified RRULE: {}", e)))?;
.map_err(|e| {
ApiError::Internal(format!(
"Failed to update event with modified RRULE: {}",
e
))
})?;
Ok(Json(DeleteEventResponse {
success: true,
message: "This and following occurrences deleted successfully".to_string(),
message: "This and following occurrences deleted successfully"
.to_string(),
}))
} else {
// No RRULE, just delete the single event
client.delete_event(&request.calendar_path, &request.event_href)
client
.delete_event(&request.calendar_path, &request.event_href)
.await
.map_err(|e| ApiError::Internal(format!("Failed to delete event: {}", e)))?;
.map_err(|e| {
ApiError::Internal(format!("Failed to delete event: {}", e))
})?;
Ok(Json(DeleteEventResponse {
success: true,
message: "Event deleted successfully".to_string(),
}))
}
} else {
Err(ApiError::BadRequest("Occurrence date is required for following deletion".to_string()))
Err(ApiError::BadRequest(
"Occurrence date is required for following deletion".to_string(),
))
}
} else {
Err(ApiError::NotFound("Event not found".to_string()))
}
},
}
"delete_series" | _ => {
// Delete the entire event/series
client.delete_event(&request.calendar_path, &request.event_href)
client
.delete_event(&request.calendar_path, &request.event_href)
.await
.map_err(|e| ApiError::Internal(format!("Failed to delete event: {}", e)))?;
Ok(Json(DeleteEventResponse {
success: true,
message: "Event deleted successfully".to_string(),
@@ -283,9 +364,11 @@ pub async fn create_event(
headers: HeaderMap,
Json(request): Json<CreateEventRequest>,
) -> Result<Json<CreateEventResponse>, ApiError> {
println!("📝 Create event request received: title='{}', all_day={}, calendar_path={:?}",
request.title, request.all_day, request.calendar_path);
println!(
"📝 Create event request received: title='{}', all_day={}, calendar_path={:?}",
request.title, request.all_day, request.calendar_path
);
// Extract and verify token
let token = extract_bearer_token(&headers)?;
let password = extract_password_header(&headers)?;
@@ -294,13 +377,17 @@ pub async fn create_event(
if request.title.trim().is_empty() {
return Err(ApiError::BadRequest("Event title is required".to_string()));
}
if request.title.len() > 200 {
return Err(ApiError::BadRequest("Event title too long (max 200 characters)".to_string()));
return Err(ApiError::BadRequest(
"Event title too long (max 200 characters)".to_string(),
));
}
// Create CalDAV config from token and password
let config = state.auth_service.caldav_config_from_token(&token, &password)?;
let config = state
.auth_service
.caldav_config_from_token(&token, &password)?;
let client = CalDAVClient::new(config);
// Determine which calendar to use
@@ -308,31 +395,41 @@ pub async fn create_event(
path
} else {
// Use the first available calendar
let calendar_paths = client.discover_calendars()
let calendar_paths = client
.discover_calendars()
.await
.map_err(|e| ApiError::Internal(format!("Failed to discover calendars: {}", e)))?;
if calendar_paths.is_empty() {
return Err(ApiError::BadRequest("No calendars available for event creation".to_string()));
return Err(ApiError::BadRequest(
"No calendars available for event creation".to_string(),
));
}
calendar_paths[0].clone()
};
// Parse dates and times
let start_datetime = parse_event_datetime(&request.start_date, &request.start_time, request.all_day)
.map_err(|e| ApiError::BadRequest(format!("Invalid start date/time: {}", e)))?;
let start_datetime =
parse_event_datetime(&request.start_date, &request.start_time, request.all_day)
.map_err(|e| ApiError::BadRequest(format!("Invalid start date/time: {}", e)))?;
let end_datetime = parse_event_datetime(&request.end_date, &request.end_time, request.all_day)
.map_err(|e| ApiError::BadRequest(format!("Invalid end date/time: {}", e)))?;
// Validate that end is after start
if end_datetime <= start_datetime {
return Err(ApiError::BadRequest("End date/time must be after start date/time".to_string()));
return Err(ApiError::BadRequest(
"End date/time must be after start date/time".to_string(),
));
}
// Generate a unique UID for the event
let uid = format!("{}-{}", uuid::Uuid::new_v4(), chrono::Utc::now().timestamp());
let uid = format!(
"{}-{}",
uuid::Uuid::new_v4(),
chrono::Utc::now().timestamp()
);
// Parse status
let status = match request.status.to_lowercase().as_str() {
@@ -352,7 +449,8 @@ pub async fn create_event(
let attendees: Vec<String> = if request.attendees.trim().is_empty() {
Vec::new()
} else {
request.attendees
request
.attendees
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
@@ -363,7 +461,8 @@ pub async fn create_event(
let categories: Vec<String> = if request.categories.trim().is_empty() {
Vec::new()
} else {
request.categories
request
.categories
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
@@ -399,10 +498,11 @@ pub async fn create_event(
"WEEKLY" => {
// Handle weekly recurrence with optional BYDAY parameter
let mut rrule = "FREQ=WEEKLY".to_string();
// Check if specific days are selected (recurrence_days has 7 elements: [Sun, Mon, Tue, Wed, Thu, Fri, Sat])
if request.recurrence_days.len() == 7 {
let selected_days: Vec<&str> = request.recurrence_days
let selected_days: Vec<&str> = request
.recurrence_days
.iter()
.enumerate()
.filter_map(|(i, &selected)| {
@@ -416,20 +516,20 @@ pub async fn create_event(
5 => "FR", // Friday
6 => "SA", // Saturday
_ => return None,
})
} else {
None
}
})
.collect();
})
} else {
None
}
})
.collect();
if !selected_days.is_empty() {
rrule = format!("{};BYDAY={}", rrule, selected_days.join(","));
if !selected_days.is_empty() {
rrule = format!("{};BYDAY={}", rrule, selected_days.join(","));
}
}
}
Some(rrule)
},
}
"MONTHLY" => Some("FREQ=MONTHLY".to_string()),
"YEARLY" => Some("FREQ=YEARLY".to_string()),
_ => None,
@@ -439,15 +539,27 @@ pub async fn create_event(
// Create the VEvent struct (RFC 5545 compliant)
let mut event = VEvent::new(uid, start_datetime);
event.dtend = Some(end_datetime);
event.summary = if request.title.trim().is_empty() { None } else { Some(request.title.clone()) };
event.description = if request.description.trim().is_empty() { None } else { Some(request.description) };
event.location = if request.location.trim().is_empty() { None } else { Some(request.location) };
event.summary = if request.title.trim().is_empty() {
None
} else {
Some(request.title.clone())
};
event.description = if request.description.trim().is_empty() {
None
} else {
Some(request.description)
};
event.location = if request.location.trim().is_empty() {
None
} else {
Some(request.location)
};
event.status = Some(status);
event.class = Some(class);
event.priority = request.priority;
event.organizer = if request.organizer.trim().is_empty() {
None
} else {
event.organizer = if request.organizer.trim().is_empty() {
None
} else {
Some(CalendarUser {
cal_address: request.organizer,
common_name: None,
@@ -456,41 +568,53 @@ pub async fn create_event(
language: None,
})
};
event.attendees = attendees.into_iter().map(|email| Attendee {
cal_address: email,
common_name: None,
role: None,
part_stat: None,
rsvp: None,
cu_type: None,
member: Vec::new(),
delegated_to: Vec::new(),
delegated_from: Vec::new(),
sent_by: None,
dir_entry_ref: None,
language: None,
}).collect();
event.attendees = attendees
.into_iter()
.map(|email| Attendee {
cal_address: email,
common_name: None,
role: None,
part_stat: None,
rsvp: None,
cu_type: None,
member: Vec::new(),
delegated_to: Vec::new(),
delegated_from: Vec::new(),
sent_by: None,
dir_entry_ref: None,
language: None,
})
.collect();
event.categories = categories;
event.rrule = rrule;
event.all_day = request.all_day;
event.alarms = alarms.into_iter().map(|reminder| VAlarm {
action: AlarmAction::Display,
trigger: AlarmTrigger::Duration(chrono::Duration::minutes(-reminder.minutes_before as i64)),
duration: None,
repeat: None,
description: reminder.description,
summary: None,
attendees: Vec::new(),
attach: Vec::new(),
}).collect();
event.alarms = alarms
.into_iter()
.map(|reminder| VAlarm {
action: AlarmAction::Display,
trigger: AlarmTrigger::Duration(chrono::Duration::minutes(
-reminder.minutes_before as i64,
)),
duration: None,
repeat: None,
description: reminder.description,
summary: None,
attendees: Vec::new(),
attach: Vec::new(),
})
.collect();
event.calendar_path = Some(calendar_path.clone());
// Create the event on the CalDAV server
let event_href = client.create_event(&calendar_path, &event)
let event_href = client
.create_event(&calendar_path, &event)
.await
.map_err(|e| ApiError::Internal(format!("Failed to create event: {}", e)))?;
println!("✅ Event created successfully with UID: {} at href: {}", event.uid, event_href);
println!(
"✅ Event created successfully with UID: {} at href: {}",
event.uid, event_href
);
Ok(Json(CreateEventResponse {
success: true,
@@ -505,7 +629,7 @@ pub async fn update_event(
Json(request): Json<UpdateEventRequest>,
) -> Result<Json<UpdateEventResponse>, ApiError> {
// Handle update request
// Extract and verify token
let token = extract_bearer_token(&headers)?;
let password = extract_password_header(&headers)?;
@@ -514,37 +638,45 @@ pub async fn update_event(
if request.uid.trim().is_empty() {
return Err(ApiError::BadRequest("Event UID is required".to_string()));
}
if request.title.trim().is_empty() {
return Err(ApiError::BadRequest("Event title is required".to_string()));
}
if request.title.len() > 200 {
return Err(ApiError::BadRequest("Event title too long (max 200 characters)".to_string()));
return Err(ApiError::BadRequest(
"Event title too long (max 200 characters)".to_string(),
));
}
// Create CalDAV config from token and password
let config = state.auth_service.caldav_config_from_token(&token, &password)?;
let config = state
.auth_service
.caldav_config_from_token(&token, &password)?;
let client = CalDAVClient::new(config);
// Find the event across all calendars (or in the specified calendar)
let calendar_paths = if let Some(path) = &request.calendar_path {
vec![path.clone()]
} else {
client.discover_calendars()
client
.discover_calendars()
.await
.map_err(|e| ApiError::Internal(format!("Failed to discover calendars: {}", e)))?
};
let mut found_event: Option<(CalendarEvent, String, String)> = None; // (event, calendar_path, event_href)
for calendar_path in &calendar_paths {
match client.fetch_events(calendar_path).await {
Ok(events) => {
for event in events {
if event.uid == request.uid {
// Use the actual href from the event, or generate one if missing
let event_href = event.href.clone().unwrap_or_else(|| format!("{}.ics", event.uid));
let event_href = event
.href
.clone()
.unwrap_or_else(|| format!("{}.ics", event.uid));
println!("🔍 Found event {} with href: {}", event.uid, event_href);
found_event = Some((event, calendar_path.clone(), event_href));
break;
@@ -553,9 +685,12 @@ pub async fn update_event(
if found_event.is_some() {
break;
}
},
}
Err(e) => {
eprintln!("Failed to fetch events from calendar {}: {}", calendar_path, e);
eprintln!(
"Failed to fetch events from calendar {}: {}",
calendar_path, e
);
continue;
}
}
@@ -565,23 +700,38 @@ pub async fn update_event(
.ok_or_else(|| ApiError::NotFound(format!("Event with UID '{}' not found", request.uid)))?;
// Parse dates and times
let start_datetime = parse_event_datetime(&request.start_date, &request.start_time, request.all_day)
.map_err(|e| ApiError::BadRequest(format!("Invalid start date/time: {}", e)))?;
let start_datetime =
parse_event_datetime(&request.start_date, &request.start_time, request.all_day)
.map_err(|e| ApiError::BadRequest(format!("Invalid start date/time: {}", e)))?;
let end_datetime = parse_event_datetime(&request.end_date, &request.end_time, request.all_day)
.map_err(|e| ApiError::BadRequest(format!("Invalid end date/time: {}", e)))?;
// Validate that end is after start
if end_datetime <= start_datetime {
return Err(ApiError::BadRequest("End date/time must be after start date/time".to_string()));
return Err(ApiError::BadRequest(
"End date/time must be after start date/time".to_string(),
));
}
// Update event properties
event.dtstart = start_datetime;
event.dtend = Some(end_datetime);
event.summary = if request.title.trim().is_empty() { None } else { Some(request.title) };
event.description = if request.description.trim().is_empty() { None } else { Some(request.description) };
event.location = if request.location.trim().is_empty() { None } else { Some(request.location) };
event.summary = if request.title.trim().is_empty() {
None
} else {
Some(request.title)
};
event.description = if request.description.trim().is_empty() {
None
} else {
Some(request.description)
};
event.location = if request.location.trim().is_empty() {
None
} else {
Some(request.location)
};
event.all_day = request.all_day;
// Parse and update status
@@ -601,11 +751,15 @@ pub async fn update_event(
event.priority = request.priority;
// Update the event on the CalDAV server
println!("📝 Updating event {} at calendar_path: {}, event_href: {}", event.uid, calendar_path, event_href);
client.update_event(&calendar_path, &event, &event_href)
println!(
"📝 Updating event {} at calendar_path: {}, event_href: {}",
event.uid, calendar_path, event_href
);
client
.update_event(&calendar_path, &event, &event_href)
.await
.map_err(|e| ApiError::Internal(format!("Failed to update event: {}", e)))?;
println!("✅ Successfully updated event {}", event.uid);
Ok(Json(UpdateEventResponse {
@@ -614,27 +768,32 @@ pub async fn update_event(
}))
}
fn parse_event_datetime(date_str: &str, time_str: &str, all_day: bool) -> Result<chrono::DateTime<chrono::Utc>, String> {
use chrono::{NaiveDate, NaiveTime, NaiveDateTime, Utc, TimeZone};
fn parse_event_datetime(
date_str: &str,
time_str: &str,
all_day: bool,
) -> Result<chrono::DateTime<chrono::Utc>, String> {
use chrono::{NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
// Parse the date
let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
.map_err(|_| format!("Invalid date format: {}. Expected YYYY-MM-DD", date_str))?;
if all_day {
// For all-day events, use midnight UTC
let datetime = date.and_hms_opt(0, 0, 0)
let datetime = date
.and_hms_opt(0, 0, 0)
.ok_or_else(|| "Failed to create midnight datetime".to_string())?;
Ok(Utc.from_utc_datetime(&datetime))
} else {
// Parse the time
let time = NaiveTime::parse_from_str(time_str, "%H:%M")
.map_err(|_| format!("Invalid time format: {}. Expected HH:MM", time_str))?;
// Combine date and time
let datetime = NaiveDateTime::new(date, time);
// Assume local time and convert to UTC (in a real app, you'd want timezone support)
Ok(Utc.from_utc_datetime(&datetime))
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,14 +3,14 @@ use axum::{
routing::{get, post},
Router,
};
use tower_http::cors::{CorsLayer, Any};
use std::sync::Arc;
use tower_http::cors::{Any, CorsLayer};
pub mod auth;
pub mod models;
pub mod handlers;
pub mod calendar;
pub mod config;
pub mod handlers;
pub mod models;
use auth::AuthService;
@@ -22,13 +22,13 @@ pub struct AppState {
pub async fn run_server() -> Result<(), Box<dyn std::error::Error>> {
// Initialize logging
println!("🚀 Starting Calendar Backend Server");
// Create auth service
let jwt_secret = std::env::var("JWT_SECRET")
.unwrap_or_else(|_| "your-super-secret-jwt-key-change-in-production".to_string());
let auth_service = AuthService::new(jwt_secret);
let app_state = AppState { auth_service };
// Build our application with routes
@@ -46,9 +46,18 @@ pub async fn run_server() -> Result<(), Box<dyn std::error::Error>> {
.route("/api/calendar/events/delete", post(handlers::delete_event))
.route("/api/calendar/events/:uid", get(handlers::refresh_event))
// Event series-specific endpoints
.route("/api/calendar/events/series/create", post(handlers::create_event_series))
.route("/api/calendar/events/series/update", post(handlers::update_event_series))
.route("/api/calendar/events/series/delete", post(handlers::delete_event_series))
.route(
"/api/calendar/events/series/create",
post(handlers::create_event_series),
)
.route(
"/api/calendar/events/series/update",
post(handlers::update_event_series),
)
.route(
"/api/calendar/events/series/delete",
post(handlers::delete_event_series),
)
.layer(
CorsLayer::new()
.allow_origin(Any)
@@ -60,7 +69,7 @@ pub async fn run_server() -> Result<(), Box<dyn std::error::Error>> {
// Start server
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
println!("📡 Server listening on http://0.0.0.0:3000");
axum::serve(listener, app).await?;
Ok(())
@@ -76,4 +85,4 @@ async fn health_check() -> Json<serde_json::Value> {
"service": "calendar-backend",
"version": "0.1.0"
}))
}
}

View File

@@ -4,4 +4,4 @@ use calendar_backend::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
run_server().await
}
}

View File

@@ -76,21 +76,21 @@ pub struct DeleteEventResponse {
pub struct CreateEventRequest {
pub title: String,
pub description: String,
pub start_date: String, // YYYY-MM-DD format
pub start_time: String, // HH:MM format
pub end_date: String, // YYYY-MM-DD format
pub end_time: String, // HH:MM format
pub start_date: String, // YYYY-MM-DD format
pub start_time: String, // HH:MM format
pub end_date: String, // YYYY-MM-DD format
pub end_time: String, // HH:MM format
pub location: String,
pub all_day: bool,
pub status: String, // confirmed, tentative, cancelled
pub class: String, // public, private, confidential
pub priority: Option<u8>, // 0-9 priority level
pub organizer: String, // organizer email
pub attendees: String, // comma-separated attendee emails
pub categories: String, // comma-separated categories
pub reminder: String, // reminder type
pub recurrence: String, // recurrence type
pub recurrence_days: Vec<bool>, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence
pub status: String, // confirmed, tentative, cancelled
pub class: String, // public, private, confidential
pub priority: Option<u8>, // 0-9 priority level
pub organizer: String, // organizer email
pub attendees: String, // comma-separated attendee emails
pub categories: String, // comma-separated categories
pub reminder: String, // reminder type
pub recurrence: String, // recurrence type
pub recurrence_days: Vec<bool>, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence
pub calendar_path: Option<String>, // Optional - use first calendar if not specified
}
@@ -103,24 +103,24 @@ pub struct CreateEventResponse {
#[derive(Debug, Deserialize)]
pub struct UpdateEventRequest {
pub uid: String, // Event UID to identify which event to update
pub uid: String, // Event UID to identify which event to update
pub title: String,
pub description: String,
pub start_date: String, // YYYY-MM-DD format
pub start_time: String, // HH:MM format
pub end_date: String, // YYYY-MM-DD format
pub end_time: String, // HH:MM format
pub start_date: String, // YYYY-MM-DD format
pub start_time: String, // HH:MM format
pub end_date: String, // YYYY-MM-DD format
pub end_time: String, // HH:MM format
pub location: String,
pub all_day: bool,
pub status: String, // confirmed, tentative, cancelled
pub class: String, // public, private, confidential
pub priority: Option<u8>, // 0-9 priority level
pub organizer: String, // organizer email
pub attendees: String, // comma-separated attendee emails
pub categories: String, // comma-separated categories
pub reminder: String, // reminder type
pub recurrence: String, // recurrence type
pub recurrence_days: Vec<bool>, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence
pub status: String, // confirmed, tentative, cancelled
pub class: String, // public, private, confidential
pub priority: Option<u8>, // 0-9 priority level
pub organizer: String, // organizer email
pub attendees: String, // comma-separated attendee emails
pub categories: String, // comma-separated categories
pub reminder: String, // reminder type
pub recurrence: String, // recurrence type
pub recurrence_days: Vec<bool>, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence
pub calendar_path: Option<String>, // Optional - search all calendars if not specified
pub update_action: Option<String>, // "update_series" for recurring events
#[serde(skip_serializing_if = "Option::is_none")]
@@ -139,22 +139,22 @@ pub struct UpdateEventResponse {
pub struct CreateEventSeriesRequest {
pub title: String,
pub description: String,
pub start_date: String, // YYYY-MM-DD format
pub start_time: String, // HH:MM format
pub end_date: String, // YYYY-MM-DD format
pub end_time: String, // HH:MM format
pub start_date: String, // YYYY-MM-DD format
pub start_time: String, // HH:MM format
pub end_date: String, // YYYY-MM-DD format
pub end_time: String, // HH:MM format
pub location: String,
pub all_day: bool,
pub status: String, // confirmed, tentative, cancelled
pub class: String, // public, private, confidential
pub priority: Option<u8>, // 0-9 priority level
pub organizer: String, // organizer email
pub attendees: String, // comma-separated attendee emails
pub categories: String, // comma-separated categories
pub reminder: String, // reminder type
pub status: String, // confirmed, tentative, cancelled
pub class: String, // public, private, confidential
pub priority: Option<u8>, // 0-9 priority level
pub organizer: String, // organizer email
pub attendees: String, // comma-separated attendee emails
pub categories: String, // comma-separated categories
pub reminder: String, // reminder type
// Series-specific fields
pub recurrence: String, // recurrence type (daily, weekly, monthly, yearly)
pub recurrence: String, // recurrence type (daily, weekly, monthly, yearly)
pub recurrence_days: Vec<bool>, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence
pub recurrence_interval: Option<u32>, // Every N days/weeks/months/years
pub recurrence_end_date: Option<String>, // When the series ends (YYYY-MM-DD)
@@ -173,33 +173,33 @@ pub struct CreateEventSeriesResponse {
#[derive(Debug, Deserialize)]
pub struct UpdateEventSeriesRequest {
pub series_uid: String, // Series UID to identify which series to update
pub series_uid: String, // Series UID to identify which series to update
pub title: String,
pub description: String,
pub start_date: String, // YYYY-MM-DD format
pub start_time: String, // HH:MM format
pub end_date: String, // YYYY-MM-DD format
pub end_time: String, // HH:MM format
pub start_date: String, // YYYY-MM-DD format
pub start_time: String, // HH:MM format
pub end_date: String, // YYYY-MM-DD format
pub end_time: String, // HH:MM format
pub location: String,
pub all_day: bool,
pub status: String, // confirmed, tentative, cancelled
pub class: String, // public, private, confidential
pub priority: Option<u8>, // 0-9 priority level
pub organizer: String, // organizer email
pub attendees: String, // comma-separated attendee emails
pub categories: String, // comma-separated categories
pub reminder: String, // reminder type
pub status: String, // confirmed, tentative, cancelled
pub class: String, // public, private, confidential
pub priority: Option<u8>, // 0-9 priority level
pub organizer: String, // organizer email
pub attendees: String, // comma-separated attendee emails
pub categories: String, // comma-separated categories
pub reminder: String, // reminder type
// Series-specific fields
pub recurrence: String, // recurrence type (daily, weekly, monthly, yearly)
pub recurrence: String, // recurrence type (daily, weekly, monthly, yearly)
pub recurrence_days: Vec<bool>, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence
pub recurrence_interval: Option<u32>, // Every N days/weeks/months/years
pub recurrence_end_date: Option<String>, // When the series ends (YYYY-MM-DD)
pub recurrence_count: Option<u32>, // Number of occurrences
pub calendar_path: Option<String>, // Optional - search all calendars if not specified
// Update scope control
pub update_scope: String, // "this_only", "this_and_future", "all_in_series"
pub update_scope: String, // "this_only", "this_and_future", "all_in_series"
pub occurrence_date: Option<String>, // ISO date string for specific occurrence being updated
pub changed_fields: Option<Vec<String>>, // List of field names that were changed (for optimization)
}
@@ -214,12 +214,12 @@ pub struct UpdateEventSeriesResponse {
#[derive(Debug, Deserialize)]
pub struct DeleteEventSeriesRequest {
pub series_uid: String, // Series UID to identify which series to delete
pub series_uid: String, // Series UID to identify which series to delete
pub calendar_path: String,
pub event_href: String,
// Delete scope control
pub delete_scope: String, // "this_only", "this_and_future", "all_in_series"
pub delete_scope: String, // "this_only", "this_and_future", "all_in_series"
pub occurrence_date: Option<String>, // ISO date string for specific occurrence being deleted
}
@@ -274,4 +274,4 @@ impl std::fmt::Display for ApiError {
}
}
impl std::error::Error for ApiError {}
impl std::error::Error for ApiError {}

View File

@@ -1,26 +1,26 @@
use calendar_backend::AppState;
use calendar_backend::auth::AuthService;
use reqwest::Client;
use serde_json::json;
use std::time::Duration;
use tokio::time::sleep;
use axum::{
response::Json,
routing::{get, post},
Router,
};
use tower_http::cors::{CorsLayer, Any};
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
@@ -33,19 +33,55 @@ mod test_utils {
.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))
.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))
.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)
@@ -58,39 +94,47 @@ mod test_utils {
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
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());
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()
login_response["token"]
.as_str()
.expect("Login response should contain token")
.to_string()
}
}
async fn root() -> &'static str {
"Calendar Backend API v0.1.0"
}
@@ -106,26 +150,27 @@ mod test_utils {
#[cfg(test)]
mod tests {
use super::*;
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
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");
}
@@ -133,31 +178,42 @@ mod tests {
#[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 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
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());
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");
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");
}
@@ -165,52 +221,57 @@ mod tests {
#[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
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]
#[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
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());
println!(
"⚠ User info test skipped (CalDAV server issues): {}",
response.status()
);
}
}
@@ -218,48 +279,59 @@ mod tests {
#[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
// 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))
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());
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());
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();
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_date": "2024-12-25",
"end_time": "11:00",
"location": "Test Location",
"all_day": false,
@@ -273,8 +345,9 @@ mod tests {
"recurrence": "none",
"recurrence_days": [false, false, false, false, false, false, false]
});
let response = server.client
let response = server
.client
.post(&format!("{}/api/calendar/events/create", server.base_url))
.header("Authorization", format!("Bearer {}", token))
.header("X-CalDAV-Password", password)
@@ -282,10 +355,10 @@ mod tests {
.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();
@@ -300,47 +373,58 @@ mod tests {
#[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))
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());
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
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());
assert!(
response.status() == 401 || response.status() == 400,
"Expected 401 or 400 for invalid token, got {}",
response.status()
);
println!("✓ Invalid authentication test passed");
}
@@ -348,13 +432,14 @@ mod tests {
#[tokio::test]
async fn test_missing_auth() {
let server = TestServer::start().await;
let response = server.client
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");
}
@@ -365,20 +450,20 @@ mod tests {
#[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_date": "2024-12-25",
"end_time": "11:00",
"location": "Test Series Location",
"all_day": false,
@@ -395,19 +480,23 @@ mod tests {
"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))
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();
@@ -420,24 +509,24 @@ mod tests {
}
/// Test event series update endpoint
#[tokio::test]
#[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_date": "2024-12-26",
"end_time": "15:00",
"location": "Updated Location",
"all_day": false,
@@ -455,27 +544,36 @@ mod tests {
"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))
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");
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)");
println!(
"⚠ Update event series test skipped (event not found - expected for test data)"
);
} else {
println!("⚠ Update event series test skipped (CalDAV server not accessible)");
}
@@ -485,40 +583,46 @@ mod tests {
#[tokio::test]
async fn test_delete_event_series() {
let server = TestServer::start().await;
// First login to get a token
// 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))
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)");
println!(
"⚠ Delete event series test skipped (event not found - expected for test data)"
);
} else {
println!("⚠ Delete event series test skipped (CalDAV server not accessible)");
}
@@ -528,17 +632,17 @@ mod tests {
#[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_date": "2024-12-25",
"end_time": "11:00",
"location": "Test",
"all_day": false,
@@ -552,16 +656,24 @@ mod tests {
"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))
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");
assert_eq!(
response.status(),
400,
"Expected 400 for invalid update scope"
);
println!("✓ Invalid update scope test passed");
}
@@ -569,16 +681,16 @@ mod tests {
#[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_date": "2024-12-25",
"end_time": "11:00",
"location": "Test",
"all_day": false,
@@ -591,16 +703,24 @@ mod tests {
"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))
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");
assert_eq!(
response.status(),
400,
"Expected 400 for non-recurring event in series endpoint"
);
println!("✓ Non-recurring series rejection test passed");
}
}
}