feat(billing): implement tenant subscription entitlements system (milestones 0-6)
This commit is contained in:
169
control/api/tests/docs_e2e_docker_gated.rs
Normal file
169
control/api/tests/docs_e2e_docker_gated.rs
Normal file
@@ -0,0 +1,169 @@
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user