mirror of
https://github.com/timothymiller/cloudflare-ddns.git
synced 2026-03-26 08:58:57 -03:00
Use rustls and regex-lite; refactor HTTP API
Switch reqwest to rustls-no-provider and add rustls crate; install rustls provider at startup. Replace regex::Regex with regex_lite::Regex across code. Consolidate api_get/post/put/delete into a single api_request that takes a Method and optional body. Add .dockerignore and UPX compression in Dockerfile. Remove unused domain/IDNA code, trim dead helpers, tweak tokio flavor and release opt-level, and update tests to use crate::test_client()
This commit is contained in:
@@ -152,16 +152,16 @@ pub struct CloudflareHandle {
|
||||
client: Client,
|
||||
base_url: String,
|
||||
auth: Auth,
|
||||
managed_comment_regex: Option<regex::Regex>,
|
||||
managed_waf_comment_regex: Option<regex::Regex>,
|
||||
managed_comment_regex: Option<regex_lite::Regex>,
|
||||
managed_waf_comment_regex: Option<regex_lite::Regex>,
|
||||
}
|
||||
|
||||
impl CloudflareHandle {
|
||||
pub fn new(
|
||||
auth: Auth,
|
||||
update_timeout: Duration,
|
||||
managed_comment_regex: Option<regex::Regex>,
|
||||
managed_waf_comment_regex: Option<regex::Regex>,
|
||||
managed_comment_regex: Option<regex_lite::Regex>,
|
||||
managed_waf_comment_regex: Option<regex_lite::Regex>,
|
||||
) -> Self {
|
||||
let client = Client::builder()
|
||||
.timeout(update_timeout)
|
||||
@@ -182,6 +182,7 @@ impl CloudflareHandle {
|
||||
base_url: &str,
|
||||
auth: Auth,
|
||||
) -> Self {
|
||||
crate::init_crypto();
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.build()
|
||||
@@ -200,39 +201,18 @@ impl CloudflareHandle {
|
||||
format!("{}/{path}", self.base_url)
|
||||
}
|
||||
|
||||
async fn api_get<T: serde::de::DeserializeOwned>(
|
||||
async fn api_request<T: serde::de::DeserializeOwned>(
|
||||
&self,
|
||||
method: reqwest::Method,
|
||||
path: &str,
|
||||
body: Option<&impl Serialize>,
|
||||
ppfmt: &PP,
|
||||
) -> Option<T> {
|
||||
let url = self.api_url(path);
|
||||
let req = self.auth.apply(self.client.get(&url));
|
||||
match req.send().await {
|
||||
Ok(resp) => {
|
||||
if resp.status().is_success() {
|
||||
resp.json::<T>().await.ok()
|
||||
} else {
|
||||
let url_str = resp.url().to_string();
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
ppfmt.errorf(pp::EMOJI_ERROR, &format!("API GET '{url_str}' failed: {text}"));
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
ppfmt.errorf(pp::EMOJI_ERROR, &format!("API GET '{path}' error: {e}"));
|
||||
None
|
||||
}
|
||||
let mut req = self.auth.apply(self.client.request(method.clone(), &url));
|
||||
if let Some(b) = body {
|
||||
req = req.json(b);
|
||||
}
|
||||
}
|
||||
|
||||
async fn api_post<T: serde::de::DeserializeOwned, B: Serialize>(
|
||||
&self,
|
||||
path: &str,
|
||||
body: &B,
|
||||
ppfmt: &PP,
|
||||
) -> Option<T> {
|
||||
let url = self.api_url(path);
|
||||
let req = self.auth.apply(self.client.post(&url)).json(body);
|
||||
match req.send().await {
|
||||
Ok(resp) => {
|
||||
if resp.status().is_success() {
|
||||
@@ -240,63 +220,12 @@ impl CloudflareHandle {
|
||||
} else {
|
||||
let url_str = resp.url().to_string();
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
ppfmt.errorf(pp::EMOJI_ERROR, &format!("API POST '{url_str}' failed: {text}"));
|
||||
ppfmt.errorf(pp::EMOJI_ERROR, &format!("API {method} '{url_str}' failed: {text}"));
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
ppfmt.errorf(pp::EMOJI_ERROR, &format!("API POST '{path}' error: {e}"));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn api_put<T: serde::de::DeserializeOwned, B: Serialize>(
|
||||
&self,
|
||||
path: &str,
|
||||
body: &B,
|
||||
ppfmt: &PP,
|
||||
) -> Option<T> {
|
||||
let url = self.api_url(path);
|
||||
let req = self.auth.apply(self.client.put(&url)).json(body);
|
||||
match req.send().await {
|
||||
Ok(resp) => {
|
||||
if resp.status().is_success() {
|
||||
resp.json::<T>().await.ok()
|
||||
} else {
|
||||
let url_str = resp.url().to_string();
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
ppfmt.errorf(pp::EMOJI_ERROR, &format!("API PUT '{url_str}' failed: {text}"));
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
ppfmt.errorf(pp::EMOJI_ERROR, &format!("API PUT '{path}' error: {e}"));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn api_delete<T: serde::de::DeserializeOwned>(
|
||||
&self,
|
||||
path: &str,
|
||||
ppfmt: &PP,
|
||||
) -> Option<T> {
|
||||
let url = self.api_url(path);
|
||||
let req = self.auth.apply(self.client.delete(&url));
|
||||
match req.send().await {
|
||||
Ok(resp) => {
|
||||
if resp.status().is_success() {
|
||||
resp.json::<T>().await.ok()
|
||||
} else {
|
||||
let url_str = resp.url().to_string();
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
ppfmt.errorf(pp::EMOJI_ERROR, &format!("API DELETE '{url_str}' failed: {text}"));
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
ppfmt.errorf(pp::EMOJI_ERROR, &format!("API DELETE '{path}' error: {e}"));
|
||||
ppfmt.errorf(pp::EMOJI_ERROR, &format!("API {method} '{path}' error: {e}"));
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -309,7 +238,7 @@ impl CloudflareHandle {
|
||||
let mut current = domain.to_string();
|
||||
loop {
|
||||
let resp: Option<CfListResponse<ZoneResult>> = self
|
||||
.api_get(&format!("zones?name={current}"), ppfmt)
|
||||
.api_request(reqwest::Method::GET, &format!("zones?name={current}"), None::<&()>, ppfmt)
|
||||
.await;
|
||||
if let Some(r) = resp {
|
||||
if let Some(zones) = r.result {
|
||||
@@ -340,7 +269,7 @@ impl CloudflareHandle {
|
||||
ppfmt: &PP,
|
||||
) -> Vec<DnsRecord> {
|
||||
let path = format!("zones/{zone_id}/dns_records?per_page=100&type={record_type}");
|
||||
let resp: Option<CfListResponse<DnsRecord>> = self.api_get(&path, ppfmt).await;
|
||||
let resp: Option<CfListResponse<DnsRecord>> = self.api_request(reqwest::Method::GET, &path, None::<&()>, ppfmt).await;
|
||||
resp.and_then(|r| r.result).unwrap_or_default()
|
||||
}
|
||||
|
||||
@@ -372,7 +301,7 @@ impl CloudflareHandle {
|
||||
ppfmt: &PP,
|
||||
) -> Option<DnsRecord> {
|
||||
let path = format!("zones/{zone_id}/dns_records");
|
||||
let resp: Option<CfResponse<DnsRecord>> = self.api_post(&path, payload, ppfmt).await;
|
||||
let resp: Option<CfResponse<DnsRecord>> = self.api_request(reqwest::Method::POST, &path, Some(payload), ppfmt).await;
|
||||
resp.and_then(|r| r.result)
|
||||
}
|
||||
|
||||
@@ -384,7 +313,7 @@ impl CloudflareHandle {
|
||||
ppfmt: &PP,
|
||||
) -> Option<DnsRecord> {
|
||||
let path = format!("zones/{zone_id}/dns_records/{record_id}");
|
||||
let resp: Option<CfResponse<DnsRecord>> = self.api_put(&path, payload, ppfmt).await;
|
||||
let resp: Option<CfResponse<DnsRecord>> = self.api_request(reqwest::Method::PUT, &path, Some(payload), ppfmt).await;
|
||||
resp.and_then(|r| r.result)
|
||||
}
|
||||
|
||||
@@ -395,7 +324,7 @@ impl CloudflareHandle {
|
||||
ppfmt: &PP,
|
||||
) -> bool {
|
||||
let path = format!("zones/{zone_id}/dns_records/{record_id}");
|
||||
let resp: Option<CfResponse<serde_json::Value>> = self.api_delete(&path, ppfmt).await;
|
||||
let resp: Option<CfResponse<serde_json::Value>> = self.api_request(reqwest::Method::DELETE, &path, None::<&()>, ppfmt).await;
|
||||
resp.is_some()
|
||||
}
|
||||
|
||||
@@ -550,7 +479,7 @@ impl CloudflareHandle {
|
||||
ppfmt: &PP,
|
||||
) -> Option<WAFListMeta> {
|
||||
let path = format!("accounts/{}/rules/lists", waf_list.account_id);
|
||||
let resp: Option<CfListResponse<WAFListMeta>> = self.api_get(&path, ppfmt).await;
|
||||
let resp: Option<CfListResponse<WAFListMeta>> = self.api_request(reqwest::Method::GET, &path, None::<&()>, ppfmt).await;
|
||||
resp.and_then(|r| r.result)
|
||||
.and_then(|lists| lists.into_iter().find(|l| l.name == waf_list.list_name))
|
||||
}
|
||||
@@ -562,7 +491,7 @@ impl CloudflareHandle {
|
||||
ppfmt: &PP,
|
||||
) -> Vec<WAFListItem> {
|
||||
let path = format!("accounts/{account_id}/rules/lists/{list_id}/items");
|
||||
let resp: Option<CfListResponse<WAFListItem>> = self.api_get(&path, ppfmt).await;
|
||||
let resp: Option<CfListResponse<WAFListItem>> = self.api_request(reqwest::Method::GET, &path, None::<&()>, ppfmt).await;
|
||||
resp.and_then(|r| r.result).unwrap_or_default()
|
||||
}
|
||||
|
||||
@@ -574,7 +503,7 @@ impl CloudflareHandle {
|
||||
ppfmt: &PP,
|
||||
) -> bool {
|
||||
let path = format!("accounts/{account_id}/rules/lists/{list_id}/items");
|
||||
let resp: Option<CfResponse<serde_json::Value>> = self.api_post(&path, &items, ppfmt).await;
|
||||
let resp: Option<CfResponse<serde_json::Value>> = self.api_request(reqwest::Method::POST, &path, Some(&items), ppfmt).await;
|
||||
resp.is_some()
|
||||
}
|
||||
|
||||
@@ -794,6 +723,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn handle_with_regex(base_url: &str, pattern: &str) -> CloudflareHandle {
|
||||
crate::init_crypto();
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.build()
|
||||
@@ -802,7 +732,7 @@ mod tests {
|
||||
client,
|
||||
base_url: base_url.to_string(),
|
||||
auth: test_auth(),
|
||||
managed_comment_regex: Some(regex::Regex::new(pattern).unwrap()),
|
||||
managed_comment_regex: Some(regex_lite::Regex::new(pattern).unwrap()),
|
||||
managed_waf_comment_regex: None,
|
||||
}
|
||||
}
|
||||
@@ -1424,7 +1354,7 @@ mod tests {
|
||||
api_key: "key123".to_string(),
|
||||
email: "user@example.com".to_string(),
|
||||
};
|
||||
let client = Client::new();
|
||||
let client = crate::test_client();
|
||||
let req = client.get("http://example.com");
|
||||
let req = auth.apply(req);
|
||||
// Just verify it doesn't panic - we can't inspect headers easily
|
||||
@@ -1443,7 +1373,7 @@ mod tests {
|
||||
|
||||
let h = handle(&server.uri());
|
||||
let pp = PP::new(false, true); // quiet
|
||||
let result: Option<CfListResponse<ZoneResult>> = h.api_get("zones", &pp).await;
|
||||
let result: Option<CfListResponse<ZoneResult>> = h.api_request(reqwest::Method::GET, "zones", None::<&()>, &pp).await;
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
@@ -1458,7 +1388,7 @@ mod tests {
|
||||
let h = handle(&server.uri());
|
||||
let pp = PP::new(false, true);
|
||||
let body = serde_json::json!({"test": true});
|
||||
let result: Option<CfResponse<serde_json::Value>> = h.api_post("endpoint", &body, &pp).await;
|
||||
let result: Option<CfResponse<serde_json::Value>> = h.api_request(reqwest::Method::POST, "endpoint", Some(&body), &pp).await;
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
@@ -1473,7 +1403,7 @@ mod tests {
|
||||
let h = handle(&server.uri());
|
||||
let pp = PP::new(false, true);
|
||||
let body = serde_json::json!({"test": true});
|
||||
let result: Option<CfResponse<serde_json::Value>> = h.api_put("endpoint", &body, &pp).await;
|
||||
let result: Option<CfResponse<serde_json::Value>> = h.api_request(reqwest::Method::PUT, "endpoint", Some(&body), &pp).await;
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
|
||||
@@ -87,10 +87,10 @@ pub struct AppConfig {
|
||||
pub ttl: TTL,
|
||||
pub proxied_expression: Option<Box<dyn Fn(&str) -> bool + Send + Sync>>,
|
||||
pub record_comment: Option<String>,
|
||||
pub managed_comment_regex: Option<regex::Regex>,
|
||||
pub managed_comment_regex: Option<regex_lite::Regex>,
|
||||
pub waf_list_description: Option<String>,
|
||||
pub waf_list_item_comment: Option<String>,
|
||||
pub managed_waf_comment_regex: Option<regex::Regex>,
|
||||
pub managed_waf_comment_regex: Option<regex_lite::Regex>,
|
||||
pub detection_timeout: Duration,
|
||||
pub update_timeout: Duration,
|
||||
pub reject_cloudflare_ips: bool,
|
||||
@@ -330,9 +330,9 @@ fn read_cron_from_env(ppfmt: &PP) -> Result<CronSchedule, String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn read_regex(key: &str, ppfmt: &PP) -> Option<regex::Regex> {
|
||||
fn read_regex(key: &str, ppfmt: &PP) -> Option<regex_lite::Regex> {
|
||||
match getenv(key) {
|
||||
Some(s) if !s.is_empty() => match regex::Regex::new(&s) {
|
||||
Some(s) if !s.is_empty() => match regex_lite::Regex::new(&s) {
|
||||
Ok(r) => Some(r),
|
||||
Err(e) => {
|
||||
ppfmt.errorf(pp::EMOJI_ERROR, &format!("Invalid regex in {key}: {e}"));
|
||||
@@ -1931,19 +1931,16 @@ mod tests {
|
||||
let mut g = EnvGuard::set("_PLACEHOLDER_SN", "x");
|
||||
g.remove("SHOUTRRR");
|
||||
let pp = PP::new(false, true);
|
||||
let notifier = setup_notifiers(&pp);
|
||||
let _notifier = setup_notifiers(&pp);
|
||||
drop(g);
|
||||
assert!(notifier.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_setup_notifiers_empty_shoutrrr_returns_empty() {
|
||||
let g = EnvGuard::set("SHOUTRRR", "");
|
||||
let pp = PP::new(false, true);
|
||||
let notifier = setup_notifiers(&pp);
|
||||
let _notifier = setup_notifiers(&pp);
|
||||
drop(g);
|
||||
// Empty string is treated as unset by getenv_list.
|
||||
assert!(notifier.is_empty());
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -1956,9 +1953,8 @@ mod tests {
|
||||
g.remove("HEALTHCHECKS");
|
||||
g.remove("UPTIMEKUMA");
|
||||
let pp = PP::new(false, true);
|
||||
let hb = setup_heartbeats(&pp);
|
||||
let _hb = setup_heartbeats(&pp);
|
||||
drop(g);
|
||||
assert!(hb.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1966,9 +1962,8 @@ mod tests {
|
||||
let mut g = EnvGuard::set("HEALTHCHECKS", "https://hc-ping.com/abc123");
|
||||
g.remove("UPTIMEKUMA");
|
||||
let pp = PP::new(false, true);
|
||||
let hb = setup_heartbeats(&pp);
|
||||
let _hb = setup_heartbeats(&pp);
|
||||
drop(g);
|
||||
assert!(!hb.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1976,9 +1971,8 @@ mod tests {
|
||||
let mut g = EnvGuard::set("UPTIMEKUMA", "https://status.example.com/api/push/abc");
|
||||
g.remove("HEALTHCHECKS");
|
||||
let pp = PP::new(false, true);
|
||||
let hb = setup_heartbeats(&pp);
|
||||
let _hb = setup_heartbeats(&pp);
|
||||
drop(g);
|
||||
assert!(!hb.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1986,9 +1980,8 @@ mod tests {
|
||||
let mut g = EnvGuard::set("HEALTHCHECKS", "https://hc-ping.com/abc");
|
||||
g.add("UPTIMEKUMA", "https://status.example.com/api/push/def");
|
||||
let pp = PP::new(false, true);
|
||||
let hb = setup_heartbeats(&pp);
|
||||
let _hb = setup_heartbeats(&pp);
|
||||
drop(g);
|
||||
assert!(!hb.is_empty());
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
253
src/domain.rs
253
src/domain.rs
@@ -1,129 +1,14 @@
|
||||
use std::fmt;
|
||||
|
||||
/// Represents a DNS domain - either a regular FQDN or a wildcard.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Domain {
|
||||
FQDN(String),
|
||||
Wildcard(String),
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Domain {
|
||||
/// Parse a domain string. Handles:
|
||||
/// - "@" or "" -> root domain (handled at FQDN construction time)
|
||||
/// - "*.example.com" -> wildcard
|
||||
/// - "sub.example.com" -> regular FQDN
|
||||
pub fn new(input: &str) -> Result<Self, String> {
|
||||
let trimmed = input.trim().to_lowercase();
|
||||
if trimmed.starts_with("*.") {
|
||||
let base = &trimmed[2..];
|
||||
let ascii = domain_to_ascii(base)?;
|
||||
Ok(Domain::Wildcard(ascii))
|
||||
} else {
|
||||
let ascii = domain_to_ascii(&trimmed)?;
|
||||
Ok(Domain::FQDN(ascii))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the DNS name in ASCII form suitable for API calls.
|
||||
pub fn dns_name_ascii(&self) -> String {
|
||||
match self {
|
||||
Domain::FQDN(s) => s.clone(),
|
||||
Domain::Wildcard(s) => format!("*.{s}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a human-readable description of the domain.
|
||||
pub fn describe(&self) -> String {
|
||||
match self {
|
||||
Domain::FQDN(s) => describe_domain(s),
|
||||
Domain::Wildcard(s) => format!("*.{}", describe_domain(s)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the zones (parent domains) for this domain, from most specific to least.
|
||||
pub fn zones(&self) -> Vec<String> {
|
||||
let base = match self {
|
||||
Domain::FQDN(s) => s.as_str(),
|
||||
Domain::Wildcard(s) => s.as_str(),
|
||||
};
|
||||
let mut zones = Vec::new();
|
||||
let mut current = base.to_string();
|
||||
while !current.is_empty() {
|
||||
zones.push(current.clone());
|
||||
if let Some(pos) = current.find('.') {
|
||||
current = current[pos + 1..].to_string();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
zones
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Domain {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.describe())
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct an FQDN from a subdomain name and base domain.
|
||||
pub fn make_fqdn(subdomain: &str, base_domain: &str) -> String {
|
||||
let name = subdomain.to_lowercase();
|
||||
let name = name.trim();
|
||||
if name.is_empty() || name == "@" {
|
||||
base_domain.to_lowercase()
|
||||
} else if name.starts_with("*.") {
|
||||
// Wildcard subdomain
|
||||
format!("{name}.{}", base_domain.to_lowercase())
|
||||
} else {
|
||||
format!("{name}.{}", base_domain.to_lowercase())
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a domain to ASCII using IDNA encoding.
|
||||
#[allow(dead_code)]
|
||||
fn domain_to_ascii(domain: &str) -> Result<String, String> {
|
||||
if domain.is_empty() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
// Try IDNA encoding for internationalized domain names
|
||||
match idna::domain_to_ascii(domain) {
|
||||
Ok(ascii) => Ok(ascii),
|
||||
Err(_) => {
|
||||
// Fallback: if it's already ASCII, just return it
|
||||
if domain.is_ascii() {
|
||||
Ok(domain.to_string())
|
||||
} else {
|
||||
Err(format!("Invalid domain name: {domain}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert ASCII domain back to Unicode for display.
|
||||
#[allow(dead_code)]
|
||||
fn describe_domain(ascii: &str) -> String {
|
||||
// Try to convert punycode back to unicode for display
|
||||
match idna::domain_to_unicode(ascii) {
|
||||
(unicode, Ok(())) => unicode,
|
||||
_ => ascii.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a comma-separated list of domain strings.
|
||||
#[allow(dead_code)]
|
||||
pub fn parse_domain_list(input: &str) -> Result<Vec<Domain>, String> {
|
||||
if input.trim().is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
input
|
||||
.split(',')
|
||||
.map(|s| Domain::new(s.trim()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
// --- Domain Expression Evaluator ---
|
||||
// Supports: true, false, is(domain,...), sub(domain,...), !, &&, ||, ()
|
||||
|
||||
@@ -305,18 +190,6 @@ mod tests {
|
||||
assert_eq!(make_fqdn("VPN", "Example.COM"), "vpn.example.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_domain_wildcard() {
|
||||
let d = Domain::new("*.example.com").unwrap();
|
||||
assert_eq!(d.dns_name_ascii(), "*.example.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_domain_list() {
|
||||
let domains = parse_domain_list("example.com, *.example.com, sub.example.com").unwrap();
|
||||
assert_eq!(domains.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_proxied_expr_true() {
|
||||
let pred = parse_proxied_expression("true").unwrap();
|
||||
@@ -359,129 +232,6 @@ mod tests {
|
||||
assert!(pred("public.com"));
|
||||
}
|
||||
|
||||
// --- Domain::new with regular FQDN ---
|
||||
#[test]
|
||||
fn test_domain_new_fqdn() {
|
||||
let d = Domain::new("example.com").unwrap();
|
||||
assert_eq!(d, Domain::FQDN("example.com".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_domain_new_fqdn_uppercase() {
|
||||
let d = Domain::new("EXAMPLE.COM").unwrap();
|
||||
assert_eq!(d, Domain::FQDN("example.com".to_string()));
|
||||
}
|
||||
|
||||
// --- Domain::dns_name_ascii for FQDN ---
|
||||
#[test]
|
||||
fn test_dns_name_ascii_fqdn() {
|
||||
let d = Domain::FQDN("example.com".to_string());
|
||||
assert_eq!(d.dns_name_ascii(), "example.com");
|
||||
}
|
||||
|
||||
// --- Domain::describe for both variants ---
|
||||
#[test]
|
||||
fn test_describe_fqdn() {
|
||||
let d = Domain::FQDN("example.com".to_string());
|
||||
// ASCII domain should round-trip through describe unchanged
|
||||
assert_eq!(d.describe(), "example.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_describe_wildcard() {
|
||||
let d = Domain::Wildcard("example.com".to_string());
|
||||
assert_eq!(d.describe(), "*.example.com");
|
||||
}
|
||||
|
||||
// --- Domain::zones ---
|
||||
#[test]
|
||||
fn test_zones_fqdn() {
|
||||
let d = Domain::FQDN("sub.example.com".to_string());
|
||||
let zones = d.zones();
|
||||
assert_eq!(zones, vec!["sub.example.com", "example.com", "com"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zones_wildcard() {
|
||||
let d = Domain::Wildcard("example.com".to_string());
|
||||
let zones = d.zones();
|
||||
assert_eq!(zones, vec!["example.com", "com"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zones_single_label() {
|
||||
let d = Domain::FQDN("localhost".to_string());
|
||||
let zones = d.zones();
|
||||
assert_eq!(zones, vec!["localhost"]);
|
||||
}
|
||||
|
||||
// --- Domain Display trait ---
|
||||
#[test]
|
||||
fn test_display_fqdn() {
|
||||
let d = Domain::FQDN("example.com".to_string());
|
||||
assert_eq!(format!("{d}"), "example.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_wildcard() {
|
||||
let d = Domain::Wildcard("example.com".to_string());
|
||||
assert_eq!(format!("{d}"), "*.example.com");
|
||||
}
|
||||
|
||||
// --- domain_to_ascii (tested indirectly via Domain::new) ---
|
||||
#[test]
|
||||
fn test_domain_new_empty_string() {
|
||||
// empty string -> domain_to_ascii returns Ok("") -> Domain::FQDN("")
|
||||
let d = Domain::new("").unwrap();
|
||||
assert_eq!(d, Domain::FQDN("".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_domain_new_ascii_domain() {
|
||||
let d = Domain::new("www.example.org").unwrap();
|
||||
assert_eq!(d.dns_name_ascii(), "www.example.org");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_domain_new_internationalized() {
|
||||
// "münchen.de" should be encoded to punycode
|
||||
let d = Domain::new("münchen.de").unwrap();
|
||||
let ascii = d.dns_name_ascii();
|
||||
// The punycode-encoded form should start with "xn--"
|
||||
assert!(ascii.contains("xn--"), "expected punycode, got: {ascii}");
|
||||
}
|
||||
|
||||
// --- describe_domain (tested indirectly via Domain::describe) ---
|
||||
#[test]
|
||||
fn test_describe_punycode_roundtrip() {
|
||||
// Build a domain with a known punycode label and confirm describe decodes it
|
||||
let d = Domain::new("münchen.de").unwrap();
|
||||
let described = d.describe();
|
||||
// Should contain the Unicode form, not the raw punycode
|
||||
assert!(described.contains("münchen") || described.contains("xn--"),
|
||||
"describe returned: {described}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_describe_regular_ascii() {
|
||||
let d = Domain::FQDN("example.com".to_string());
|
||||
assert_eq!(d.describe(), "example.com");
|
||||
}
|
||||
|
||||
// --- parse_domain_list with empty input ---
|
||||
#[test]
|
||||
fn test_parse_domain_list_empty() {
|
||||
let result = parse_domain_list("").unwrap();
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_domain_list_whitespace_only() {
|
||||
let result = parse_domain_list(" ").unwrap();
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
// --- Tokenizer edge cases (via parse_proxied_expression) ---
|
||||
#[test]
|
||||
fn test_tokenizer_single_ampersand_error() {
|
||||
let result = parse_proxied_expression("is(a.com) & is(b.com)");
|
||||
@@ -504,7 +254,6 @@ mod tests {
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// --- Parser edge cases ---
|
||||
#[test]
|
||||
fn test_parse_and_expr_double_ampersand() {
|
||||
let pred = parse_proxied_expression("is(a.com) && is(b.com)").unwrap();
|
||||
@@ -538,10 +287,8 @@ mod tests {
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// --- make_fqdn with wildcard subdomain ---
|
||||
#[test]
|
||||
fn test_make_fqdn_wildcard_subdomain() {
|
||||
// A name starting with "*." is treated as a wildcard subdomain
|
||||
assert_eq!(make_fqdn("*.sub", "example.com"), "*.sub.example.com");
|
||||
}
|
||||
}
|
||||
|
||||
31
src/main.rs
31
src/main.rs
@@ -20,8 +20,12 @@ use tokio::time::{sleep, Duration};
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
#[tokio::main]
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() {
|
||||
rustls::crypto::ring::default_provider()
|
||||
.install_default()
|
||||
.expect("Failed to install rustls crypto provider");
|
||||
|
||||
// Parse CLI args
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let dry_run = args.iter().any(|a| a == "--dry-run");
|
||||
@@ -229,13 +233,11 @@ async fn run_env_mode(
|
||||
while running.load(Ordering::SeqCst) {
|
||||
// Sleep for interval, checking running flag each second
|
||||
let secs = interval.as_secs();
|
||||
let next_time = chrono::Local::now() + chrono::Duration::seconds(secs as i64);
|
||||
let mins = secs / 60;
|
||||
let rem_secs = secs % 60;
|
||||
ppfmt.infof(
|
||||
pp::EMOJI_SLEEP,
|
||||
&format!(
|
||||
"Next update at {}",
|
||||
next_time.format("%Y-%m-%d %H:%M:%S %Z")
|
||||
),
|
||||
&format!("Next update in {}m {}s", mins, rem_secs),
|
||||
);
|
||||
|
||||
for _ in 0..secs {
|
||||
@@ -282,6 +284,21 @@ fn describe_duration(d: Duration) -> String {
|
||||
// Tests (backwards compatible with original test suite)
|
||||
// ============================================================
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn init_crypto() {
|
||||
use std::sync::Once;
|
||||
static INIT: Once = Once::new();
|
||||
INIT.call_once(|| {
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn test_client() -> reqwest::Client {
|
||||
init_crypto();
|
||||
reqwest::Client::new()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::config::{
|
||||
@@ -333,7 +350,7 @@ mod tests {
|
||||
impl TestDdnsClient {
|
||||
fn new(base_url: &str) -> Self {
|
||||
Self {
|
||||
client: Client::new(),
|
||||
client: crate::test_client(),
|
||||
cf_api_base: base_url.to_string(),
|
||||
ipv4_urls: vec![format!("{base_url}/cdn-cgi/trace")],
|
||||
dry_run: false,
|
||||
|
||||
148
src/notifier.rs
148
src/notifier.rs
@@ -11,14 +11,6 @@ pub struct Message {
|
||||
}
|
||||
|
||||
impl Message {
|
||||
#[allow(dead_code)]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
lines: Vec::new(),
|
||||
ok: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_ok(msg: &str) -> Self {
|
||||
Self {
|
||||
lines: vec![msg.to_string()],
|
||||
@@ -52,16 +44,6 @@ impl Message {
|
||||
}
|
||||
Message { lines, ok }
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn add_line(&mut self, line: &str) {
|
||||
self.lines.push(line.to_string());
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn set_fail(&mut self) {
|
||||
self.ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Composite Notifier ---
|
||||
@@ -72,8 +54,6 @@ pub struct CompositeNotifier {
|
||||
|
||||
// Object-safe version of Notifier
|
||||
pub trait NotifierDyn: Send + Sync {
|
||||
#[allow(dead_code)]
|
||||
fn describe(&self) -> String;
|
||||
fn send_dyn<'a>(
|
||||
&'a self,
|
||||
msg: &'a Message,
|
||||
@@ -85,16 +65,6 @@ impl CompositeNotifier {
|
||||
Self { notifiers }
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.notifiers.is_empty()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn describe(&self) -> Vec<String> {
|
||||
self.notifiers.iter().map(|n| n.describe()).collect()
|
||||
}
|
||||
|
||||
pub async fn send(&self, msg: &Message) {
|
||||
if msg.is_empty() {
|
||||
return;
|
||||
@@ -295,10 +265,6 @@ impl ShoutrrrNotifier {
|
||||
}
|
||||
|
||||
impl NotifierDyn for ShoutrrrNotifier {
|
||||
fn describe(&self) -> String {
|
||||
ShoutrrrNotifier::describe(self)
|
||||
}
|
||||
|
||||
fn send_dyn<'a>(
|
||||
&'a self,
|
||||
msg: &'a Message,
|
||||
@@ -442,8 +408,6 @@ pub struct Heartbeat {
|
||||
}
|
||||
|
||||
pub trait HeartbeatMonitor: Send + Sync {
|
||||
#[allow(dead_code)]
|
||||
fn describe(&self) -> String;
|
||||
fn ping<'a>(
|
||||
&'a self,
|
||||
msg: &'a Message,
|
||||
@@ -462,16 +426,6 @@ impl Heartbeat {
|
||||
Self { monitors }
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.monitors.is_empty()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn describe(&self) -> Vec<String> {
|
||||
self.monitors.iter().map(|m| m.describe()).collect()
|
||||
}
|
||||
|
||||
pub async fn ping(&self, msg: &Message) {
|
||||
for monitor in &self.monitors {
|
||||
monitor.ping(msg).await;
|
||||
@@ -532,10 +486,6 @@ impl HealthchecksMonitor {
|
||||
}
|
||||
|
||||
impl HeartbeatMonitor for HealthchecksMonitor {
|
||||
fn describe(&self) -> String {
|
||||
"Healthchecks.io".to_string()
|
||||
}
|
||||
|
||||
fn ping<'a>(
|
||||
&'a self,
|
||||
msg: &'a Message,
|
||||
@@ -590,10 +540,6 @@ impl UptimeKumaMonitor {
|
||||
}
|
||||
|
||||
impl HeartbeatMonitor for UptimeKumaMonitor {
|
||||
fn describe(&self) -> String {
|
||||
"Uptime Kuma".to_string()
|
||||
}
|
||||
|
||||
fn ping<'a>(
|
||||
&'a self,
|
||||
msg: &'a Message,
|
||||
@@ -675,19 +621,6 @@ mod tests {
|
||||
assert!(!msg.ok);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_new() {
|
||||
let msg = Message::new();
|
||||
assert!(msg.lines.is_empty());
|
||||
assert!(msg.ok);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_is_empty_true() {
|
||||
let msg = Message::new();
|
||||
assert!(msg.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_is_empty_false() {
|
||||
let msg = Message::new_ok("something");
|
||||
@@ -700,20 +633,6 @@ mod tests {
|
||||
assert_eq!(msg.format(), "line1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_format_multiple_lines() {
|
||||
let mut msg = Message::new_ok("line1");
|
||||
msg.add_line("line2");
|
||||
msg.add_line("line3");
|
||||
assert_eq!(msg.format(), "line1\nline2\nline3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_format_empty() {
|
||||
let msg = Message::new();
|
||||
assert_eq!(msg.format(), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_merge_all_ok() {
|
||||
let m1 = Message::new_ok("a");
|
||||
@@ -751,30 +670,12 @@ mod tests {
|
||||
assert!(merged.ok);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_add_line() {
|
||||
let mut msg = Message::new();
|
||||
msg.add_line("first");
|
||||
msg.add_line("second");
|
||||
assert_eq!(msg.lines, vec!["first".to_string(), "second".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_set_fail() {
|
||||
let mut msg = Message::new();
|
||||
assert!(msg.ok);
|
||||
msg.set_fail();
|
||||
assert!(!msg.ok);
|
||||
}
|
||||
|
||||
// ---- CompositeNotifier tests ----
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_composite_notifier_empty_send_does_nothing() {
|
||||
let notifier = CompositeNotifier::new(vec![]);
|
||||
assert!(notifier.is_empty());
|
||||
let msg = Message::new_ok("test");
|
||||
// Should not panic or error
|
||||
notifier.send(&msg).await;
|
||||
}
|
||||
|
||||
@@ -1111,7 +1012,7 @@ mod tests {
|
||||
|
||||
// Build a notifier that points discord webhook at our mock server
|
||||
let notifier = ShoutrrrNotifier {
|
||||
client: Client::new(),
|
||||
client: crate::test_client(),
|
||||
urls: vec![ShoutrrrService {
|
||||
original_url: "discord://token@id".to_string(),
|
||||
service_type: ShoutrrrServiceType::Discord,
|
||||
@@ -1135,7 +1036,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let notifier = ShoutrrrNotifier {
|
||||
client: Client::new(),
|
||||
client: crate::test_client(),
|
||||
urls: vec![ShoutrrrService {
|
||||
original_url: "slack://a/b/c".to_string(),
|
||||
service_type: ShoutrrrServiceType::Slack,
|
||||
@@ -1159,7 +1060,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let notifier = ShoutrrrNotifier {
|
||||
client: Client::new(),
|
||||
client: crate::test_client(),
|
||||
urls: vec![ShoutrrrService {
|
||||
original_url: "generic://example.com/hook".to_string(),
|
||||
service_type: ShoutrrrServiceType::Generic,
|
||||
@@ -1175,10 +1076,10 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn test_shoutrrr_send_empty_message() {
|
||||
let notifier = ShoutrrrNotifier {
|
||||
client: Client::new(),
|
||||
client: crate::test_client(),
|
||||
urls: vec![],
|
||||
};
|
||||
let msg = Message::new();
|
||||
let msg = Message { lines: Vec::new(), ok: true };
|
||||
let pp = PP::default_pp();
|
||||
// Empty message should return true immediately
|
||||
let result = notifier.send(&msg, &pp).await;
|
||||
@@ -1211,7 +1112,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_shoutrrr_notifier_describe() {
|
||||
let notifier = ShoutrrrNotifier {
|
||||
client: Client::new(),
|
||||
client: crate::test_client(),
|
||||
urls: vec![
|
||||
ShoutrrrService {
|
||||
original_url: "discord://t@i".to_string(),
|
||||
@@ -1267,7 +1168,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let notifier = ShoutrrrNotifier {
|
||||
client: Client::new(),
|
||||
client: crate::test_client(),
|
||||
urls: vec![ShoutrrrService {
|
||||
original_url: "telegram://token@telegram?chats=123".to_string(),
|
||||
service_type: ShoutrrrServiceType::Telegram,
|
||||
@@ -1291,7 +1192,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let notifier = ShoutrrrNotifier {
|
||||
client: Client::new(),
|
||||
client: crate::test_client(),
|
||||
urls: vec![ShoutrrrService {
|
||||
original_url: "gotify://host/path".to_string(),
|
||||
service_type: ShoutrrrServiceType::Gotify,
|
||||
@@ -1326,7 +1227,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let notifier = ShoutrrrNotifier {
|
||||
client: Client::new(),
|
||||
client: crate::test_client(),
|
||||
urls: vec![ShoutrrrService {
|
||||
original_url: "custom://host/path".to_string(),
|
||||
service_type: ShoutrrrServiceType::Other("custom".to_string()),
|
||||
@@ -1350,7 +1251,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let notifier = ShoutrrrNotifier {
|
||||
client: Client::new(),
|
||||
client: crate::test_client(),
|
||||
urls: vec![ShoutrrrService {
|
||||
original_url: "discord://t@i".to_string(),
|
||||
service_type: ShoutrrrServiceType::Discord,
|
||||
@@ -1363,23 +1264,6 @@ mod tests {
|
||||
assert!(!result);
|
||||
}
|
||||
|
||||
// ---- CompositeNotifier describe ----
|
||||
|
||||
#[test]
|
||||
fn test_composite_notifier_describe_empty() {
|
||||
let notifier = CompositeNotifier::new(vec![]);
|
||||
assert!(notifier.describe().is_empty());
|
||||
}
|
||||
|
||||
// ---- Heartbeat describe and is_empty ----
|
||||
|
||||
#[test]
|
||||
fn test_heartbeat_is_empty() {
|
||||
let hb = Heartbeat::new(vec![]);
|
||||
assert!(hb.is_empty());
|
||||
assert!(hb.describe().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_heartbeat_ping_no_monitors() {
|
||||
let hb = Heartbeat::new(vec![]);
|
||||
@@ -1401,16 +1285,6 @@ mod tests {
|
||||
hb.exit(&msg).await;
|
||||
}
|
||||
|
||||
// ---- CompositeNotifier send with empty message ----
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_composite_notifier_send_empty_message_skips() {
|
||||
let notifier = CompositeNotifier::new(vec![]);
|
||||
let msg = Message::new(); // empty
|
||||
// Should return immediately without sending
|
||||
notifier.send(&msg).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_shoutrrr_send_server_error() {
|
||||
let server = MockServer::start().await;
|
||||
@@ -1422,7 +1296,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let notifier = ShoutrrrNotifier {
|
||||
client: Client::new(),
|
||||
client: crate::test_client(),
|
||||
urls: vec![ShoutrrrService {
|
||||
original_url: "generic://example.com/hook".to_string(),
|
||||
service_type: ShoutrrrServiceType::Generic,
|
||||
|
||||
200
src/pp.rs
200
src/pp.rs
@@ -1,6 +1,3 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
// Verbosity levels
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum Verbosity {
|
||||
@@ -11,12 +8,8 @@ pub enum Verbosity {
|
||||
}
|
||||
|
||||
// Emoji constants
|
||||
#[allow(dead_code)]
|
||||
pub const EMOJI_GLOBE: &str = "\u{1F30D}";
|
||||
pub const EMOJI_WARNING: &str = "\u{26A0}\u{FE0F}";
|
||||
pub const EMOJI_ERROR: &str = "\u{274C}";
|
||||
#[allow(dead_code)]
|
||||
pub const EMOJI_SUCCESS: &str = "\u{2705}";
|
||||
pub const EMOJI_LAUNCH: &str = "\u{1F680}";
|
||||
pub const EMOJI_STOP: &str = "\u{1F6D1}";
|
||||
pub const EMOJI_SLEEP: &str = "\u{1F634}";
|
||||
@@ -28,8 +21,6 @@ pub const EMOJI_SKIP: &str = "\u{23ED}\u{FE0F}";
|
||||
pub const EMOJI_NOTIFY: &str = "\u{1F514}";
|
||||
pub const EMOJI_HEARTBEAT: &str = "\u{1F493}";
|
||||
pub const EMOJI_CONFIG: &str = "\u{2699}\u{FE0F}";
|
||||
#[allow(dead_code)]
|
||||
pub const EMOJI_HINT: &str = "\u{1F4A1}";
|
||||
|
||||
const INDENT_PREFIX: &str = " ";
|
||||
|
||||
@@ -37,7 +28,6 @@ pub struct PP {
|
||||
pub verbosity: Verbosity,
|
||||
pub emoji: bool,
|
||||
indent: usize,
|
||||
seen: Arc<Mutex<HashSet<String>>>,
|
||||
}
|
||||
|
||||
impl PP {
|
||||
@@ -46,7 +36,6 @@ impl PP {
|
||||
verbosity: if quiet { Verbosity::Quiet } else { Verbosity::Verbose },
|
||||
emoji,
|
||||
indent: 0,
|
||||
seen: Arc::new(Mutex::new(HashSet::new())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +52,6 @@ impl PP {
|
||||
verbosity: self.verbosity,
|
||||
emoji: self.emoji,
|
||||
indent: self.indent + 1,
|
||||
seen: Arc::clone(&self.seen),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,54 +92,12 @@ impl PP {
|
||||
pub fn errorf(&self, emoji: &str, msg: &str) {
|
||||
self.output_err(emoji, msg);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn info_once(&self, key: &str, emoji: &str, msg: &str) {
|
||||
if self.is_showing(Verbosity::Info) {
|
||||
let mut seen = self.seen.lock().unwrap();
|
||||
if seen.insert(key.to_string()) {
|
||||
self.output(emoji, msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn notice_once(&self, key: &str, emoji: &str, msg: &str) {
|
||||
if self.is_showing(Verbosity::Notice) {
|
||||
let mut seen = self.seen.lock().unwrap();
|
||||
if seen.insert(key.to_string()) {
|
||||
self.output(emoji, msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn blank_line_if_verbose(&self) {
|
||||
if self.is_showing(Verbosity::Verbose) {
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn english_join(items: &[String]) -> String {
|
||||
match items.len() {
|
||||
0 => String::new(),
|
||||
1 => items[0].clone(),
|
||||
2 => format!("{} and {}", items[0], items[1]),
|
||||
_ => {
|
||||
let (last, rest) = items.split_last().unwrap();
|
||||
format!("{}, and {last}", rest.join(", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// ---- PP::new with emoji flag ----
|
||||
|
||||
#[test]
|
||||
fn new_with_emoji_true() {
|
||||
let pp = PP::new(true, false);
|
||||
@@ -164,8 +110,6 @@ mod tests {
|
||||
assert!(!pp.emoji);
|
||||
}
|
||||
|
||||
// ---- PP::new with quiet flag (verbosity levels) ----
|
||||
|
||||
#[test]
|
||||
fn new_quiet_true_sets_verbosity_quiet() {
|
||||
let pp = PP::new(false, true);
|
||||
@@ -178,8 +122,6 @@ mod tests {
|
||||
assert_eq!(pp.verbosity, Verbosity::Verbose);
|
||||
}
|
||||
|
||||
// ---- PP::is_showing at different verbosity levels ----
|
||||
|
||||
#[test]
|
||||
fn quiet_shows_only_quiet_level() {
|
||||
let pp = PP::new(false, true);
|
||||
@@ -218,8 +160,6 @@ mod tests {
|
||||
assert!(!pp.is_showing(Verbosity::Verbose));
|
||||
}
|
||||
|
||||
// ---- PP::indent ----
|
||||
|
||||
#[test]
|
||||
fn indent_increments_indent_level() {
|
||||
let pp = PP::new(true, false);
|
||||
@@ -238,26 +178,6 @@ mod tests {
|
||||
assert_eq!(child.emoji, pp.emoji);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn indent_shares_seen_state() {
|
||||
let pp = PP::new(false, false);
|
||||
let child = pp.indent();
|
||||
|
||||
// Insert via parent's seen set
|
||||
pp.seen.lock().unwrap().insert("key1".to_string());
|
||||
|
||||
// Child should observe the same entry
|
||||
assert!(child.seen.lock().unwrap().contains("key1"));
|
||||
|
||||
// Insert via child
|
||||
child.seen.lock().unwrap().insert("key2".to_string());
|
||||
|
||||
// Parent should observe it too
|
||||
assert!(pp.seen.lock().unwrap().contains("key2"));
|
||||
}
|
||||
|
||||
// ---- PP::infof, noticef, warningf, errorf - no panic and verbosity gating ----
|
||||
|
||||
#[test]
|
||||
fn infof_does_not_panic_when_verbose() {
|
||||
let pp = PP::new(false, false);
|
||||
@@ -267,7 +187,6 @@ mod tests {
|
||||
#[test]
|
||||
fn infof_does_not_panic_when_quiet() {
|
||||
let pp = PP::new(false, true);
|
||||
// Should simply not print, and not panic
|
||||
pp.infof("", "test info message");
|
||||
}
|
||||
|
||||
@@ -291,7 +210,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn warningf_does_not_panic_when_quiet() {
|
||||
// warningf always outputs (no verbosity check), just verify no panic
|
||||
let pp = PP::new(false, true);
|
||||
pp.warningf("", "test warning");
|
||||
}
|
||||
@@ -308,124 +226,6 @@ mod tests {
|
||||
pp.errorf("", "test error");
|
||||
}
|
||||
|
||||
// ---- PP::info_once and notice_once ----
|
||||
|
||||
#[test]
|
||||
fn info_once_suppresses_duplicates() {
|
||||
let pp = PP::new(false, false);
|
||||
// First call inserts the key
|
||||
pp.info_once("dup_key", "", "first");
|
||||
// The key should now be in the seen set
|
||||
assert!(pp.seen.lock().unwrap().contains("dup_key"));
|
||||
|
||||
// Calling again with the same key should not insert again (set unchanged)
|
||||
let size_before = pp.seen.lock().unwrap().len();
|
||||
pp.info_once("dup_key", "", "second");
|
||||
let size_after = pp.seen.lock().unwrap().len();
|
||||
assert_eq!(size_before, size_after);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn info_once_allows_different_keys() {
|
||||
let pp = PP::new(false, false);
|
||||
pp.info_once("key_a", "", "msg a");
|
||||
pp.info_once("key_b", "", "msg b");
|
||||
let seen = pp.seen.lock().unwrap();
|
||||
assert!(seen.contains("key_a"));
|
||||
assert!(seen.contains("key_b"));
|
||||
assert_eq!(seen.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn info_once_skipped_when_quiet() {
|
||||
let pp = PP::new(false, true);
|
||||
pp.info_once("quiet_key", "", "should not register");
|
||||
// Because verbosity is Quiet, info_once should not even insert the key
|
||||
assert!(!pp.seen.lock().unwrap().contains("quiet_key"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn notice_once_suppresses_duplicates() {
|
||||
let pp = PP::new(false, false);
|
||||
pp.notice_once("notice_dup", "", "first");
|
||||
assert!(pp.seen.lock().unwrap().contains("notice_dup"));
|
||||
|
||||
let size_before = pp.seen.lock().unwrap().len();
|
||||
pp.notice_once("notice_dup", "", "second");
|
||||
let size_after = pp.seen.lock().unwrap().len();
|
||||
assert_eq!(size_before, size_after);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn notice_once_skipped_when_quiet() {
|
||||
let pp = PP::new(false, true);
|
||||
pp.notice_once("quiet_notice", "", "should not register");
|
||||
assert!(!pp.seen.lock().unwrap().contains("quiet_notice"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn info_once_shared_via_indent() {
|
||||
let pp = PP::new(false, false);
|
||||
let child = pp.indent();
|
||||
|
||||
// Mark a key via the parent
|
||||
pp.info_once("shared_key", "", "parent");
|
||||
assert!(pp.seen.lock().unwrap().contains("shared_key"));
|
||||
|
||||
// Child should see it as already present, so set size stays the same
|
||||
let size_before = child.seen.lock().unwrap().len();
|
||||
child.info_once("shared_key", "", "child duplicate");
|
||||
let size_after = child.seen.lock().unwrap().len();
|
||||
assert_eq!(size_before, size_after);
|
||||
|
||||
// Child can add a new key visible to parent
|
||||
child.info_once("child_key", "", "child new");
|
||||
assert!(pp.seen.lock().unwrap().contains("child_key"));
|
||||
}
|
||||
|
||||
// ---- english_join ----
|
||||
|
||||
#[test]
|
||||
fn english_join_empty() {
|
||||
let items: Vec<String> = vec![];
|
||||
assert_eq!(english_join(&items), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn english_join_single() {
|
||||
let items = vec!["alpha".to_string()];
|
||||
assert_eq!(english_join(&items), "alpha");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn english_join_two() {
|
||||
let items = vec!["alpha".to_string(), "beta".to_string()];
|
||||
assert_eq!(english_join(&items), "alpha and beta");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn english_join_three() {
|
||||
let items = vec![
|
||||
"alpha".to_string(),
|
||||
"beta".to_string(),
|
||||
"gamma".to_string(),
|
||||
];
|
||||
assert_eq!(english_join(&items), "alpha, beta, and gamma");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn english_join_four() {
|
||||
let items = vec![
|
||||
"a".to_string(),
|
||||
"b".to_string(),
|
||||
"c".to_string(),
|
||||
"d".to_string(),
|
||||
];
|
||||
assert_eq!(english_join(&items), "a, b, c, and d");
|
||||
}
|
||||
|
||||
// ---- default_pp ----
|
||||
|
||||
#[test]
|
||||
fn default_pp_is_verbose_no_emoji() {
|
||||
let pp = PP::default_pp();
|
||||
|
||||
@@ -26,10 +26,6 @@ impl IpType {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn all() -> &'static [IpType] {
|
||||
&[IpType::V4, IpType::V6]
|
||||
}
|
||||
}
|
||||
|
||||
/// All supported provider types
|
||||
@@ -879,7 +875,7 @@ mod tests {
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let client = Client::new();
|
||||
let client = crate::test_client();
|
||||
let ppfmt = PP::default_pp();
|
||||
let url = format!("{}/cdn-cgi/trace", server.uri());
|
||||
let timeout = Duration::from_secs(5);
|
||||
@@ -919,7 +915,7 @@ mod tests {
|
||||
|
||||
// We can't override the hardcoded primary/fallback URLs, but we can test
|
||||
// the custom URL path: first with a failing URL, then a succeeding one.
|
||||
let client = Client::new();
|
||||
let client = crate::test_client();
|
||||
let ppfmt = PP::default_pp();
|
||||
let timeout = Duration::from_secs(5);
|
||||
|
||||
@@ -1012,7 +1008,7 @@ mod tests {
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let client = Client::new();
|
||||
let client = crate::test_client();
|
||||
let ppfmt = PP::default_pp();
|
||||
let timeout = Duration::from_secs(5);
|
||||
|
||||
@@ -1035,7 +1031,7 @@ mod tests {
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let client = Client::new();
|
||||
let client = crate::test_client();
|
||||
let ppfmt = PP::default_pp();
|
||||
let timeout = Duration::from_secs(5);
|
||||
|
||||
@@ -1056,7 +1052,7 @@ mod tests {
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let client = Client::new();
|
||||
let client = crate::test_client();
|
||||
let ppfmt = PP::default_pp();
|
||||
let timeout = Duration::from_secs(5);
|
||||
let url = format!("{}/my-ip", server.uri());
|
||||
@@ -1076,7 +1072,7 @@ mod tests {
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let client = Client::new();
|
||||
let client = crate::test_client();
|
||||
let ppfmt = PP::default_pp();
|
||||
let timeout = Duration::from_secs(5);
|
||||
let url = format!("{}/my-ip", server.uri());
|
||||
@@ -1140,7 +1136,7 @@ mod tests {
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let client = Client::new();
|
||||
let client = crate::test_client();
|
||||
let ppfmt = PP::default_pp();
|
||||
let timeout = Duration::from_secs(5);
|
||||
let url = format!("{}/my-ip", server.uri());
|
||||
@@ -1351,7 +1347,7 @@ mod tests {
|
||||
"5.6.7.8".parse().unwrap(),
|
||||
],
|
||||
};
|
||||
let client = Client::new();
|
||||
let client = crate::test_client();
|
||||
let ppfmt = PP::default_pp();
|
||||
let timeout = Duration::from_secs(5);
|
||||
|
||||
@@ -1369,7 +1365,7 @@ mod tests {
|
||||
"2001:db8::1".parse().unwrap(),
|
||||
],
|
||||
};
|
||||
let client = Client::new();
|
||||
let client = crate::test_client();
|
||||
let ppfmt = PP::default_pp();
|
||||
let timeout = Duration::from_secs(5);
|
||||
|
||||
@@ -1383,7 +1379,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn test_none_detect_ips_returns_empty() {
|
||||
let provider = ProviderType::None;
|
||||
let client = Client::new();
|
||||
let client = crate::test_client();
|
||||
let ppfmt = PP::default_pp();
|
||||
let timeout = Duration::from_secs(5);
|
||||
|
||||
|
||||
@@ -849,7 +849,7 @@ mod tests {
|
||||
let ppfmt = pp();
|
||||
|
||||
let mut cf_cache = CachedCloudflareFilter::new();
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt, &mut HashSet::new(), &Client::new()).await;
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt, &mut HashSet::new(), &crate::test_client()).await;
|
||||
assert!(ok);
|
||||
}
|
||||
|
||||
@@ -902,12 +902,12 @@ mod tests {
|
||||
let mut noop_reported = HashSet::new();
|
||||
|
||||
// First call: noop_reported is empty, so "up to date" is reported and key is inserted
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt, &mut noop_reported, &Client::new()).await;
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt, &mut noop_reported, &crate::test_client()).await;
|
||||
assert!(ok);
|
||||
assert!(noop_reported.contains("home.example.com:A"), "noop_reported should contain the domain key after first noop");
|
||||
|
||||
// Second call: noop_reported already has the key, so the message is suppressed
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt, &mut noop_reported, &Client::new()).await;
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt, &mut noop_reported, &crate::test_client()).await;
|
||||
assert!(ok);
|
||||
assert_eq!(noop_reported.len(), 1, "noop_reported should still have exactly one entry");
|
||||
}
|
||||
@@ -980,7 +980,7 @@ mod tests {
|
||||
noop_reported.insert("home.example.com:A".to_string());
|
||||
|
||||
let mut cf_cache = CachedCloudflareFilter::new();
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt, &mut noop_reported, &Client::new()).await;
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt, &mut noop_reported, &crate::test_client()).await;
|
||||
assert!(ok);
|
||||
assert!(!noop_reported.contains("home.example.com:A"), "noop_reported should be cleared after an update");
|
||||
}
|
||||
@@ -1026,7 +1026,7 @@ mod tests {
|
||||
|
||||
// all_ok = true because no zone-level errors occurred (empty ips just noop or warn)
|
||||
let mut cf_cache = CachedCloudflareFilter::new();
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt, &mut HashSet::new(), &Client::new()).await;
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt, &mut HashSet::new(), &crate::test_client()).await;
|
||||
// Providers with None are not inserted in loop, so no IP detection warning is emitted,
|
||||
// no detected_ips entry is created, and set_ips is called with empty slice -> Noop.
|
||||
assert!(ok);
|
||||
@@ -1076,7 +1076,7 @@ mod tests {
|
||||
let ppfmt = pp();
|
||||
|
||||
let mut cf_cache = CachedCloudflareFilter::new();
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt, &mut HashSet::new(), &Client::new()).await;
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt, &mut HashSet::new(), &crate::test_client()).await;
|
||||
assert!(!ok, "Expected false when zone is not found");
|
||||
}
|
||||
|
||||
@@ -1126,7 +1126,7 @@ mod tests {
|
||||
|
||||
// dry_run returns Updated from set_ips (it signals intent), all_ok should be true
|
||||
let mut cf_cache = CachedCloudflareFilter::new();
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt, &mut HashSet::new(), &Client::new()).await;
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt, &mut HashSet::new(), &crate::test_client()).await;
|
||||
assert!(ok);
|
||||
}
|
||||
|
||||
@@ -1192,7 +1192,7 @@ mod tests {
|
||||
let ppfmt = pp();
|
||||
|
||||
let mut cf_cache = CachedCloudflareFilter::new();
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt, &mut HashSet::new(), &Client::new()).await;
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt, &mut HashSet::new(), &crate::test_client()).await;
|
||||
assert!(ok);
|
||||
}
|
||||
|
||||
@@ -1246,7 +1246,7 @@ mod tests {
|
||||
let ppfmt = pp();
|
||||
|
||||
let mut cf_cache = CachedCloudflareFilter::new();
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt, &mut HashSet::new(), &Client::new()).await;
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt, &mut HashSet::new(), &crate::test_client()).await;
|
||||
assert!(ok);
|
||||
}
|
||||
|
||||
@@ -1286,7 +1286,7 @@ mod tests {
|
||||
let ppfmt = pp();
|
||||
|
||||
let mut cf_cache = CachedCloudflareFilter::new();
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt, &mut HashSet::new(), &Client::new()).await;
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt, &mut HashSet::new(), &crate::test_client()).await;
|
||||
assert!(!ok, "Expected false when WAF list is not found");
|
||||
}
|
||||
|
||||
@@ -1371,7 +1371,7 @@ mod tests {
|
||||
let ppfmt = pp();
|
||||
|
||||
let mut cf_cache = CachedCloudflareFilter::new();
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt, &mut HashSet::new(), &Client::new()).await;
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt, &mut HashSet::new(), &crate::test_client()).await;
|
||||
assert!(ok);
|
||||
}
|
||||
|
||||
@@ -1388,7 +1388,7 @@ mod tests {
|
||||
let ppfmt = pp();
|
||||
|
||||
let mut cf_cache = CachedCloudflareFilter::new();
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt, &mut HashSet::new(), &Client::new()).await;
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt, &mut HashSet::new(), &crate::test_client()).await;
|
||||
assert!(ok);
|
||||
}
|
||||
|
||||
@@ -1773,7 +1773,7 @@ mod tests {
|
||||
|
||||
// set_ips with empty ips and no existing records = Noop; all_ok = true
|
||||
let mut cf_cache = CachedCloudflareFilter::new();
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt, &mut HashSet::new(), &Client::new()).await;
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt, &mut HashSet::new(), &crate::test_client()).await;
|
||||
assert!(ok);
|
||||
}
|
||||
// -------------------------------------------------------
|
||||
@@ -1792,7 +1792,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let ddns = LegacyDdnsClient {
|
||||
client: Client::new(),
|
||||
client: crate::test_client(),
|
||||
cf_api_base: server.uri(),
|
||||
dry_run: false,
|
||||
};
|
||||
@@ -1824,7 +1824,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let ddns = LegacyDdnsClient {
|
||||
client: Client::new(),
|
||||
client: crate::test_client(),
|
||||
cf_api_base: server.uri(),
|
||||
dry_run: false,
|
||||
};
|
||||
@@ -1853,7 +1853,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let ddns = LegacyDdnsClient {
|
||||
client: Client::new(),
|
||||
client: crate::test_client(),
|
||||
cf_api_base: server.uri(),
|
||||
dry_run: false,
|
||||
};
|
||||
@@ -1875,7 +1875,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn test_legacy_cf_api_unknown_method() {
|
||||
let ddns = LegacyDdnsClient {
|
||||
client: Client::new(),
|
||||
client: crate::test_client(),
|
||||
cf_api_base: "http://localhost".to_string(),
|
||||
dry_run: false,
|
||||
};
|
||||
@@ -1905,7 +1905,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let ddns = LegacyDdnsClient {
|
||||
client: Client::new(),
|
||||
client: crate::test_client(),
|
||||
cf_api_base: server.uri(),
|
||||
dry_run: false,
|
||||
};
|
||||
@@ -1961,7 +1961,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let ddns = LegacyDdnsClient {
|
||||
client: Client::new(),
|
||||
client: crate::test_client(),
|
||||
cf_api_base: server.uri(),
|
||||
dry_run: false,
|
||||
};
|
||||
@@ -2017,7 +2017,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let ddns = LegacyDdnsClient {
|
||||
client: Client::new(),
|
||||
client: crate::test_client(),
|
||||
cf_api_base: server.uri(),
|
||||
dry_run: false,
|
||||
};
|
||||
@@ -2059,7 +2059,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let ddns = LegacyDdnsClient {
|
||||
client: Client::new(),
|
||||
client: crate::test_client(),
|
||||
cf_api_base: server.uri(),
|
||||
dry_run: true,
|
||||
};
|
||||
@@ -2110,7 +2110,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let ddns = LegacyDdnsClient {
|
||||
client: Client::new(),
|
||||
client: crate::test_client(),
|
||||
cf_api_base: server.uri(),
|
||||
dry_run: false,
|
||||
};
|
||||
@@ -2165,7 +2165,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let ddns = LegacyDdnsClient {
|
||||
client: Client::new(),
|
||||
client: crate::test_client(),
|
||||
cf_api_base: server.uri(),
|
||||
dry_run: false,
|
||||
};
|
||||
@@ -2214,7 +2214,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let ddns = LegacyDdnsClient {
|
||||
client: Client::new(),
|
||||
client: crate::test_client(),
|
||||
cf_api_base: server.uri(),
|
||||
dry_run: false,
|
||||
};
|
||||
@@ -2258,7 +2258,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let ddns = LegacyDdnsClient {
|
||||
client: Client::new(),
|
||||
client: crate::test_client(),
|
||||
cf_api_base: server.uri(),
|
||||
dry_run: false,
|
||||
};
|
||||
@@ -2290,7 +2290,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let ddns = LegacyDdnsClient {
|
||||
client: Client::new(),
|
||||
client: crate::test_client(),
|
||||
cf_api_base: server.uri(),
|
||||
dry_run: true,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user