use axum::body::{to_bytes, Body}; use axum::http::{Request, StatusCode}; use serde_json::{json, Value}; use tempfile::TempDir; use tower::ServiceExt; use kosync_rs::http::{router, AppState}; use kosync_rs::store::Store; async fn test_app(registration_enabled: bool) -> (axum::Router, TempDir) { let tmp = TempDir::new().unwrap(); let db = tmp.path().join("kosync.sqlite3"); let store = Store::new(db); store.init().await.unwrap(); let app = router(AppState { store, registration_enabled, }); (app, tmp) } async fn hit( app: axum::Router, method: &str, path: &str, auth: Option<(&str, &str)>, body: Option, ) -> (StatusCode, Value) { let mut builder = Request::builder().method(method).uri(path); if let Some((username, key)) = auth { builder = builder .header("x-auth-user", username) .header("x-auth-key", key); } let request = if let Some(body) = body { builder .header("content-type", "application/json") .body(Body::from(body.to_string())) .unwrap() } else { builder.body(Body::empty()).unwrap() }; let response = app.oneshot(request).await.unwrap(); let status = response.status(); let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap(); let json = serde_json::from_slice(&bytes).unwrap(); (status, json) } async fn register(app: axum::Router, username: &str, password: &str) -> (StatusCode, Value) { hit( app, "POST", "/users/create", None, Some(json!({ "username": username, "password": password })), ) .await } async fn update( app: axum::Router, username: &str, key: &str, document: &str, percentage: Value, progress: Value, device: Value, ) -> (StatusCode, Value) { hit( app, "PUT", "/syncs/progress", Some((username, key)), Some(json!({ "document": document, "percentage": percentage, "progress": progress, "device": device })), ) .await } #[tokio::test] async fn register_and_reject_duplicates() { let (app, _tmp) = test_app(true).await; let (status, body) = register(app.clone(), "new-user", "passwd123").await; assert_eq!(status, StatusCode::CREATED); assert_eq!(body, json!({ "username": "new-user" })); let (status, body) = register(app, "new-user", "passwd123").await; assert_eq!(status, StatusCode::PAYMENT_REQUIRED); assert_eq!( body, json!({ "code": 2002, "message": "Username is already registered." }) ); } #[tokio::test] async fn registration_can_be_disabled() { let (app, _tmp) = test_app(false).await; let (status, body) = register(app, "new-user", "passwd123").await; assert_eq!(status, StatusCode::PAYMENT_REQUIRED); assert_eq!( body, json!({ "code": 2005, "message": "User registration is disabled." }) ); } #[tokio::test] async fn auth_matches_original_behavior() { let (app, _tmp) = test_app(true).await; register(app.clone(), "user1", "passwd123").await; let (status, body) = hit(app.clone(), "GET", "/users/auth", Some(("user1", "")), None).await; assert_eq!(status, StatusCode::UNAUTHORIZED); assert_eq!(body, json!({ "code": 2001, "message": "Unauthorized" })); let (status, body) = hit( app.clone(), "GET", "/users/auth", Some(("user1", "wrong_password")), None, ) .await; assert_eq!(status, StatusCode::UNAUTHORIZED); assert_eq!(body, json!({ "code": 2001, "message": "Unauthorized" })); let (status, body) = hit( app, "GET", "/users/auth", Some(("user1", "passwd123")), None, ) .await; assert_eq!(status, StatusCode::OK); assert_eq!(body, json!({ "authorized": "OK" })); } #[tokio::test] async fn progress_round_trip_and_last_write_wins() { let (app, _tmp) = test_app(true).await; register(app.clone(), "user1", "passwd123").await; let (status, body) = update( app.clone(), "user1", "wrong", "89isjkdaj9j", json!(0.32), json!("56"), json!("my kpw"), ) .await; assert_eq!(status, StatusCode::UNAUTHORIZED); assert_eq!(body, json!({ "code": 2001, "message": "Unauthorized" })); let (status, body) = update( app.clone(), "user1", "passwd123", "89isjkdaj9j", json!(0.32), json!("56"), json!("my kpw"), ) .await; assert_eq!(status, StatusCode::OK); assert_eq!(body["document"], "89isjkdaj9j"); assert!(body["timestamp"].as_i64().is_some()); let (status, body) = hit( app.clone(), "GET", "/syncs/progress/89isjkdaj9jnon_existent", Some(("user1", "passwd123")), None, ) .await; assert_eq!(status, StatusCode::OK); assert_eq!(body, json!({})); update( app.clone(), "user1", "passwd123", "89isjkdaj9j", json!(0.22), json!("36"), json!("my pb"), ) .await; let (status, mut body) = hit( app, "GET", "/syncs/progress/89isjkdaj9j", Some(("user1", "passwd123")), None, ) .await; assert_eq!(status, StatusCode::OK); assert!(body["timestamp"].as_i64().is_some()); body.as_object_mut().unwrap().remove("timestamp"); assert_eq!( body, json!({ "document": "89isjkdaj9j", "percentage": 0.22, "progress": "36", "device": "my pb" }) ); } #[tokio::test] async fn validation_errors_match_compatibility_contract() { let (app, _tmp) = test_app(true).await; register(app.clone(), "user1", "passwd123").await; let (status, body) = register(app.clone(), "bad:user", "passwd123").await; assert_eq!(status, StatusCode::FORBIDDEN); assert_eq!(body, json!({ "code": 2003, "message": "Invalid request" })); let (status, body) = update( app.clone(), "user1", "passwd123", "bad:doc", json!(0), json!(""), json!(""), ) .await; assert_eq!(status, StatusCode::FORBIDDEN); assert_eq!( body, json!({ "code": 2004, "message": "Field 'document' not provided." }) ); let (status, body) = update( app, "user1", "passwd123", "doc", json!("not-a-number"), json!(""), json!(""), ) .await; assert_eq!(status, StatusCode::FORBIDDEN); assert_eq!(body, json!({ "code": 2003, "message": "Invalid request" })); } #[tokio::test] async fn healthcheck_returns_ok() { let (app, _tmp) = test_app(true).await; let (status, body) = hit(app, "GET", "/healthcheck", None, None).await; assert_eq!(status, StatusCode::OK); assert_eq!(body, json!({ "state": "OK" })); }