Skip to content

Commit

Permalink
Create smoke tests for win.rustup.rs
Browse files Browse the repository at this point in the history
The distribution win.rustup.rs provides convenient access to the latest
Rustup installers for Windows. The distribution has two paths, one for
each architecture that is currently supported. The smoke tests send a
`HEAD` request to both and check that the response sets the correct
headers to indicate the payload in its body.

Because the Rustup installer changes with every release, we are not
checking the `Content-Length` of the response or the installer's SHA
checksum.
  • Loading branch information
jdno committed Jun 4, 2024
1 parent c65ce8c commit 4487dd3
Show file tree
Hide file tree
Showing 5 changed files with 484 additions and 1 deletion.
7 changes: 6 additions & 1 deletion src/rustup/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ use async_trait::async_trait;

use crate::environment::Environment;
use crate::rustup::rustup_sh::RustupSh;
use crate::rustup::win_rustup_rs::WinRustupRs;
use crate::test::{TestGroup, TestSuite, TestSuiteResult};

mod rustup_sh;
mod win_rustup_rs;

/// Smoke tests for rustup
///
Expand Down Expand Up @@ -36,7 +38,10 @@ impl Display for Rustup {
#[async_trait]
impl TestSuite for Rustup {
async fn run(&self) -> TestSuiteResult {
let groups: Vec<Box<dyn TestGroup>> = vec![Box::new(RustupSh::new(self.env))];
let groups: Vec<Box<dyn TestGroup>> = vec![
Box::new(RustupSh::new(self.env)),
Box::new(WinRustupRs::new(self.env)),
];

let mut results = Vec::with_capacity(groups.len());
for group in &groups {
Expand Down
54 changes: 54 additions & 0 deletions src/rustup/win_rustup_rs/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//! Configuration to test `win.rustup.rs`
use getset::Getters;
#[cfg(test)]
use typed_builder::TypedBuilder;

use crate::environment::Environment;

/// Configuration to test `win.rustup.rs`
///
/// `win.rustup.rs` is only served by CloudFront, thus only the CloudFront URL is needed.
#[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,
}

impl Config {
/// Return the configuration for the given environment
pub fn for_env(env: Environment) -> Self {
match env {
Environment::Staging => Self {
cloudfront_url: "https://dev-win.rustup.rs".into(),
},
Environment::Production => Self {
cloudfront_url: "https://win.rustup.rs".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>();
}
}
134 changes: 134 additions & 0 deletions src/rustup/win_rustup_rs/i686.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
//! Test `win.rustup.rs/i686`
use async_trait::async_trait;

use crate::rustup::win_rustup_rs::request_installer_and_expect_attachment;
use crate::test::{Test, TestResult};

use super::config::Config;

/// The name of the test
const NAME: &str = "i686";

/// Test that `win.rustup.rs/i686` serves the Rustup installer
///
/// This test requests the installer from `win.rustup.rs/i686` and expects the response to contain
/// the correct file as an attachment.
pub struct I686<'a> {
/// Configuration for this test
config: &'a Config,
}

impl<'a> I686<'a> {
/// Create a new instance of the test
pub fn new(config: &'a Config) -> Self {
Self { config }
}
}

#[async_trait]
impl<'a> Test for I686<'a> {
async fn run(&self) -> TestResult {
request_installer_and_expect_attachment(
NAME,
&format!("{}/i686", self.config.cloudfront_url()),
)
.await
}
}

#[cfg(test)]
mod tests {
use crate::test_utils::*;

use super::*;

#[tokio::test]
async fn succeeds_with_http_200_and_attachment() {
let mut server = mockito::Server::new_async().await;

let config = Config::builder().cloudfront_url(server.url()).build();

let mock = server
.mock("HEAD", "/i686")
.with_status(200)
.with_header("Content-Type", "application/x-msdownload")
.with_header(
"Content-Disposition",
r#"attachment; filename="rustup-init.exe""#,
)
.create();

let result = I686::new(&config).run().await;

// Assert that the mock was called
mock.assert();

assert_eq!(&None, result.message());
assert!(result.success());
}

#[tokio::test]
async fn fails_without_content_type() {
let mut server = mockito::Server::new_async().await;

let config = Config::builder().cloudfront_url(server.url()).build();

let mock = server
.mock("HEAD", "/i686")
.with_status(200)
.with_header(
"Content-Disposition",
r#"attachment; filename="rustup-init.exe""#,
)
.create();

let result = I686::new(&config).run().await;

// Assert that the mock was called
mock.assert();

let message = result.message().as_ref().unwrap();

assert!(message.contains("Content-Type"));
assert!(!result.success());
}

#[tokio::test]
async fn fails_without_content_disposition() {
let mut server = mockito::Server::new_async().await;

let config = Config::builder().cloudfront_url(server.url()).build();

let mock = server
.mock("HEAD", "/i686")
.with_status(200)
.with_header("Content-Type", "application/x-msdownload")
.create();

let result = I686::new(&config).run().await;

// Assert that the mock was called
mock.assert();

let message = result.message().as_ref().unwrap();

assert!(message.contains("Content-Disposition"));
assert!(!result.success());
}

#[test]
fn trait_send() {
assert_send::<I686>();
}

#[test]
fn trait_sync() {
assert_sync::<I686>();
}

#[test]
fn trait_unpin() {
assert_unpin::<I686>();
}
}
156 changes: 156 additions & 0 deletions src/rustup/win_rustup_rs/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
//! Serve Rustup for Windows from short URLs
//!
//! This module tests the three artifacts that can be downloaded from `win.rustup.rs`.
use std::fmt::{Display, Formatter};

use async_trait::async_trait;
use reqwest::Client;

use crate::environment::Environment;
use crate::test::{Test, TestGroup, TestGroupResult, TestResult};

pub use self::config::Config;
pub use self::i686::I686;
pub use self::x86_64::X86_64;

mod config;
mod i686;
mod x86_64;

/// The name of the test group
const NAME: &str = "win.rustup.rs";

/// Serve Rustup for Windows from short URLs
///
/// This test group tests the three artifacts that can be downloaded from `win.rustup.rs`. Each path
/// on the domain represents a specific architecture and serves the installer as an attachment.
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
pub struct WinRustupRs {
/// Configuration for the test group
config: Config,
}

impl WinRustupRs {
/// Create a new instance of the test group
pub fn new(env: Environment) -> Self {
Self {
config: Config::for_env(env),
}
}
}

impl Display for WinRustupRs {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", NAME)
}
}

#[async_trait]
impl TestGroup for WinRustupRs {
async fn run(&self) -> TestGroupResult {
let tests: Vec<Box<dyn Test>> = vec![
Box::new(I686::new(&self.config)),
Box::new(X86_64::new(&self.config)),
];

let mut results = Vec::new();
for test in tests {
results.push(test.run().await);
}

TestGroupResult::builder()
.name(NAME)
.results(results)
.build()
}
}

/// Request an artifact from `win.rustup.rs` and expect the correct response
///
/// This function requests the given path and expects the response to contain the correct file as an
/// attachment.
async fn request_installer_and_expect_attachment(name: &'static str, url: &str) -> TestResult {
let test_result = TestResult::builder().name(name).success(false);

let response = match Client::builder()
.build()
.expect("failed to build reqwest client")
.head(url)
.send()
.await
{
Ok(response) => response,
Err(error) => {
return test_result.message(Some(error.to_string())).build();
}
};

if response.status() != 200 {
return test_result
.message(Some(format!(
"Expected HTTP 200, got HTTP {}",
response.status()
)))
.build();
}

if !response
.headers()
.get("Content-Type")
.and_then(|header| header.to_str().ok())
.is_some_and(|header| header == "application/x-msdownload")
{
return test_result
.message(Some(
"Expected the Content-Type header to be set to 'application/x-msdownload'".into(),
))
.build();
}

if !response
.headers()
.get("Content-Disposition")
.and_then(|header| header.to_str().ok())
.is_some_and(|header| header.contains(r#"attachment; filename="rustup-init.exe""#))
{
return test_result
.message(Some(
"Expected the Content-Disposition header to indicate an attachment".into(),
))
.build();
}

TestResult::builder().name(name).success(true).build()
}

#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;

use crate::test_utils::*;

use super::*;

#[test]
fn trait_display() {
let rustup_sh = WinRustupRs::new(Environment::Staging);

assert_eq!("win.rustup.rs", rustup_sh.to_string());
}

#[test]
fn trait_send() {
assert_send::<WinRustupRs>();
}

#[test]
fn trait_sync() {
assert_sync::<WinRustupRs>();
}

#[test]
fn trait_unpin() {
assert_unpin::<WinRustupRs>();
}
}
Loading

0 comments on commit 4487dd3

Please sign in to comment.