Files
huly-platform/foundations/hulypulse/tests/rest_api.rs
T
Denis Bykhov 43fb903924 Add 'foundations/hulypulse/' from commit '27e5a23ee935ac15cdfdf032cd649644390572a5'
git-subtree-dir: foundations/hulypulse
git-subtree-mainline: 64e456e0ec
git-subtree-split: 27e5a23ee9
2025-11-26 22:57:14 +05:00

331 lines
9.9 KiB
Rust

use core::panic;
use reqwest::StatusCode;
use serde_json::Value;
use std::env;
#[derive(Debug)]
struct ApiResponse {
code: u16,
#[allow(dead_code)]
headers: reqwest::header::HeaderMap,
etag: String,
text: String,
json: Value,
}
fn server_url() -> String {
env::var("TEST_SERVER_URL").unwrap_or_else(|_| "http://127.0.0.1/api".to_string())
}
#[allow(dead_code)]
fn etag(data: &str) -> String {
format!("{:x}", md5::compute(data))
}
#[allow(dead_code)]
async fn status(base: &str, client: &reqwest::Client) -> () {
let status_url = format!("{}/status", &base);
let resp = client.get(&status_url).send().await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let text = resp.text().await.unwrap();
let json: Value = serde_json::from_str(&text).unwrap();
assert_eq!(json["backend"], "memory");
assert_eq!(json["status"], "OK");
assert!(json.get("memory_info").is_some());
assert!(json.get("websockets").is_some());
// println!("Status: {}", text);
}
#[allow(dead_code)]
async fn get_not(base: &str, client: &reqwest::Client, workspace: &str, key: &str) -> () {
let r = engine("get", base, client, workspace, key, "", &[]).await;
assert!(r.code == 404);
}
#[allow(dead_code)]
async fn get(base: &str, client: &reqwest::Client, workspace: &str, key: &str) -> () {
let r = engine("get", base, client, workspace, key, "", &[]).await;
assert!(r.code == 200);
}
#[allow(dead_code)]
async fn get_data(base: &str, client: &reqwest::Client, workspace: &str, key: &str) -> String {
let r = engine("get", base, client, workspace, key, "", &[]).await;
assert!(r.code == 200);
r.json["data"].as_str().unwrap_or("").to_string()
}
#[allow(dead_code)]
async fn get_ttl(base: &str, client: &reqwest::Client, workspace: &str, key: &str) -> u64 {
let r = engine("get", base, client, workspace, key, "", &[]).await;
assert!(r.code == 200);
r.json["expires_at"].as_u64().unwrap_or(0)
}
#[allow(dead_code)]
async fn get_etag(base: &str, client: &reqwest::Client, workspace: &str, key: &str) -> String {
let r = engine("get", base, client, workspace, key, "", &[]).await;
assert!(r.code == 200);
r.etag
}
#[allow(dead_code)]
async fn put(
base: &str,
client: &reqwest::Client,
workspace: &str,
key: &str,
data: &str,
headers: &[&str],
) -> () {
let r = engine("put", base, client, workspace, key, data, headers).await;
// assert!(r.code == 412);
assert!(r.code == 200);
assert!(r.text == "DONE");
}
async fn engine(
method: &str,
base: &str,
client: &reqwest::Client,
workspace: &str,
key: &str,
data: &str,
headers: &[&str],
) -> ApiResponse {
let url = format!("{}/api/{}/{}", base, workspace, key);
let req = client;
let mut req = match method {
"get" => req.get(&url),
"put" => req.put(&url).body(data.to_string()),
"delete" => req.delete(&url),
"list" => req.get(&url),
_ => panic!("Unknown method: {}", method),
};
if method == "put" {
req = req.body(data.to_string());
}
for h in headers {
if let Some((mut k, v)) = h.split_once(':') {
k = k.trim();
if k != "IF-MATCH" && k != "IF-NON-MATCH" && k != "HULY-TTL" {
panic!("Only `IF-MATCH`, `IF-NON-MATCH`, `HULY-TTL` headers allowed");
}
req = req.header(k, v.trim());
} else {
panic!("Unknown format for header: {}", h);
}
}
let resp = req.send().await.unwrap();
let code = resp.status();
let headers = resp.headers().clone();
let text = resp.text().await.unwrap();
ApiResponse {
code: code.as_u16(),
etag: headers
.get("ETag")
.and_then(|h| h.to_str().ok())
.unwrap_or("")
.to_string(),
text: text.clone(),
json: serde_json::from_str(&text).unwrap_or_default(),
headers: headers.clone(),
}
}
#[allow(dead_code)]
async fn delete_any(base: &str, client: &reqwest::Client, workspace: &str, key: &str) -> () {
engine("delete", base, client, workspace, key, "", &[]).await;
}
#[allow(dead_code)]
async fn delete(
base: &str,
client: &reqwest::Client,
workspace: &str,
key: &str,
headers: &[&str],
) -> () {
let r = engine("delete", base, client, workspace, key, "", headers).await;
assert_eq!(r.code, StatusCode::NO_CONTENT); // 204
assert!(r.text.is_empty());
}
#[allow(dead_code)]
async fn delete_not(
base: &str,
client: &reqwest::Client,
workspace: &str,
key: &str,
headers: &[&str],
) -> () {
let r = engine("delete", base, client, workspace, key, "", headers).await;
assert!(
(r.code == StatusCode::NOT_FOUND && r.text == "not found")
|| (!headers.is_empty()
&& r.code == StatusCode::PRECONDITION_FAILED
&& (r.text.contains("412 md5 mismatch")
|| r.text.contains("404 Equal: key does not exist")))
);
// println!("CODE: {}", r.code);
// println!("TEXT: {}", r.text);
}
#[tokio::test]
async fn put_and_get() {
let base = server_url();
// println!("Use url: {}", &base);
let client = reqwest::Client::builder()
.danger_accept_invalid_certs(true) // только на билдере!
.build()
.unwrap();
// Check status
status(&base, &client).await;
let workspace = "00000000-0000-0000-0000-000000000001";
let key = "TESTS/key1";
let data = "Value_1";
// test put/get/delete
delete_any(&base, &client, workspace, key).await;
get_not(&base, &client, workspace, key).await;
put(&base, &client, workspace, key, data, &[]).await;
get(&base, &client, workspace, key).await;
delete(&base, &client, workspace, key, &[]).await;
delete_not(&base, &client, workspace, key, &[]).await;
// test If-Match
delete_any(&base, &client, workspace, key).await;
// delete only tag
put(&base, &client, workspace, key, data, &["HULY-TTL: 2"]).await;
delete_not(
&base,
&client,
workspace,
key,
&["IF-MATCH: 11111111111111111111111111111111"],
)
.await;
get(&base, &client, workspace, key).await;
delete(
&base,
&client,
workspace,
key,
&["IF-MATCH: c7bcabf6b98a220f2f4888a18d01568d"],
)
.await;
get_not(&base, &client, workspace, key).await;
// update with tag
put(&base, &client, workspace, key, data, &["HULY-TTL: 2"]).await;
// replace with match
put(
&base,
&client,
workspace,
key,
"Another Data",
&["IF-MATCH: c7bcabf6b98a220f2f4888a18d01568d"],
)
.await;
assert!(get_data(&base, &client, workspace, key).await == "Another Data".to_string());
// replace with wrong mismatch
let r = engine(
"put",
&base,
&client,
&workspace,
&key,
"Next Another Data",
&["IF-MATCH: c7bcabf6b98a220f2f4888a18d01568d"],
)
.await;
assert!(r.code == StatusCode::PRECONDITION_FAILED && r.text.contains("412 md5 mismatch"));
// match: *
assert!(get_data(&base, &client, workspace, key).await == "Another Data".to_string());
put(
&base,
&client,
workspace,
key,
"Another Data 2",
&["IF-MATCH: *"],
)
.await;
assert!(get_data(&base, &client, workspace, key).await == "Another Data 2".to_string());
// unknown key matched
let fake_key = format!("{}{}", &key, "xxx");
let r = engine(
"put",
&base,
&client,
&workspace,
&fake_key,
"Next Another Data",
&["IF-MATCH: c7bcabf6b98a220f2f4888a18d01568d"],
)
.await;
assert!(r.code == StatusCode::NOT_FOUND && r.text.contains("404 Equal: key does not exist"));
// if not match
let data3 = "Another Data 3";
put(
&base,
&client,
&workspace,
&key,
&data3,
&["IF-NON-MATCH: c7bcabf6b98a220f2f4888a18d01568d"],
)
.await;
assert!(get_data(&base, &client, &workspace, &key).await == data3.to_string());
// DELETE if match
put(&base, &client, &workspace, &key, &data, &[]).await;
delete(
&base,
&client,
&workspace,
&key,
&["IF-MATCH: c7bcabf6b98a220f2f4888a18d01568d"],
)
.await;
put(&base, &client, &workspace, &key, &data, &[]).await;
delete(&base, &client, &workspace, &key, &["IF-MATCH: *"]).await;
get_not(&base, &client, &workspace, &key).await;
put(&base, &client, &workspace, &key, &data, &[]).await;
delete(&base, &client, &workspace, &key, &["IF-NON-MATCH: *"]).await; // TODO !!!
get_not(&base, &client, &workspace, &key).await;
put(&base, &client, &workspace, &key, &data, &[]).await;
let tag = get_etag(&base, &client, &workspace, &key).await;
assert!(tag == "c7bcabf6b98a220f2f4888a18d01568d".to_string());
delete(
&base,
&client,
&workspace,
&key,
&["IF-NON-MATCH: c7bcabf6b98a220f2f4888a18d01568d"],
)
.await;
get_not(&base, &client, &workspace, &key).await;
// let r = engine("put", &base, &client, &workspace, &key, "Next Another Data", &["HULY-TTL: 2","IF-NON-MATCH: c7bcabf6b98a220f2f4888a18d01568d"]).await;
// println!("ALL: {:?}", r);
// test TTL
delete_any(&base, &client, workspace, key).await;
put(&base, &client, workspace, key, data, &["HULY-TTL: 7"]).await;
let ttl = get_ttl(&base, &client, workspace, key).await;
assert!(ttl == 7 || ttl == 6, "TTL should be 7 (ok, may by 6)");
put(&base, &client, workspace, key, data, &["HULY-TTL: 1"]).await;
let ttl = get_ttl(&base, &client, workspace, key).await;
assert!(ttl == 1 || ttl == 0, "TTL should be 1 (ok, may by 0)");
// wait for 1.05 seconds
tokio::time::sleep(tokio::time::Duration::from_millis(1050)).await;
get_not(&base, &client, workspace, key).await;
}