#![allow(dead_code)]
use parse_rust_mongo::MongoAdapter;
use parse_rust_server::{AppState, ServerConfig};
use serde_json::Value;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
pub const APP_ID: &str = "test";
pub const MASTER_KEY: &str = "test";
pub const MAINTENANCE_KEY: &str = "maint";
pub const REST_KEY: &str = "rest";
pub const JS_KEY: &str = "js";
pub struct Server {
pub host: String,
pub database: String,
}
const TEST_ALLOW_CLIENT_CLASS_CREATION: bool = true;
pub async fn boot() -> Server {
let database = format!(
"parse_rust_it_{}_{}",
std::process::id(),
next_database_ordinal()
);
let server = boot_on(&database, base_config()).await;
Server {
host: server,
database,
}
}
pub async fn reboot(database: &str) -> String {
boot_on(database, base_config()).await
}
fn base_config() -> ServerConfig {
let mut config = ServerConfig::new(APP_ID, MASTER_KEY);
config.maintenance_key = Some(MAINTENANCE_KEY.to_string());
config.allow_client_class_creation = TEST_ALLOW_CLIENT_CLASS_CREATION;
config
}
pub async fn boot_with(database: &str, config: ServerConfig) -> String {
boot_on(database, config).await
}
pub async fn boot_fresh_with(mut build: impl FnMut(ServerConfig) -> ServerConfig) -> Server {
let database = format!(
"parse_rust_it_{}_{}",
std::process::id(),
next_database_ordinal()
);
let config = build(base_config());
let host = boot_on(&database, config).await;
Server { host, database }
}
async fn boot_on(database: &str, config: ServerConfig) -> String {
let config = config
.rest_api_key(REST_KEY)
.javascript_key(JS_KEY)
.mount_path("/parse");
let storage = MongoAdapter::connect("mongodb://127.0.0.1:27017", database)
.await
.expect("MongoDB must be running on 27017");
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 0));
let (bound, server) = parse_rust_server::serve(AppState::new(config, storage), addr)
.await
.expect("bind failed");
tokio::spawn(server);
bound.to_string()
}
pub async fn index_names(database: &str, collection: &str) -> Vec<String> {
let client = mongodb::Client::with_uri_str("mongodb://127.0.0.1:27017")
.await
.expect("MongoDB must be running on 27017");
client
.database(database)
.collection::<bson::Document>(collection)
.list_index_names()
.await
.unwrap_or_default()
}
fn next_database_ordinal() -> u64 {
use std::sync::atomic::{AtomicU64, Ordering};
static NEXT: AtomicU64 = AtomicU64::new(0);
static SEED: std::sync::OnceLock<u64> = std::sync::OnceLock::new();
let seed = *SEED.get_or_init(|| {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0)
});
seed.wrapping_add(NEXT.fetch_add(1, Ordering::Relaxed))
}
pub struct Response {
pub status: u16,
pub body: Value,
pub raw: String,
}
impl Response {
pub fn code(&self) -> Option<i64> {
self.body.get("code").and_then(Value::as_i64)
}
pub fn error(&self) -> String {
self.body
.get("error")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string()
}
pub fn results(&self) -> Vec<Value> {
self.body
.get("results")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default()
}
}
#[derive(Debug, Clone, Default)]
pub struct As {
pub master: bool,
pub maintenance: bool,
pub session_token: Option<String>,
pub no_headers: bool,
pub extra_headers: Vec<(String, String)>,
}
impl As {
pub fn anonymous() -> Self {
Self::default()
}
pub fn anonymous_without_keys() -> Self {
Self {
no_headers: true,
..Self::default()
}
}
pub fn master() -> Self {
Self {
master: true,
..Self::default()
}
}
pub fn maintenance() -> Self {
Self {
maintenance: true,
..Self::default()
}
}
pub fn user(token: &str) -> Self {
Self {
session_token: Some(token.to_string()),
..Self::default()
}
}
pub fn with_header(mut self, name: &str, value: &str) -> Self {
self.extra_headers
.push((name.to_string(), value.to_string()));
self
}
}
pub async fn request(
host: &str,
method: &str,
path: &str,
who: &As,
body: Option<&Value>,
) -> Response {
let mut headers = if who.no_headers {
Vec::new()
} else {
vec![
("X-Parse-Application-Id".to_string(), APP_ID.to_string()),
("X-Parse-REST-API-Key".to_string(), REST_KEY.to_string()),
]
};
if who.master {
headers.push(("X-Parse-Master-Key".to_string(), MASTER_KEY.to_string()));
}
if who.maintenance {
headers.push((
"X-Parse-Maintenance-Key".to_string(),
MAINTENANCE_KEY.to_string(),
));
}
if let Some(token) = &who.session_token {
headers.push(("X-Parse-Session-Token".to_string(), token.clone()));
}
headers.extend(who.extra_headers.iter().cloned());
let payload = body.map(|b| serde_json::to_string(b).expect("serialize"));
let mut req = format!("{method} /parse{path} HTTP/1.1\r\nHost: {host}\r\n");
for (k, v) in &headers {
req.push_str(&format!("{k}: {v}\r\n"));
}
if let Some(payload) = &payload {
req.push_str("Content-Type: application/json\r\n");
req.push_str(&format!("Content-Length: {}\r\n", payload.len()));
}
req.push_str("Connection: close\r\n\r\n");
if let Some(payload) = &payload {
req.push_str(payload);
}
let mut socket = tokio::net::TcpStream::connect(host).await.expect("connect");
socket
.write_all(req.as_bytes())
.await
.expect("write request");
let mut raw = String::new();
socket
.read_to_string(&mut raw)
.await
.expect("read response");
let status = raw
.split_whitespace()
.nth(1)
.and_then(|c| c.parse().ok())
.unwrap_or_else(|| panic!("no status line in: {raw}"));
let text = raw.split("\r\n\r\n").nth(1).unwrap_or("").to_string();
let body = serde_json::from_str(&text).unwrap_or(Value::Null);
Response { status, body, raw }
}
pub async fn get(host: &str, path: &str, who: &As) -> Response {
request(host, "GET", path, who, None).await
}
pub async fn post(host: &str, path: &str, who: &As, body: &Value) -> Response {
request(host, "POST", path, who, Some(body)).await
}
pub async fn put(host: &str, path: &str, who: &As, body: &Value) -> Response {
request(host, "PUT", path, who, Some(body)).await
}
pub async fn delete(host: &str, path: &str, who: &As) -> Response {
request(host, "DELETE", path, who, None).await
}
pub async fn signup(host: &str, username: &str, password: &str) -> (String, String) {
let response = post(
host,
"/users",
&As::anonymous(),
&serde_json::json!({ "username": username, "password": password }),
)
.await;
assert_eq!(response.status, 201, "signup failed: {}", response.raw);
(
response.body["objectId"]
.as_str()
.expect("objectId")
.to_string(),
response.body["sessionToken"]
.as_str()
.expect("sessionToken")
.to_string(),
)
}
pub fn where_query(value: Value) -> String {
format!("?where={}", urlencode(&value.to_string()))
}
pub fn urlencode(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for byte in s.bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
out.push(byte as char)
}
_ => out.push_str(&format!("%{byte:02X}")),
}
}
out
}
pub async fn create_unique_index(database: &str, collection: &str, field: &str) {
use mongodb::options::IndexOptions;
use mongodb::{Client, IndexModel};
let client = Client::with_uri_str("mongodb://127.0.0.1:27017")
.await
.expect("mongo client");
client
.database(database)
.collection::<bson::Document>(collection)
.create_index(
IndexModel::builder()
.keys(bson::doc! { field: 1 })
.options(IndexOptions::builder().unique(true).sparse(true).build())
.build(),
)
.await
.expect("create unique index");
}