Skip to content

Commit 1ac6623

Browse files
committed
feat(tests): add end-to-end integration tests for HTTP server
1 parent 53d2644 commit 1ac6623

File tree

1 file changed

+242
-0
lines changed

1 file changed

+242
-0
lines changed
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
//! End-to-end integration tests with actual HTTP server
2+
//!
3+
//! These tests start a real HTTP server and make network requests to it,
4+
//! testing the full stack including server startup, routing, and shutdown.
5+
6+
mod common;
7+
8+
use leanspec_http::state::AppState;
9+
use reqwest::{Client, StatusCode};
10+
use std::net::SocketAddr;
11+
use std::time::Duration;
12+
use tempfile::TempDir;
13+
use tokio::net::TcpListener;
14+
15+
use common::*;
16+
17+
/// Start a test HTTP server on a random available port
18+
async fn start_test_server(state: AppState) -> (SocketAddr, tokio::task::JoinHandle<()>) {
19+
// Bind to port 0 to get a random available port
20+
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
21+
let addr = listener.local_addr().unwrap();
22+
23+
let app = leanspec_http::create_router(state);
24+
25+
let handle = tokio::spawn(async move {
26+
axum::serve(listener, app).await.unwrap();
27+
});
28+
29+
// Give the server a moment to start
30+
tokio::time::sleep(Duration::from_millis(100)).await;
31+
32+
(addr, handle)
33+
}
34+
35+
#[tokio::test]
36+
async fn test_server_health_endpoint() {
37+
let temp_dir = TempDir::new().unwrap();
38+
let state = create_test_state(&temp_dir).await;
39+
let (addr, handle) = start_test_server(state).await;
40+
41+
let client = Client::new();
42+
let response = client
43+
.get(format!("http://{}/health", addr))
44+
.send()
45+
.await
46+
.unwrap();
47+
48+
assert_eq!(response.status(), StatusCode::OK);
49+
let body: serde_json::Value = response.json().await.unwrap();
50+
assert_eq!(body["status"], "ok");
51+
assert!(body["version"].is_string());
52+
53+
// Cleanup
54+
handle.abort();
55+
}
56+
57+
#[tokio::test]
58+
async fn test_server_list_projects() {
59+
let temp_dir = TempDir::new().unwrap();
60+
let state = create_test_state(&temp_dir).await;
61+
let (addr, handle) = start_test_server(state).await;
62+
63+
let client = Client::new();
64+
let response = client
65+
.get(format!("http://{}/api/projects", addr))
66+
.send()
67+
.await
68+
.unwrap();
69+
70+
assert_eq!(response.status(), StatusCode::OK);
71+
let body: serde_json::Value = response.json().await.unwrap();
72+
assert!(body["projects"].is_array());
73+
74+
// Cleanup
75+
handle.abort();
76+
}
77+
78+
#[tokio::test]
79+
async fn test_server_add_and_remove_project() {
80+
let temp_dir = TempDir::new().unwrap();
81+
create_test_project(temp_dir.path());
82+
83+
let state = create_empty_state().await;
84+
let (addr, handle) = start_test_server(state).await;
85+
86+
let client = Client::new();
87+
88+
// Add project
89+
let add_response = client
90+
.post(format!("http://{}/api/projects", addr))
91+
.json(&serde_json::json!({ "path": temp_dir.path().to_string_lossy() }))
92+
.send()
93+
.await
94+
.unwrap();
95+
96+
assert_eq!(add_response.status(), StatusCode::OK);
97+
let add_body: serde_json::Value = add_response.json().await.unwrap();
98+
let project_id = add_body["project"]["id"].as_str().unwrap();
99+
100+
// Verify project exists
101+
let list_response = client
102+
.get(format!("http://{}/api/projects", addr))
103+
.send()
104+
.await
105+
.unwrap();
106+
107+
let list_body: serde_json::Value = list_response.json().await.unwrap();
108+
let projects = list_body["projects"].as_array().unwrap();
109+
assert_eq!(projects.len(), 1);
110+
111+
// Remove project
112+
let remove_response = client
113+
.delete(format!("http://{}/api/projects/{}", addr, project_id))
114+
.send()
115+
.await
116+
.unwrap();
117+
118+
assert_eq!(remove_response.status(), StatusCode::OK);
119+
120+
// Verify project was removed
121+
let final_list = client
122+
.get(format!("http://{}/api/projects", addr))
123+
.send()
124+
.await
125+
.unwrap();
126+
127+
let final_body: serde_json::Value = final_list.json().await.unwrap();
128+
let final_projects = final_body["projects"].as_array().unwrap();
129+
assert_eq!(final_projects.len(), 0);
130+
131+
// Cleanup
132+
handle.abort();
133+
}
134+
135+
#[tokio::test]
136+
async fn test_server_get_specs() {
137+
let temp_dir = TempDir::new().unwrap();
138+
create_test_project(temp_dir.path());
139+
140+
let state = create_empty_state().await;
141+
let (addr, handle) = start_test_server(state).await;
142+
143+
let client = Client::new();
144+
145+
// Add project
146+
let add_response = client
147+
.post(format!("http://{}/api/projects", addr))
148+
.json(&serde_json::json!({ "path": temp_dir.path().to_string_lossy() }))
149+
.send()
150+
.await
151+
.unwrap();
152+
153+
let add_body: serde_json::Value = add_response.json().await.unwrap();
154+
let project_id = add_body["project"]["id"].as_str().unwrap();
155+
156+
// Get specs
157+
let specs_response = client
158+
.get(format!("http://{}/api/projects/{}/specs", addr, project_id))
159+
.send()
160+
.await
161+
.unwrap();
162+
163+
assert_eq!(specs_response.status(), StatusCode::OK);
164+
let specs_body: serde_json::Value = specs_response.json().await.unwrap();
165+
assert!(specs_body["specs"].is_array());
166+
167+
// Cleanup
168+
handle.abort();
169+
}
170+
171+
#[tokio::test]
172+
async fn test_server_cors_headers() {
173+
let temp_dir = TempDir::new().unwrap();
174+
let state = create_test_state(&temp_dir).await;
175+
let (addr, handle) = start_test_server(state).await;
176+
177+
let client = Client::new();
178+
let response = client
179+
.get(format!("http://{}/api/projects", addr))
180+
.header("Origin", "http://localhost:3000")
181+
.send()
182+
.await
183+
.unwrap();
184+
185+
// CORS should be enabled by default - check response headers
186+
assert!(response
187+
.headers()
188+
.get("access-control-allow-origin")
189+
.is_some());
190+
191+
// Cleanup
192+
handle.abort();
193+
}
194+
195+
#[tokio::test]
196+
async fn test_server_error_handling() {
197+
let temp_dir = TempDir::new().unwrap();
198+
let state = create_test_state(&temp_dir).await;
199+
let (addr, handle) = start_test_server(state).await;
200+
201+
let client = Client::new();
202+
203+
// Try to get a non-existent project
204+
let response = client
205+
.get(format!("http://{}/api/projects/nonexistent-id/specs", addr))
206+
.send()
207+
.await
208+
.unwrap();
209+
210+
assert_eq!(response.status(), StatusCode::NOT_FOUND);
211+
212+
// Cleanup
213+
handle.abort();
214+
}
215+
216+
#[tokio::test]
217+
async fn test_server_concurrent_requests() {
218+
let temp_dir = TempDir::new().unwrap();
219+
let state = create_test_state(&temp_dir).await;
220+
let (addr, handle) = start_test_server(state).await;
221+
222+
let client = Client::new();
223+
224+
// Make multiple concurrent requests
225+
let mut tasks = vec![];
226+
for _ in 0..10 {
227+
let client = client.clone();
228+
let url = format!("http://{}/health", addr);
229+
tasks.push(tokio::spawn(async move {
230+
client.get(&url).send().await.unwrap()
231+
}));
232+
}
233+
234+
// Wait for all requests to complete
235+
for task in tasks {
236+
let response = task.await.unwrap();
237+
assert_eq!(response.status(), StatusCode::OK);
238+
}
239+
240+
// Cleanup
241+
handle.abort();
242+
}

0 commit comments

Comments
 (0)