use std::sync::Arc; use std::time::Duration; use origin_client::{Client, Config, FnLogger}; use tokio_util::sync::CancellationToken; fn test_config(url: String) -> Config { Config { url: Some(url), service_key: "test-key".to_string(), type_: "icy2".to_string(), version: "v1.0.0".to_string(), name: "icy2-test".to_string(), site: Some("Test Site".to_string()), environment: Some("test".to_string()), install_id: Some("test-install-id".to_string()), ..Default::default() } } #[tokio::test] async fn new_requires_fields() { assert!(Client::new(Config::default()).is_err()); } #[tokio::test] async fn checkin_once_success() { let mut server = mockito::Server::new_async().await; let mock = server .mock("POST", "/") .match_header("x-origin-service-key", "test-key") .with_status(200) .with_header("content-type", "application/json") .with_body( r#"{"success":true,"service_instance_id":42,"unique_install_id":"test-install-id","status":"provisioning","pinged_at":"2026-08-01T00:00:00Z"}"#, ) .create_async() .await; let client = Client::new(test_config(server.url())).unwrap(); let resp = client.checkin_once().await.unwrap(); mock.assert_async().await; assert!(resp.success); assert_eq!(resp.service_instance_id, 42); assert_eq!(resp.unique_install_id, "test-install-id"); } #[tokio::test] async fn checkin_once_server_error_returns_error() { let mut server = mockito::Server::new_async().await; server .mock("POST", "/") .with_status(500) .create_async() .await; let client = Client::new(test_config(server.url())).unwrap(); assert!(client.checkin_once().await.is_err()); } #[tokio::test] async fn checkin_once_unreachable_does_not_panic() { let client = Client::new(test_config("http://127.0.0.1:1".to_string())).unwrap(); let result = client.checkin_once().await; assert!(result.is_err()); } #[tokio::test] async fn checkin_once_logs_on_failure() { let mut server = mockito::Server::new_async().await; server .mock("POST", "/") .with_status(500) .create_async() .await; let warned = Arc::new(std::sync::atomic::AtomicBool::new(false)); let warned_clone = warned.clone(); let mut cfg = test_config(server.url()); cfg.logger = Arc::new(FnLogger( move |_msg: &str, _err: Option<&(dyn std::error::Error + Send + Sync)>| { warned_clone.store(true, std::sync::atomic::Ordering::SeqCst); }, )); let client = Client::new(cfg).unwrap(); let _ = client.checkin_once().await; assert!(warned.load(std::sync::atomic::Ordering::SeqCst)); } #[tokio::test] async fn run_checks_in_immediately_then_stops_on_cancel() { let mut server = mockito::Server::new_async().await; let mock = server .mock("POST", "/") .with_status(200) .with_header("content-type", "application/json") .with_body( r#"{"success":true,"service_instance_id":1,"unique_install_id":"x","status":"provisioning","pinged_at":"2026-08-01T00:00:00Z"}"#, ) .expect(1) .create_async() .await; let client = Client::new(test_config(server.url())).unwrap(); let cancel = CancellationToken::new(); let cancel_clone = cancel.clone(); let handle = tokio::spawn(async move { client.run(cancel_clone).await; }); // The immediate check-in happens synchronously at the start of run(); // give the spawned task a moment to execute it, then cancel before the // (24h) interval could ever fire again. tokio::time::sleep(Duration::from_millis(50)).await; cancel.cancel(); handle.await.unwrap(); mock.assert_async().await; }