diff --git a/crates/openshell-driver-podman/src/driver.rs b/crates/openshell-driver-podman/src/driver.rs index a2a1e15d6..3d88e12ac 100644 --- a/crates/openshell-driver-podman/src/driver.rs +++ b/crates/openshell-driver-podman/src/driver.rs @@ -528,18 +528,9 @@ fn check_subuid_range() { #[cfg(test)] mod tests { use super::*; - use http_body_util::Full; - use hyper::body::Bytes; - use hyper::server::conn::http1; - use hyper::service::service_fn; - use hyper::{Response, StatusCode}; - use hyper_util::rt::TokioIo; - use std::collections::VecDeque; - use std::convert::Infallible; + use crate::test_utils::{StubResponse, spawn_podman_stub}; + use hyper::StatusCode; use std::path::PathBuf; - use std::sync::{Arc, Mutex}; - use std::time::{SystemTime, UNIX_EPOCH}; - use tokio::net::UnixListener; #[test] fn podman_driver_error_from_conflict() { @@ -625,32 +616,6 @@ mod tests { assert_eq!(cfg.grpc_endpoint, "https://gateway.internal:9000"); } - #[derive(Clone)] - struct StubResponse { - status: StatusCode, - body: String, - } - - impl StubResponse { - fn new(status: StatusCode, body: impl Into) -> Self { - Self { - status, - body: body.into(), - } - } - } - - fn unique_socket_path(test_name: &str) -> PathBuf { - let nanos = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("clock should be after unix epoch") - .as_nanos(); - PathBuf::from(format!( - "/tmp/openshell-podman-{test_name}-{}-{nanos}.sock", - std::process::id() - )) - } - fn test_driver(socket_path: PathBuf) -> PodmanComputeDriver { let config = PodmanComputeConfig { socket_path, @@ -664,71 +629,6 @@ mod tests { format!("/v5.0.0{path}") } - fn spawn_podman_stub( - test_name: &str, - responses: Vec, - ) -> ( - PathBuf, - Arc>>, - tokio::task::JoinHandle<()>, - ) { - let socket_path = unique_socket_path(test_name); - let _ = std::fs::remove_file(&socket_path); - let listener = UnixListener::bind(&socket_path).expect("test socket should bind"); - let request_log = Arc::new(Mutex::new(Vec::new())); - let response_queue = Arc::new(Mutex::new(VecDeque::from(responses))); - let expected = response_queue - .lock() - .expect("response queue lock should not be poisoned") - .len(); - let socket_path_for_task = socket_path.clone(); - let log_for_task = request_log.clone(); - let queue_for_task = response_queue; - let handle = tokio::spawn(async move { - for _ in 0..expected { - let (stream, _) = listener.accept().await.expect("test stub should accept"); - let log = log_for_task.clone(); - let queue = queue_for_task.clone(); - let result = http1::Builder::new() - .serve_connection( - TokioIo::new(stream), - service_fn(move |req| { - let log = log.clone(); - let queue = queue.clone(); - async move { - let path = req.uri().path_and_query().map_or_else( - || req.uri().path().to_string(), - |pq| pq.as_str().to_string(), - ); - log.lock() - .expect("request log lock should not be poisoned") - .push(format!("{} {}", req.method(), path)); - let response = queue - .lock() - .expect("response queue lock should not be poisoned") - .pop_front() - .expect("stub response should exist"); - Ok::<_, Infallible>( - Response::builder() - .status(response.status) - .body(Full::new(Bytes::from(response.body))) - .expect("stub response should build"), - ) - } - }), - ) - .await; - // The one-shot test client can close the Unix socket after the - // response, which Hyper reports as a shutdown error. Let the - // request log assertions below decide whether the stub served - // the expected API calls. - let _ = result; - } - let _ = std::fs::remove_file(&socket_path_for_task); - }); - (socket_path, request_log, handle) - } - #[tokio::test] async fn delete_sandbox_cleans_up_with_request_id_when_container_is_already_gone() { let sandbox_id = "sandbox-123"; diff --git a/crates/openshell-driver-podman/src/grpc.rs b/crates/openshell-driver-podman/src/grpc.rs index f39832b9b..0c6015776 100644 --- a/crates/openshell-driver-podman/src/grpc.rs +++ b/crates/openshell-driver-podman/src/grpc.rs @@ -156,18 +156,10 @@ mod tests { use super::*; use crate::config::PodmanComputeConfig; use crate::container; - use http_body_util::Full; - use hyper::body::Bytes; - use hyper::server::conn::http1; - use hyper::service::service_fn; - use hyper::{Response as HyperResponse, StatusCode}; - use hyper_util::rt::TokioIo; + use crate::test_utils::{StubResponse, spawn_podman_stub, unique_socket_path}; + use hyper::StatusCode; use openshell_core::ComputeDriverError; - use std::collections::VecDeque; - use std::convert::Infallible; use std::path::PathBuf; - use std::sync::{Arc, Mutex}; - use std::time::{SystemTime, UNIX_EPOCH}; #[test] fn precondition_driver_errors_map_to_failed_precondition_status() { @@ -184,98 +176,6 @@ mod tests { assert_eq!(status.code(), tonic::Code::AlreadyExists); } - #[derive(Clone)] - struct StubResponse { - status: StatusCode, - body: String, - } - - impl StubResponse { - fn new(status: StatusCode, body: impl Into) -> Self { - Self { - status, - body: body.into(), - } - } - } - - fn unique_socket_path(test_name: &str) -> PathBuf { - let nanos = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("clock should be after unix epoch") - .as_nanos(); - PathBuf::from(format!( - "/tmp/openshell-podman-grpc-{test_name}-{}-{nanos}.sock", - std::process::id() - )) - } - - fn spawn_podman_stub( - test_name: &str, - responses: Vec, - ) -> ( - PathBuf, - Arc>>, - tokio::task::JoinHandle<()>, - ) { - let socket_path = unique_socket_path(test_name); - let _ = std::fs::remove_file(&socket_path); - let listener = - tokio::net::UnixListener::bind(&socket_path).expect("test socket should bind"); - let request_log = Arc::new(Mutex::new(Vec::new())); - let response_queue = Arc::new(Mutex::new(VecDeque::from(responses))); - let expected = response_queue - .lock() - .expect("response queue lock should not be poisoned") - .len(); - let socket_path_for_task = socket_path.clone(); - let log_for_task = request_log.clone(); - let queue_for_task = response_queue; - let handle = tokio::spawn(async move { - for _ in 0..expected { - let (stream, _) = listener.accept().await.expect("test stub should accept"); - let log = log_for_task.clone(); - let queue = queue_for_task.clone(); - let result = http1::Builder::new() - .serve_connection( - TokioIo::new(stream), - service_fn(move |req| { - let log = log.clone(); - let queue = queue.clone(); - async move { - let path = req.uri().path_and_query().map_or_else( - || req.uri().path().to_string(), - |pq| pq.as_str().to_string(), - ); - log.lock() - .expect("request log lock should not be poisoned") - .push(format!("{} {}", req.method(), path)); - let response = queue - .lock() - .expect("response queue lock should not be poisoned") - .pop_front() - .expect("stub response should exist"); - Ok::<_, Infallible>( - HyperResponse::builder() - .status(response.status) - .body(Full::new(Bytes::from(response.body))) - .expect("stub response should build"), - ) - } - }), - ) - .await; - // The one-shot test client can close the Unix socket after the - // response, which Hyper reports as a shutdown error. Let the - // request log assertions below decide whether the stub served - // the expected API calls. - let _ = result; - } - let _ = std::fs::remove_file(&socket_path_for_task); - }); - (socket_path, request_log, handle) - } - fn test_service(socket_path: PathBuf) -> ComputeDriverService { let config = PodmanComputeConfig { socket_path, diff --git a/crates/openshell-driver-podman/src/lib.rs b/crates/openshell-driver-podman/src/lib.rs index 630deaee1..5847a10ea 100644 --- a/crates/openshell-driver-podman/src/lib.rs +++ b/crates/openshell-driver-podman/src/lib.rs @@ -6,6 +6,8 @@ pub mod config; pub(crate) mod container; pub mod driver; pub mod grpc; +#[cfg(test)] +pub(crate) mod test_utils; pub(crate) mod watcher; pub use config::PodmanComputeConfig; diff --git a/crates/openshell-driver-podman/src/test_utils.rs b/crates/openshell-driver-podman/src/test_utils.rs new file mode 100644 index 000000000..94794bc22 --- /dev/null +++ b/crates/openshell-driver-podman/src/test_utils.rs @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Shared test helpers for openshell-driver-podman unit tests. + +use http_body_util::Full; +use hyper::StatusCode; +use hyper::body::Bytes; +use hyper::server::conn::http1; +use hyper::service::service_fn; +use hyper_util::rt::TokioIo; +use std::collections::VecDeque; +use std::convert::Infallible; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; +use std::time::{SystemTime, UNIX_EPOCH}; +use tokio::net::UnixListener; + +/// A canned HTTP response for the Podman stub server. +#[derive(Clone)] +pub struct StubResponse { + pub status: StatusCode, + pub body: String, +} + +impl StubResponse { + pub fn new(status: StatusCode, body: impl Into) -> Self { + Self { + status, + body: body.into(), + } + } +} + +/// Generate a unique Unix socket path for a test. +/// +/// Uses the current PID and nanosecond timestamp to avoid collisions between +/// concurrent test runs. +pub fn unique_socket_path(test_name: &str) -> PathBuf { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock should be after unix epoch") + .as_nanos(); + PathBuf::from(format!( + "/tmp/openshell-podman-{test_name}-{}-{nanos}.sock", + std::process::id() + )) +} + +/// Spawn a Unix-socket HTTP stub that serves the given `responses` in order. +/// +/// Returns: +/// - the socket path (already bound and listening) +/// - a shared log of `"METHOD /path"` strings, one per request received +/// - a join handle that resolves once all expected requests have been served +pub fn spawn_podman_stub( + test_name: &str, + responses: Vec, +) -> ( + PathBuf, + Arc>>, + tokio::task::JoinHandle<()>, +) { + let socket_path = unique_socket_path(test_name); + let _ = std::fs::remove_file(&socket_path); + let listener = UnixListener::bind(&socket_path).expect("test socket should bind"); + let request_log = Arc::new(Mutex::new(Vec::new())); + let response_queue = Arc::new(Mutex::new(VecDeque::from(responses))); + let expected = response_queue + .lock() + .expect("response queue lock should not be poisoned") + .len(); + let socket_path_for_task = socket_path.clone(); + let log_for_task = request_log.clone(); + let queue_for_task = response_queue; + let handle = tokio::spawn(async move { + for _ in 0..expected { + let (stream, _) = listener.accept().await.expect("test stub should accept"); + let log = log_for_task.clone(); + let queue = queue_for_task.clone(); + let result = http1::Builder::new() + .serve_connection( + TokioIo::new(stream), + service_fn(move |req| { + let log = log.clone(); + let queue = queue.clone(); + async move { + let path = req.uri().path_and_query().map_or_else( + || req.uri().path().to_string(), + |pq| pq.as_str().to_string(), + ); + log.lock() + .expect("request log lock should not be poisoned") + .push(format!("{} {}", req.method(), path)); + let response = queue + .lock() + .expect("response queue lock should not be poisoned") + .pop_front() + .expect("stub response should exist"); + Ok::<_, Infallible>( + hyper::Response::builder() + .status(response.status) + .body(Full::new(Bytes::from(response.body))) + .expect("stub response should build"), + ) + } + }), + ) + .await; + // The one-shot test client can close the Unix socket after the + // response, which Hyper reports as a shutdown error. Let the + // request log assertions below decide whether the stub served + // the expected API calls. + let _ = result; + } + let _ = std::fs::remove_file(&socket_path_for_task); + }); + (socket_path, request_log, handle) +} diff --git a/crates/openshell-server/src/grpc/mod.rs b/crates/openshell-server/src/grpc/mod.rs index 9ea8d7ece..db3d2350d 100644 --- a/crates/openshell-server/src/grpc/mod.rs +++ b/crates/openshell-server/src/grpc/mod.rs @@ -534,6 +534,45 @@ impl OpenShell for OpenShellService { } } +// --------------------------------------------------------------------------- +// Shared test support +// --------------------------------------------------------------------------- + +/// Shared test helpers for grpc submodule unit tests. +#[cfg(test)] +pub mod test_support { + use std::sync::Arc; + + use crate::ServerState; + use crate::compute::new_test_runtime; + use crate::persistence::Store; + use crate::sandbox_index::SandboxIndex; + use crate::sandbox_watch::SandboxWatchBus; + use crate::supervisor_session::SupervisorSessionRegistry; + use crate::tracing_bus::TracingLogBus; + use openshell_core::Config; + + /// Build an in-memory `ServerState` for unit tests. + pub async fn test_server_state() -> Arc { + let store = Arc::new( + Store::connect("sqlite::memory:?cache=shared") + .await + .unwrap(), + ); + let compute = new_test_runtime(store.clone()).await; + Arc::new(ServerState::new( + Config::new(None).with_database_url("sqlite::memory:?cache=shared"), + store, + compute, + SandboxIndex::new(), + SandboxWatchBus::new(), + TracingLogBus::new(), + Arc::new(SupervisorSessionRegistry::new()), + None, + )) + } +} + // --------------------------------------------------------------------------- // Tests for mod-level utilities // --------------------------------------------------------------------------- diff --git a/crates/openshell-server/src/grpc/policy.rs b/crates/openshell-server/src/grpc/policy.rs index 315b06f3c..241637344 100644 --- a/crates/openshell-server/src/grpc/policy.rs +++ b/crates/openshell-server/src/grpc/policy.rs @@ -2788,14 +2788,7 @@ fn materialize_global_settings( #[cfg(test)] mod tests { use super::*; - use crate::ServerState; - use crate::compute::new_test_runtime; - use crate::persistence::Store; - use crate::sandbox_index::SandboxIndex; - use crate::sandbox_watch::SandboxWatchBus; - use crate::supervisor_session::SupervisorSessionRegistry; - use crate::tracing_bus::TracingLogBus; - use openshell_core::Config; + use crate::grpc::test_support::test_server_state; use std::collections::HashMap; use std::sync::Arc; use tonic::Code; @@ -3914,25 +3907,6 @@ mod tests { assert_eq!(policy.process.unwrap().run_as_user, "sandbox"); } - async fn test_server_state() -> Arc { - let store = Arc::new( - Store::connect("sqlite::memory:?cache=shared") - .await - .unwrap(), - ); - let compute = new_test_runtime(store.clone()).await; - Arc::new(ServerState::new( - Config::new(None).with_database_url("sqlite::memory:?cache=shared"), - store, - compute, - SandboxIndex::new(), - SandboxWatchBus::new(), - TracingLogBus::new(), - Arc::new(SupervisorSessionRegistry::new()), - None, - )) - } - #[tokio::test] async fn draft_chunk_handler_lifecycle_round_trip() { use openshell_core::proto::{ diff --git a/crates/openshell-server/src/grpc/provider.rs b/crates/openshell-server/src/grpc/provider.rs index 88b3ea743..9db2e532d 100644 --- a/crates/openshell-server/src/grpc/provider.rs +++ b/crates/openshell-server/src/grpc/provider.rs @@ -752,14 +752,8 @@ pub(super) async fn handle_delete_provider( #[cfg(test)] mod tests { use super::*; - use crate::ServerState; - use crate::compute::new_test_runtime; use crate::grpc::MAX_MAP_KEY_LEN; - use crate::sandbox_index::SandboxIndex; - use crate::sandbox_watch::SandboxWatchBus; - use crate::supervisor_session::SupervisorSessionRegistry; - use crate::tracing_bus::TracingLogBus; - use openshell_core::Config; + use crate::grpc::test_support::test_server_state; use openshell_core::proto::{ DeleteProviderProfileRequest, GetProviderProfileRequest, ImportProviderProfilesRequest, L7Allow, L7Rule, LintProviderProfilesRequest, ListProviderProfilesRequest, NetworkBinary, @@ -768,7 +762,6 @@ mod tests { }; use openshell_core::{ObjectId, ObjectName}; use std::collections::HashMap; - use std::sync::Arc; use tonic::{Code, Request}; #[test] @@ -835,25 +828,6 @@ mod tests { profile } - async fn test_server_state() -> Arc { - let store = Arc::new( - Store::connect("sqlite::memory:?cache=shared") - .await - .unwrap(), - ); - let compute = new_test_runtime(store.clone()).await; - Arc::new(ServerState::new( - Config::new(None).with_database_url("sqlite::memory:?cache=shared"), - store, - compute, - SandboxIndex::new(), - SandboxWatchBus::new(), - TracingLogBus::new(), - Arc::new(SupervisorSessionRegistry::new()), - None, - )) - } - #[tokio::test] async fn list_provider_profiles_returns_built_in_profile_categories() { let state = test_server_state().await; diff --git a/crates/openshell-server/src/grpc/sandbox.rs b/crates/openshell-server/src/grpc/sandbox.rs index 5c523b10e..b494f9210 100644 --- a/crates/openshell-server/src/grpc/sandbox.rs +++ b/crates/openshell-server/src/grpc/sandbox.rs @@ -1759,13 +1759,7 @@ async fn run_exec_with_russh( #[cfg(test)] mod tests { use super::*; - use crate::compute::new_test_runtime; - use crate::persistence::Store; - use crate::sandbox_index::SandboxIndex; - use crate::sandbox_watch::SandboxWatchBus; - use crate::supervisor_session::SupervisorSessionRegistry; - use crate::tracing_bus::TracingLogBus; - use openshell_core::Config; + use crate::grpc::test_support::test_server_state; use openshell_core::proto::datamodel::v1::ObjectMeta; use std::collections::HashMap; @@ -1978,21 +1972,6 @@ mod tests { } } - async fn test_server_state() -> Arc { - let store = Arc::new(Store::connect("sqlite::memory:").await.unwrap()); - let compute = new_test_runtime(store.clone()).await; - Arc::new(ServerState::new( - Config::new(None).with_database_url("sqlite::memory:"), - store, - compute, - SandboxIndex::new(), - SandboxWatchBus::new(), - TracingLogBus::new(), - Arc::new(SupervisorSessionRegistry::new()), - None, - )) - } - fn test_provider(name: &str, provider_type: &str) -> Provider { Provider { metadata: Some(ObjectMeta { diff --git a/crates/openshell-server/src/persistence/mod.rs b/crates/openshell-server/src/persistence/mod.rs index 82b63a7b2..87aa86581 100644 --- a/crates/openshell-server/src/persistence/mod.rs +++ b/crates/openshell-server/src/persistence/mod.rs @@ -19,6 +19,11 @@ use thiserror::Error; pub use postgres::PostgresStore; pub use sqlite::SqliteStore; +/// Object type string for sandbox policy records. +pub const POLICY_OBJECT_TYPE: &str = "sandbox_policy"; +/// Object type string for draft policy chunk records. +pub const DRAFT_CHUNK_OBJECT_TYPE: &str = "draft_policy_chunk"; + pub type PersistenceResult = Result; /// Persistence-layer error type. diff --git a/crates/openshell-server/src/persistence/postgres.rs b/crates/openshell-server/src/persistence/postgres.rs index dcdf24f67..751d70073 100644 --- a/crates/openshell-server/src/persistence/postgres.rs +++ b/crates/openshell-server/src/persistence/postgres.rs @@ -14,8 +14,7 @@ use sqlx::{PgPool, Row}; static POSTGRES_MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("./migrations/postgres"); -const POLICY_OBJECT_TYPE: &str = "sandbox_policy"; -const DRAFT_CHUNK_OBJECT_TYPE: &str = "draft_policy_chunk"; +use super::{DRAFT_CHUNK_OBJECT_TYPE, POLICY_OBJECT_TYPE}; #[derive(Debug, Clone)] pub struct PostgresStore { diff --git a/crates/openshell-server/src/persistence/sqlite.rs b/crates/openshell-server/src/persistence/sqlite.rs index bad288a7b..04c4d8d8a 100644 --- a/crates/openshell-server/src/persistence/sqlite.rs +++ b/crates/openshell-server/src/persistence/sqlite.rs @@ -17,8 +17,7 @@ use std::str::FromStr; static SQLITE_MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("./migrations/sqlite"); -const POLICY_OBJECT_TYPE: &str = "sandbox_policy"; -const DRAFT_CHUNK_OBJECT_TYPE: &str = "draft_policy_chunk"; +use super::{DRAFT_CHUNK_OBJECT_TYPE, POLICY_OBJECT_TYPE}; #[derive(Debug, Clone)] pub struct SqliteStore { diff --git a/crates/openshell-server/tests/auth_endpoint_integration.rs b/crates/openshell-server/tests/auth_endpoint_integration.rs index 59c2a23f6..c1ea74b9b 100644 --- a/crates/openshell-server/tests/auth_endpoint_integration.rs +++ b/crates/openshell-server/tests/auth_endpoint_integration.rs @@ -11,6 +11,8 @@ //! a full `ServerState`. The test handler mirrors the production logic in //! `auth.rs` but uses a simple `SocketAddr` as state. +mod common; + use axum::{ Router, extract::{Query, State}, @@ -378,440 +380,7 @@ async fn auth_connect_falls_back_to_bind_address() { server.abort(); } -// --------------------------------------------------------------------------- -// Minimal OpenShell for test 7 (plaintext gRPC+HTTP) -// --------------------------------------------------------------------------- - -#[derive(Clone, Default)] -struct TestOpenShell; - -#[tonic::async_trait] -impl openshell_core::proto::open_shell_server::OpenShell for TestOpenShell { - async fn health( - &self, - _request: tonic::Request, - ) -> Result, tonic::Status> { - Ok(tonic::Response::new( - openshell_core::proto::HealthResponse { - status: openshell_core::proto::ServiceStatus::Healthy.into(), - version: "test".to_string(), - }, - )) - } - - async fn create_sandbox( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> { - Ok(tonic::Response::new( - openshell_core::proto::SandboxResponse::default(), - )) - } - - async fn get_sandbox( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> { - Ok(tonic::Response::new( - openshell_core::proto::SandboxResponse::default(), - )) - } - - async fn list_sandboxes( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> { - Ok(tonic::Response::new( - openshell_core::proto::ListSandboxesResponse::default(), - )) - } - - async fn list_sandbox_providers( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> - { - Ok(tonic::Response::new( - openshell_core::proto::ListSandboxProvidersResponse::default(), - )) - } - - async fn attach_sandbox_provider( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> - { - Ok(tonic::Response::new( - openshell_core::proto::AttachSandboxProviderResponse::default(), - )) - } - - async fn detach_sandbox_provider( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> - { - Ok(tonic::Response::new( - openshell_core::proto::DetachSandboxProviderResponse::default(), - )) - } - - async fn delete_sandbox( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> { - Ok(tonic::Response::new( - openshell_core::proto::DeleteSandboxResponse { deleted: true }, - )) - } - - async fn get_sandbox_config( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> - { - Ok(tonic::Response::new( - openshell_core::proto::GetSandboxConfigResponse::default(), - )) - } - - async fn get_gateway_config( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> - { - Ok(tonic::Response::new( - openshell_core::proto::GetGatewayConfigResponse::default(), - )) - } - - async fn get_sandbox_provider_environment( - &self, - _: tonic::Request, - ) -> Result< - tonic::Response, - tonic::Status, - > { - Ok(tonic::Response::new( - openshell_core::proto::GetSandboxProviderEnvironmentResponse::default(), - )) - } - - async fn create_ssh_session( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> - { - Ok(tonic::Response::new( - openshell_core::proto::CreateSshSessionResponse::default(), - )) - } - - async fn expose_service( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> - { - Ok(tonic::Response::new( - openshell_core::proto::ServiceEndpointResponse::default(), - )) - } - - async fn get_service( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> - { - Err(tonic::Status::unimplemented("unused")) - } - - async fn list_services( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> { - Err(tonic::Status::unimplemented("unused")) - } - - async fn delete_service( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> { - Err(tonic::Status::unimplemented("unused")) - } - - async fn revoke_ssh_session( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> - { - Ok(tonic::Response::new( - openshell_core::proto::RevokeSshSessionResponse::default(), - )) - } - - async fn create_provider( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> { - Err(tonic::Status::unimplemented("test")) - } - - async fn get_provider( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> { - Err(tonic::Status::unimplemented("test")) - } - - async fn list_providers( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> { - Err(tonic::Status::unimplemented("test")) - } - - async fn list_provider_profiles( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> - { - Err(tonic::Status::unimplemented("test")) - } - - async fn get_provider_profile( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> - { - Err(tonic::Status::unimplemented("test")) - } - - async fn import_provider_profiles( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> - { - Err(tonic::Status::unimplemented("test")) - } - - async fn lint_provider_profiles( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> - { - Err(tonic::Status::unimplemented("test")) - } - - async fn delete_provider_profile( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> - { - Err(tonic::Status::unimplemented("test")) - } - - async fn update_provider( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> { - Err(tonic::Status::unimplemented("test")) - } - - async fn delete_provider( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> { - Err(tonic::Status::unimplemented("test")) - } - - type WatchSandboxStream = tokio_stream::wrappers::ReceiverStream< - Result, - >; - type ExecSandboxStream = tokio_stream::wrappers::ReceiverStream< - Result, - >; - type ConnectSupervisorStream = tokio_stream::wrappers::ReceiverStream< - Result, - >; - - async fn watch_sandbox( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> { - let (_tx, rx) = tokio::sync::mpsc::channel(1); - Ok(tonic::Response::new( - tokio_stream::wrappers::ReceiverStream::new(rx), - )) - } - - async fn exec_sandbox( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> { - let (_tx, rx) = tokio::sync::mpsc::channel(1); - Ok(tonic::Response::new( - tokio_stream::wrappers::ReceiverStream::new(rx), - )) - } - - type ExecSandboxInteractiveStream = tokio_stream::wrappers::ReceiverStream< - Result, - >; - async fn exec_sandbox_interactive( - &self, - _: tonic::Request>, - ) -> Result, tonic::Status> { - Err(tonic::Status::unimplemented("test")) - } - - async fn update_config( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> { - Err(tonic::Status::unimplemented("test")) - } - - async fn get_sandbox_policy_status( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> - { - Err(tonic::Status::unimplemented("test")) - } - - async fn list_sandbox_policies( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> - { - Err(tonic::Status::unimplemented("test")) - } - - async fn report_policy_status( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> - { - Err(tonic::Status::unimplemented("test")) - } - - async fn get_sandbox_logs( - &self, - _: tonic::Request, - ) -> Result, tonic::Status> { - Err(tonic::Status::unimplemented("test")) - } - - async fn push_sandbox_logs( - &self, - _: tonic::Request>, - ) -> Result, tonic::Status> - { - Err(tonic::Status::unimplemented("test")) - } - - async fn submit_policy_analysis( - &self, - _request: tonic::Request, - ) -> Result, tonic::Status> - { - Err(tonic::Status::unimplemented("not implemented in test")) - } - - async fn get_draft_policy( - &self, - _request: tonic::Request, - ) -> Result, tonic::Status> { - Err(tonic::Status::unimplemented("not implemented in test")) - } - - async fn approve_draft_chunk( - &self, - _request: tonic::Request, - ) -> Result, tonic::Status> - { - Err(tonic::Status::unimplemented("not implemented in test")) - } - - async fn reject_draft_chunk( - &self, - _request: tonic::Request, - ) -> Result, tonic::Status> - { - Err(tonic::Status::unimplemented("not implemented in test")) - } - - async fn approve_all_draft_chunks( - &self, - _request: tonic::Request, - ) -> Result, tonic::Status> - { - Err(tonic::Status::unimplemented("not implemented in test")) - } - - async fn edit_draft_chunk( - &self, - _request: tonic::Request, - ) -> Result, tonic::Status> { - Err(tonic::Status::unimplemented("not implemented in test")) - } - - async fn undo_draft_chunk( - &self, - _request: tonic::Request, - ) -> Result, tonic::Status> { - Err(tonic::Status::unimplemented("not implemented in test")) - } - - async fn clear_draft_chunks( - &self, - _request: tonic::Request, - ) -> Result, tonic::Status> - { - Err(tonic::Status::unimplemented("not implemented in test")) - } - - async fn get_draft_history( - &self, - _request: tonic::Request, - ) -> Result, tonic::Status> - { - Err(tonic::Status::unimplemented("not implemented in test")) - } - - async fn connect_supervisor( - &self, - _request: tonic::Request>, - ) -> Result, tonic::Status> { - Err(tonic::Status::unimplemented("not implemented in test")) - } - - type RelayStreamStream = tokio_stream::wrappers::ReceiverStream< - Result, - >; - - async fn relay_stream( - &self, - _request: tonic::Request>, - ) -> Result, tonic::Status> { - Err(tonic::Status::unimplemented("not implemented in test")) - } - - type ForwardTcpStream = std::pin::Pin< - Box< - dyn tokio_stream::Stream< - Item = Result, - > + Send, - >, - >; - - async fn forward_tcp( - &self, - _request: tonic::Request>, - ) -> Result, tonic::Status> { - Err(tonic::Status::unimplemented("not implemented in test")) - } -} +use common::TestOpenShell; /// Test 7: Plaintext server (no TLS) accepts both gRPC and HTTP. /// diff --git a/crates/openshell-server/tests/common/mod.rs b/crates/openshell-server/tests/common/mod.rs new file mode 100644 index 000000000..b366c82a9 --- /dev/null +++ b/crates/openshell-server/tests/common/mod.rs @@ -0,0 +1,531 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Shared helpers for openshell-server integration tests. +//! +//! Include with `mod common;` at the top of each integration test file. +//! Items may not be used by every test file; the blanket `#[allow]` prevents +//! spurious dead-code warnings. + +#![allow(dead_code)] + +use hyper_util::{ + rt::{TokioExecutor, TokioIo}, + server::conn::auto::Builder, +}; +use openshell_core::proto::{ + CreateProviderRequest, CreateSandboxRequest, CreateSshSessionRequest, CreateSshSessionResponse, + DeleteProviderRequest, DeleteProviderResponse, DeleteSandboxRequest, DeleteSandboxResponse, + ExecSandboxEvent, ExecSandboxInput, ExecSandboxRequest, GatewayMessage, GetGatewayConfigRequest, + GetGatewayConfigResponse, GetProviderRequest, GetSandboxConfigRequest, + GetSandboxConfigResponse, GetSandboxProviderEnvironmentRequest, + GetSandboxProviderEnvironmentResponse, GetSandboxRequest, HealthRequest, HealthResponse, + ListProvidersRequest, ListProvidersResponse, ListSandboxesRequest, ListSandboxesResponse, + ProviderResponse, RelayFrame, RevokeSshSessionRequest, RevokeSshSessionResponse, + SandboxResponse, SandboxStreamEvent, ServiceStatus, SupervisorMessage, TcpForwardFrame, + UpdateProviderRequest, WatchSandboxRequest, + open_shell_server::{OpenShell, OpenShellServer}, +}; +use openshell_server::{MultiplexedService, TlsAcceptor, health_router}; +use rcgen::{CertificateParams, IsCa, KeyPair}; +use std::io::Write; +use std::net::SocketAddr; +use tempfile::tempdir; +use tokio::net::TcpListener; +use tokio::sync::mpsc; +use tokio_stream::wrappers::ReceiverStream; +use tonic::{Response, Status}; + +// --------------------------------------------------------------------------- +// Minimal OpenShell stub: all methods return defaults or Unimplemented. +// --------------------------------------------------------------------------- + +#[derive(Clone, Default)] +pub struct TestOpenShell; + +#[tonic::async_trait] +impl OpenShell for TestOpenShell { + async fn health( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new(HealthResponse { + status: ServiceStatus::Healthy.into(), + version: "test".to_string(), + })) + } + + async fn create_sandbox( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new(SandboxResponse::default())) + } + + async fn get_sandbox( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new(SandboxResponse::default())) + } + + async fn list_sandboxes( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new(ListSandboxesResponse::default())) + } + + async fn list_sandbox_providers( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new( + openshell_core::proto::ListSandboxProvidersResponse::default(), + )) + } + + async fn attach_sandbox_provider( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new( + openshell_core::proto::AttachSandboxProviderResponse::default(), + )) + } + + async fn detach_sandbox_provider( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new( + openshell_core::proto::DetachSandboxProviderResponse::default(), + )) + } + + async fn delete_sandbox( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new(DeleteSandboxResponse { deleted: true })) + } + + async fn get_sandbox_config( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new(GetSandboxConfigResponse::default())) + } + + async fn get_gateway_config( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new(GetGatewayConfigResponse::default())) + } + + async fn get_sandbox_provider_environment( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new( + GetSandboxProviderEnvironmentResponse::default(), + )) + } + + async fn create_ssh_session( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new(CreateSshSessionResponse::default())) + } + + async fn expose_service( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new( + openshell_core::proto::ServiceEndpointResponse::default(), + )) + } + + async fn get_service( + &self, + _: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn list_services( + &self, + _: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn delete_service( + &self, + _: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn revoke_ssh_session( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Ok(Response::new(RevokeSshSessionResponse::default())) + } + + async fn create_provider( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "create_provider not implemented in test", + )) + } + + async fn get_provider( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "get_provider not implemented in test", + )) + } + + async fn list_providers( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "list_providers not implemented in test", + )) + } + + async fn list_provider_profiles( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + + async fn get_provider_profile( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + + async fn import_provider_profiles( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + + async fn lint_provider_profiles( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + + async fn delete_provider_profile( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + + async fn update_provider( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "update_provider not implemented in test", + )) + } + + async fn delete_provider( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "delete_provider not implemented in test", + )) + } + + type WatchSandboxStream = ReceiverStream>; + type ExecSandboxStream = ReceiverStream>; + type ConnectSupervisorStream = ReceiverStream>; + + async fn watch_sandbox( + &self, + _request: tonic::Request, + ) -> Result, Status> { + let (_tx, rx) = mpsc::channel(1); + Ok(Response::new(ReceiverStream::new(rx))) + } + + async fn exec_sandbox( + &self, + _request: tonic::Request, + ) -> Result, Status> { + let (_tx, rx) = mpsc::channel(1); + Ok(Response::new(ReceiverStream::new(rx))) + } + + type ExecSandboxInteractiveStream = ReceiverStream>; + + async fn exec_sandbox_interactive( + &self, + _request: tonic::Request>, + ) -> Result, Status> { + let (_tx, rx) = mpsc::channel(1); + Ok(Response::new(ReceiverStream::new(rx))) + } + + async fn update_config( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + + async fn get_sandbox_policy_status( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + + async fn list_sandbox_policies( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + + async fn report_policy_status( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + + async fn get_sandbox_logs( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + + async fn push_sandbox_logs( + &self, + _request: tonic::Request>, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + + async fn submit_policy_analysis( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + + async fn get_draft_policy( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + + async fn approve_draft_chunk( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + + async fn reject_draft_chunk( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + + async fn approve_all_draft_chunks( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + + async fn edit_draft_chunk( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + + async fn undo_draft_chunk( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + + async fn clear_draft_chunks( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + + async fn get_draft_history( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + + async fn connect_supervisor( + &self, + _request: tonic::Request>, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + + type RelayStreamStream = ReceiverStream>; + + async fn relay_stream( + &self, + _request: tonic::Request>, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } + + type ForwardTcpStream = + std::pin::Pin> + Send>>; + + async fn forward_tcp( + &self, + _request: tonic::Request>, + ) -> Result, Status> { + Err(Status::unimplemented("not implemented in test")) + } +} + +// --------------------------------------------------------------------------- +// TLS / PKI helpers (used by TLS integration tests) +// --------------------------------------------------------------------------- + +/// Initialise the rustls crypto provider (idempotent). +pub fn install_rustls_provider() { + let _ = rustls::crypto::ring::default_provider().install_default(); +} + +/// PKI bundle: CA cert, server cert+key, client cert+key (all PEM). +#[allow(clippy::struct_field_names)] +pub struct PkiBundle { + pub ca_cert_pem: Vec, + pub server_cert_pem: Vec, + pub server_key_pem: Vec, + pub client_cert_pem: Vec, + pub client_key_pem: Vec, +} + +/// Generate a full PKI: CA → server cert (for `localhost`) + client cert. +/// Returns a `TempDir` that must be kept alive while the paths are in use. +pub fn generate_pki() -> (tempfile::TempDir, PkiBundle) { + // Generate CA + let mut ca_params = + CertificateParams::new(Vec::::new()).expect("failed to create CA params"); + ca_params.is_ca = IsCa::Ca(rcgen::BasicConstraints::Unconstrained); + ca_params + .distinguished_name + .push(rcgen::DnType::CommonName, "test-ca"); + let ca_key = KeyPair::generate().expect("failed to generate CA key"); + let ca_cert = ca_params + .self_signed(&ca_key) + .expect("failed to sign CA cert"); + + // Generate server cert signed by CA + let server_params = CertificateParams::new(vec!["localhost".to_string()]) + .expect("failed to create server params"); + let server_key = KeyPair::generate().expect("failed to generate server key"); + let server_cert = server_params + .signed_by(&server_key, &ca_cert, &ca_key) + .expect("failed to sign server cert"); + + // Generate client cert signed by CA + let mut client_params = + CertificateParams::new(Vec::::new()).expect("failed to create client params"); + client_params + .distinguished_name + .push(rcgen::DnType::CommonName, "test-client"); + let client_key = KeyPair::generate().expect("failed to generate client key"); + let client_cert = client_params + .signed_by(&client_key, &ca_cert, &ca_key) + .expect("failed to sign client cert"); + + let dir = tempdir().expect("failed to create tempdir"); + let write_file = |name: &str, data: &[u8]| { + let path = dir.path().join(name); + std::fs::File::create(&path) + .and_then(|mut f| f.write_all(data)) + .expect("failed to write file"); + }; + + write_file("ca.pem", ca_cert.pem().as_bytes()); + write_file("server-cert.pem", server_cert.pem().as_bytes()); + write_file("server-key.pem", server_key.serialize_pem().as_bytes()); + write_file("client-cert.pem", client_cert.pem().as_bytes()); + write_file("client-key.pem", client_key.serialize_pem().as_bytes()); + + let bundle = PkiBundle { + ca_cert_pem: ca_cert.pem().into_bytes(), + server_cert_pem: server_cert.pem().into_bytes(), + server_key_pem: server_key.serialize_pem().into_bytes(), + client_cert_pem: client_cert.pem().into_bytes(), + client_key_pem: client_key.serialize_pem().into_bytes(), + }; + + (dir, bundle) +} + +/// Start a TLS-wrapped test server using the given `TlsAcceptor`. +/// Returns the bound address and a task handle (abort to stop). +pub async fn start_test_server( + tls_acceptor: TlsAcceptor, +) -> (SocketAddr, tokio::task::JoinHandle<()>) { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + + let grpc_service = OpenShellServer::new(TestOpenShell); + let http_service = health_router(); + let service = MultiplexedService::new(grpc_service, http_service); + + let handle = tokio::spawn(async move { + loop { + let Ok((stream, _)) = listener.accept().await else { + continue; + }; + let svc = service.clone(); + let tls = tls_acceptor.clone(); + tokio::spawn(async move { + let Ok(tls_stream) = tls.inner().accept(stream).await else { + return; + }; + let _ = Builder::new(TokioExecutor::new()) + .serve_connection(TokioIo::new(tls_stream), svc) + .await; + }); + } + }); + + (addr, handle) +} diff --git a/crates/openshell-server/tests/edge_tunnel_auth.rs b/crates/openshell-server/tests/edge_tunnel_auth.rs index 73ad0aff0..0ec4ca106 100644 --- a/crates/openshell-server/tests/edge_tunnel_auth.rs +++ b/crates/openshell-server/tests/edge_tunnel_auth.rs @@ -24,520 +24,27 @@ //! validated when present (rogue-CA certs are rejected) but never required. //! Authentication is handled at the application layer (OIDC bearer tokens). +mod common; + use bytes::Bytes; +use common::{PkiBundle, generate_pki, install_rustls_provider, start_test_server}; use http_body_util::Empty; use hyper::{Request, StatusCode}; use hyper_rustls::HttpsConnectorBuilder; -use hyper_util::{ - client::legacy::Client, - rt::{TokioExecutor, TokioIo}, - server::conn::auto::Builder, -}; -use openshell_core::proto::{ - CreateProviderRequest, CreateSandboxRequest, CreateSshSessionRequest, CreateSshSessionResponse, - DeleteProviderRequest, DeleteProviderResponse, DeleteSandboxRequest, DeleteSandboxResponse, - ExecSandboxEvent, ExecSandboxInput, ExecSandboxRequest, GatewayMessage, - GetGatewayConfigRequest, GetGatewayConfigResponse, GetProviderRequest, GetSandboxConfigRequest, - GetSandboxConfigResponse, GetSandboxProviderEnvironmentRequest, - GetSandboxProviderEnvironmentResponse, GetSandboxRequest, HealthRequest, HealthResponse, - ListProvidersRequest, ListProvidersResponse, ListSandboxesRequest, ListSandboxesResponse, - ProviderResponse, RelayFrame, RevokeSshSessionRequest, RevokeSshSessionResponse, - SandboxResponse, SandboxStreamEvent, ServiceStatus, SupervisorMessage, TcpForwardFrame, - UpdateProviderRequest, WatchSandboxRequest, - open_shell_client::OpenShellClient, - open_shell_server::{OpenShell, OpenShellServer}, -}; -use openshell_server::{MultiplexedService, TlsAcceptor, health_router}; +use hyper_util::{client::legacy::Client, rt::TokioExecutor}; +use openshell_core::proto::{HealthRequest, ServiceStatus, open_shell_client::OpenShellClient}; +use openshell_server::TlsAcceptor; use rcgen::{CertificateParams, IsCa, KeyPair}; use rustls::RootCertStore; use rustls::pki_types::CertificateDer; use rustls_pemfile::certs; -use std::io::Write; -use tempfile::tempdir; -use tokio::net::TcpListener; -use tokio::sync::mpsc; -use tokio_stream::wrappers::ReceiverStream; +use tonic::Status; use tonic::transport::{Channel, ClientTlsConfig, Endpoint}; -use tonic::{Response, Status}; // --------------------------------------------------------------------------- -// Helpers +// Client helpers // --------------------------------------------------------------------------- -fn install_rustls_provider() { - let _ = rustls::crypto::ring::default_provider().install_default(); -} - -/// Minimal `OpenShell` implementation for testing. -#[derive(Clone, Default)] -struct TestOpenShell; - -#[tonic::async_trait] -impl OpenShell for TestOpenShell { - async fn health( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(HealthResponse { - status: ServiceStatus::Healthy.into(), - version: "test".to_string(), - })) - } - - async fn create_sandbox( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(SandboxResponse::default())) - } - - async fn get_sandbox( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(SandboxResponse::default())) - } - - async fn list_sandboxes( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(ListSandboxesResponse::default())) - } - - async fn list_sandbox_providers( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new( - openshell_core::proto::ListSandboxProvidersResponse::default(), - )) - } - - async fn attach_sandbox_provider( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new( - openshell_core::proto::AttachSandboxProviderResponse::default(), - )) - } - - async fn detach_sandbox_provider( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new( - openshell_core::proto::DetachSandboxProviderResponse::default(), - )) - } - - async fn delete_sandbox( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(DeleteSandboxResponse { deleted: true })) - } - - async fn get_sandbox_config( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(GetSandboxConfigResponse::default())) - } - - async fn get_gateway_config( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(GetGatewayConfigResponse::default())) - } - - async fn get_sandbox_provider_environment( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new( - GetSandboxProviderEnvironmentResponse::default(), - )) - } - - async fn create_ssh_session( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(CreateSshSessionResponse::default())) - } - - async fn expose_service( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new( - openshell_core::proto::ServiceEndpointResponse::default(), - )) - } - - async fn get_service( - &self, - _: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("unused")) - } - - async fn list_services( - &self, - _: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("unused")) - } - - async fn delete_service( - &self, - _: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("unused")) - } - - async fn revoke_ssh_session( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(RevokeSshSessionResponse::default())) - } - - async fn create_provider( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn get_provider( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn list_providers( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn list_provider_profiles( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn get_provider_profile( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn import_provider_profiles( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn lint_provider_profiles( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn delete_provider_profile( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn update_provider( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn delete_provider( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - type WatchSandboxStream = ReceiverStream>; - type ExecSandboxStream = ReceiverStream>; - type ConnectSupervisorStream = ReceiverStream>; - - async fn watch_sandbox( - &self, - _request: tonic::Request, - ) -> Result, Status> { - let (_tx, rx) = mpsc::channel(1); - Ok(Response::new(ReceiverStream::new(rx))) - } - - async fn exec_sandbox( - &self, - _request: tonic::Request, - ) -> Result, Status> { - let (_tx, rx) = mpsc::channel(1); - Ok(Response::new(ReceiverStream::new(rx))) - } - - type ExecSandboxInteractiveStream = ReceiverStream>; - async fn exec_sandbox_interactive( - &self, - _request: tonic::Request>, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn update_config( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn get_sandbox_policy_status( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn list_sandbox_policies( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn report_policy_status( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn get_sandbox_logs( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn push_sandbox_logs( - &self, - _request: tonic::Request>, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn submit_policy_analysis( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn get_draft_policy( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn approve_draft_chunk( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn reject_draft_chunk( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn approve_all_draft_chunks( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn edit_draft_chunk( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn undo_draft_chunk( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn clear_draft_chunks( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn get_draft_history( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn connect_supervisor( - &self, - _request: tonic::Request>, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - type RelayStreamStream = ReceiverStream>; - - async fn relay_stream( - &self, - _request: tonic::Request>, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - type ForwardTcpStream = - std::pin::Pin> + Send>>; - - async fn forward_tcp( - &self, - _request: tonic::Request>, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } -} - -// --------------------------------------------------------------------------- -// PKI generation -// --------------------------------------------------------------------------- - -#[allow(dead_code, clippy::struct_field_names)] -struct PkiBundle { - ca_cert_pem: Vec, - server_cert_pem: Vec, - server_key_pem: Vec, - client_cert_pem: Vec, - client_key_pem: Vec, -} - -fn generate_pki() -> (tempfile::TempDir, PkiBundle) { - let mut ca_params = - CertificateParams::new(Vec::::new()).expect("failed to create CA params"); - ca_params.is_ca = IsCa::Ca(rcgen::BasicConstraints::Unconstrained); - ca_params - .distinguished_name - .push(rcgen::DnType::CommonName, "test-ca"); - let ca_key = KeyPair::generate().expect("failed to generate CA key"); - let ca_cert = ca_params - .self_signed(&ca_key) - .expect("failed to sign CA cert"); - - let server_params = CertificateParams::new(vec!["localhost".to_string()]) - .expect("failed to create server params"); - let server_key = KeyPair::generate().expect("failed to generate server key"); - let server_cert = server_params - .signed_by(&server_key, &ca_cert, &ca_key) - .expect("failed to sign server cert"); - - let mut client_params = - CertificateParams::new(Vec::::new()).expect("failed to create client params"); - client_params - .distinguished_name - .push(rcgen::DnType::CommonName, "test-client"); - let client_key = KeyPair::generate().expect("failed to generate client key"); - let client_cert = client_params - .signed_by(&client_key, &ca_cert, &ca_key) - .expect("failed to sign client cert"); - - let dir = tempdir().expect("failed to create tempdir"); - let write_file = |name: &str, data: &[u8]| { - let path = dir.path().join(name); - std::fs::File::create(&path) - .and_then(|mut f| f.write_all(data)) - .expect("failed to write file"); - }; - - write_file("ca.pem", ca_cert.pem().as_bytes()); - write_file("server-cert.pem", server_cert.pem().as_bytes()); - write_file("server-key.pem", server_key.serialize_pem().as_bytes()); - write_file("client-cert.pem", client_cert.pem().as_bytes()); - write_file("client-key.pem", client_key.serialize_pem().as_bytes()); - - let bundle = PkiBundle { - ca_cert_pem: ca_cert.pem().into_bytes(), - server_cert_pem: server_cert.pem().into_bytes(), - server_key_pem: server_key.serialize_pem().into_bytes(), - client_cert_pem: client_cert.pem().into_bytes(), - client_key_pem: client_key.serialize_pem().into_bytes(), - }; - - (dir, bundle) -} - -// --------------------------------------------------------------------------- -// Server + client helpers -// --------------------------------------------------------------------------- - -async fn start_test_server( - tls_acceptor: TlsAcceptor, -) -> (std::net::SocketAddr, tokio::task::JoinHandle<()>) { - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - - let grpc_service = OpenShellServer::new(TestOpenShell); - let http_service = health_router(); - let service = MultiplexedService::new(grpc_service, http_service); - - let handle = tokio::spawn(async move { - loop { - let Ok((stream, _)) = listener.accept().await else { - continue; - }; - let svc = service.clone(); - let tls = tls_acceptor.clone(); - tokio::spawn(async move { - let Ok(tls_stream) = tls.inner().accept(stream).await else { - return; - }; - let _ = Builder::new(TokioExecutor::new()) - .serve_connection(TokioIo::new(tls_stream), svc) - .await; - }); - } - }); - - (addr, handle) -} - /// Build a gRPC client with mTLS (CA + client cert). async fn grpc_client_mtls( addr: std::net::SocketAddr, diff --git a/crates/openshell-server/tests/multiplex_integration.rs b/crates/openshell-server/tests/multiplex_integration.rs index 14a63c566..9ca1ee3ee 100644 --- a/crates/openshell-server/tests/multiplex_integration.rs +++ b/crates/openshell-server/tests/multiplex_integration.rs @@ -1,7 +1,10 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +mod common; + use bytes::Bytes; +use common::TestOpenShell; use http_body_util::Empty; use hyper::{Request, StatusCode}; use hyper_util::{ @@ -9,401 +12,11 @@ use hyper_util::{ server::conn::auto::Builder, }; use openshell_core::proto::{ - CreateProviderRequest, CreateSandboxRequest, CreateSshSessionRequest, CreateSshSessionResponse, - DeleteProviderRequest, DeleteProviderResponse, DeleteSandboxRequest, DeleteSandboxResponse, - ExecSandboxEvent, ExecSandboxInput, ExecSandboxRequest, GatewayMessage, - GetGatewayConfigRequest, GetGatewayConfigResponse, GetProviderRequest, GetSandboxConfigRequest, - GetSandboxConfigResponse, GetSandboxProviderEnvironmentRequest, - GetSandboxProviderEnvironmentResponse, GetSandboxRequest, HealthRequest, HealthResponse, - ListProvidersRequest, ListProvidersResponse, ListSandboxesRequest, ListSandboxesResponse, - ProviderResponse, RelayFrame, RevokeSshSessionRequest, RevokeSshSessionResponse, - SandboxResponse, SandboxStreamEvent, ServiceStatus, SupervisorMessage, TcpForwardFrame, - UpdateProviderRequest, WatchSandboxRequest, - open_shell_client::OpenShellClient, - open_shell_server::{OpenShell, OpenShellServer}, + HealthRequest, ServiceStatus, open_shell_client::OpenShellClient, + open_shell_server::OpenShellServer, }; use openshell_server::{MultiplexedService, health_router}; use tokio::net::TcpListener; -use tokio::sync::mpsc; -use tokio_stream::wrappers::ReceiverStream; -use tonic::{Response, Status}; - -#[derive(Clone, Default)] -struct TestOpenShell; - -#[tonic::async_trait] -impl OpenShell for TestOpenShell { - async fn health( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(HealthResponse { - status: ServiceStatus::Healthy.into(), - version: "test".to_string(), - })) - } - - async fn create_sandbox( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(SandboxResponse::default())) - } - - async fn get_sandbox( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(SandboxResponse::default())) - } - - async fn list_sandboxes( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(ListSandboxesResponse::default())) - } - - async fn list_sandbox_providers( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new( - openshell_core::proto::ListSandboxProvidersResponse::default(), - )) - } - - async fn attach_sandbox_provider( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new( - openshell_core::proto::AttachSandboxProviderResponse::default(), - )) - } - - async fn detach_sandbox_provider( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new( - openshell_core::proto::DetachSandboxProviderResponse::default(), - )) - } - - async fn delete_sandbox( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(DeleteSandboxResponse { deleted: true })) - } - - async fn get_sandbox_config( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(GetSandboxConfigResponse::default())) - } - - async fn get_gateway_config( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(GetGatewayConfigResponse::default())) - } - - async fn get_sandbox_provider_environment( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new( - GetSandboxProviderEnvironmentResponse::default(), - )) - } - - async fn create_ssh_session( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(CreateSshSessionResponse::default())) - } - - async fn expose_service( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new( - openshell_core::proto::ServiceEndpointResponse::default(), - )) - } - - async fn get_service( - &self, - _: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("unused")) - } - - async fn list_services( - &self, - _: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("unused")) - } - - async fn delete_service( - &self, - _: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("unused")) - } - - async fn revoke_ssh_session( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(RevokeSshSessionResponse::default())) - } - - async fn create_provider( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented( - "create_provider not implemented in test", - )) - } - - async fn get_provider( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented( - "get_provider not implemented in test", - )) - } - - async fn list_providers( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented( - "list_providers not implemented in test", - )) - } - - async fn list_provider_profiles( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn get_provider_profile( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn import_provider_profiles( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn lint_provider_profiles( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn delete_provider_profile( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn update_provider( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented( - "update_provider not implemented in test", - )) - } - - async fn delete_provider( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented( - "delete_provider not implemented in test", - )) - } - - type WatchSandboxStream = ReceiverStream>; - type ExecSandboxStream = ReceiverStream>; - type ConnectSupervisorStream = ReceiverStream>; - - async fn watch_sandbox( - &self, - _request: tonic::Request, - ) -> Result, Status> { - let (_tx, rx) = mpsc::channel(1); - Ok(Response::new(ReceiverStream::new(rx))) - } - - async fn exec_sandbox( - &self, - _request: tonic::Request, - ) -> Result, Status> { - let (_tx, rx) = mpsc::channel(1); - Ok(Response::new(ReceiverStream::new(rx))) - } - - type ExecSandboxInteractiveStream = ReceiverStream>; - async fn exec_sandbox_interactive( - &self, - _request: tonic::Request>, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn update_config( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn get_sandbox_policy_status( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn list_sandbox_policies( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn report_policy_status( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn get_sandbox_logs( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn push_sandbox_logs( - &self, - _request: tonic::Request>, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn submit_policy_analysis( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn get_draft_policy( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn approve_draft_chunk( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn reject_draft_chunk( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn approve_all_draft_chunks( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn edit_draft_chunk( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn undo_draft_chunk( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn clear_draft_chunks( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn get_draft_history( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn connect_supervisor( - &self, - _request: tonic::Request>, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - type RelayStreamStream = ReceiverStream>; - - async fn relay_stream( - &self, - _request: tonic::Request>, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - type ForwardTcpStream = - std::pin::Pin> + Send>>; - - async fn forward_tcp( - &self, - _request: tonic::Request>, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } -} #[tokio::test] async fn serves_grpc_and_http_on_same_port() { diff --git a/crates/openshell-server/tests/multiplex_tls_integration.rs b/crates/openshell-server/tests/multiplex_tls_integration.rs index 00ed1657f..bc6949f42 100644 --- a/crates/openshell-server/tests/multiplex_tls_integration.rs +++ b/crates/openshell-server/tests/multiplex_tls_integration.rs @@ -1,522 +1,32 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +mod common; + use bytes::Bytes; +use common::{PkiBundle, generate_pki, install_rustls_provider, start_test_server}; use http_body_util::Empty; -use hyper::{Request, StatusCode}; +use hyper::Request; +use hyper::StatusCode; use hyper_rustls::HttpsConnectorBuilder; -use hyper_util::{ - client::legacy::Client, - rt::{TokioExecutor, TokioIo}, - server::conn::auto::Builder, -}; -use openshell_core::proto::{ - CreateProviderRequest, CreateSandboxRequest, CreateSshSessionRequest, CreateSshSessionResponse, - DeleteProviderRequest, DeleteProviderResponse, DeleteSandboxRequest, DeleteSandboxResponse, - ExecSandboxEvent, ExecSandboxInput, ExecSandboxRequest, GatewayMessage, - GetGatewayConfigRequest, GetGatewayConfigResponse, GetProviderRequest, GetSandboxConfigRequest, - GetSandboxConfigResponse, GetSandboxProviderEnvironmentRequest, - GetSandboxProviderEnvironmentResponse, GetSandboxRequest, HealthRequest, HealthResponse, - ListProvidersRequest, ListProvidersResponse, ListSandboxesRequest, ListSandboxesResponse, - ProviderResponse, RelayFrame, RevokeSshSessionRequest, RevokeSshSessionResponse, - SandboxResponse, SandboxStreamEvent, ServiceStatus, SupervisorMessage, TcpForwardFrame, - UpdateProviderRequest, WatchSandboxRequest, - open_shell_client::OpenShellClient, - open_shell_server::{OpenShell, OpenShellServer}, -}; -use openshell_server::{MultiplexedService, TlsAcceptor, health_router}; +use hyper_util::{client::legacy::Client, rt::TokioExecutor}; +use openshell_core::proto::{HealthRequest, ServiceStatus, open_shell_client::OpenShellClient}; use rcgen::{CertificateParams, IsCa, KeyPair}; use rustls::RootCertStore; use rustls::pki_types::CertificateDer; use rustls_pemfile::certs; -use std::io::Write; -use tempfile::tempdir; -use tokio::net::TcpListener; -use tokio::sync::mpsc; -use tokio_stream::wrappers::ReceiverStream; use tonic::transport::{Channel, ClientTlsConfig, Endpoint}; -use tonic::{Response, Status}; - -fn install_rustls_provider() { - let _ = rustls::crypto::ring::default_provider().install_default(); -} - -#[derive(Clone, Default)] -struct TestOpenShell; - -#[tonic::async_trait] -impl OpenShell for TestOpenShell { - async fn health( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(HealthResponse { - status: ServiceStatus::Healthy.into(), - version: "test".to_string(), - })) - } - - async fn create_sandbox( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(SandboxResponse::default())) - } - - async fn get_sandbox( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(SandboxResponse::default())) - } - - async fn list_sandboxes( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(ListSandboxesResponse::default())) - } - - async fn list_sandbox_providers( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new( - openshell_core::proto::ListSandboxProvidersResponse::default(), - )) - } - - async fn attach_sandbox_provider( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new( - openshell_core::proto::AttachSandboxProviderResponse::default(), - )) - } - - async fn detach_sandbox_provider( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new( - openshell_core::proto::DetachSandboxProviderResponse::default(), - )) - } - - async fn delete_sandbox( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(DeleteSandboxResponse { deleted: true })) - } - - async fn get_sandbox_config( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(GetSandboxConfigResponse::default())) - } - - async fn get_gateway_config( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(GetGatewayConfigResponse::default())) - } - - async fn get_sandbox_provider_environment( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new( - GetSandboxProviderEnvironmentResponse::default(), - )) - } - - async fn create_ssh_session( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(CreateSshSessionResponse::default())) - } - - async fn expose_service( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new( - openshell_core::proto::ServiceEndpointResponse::default(), - )) - } - - async fn get_service( - &self, - _: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("unused")) - } - - async fn list_services( - &self, - _: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("unused")) - } - - async fn delete_service( - &self, - _: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("unused")) - } - - async fn revoke_ssh_session( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(RevokeSshSessionResponse::default())) - } - - async fn create_provider( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented( - "create_provider not implemented in test", - )) - } - - async fn get_provider( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented( - "get_provider not implemented in test", - )) - } - - async fn list_providers( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented( - "list_providers not implemented in test", - )) - } - - async fn list_provider_profiles( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn get_provider_profile( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn import_provider_profiles( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn lint_provider_profiles( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn delete_provider_profile( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn update_provider( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented( - "update_provider not implemented in test", - )) - } - - async fn delete_provider( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented( - "delete_provider not implemented in test", - )) - } - - type WatchSandboxStream = ReceiverStream>; - type ExecSandboxStream = ReceiverStream>; - type ConnectSupervisorStream = ReceiverStream>; - - async fn watch_sandbox( - &self, - _request: tonic::Request, - ) -> Result, Status> { - let (_tx, rx) = mpsc::channel(1); - Ok(Response::new(ReceiverStream::new(rx))) - } - - async fn exec_sandbox( - &self, - _request: tonic::Request, - ) -> Result, Status> { - let (_tx, rx) = mpsc::channel(1); - Ok(Response::new(ReceiverStream::new(rx))) - } - - type ExecSandboxInteractiveStream = ReceiverStream>; - async fn exec_sandbox_interactive( - &self, - _request: tonic::Request>, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn update_config( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn get_sandbox_policy_status( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - async fn list_sandbox_policies( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn report_policy_status( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn get_sandbox_logs( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn push_sandbox_logs( - &self, - _request: tonic::Request>, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn submit_policy_analysis( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn get_draft_policy( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn approve_draft_chunk( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn reject_draft_chunk( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn approve_all_draft_chunks( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn edit_draft_chunk( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn undo_draft_chunk( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn clear_draft_chunks( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn get_draft_history( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn connect_supervisor( - &self, - _request: tonic::Request>, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - type RelayStreamStream = ReceiverStream>; - - async fn relay_stream( - &self, - _request: tonic::Request>, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - type ForwardTcpStream = - std::pin::Pin> + Send>>; - - async fn forward_tcp( - &self, - _request: tonic::Request>, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) +fn build_tls_root(cert_pem: &[u8]) -> RootCertStore { + let mut roots = RootCertStore::empty(); + let mut cursor = std::io::Cursor::new(cert_pem); + let parsed = certs(&mut cursor) + .collect::>, _>>() + .expect("failed to parse cert pem"); + for cert in parsed { + roots.add(cert).expect("failed to add cert"); } -} - -/// PKI bundle: CA cert, server cert+key, client cert+key. -#[allow(dead_code, clippy::struct_field_names)] -struct PkiBundle { - ca_cert_pem: Vec, - server_cert_pem: Vec, - server_key_pem: Vec, - client_cert_pem: Vec, - client_key_pem: Vec, -} - -/// Generate a full PKI: CA -> server cert (for localhost) + client cert. -fn generate_pki() -> (tempfile::TempDir, PkiBundle) { - // Generate CA - let mut ca_params = - CertificateParams::new(Vec::::new()).expect("failed to create CA params"); - ca_params.is_ca = IsCa::Ca(rcgen::BasicConstraints::Unconstrained); - ca_params - .distinguished_name - .push(rcgen::DnType::CommonName, "test-ca"); - let ca_key = KeyPair::generate().expect("failed to generate CA key"); - let ca_cert = ca_params - .self_signed(&ca_key) - .expect("failed to sign CA cert"); - - // Generate server cert signed by CA - let server_params = CertificateParams::new(vec!["localhost".to_string()]) - .expect("failed to create server params"); - let server_key = KeyPair::generate().expect("failed to generate server key"); - let server_cert = server_params - .signed_by(&server_key, &ca_cert, &ca_key) - .expect("failed to sign server cert"); - - // Generate client cert signed by CA - let mut client_params = - CertificateParams::new(Vec::::new()).expect("failed to create client params"); - client_params - .distinguished_name - .push(rcgen::DnType::CommonName, "test-client"); - let client_key = KeyPair::generate().expect("failed to generate client key"); - let client_cert = client_params - .signed_by(&client_key, &ca_cert, &ca_key) - .expect("failed to sign client cert"); - - let dir = tempdir().expect("failed to create tempdir"); - let write = |name: &str, data: &[u8]| { - let path = dir.path().join(name); - std::fs::File::create(&path) - .and_then(|mut f| f.write_all(data)) - .expect("failed to write file"); - }; - - write("ca.pem", ca_cert.pem().as_bytes()); - write("server-cert.pem", server_cert.pem().as_bytes()); - write("server-key.pem", server_key.serialize_pem().as_bytes()); - write("client-cert.pem", client_cert.pem().as_bytes()); - write("client-key.pem", client_key.serialize_pem().as_bytes()); - - let bundle = PkiBundle { - ca_cert_pem: ca_cert.pem().into_bytes(), - server_cert_pem: server_cert.pem().into_bytes(), - server_key_pem: server_key.serialize_pem().into_bytes(), - client_cert_pem: client_cert.pem().into_bytes(), - client_key_pem: client_key.serialize_pem().into_bytes(), - }; - - (dir, bundle) -} - -/// Start a test server with the given TLS acceptor, returning its address and -/// a handle that aborts the server on drop. -async fn start_test_server( - tls_acceptor: TlsAcceptor, -) -> (std::net::SocketAddr, tokio::task::JoinHandle<()>) { - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - - let grpc_service = OpenShellServer::new(TestOpenShell); - let http_service = health_router(); - let service = MultiplexedService::new(grpc_service, http_service); - - let handle = tokio::spawn(async move { - loop { - let Ok((stream, _)) = listener.accept().await else { - continue; - }; - let svc = service.clone(); - let tls = tls_acceptor.clone(); - tokio::spawn(async move { - let Ok(tls_stream) = tls.inner().accept(stream).await else { - return; - }; - let _ = Builder::new(TokioExecutor::new()) - .serve_connection(TokioIo::new(tls_stream), svc) - .await; - }); - } - }); - - (addr, handle) + roots } /// Build a gRPC client with mTLS (CA + client cert). @@ -540,18 +50,6 @@ async fn grpc_client_mtls( OpenShellClient::new(channel) } -fn build_tls_root(cert_pem: &[u8]) -> RootCertStore { - let mut roots = RootCertStore::empty(); - let mut cursor = std::io::Cursor::new(cert_pem); - let parsed = certs(&mut cursor) - .collect::>, _>>() - .expect("failed to parse cert pem"); - for cert in parsed { - roots.add(cert).expect("failed to add cert"); - } - roots -} - /// Build an HTTPS client with mTLS (CA trust + client cert/key). fn https_client_mtls( pki: &PkiBundle, @@ -591,7 +89,7 @@ async fn serves_grpc_and_http_over_tls_on_same_port() { install_rustls_provider(); let (temp, pki) = generate_pki(); - let tls_acceptor = TlsAcceptor::from_files( + let tls_acceptor = openshell_server::TlsAcceptor::from_files( &temp.path().join("server-cert.pem"), &temp.path().join("server-key.pem"), Some(temp.path().join("ca.pem").as_path()), @@ -630,7 +128,7 @@ async fn mtls_valid_client_cert_accepted() { install_rustls_provider(); let (temp, pki) = generate_pki(); - let tls_acceptor = TlsAcceptor::from_files( + let tls_acceptor = openshell_server::TlsAcceptor::from_files( &temp.path().join("server-cert.pem"), &temp.path().join("server-key.pem"), Some(temp.path().join("ca.pem").as_path()), @@ -658,7 +156,7 @@ async fn no_client_cert_accepted_with_ca() { install_rustls_provider(); let (temp, pki) = generate_pki(); - let tls_acceptor = TlsAcceptor::from_files( + let tls_acceptor = openshell_server::TlsAcceptor::from_files( &temp.path().join("server-cert.pem"), &temp.path().join("server-key.pem"), Some(temp.path().join("ca.pem").as_path()), @@ -694,7 +192,7 @@ async fn no_client_cert_rejected_when_required() { install_rustls_provider(); let (temp, pki) = generate_pki(); - let tls_acceptor = TlsAcceptor::from_files( + let tls_acceptor = openshell_server::TlsAcceptor::from_files( &temp.path().join("server-cert.pem"), &temp.path().join("server-key.pem"), Some(temp.path().join("ca.pem").as_path()), @@ -731,7 +229,7 @@ async fn mtls_wrong_ca_client_cert_rejected() { install_rustls_provider(); let (temp, pki) = generate_pki(); - let tls_acceptor = TlsAcceptor::from_files( + let tls_acceptor = openshell_server::TlsAcceptor::from_files( &temp.path().join("server-cert.pem"), &temp.path().join("server-key.pem"), Some(temp.path().join("ca.pem").as_path()), diff --git a/crates/openshell-server/tests/ws_tunnel_integration.rs b/crates/openshell-server/tests/ws_tunnel_integration.rs index 277cffb51..ee253e9dd 100644 --- a/crates/openshell-server/tests/ws_tunnel_integration.rs +++ b/crates/openshell-server/tests/ws_tunnel_integration.rs @@ -23,6 +23,8 @@ //! The WS tunnel handler is kept standalone so it stays isolated from the full //! `ServerState` dependency while still matching the production bridge logic. +mod common; + use axum::{ Router, extract::{State, WebSocketUpgrade, ws::Message}, @@ -30,6 +32,7 @@ use axum::{ routing::get, }; use bytes::Bytes; +use common::TestOpenShell; use futures_util::{SinkExt, StreamExt}; use http_body_util::Empty; use hyper::{Request, StatusCode}; @@ -38,398 +41,14 @@ use hyper_util::{ server::conn::auto::Builder, }; use openshell_core::proto::{ - CreateProviderRequest, CreateSandboxRequest, CreateSshSessionRequest, CreateSshSessionResponse, - DeleteProviderRequest, DeleteProviderResponse, DeleteSandboxRequest, DeleteSandboxResponse, - ExecSandboxEvent, ExecSandboxInput, ExecSandboxRequest, GatewayMessage, - GetGatewayConfigRequest, GetGatewayConfigResponse, GetProviderRequest, GetSandboxConfigRequest, - GetSandboxConfigResponse, GetSandboxProviderEnvironmentRequest, - GetSandboxProviderEnvironmentResponse, GetSandboxRequest, HealthRequest, HealthResponse, - ListProvidersRequest, ListProvidersResponse, ListSandboxesRequest, ListSandboxesResponse, - ProviderResponse, RelayFrame, RevokeSshSessionRequest, RevokeSshSessionResponse, - SandboxResponse, SandboxStreamEvent, ServiceStatus, SupervisorMessage, TcpForwardFrame, - UpdateProviderRequest, WatchSandboxRequest, - open_shell_client::OpenShellClient, - open_shell_server::{OpenShell, OpenShellServer}, + HealthRequest, ServiceStatus, open_shell_client::OpenShellClient, + open_shell_server::OpenShellServer, }; use openshell_server::{MultiplexedService, health_router}; use std::net::SocketAddr; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tokio::net::{TcpListener, TcpStream}; -use tokio::sync::mpsc; -use tokio_stream::wrappers::ReceiverStream; use tokio_tungstenite::tungstenite; -use tonic::{Response, Status}; - -// --------------------------------------------------------------------------- -// Minimal OpenShell implementation (same as other integration tests) -// --------------------------------------------------------------------------- - -#[derive(Clone, Default)] -struct TestOpenShell; - -#[tonic::async_trait] -impl OpenShell for TestOpenShell { - async fn health( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(HealthResponse { - status: ServiceStatus::Healthy.into(), - version: "test".to_string(), - })) - } - - async fn create_sandbox( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(SandboxResponse::default())) - } - - async fn get_sandbox( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(SandboxResponse::default())) - } - - async fn list_sandboxes( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(ListSandboxesResponse::default())) - } - - async fn list_sandbox_providers( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new( - openshell_core::proto::ListSandboxProvidersResponse::default(), - )) - } - - async fn attach_sandbox_provider( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new( - openshell_core::proto::AttachSandboxProviderResponse::default(), - )) - } - - async fn detach_sandbox_provider( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new( - openshell_core::proto::DetachSandboxProviderResponse::default(), - )) - } - - async fn delete_sandbox( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(DeleteSandboxResponse { deleted: true })) - } - - async fn get_sandbox_config( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(GetSandboxConfigResponse::default())) - } - - async fn get_gateway_config( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(GetGatewayConfigResponse::default())) - } - - async fn get_sandbox_provider_environment( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new( - GetSandboxProviderEnvironmentResponse::default(), - )) - } - - async fn create_ssh_session( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(CreateSshSessionResponse::default())) - } - - async fn expose_service( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new( - openshell_core::proto::ServiceEndpointResponse::default(), - )) - } - - async fn get_service( - &self, - _: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("unused")) - } - - async fn list_services( - &self, - _: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("unused")) - } - - async fn delete_service( - &self, - _: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("unused")) - } - - async fn revoke_ssh_session( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Ok(Response::new(RevokeSshSessionResponse::default())) - } - - async fn create_provider( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn get_provider( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn list_providers( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn list_provider_profiles( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn get_provider_profile( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn import_provider_profiles( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn lint_provider_profiles( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn delete_provider_profile( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn update_provider( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn delete_provider( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - type WatchSandboxStream = ReceiverStream>; - type ExecSandboxStream = ReceiverStream>; - type ConnectSupervisorStream = ReceiverStream>; - - async fn watch_sandbox( - &self, - _request: tonic::Request, - ) -> Result, Status> { - let (_tx, rx) = mpsc::channel(1); - Ok(Response::new(ReceiverStream::new(rx))) - } - - async fn exec_sandbox( - &self, - _request: tonic::Request, - ) -> Result, Status> { - let (_tx, rx) = mpsc::channel(1); - Ok(Response::new(ReceiverStream::new(rx))) - } - - type ExecSandboxInteractiveStream = ReceiverStream>; - async fn exec_sandbox_interactive( - &self, - _request: tonic::Request>, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn update_config( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn get_sandbox_policy_status( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn list_sandbox_policies( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn report_policy_status( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn get_sandbox_logs( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn push_sandbox_logs( - &self, - _request: tonic::Request>, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn submit_policy_analysis( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn get_draft_policy( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn approve_draft_chunk( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn reject_draft_chunk( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn approve_all_draft_chunks( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn edit_draft_chunk( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn undo_draft_chunk( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn clear_draft_chunks( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn get_draft_history( - &self, - _request: tonic::Request, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - async fn connect_supervisor( - &self, - _request: tonic::Request>, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - type RelayStreamStream = ReceiverStream>; - - async fn relay_stream( - &self, - _request: tonic::Request>, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } - - type ForwardTcpStream = - std::pin::Pin> + Send>>; - - async fn forward_tcp( - &self, - _request: tonic::Request>, - ) -> Result, Status> { - Err(Status::unimplemented("not implemented in test")) - } -} // --------------------------------------------------------------------------- // Test WS tunnel handler (standalone, no ServerState dependency)