170 lines
5.1 KiB
Rust
170 lines
5.1 KiB
Rust
use jsonwebtoken::{EncodingKey, Header, encode};
|
|
use reqwest::header::{HeaderMap, HeaderValue};
|
|
use serde::Serialize;
|
|
use std::{path::PathBuf, process::Command, time::Duration};
|
|
use uuid::Uuid;
|
|
|
|
fn repo_root() -> PathBuf {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.parent()
|
|
.and_then(|p| p.parent())
|
|
.expect("api crate should live under repo root")
|
|
.to_path_buf()
|
|
}
|
|
|
|
fn docker_enabled() -> bool {
|
|
std::env::var("CONTROL_TEST_DOCKER")
|
|
.ok()
|
|
.is_some_and(|v| v.trim() == "1")
|
|
}
|
|
|
|
fn compose_file() -> PathBuf {
|
|
repo_root().join("docker-compose.yml")
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct TestClaims {
|
|
sub: String,
|
|
session_id: String,
|
|
permissions: Vec<String>,
|
|
exp: usize,
|
|
}
|
|
|
|
fn make_token(secret: &[u8], perms: &[&str]) -> String {
|
|
let exp = (std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_secs()
|
|
+ 300) as usize;
|
|
encode(
|
|
&Header::default(),
|
|
&TestClaims {
|
|
sub: "user_1".to_string(),
|
|
session_id: "sess_1".to_string(),
|
|
permissions: perms.iter().map(|p| (*p).to_string()).collect(),
|
|
exp,
|
|
},
|
|
&EncodingKey::from_secret(secret),
|
|
)
|
|
.unwrap()
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn documents_upload_list_download_roundtrip_via_control_api_compose() {
|
|
if !docker_enabled() {
|
|
eprintln!("skipping: set CONTROL_TEST_DOCKER=1 to enable docker compose tests");
|
|
return;
|
|
}
|
|
|
|
// Must match docker-compose.yml CONTROL_GATEWAY_JWT_HS256_SECRET.
|
|
let jwt_secret = b"dev_secret";
|
|
let token = make_token(jwt_secret, &["control:read", "control:write"]);
|
|
|
|
let compose = compose_file();
|
|
|
|
let up = Command::new("docker")
|
|
.args(["compose", "-f"])
|
|
.arg(&compose)
|
|
.args(["up", "-d", "control-api"])
|
|
.status()
|
|
.expect("failed to run docker compose up control-api");
|
|
assert!(up.success(), "docker compose up control-api failed");
|
|
|
|
// Wait for control-api to be reachable (port publish is in compose).
|
|
let http = reqwest::Client::builder()
|
|
.timeout(Duration::from_secs(10))
|
|
.build()
|
|
.unwrap();
|
|
|
|
let base = "http://127.0.0.1:38080";
|
|
let health_deadline = tokio::time::Instant::now() + Duration::from_secs(30);
|
|
loop {
|
|
if tokio::time::Instant::now() > health_deadline {
|
|
panic!("control-api did not become healthy in time");
|
|
}
|
|
match http.get(format!("{base}/health")).send().await {
|
|
Ok(res) if res.status().is_success() => break,
|
|
_ => tokio::time::sleep(Duration::from_millis(250)).await,
|
|
}
|
|
}
|
|
|
|
let tenant_id = Uuid::new_v4().to_string();
|
|
let doc_type = "deployments";
|
|
let doc_id = Uuid::new_v4().to_string();
|
|
let filename = "hello.txt";
|
|
let bytes = b"hello-docs".to_vec();
|
|
|
|
let mut headers = HeaderMap::new();
|
|
headers.insert(
|
|
"authorization",
|
|
HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
|
|
);
|
|
headers.insert("x-tenant-id", HeaderValue::from_str(&tenant_id).unwrap());
|
|
|
|
// Upload (proxy endpoint).
|
|
let put_url =
|
|
format!("{base}/admin/v1/tenants/{tenant_id}/docs/{doc_type}/{doc_id}/{filename}");
|
|
let put = http
|
|
.put(&put_url)
|
|
.headers(headers.clone())
|
|
.header("content-type", "text/plain")
|
|
.body(bytes.clone())
|
|
.send()
|
|
.await
|
|
.expect("upload request failed");
|
|
assert!(
|
|
put.status().is_success(),
|
|
"upload failed: {}",
|
|
put.text().await.unwrap_or_default()
|
|
);
|
|
let put_json: serde_json::Value = put.json().await.expect("invalid upload json");
|
|
let key = put_json
|
|
.get("key")
|
|
.and_then(|v| v.as_str())
|
|
.expect("missing key")
|
|
.to_string();
|
|
|
|
// List should include the key.
|
|
let list_url = format!("{base}/admin/v1/tenants/{tenant_id}/docs?prefix={doc_type}/");
|
|
let list = http
|
|
.get(&list_url)
|
|
.headers(headers.clone())
|
|
.send()
|
|
.await
|
|
.expect("list request failed");
|
|
assert!(list.status().is_success(), "list failed");
|
|
let list_json: serde_json::Value = list.json().await.expect("invalid list json");
|
|
let objects = list_json
|
|
.get("objects")
|
|
.and_then(|v| v.as_array())
|
|
.expect("missing objects");
|
|
assert!(
|
|
objects
|
|
.iter()
|
|
.any(|o| o.get("key").and_then(|k| k.as_str()) == Some(key.as_str())),
|
|
"expected list to include uploaded key"
|
|
);
|
|
|
|
// Download (proxy endpoint) returns same bytes.
|
|
let get_url = format!(
|
|
"{base}/admin/v1/tenants/{tenant_id}/docs/object/{}",
|
|
urlencoding::encode(&key)
|
|
);
|
|
let got = http
|
|
.get(&get_url)
|
|
.headers(headers.clone())
|
|
.send()
|
|
.await
|
|
.expect("download request failed");
|
|
assert!(got.status().is_success(), "download failed");
|
|
let got_bytes = got.bytes().await.expect("download bytes failed").to_vec();
|
|
assert_eq!(got_bytes, bytes);
|
|
|
|
// Best-effort cleanup.
|
|
let _ = Command::new("docker")
|
|
.args(["compose", "-f"])
|
|
.arg(&compose)
|
|
.args(["down", "-v"])
|
|
.status();
|
|
}
|