use jsonwebtoken::{EncodingKey, Header, encode}; use reqwest::StatusCode; use serde::Serialize; use serde_json::json; use std::time::Duration; use uuid::Uuid; #[derive(Serialize)] struct TestClaims { sub: String, session_id: String, permissions: Vec, 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: "smoke".to_string(), session_id: "smoke".to_string(), permissions: perms.iter().map(|p| (*p).to_string()).collect(), exp, }, &EncodingKey::from_secret(secret), ) .unwrap() } #[tokio::test] async fn control_api_docs_smoke_is_env_gated() { let enabled = std::env::var("CONTROL_TEST_SMOKE").ok(); if enabled.as_deref() != Some("1") { eprintln!("skipping: set CONTROL_TEST_SMOKE=1 to enable env smoke tests"); return; } let base_url = std::env::var("CONTROL_TEST_BASE_URL").expect("CONTROL_TEST_BASE_URL is required"); let base_url = base_url.trim_end_matches('/').to_string(); // Either provide a token directly, or provide secret+perms to mint one. let token = if let Ok(t) = std::env::var("CONTROL_TEST_TOKEN") { t } else { let secret = std::env::var("CONTROL_TEST_JWT_SECRET") .expect("CONTROL_TEST_TOKEN or CONTROL_TEST_JWT_SECRET is required"); make_token(secret.as_bytes(), &["control:read", "control:write"]) }; let tenant_id = std::env::var("CONTROL_TEST_TENANT_ID") .ok() .unwrap_or_else(|| Uuid::new_v4().to_string()); let http = reqwest::Client::builder() .timeout(Duration::from_secs(15)) .build() .unwrap(); // Health. let health = http .get(format!("{base_url}/health")) .send() .await .expect("health request failed"); assert!(health.status().is_success(), "health not ok"); // Presign upload. let doc_id = Uuid::new_v4().to_string(); let filename = "smoke.txt"; let presign_up = http .post(format!( "{base_url}/admin/v1/tenants/{tenant_id}/docs/presign/upload" )) .header("authorization", format!("Bearer {token}")) .header("x-tenant-id", &tenant_id) .json(&json!({ "doc_type": "deployments", "doc_id": doc_id, "filename": filename, "content_type": "text/plain", })) .send() .await .expect("presign upload failed"); assert!( presign_up.status().is_success(), "presign upload not ok: {}", presign_up.status() ); let up_json: serde_json::Value = presign_up.json().await.unwrap(); let put_url = up_json.get("url").and_then(|v| v.as_str()).unwrap(); let key = up_json .get("key") .and_then(|v| v.as_str()) .unwrap() .to_string(); // PUT bytes to S3 directly. let payload = b"hello-smoke".to_vec(); let put = http .put(put_url) .header("content-type", "text/plain") .body(payload.clone()) .send() .await .expect("s3 put failed"); assert!(put.status().is_success(), "s3 put not ok: {}", put.status()); // List should include key. let list = http .get(format!( "{base_url}/admin/v1/tenants/{tenant_id}/docs?prefix=deployments/" )) .header("authorization", format!("Bearer {token}")) .header("x-tenant-id", &tenant_id) .send() .await .expect("list failed"); assert!(list.status().is_success(), "list not ok"); let list_json: serde_json::Value = list.json().await.unwrap(); let objects = list_json.get("objects").and_then(|v| v.as_array()).unwrap(); assert!( objects .iter() .any(|o| o.get("key").and_then(|k| k.as_str()) == Some(key.as_str())), "expected list to include presigned upload key" ); // Presign download and fetch bytes. let presign_down = http .post(format!( "{base_url}/admin/v1/tenants/{tenant_id}/docs/presign/download" )) .header("authorization", format!("Bearer {token}")) .header("x-tenant-id", &tenant_id) .json(&json!({ "key": key })) .send() .await .expect("presign download failed"); assert!( presign_down.status().is_success(), "presign download not ok" ); let down_json: serde_json::Value = presign_down.json().await.unwrap(); let get_url = down_json.get("url").and_then(|v| v.as_str()).unwrap(); let got = http.get(get_url).send().await.expect("s3 get failed"); assert_eq!(got.status(), StatusCode::OK); let got_bytes = got.bytes().await.unwrap().to_vec(); assert_eq!(got_bytes, payload); }