//! Tests for el-publish. #[cfg(test)] mod tests { use std::time::{SystemTime, UNIX_EPOCH}; use crate::{ cert::{CertInfo, CertStore}, config::{AppleConfig, GoogleConfig, PublishConfig, RolloutConfig}, metadata::{LocaleMetadata, StoreMetadata}, rollout::{RolloutDecision, RolloutMonitor, RolloutState}, screenshot::{ScreenshotCapture, ScreenshotTarget, StubScreenshotCapture}, PublishError, }; fn unix_now() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) .map(|d| d.as_secs()) .unwrap_or(0) } fn test_config() -> PublishConfig { PublishConfig::new("1.2.0", 42) .with_apple(AppleConfig::new( "will@example.com", "TEAM123", "ai.example.myapp", )) .with_google(GoogleConfig::new("./secrets/google.json", "ai.example.myapp")) } // ── Test 1: PublishConfig parses version and build number ───────────────── #[test] fn test_publish_config_basic() { let cfg = test_config(); assert_eq!(cfg.version, "1.2.0"); assert_eq!(cfg.build_number, 42); assert!(cfg.apple.is_some()); assert!(cfg.google.is_some()); } // ── Test 2: AppleConfig has correct fields ──────────────────────────────── #[test] fn test_apple_config() { let cfg = AppleConfig::new("will@example.com", "TEAM123", "ai.example.myapp") .with_category("utilities"); assert_eq!(cfg.account, "will@example.com"); assert_eq!(cfg.team_id, "TEAM123"); assert_eq!(cfg.category, "utilities"); } // ── Test 3: GoogleConfig track defaults to internal ─────────────────────── #[test] fn test_google_config_default_track() { let cfg = GoogleConfig::new("./google.json", "ai.example.myapp"); assert_eq!(cfg.track, "internal"); } // ── Test 4: GoogleConfig with_track ────────────────────────────────────── #[test] fn test_google_config_track() { let cfg = GoogleConfig::new("./google.json", "ai.example.myapp") .with_track("production"); assert_eq!(cfg.track, "production"); } // ── Test 5: RolloutConfig default values ───────────────────────────────── #[test] fn test_rollout_config_default() { let cfg = RolloutConfig::default(); assert_eq!(cfg.initial_percent, 100); assert!(!cfg.auto_advance); assert_eq!(cfg.max_crash_rate, 0.01); } // ── Test 6: RolloutMonitor evaluates normal state as Maintain ───────────── #[test] fn test_rollout_maintain() { let cfg = RolloutConfig::new(10).with_auto_advance(24); let monitor = RolloutMonitor::new(cfg); let state = RolloutState { track: "production".into(), current_percent: 10, crash_rate: 0.001, // well below threshold is_halted: false, hours_elapsed: 5, // less than 24h advance threshold }; let decision = monitor.evaluate(&state); assert!(matches!(decision, RolloutDecision::Maintain)); } // ── Test 7: RolloutMonitor halts on high crash rate ─────────────────────── #[test] fn test_rollout_halt_on_crash_rate() { let cfg = RolloutConfig::new(10); let monitor = RolloutMonitor::new(cfg); let state = RolloutState { track: "production".into(), current_percent: 10, crash_rate: 0.05, // 5% > 1% threshold is_halted: false, hours_elapsed: 2, }; let decision = monitor.evaluate(&state); assert!(matches!(decision, RolloutDecision::Halt { .. })); } // ── Test 8: RolloutMonitor advances after threshold hours ───────────────── #[test] fn test_rollout_advance() { let cfg = RolloutConfig::new(10).with_auto_advance(24); let monitor = RolloutMonitor::new(cfg); let state = RolloutState { track: "production".into(), current_percent: 10, crash_rate: 0.001, is_halted: false, hours_elapsed: 25, // exceeds 24h threshold }; let decision = monitor.evaluate(&state); assert!(matches!(decision, RolloutDecision::Advance { to_percent: 25 })); } // ── Test 9: RolloutMonitor advance progression 10→25→50→100 ────────────── #[test] fn test_rollout_advance_progression() { let cfg = RolloutConfig::new(10).with_auto_advance(1); let monitor = RolloutMonitor::new(cfg); let mut state = RolloutState { track: "production".into(), current_percent: 10, crash_rate: 0.001, is_halted: false, hours_elapsed: 5, }; let expected = [25u8, 50, 100]; for expected_next in expected { let decision = monitor.evaluate(&state); if let RolloutDecision::Advance { to_percent } = decision { state.current_percent = to_percent; state.hours_elapsed = 5; assert_eq!(to_percent, expected_next); } else { panic!("expected Advance decision, got {:?}", decision); } } } // ── Test 10: CertInfo::days_until_expiry ───────────────────────────────── #[test] fn test_cert_days_until_expiry() { let future = unix_now() + 30 * 86400; // 30 days from now let cert = CertInfo::new("My Cert", "TEAM123", "SN001", "Distribution", future); let days = cert.days_until_expiry(); assert!(days >= 29 && days <= 30, "should be ~30 days: {}", days); } // ── Test 11: CertInfo::is_expired for past cert ─────────────────────────── #[test] fn test_cert_is_expired() { let past = unix_now() - 86400; // yesterday let cert = CertInfo::new("Old Cert", "TEAM123", "SN002", "Distribution", past); assert!(cert.is_expired()); assert_eq!(cert.days_until_expiry(), 0); } // ── Test 12: CertStore::renewal_warnings finds expiring certs ───────────── #[test] fn test_cert_store_renewal_warnings() { let mut store = CertStore::new().with_warn_days(45); let soon = unix_now() + 10 * 86400; // 10 days away store.add(CertInfo::new("My Cert", "T1", "SN1", "Distribution", soon)); let warnings = store.renewal_warnings(); assert!(!warnings.is_empty(), "should have warnings for cert expiring in 10 days"); assert!(warnings[0].contains("EXPIRING SOON") || warnings[0].contains("EXPIRED")); } // ── Test 13: CertStore::validate_for_distribution fails without cert ─────── #[test] fn test_cert_store_validate_no_cert() { let store = CertStore::new(); let result = store.validate_for_distribution("TEAM123"); assert!(result.is_err()); match result { Err(PublishError::Certificate(_)) => {} _ => panic!("expected Certificate error"), } } // ── Test 14: StoreMetadata::for_locale fallback to en-US ────────────────── #[test] fn test_store_metadata_locale_fallback() { let meta = StoreMetadata::new() .add_locale( "en-US", LocaleMetadata::new("My App", "The best app ever"), ); // de-DE not set, should fall back to en-US let locale_meta = meta.for_locale("de-DE").unwrap(); assert_eq!(locale_meta.title, "My App"); } // ── Test 15: StoreMetadata::load_from_dir with missing dir is ok ────────── #[test] fn test_store_metadata_missing_dir() { // Missing directory should not error — returns empty metadata let result = StoreMetadata::load_from_dir("/nonexistent/path/that/doesnt/exist"); assert!(result.is_ok()); } // ── Test 16: ScreenshotTarget::all_standard has expected targets ────────── #[test] fn test_screenshot_targets() { let targets = ScreenshotTarget::all_standard(); assert!(targets.len() >= 4, "should have at least 4 standard targets"); let ids: Vec<&str> = targets.iter().map(|t| t.id.as_str()).collect(); assert!(ids.contains(&"iphone-6.7")); assert!(ids.contains(&"android-phone")); } // ── Test 17: ScreenshotTarget::find by ID ──────────────────────────────── #[test] fn test_screenshot_target_find() { let target = ScreenshotTarget::find("ipad-13").unwrap(); assert_eq!(target.name, "iPad 13\""); assert_eq!(target.width, 2064); } // ── Test 18: StubScreenshotCapture produces paths ───────────────────────── #[test] fn test_stub_screenshot_capture() { let capture = StubScreenshotCapture; let target = ScreenshotTarget::find("iphone-6.7").unwrap(); let shot = capture.capture("home_screen", &target, "en-US", "/tmp/screenshots").unwrap(); assert!(shot.path.contains("iphone-6.7")); assert!(shot.path.contains("home_screen")); assert_eq!(shot.locale, "en-US"); } // ── Test 19: PublishConfig with rollout ─────────────────────────────────── #[test] fn test_publish_config_with_rollout() { let rollout = RolloutConfig::new(10) .with_auto_advance(24) .with_max_crash_rate(0.005); let cfg = PublishConfig::new("1.0.0", 1).with_rollout(rollout); let r = cfg.rollout.unwrap(); assert_eq!(r.initial_percent, 10); assert!(r.auto_advance); assert_eq!(r.max_crash_rate, 0.005); } // ── Test 20: PublishConfig screenshot_targets ───────────────────────────── #[test] fn test_publish_config_screenshot_targets() { let cfg = PublishConfig::new("1.0.0", 1) .with_screenshot_targets(vec!["iphone-6.7", "android-phone"]); assert_eq!(cfg.screenshot_targets.len(), 2); assert!(cfg.screenshot_targets.contains(&"iphone-6.7".to_string())); } }