#[path = "test_support/state_guard.rs"]
mod state_guard;
use std::time::Duration;
use aion_server::api::http::http_router;
use aion_server::config::ServerConfig;
use aion_server::update_check::{UpdateCheckInstall, install_embedded_update_check_for_server};
use aion_server::{ServerState, update_check::UPDATE_CHECK_WORKFLOW_TYPE};
use aion_store::InMemoryStore;
use axum::{
body,
http::{Request, StatusCode},
};
use serde_json::{Value, json};
use tower::ServiceExt;
use state_guard::StateUnderTest;
type TestError = Box<dyn std::error::Error>;
const LIVE_GATE: &str = "AION_UPDATE_CHECK_LIVE";
const STOCK_CONFIG: &[u8] = b"[observability]\nmax_batch_events = 64\nmax_batch_hold_ms = 0\n";
const LIVE_DEADLINE: Duration = Duration::from_secs(120);
const POLL_INTERVAL: Duration = Duration::from_millis(500);
fn authorized(path: &str) -> Result<Request<body::Body>, TestError> {
Ok(Request::builder()
.uri(path)
.method("GET")
.header("x-aion-subject", "update-check-live-e2e")
.header("x-aion-namespaces", "default")
.body(body::Body::empty())?)
}
fn private_tempdir() -> Result<tempfile::TempDir, TestError> {
let dir = tempfile::tempdir()?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o700))?;
}
Ok(dir)
}
#[tokio::test(flavor = "multi_thread")]
async fn gated_live_check_moves_update_status_or_skips_without_env() -> Result<(), TestError> {
if std::env::var(LIVE_GATE).as_deref() != Ok("1") {
tracing::info!(
gate = LIVE_GATE,
"skipping the live update-check e2e: the gate variable is not `1` and the chain \
needs the network (the offline wiring twin runs in every battery)"
);
eprintln!(
"(PROVES NOTHING): SKIPPED (not a pass of the live chain): {LIVE_GATE} != 1, network-gated e2e \
returned Ok(()) without running anything"
);
return Ok(());
}
let home = private_tempdir()?;
let config = ServerConfig::from_slice_with_home(STOCK_CONFIG, home.path())?;
let (_, runtime) = config.into_parts();
let server = StateUnderTest::new(
ServerState::build_with_store(InMemoryStore::default(), runtime).await?,
);
let install = install_embedded_update_check_for_server(&server.state).await;
assert!(
matches!(install, UpdateCheckInstall::Installed { .. }),
"a fresh temp home must install the embedded document, got {install:?}"
);
let router = http_router(server.state.clone())?;
let before = router
.clone()
.oneshot(authorized("/update-status")?)
.await?;
assert_eq!(before.status(), StatusCode::OK);
let before: Value =
serde_json::from_slice(&body::to_bytes(before.into_body(), usize::MAX).await?)?;
assert_eq!(before["latest_known"], Value::Null);
let start_body = serde_json::to_vec(&json!({
"namespace": "default",
"workflow_type": UPDATE_CHECK_WORKFLOW_TYPE,
"input": {},
}))?;
let start = router
.clone()
.oneshot(
Request::builder()
.uri("/workflows/start")
.method("POST")
.header("content-type", "application/json")
.header("x-aion-subject", "update-check-live-e2e")
.header("x-aion-namespaces", "default")
.body(body::Body::from(start_body))?,
)
.await?;
assert_eq!(
start.status(),
StatusCode::OK,
"the start must be accepted: {}",
String::from_utf8_lossy(&body::to_bytes(start.into_body(), usize::MAX).await?)
);
let deadline = tokio::time::Instant::now() + LIVE_DEADLINE;
loop {
let response = router
.clone()
.oneshot(authorized("/update-status")?)
.await?;
assert_eq!(response.status(), StatusCode::OK);
let status: Value =
serde_json::from_slice(&body::to_bytes(response.into_body(), usize::MAX).await?)?;
if let Some(latest) = status["latest_known"].as_str() {
assert!(
!latest.contains('-')
&& latest.split('.').count() == 3
&& latest
.split('.')
.all(|part| !part.is_empty() && part.bytes().all(|b| b.is_ascii_digit())),
"latest_known must be a stable release version, got `{latest}`"
);
assert!(
status["checked_at"].as_str().is_some(),
"a recorded check carries its timestamp"
);
tracing::info!(%latest, "live update check recorded and served");
server.shutdown()?;
return Ok(());
}
if tokio::time::Instant::now() >= deadline {
return Err(format!(
"the live check did not record within {LIVE_DEADLINE:?}; last answer: {status}"
)
.into());
}
tokio::time::sleep(POLL_INTERVAL).await;
}
}