Skip to content

Commit 0abd9f1

Browse files
authored
add test for crates.io API (#231)
1 parent 67aee33 commit 0abd9f1

File tree

4 files changed

+269
-0
lines changed

4 files changed

+269
-0
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
//! Test for crates.io API health
2+
3+
use std::sync::Arc;
4+
5+
use async_trait::async_trait;
6+
7+
use crate::http_client::custom_http_client;
8+
use crate::test::{Test, TestResult};
9+
10+
use super::config::Config;
11+
12+
/// The name of the test
13+
const NAME: &str = "crates.io API";
14+
15+
/// Test that the crates.io API is accessible
16+
pub struct ApiHealth {
17+
/// Configuration for this test
18+
config: Arc<Config>,
19+
}
20+
21+
impl ApiHealth {
22+
/// Create a new instance of the test
23+
pub fn new(config: Arc<Config>) -> Self {
24+
Self { config }
25+
}
26+
}
27+
28+
#[async_trait]
29+
impl Test for ApiHealth {
30+
async fn run(&self) -> TestResult {
31+
let response = match custom_http_client()
32+
.build()
33+
.expect("failed to build reqwest client")
34+
.get(self.config.api_url())
35+
.send()
36+
.await
37+
{
38+
Ok(response) => response,
39+
Err(error) => {
40+
return TestResult::builder()
41+
.name(NAME)
42+
.success(false)
43+
.message(Some(error.to_string()))
44+
.build()
45+
}
46+
};
47+
48+
if response.status().is_success() {
49+
TestResult::builder().name(NAME).success(true).build()
50+
} else {
51+
TestResult::builder()
52+
.name(NAME)
53+
.success(false)
54+
.message(Some(format!(
55+
"Expected HTTP 200, got HTTP {}",
56+
response.status()
57+
)))
58+
.build()
59+
}
60+
}
61+
}
62+
63+
#[cfg(test)]
64+
mod tests {
65+
use mockito::ServerGuard;
66+
67+
use crate::test_utils::*;
68+
69+
use super::*;
70+
71+
pub async fn setup() -> (ServerGuard, Config) {
72+
let server = mockito::Server::new_async().await;
73+
74+
let config = Config::builder().api_url(server.url()).build();
75+
76+
(server, config)
77+
}
78+
79+
#[tokio::test]
80+
async fn succeeds_with_http_200_response() {
81+
let (mut server, config) = setup().await;
82+
83+
let mock = server.mock("GET", "/").with_status(200).create();
84+
85+
let result = ApiHealth::new(Arc::new(config)).run().await;
86+
87+
// Assert that the mock was called
88+
mock.assert();
89+
90+
assert!(result.success());
91+
}
92+
93+
#[tokio::test]
94+
async fn fails_with_other_http_responses() {
95+
let (mut server, config) = setup().await;
96+
97+
let mock = server.mock("GET", "/").with_status(500).create();
98+
99+
let result = ApiHealth::new(Arc::new(config)).run().await;
100+
101+
// Assert that the mock was called
102+
mock.assert();
103+
104+
assert!(!result.success());
105+
}
106+
107+
#[test]
108+
fn trait_send() {
109+
assert_send::<ApiHealth>();
110+
}
111+
112+
#[test]
113+
fn trait_sync() {
114+
assert_sync::<ApiHealth>();
115+
}
116+
117+
#[test]
118+
fn trait_unpin() {
119+
assert_unpin::<ApiHealth>();
120+
}
121+
}

src/crates/crates_api/config.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//! Configuration for crates.io API tests
2+
3+
use getset::Getters;
4+
#[cfg(test)]
5+
use typed_builder::TypedBuilder;
6+
7+
use crate::environment::Environment;
8+
9+
/// Configuration for crates.io API tests
10+
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Getters)]
11+
#[cfg_attr(test, derive(TypedBuilder))]
12+
pub struct Config {
13+
/// The URL for the crates.io API
14+
#[getset(get = "pub")]
15+
api_url: String,
16+
}
17+
18+
impl Config {
19+
/// Return the configuration for the given environment
20+
pub fn for_env(env: Environment) -> Self {
21+
// Use the summary endpoint as a simple health check
22+
const PATH: &str = "/api/v1/summary";
23+
match env {
24+
Environment::Staging => Self {
25+
api_url: format!("https://staging.crates.io{PATH}"),
26+
},
27+
Environment::Production => Self {
28+
api_url: format!("https://crates.io{PATH}"),
29+
},
30+
}
31+
}
32+
}
33+
34+
#[cfg(test)]
35+
mod tests {
36+
use crate::test_utils::*;
37+
38+
use super::*;
39+
40+
#[test]
41+
fn trait_send() {
42+
assert_send::<Config>();
43+
}
44+
45+
#[test]
46+
fn trait_sync() {
47+
assert_sync::<Config>();
48+
}
49+
50+
#[test]
51+
fn trait_unpin() {
52+
assert_unpin::<Config>();
53+
}
54+
}

src/crates/crates_api/mod.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
//! crates.io API tests
2+
3+
use std::fmt::{Display, Formatter};
4+
use std::sync::Arc;
5+
6+
use async_trait::async_trait;
7+
use tokio::task::JoinSet;
8+
9+
use crate::environment::Environment;
10+
use crate::test::{Test, TestGroup, TestGroupResult};
11+
12+
pub use self::api_health::ApiHealth;
13+
pub use self::config::Config;
14+
15+
mod api_health;
16+
mod config;
17+
18+
/// The name of the test group
19+
const GROUP_NAME: &str = "crates.io API";
20+
21+
/// Test crates.io API is accessible
22+
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
23+
pub struct CratesApi {
24+
/// Configuration for the test group
25+
config: Arc<Config>,
26+
}
27+
28+
impl CratesApi {
29+
/// Create a new instance of the test group
30+
pub fn new(env: Environment) -> Self {
31+
Self {
32+
config: Arc::new(Config::for_env(env)),
33+
}
34+
}
35+
}
36+
37+
impl Display for CratesApi {
38+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
39+
write!(f, "{GROUP_NAME}")
40+
}
41+
}
42+
43+
#[async_trait]
44+
impl TestGroup for CratesApi {
45+
async fn run(&self) -> TestGroupResult {
46+
let tests: Vec<Box<dyn Test>> = vec![Box::new(ApiHealth::new(self.config.clone()))];
47+
48+
let mut js = JoinSet::new();
49+
for test in tests {
50+
js.spawn(async move { test.run().await });
51+
}
52+
53+
let results = js.join_all().await;
54+
55+
TestGroupResult::builder()
56+
.name(GROUP_NAME)
57+
.results(results)
58+
.build()
59+
}
60+
}
61+
62+
#[cfg(test)]
63+
mod tests {
64+
use pretty_assertions::assert_eq;
65+
66+
use crate::test_utils::*;
67+
68+
use super::*;
69+
70+
#[test]
71+
fn trait_display() {
72+
let crates_api = CratesApi::new(Environment::Staging);
73+
74+
assert_eq!("crates.io API", crates_api.to_string());
75+
}
76+
77+
#[test]
78+
fn trait_send() {
79+
assert_send::<CratesApi>();
80+
}
81+
82+
#[test]
83+
fn trait_sync() {
84+
assert_sync::<CratesApi>();
85+
}
86+
87+
#[test]
88+
fn trait_unpin() {
89+
assert_unpin::<CratesApi>();
90+
}
91+
}

src/crates/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ use crate::test::{TestGroup, TestSuite, TestSuiteResult};
1010

1111
use self::crates_4891::Crates4891;
1212
use self::crates_6164::Crates6164;
13+
use self::crates_api::CratesApi;
1314
use self::crates_index::CratesIndex;
1415
use self::db_dump::DbDump;
1516

1617
mod crates_4891;
1718
mod crates_6164;
19+
mod crates_api;
1820
mod crates_index;
1921
mod db_dump;
2022
mod utils;
@@ -49,6 +51,7 @@ impl TestSuite for Crates {
4951
let groups: Vec<Box<dyn TestGroup>> = vec![
5052
Box::new(Crates4891::new(self.env)),
5153
Box::new(Crates6164::new(self.env)),
54+
Box::new(CratesApi::new(self.env)),
5255
Box::new(CratesIndex::new(self.env)),
5356
Box::new(DbDump::new(self.env)),
5457
];

0 commit comments

Comments
 (0)