church-core/tests/unit_tests.rs
RTSDA 4d6b23beb3
Some checks are pending
iOS UniFFI Build / build-ios (push) Waiting to run
Initial commit: Church Core Rust library
Add church management API library with cross-platform support for iOS, Android, and WASM.
Features include event management, bulletin handling, contact forms, and authentication.
2025-08-16 19:25:01 -04:00

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);
}
}