this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 505 lines 18 kB view raw
1use std::sync::Arc; 2use std::time::Duration; 3use tokio::time::sleep; 4use serde_json::{json, Value}; 5use reqwest::{Client, Response}; 6use gigabrain::Graph; 7use gigabrain::server::{ServerConfig, rest::RestServer}; 8 9/// Integration test client for HTTP API testing 10#[derive(Clone)] 11pub struct TestClient { 12 client: Client, 13 base_url: String, 14} 15 16impl TestClient { 17 pub fn new(base_url: &str) -> Self { 18 Self { 19 client: Client::new(), 20 base_url: base_url.to_string(), 21 } 22 } 23 24 /// Helper to make GET requests 25 pub async fn get(&self, path: &str) -> Result<Response, reqwest::Error> { 26 self.client 27 .get(&format!("{}{}", self.base_url, path)) 28 .send() 29 .await 30 } 31 32 /// Helper to make POST requests with JSON body 33 pub async fn post_json(&self, path: &str, body: Value) -> Result<Response, reqwest::Error> { 34 self.client 35 .post(&format!("{}{}", self.base_url, path)) 36 .header("Content-Type", "application/json") 37 .json(&body) 38 .send() 39 .await 40 } 41 42 /// Helper to make PUT requests with JSON body 43 pub async fn put_json(&self, path: &str, body: Value) -> Result<Response, reqwest::Error> { 44 self.client 45 .put(&format!("{}{}", self.base_url, path)) 46 .header("Content-Type", "application/json") 47 .json(&body) 48 .send() 49 .await 50 } 51 52 /// Helper to make DELETE requests 53 pub async fn delete(&self, path: &str) -> Result<Response, reqwest::Error> { 54 self.client 55 .delete(&format!("{}{}", self.base_url, path)) 56 .send() 57 .await 58 } 59} 60 61/// Test server manager for integration tests 62pub struct TestServer { 63 _handle: tokio::task::JoinHandle<()>, 64 base_url: String, 65 port: u16, 66} 67 68impl TestServer { 69 /// Start a test server on a random available port 70 pub async fn start() -> Result<Self, Box<dyn std::error::Error>> { 71 use std::net::TcpListener; 72 73 // Find an available port 74 let listener = TcpListener::bind("127.0.0.1:0")?; 75 let port = listener.local_addr()?.port(); 76 drop(listener); 77 78 let graph = Arc::new(Graph::new()); 79 let config = ServerConfig::default(); 80 let server = RestServer::new(graph, config); 81 82 // Start server in background 83 let handle = tokio::spawn(async move { 84 if let Err(e) = server.serve(port).await { 85 eprintln!("Test server error: {}", e); 86 } 87 }); 88 89 // Wait a bit for server to start 90 sleep(Duration::from_millis(100)).await; 91 92 let base_url = format!("http://localhost:{}", port); 93 94 Ok(Self { 95 _handle: handle, 96 base_url, 97 port, 98 }) 99 } 100 101 pub fn client(&self) -> TestClient { 102 TestClient::new(&self.base_url) 103 } 104 105 pub fn port(&self) -> u16 { 106 self.port 107 } 108} 109 110#[cfg(test)] 111mod tests { 112 use super::*; 113 use serde_json::json; 114 115 /// Helper to start test server for each test 116 async fn setup() -> TestClient { 117 let server = TestServer::start().await.expect("Failed to start test server"); 118 server.client() 119 } 120 121 #[tokio::test] 122 async fn test_health_check() { 123 let client = setup().await; 124 125 let response = client.get("/health").await.expect("Request failed"); 126 assert_eq!(response.status(), 200); 127 128 let body: Value = response.json().await.expect("Failed to parse JSON"); 129 assert_eq!(body["status"], "healthy"); 130 assert!(body["version"].is_string()); 131 } 132 133 #[tokio::test] 134 async fn test_node_crud_operations() { 135 let client = setup().await; 136 137 // Create a node 138 let create_request = json!({ 139 "labels": ["Person", "Employee"], 140 "properties": { 141 "name": "John Doe", 142 "age": 30, 143 "active": true, 144 "salary": 75000 145 } 146 }); 147 148 let response = client.post_json("/api/v1/nodes", create_request).await.expect("Create failed"); 149 assert_eq!(response.status(), 200); 150 151 let create_body: Value = response.json().await.expect("Failed to parse JSON"); 152 let node_id = create_body["node_id"].as_u64().expect("node_id should be number"); 153 154 // Get the node 155 let response = client.get(&format!("/api/v1/nodes/{}", node_id)).await.expect("Get failed"); 156 assert_eq!(response.status(), 200); 157 158 let get_body: Value = response.json().await.expect("Failed to parse JSON"); 159 assert_eq!(get_body["id"], node_id); 160 assert_eq!(get_body["labels"], json!(["Person", "Employee"])); 161 assert_eq!(get_body["properties"]["name"], "John Doe"); 162 assert_eq!(get_body["properties"]["age"], 30); 163 assert_eq!(get_body["properties"]["active"], true); 164 assert_eq!(get_body["properties"]["salary"], 75000); 165 166 // Update the node 167 let update_request = json!({ 168 "labels": ["Person", "Manager"], 169 "properties": { 170 "title": "Engineering Manager", 171 "team_size": 5 172 } 173 }); 174 175 let response = client.put_json(&format!("/api/v1/nodes/{}", node_id), update_request).await.expect("Update failed"); 176 assert_eq!(response.status(), 200); 177 178 let update_body: Value = response.json().await.expect("Failed to parse JSON"); 179 assert_eq!(update_body["labels"], json!(["Person", "Manager"])); 180 assert_eq!(update_body["properties"]["title"], "Engineering Manager"); 181 assert_eq!(update_body["properties"]["team_size"], 5); 182 // Existing properties should be preserved 183 assert_eq!(update_body["properties"]["name"], "John Doe"); 184 185 // Delete a specific property 186 let response = client.delete(&format!("/api/v1/nodes/{}/properties/age", node_id)).await.expect("Property delete failed"); 187 assert_eq!(response.status(), 200); 188 189 let delete_prop_body: Value = response.json().await.expect("Failed to parse JSON"); 190 assert!(delete_prop_body["properties"]["age"].is_null()); 191 assert_eq!(delete_prop_body["properties"]["name"], "John Doe"); 192 193 // Delete the node 194 let response = client.delete(&format!("/api/v1/nodes/{}", node_id)).await.expect("Delete failed"); 195 assert_eq!(response.status(), 204); 196 197 // Verify node is deleted 198 let response = client.get(&format!("/api/v1/nodes/{}", node_id)).await.expect("Get failed"); 199 assert_eq!(response.status(), 404); 200 } 201 202 #[tokio::test] 203 async fn test_relationship_operations() { 204 let client = setup().await; 205 206 // Create two nodes first 207 let node1_request = json!({ 208 "labels": ["Person"], 209 "properties": {"name": "Alice"} 210 }); 211 let response = client.post_json("/api/v1/nodes", node1_request).await.expect("Create node1 failed"); 212 let node1_id = response.json::<Value>().await.expect("Parse failed")["node_id"].as_u64().unwrap(); 213 214 let node2_request = json!({ 215 "labels": ["Person"], 216 "properties": {"name": "Bob"} 217 }); 218 let response = client.post_json("/api/v1/nodes", node2_request).await.expect("Create node2 failed"); 219 let node2_id = response.json::<Value>().await.expect("Parse failed")["node_id"].as_u64().unwrap(); 220 221 // Create relationship 222 let rel_request = json!({ 223 "start_node": node1_id, 224 "end_node": node2_id, 225 "rel_type": "KNOWS", 226 "properties": { 227 "since": "2023-01-01", 228 "strength": 8 229 } 230 }); 231 232 let response = client.post_json("/api/v1/relationships", rel_request).await.expect("Create relationship failed"); 233 assert_eq!(response.status(), 200); 234 235 let rel_body: Value = response.json().await.expect("Failed to parse JSON"); 236 let rel_id = rel_body["relationship_id"].as_u64().expect("relationship_id should be number"); 237 238 // Get the relationship 239 let response = client.get(&format!("/api/v1/relationships/{}", rel_id)).await.expect("Get relationship failed"); 240 assert_eq!(response.status(), 200); 241 242 let get_rel_body: Value = response.json().await.expect("Failed to parse JSON"); 243 assert_eq!(get_rel_body["id"], rel_id); 244 assert_eq!(get_rel_body["start_node"], node1_id); 245 assert_eq!(get_rel_body["end_node"], node2_id); 246 assert_eq!(get_rel_body["rel_type"], "KNOWS"); 247 248 // Delete relationship 249 let response = client.delete(&format!("/api/v1/relationships/{}", rel_id)).await.expect("Delete relationship failed"); 250 assert_eq!(response.status(), 204); 251 252 // Verify relationship is deleted 253 let response = client.get(&format!("/api/v1/relationships/{}", rel_id)).await.expect("Get relationship failed"); 254 assert_eq!(response.status(), 404); 255 } 256 257 #[tokio::test] 258 async fn test_schema_constraints() { 259 let client = setup().await; 260 261 // Create required constraint 262 let constraint_request = json!({ 263 "constraint_type": "required", 264 "label": "Employee", 265 "property": "email" 266 }); 267 268 let response = client.post_json("/api/v1/constraints", constraint_request).await.expect("Create constraint failed"); 269 assert_eq!(response.status(), 200); 270 271 let constraint_body: Value = response.json().await.expect("Failed to parse JSON"); 272 assert_eq!(constraint_body["constraint_type"], "required"); 273 assert_eq!(constraint_body["label"], "Employee"); 274 assert_eq!(constraint_body["property"], "email"); 275 276 // Try to create node without required property (should fail) 277 let invalid_node = json!({ 278 "labels": ["Employee"], 279 "properties": {"name": "John Doe"} 280 }); 281 282 let response = client.post_json("/api/v1/nodes", invalid_node).await.expect("Request failed"); 283 assert_eq!(response.status(), 400); 284 285 let error_body: Value = response.json().await.expect("Failed to parse JSON"); 286 assert!(error_body["error"].as_str().unwrap().contains("Required property 'email' is missing")); 287 288 // Create valid node with required property 289 let valid_node = json!({ 290 "labels": ["Employee"], 291 "properties": { 292 "name": "John Doe", 293 "email": "john@example.com" 294 } 295 }); 296 297 let response = client.post_json("/api/v1/nodes", valid_node).await.expect("Create valid node failed"); 298 assert_eq!(response.status(), 200); 299 } 300 301 #[tokio::test] 302 async fn test_type_constraints() { 303 let client = setup().await; 304 305 // Create type constraint 306 let constraint_request = json!({ 307 "constraint_type": "type", 308 "label": "Employee", 309 "property": "salary", 310 "type_value": "integer" 311 }); 312 313 let response = client.post_json("/api/v1/constraints", constraint_request).await.expect("Create constraint failed"); 314 assert_eq!(response.status(), 200); 315 316 // Try to create node with wrong type (should fail) 317 let invalid_node = json!({ 318 "labels": ["Employee"], 319 "properties": { 320 "name": "John Doe", 321 "salary": "fifty thousand" 322 } 323 }); 324 325 let response = client.post_json("/api/v1/nodes", invalid_node).await.expect("Request failed"); 326 assert_eq!(response.status(), 400); 327 328 let error_body: Value = response.json().await.expect("Failed to parse JSON"); 329 assert!(error_body["error"].as_str().unwrap().contains("wrong type")); 330 331 // Create valid node with correct type 332 let valid_node = json!({ 333 "labels": ["Employee"], 334 "properties": { 335 "name": "John Doe", 336 "salary": 50000 337 } 338 }); 339 340 let response = client.post_json("/api/v1/nodes", valid_node).await.expect("Create valid node failed"); 341 assert_eq!(response.status(), 200); 342 } 343 344 #[tokio::test] 345 async fn test_range_constraints() { 346 let client = setup().await; 347 348 // Create range constraint 349 let constraint_request = json!({ 350 "constraint_type": "range", 351 "label": "Employee", 352 "property": "salary", 353 "min_value": 30000, 354 "max_value": 200000 355 }); 356 357 let response = client.post_json("/api/v1/constraints", constraint_request).await.expect("Create constraint failed"); 358 assert_eq!(response.status(), 200); 359 360 // Try to create node with value out of range (should fail) 361 let invalid_node = json!({ 362 "labels": ["Employee"], 363 "properties": { 364 "name": "John Doe", 365 "salary": 250000 366 } 367 }); 368 369 let response = client.post_json("/api/v1/nodes", invalid_node).await.expect("Request failed"); 370 assert_eq!(response.status(), 400); 371 372 let error_body: Value = response.json().await.expect("Failed to parse JSON"); 373 assert!(error_body["error"].as_str().unwrap().contains("out of range")); 374 375 // Create valid node within range 376 let valid_node = json!({ 377 "labels": ["Employee"], 378 "properties": { 379 "name": "John Doe", 380 "salary": 75000 381 } 382 }); 383 384 let response = client.post_json("/api/v1/nodes", valid_node).await.expect("Create valid node failed"); 385 assert_eq!(response.status(), 200); 386 } 387 388 #[tokio::test] 389 async fn test_graph_statistics() { 390 let client = setup().await; 391 392 // Get initial stats 393 let response = client.get("/api/v1/stats").await.expect("Get stats failed"); 394 assert_eq!(response.status(), 200); 395 396 let stats: Value = response.json().await.expect("Failed to parse JSON"); 397 let initial_count = stats["node_count"].as_u64().unwrap(); 398 399 // Create some nodes 400 for i in 0..5 { 401 let node_request = json!({ 402 "labels": ["TestNode"], 403 "properties": {"id": i} 404 }); 405 let response = client.post_json("/api/v1/nodes", node_request).await.expect("Create node failed"); 406 assert_eq!(response.status(), 200); 407 } 408 409 // Check updated stats 410 let response = client.get("/api/v1/stats").await.expect("Get stats failed"); 411 let stats: Value = response.json().await.expect("Failed to parse JSON"); 412 assert_eq!(stats["node_count"].as_u64().unwrap(), initial_count + 5); 413 414 let labels = stats["labels"].as_array().unwrap(); 415 assert!(labels.contains(&json!("TestNode"))); 416 } 417 418 #[tokio::test] 419 async fn test_api_documentation() { 420 let client = setup().await; 421 422 let response = client.get("/api/v1/docs").await.expect("Get docs failed"); 423 assert_eq!(response.status(), 200); 424 425 let docs: Value = response.json().await.expect("Failed to parse JSON"); 426 assert_eq!(docs["title"], "GigaBrain Graph Database API"); 427 assert!(docs["endpoints"].is_object()); 428 assert!(docs["endpoints"]["nodes"].is_object()); 429 assert!(docs["endpoints"]["relationships"].is_object()); 430 assert!(docs["endpoints"]["constraints"].is_object()); 431 } 432 433 #[tokio::test] 434 async fn test_error_handling() { 435 let client = setup().await; 436 437 // Test non-existent node 438 let response = client.get("/api/v1/nodes/999999").await.expect("Request failed"); 439 assert_eq!(response.status(), 404); 440 441 let error: Value = response.json().await.expect("Failed to parse JSON"); 442 assert_eq!(error["code"], 404); 443 assert!(error["error"].as_str().unwrap().contains("not found")); 444 445 // Test malformed JSON 446 let response = client.client 447 .post(&format!("{}/api/v1/nodes", client.base_url)) 448 .header("Content-Type", "application/json") 449 .body("invalid json") 450 .send() 451 .await 452 .expect("Request failed"); 453 assert_eq!(response.status(), 400); 454 455 // Test non-existent relationship 456 let response = client.get("/api/v1/relationships/999999").await.expect("Request failed"); 457 assert_eq!(response.status(), 404); 458 459 // Test invalid constraint type 460 let invalid_constraint = json!({ 461 "constraint_type": "invalid_type", 462 "label": "Test", 463 "property": "test" 464 }); 465 466 let response = client.post_json("/api/v1/constraints", invalid_constraint).await.expect("Request failed"); 467 assert_eq!(response.status(), 400); 468 } 469 470 #[tokio::test] 471 async fn test_concurrent_operations() { 472 let client = setup().await; 473 474 // Create multiple nodes concurrently 475 let mut handles = vec![]; 476 477 for i in 0..10 { 478 let client = client.clone(); 479 let handle = tokio::spawn(async move { 480 let node_request = json!({ 481 "labels": ["ConcurrentTest"], 482 "properties": {"thread_id": i} 483 }); 484 485 client.post_json("/api/v1/nodes", node_request).await 486 }); 487 handles.push(handle); 488 } 489 490 // Wait for all to complete 491 let results: Vec<_> = futures::future::join_all(handles).await; 492 493 // All should succeed 494 for result in results { 495 let response = result.expect("Task failed").expect("Request failed"); 496 assert_eq!(response.status(), 200); 497 } 498 499 // Verify all nodes were created 500 let response = client.get("/api/v1/stats").await.expect("Get stats failed"); 501 let stats: Value = response.json().await.expect("Failed to parse JSON"); 502 let node_count = stats["node_count"].as_u64().unwrap(); 503 assert!(node_count >= 10); 504 } 505}