use axum::body::Body;
use axum::http::{Method, Request, StatusCode};
use tower::ServiceExt;
use opendev_history::SessionManager;
use opendev_models::{AppConfig, Session};
use opendev_web::server::build_app;
use opendev_web::state::AppState;
use tempfile::TempDir;
fn make_test_state() -> (TempDir, AppState) {
let tmp = TempDir::new().unwrap();
let session_manager = SessionManager::new(tmp.path().to_path_buf()).unwrap();
let config = AppConfig::default();
let user_store = opendev_http::UserStore::new(tmp.path().to_path_buf()).unwrap();
let model_registry = opendev_config::ModelRegistry::new();
let state = AppState::new(
session_manager,
config,
tmp.path().to_string_lossy().to_string(),
user_store,
model_registry,
);
(tmp, state)
}
async fn send_request(
state: AppState,
method: Method,
uri: &str,
body: Option<serde_json::Value>,
) -> (StatusCode, serde_json::Value) {
let app = build_app(state, None);
let mut builder = Request::builder().method(method).uri(uri);
let body = if let Some(json) = body {
builder = builder.header("content-type", "application/json");
Body::from(serde_json::to_string(&json).unwrap())
} else {
Body::empty()
};
let request = builder.body(body).unwrap();
let response = app.oneshot(request).await.unwrap();
let status = response.status();
let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap_or(serde_json::json!(null));
(status, json)
}
#[tokio::test]
async fn health_check_returns_ok() {
let (_tmp, state) = make_test_state();
let (status, json) = send_request(state, Method::GET, "/api/health", None).await;
assert_eq!(status, StatusCode::OK);
assert_eq!(json["status"], "ok");
assert_eq!(json["service"], "opendev-web-ui");
}
#[tokio::test]
async fn create_session() {
let (_tmp, state) = make_test_state();
let (status, json) = send_request(
state,
Method::POST,
"/api/sessions",
Some(serde_json::json!({})),
)
.await;
assert_eq!(status, StatusCode::OK);
assert_eq!(json["status"], "created");
assert!(json["id"].as_str().is_some(), "should return session ID");
}
#[tokio::test]
async fn create_session_with_working_dir() {
let (_tmp, state) = make_test_state();
let (status, json) = send_request(
state,
Method::POST,
"/api/sessions",
Some(serde_json::json!({"working_directory": "/tmp/my-project"})),
)
.await;
assert_eq!(status, StatusCode::OK);
assert!(json["id"].as_str().is_some());
}
#[tokio::test]
async fn list_sessions_empty() {
let (_tmp, state) = make_test_state();
let (status, json) = send_request(state, Method::GET, "/api/sessions", None).await;
assert_eq!(status, StatusCode::OK);
assert!(json.as_array().is_some());
assert!(json.as_array().unwrap().is_empty());
}
#[tokio::test]
async fn list_sessions_after_create() {
let (tmp, state) = make_test_state();
{
let mut mgr = state.session_manager_mut().await;
let session = mgr.create_session();
let _ = session; mgr.save_current().unwrap();
}
let (status, json) = send_request(state, Method::GET, "/api/sessions", None).await;
assert_eq!(status, StatusCode::OK);
let sessions = json.as_array().unwrap();
assert_eq!(sessions.len(), 1);
}
#[tokio::test]
async fn get_session_by_id() {
let (tmp, state) = make_test_state();
let session_id;
{
let mgr = SessionManager::new(tmp.path().to_path_buf()).unwrap();
let mut session = Session::new();
session.id = "test-get".to_string();
mgr.save_session(&session).unwrap();
session_id = session.id;
}
let (status, json) = send_request(
state,
Method::GET,
&format!("/api/sessions/{session_id}"),
None,
)
.await;
assert_eq!(status, StatusCode::OK);
assert_eq!(json["id"], session_id);
}
#[tokio::test]
async fn get_nonexistent_session_returns_404() {
let (_tmp, state) = make_test_state();
let (status, json) = send_request(state, Method::GET, "/api/sessions/nonexistent", None).await;
assert_eq!(status, StatusCode::NOT_FOUND);
assert!(json["error"].as_str().is_some());
}
#[tokio::test]
async fn resume_session() {
let (tmp, state) = make_test_state();
{
let mgr = SessionManager::new(tmp.path().to_path_buf()).unwrap();
let mut session = Session::new();
session.id = "test-resume".to_string();
mgr.save_session(&session).unwrap();
}
let (status, json) = send_request(
state,
Method::POST,
"/api/sessions/test-resume/resume",
None,
)
.await;
assert_eq!(status, StatusCode::OK);
assert_eq!(json["status"], "resumed");
assert_eq!(json["session_id"], "test-resume");
}
#[tokio::test]
async fn resume_nonexistent_session_returns_404() {
let (_tmp, state) = make_test_state();
let (status, _) = send_request(
state,
Method::POST,
"/api/sessions/nonexistent/resume",
None,
)
.await;
assert_eq!(status, StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn get_config() {
let (_tmp, state) = make_test_state();
let (status, json) = send_request(state, Method::GET, "/api/config", None).await;
assert_eq!(status, StatusCode::OK);
assert_eq!(json["model_provider"], "fireworks");
assert_eq!(json["mode"], "normal");
assert_eq!(json["autonomy_level"], "Manual");
}
#[tokio::test]
async fn update_config() {
let (_tmp, state) = make_test_state();
let (status, json) = send_request(
state.clone(),
Method::PUT,
"/api/config",
Some(serde_json::json!({
"model_provider": "openai",
"model": "gpt-4o",
"temperature": 0.9
})),
)
.await;
assert_eq!(status, StatusCode::OK);
assert_eq!(json["status"], "success");
let (status, json) = send_request(state, Method::GET, "/api/config", None).await;
assert_eq!(status, StatusCode::OK);
assert_eq!(json["model_provider"], "openai");
assert_eq!(json["model"], "gpt-4o");
}
#[tokio::test]
async fn set_mode() {
let (_tmp, state) = make_test_state();
let (status, json) = send_request(
state.clone(),
Method::POST,
"/api/config/mode",
Some(serde_json::json!({"mode": "plan"})),
)
.await;
assert_eq!(status, StatusCode::OK);
assert_eq!(json["status"], "success");
let (_, config) = send_request(state, Method::GET, "/api/config", None).await;
assert_eq!(config["mode"], "plan");
}
#[tokio::test]
async fn set_invalid_mode_returns_400() {
let (_tmp, state) = make_test_state();
let (status, json) = send_request(
state,
Method::POST,
"/api/config/mode",
Some(serde_json::json!({"mode": "invalid"})),
)
.await;
assert_eq!(status, StatusCode::BAD_REQUEST);
assert!(json["error"].as_str().is_some());
}
#[tokio::test]
async fn set_autonomy_level() {
let (_tmp, state) = make_test_state();
let (status, json) = send_request(
state.clone(),
Method::POST,
"/api/config/autonomy",
Some(serde_json::json!({"level": "Auto"})),
)
.await;
assert_eq!(status, StatusCode::OK);
let (_, config) = send_request(state, Method::GET, "/api/config", None).await;
assert_eq!(config["autonomy_level"], "Auto");
}
#[tokio::test]
async fn set_invalid_autonomy_returns_400() {
let (_tmp, state) = make_test_state();
let (status, _) = send_request(
state,
Method::POST,
"/api/config/autonomy",
Some(serde_json::json!({"level": "SuperAuto"})),
)
.await;
assert_eq!(status, StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn state_interrupt_flag() {
let (_tmp, state) = make_test_state();
assert!(!state.is_interrupt_requested().await);
state.request_interrupt().await;
assert!(state.is_interrupt_requested().await);
state.clear_interrupt().await;
assert!(!state.is_interrupt_requested().await);
}
#[tokio::test]
async fn state_session_running() {
let (_tmp, state) = make_test_state();
assert!(!state.is_session_running("s1").await);
state.set_session_running("s1".to_string()).await;
assert!(state.is_session_running("s1").await);
state.set_session_idle("s1").await;
assert!(!state.is_session_running("s1").await);
}
#[tokio::test]
async fn state_ws_broadcast() {
let (_tmp, state) = make_test_state();
let mut rx = state.ws_subscribe();
state.broadcast(opendev_web::state::WsBroadcast {
msg_type: "test_event".to_string(),
data: serde_json::json!({"key": "value"}),
});
let msg = rx.recv().await.unwrap();
assert_eq!(msg.msg_type, "test_event");
assert_eq!(msg.data["key"], "value");
}