Files
kosync-rs/tests/api.rs
2026-06-12 09:42:23 +00:00

265 lines
6.9 KiB
Rust

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<Value>,
) -> (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" }));
}