From e5eb94d8c99df62fc084cb6d56c41049114d8f76 Mon Sep 17 00:00:00 2001 From: Oliver Stenbom Date: Fri, 18 Sep 2026 07:50:03 +0200 Subject: [PATCH 1/2] fix(worker): keep tunnel cleanup consistent --- Cargo.lock | 2 + worker/Cargo.toml | 4 ++ worker/src/tunnel.rs | 143 ++++++++++++++++++++++++++++++++++++------- 3 files changed, 128 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0bed81c2..053077bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1974,10 +1974,12 @@ dependencies = [ "getrandom 0.4.2", "http", "linkup", + "mockito", "regex", "reqwest 0.13.2", "serde", "serde_json", + "tokio", "tower-service", "worker", ] diff --git a/worker/Cargo.toml b/worker/Cargo.toml index 98e81c7e..9f1a26a4 100644 --- a/worker/Cargo.toml +++ b/worker/Cargo.toml @@ -22,3 +22,7 @@ console_error_panic_hook = { version = "0.1.7" } getrandom = { version = "0.4.2", features = ["wasm_js"] } tower-service = "0.3.3" worker = { version = "0.5.0", features = ["http", "axum"] } + +[dev-dependencies] +mockito = "1.6.1" +tokio = { workspace = true, features = ["macros", "rt"] } diff --git a/worker/src/tunnel.rs b/worker/src/tunnel.rs index d57b036a..d3968806 100644 --- a/worker/src/tunnel.rs +++ b/worker/src/tunnel.rs @@ -59,17 +59,15 @@ pub async fn delete_tunnel( ) -> Result<(), DeleteTunnelError> { let client = crate::cloudflare_client(api_token); - let delete_tunnel_req = cloudflare::endpoints::cfd_tunnel::delete_tunnel::DeleteTunnel { - account_identifier: account_id, - tunnel_id, - params: cloudflare::endpoints::cfd_tunnel::delete_tunnel::Params { cascade: true }, - }; - - client - .request(&delete_tunnel_req) - .await - .map_err(|error| DeleteTunnelError::DeleteCloudflareTunnel(error.to_string()))?; + delete_tunnel_with_client(&client, account_id, zone_id, tunnel_id).await +} +async fn delete_tunnel_with_client( + client: &cloudflare::framework::async_api::Client, + account_id: &str, + zone_id: &str, + tunnel_id: &str, +) -> Result<(), DeleteTunnelError> { let get_dns_record_req = cloudflare::endpoints::dns::ListDnsRecords { zone_identifier: zone_id, params: cloudflare::endpoints::dns::ListDnsRecordsParams { @@ -87,12 +85,8 @@ pub async fn delete_tunnel( .result; let record = match records.len() { - 0 => { - return Err(DeleteTunnelError::GetDNSRecord( - "Fetching DNS for tunnel returned empty".to_string(), - )); - } - 1 => &records[0], + 0 => None, + 1 => Some(&records[0]), 2.. => { return Err(DeleteTunnelError::GetDNSRecord( "Fetching DNS for tunnel returned more than one record".to_string(), @@ -100,15 +94,28 @@ pub async fn delete_tunnel( } }; - let delete_dns_record_red = cloudflare::endpoints::dns::DeleteDnsRecord { - zone_identifier: zone_id, - identifier: &record.id, + if let Some(record) = record { + let delete_dns_record_req = cloudflare::endpoints::dns::DeleteDnsRecord { + zone_identifier: zone_id, + identifier: &record.id, + }; + + client + .request(&delete_dns_record_req) + .await + .map_err(|error| DeleteTunnelError::DeleteDNSRecord(error.to_string()))?; + } + + let delete_tunnel_req = cloudflare::endpoints::cfd_tunnel::delete_tunnel::DeleteTunnel { + account_identifier: account_id, + tunnel_id, + params: cloudflare::endpoints::cfd_tunnel::delete_tunnel::Params { cascade: true }, }; client - .request(&delete_dns_record_red) + .request(&delete_tunnel_req) .await - .map_err(|error| DeleteTunnelError::DeleteDNSRecord(error.to_string()))?; + .map_err(|error| DeleteTunnelError::DeleteCloudflareTunnel(error.to_string()))?; Ok(()) } @@ -219,3 +226,97 @@ async fn create_tunnel( Ok(tunnel_data) } + +#[cfg(test)] +mod tests { + use cloudflare::framework::{ + Environment, HttpApiClientConfig, async_api::Client, auth::Credentials, + }; + use mockito::Matcher; + + use super::delete_tunnel_with_client; + + fn test_client(server_url: &str) -> Client { + Client::new( + Credentials::UserAuthToken { + token: "test-token".to_string(), + }, + HttpApiClientConfig::default(), + Environment::Custom(server_url.parse().unwrap()), + ) + .unwrap() + } + + #[tokio::test] + async fn leaves_tunnel_intact_when_dns_lookup_fails() { + let mut server = mockito::Server::new_async().await; + let dns_lookup = server + .mock("GET", "/zones/zone-id/dns_records") + .match_query(Matcher::AllOf(vec![ + Matcher::UrlEncoded("type".into(), "CNAME".into()), + Matcher::UrlEncoded( + "content".into(), + "tunnel-id.cfargotunnel.com".into(), + ), + ])) + .with_status(500) + .with_body( + r#"{"success":false,"errors":[{"code":1000,"message":"failure"}],"messages":[],"result":null}"#, + ) + .create_async() + .await; + let tunnel_delete = server + .mock("DELETE", "/accounts/account-id/cfd_tunnel/tunnel-id") + .expect(0) + .create_async() + .await; + + let result = delete_tunnel_with_client( + &test_client(&server.url()), + "account-id", + "zone-id", + "tunnel-id", + ) + .await; + + assert!(result.is_err()); + dns_lookup.assert_async().await; + tunnel_delete.assert_async().await; + } + + #[tokio::test] + async fn deletes_tunnel_when_dns_record_is_already_missing() { + let mut server = mockito::Server::new_async().await; + let dns_lookup = server + .mock("GET", "/zones/zone-id/dns_records") + .match_query(Matcher::AllOf(vec![ + Matcher::UrlEncoded("type".into(), "CNAME".into()), + Matcher::UrlEncoded("content".into(), "tunnel-id.cfargotunnel.com".into()), + ])) + .with_status(200) + .with_body(r#"{"success":true,"errors":[],"messages":[],"result":[]}"#) + .create_async() + .await; + let tunnel_delete = server + .mock("DELETE", "/accounts/account-id/cfd_tunnel/tunnel-id") + .match_query(Matcher::UrlEncoded("cascade".into(), "true".into())) + .with_status(200) + .with_body( + r#"{"success":true,"errors":[],"messages":[],"result":{"id":"00000000-0000-0000-0000-000000000000","created_at":"2026-01-01T00:00:00Z","deleted_at":"2026-01-01T00:00:00Z","name":"test","connections":[],"metadata":{}}}"#, + ) + .create_async() + .await; + + let result = delete_tunnel_with_client( + &test_client(&server.url()), + "account-id", + "zone-id", + "tunnel-id", + ) + .await; + + assert!(result.is_ok()); + dns_lookup.assert_async().await; + tunnel_delete.assert_async().await; + } +} From 9d0c15855435a6f64e393215d8f07c9e3f9c5290 Mon Sep 17 00:00:00 2001 From: Oliver Stenbom Date: Fri, 18 Sep 2026 07:54:40 +0200 Subject: [PATCH 2/2] test(worker): avoid mock server dependency --- Cargo.lock | 2 - worker/Cargo.toml | 4 -- worker/src/tunnel.rs | 104 ------------------------------------------- 3 files changed, 110 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 053077bd..0bed81c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1974,12 +1974,10 @@ dependencies = [ "getrandom 0.4.2", "http", "linkup", - "mockito", "regex", "reqwest 0.13.2", "serde", "serde_json", - "tokio", "tower-service", "worker", ] diff --git a/worker/Cargo.toml b/worker/Cargo.toml index 9f1a26a4..98e81c7e 100644 --- a/worker/Cargo.toml +++ b/worker/Cargo.toml @@ -22,7 +22,3 @@ console_error_panic_hook = { version = "0.1.7" } getrandom = { version = "0.4.2", features = ["wasm_js"] } tower-service = "0.3.3" worker = { version = "0.5.0", features = ["http", "axum"] } - -[dev-dependencies] -mockito = "1.6.1" -tokio = { workspace = true, features = ["macros", "rt"] } diff --git a/worker/src/tunnel.rs b/worker/src/tunnel.rs index d3968806..9443b576 100644 --- a/worker/src/tunnel.rs +++ b/worker/src/tunnel.rs @@ -58,16 +58,6 @@ pub async fn delete_tunnel( tunnel_id: &str, ) -> Result<(), DeleteTunnelError> { let client = crate::cloudflare_client(api_token); - - delete_tunnel_with_client(&client, account_id, zone_id, tunnel_id).await -} - -async fn delete_tunnel_with_client( - client: &cloudflare::framework::async_api::Client, - account_id: &str, - zone_id: &str, - tunnel_id: &str, -) -> Result<(), DeleteTunnelError> { let get_dns_record_req = cloudflare::endpoints::dns::ListDnsRecords { zone_identifier: zone_id, params: cloudflare::endpoints::dns::ListDnsRecordsParams { @@ -226,97 +216,3 @@ async fn create_tunnel( Ok(tunnel_data) } - -#[cfg(test)] -mod tests { - use cloudflare::framework::{ - Environment, HttpApiClientConfig, async_api::Client, auth::Credentials, - }; - use mockito::Matcher; - - use super::delete_tunnel_with_client; - - fn test_client(server_url: &str) -> Client { - Client::new( - Credentials::UserAuthToken { - token: "test-token".to_string(), - }, - HttpApiClientConfig::default(), - Environment::Custom(server_url.parse().unwrap()), - ) - .unwrap() - } - - #[tokio::test] - async fn leaves_tunnel_intact_when_dns_lookup_fails() { - let mut server = mockito::Server::new_async().await; - let dns_lookup = server - .mock("GET", "/zones/zone-id/dns_records") - .match_query(Matcher::AllOf(vec![ - Matcher::UrlEncoded("type".into(), "CNAME".into()), - Matcher::UrlEncoded( - "content".into(), - "tunnel-id.cfargotunnel.com".into(), - ), - ])) - .with_status(500) - .with_body( - r#"{"success":false,"errors":[{"code":1000,"message":"failure"}],"messages":[],"result":null}"#, - ) - .create_async() - .await; - let tunnel_delete = server - .mock("DELETE", "/accounts/account-id/cfd_tunnel/tunnel-id") - .expect(0) - .create_async() - .await; - - let result = delete_tunnel_with_client( - &test_client(&server.url()), - "account-id", - "zone-id", - "tunnel-id", - ) - .await; - - assert!(result.is_err()); - dns_lookup.assert_async().await; - tunnel_delete.assert_async().await; - } - - #[tokio::test] - async fn deletes_tunnel_when_dns_record_is_already_missing() { - let mut server = mockito::Server::new_async().await; - let dns_lookup = server - .mock("GET", "/zones/zone-id/dns_records") - .match_query(Matcher::AllOf(vec![ - Matcher::UrlEncoded("type".into(), "CNAME".into()), - Matcher::UrlEncoded("content".into(), "tunnel-id.cfargotunnel.com".into()), - ])) - .with_status(200) - .with_body(r#"{"success":true,"errors":[],"messages":[],"result":[]}"#) - .create_async() - .await; - let tunnel_delete = server - .mock("DELETE", "/accounts/account-id/cfd_tunnel/tunnel-id") - .match_query(Matcher::UrlEncoded("cascade".into(), "true".into())) - .with_status(200) - .with_body( - r#"{"success":true,"errors":[],"messages":[],"result":{"id":"00000000-0000-0000-0000-000000000000","created_at":"2026-01-01T00:00:00Z","deleted_at":"2026-01-01T00:00:00Z","name":"test","connections":[],"metadata":{}}}"#, - ) - .create_async() - .await; - - let result = delete_tunnel_with_client( - &test_client(&server.url()), - "account-id", - "zone-id", - "tunnel-id", - ) - .await; - - assert!(result.is_ok()); - dns_lookup.assert_async().await; - tunnel_delete.assert_async().await; - } -}