//! Integration tests for el-ide-server API endpoints. use std::sync::Arc; use tokio::sync::Mutex; use axum::{ body::Body, http::{Request, StatusCode}, }; use tower::ServiceExt; use el_plugin_host::PluginHost; use crate::{build_router, config::Config, AppState}; fn test_state(project_path: &str) -> AppState { AppState { config: Arc::new(Config { port: 7771, project_path: project_path.to_string(), engram_url: "http://localhost:8742".into(), }), plugins: Arc::new(Mutex::new(PluginHost::new())), } } fn test_project_path() -> String { // Use the examples/hello-project as the test project. // CARGO_MANIFEST_DIR = el-ide/crates/el-ide-server let manifest = env!("CARGO_MANIFEST_DIR"); // Normalize: go up three directories from the crate to workspace root, then into examples let ws_root = std::path::Path::new(manifest) .parent().unwrap() // crates/ .parent().unwrap() // el-ide/ .to_path_buf(); ws_root.join("examples/hello-project") .canonicalize() .unwrap_or_else(|_| ws_root.join("examples/hello-project")) .to_string_lossy() .to_string() } async fn get_json(app: axum::Router, uri: &str) -> (StatusCode, serde_json::Value) { let resp = app .oneshot(Request::get(uri).body(Body::empty()).unwrap()) .await .unwrap(); let status = resp.status(); let bytes = axum::body::to_bytes(resp.into_body(), 1024 * 1024).await.unwrap(); let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap_or(serde_json::Value::Null); (status, json) } // ── GET / ───────────────────────────────────────────────────────────────────── #[tokio::test] async fn test_root_returns_html() { let state = test_state(&test_project_path()); let app = build_router(state); let resp = app .oneshot(Request::get("/").body(Body::empty()).unwrap()) .await .unwrap(); // The embedded HTML should return 200 assert_eq!(resp.status(), StatusCode::OK); } // ── GET /api/files ──────────────────────────────────────────────────────────── #[tokio::test] async fn test_list_files_returns_entries() { let project = test_project_path(); let state = test_state(&project); let app = build_router(state); let (status, json) = get_json(app, "/api/files?path=.").await; assert_eq!(status, StatusCode::OK, "body: {json}"); assert!(json.is_array(), "expected array, got {json}"); let arr = json.as_array().unwrap(); assert!(!arr.is_empty(), "expected non-empty file listing"); } #[tokio::test] async fn test_list_files_contains_src_dir() { let project = test_project_path(); let state = test_state(&project); let app = build_router(state); let (status, json) = get_json(app, "/api/files?path=.").await; assert_eq!(status, StatusCode::OK); let arr = json.as_array().unwrap(); let has_src = arr.iter().any(|e| e["name"] == "src" || e["name"] == "el.toml"); assert!(has_src, "expected src or el.toml in listing; got {arr:?}"); } // ── GET /api/file ───────────────────────────────────────────────────────────── #[tokio::test] async fn test_read_file_main_el() { let project = test_project_path(); let state = test_state(&project); let app = build_router(state); let (status, json) = get_json(app, "/api/file?path=src/main.el").await; assert_eq!(status, StatusCode::OK, "body: {json}"); assert!(json["content"].is_string(), "expected content field"); let content = json["content"].as_str().unwrap(); assert!(content.contains("fn main"), "expected fn main in content"); } #[tokio::test] async fn test_read_missing_file_returns_404() { let project = test_project_path(); let state = test_state(&project); let app = build_router(state); let (status, _) = get_json(app, "/api/file?path=nonexistent.el").await; assert_eq!(status, StatusCode::NOT_FOUND); } // ── GET /api/lsp/errors ─────────────────────────────────────────────────────── #[tokio::test] async fn test_lsp_errors_clean_source() { let state = test_state("."); let app = build_router(state); let source = "fn main() -> Void { let x: Int = 42 }"; let uri = format!("/api/lsp/errors?source={}", urlencoding::encode(source)); let (status, json) = get_json(app, &uri).await; assert_eq!(status, StatusCode::OK, "body: {json}"); assert!(json.is_array()); let errors: Vec<_> = json.as_array().unwrap().iter() .filter(|d| d["severity"] == "error") .collect(); assert!(errors.is_empty(), "unexpected errors: {errors:?}"); } #[tokio::test] async fn test_lsp_errors_bad_type() { let state = test_state("."); let app = build_router(state); let source = "let x: NonExistentType = 42"; let uri = format!("/api/lsp/errors?source={}", urlencoding::encode(source)); let (status, json) = get_json(app, &uri).await; assert_eq!(status, StatusCode::OK, "body: {json}"); assert!(json.is_array()); // Should have at least one diagnostic assert!(!json.as_array().unwrap().is_empty(), "expected diagnostic for unknown type"); } // ── GET /api/type-graph ─────────────────────────────────────────────────────── #[tokio::test] async fn test_type_graph_returns_nodes_and_edges() { let state = test_state("."); let app = build_router(state); let source = "type Point { x: Float y: Float } type Circle { center: Point radius: Float }"; let uri = format!("/api/type-graph?source={}", urlencoding::encode(source)); let (status, json) = get_json(app, &uri).await; assert_eq!(status, StatusCode::OK, "body: {json}"); assert!(json["nodes"].is_array()); assert!(json["edges"].is_array()); let node_names: Vec<_> = json["nodes"].as_array().unwrap() .iter().map(|n| n["name"].as_str().unwrap_or("")).collect(); assert!(node_names.contains(&"Point"), "expected Point node"); assert!(node_names.contains(&"Circle"), "expected Circle node"); } #[tokio::test] async fn test_type_graph_has_field_edge() { let state = test_state("."); let app = build_router(state); let source = "type Point { x: Float y: Float } type Circle { center: Point radius: Float }"; let uri = format!("/api/type-graph?source={}", urlencoding::encode(source)); let (status, json) = get_json(app, &uri).await; assert_eq!(status, StatusCode::OK); let edges = json["edges"].as_array().unwrap(); let has_edge = edges.iter().any(|e| e["from"] == "Circle" && e["to"] == "Point"); assert!(has_edge, "expected Circle->Point edge, edges: {edges:?}"); } // ── GET /api/plugins ────────────────────────────────────────────────────────── #[tokio::test] async fn test_list_plugins_returns_five() { let state = test_state("."); let app = build_router(state); let (status, json) = get_json(app, "/api/plugins").await; assert_eq!(status, StatusCode::OK, "body: {json}"); let plugins = json.as_array().unwrap(); assert_eq!(plugins.len(), 5, "expected 5 first-party plugins"); } #[tokio::test] async fn test_dark_theme_installed() { let state = test_state("."); let app = build_router(state); let (status, json) = get_json(app, "/api/plugins").await; assert_eq!(status, StatusCode::OK); let plugins = json.as_array().unwrap(); let dark = plugins.iter().find(|p| p["name"] == "el-theme-dark").unwrap(); assert_eq!(dark["installed"], true); } // ── GET /api/lsp/complete ───────────────────────────────────────────────────── #[tokio::test] async fn test_completions_return_keywords() { let state = test_state("."); let app = build_router(state); let uri = "/api/lsp/complete?source=&pos=0"; let (status, json) = get_json(app, uri).await; assert_eq!(status, StatusCode::OK); assert!(json.is_array()); let labels: Vec<_> = json.as_array().unwrap().iter() .map(|c| c["label"].as_str().unwrap_or("")) .collect(); assert!(labels.contains(&"let"), "expected 'let' in completions"); assert!(labels.contains(&"fn"), "expected 'fn' in completions"); }