Skip to content

Commit bb83e84

Browse files
committed
fix: add retries and parallel requests to logo URL test
CDN requests can intermittently fail in CI. Use backon for exponential backoff retries (3 attempts) and futures::stream for parallel requests (up to 10 concurrent) to improve test reliability.
1 parent 4d1ce07 commit bb83e84

File tree

1 file changed

+112
-72
lines changed
  • backend/src/server/services/impl

1 file changed

+112
-72
lines changed

backend/src/server/services/impl/tests.rs

Lines changed: 112 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -594,9 +594,12 @@ fn test_service_definition_serialization() {
594594
}
595595
#[tokio::test]
596596
async fn test_service_definition_logo_urls_resolve() {
597+
use backon::{ExponentialBuilder, Retryable};
598+
use futures::stream::{self, StreamExt};
599+
597600
let registry = ServiceDefinitionRegistry::all_service_definitions();
598601
let client = reqwest::Client::builder()
599-
.timeout(std::time::Duration::from_secs(5))
602+
.timeout(std::time::Duration::from_secs(10))
600603
.build()
601604
.expect("Failed to create HTTP client");
602605

@@ -607,89 +610,126 @@ async fn test_service_definition_logo_urls_resolve() {
607610
"cdn.prod.website-files.com",
608611
];
609612

610-
for service in registry {
611-
let logo_url = service.logo_url();
612-
613-
// Skip services without logo URLs
614-
if logo_url.is_empty() {
615-
continue;
616-
}
617-
618-
// Check if it's a local file path or external URL
619-
if logo_url.starts_with('/') {
620-
// Local file path like /logos/scanopy-logo.png
621-
assert!(
622-
logo_url.starts_with("/logos/"),
623-
"Service '{}' has local logo URL '{}' that doesn't start with /logos/",
624-
ServiceDefinition::name(&service),
625-
logo_url
626-
);
627-
// We can't verify local files exist in tests, so just validate the path format
628-
continue;
629-
}
613+
// Collect services with external logo URLs to validate
614+
let services_to_check: Vec<_> = registry
615+
.into_iter()
616+
.filter_map(|service| {
617+
let logo_url = service.logo_url();
630618

631-
// Must be a URL - parse it
632-
let url = match reqwest::Url::parse(logo_url) {
633-
Ok(url) => url,
634-
Err(e) => {
635-
panic!(
636-
"Service '{}' has invalid logo URL '{}': {}",
637-
ServiceDefinition::name(&service),
638-
logo_url,
639-
e
640-
);
619+
// Skip services without logo URLs
620+
if logo_url.is_empty() {
621+
return None;
641622
}
642-
};
643-
644-
// Check domain is in allowed list
645-
let domain = url.domain().unwrap_or("");
646-
let is_allowed = ALLOWED_DOMAINS
647-
.iter()
648-
.any(|allowed| domain.ends_with(allowed));
649-
650-
assert!(
651-
is_allowed,
652-
"Service '{}' has logo URL '{}' from unauthorized domain '{}'. \
653-
Allowed domains: {}",
654-
ServiceDefinition::name(&service),
655-
logo_url,
656-
domain,
657-
ALLOWED_DOMAINS.join(", ")
658-
);
659623

660-
// Attempt to fetch the logo URL
661-
match client.head(logo_url).send().await {
662-
Ok(response) => {
624+
// Check if it's a local file path
625+
if logo_url.starts_with('/') {
626+
// Local file path like /logos/scanopy-logo.png
663627
assert!(
664-
response.status().is_success(),
665-
"Service '{}' has logo URL '{}' that returned status {}",
628+
logo_url.starts_with("/logos/"),
629+
"Service '{}' has local logo URL '{}' that doesn't start with /logos/",
666630
ServiceDefinition::name(&service),
667-
logo_url,
668-
response.status()
631+
logo_url
669632
);
633+
return None;
634+
}
670635

671-
// Verify Content-Type is an image
672-
if let Some(content_type) = response.headers().get("content-type") {
673-
let content_type_str = content_type.to_str().unwrap_or("");
674-
assert!(
675-
content_type_str.starts_with("image/")
676-
|| content_type_str.starts_with("text/plain"),
677-
"Service '{}' has logo URL '{}' with non-image Content-Type: {}",
636+
// Must be a URL - parse it
637+
let url = match reqwest::Url::parse(logo_url) {
638+
Ok(url) => url,
639+
Err(e) => {
640+
panic!(
641+
"Service '{}' has invalid logo URL '{}': {}",
678642
ServiceDefinition::name(&service),
679643
logo_url,
680-
content_type_str
644+
e
681645
);
682646
}
647+
};
648+
649+
// Check domain is in allowed list
650+
let domain = url.domain().unwrap_or("");
651+
let is_allowed = ALLOWED_DOMAINS
652+
.iter()
653+
.any(|allowed| domain.ends_with(allowed));
654+
655+
assert!(
656+
is_allowed,
657+
"Service '{}' has logo URL '{}' from unauthorized domain '{}'. \
658+
Allowed domains: {}",
659+
ServiceDefinition::name(&service),
660+
logo_url,
661+
domain,
662+
ALLOWED_DOMAINS.join(", ")
663+
);
664+
665+
Some((service.name().to_string(), logo_url.to_string()))
666+
})
667+
.collect();
668+
669+
// Fetch all logo URLs in parallel with retries
670+
let results: Vec<Result<(), String>> = stream::iter(services_to_check)
671+
.map(|(service_name, logo_url)| {
672+
let client = client.clone();
673+
async move {
674+
let fetch_with_retry = || {
675+
let client = client.clone();
676+
let url = logo_url.clone();
677+
async move {
678+
let response = client
679+
.head(&url)
680+
.send()
681+
.await
682+
.map_err(|e| format!("request failed: {}", e))?;
683+
684+
if response.status().is_success() {
685+
// Verify Content-Type is an image
686+
if let Some(content_type) = response.headers().get("content-type") {
687+
let content_type_str = content_type.to_str().unwrap_or("");
688+
if !content_type_str.starts_with("image/")
689+
&& !content_type_str.starts_with("text/plain")
690+
{
691+
return Err(format!(
692+
"non-image Content-Type: {}",
693+
content_type_str
694+
));
695+
}
696+
}
697+
Ok(())
698+
} else {
699+
Err(format!("returned status {}", response.status()))
700+
}
701+
}
702+
};
703+
704+
fetch_with_retry
705+
.retry(
706+
ExponentialBuilder::default()
707+
.with_max_times(3)
708+
.with_min_delay(std::time::Duration::from_millis(500))
709+
.with_max_delay(std::time::Duration::from_secs(5)),
710+
)
711+
.await
712+
.map_err(|e| {
713+
format!(
714+
"Service '{}' has logo URL '{}' that failed to resolve: {}",
715+
service_name, logo_url, e
716+
)
717+
})
683718
}
684-
Err(e) => {
685-
panic!(
686-
"Service '{}' has logo URL '{}' that failed to resolve: {}",
687-
ServiceDefinition::name(&service),
688-
logo_url,
689-
e
690-
);
691-
}
692-
}
719+
})
720+
.buffer_unordered(10) // Run up to 10 requests in parallel
721+
.collect()
722+
.await;
723+
724+
// Collect all failures and report them together
725+
let failures: Vec<_> = results.into_iter().filter_map(|r| r.err()).collect();
726+
727+
if !failures.is_empty() {
728+
panic!(
729+
"Logo URL validation failed for {} service(s):\n{}",
730+
failures.len(),
731+
failures.join("\n")
732+
);
693733
}
694734
}
695735

0 commit comments

Comments
 (0)