use parse_rust_mongo::MongoAdapter;
use parse_rust_server::{AppState, ServerConfig};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
async fn boot() -> String {
let config = ServerConfig::new("test", "test")
.rest_api_key("rest")
.mount_path("/parse");
let db = format!("parse_rust_srv_{}", std::process::id());
let storage = MongoAdapter::connect("mongodb://127.0.0.1:27017", &db)
.await
.expect("building a lazy Mongo client cannot fail for a valid URI");
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 0));
let listener = tokio::net::TcpListener::bind(addr).await.expect("bind");
let bound = listener.local_addr().expect("local_addr");
let app = parse_rust_server::router(AppState::new(config, storage));
tokio::spawn(async move { axum::serve(listener, app).await });
bound.to_string()
}
async fn get(host: &str, path: &str, headers: &[(&str, &str)]) -> (u16, String) {
let mut req = format!("GET {path} HTTP/1.1\r\nHost: {host}\r\n");
for (k, v) in headers {
req.push_str(&format!("{k}: {v}\r\n"));
}
req.push_str("Connection: close\r\n\r\n");
let mut s = tokio::net::TcpStream::connect(host).await.expect("connect");
s.write_all(req.as_bytes()).await.expect("write");
let mut buf = String::new();
s.read_to_string(&mut buf).await.expect("read");
let status = buf
.split_whitespace()
.nth(1)
.and_then(|c| c.parse().ok())
.expect("status line");
let body = buf.split("\r\n\r\n").nth(1).unwrap_or("").to_string();
(status, body)
}
fn json(body: &str) -> serde_json::Value {
serde_json::from_str(body).unwrap_or_else(|e| panic!("body was not JSON: {e}: {body}"))
}
#[tokio::test]
async fn server_info_with_master_key() {
let host = boot().await;
let (status, body) = get(
&host,
"/parse/serverInfo",
&[
("X-Parse-Application-Id", "test"),
("X-Parse-REST-API-Key", "rest"),
("X-Parse-Master-Key", "test"),
],
)
.await;
assert_eq!(status, 200, "body: {body}");
let v = json(&body);
assert!(v.get("features").is_some(), "features missing");
assert!(
v.get("parseServerVersion").is_some(),
"parseServerVersion missing"
);
assert_eq!(
v["features"]["schemas"]["exportClass"],
serde_json::json!(false)
);
}
#[tokio::test]
async fn unimplemented_capabilities_are_not_advertised() {
let host = boot().await;
let (status, body) = get(
&host,
"/parse/serverInfo",
&[
("X-Parse-Application-Id", "test"),
("X-Parse-REST-API-Key", "rest"),
("X-Parse-Master-Key", "test"),
],
)
.await;
assert_eq!(status, 200, "body: {body}");
let v = json(&body);
for (subsystem, path) in [
("hooks", ["hooks", "create"]),
("global config", ["globalConfig", "read"]),
("the log API", ["logs", "level"]),
("cloud jobs", ["cloudCode", "jobs"]),
("push audiences", ["push", "pushAudiences"]),
] {
assert_eq!(
v["features"][path[0]][path[1]],
serde_json::json!(false),
"features.{}.{} advertises {} , which has no route. Either the subsystem landed and \
this test should be updated in that commit, or a client is being told about a \
capability that will 404.",
path[0],
path[1],
subsystem,
);
}
for capability in [
"addField",
"removeField",
"addClass",
"removeClass",
"clearAllDataFromClass",
"editClassLevelPermissions",
"editPointerPermissions",
] {
assert_eq!(
v["features"]["schemas"][capability],
serde_json::json!(true),
"the schema API landed in 0.2.0, so features.schemas.{capability} is advertised"
);
}
}
#[tokio::test]
async fn server_info_without_master_key_is_permission_denied() {
let host = boot().await;
let (status, body) = get(
&host,
"/parse/serverInfo",
&[
("X-Parse-Application-Id", "test"),
("X-Parse-REST-API-Key", "rest"),
],
)
.await;
assert_eq!(status, 403, "body: {body}");
let v = json(&body);
assert_eq!(v["error"], serde_json::json!("Permission denied"));
assert!(v.get("code").is_none(), "must not carry a code: {body}");
}
#[tokio::test]
async fn the_header_layer_rejects_differently_than_the_master_key_gate() {
let host = boot().await;
let (status, body) = get(
&host,
"/parse/serverInfo",
&[("X-Parse-Master-Key", "test")], )
.await;
assert_eq!(status, 403);
assert_eq!(json(&body)["error"], serde_json::json!("unauthorized"));
}
#[tokio::test]
async fn a_configured_client_key_is_required_for_non_master_requests() {
let host = boot().await;
let (status, body) = get(
&host,
"/parse/serverInfo",
&[("X-Parse-Application-Id", "test")], )
.await;
assert_eq!(status, 403);
assert_eq!(json(&body)["error"], serde_json::json!("unauthorized"));
}
#[tokio::test]
async fn health_needs_no_credentials() {
let host = boot().await;
let (status, body) = get(&host, "/parse/health", &[]).await;
assert_eq!(status, 200, "body: {body}");
assert_eq!(json(&body)["status"], serde_json::json!("ok"));
}
#[tokio::test]
async fn nothing_is_served_off_the_mount_path() {
let host = boot().await;
let (status, _) = get(
&host,
"/serverInfo",
&[
("X-Parse-Application-Id", "test"),
("X-Parse-Master-Key", "test"),
],
)
.await;
assert_eq!(
status, 404,
"route must not be reachable off the mount path"
);
}