Files
el/ui/vessels/el-publish/src/tests.rs
T

265 lines
11 KiB
Rust

//! 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()));
}
}