Files
cloudlysis/control/api/tests/drift_classification.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

124 lines
3.8 KiB
Rust

use api::{
AppState, AuditStore, AuthConfig, ConfigLocks, ConfigRegistry, JobStore, PlacementStore,
SwarmStore, TenantLocks,
};
use axum::{
Router,
body::Body,
http::{Request, StatusCode, header},
};
use jsonwebtoken::{EncodingKey, Header, encode};
use metrics_exporter_prometheus::PrometheusBuilder;
use serde::Serialize;
use std::{fs, path::PathBuf, sync::OnceLock};
use tower::ServiceExt;
static HANDLE: OnceLock<metrics_exporter_prometheus::PrometheusHandle> = OnceLock::new();
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()
}
#[derive(Serialize)]
struct TestClaims {
sub: String,
session_id: String,
permissions: Vec<String>,
exp: usize,
}
fn make_token(perms: &[&str]) -> String {
let exp = (std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
+ 60) 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(b"test_secret"),
)
.unwrap()
}
fn temp_swarm_file(raw: &str) -> PathBuf {
let mut dst = std::env::temp_dir();
dst.push(format!(
"cloudlysis-control-swarm-{}-{}.json",
std::process::id(),
uuid::Uuid::new_v4()
));
fs::write(&dst, raw).expect("failed to write temp swarm file");
dst
}
fn test_app_with_swarm(swarm_path: PathBuf) -> Router {
let handle = HANDLE
.get_or_init(|| {
PrometheusBuilder::new()
.install_recorder()
.expect("failed to install prometheus recorder")
})
.clone();
api::build_app(AppState {
prometheus: handle,
auth: AuthConfig {
hs256_secret: Some(b"test_secret".to_vec()),
},
jobs: JobStore::default(),
audit: AuditStore::default(),
tenant_locks: TenantLocks::default(),
config_locks: ConfigLocks::default(),
http: reqwest::Client::new(),
placement: PlacementStore::new(repo_root().join("config/placement/dev.json")),
billing: api::billing::BillingStore::new(
std::env::temp_dir().join("billing-drift-test.json"),
),
billing_provider: std::sync::Arc::new(api::billing::MockProvider),
billing_enforcement_enabled: false,
config: ConfigRegistry::new(None, None),
fleet_services: vec![],
swarm: SwarmStore::new(swarm_path),
docs: None,
})
}
#[tokio::test]
async fn drift_marks_extra_services_vs_desired_observation_set() {
let swarm = temp_swarm_file(
r#"{ "services": [{"name":"extra-1","image":null,"mode":null,"replicas":null,"updated_at":null}], "tasks": [] }"#,
);
let app = test_app_with_swarm(swarm);
let token = make_token(&["control:read"]);
let res = app
.oneshot(
Request::builder()
.uri("/admin/v1/platform/drift")
.header(header::AUTHORIZATION, format!("Bearer {token}"))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = axum::body::to_bytes(res.into_body(), 1024 * 1024)
.await
.unwrap();
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
let items = v.get("items").and_then(|x| x.as_array()).unwrap();
assert!(items.iter().any(|i| {
i.get("kind").and_then(|k| k.as_str()) == Some("extra")
&& i.get("service").and_then(|s| s.as_str()) == Some("extra-1")
}));
}