Skip to content

Commit b636853

Browse files
authored
Merge pull request #109 from codervisor/copilot/implement-spec-191
Strengthen Rust HTTP API integration coverage for spec 191
2 parents 6e01665 + d704a83 commit b636853

File tree

2 files changed

+238
-8
lines changed

2 files changed

+238
-8
lines changed

rust/leanspec-http/tests/integration_tests.rs

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
55
use axum::http::StatusCode;
66
use leanspec_http::{create_router, AppState, ServerConfig};
7+
use serde_json::Value;
78
use std::fs;
89
use tempfile::TempDir;
910

@@ -96,6 +97,35 @@ This spec is complete.
9697
.unwrap();
9798
}
9899

100+
/// Create a project with an invalid spec for validation tests
101+
fn create_invalid_project(dir: &std::path::Path) {
102+
let specs_dir = dir.join("specs");
103+
fs::create_dir_all(&specs_dir).unwrap();
104+
105+
let spec_dir = specs_dir.join("004-invalid-spec");
106+
fs::create_dir_all(&spec_dir).unwrap();
107+
fs::write(
108+
spec_dir.join("README.md"),
109+
r#"---
110+
status: planned
111+
created: '2025-02-28'
112+
priority: medium
113+
tags:
114+
- invalid
115+
depends_on:
116+
- "" # intentionally empty to trigger validation error
117+
---
118+
119+
# Invalid Spec
120+
121+
## Overview
122+
123+
Empty dependency should surface validation errors.
124+
"#,
125+
)
126+
.unwrap();
127+
}
128+
99129
/// Create a test state with a project (async version)
100130
async fn create_test_state(temp_dir: &TempDir) -> AppState {
101131
create_test_project(temp_dir.path());
@@ -192,6 +222,75 @@ async fn test_list_projects() {
192222
assert!(body.contains("projects"));
193223
}
194224

225+
#[tokio::test]
226+
async fn test_add_project_and_get_detail() {
227+
let temp_dir = TempDir::new().unwrap();
228+
create_test_project(temp_dir.path());
229+
230+
let config = ServerConfig::default();
231+
let registry = leanspec_http::ProjectRegistry::default();
232+
let state = AppState::with_registry(config, registry);
233+
let app = create_router(state);
234+
235+
let (status, body) = make_json_request(
236+
app.clone(),
237+
"POST",
238+
"/api/projects",
239+
&serde_json::json!({ "path": temp_dir.path().to_string_lossy() }).to_string(),
240+
)
241+
.await;
242+
243+
assert_eq!(status, StatusCode::OK);
244+
let project: Value = serde_json::from_str(&body).unwrap();
245+
let project_id = project["id"].as_str().unwrap();
246+
assert!(project.get("specsDir").is_some());
247+
assert!(project.get("lastAccessed").is_some());
248+
assert!(project.get("addedAt").is_some());
249+
250+
let (status, body) = make_request(app.clone(), "GET", "/api/projects").await;
251+
assert_eq!(status, StatusCode::OK);
252+
let projects: Value = serde_json::from_str(&body).unwrap();
253+
assert_eq!(projects["projects"].as_array().unwrap().len(), 1);
254+
assert_eq!(projects["currentProjectId"].as_str(), Some(project_id));
255+
}
256+
257+
#[tokio::test]
258+
async fn test_update_project_and_toggle_favorite() {
259+
let temp_dir = TempDir::new().unwrap();
260+
let state = create_test_state(&temp_dir).await;
261+
let app = create_router(state);
262+
263+
let (status, body) = make_request(app.clone(), "GET", "/api/projects").await;
264+
assert_eq!(status, StatusCode::OK);
265+
let projects: Value = serde_json::from_str(&body).unwrap();
266+
let project_id = projects["projects"][0]["id"].as_str().unwrap();
267+
268+
let (status, body) = make_json_request(
269+
app.clone(),
270+
"PATCH",
271+
&format!("/api/projects/{project_id}"),
272+
&serde_json::json!({
273+
"name": "Updated Project",
274+
"favorite": true,
275+
"color": "#ffcc00"
276+
})
277+
.to_string(),
278+
)
279+
.await;
280+
281+
assert_eq!(status, StatusCode::OK);
282+
let updated: Value = serde_json::from_str(&body).unwrap();
283+
assert_eq!(updated["name"], "Updated Project");
284+
assert_eq!(updated["favorite"], true);
285+
assert_eq!(updated["color"], "#ffcc00");
286+
287+
let (status, body) =
288+
make_request(app.clone(), "POST", &format!("/api/projects/{project_id}/favorite")).await;
289+
assert_eq!(status, StatusCode::OK);
290+
let toggled: Value = serde_json::from_str(&body).unwrap();
291+
assert_eq!(toggled["favorite"], false);
292+
}
293+
195294
#[tokio::test]
196295
async fn test_specs_without_project_selected() {
197296
let config = ServerConfig::default();
@@ -220,6 +319,25 @@ async fn test_list_specs_with_project() {
220319
assert!(body.contains("003-complete-spec"));
221320
}
222321

322+
#[tokio::test]
323+
async fn test_list_specs_filters_and_camelcase() {
324+
let temp_dir = TempDir::new().unwrap();
325+
let state = create_test_state(&temp_dir).await;
326+
let app = create_router(state);
327+
328+
let (status, body) = make_request(app.clone(), "GET", "/api/specs?status=in-progress").await;
329+
330+
assert_eq!(status, StatusCode::OK);
331+
let specs: Value = serde_json::from_str(&body).unwrap();
332+
assert_eq!(specs["total"], 1);
333+
let spec = &specs["specs"][0];
334+
assert_eq!(spec["status"], "in-progress");
335+
assert!(spec.get("specNumber").is_some());
336+
assert!(spec.get("specName").is_some());
337+
assert!(spec.get("filePath").is_some());
338+
assert!(spec.get("spec_name").is_none());
339+
}
340+
223341
#[tokio::test]
224342
async fn test_get_spec_detail() {
225343
let temp_dir = TempDir::new().unwrap();
@@ -234,6 +352,66 @@ async fn test_get_spec_detail() {
234352
assert!(body.contains("contentMd"));
235353
}
236354

355+
#[tokio::test]
356+
async fn test_switch_project_and_refresh_cleanup() {
357+
let first_project = TempDir::new().unwrap();
358+
let second_project = TempDir::new().unwrap();
359+
create_test_project(first_project.path());
360+
create_test_project(second_project.path());
361+
362+
let config = ServerConfig::default();
363+
let registry = leanspec_http::ProjectRegistry::default();
364+
let state = AppState::with_registry(config, registry);
365+
let app = create_router(state);
366+
367+
let (_, body) = make_json_request(
368+
app.clone(),
369+
"POST",
370+
"/api/projects",
371+
&serde_json::json!({ "path": first_project.path().to_string_lossy() }).to_string(),
372+
)
373+
.await;
374+
let first_id = serde_json::from_str::<Value>(&body).unwrap()["id"]
375+
.as_str()
376+
.unwrap()
377+
.to_string();
378+
379+
let (_, body) = make_json_request(
380+
app.clone(),
381+
"POST",
382+
"/api/projects",
383+
&serde_json::json!({ "path": second_project.path().to_string_lossy() }).to_string(),
384+
)
385+
.await;
386+
let second_id = serde_json::from_str::<Value>(&body).unwrap()["id"]
387+
.as_str()
388+
.unwrap()
389+
.to_string();
390+
391+
let (_, body) = make_request(app.clone(), "GET", "/api/projects").await;
392+
let projects: Value = serde_json::from_str(&body).unwrap();
393+
assert_eq!(projects["currentProjectId"].as_str(), Some(second_id.as_str()));
394+
395+
let (status, body) =
396+
make_request(app.clone(), "POST", &format!("/api/projects/{first_id}/switch")).await;
397+
assert_eq!(status, StatusCode::OK);
398+
let switched: Value = serde_json::from_str(&body).unwrap();
399+
assert_eq!(switched["id"], first_id);
400+
401+
assert!(fs::remove_dir_all(second_project.path()).is_ok());
402+
std::thread::sleep(std::time::Duration::from_millis(10));
403+
assert!(!second_project.path().exists());
404+
let (status, body) = make_request(app.clone(), "POST", "/api/projects/refresh").await;
405+
assert_eq!(status, StatusCode::OK);
406+
let refresh: Value = serde_json::from_str(&body).unwrap();
407+
assert_eq!(refresh["removed"].as_u64(), Some(1));
408+
409+
let (_, body) = make_request(app.clone(), "GET", "/api/projects").await;
410+
let projects: Value = serde_json::from_str(&body).unwrap();
411+
assert_eq!(projects["projects"].as_array().unwrap().len(), 1);
412+
assert_eq!(projects["currentProjectId"].as_str(), Some(first_id.as_str()));
413+
}
414+
237415
#[tokio::test]
238416
async fn test_search_specs() {
239417
let temp_dir = TempDir::new().unwrap();
@@ -266,6 +444,30 @@ async fn test_get_stats() {
266444
assert!(body.contains("byPriority"));
267445
}
268446

447+
#[tokio::test]
448+
async fn test_stats_camel_case_structure_and_counts() {
449+
let temp_dir = TempDir::new().unwrap();
450+
let state = create_test_state(&temp_dir).await;
451+
let app = create_router(state);
452+
453+
let (status, body) = make_request(app.clone(), "GET", "/api/stats").await;
454+
455+
assert_eq!(status, StatusCode::OK);
456+
let stats: Value = serde_json::from_str(&body).unwrap();
457+
assert_eq!(stats["total"], 3);
458+
459+
let by_status = stats["byStatus"].as_object().unwrap();
460+
assert_eq!(by_status.get("planned").and_then(|v| v.as_u64()), Some(1));
461+
assert_eq!(by_status.get("inProgress").and_then(|v| v.as_u64()), Some(1));
462+
assert_eq!(by_status.get("complete").and_then(|v| v.as_u64()), Some(1));
463+
assert!(by_status.get("in_progress").is_none());
464+
465+
let by_priority = stats["byPriority"].as_object().unwrap();
466+
assert_eq!(by_priority.get("high").and_then(|v| v.as_u64()), Some(1));
467+
assert_eq!(by_priority.get("medium").and_then(|v| v.as_u64()), Some(1));
468+
assert_eq!(by_priority.get("low").and_then(|v| v.as_u64()), Some(1));
469+
}
470+
269471
#[tokio::test]
270472
async fn test_get_dependencies() {
271473
let temp_dir = TempDir::new().unwrap();
@@ -292,6 +494,28 @@ async fn test_validate_all() {
292494
assert!(body.contains("issues"));
293495
}
294496

497+
#[tokio::test]
498+
async fn test_validate_detects_invalid_frontmatter() {
499+
let temp_dir = TempDir::new().unwrap();
500+
create_invalid_project(temp_dir.path());
501+
502+
let config = ServerConfig::default();
503+
let registry = leanspec_http::ProjectRegistry::default();
504+
let state = AppState::with_registry(config, registry);
505+
{
506+
let mut reg = state.registry.write().await;
507+
let _ = reg.add(temp_dir.path());
508+
}
509+
510+
let app = create_router(state);
511+
let (status, body) = make_request(app, "GET", "/api/validate").await;
512+
513+
assert_eq!(status, StatusCode::OK);
514+
let validation: Value = serde_json::from_str(&body).unwrap();
515+
assert_eq!(validation["isValid"], false);
516+
assert!(!validation["issues"].as_array().unwrap().is_empty());
517+
}
518+
295519
#[tokio::test]
296520
async fn test_validate_single_spec() {
297521
let temp_dir = TempDir::new().unwrap();

specs/191-rust-http-api-test-suite/README.md

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
---
2-
status: planned
3-
created: 2025-12-19
2+
status: in-progress
3+
created: '2025-12-19'
44
priority: high
55
tags:
6-
- rust
7-
- http
8-
- testing
9-
- api
10-
created_at: 2025-12-19T06:33:51.382148Z
11-
updated_at: 2025-12-19T06:33:51.382148Z
6+
- rust
7+
- http
8+
- testing
9+
- api
10+
created_at: '2025-12-19T06:33:51.382148Z'
11+
updated_at: '2025-12-20T01:50:43.965Z'
12+
transitions:
13+
- status: in-progress
14+
at: '2025-12-20T01:50:43.965Z'
1215
---
1316

1417
# Rust HTTP API Test Suite
1518

19+
> **Status**: ⏳ In progress · **Priority**: High · **Created**: 2025-12-19 · **Tags**: rust, http, testing, api
20+
21+
1622
> Comprehensive integration test suite for Rust HTTP server before UI migration
1723
1824
## Overview

0 commit comments

Comments
 (0)