-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create smoke tests for database dump (#21)
crates.io provides a database dump that can be downloaded by users. The database dump is too large to serve through Fastly's Compute@Edge platform, which is why we redirect all requests to CloudFront. This functionality is now being tested by a smoke test.
- Loading branch information
Showing
5 changed files
with
440 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
//! Test that CloudFront serves the database dump | ||
use async_trait::async_trait; | ||
use reqwest::Client; | ||
|
||
use crate::test::{Test, TestResult}; | ||
|
||
use super::config::Config; | ||
|
||
/// The name of the test | ||
const NAME: &str = "CloudFront"; | ||
|
||
/// Test that CloudFront serves the database dump | ||
/// | ||
/// The database dump cannot be served directly from Fastly, so it is served from CloudFront. | ||
pub struct CloudFront<'a> { | ||
/// Configuration for this test | ||
config: &'a Config, | ||
} | ||
|
||
impl<'a> CloudFront<'a> { | ||
/// Create a new instance of the test | ||
pub fn new(config: &'a Config) -> Self { | ||
Self { config } | ||
} | ||
} | ||
|
||
#[async_trait] | ||
impl<'a> Test for CloudFront<'a> { | ||
async fn run(&self) -> TestResult { | ||
let response = match Client::builder() | ||
.build() | ||
.expect("failed to build reqwest client") | ||
.head(format!("{}/db-dump.tar.gz", self.config.cloudfront_url())) | ||
.send() | ||
.await | ||
{ | ||
Ok(response) => response, | ||
Err(error) => { | ||
return TestResult::builder() | ||
.name(NAME) | ||
.success(false) | ||
.message(Some(error.to_string())) | ||
.build() | ||
} | ||
}; | ||
|
||
if response.status().is_success() { | ||
TestResult::builder().name(NAME).success(true).build() | ||
} else { | ||
TestResult::builder() | ||
.name(NAME) | ||
.success(false) | ||
.message(Some(format!( | ||
"Expected HTTP 200, got HTTP {}", | ||
response.status() | ||
))) | ||
.build() | ||
} | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use mockito::ServerGuard; | ||
|
||
use crate::test_utils::*; | ||
|
||
use super::*; | ||
|
||
pub async fn setup() -> (ServerGuard, Config) { | ||
let server = mockito::Server::new_async().await; | ||
|
||
let config = Config::builder() | ||
.cloudfront_url(server.url()) | ||
.fastly_url(server.url()) | ||
.build(); | ||
|
||
(server, config) | ||
} | ||
|
||
#[tokio::test] | ||
async fn succeeds_with_http_200_response() { | ||
let (mut server, config) = setup().await; | ||
|
||
let mock = server | ||
.mock("HEAD", "/db-dump.tar.gz") | ||
.with_status(200) | ||
.create(); | ||
|
||
let result = CloudFront::new(&config).run().await; | ||
|
||
// Assert that the mock was called | ||
mock.assert(); | ||
|
||
assert!(result.success()); | ||
} | ||
|
||
#[tokio::test] | ||
async fn fails_with_other_http_responses() { | ||
let (mut server, config) = setup().await; | ||
|
||
let mock = server | ||
.mock("HEAD", "/db-dump.tar.gz") | ||
.with_status(500) | ||
.create(); | ||
|
||
let result = CloudFront::new(&config).run().await; | ||
|
||
// Assert that the mock was called | ||
mock.assert(); | ||
|
||
assert!(!result.success()); | ||
} | ||
|
||
#[test] | ||
fn trait_send() { | ||
assert_send::<CloudFront>(); | ||
} | ||
|
||
#[test] | ||
fn trait_sync() { | ||
assert_sync::<CloudFront>(); | ||
} | ||
|
||
#[test] | ||
fn trait_unpin() { | ||
assert_unpin::<CloudFront>(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
//! Configuration to test the database dump | ||
use getset::Getters; | ||
#[cfg(test)] | ||
use typed_builder::TypedBuilder; | ||
|
||
use crate::environment::Environment; | ||
|
||
/// Configuration to test the database dump | ||
/// | ||
/// The smoke tests request the database dump from Fastly and CloudFront and check for the correct | ||
/// response. | ||
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Getters)] | ||
#[cfg_attr(test, derive(TypedBuilder))] | ||
pub struct Config { | ||
/// The URL for the CloudFront CDN | ||
#[getset(get = "pub")] | ||
cloudfront_url: String, | ||
|
||
/// The URL for the Fastly CDN | ||
#[getset(get = "pub")] | ||
fastly_url: String, | ||
} | ||
|
||
impl Config { | ||
/// Return the configuration for the given environment | ||
pub fn for_env(env: Environment) -> Self { | ||
match env { | ||
Environment::Staging => Self { | ||
cloudfront_url: "https://cloudfront-static.staging.crates.io".into(), | ||
fastly_url: "https://fastly-static.staging.crates.io".into(), | ||
}, | ||
Environment::Production => Self { | ||
cloudfront_url: "https://cloudfront-static.crates.io".into(), | ||
fastly_url: "https://fastly-static.crates.io".into(), | ||
}, | ||
} | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use crate::test_utils::*; | ||
|
||
use super::*; | ||
|
||
#[test] | ||
fn trait_send() { | ||
assert_send::<Config>(); | ||
} | ||
|
||
#[test] | ||
fn trait_sync() { | ||
assert_sync::<Config>(); | ||
} | ||
|
||
#[test] | ||
fn trait_unpin() { | ||
assert_unpin::<Config>(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
//! Test that Fastly redirects to CloudFront | ||
use async_trait::async_trait; | ||
use reqwest::redirect::Policy; | ||
use reqwest::{Client, Response}; | ||
|
||
use crate::test::{Test, TestResult}; | ||
|
||
use super::config::Config; | ||
|
||
/// The name of the test | ||
const NAME: &str = "Fastly"; | ||
|
||
/// Test that Fastly redirects to CloudFront | ||
/// | ||
/// The database dump cannot be served directly from Fastly, so it should redirect to CloudFront. | ||
pub struct Fastly<'a> { | ||
/// Configuration for this test | ||
config: &'a Config, | ||
} | ||
|
||
impl<'a> Fastly<'a> { | ||
/// Create a new instance of the test | ||
pub fn new(config: &'a Config) -> Self { | ||
Self { config } | ||
} | ||
|
||
/// Check if a response is a redirect | ||
fn is_redirect(&self, response: &Response) -> bool { | ||
response.status().is_redirection() | ||
} | ||
|
||
/// Check if a response redirects to the given URL | ||
fn redirects_to(&self, response: &Response, url: &str) -> bool { | ||
response | ||
.headers() | ||
.get("Location") | ||
.and_then(|header| header.to_str().ok()) | ||
.is_some_and(|location| location == url) | ||
} | ||
} | ||
|
||
#[async_trait] | ||
impl<'a> Test for Fastly<'a> { | ||
async fn run(&self) -> TestResult { | ||
let response = match Client::builder() | ||
// Don't follow the redirect, we want to check the redirect location | ||
.redirect(Policy::none()) | ||
.build() | ||
.expect("failed to build reqwest client") | ||
.head(format!("{}/db-dump.tar.gz", self.config.fastly_url())) | ||
.send() | ||
.await | ||
{ | ||
Ok(response) => response, | ||
Err(error) => { | ||
return TestResult::builder() | ||
.name(NAME) | ||
.success(false) | ||
.message(Some(error.to_string())) | ||
.build() | ||
} | ||
}; | ||
|
||
let expected_location = format!("{}/db-dump.tar.gz", self.config.cloudfront_url()); | ||
|
||
if self.is_redirect(&response) && self.redirects_to(&response, &expected_location) { | ||
TestResult::builder().name(NAME).success(true).build() | ||
} else { | ||
TestResult::builder() | ||
.name(NAME) | ||
.success(false) | ||
.message(Some(format!( | ||
"Expected a redirect to {}, got {}", | ||
expected_location, | ||
response.url().as_str() | ||
))) | ||
.build() | ||
} | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use crate::test_utils::*; | ||
|
||
use super::*; | ||
|
||
#[tokio::test] | ||
async fn succeeds_with_redirect() { | ||
let mut server = mockito::Server::new_async().await; | ||
|
||
let config = Config::builder() | ||
.cloudfront_url("https://cloudfront".into()) | ||
.fastly_url(server.url()) | ||
.build(); | ||
|
||
let mock = server | ||
.mock("HEAD", "/db-dump.tar.gz") | ||
.with_status(307) | ||
.with_header("Location", "https://cloudfront/db-dump.tar.gz") | ||
.create(); | ||
|
||
let result = Fastly::new(&config).run().await; | ||
|
||
// Assert that the mock was called | ||
mock.assert(); | ||
|
||
assert!(result.success()); | ||
} | ||
|
||
#[tokio::test] | ||
async fn fails_without_redirect() { | ||
let mut server = mockito::Server::new_async().await; | ||
|
||
let config = Config::builder() | ||
.cloudfront_url(server.url()) | ||
.fastly_url(server.url()) | ||
.build(); | ||
|
||
let mock = server | ||
.mock("HEAD", "/db-dump.tar.gz") | ||
.with_status(200) | ||
.create(); | ||
|
||
let result = Fastly::new(&config).run().await; | ||
|
||
// Assert that the mock was called | ||
mock.assert(); | ||
|
||
assert!(!result.success()); | ||
} | ||
|
||
#[test] | ||
fn trait_send() { | ||
assert_send::<Fastly>(); | ||
} | ||
|
||
#[test] | ||
fn trait_sync() { | ||
assert_sync::<Fastly>(); | ||
} | ||
|
||
#[test] | ||
fn trait_unpin() { | ||
assert_unpin::<Fastly>(); | ||
} | ||
} |
Oops, something went wrong.