
Some checks are pending
iOS UniFFI Build / build-ios (push) Waiting to run
Add church management API library with cross-platform support for iOS, Android, and WASM. Features include event management, bulletin handling, contact forms, and authentication.
489 lines
17 KiB
Rust
489 lines
17 KiB
Rust
use church_core::{
|
|
models::*,
|
|
cache::MemoryCache,
|
|
ChurchCoreConfig,
|
|
ChurchApiClient,
|
|
};
|
|
use chrono::{DateTime, Utc, NaiveDate};
|
|
use std::time::Duration;
|
|
use tokio_test;
|
|
|
|
#[cfg(test)]
|
|
mod model_tests {
|
|
use super::*;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
#[test]
|
|
fn test_event_creation() {
|
|
let event = Event {
|
|
id: "test-id".to_string(),
|
|
title: "Bible Study".to_string(),
|
|
description: "Weekly study".to_string(),
|
|
start_time: Utc::now(),
|
|
end_time: Utc::now() + chrono::Duration::hours(2),
|
|
location: "Fellowship Hall".to_string(),
|
|
location_url: Some("https://maps.google.com".to_string()),
|
|
image: None,
|
|
thumbnail: None,
|
|
category: EventCategory::Education,
|
|
is_featured: true,
|
|
recurring_type: Some(RecurringType::Weekly),
|
|
tags: Some(vec!["bible".to_string(), "study".to_string()]),
|
|
contact_email: Some("contact@church.org".to_string()),
|
|
contact_phone: None,
|
|
registration_url: None,
|
|
max_attendees: Some(30),
|
|
current_attendees: Some(15),
|
|
created_at: Utc::now(),
|
|
updated_at: Utc::now(),
|
|
};
|
|
|
|
assert_eq!(event.title, "Bible Study");
|
|
assert_eq!(event.category, EventCategory::Education);
|
|
assert!(event.is_featured);
|
|
assert_eq!(event.spots_remaining(), Some(15));
|
|
assert!(!event.is_full());
|
|
}
|
|
|
|
#[test]
|
|
fn test_event_time_methods() {
|
|
let now = Utc::now();
|
|
let past_time = now - chrono::Duration::hours(1);
|
|
let future_time = now + chrono::Duration::hours(1);
|
|
|
|
// Test upcoming event
|
|
let upcoming_event = Event {
|
|
id: "upcoming".to_string(),
|
|
title: "Future Event".to_string(),
|
|
description: "".to_string(),
|
|
start_time: future_time,
|
|
end_time: future_time + chrono::Duration::hours(1),
|
|
location: "".to_string(),
|
|
location_url: None,
|
|
image: None,
|
|
thumbnail: None,
|
|
category: EventCategory::Service,
|
|
is_featured: false,
|
|
recurring_type: None,
|
|
tags: None,
|
|
contact_email: None,
|
|
contact_phone: None,
|
|
registration_url: None,
|
|
max_attendees: None,
|
|
current_attendees: None,
|
|
created_at: now,
|
|
updated_at: now,
|
|
};
|
|
|
|
// Test duration calculation
|
|
assert_eq!(upcoming_event.duration_minutes(), 60);
|
|
|
|
// Test past event
|
|
let past_event = Event {
|
|
id: "past".to_string(),
|
|
title: "Past Event".to_string(),
|
|
description: "".to_string(),
|
|
start_time: past_time - chrono::Duration::hours(1),
|
|
end_time: past_time,
|
|
location: "".to_string(),
|
|
location_url: None,
|
|
image: None,
|
|
thumbnail: None,
|
|
category: EventCategory::Service,
|
|
is_featured: false,
|
|
recurring_type: None,
|
|
tags: None,
|
|
contact_email: None,
|
|
contact_phone: None,
|
|
registration_url: None,
|
|
max_attendees: None,
|
|
current_attendees: None,
|
|
created_at: past_time,
|
|
updated_at: past_time,
|
|
};
|
|
|
|
// Test duration calculation for past event
|
|
assert_eq!(past_event.duration_minutes(), 60);
|
|
}
|
|
|
|
#[test]
|
|
fn test_bulletin_creation() {
|
|
let bulletin = Bulletin {
|
|
id: "bulletin-1".to_string(),
|
|
title: "Weekly Bulletin".to_string(),
|
|
date: NaiveDate::from_ymd_opt(2024, 1, 6).unwrap(),
|
|
sabbath_school: "9:30 AM".to_string(),
|
|
divine_worship: "11:00 AM".to_string(),
|
|
scripture_reading: "John 3:16".to_string(),
|
|
sunset: "5:45 PM".to_string(),
|
|
pdf_path: Some("/bulletins/2024-01-06.pdf".to_string()),
|
|
cover_image: Some("/images/cover.jpg".to_string()),
|
|
is_active: true,
|
|
announcements: Some(vec![
|
|
Announcement {
|
|
id: Some("ann-1".to_string()),
|
|
title: "Potluck Dinner".to_string(),
|
|
content: "Join us for fellowship".to_string(),
|
|
category: Some(AnnouncementCategory::Social),
|
|
is_urgent: false,
|
|
contact_info: None,
|
|
expires_at: Some(Utc::now() + chrono::Duration::days(7)),
|
|
}
|
|
]),
|
|
hymns: Some(vec![
|
|
BulletinHymn {
|
|
number: 15,
|
|
title: "Amazing Grace".to_string(),
|
|
category: Some(HymnCategory::Opening),
|
|
verses: Some(vec![1, 2, 4]),
|
|
}
|
|
]),
|
|
special_music: Some("Choir Special".to_string()),
|
|
offering_type: Some("Local Church Budget".to_string()),
|
|
sermon_title: Some("Walking in Faith".to_string()),
|
|
speaker: Some("Pastor Smith".to_string()),
|
|
liturgy: None,
|
|
created_at: Utc::now(),
|
|
updated_at: Utc::now(),
|
|
};
|
|
|
|
assert_eq!(bulletin.title, "Weekly Bulletin");
|
|
assert!(bulletin.has_pdf());
|
|
assert!(bulletin.has_cover_image());
|
|
assert_eq!(bulletin.active_announcements().len(), 1);
|
|
assert_eq!(bulletin.urgent_announcements().len(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_contact_form_creation() {
|
|
let contact = ContactForm::new(
|
|
"John Doe".to_string(),
|
|
"john@example.com".to_string(),
|
|
"Prayer Request".to_string(),
|
|
"Please pray for my family.".to_string(),
|
|
)
|
|
.with_category(ContactCategory::PrayerRequest)
|
|
.with_phone("555-123-4567".to_string())
|
|
.mark_urgent();
|
|
|
|
assert_eq!(contact.name, "John Doe");
|
|
assert_eq!(contact.email, "john@example.com");
|
|
assert!(contact.is_urgent());
|
|
assert!(!contact.is_visitor());
|
|
assert_eq!(contact.category, Some(ContactCategory::PrayerRequest));
|
|
}
|
|
|
|
#[test]
|
|
fn test_sermon_search() {
|
|
let sermon = Sermon {
|
|
id: "sermon-1".to_string(),
|
|
title: "The Power of Prayer".to_string(),
|
|
speaker: "Pastor Johnson".to_string(),
|
|
description: "Learning to pray effectively".to_string(),
|
|
date: Utc::now() - chrono::Duration::days(7),
|
|
scripture_reference: "Matthew 6:9-13".to_string(),
|
|
series: Some("Prayer Series".to_string()),
|
|
duration_seconds: Some(2400), // 40 minutes
|
|
media_url: Some("https://media.church.org/sermon1.mp4".to_string()),
|
|
audio_url: Some("https://media.church.org/sermon1.mp3".to_string()),
|
|
video_url: Some("https://video.church.org/sermon1.mp4".to_string()),
|
|
transcript: Some("Today we explore...".to_string()),
|
|
thumbnail: Some("https://media.church.org/thumb1.jpg".to_string()),
|
|
tags: Some(vec!["prayer".to_string(), "spiritual".to_string()]),
|
|
category: SermonCategory::Regular,
|
|
is_featured: true,
|
|
view_count: 150,
|
|
download_count: 45,
|
|
created_at: Utc::now() - chrono::Duration::days(8),
|
|
updated_at: Utc::now() - chrono::Duration::days(7),
|
|
};
|
|
|
|
assert_eq!(sermon.duration_formatted(), "40m");
|
|
assert!(sermon.has_media());
|
|
assert!(sermon.has_audio());
|
|
assert!(sermon.has_video());
|
|
assert!(sermon.has_transcript());
|
|
assert!(sermon.is_popular());
|
|
|
|
// Test search matching
|
|
let search = SermonSearch::new()
|
|
.with_query("prayer".to_string())
|
|
.with_speaker("Johnson".to_string())
|
|
.featured_only();
|
|
|
|
assert!(sermon.matches_search(&search));
|
|
|
|
let non_matching_search = SermonSearch::new()
|
|
.with_query("baptism".to_string());
|
|
|
|
assert!(!sermon.matches_search(&non_matching_search));
|
|
}
|
|
|
|
#[test]
|
|
fn test_auth_token() {
|
|
let token = AuthToken {
|
|
token: "abc123".to_string(),
|
|
token_type: "Bearer".to_string(),
|
|
expires_at: Utc::now() + chrono::Duration::hours(1),
|
|
user_id: Some("user-1".to_string()),
|
|
user_name: Some("John Doe".to_string()),
|
|
user_email: Some("john@church.org".to_string()),
|
|
permissions: vec!["events.read".to_string(), "bulletins.read".to_string()],
|
|
};
|
|
|
|
assert!(token.is_valid());
|
|
assert!(!token.is_expired());
|
|
assert!(token.has_permission("events.read"));
|
|
assert!(!token.has_permission("admin.write"));
|
|
assert!(token.has_any_permission(&["events.read", "admin.write"]));
|
|
assert!(!token.has_all_permissions(&["events.read", "admin.write"]));
|
|
}
|
|
|
|
#[test]
|
|
fn test_user_roles() {
|
|
let admin_user = AuthUser {
|
|
id: "admin-1".to_string(),
|
|
email: "admin@church.org".to_string(),
|
|
name: "Admin User".to_string(),
|
|
username: Some("admin".to_string()),
|
|
avatar: None,
|
|
verified: true,
|
|
role: UserRole::Admin,
|
|
permissions: vec!["admin.*".to_string()],
|
|
created_at: Utc::now(),
|
|
updated_at: Utc::now(),
|
|
last_login: Some(Utc::now()),
|
|
};
|
|
|
|
assert!(admin_user.is_admin());
|
|
assert!(admin_user.is_leadership());
|
|
assert!(admin_user.can_edit_content());
|
|
assert!(admin_user.can_moderate());
|
|
assert_eq!(admin_user.display_name(), "Admin User");
|
|
|
|
let member_user = AuthUser {
|
|
id: "member-1".to_string(),
|
|
email: "member@church.org".to_string(),
|
|
name: "".to_string(),
|
|
username: Some("member123".to_string()),
|
|
avatar: None,
|
|
verified: true,
|
|
role: UserRole::Member,
|
|
permissions: vec!["events.read".to_string()],
|
|
created_at: Utc::now(),
|
|
updated_at: Utc::now(),
|
|
last_login: Some(Utc::now()),
|
|
};
|
|
|
|
assert!(!member_user.is_admin());
|
|
assert!(!member_user.is_leadership());
|
|
assert!(!member_user.can_edit_content());
|
|
assert!(!member_user.can_moderate());
|
|
assert!(member_user.is_member());
|
|
assert_eq!(member_user.display_name(), "member123");
|
|
}
|
|
|
|
#[test]
|
|
fn test_api_response_models() {
|
|
let response: ApiResponse<String> = ApiResponse {
|
|
success: true,
|
|
data: Some("test data".to_string()),
|
|
message: Some("Success".to_string()),
|
|
error: None,
|
|
};
|
|
|
|
assert!(response.success);
|
|
assert_eq!(response.data, Some("test data".to_string()));
|
|
|
|
let error_response: ApiResponse<String> = ApiResponse {
|
|
success: false,
|
|
data: None,
|
|
message: None,
|
|
error: Some("Not found".to_string()),
|
|
};
|
|
|
|
assert!(!error_response.success);
|
|
assert_eq!(error_response.data, None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_pagination_params() {
|
|
let params = PaginationParams::new()
|
|
.with_page(2)
|
|
.with_per_page(25)
|
|
.with_sort("created_at".to_string())
|
|
.with_filter("active=true".to_string());
|
|
|
|
assert_eq!(params.page, Some(2));
|
|
assert_eq!(params.per_page, Some(25));
|
|
assert_eq!(params.sort, Some("created_at".to_string()));
|
|
assert_eq!(params.filter, Some("active=true".to_string()));
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod cache_tests {
|
|
use super::*;
|
|
use tokio_test;
|
|
|
|
#[tokio::test]
|
|
async fn test_memory_cache_basic_operations() {
|
|
let cache = MemoryCache::new(100);
|
|
let ttl = Duration::from_secs(60);
|
|
|
|
// Test set and get
|
|
cache.set("key1", &"value1", ttl).await;
|
|
let result: Option<String> = cache.get("key1").await;
|
|
assert_eq!(result, Some("value1".to_string()));
|
|
|
|
// Test get non-existent key
|
|
let result: Option<String> = cache.get("nonexistent").await;
|
|
assert_eq!(result, None);
|
|
|
|
// Test remove
|
|
cache.remove("key1").await;
|
|
let result: Option<String> = cache.get("key1").await;
|
|
assert_eq!(result, None);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_memory_cache_ttl_expiration() {
|
|
let cache = MemoryCache::new(100);
|
|
let short_ttl = Duration::from_millis(50);
|
|
|
|
// Set value with short TTL
|
|
cache.set("expire_key", &"expire_value", short_ttl).await;
|
|
|
|
// Should be available immediately
|
|
let result: Option<String> = cache.get("expire_key").await;
|
|
assert_eq!(result, Some("expire_value".to_string()));
|
|
|
|
// Wait for expiration
|
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
|
|
// Should be expired
|
|
let result: Option<String> = cache.get("expire_key").await;
|
|
assert_eq!(result, None);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_memory_cache_clear() {
|
|
let cache = MemoryCache::new(100);
|
|
let ttl = Duration::from_secs(60);
|
|
|
|
// Set multiple values
|
|
cache.set("key1", &"value1", ttl).await;
|
|
cache.set("key2", &"value2", ttl).await;
|
|
cache.set("key3", &"value3", ttl).await;
|
|
|
|
assert_eq!(cache.len().await, 3);
|
|
|
|
// Clear cache
|
|
cache.clear().await;
|
|
assert_eq!(cache.len().await, 0);
|
|
|
|
// Verify values are gone
|
|
let result: Option<String> = cache.get("key1").await;
|
|
assert_eq!(result, None);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_memory_cache_invalidate_prefix() {
|
|
let cache = MemoryCache::new(100);
|
|
let ttl = Duration::from_secs(60);
|
|
|
|
// Set values with different prefixes
|
|
cache.set("user:1", &"user1", ttl).await;
|
|
cache.set("user:2", &"user2", ttl).await;
|
|
cache.set("event:1", &"event1", ttl).await;
|
|
cache.set("event:2", &"event2", ttl).await;
|
|
|
|
assert_eq!(cache.len().await, 4);
|
|
|
|
// Invalidate user prefix
|
|
cache.invalidate_prefix("user:").await;
|
|
assert_eq!(cache.len().await, 2);
|
|
|
|
// Verify user keys are gone but event keys remain
|
|
let result: Option<String> = cache.get("user:1").await;
|
|
assert_eq!(result, None);
|
|
|
|
let result: Option<String> = cache.get("event:1").await;
|
|
assert_eq!(result, Some("event1".to_string()));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_memory_cache_complex_types() {
|
|
let cache = MemoryCache::new(100);
|
|
let ttl = Duration::from_secs(60);
|
|
|
|
let event = Event {
|
|
id: "test-event".to_string(),
|
|
title: "Test Event".to_string(),
|
|
description: "A test event".to_string(),
|
|
start_time: Utc::now(),
|
|
end_time: Utc::now() + chrono::Duration::hours(1),
|
|
location: "Test Location".to_string(),
|
|
location_url: None,
|
|
image: None,
|
|
thumbnail: None,
|
|
category: EventCategory::Service,
|
|
is_featured: false,
|
|
recurring_type: None,
|
|
tags: None,
|
|
contact_email: None,
|
|
contact_phone: None,
|
|
registration_url: None,
|
|
max_attendees: None,
|
|
current_attendees: None,
|
|
created_at: Utc::now(),
|
|
updated_at: Utc::now(),
|
|
};
|
|
|
|
// Cache complex object
|
|
cache.set("event:test", &event, ttl).await;
|
|
|
|
// Retrieve and verify
|
|
let result: Option<Event> = cache.get("event:test").await;
|
|
assert!(result.is_some());
|
|
let cached_event = result.unwrap();
|
|
assert_eq!(cached_event.id, "test-event");
|
|
assert_eq!(cached_event.title, "Test Event");
|
|
assert_eq!(cached_event.category, EventCategory::Service);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod config_tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_church_core_config() {
|
|
let config = ChurchCoreConfig::new()
|
|
.with_base_url("https://api.test.church")
|
|
.with_cache_ttl(Duration::from_secs(600))
|
|
.with_timeout(Duration::from_secs(15))
|
|
.with_retry_attempts(5)
|
|
.with_offline_mode(false)
|
|
.with_max_cache_size(2000);
|
|
|
|
assert_eq!(config.api_base_url, "https://api.test.church");
|
|
assert_eq!(config.cache_ttl, Duration::from_secs(600));
|
|
assert_eq!(config.timeout, Duration::from_secs(15));
|
|
assert_eq!(config.retry_attempts, 5);
|
|
assert!(!config.enable_offline_mode);
|
|
assert_eq!(config.max_cache_size, 2000);
|
|
}
|
|
|
|
#[test]
|
|
fn test_default_config() {
|
|
let config = ChurchCoreConfig::default();
|
|
|
|
assert_eq!(config.api_base_url, "https://api.rockvilletollandsda.church/api");
|
|
assert_eq!(config.cache_ttl, Duration::from_secs(300));
|
|
assert_eq!(config.timeout, Duration::from_secs(10));
|
|
assert_eq!(config.retry_attempts, 3);
|
|
assert!(config.enable_offline_mode);
|
|
assert_eq!(config.max_cache_size, 1000);
|
|
}
|
|
} |