265 lines
6.9 KiB
Rust
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" }));
|
|
}
|