this repo has no description
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}