test: deepen and complete project-wide test coverage (#297)

* test: deepen coverage for health doctor provider and tunnels

* test: add broad trait and module re-export coverage
This commit is contained in:
Chummy 2026-02-16 18:58:24 +08:00 committed by GitHub
parent 79a6f180a8
commit 49fcc7a2c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 1156 additions and 0 deletions

View file

@ -109,3 +109,33 @@ impl Tunnel for CloudflareTunnel {
.and_then(|g| g.as_ref().map(|tp| tp.public_url.clone()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constructor_stores_token() {
let tunnel = CloudflareTunnel::new("cf-token".into());
assert_eq!(tunnel.token, "cf-token");
}
#[test]
fn public_url_is_none_before_start() {
let tunnel = CloudflareTunnel::new("cf-token".into());
assert!(tunnel.public_url().is_none());
}
#[tokio::test]
async fn stop_without_started_process_is_ok() {
let tunnel = CloudflareTunnel::new("cf-token".into());
let result = tunnel.stop().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn health_check_is_false_before_start() {
let tunnel = CloudflareTunnel::new("cf-token".into());
assert!(!tunnel.health_check().await);
}
}

View file

@ -143,3 +143,78 @@ impl Tunnel for CustomTunnel {
.and_then(|g| g.as_ref().map(|tp| tp.public_url.clone()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn start_with_empty_command_returns_error() {
let tunnel = CustomTunnel::new(" ".into(), None, None);
let result = tunnel.start("127.0.0.1", 8080).await;
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("start_command is empty"));
}
#[tokio::test]
async fn start_without_pattern_returns_local_url() {
let tunnel = CustomTunnel::new("sleep 1".into(), None, None);
let url = tunnel.start("127.0.0.1", 4455).await.unwrap();
assert_eq!(url, "http://127.0.0.1:4455");
assert_eq!(
tunnel.public_url().as_deref(),
Some("http://127.0.0.1:4455")
);
tunnel.stop().await.unwrap();
}
#[tokio::test]
async fn start_with_pattern_extracts_url() {
let tunnel = CustomTunnel::new(
"echo https://public.example".into(),
None,
Some("public.example".into()),
);
let url = tunnel.start("localhost", 9999).await.unwrap();
assert_eq!(url, "https://public.example");
assert_eq!(
tunnel.public_url().as_deref(),
Some("https://public.example")
);
tunnel.stop().await.unwrap();
}
#[tokio::test]
async fn start_replaces_host_and_port_placeholders() {
let tunnel = CustomTunnel::new(
"echo http://{host}:{port}".into(),
None,
Some("http://".into()),
);
let url = tunnel.start("10.1.2.3", 4321).await.unwrap();
assert_eq!(url, "http://10.1.2.3:4321");
tunnel.stop().await.unwrap();
}
#[tokio::test]
async fn health_check_with_unreachable_health_url_returns_false() {
let tunnel = CustomTunnel::new(
"sleep 1".into(),
Some("http://127.0.0.1:9/healthz".into()),
None,
);
assert!(!tunnel.health_check().await);
}
}

View file

@ -128,6 +128,7 @@ mod tests {
use crate::config::schema::{
CloudflareTunnelConfig, CustomTunnelConfig, NgrokTunnelConfig, TunnelConfig,
};
use tokio::process::Command;
/// Helper: assert `create_tunnel` returns an error containing `needle`.
fn assert_tunnel_err(cfg: &TunnelConfig, needle: &str) {
@ -313,4 +314,62 @@ mod tests {
assert_eq!(t.name(), "custom");
assert!(t.public_url().is_none());
}
#[tokio::test]
async fn kill_shared_no_process_is_ok() {
let proc = new_shared_process();
let result = kill_shared(&proc).await;
assert!(result.is_ok());
assert!(proc.lock().await.is_none());
}
#[tokio::test]
async fn kill_shared_terminates_and_clears_child() {
let proc = new_shared_process();
let child = Command::new("sleep")
.arg("30")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.expect("sleep should spawn for lifecycle test");
{
let mut guard = proc.lock().await;
*guard = Some(TunnelProcess {
child,
public_url: "https://example.test".into(),
});
}
kill_shared(&proc).await.unwrap();
let guard = proc.lock().await;
assert!(guard.is_none());
}
#[tokio::test]
async fn cloudflare_health_false_before_start() {
let tunnel = CloudflareTunnel::new("tok".into());
assert!(!tunnel.health_check().await);
}
#[tokio::test]
async fn ngrok_health_false_before_start() {
let tunnel = NgrokTunnel::new("tok".into(), None);
assert!(!tunnel.health_check().await);
}
#[tokio::test]
async fn tailscale_health_false_before_start() {
let tunnel = TailscaleTunnel::new(false, None);
assert!(!tunnel.health_check().await);
}
#[tokio::test]
async fn custom_health_false_before_start_without_health_url() {
let tunnel = CustomTunnel::new("echo hi".into(), None, Some("https://".into()));
assert!(!tunnel.health_check().await);
}
}

View file

@ -119,3 +119,33 @@ impl Tunnel for NgrokTunnel {
.and_then(|g| g.as_ref().map(|tp| tp.public_url.clone()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constructor_stores_domain() {
let tunnel = NgrokTunnel::new("ngrok-token".into(), Some("my.ngrok.app".into()));
assert_eq!(tunnel.domain.as_deref(), Some("my.ngrok.app"));
}
#[test]
fn public_url_is_none_before_start() {
let tunnel = NgrokTunnel::new("ngrok-token".into(), None);
assert!(tunnel.public_url().is_none());
}
#[tokio::test]
async fn stop_without_started_process_is_ok() {
let tunnel = NgrokTunnel::new("ngrok-token".into(), None);
let result = tunnel.stop().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn health_check_is_false_before_start() {
let tunnel = NgrokTunnel::new("ngrok-token".into(), None);
assert!(!tunnel.health_check().await);
}
}

View file

@ -26,3 +26,39 @@ impl Tunnel for NoneTunnel {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn name_is_none() {
let tunnel = NoneTunnel;
assert_eq!(tunnel.name(), "none");
}
#[tokio::test]
async fn start_returns_local_url() {
let tunnel = NoneTunnel;
let url = tunnel.start("127.0.0.1", 7788).await.unwrap();
assert_eq!(url, "http://127.0.0.1:7788");
}
#[tokio::test]
async fn stop_is_noop_success() {
let tunnel = NoneTunnel;
assert!(tunnel.stop().await.is_ok());
}
#[tokio::test]
async fn health_check_is_always_true() {
let tunnel = NoneTunnel;
assert!(tunnel.health_check().await);
}
#[test]
fn public_url_is_always_none() {
let tunnel = NoneTunnel;
assert!(tunnel.public_url().is_none());
}
}

View file

@ -100,3 +100,34 @@ impl Tunnel for TailscaleTunnel {
.and_then(|g| g.as_ref().map(|tp| tp.public_url.clone()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constructor_stores_hostname_and_mode() {
let tunnel = TailscaleTunnel::new(true, Some("myhost.tailnet.ts.net".into()));
assert!(tunnel.funnel);
assert_eq!(tunnel.hostname.as_deref(), Some("myhost.tailnet.ts.net"));
}
#[test]
fn public_url_is_none_before_start() {
let tunnel = TailscaleTunnel::new(false, None);
assert!(tunnel.public_url().is_none());
}
#[tokio::test]
async fn health_check_is_false_before_start() {
let tunnel = TailscaleTunnel::new(false, None);
assert!(!tunnel.health_check().await);
}
#[tokio::test]
async fn stop_without_started_process_is_ok() {
let tunnel = TailscaleTunnel::new(false, None);
let result = tunnel.stop().await;
assert!(result.is_ok());
}
}