use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use parking_lot::Mutex;
use studio_worker::auto_register::RegistrationState;
use studio_worker::config::{self, Config};
use studio_worker::runtime::{self, RegistrationGate};
use tempfile::tempdir;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn polling_cfg(api: &str) -> Config {
Config {
api_base_url: api.into(),
worker_id: None,
auth_token: None,
auto_update_enabled: false,
install_id: Some("install-abc".into()),
registration_request_id: Some("rr-gate".into()),
registration_secret: Some("secret-xyz".into()),
..Config::default()
}
}
fn write_cfg(dir: &tempfile::TempDir, cfg: &Config) -> std::path::PathBuf {
let path = dir.path().join("config.toml");
config::save(cfg, &path).unwrap();
path
}
#[tokio::test]
async fn returns_ok_immediately_when_already_registered() {
let dir = tempdir().unwrap();
let mut cfg = polling_cfg("http://127.0.0.1:1/unreachable");
cfg.worker_id = Some("w-known".into());
cfg.auth_token = Some("tok-known".into());
let path = write_cfg(&dir, &cfg);
let shared = Arc::new(Mutex::new(cfg));
let observers = Arc::new(Mutex::new(RegistrationState::Pristine));
let stop = Arc::new(AtomicBool::new(false));
let gate = runtime::ensure_registered(&shared, &path, &observers, &stop)
.await
.expect("an already-registered worker must pass the gate");
assert_eq!(gate, RegistrationGate::Ready);
}
#[tokio::test]
async fn returns_stopped_when_stop_is_set_before_registration() {
let dir = tempdir().unwrap();
let cfg = polling_cfg("http://127.0.0.1:1/unreachable");
let path = write_cfg(&dir, &cfg);
let shared = Arc::new(Mutex::new(cfg));
let observers = Arc::new(Mutex::new(RegistrationState::Pristine));
let stop = Arc::new(AtomicBool::new(true));
let gate = runtime::ensure_registered(&shared, &path, &observers, &stop)
.await
.expect("a pre-set stop flag is a clean shutdown, not an error");
assert_eq!(gate, RegistrationGate::Stopped);
}
#[tokio::test]
async fn returns_stopped_when_stop_is_set_during_the_wait() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/graphics/api/workers/register-requests/rr-gate"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"status": "pending",
})))
.mount(&server)
.await;
let dir = tempdir().unwrap();
let cfg = polling_cfg(&server.uri());
let path = write_cfg(&dir, &cfg);
let shared = Arc::new(Mutex::new(cfg));
let observers = Arc::new(Mutex::new(RegistrationState::Pristine));
let stop = Arc::new(AtomicBool::new(false));
let stop_flipper = stop.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(100)).await;
stop_flipper.store(true, Ordering::SeqCst);
});
let gate = runtime::ensure_registered(&shared, &path, &observers, &stop)
.await
.expect("a clean stop during the wait must not be a fatal error");
assert_eq!(gate, RegistrationGate::Stopped);
}
#[tokio::test]
async fn surfaces_operator_rejection_with_reset_guidance() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/graphics/api/workers/register-requests/rr-gate"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"status": "rejected",
"reason": "unknown host",
})))
.mount(&server)
.await;
let dir = tempdir().unwrap();
let cfg = polling_cfg(&server.uri());
let path = write_cfg(&dir, &cfg);
let shared = Arc::new(Mutex::new(cfg));
let observers = Arc::new(Mutex::new(RegistrationState::Pristine));
let stop = Arc::new(AtomicBool::new(false));
let err = runtime::ensure_registered(&shared, &path, &observers, &stop)
.await
.expect_err("a studio rejection must fail the gate");
let msg = err.to_string();
assert!(
msg.contains("registration rejected"),
"must name the rejection: {msg}"
);
assert!(
msg.contains("unknown host"),
"must carry the operator's reason: {msg}"
);
assert!(
msg.contains("register --reset"),
"must tell the operator how to recover: {msg}"
);
}
#[tokio::test]
async fn passes_the_gate_when_the_studio_approves() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/graphics/api/workers/register-requests/rr-gate"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"status": "approved",
"workerId": "w-approved",
"authToken": "tok-approved",
})))
.mount(&server)
.await;
let dir = tempdir().unwrap();
let cfg = polling_cfg(&server.uri());
let path = write_cfg(&dir, &cfg);
let shared = Arc::new(Mutex::new(cfg));
let observers = Arc::new(Mutex::new(RegistrationState::Pristine));
let stop = Arc::new(AtomicBool::new(false));
let gate = runtime::ensure_registered(&shared, &path, &observers, &stop)
.await
.expect("approval must pass the gate");
assert_eq!(gate, RegistrationGate::Ready);
let snap = shared.lock().clone();
assert_eq!(snap.worker_id.as_deref(), Some("w-approved"));
assert_eq!(snap.auth_token.as_deref(), Some("tok-approved"));
assert!(!stop.load(Ordering::SeqCst));
}