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