Files
cloudlysis/control/api/tests/docs_e2e_docker_gated.rs
Vlad Durnea 2595e7f1c5
Some checks failed
ci / ui (push) Failing after 28s
ci / rust (push) Failing after 2m40s
images / build-and-push (push) Failing after 19s
feat(billing): implement tenant subscription entitlements system (milestones 0-6)
2026-03-30 18:41:23 +03:00

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();
}