summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorEmīls <emils@mullvad.net>2026-03-03 15:41:04 +0100
committerEmīls <emils@mullvad.net>2026-03-03 15:41:04 +0100
commit8fc0058acfa0a7b5079a5ca8b44a7408df0fde52 (patch)
tree791fa631d3ea993a690e605c85d7ac5a6312328b
parent4e11af81d866652d6cedd2874ee6d7014858d020 (diff)
downloadmullvadvpn-domain-fronting-server.tar.xz
mullvadvpn-domain-fronting-server.zip
Add more server testsdomain-fronting-server
-rw-r--r--mullvad-api/src/domain_fronting/server.rs95
1 files changed, 95 insertions, 0 deletions
diff --git a/mullvad-api/src/domain_fronting/server.rs b/mullvad-api/src/domain_fronting/server.rs
index 4ef8c93b18..f02dce44e5 100644
--- a/mullvad-api/src/domain_fronting/server.rs
+++ b/mullvad-api/src/domain_fronting/server.rs
@@ -305,3 +305,98 @@ impl SessionCommand {
let _ = self.return_tx.send(received_bytes);
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use tokio::io::DuplexStream;
+
+ /// Mock connector that returns pre-configured duplex streams.
+ #[derive(Clone)]
+ struct MockConnector {
+ streams: Arc<tokio::sync::Mutex<Vec<DuplexStream>>>,
+ }
+
+ impl MockConnector {
+ fn new(streams: Vec<DuplexStream>) -> Self {
+ Self {
+ streams: Arc::new(tokio::sync::Mutex::new(streams)),
+ }
+ }
+ }
+
+ impl UpstreamConnector for MockConnector {
+ type Stream = DuplexStream;
+
+ async fn connect(&self, _addr: SocketAddr) -> io::Result<DuplexStream> {
+ self.streams.lock().await.pop().ok_or_else(|| {
+ io::Error::new(io::ErrorKind::ConnectionRefused, "no streams available")
+ })
+ }
+ }
+
+ fn dummy_addr() -> SocketAddr {
+ "127.0.0.1:1234".parse().unwrap()
+ }
+
+ /// Verify that a session is removed from the session map after
+ /// `CONNECTION_TIMEOUT` elapses with no incoming requests.
+ #[tokio::test(start_paused = true)]
+ async fn session_removed_after_connection_timeout() {
+ let (upstream, _upstream_remote) = tokio::io::duplex(8192);
+ let connector = MockConnector::new(vec![upstream]);
+ let sessions =
+ Sessions::with_connector(dummy_addr(), "X-Session".to_string(), connector);
+
+ let session_id = Uuid::new_v4();
+
+ // First request creates the session
+ let response = sessions
+ .clone()
+ .handle_request_inner(session_id, Bytes::from("hello"))
+ .await;
+ assert_eq!(response.status(), StatusCode::OK);
+
+ // Session should be tracked
+ assert!(
+ sessions.sessions.pin().get(&session_id).is_some(),
+ "Session should exist after first request"
+ );
+
+ // Advance time past CONNECTION_TIMEOUT with no further requests
+ tokio::time::advance(CONNECTION_TIMEOUT + Duration::from_secs(1)).await;
+ // Let the session task process the timeout and run its Drop cleanup
+ tokio::time::sleep(Duration::from_millis(1)).await;
+
+ // Session should have been cleaned up
+ assert!(
+ sessions.sessions.pin().get(&session_id).is_none(),
+ "Session should be removed after connection timeout"
+ );
+ }
+
+ /// Verify that when the upstream does not respond within `READ_TIMEOUT`,
+ /// the server returns an OK response with an empty body.
+ #[tokio::test(start_paused = true)]
+ async fn read_timeout_returns_empty_body() {
+ // Upstream that accepts writes but never sends data back
+ let (upstream, _upstream_remote) = tokio::io::duplex(8192);
+ let connector = MockConnector::new(vec![upstream]);
+ let sessions =
+ Sessions::with_connector(dummy_addr(), "X-Session".to_string(), connector);
+
+ let session_id = Uuid::new_v4();
+
+ let response = sessions
+ .clone()
+ .handle_request_inner(session_id, Bytes::from("ping"))
+ .await;
+ assert_eq!(response.status(), StatusCode::OK);
+
+ let body = response.into_body().collect().await.unwrap().to_bytes();
+ assert!(
+ body.is_empty(),
+ "Body should be empty when upstream does not respond within read timeout"
+ );
+ }
+}