Jesús Pérez 93b0e5225c
feat(platform): control plane — NATS JetStream + SurrealDB + SOLID enforcement
New crates
  - platform-nats: async_nats JetStream bridge; pull/push consumers, explicit ACK,
    subject prefixing under provisioning.>, 6 stream definitions on startup
  - platform-db: SurrealDB pool (embedded RocksDB solo, Surreal<Mem> tests,
    WebSocket server multi-user); migrate() with DEFINE TABLE IF NOT EXISTS DDL

  Service integrations
  - orchestrator: NATS pub on task state transitions, execution_logs → SurrealDB,
    webhook handler (HMAC-SHA256), AuditCollector (batch INSERT, 100-event/1s flush)
  - control-center: solo_auth_middleware (intentional bypass, --mode solo only),
    NATS session events, WebSocket bridge via JetStream subscription (no polling)
  - vault-service: NATS lease flow; credentials over HTTPS only (lease_id in NATS);
    SurrealDB storage backend with MVCC retry + exponential backoff
  - secretumvault: complete SurrealDB backend replacing HashMap; 9 unit + 19 integration tests
  - extension-registry: NATS lifecycle events, vault:// credential resolver with TTL cache,
    cache invalidation via provisioning.workspace.*.deploy.done

  Clippy workspace clean
  cargo clippy --workspace -- -D warnings: 0 errors
  Patterns fixed: derivable_impls (#[default] on enum variants), excessive_nesting
  (let-else, boolean arithmetic in retain, extracted helpers), io_error_other,
  redundant_closure, iter_kv_map, manual_range_contains, pathbuf_instead_of_path
2026-02-17 23:58:14 +00:00

189 lines
5.5 KiB
Rust

use axum::body::Body;
use axum::http::{Request, StatusCode};
use extension_registry::config::OciConfig;
use extension_registry::{build_routes, AppState, Config};
use http_body_util::BodyExt;
use tower::ServiceExt;
/// Create a minimal test config with a mock OCI backend
fn create_test_config() -> Config {
Config {
server: extension_registry::config::ServerConfig::default(),
gitea: None,
// Use OCI as test backend (doesn't require file validation for auth_token_path)
oci: Some(OciConfig {
id: Some("test-oci".to_string()),
registry: "localhost:5000".to_string(),
namespace: "test".to_string(),
auth_token_path: None,
timeout_seconds: 30,
verify_ssl: false,
}),
sources: extension_registry::config::SourcesConfig::default(),
distributions: extension_registry::config::DistributionsConfig::default(),
cache: extension_registry::config::CacheConfig::default(),
}
}
#[tokio::test]
#[ignore] // Requires OCI registry or Gitea service to be running
#[ignore] // Requires OCI registry service to be running
async fn test_health_check() {
let config = create_test_config();
let state = AppState::new(config, None)
.await
.expect("Failed to create app state");
let app = build_routes(state);
let response = app
.oneshot(
Request::builder()
.uri("/api/v1/health")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let health: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(health.get("status").is_some());
assert!(health.get("version").is_some());
assert!(health.get("uptime").is_some());
}
#[tokio::test]
#[ignore] // Requires OCI registry or Gitea service to be running
async fn test_list_extensions_empty() {
let config = create_test_config();
let state = AppState::new(config, None)
.await
.expect("Failed to create app state");
let app = build_routes(state);
let response = app
.oneshot(
Request::builder()
.uri("/api/v1/extensions")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let extensions: Vec<serde_json::Value> = serde_json::from_slice(&body).unwrap();
// Should be empty when no backends configured
assert_eq!(extensions.len(), 0);
}
#[tokio::test]
#[ignore] // Requires OCI registry or Gitea service to be running
async fn test_get_nonexistent_extension() {
let config = create_test_config();
let state = AppState::new(config, None)
.await
.expect("Failed to create app state");
let app = build_routes(state);
let response = app
.oneshot(
Request::builder()
.uri("/api/v1/extensions/provider/nonexistent")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
#[ignore] // Requires OCI registry or Gitea service to be running
async fn test_metrics_endpoint() {
let config = create_test_config();
let state = AppState::new(config, None)
.await
.expect("Failed to create app state");
let app = build_routes(state);
let response = app
.oneshot(
Request::builder()
.uri("/api/v1/metrics")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let metrics = String::from_utf8(body.to_vec()).unwrap();
assert!(metrics.contains("http_requests_total"));
}
#[tokio::test]
#[ignore] // Requires OCI registry or Gitea service to be running
async fn test_cache_stats_endpoint() {
let config = create_test_config();
let state = AppState::new(config, None)
.await
.expect("Failed to create app state");
let app = build_routes(state);
let response = app
.oneshot(
Request::builder()
.uri("/api/v1/cache/stats")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let stats: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(stats.get("list_entries").is_some());
assert!(stats.get("metadata_entries").is_some());
assert!(stats.get("total_entries").is_some());
}
#[tokio::test]
#[ignore] // Requires OCI registry or Gitea service to be running
async fn test_invalid_extension_type() {
let config = create_test_config();
let state = AppState::new(config, None)
.await
.expect("Failed to create app state");
let app = build_routes(state);
let response = app
.oneshot(
Request::builder()
.uri("/api/v1/extensions/invalid_type/test")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}