#![allow(clippy::unit_arg)]
//! ## Integration Tests For [`BriteVerifyClient`](BriteVerifyClient)'s
//! ## Miscellaneous Internal Utility Methods
// Module Declarations
pub mod utils;
// Standard Library Imports
use std::{
collections::HashMap,
sync::{
atomic::{AtomicU8, Ordering},
Arc, Mutex,
},
};
// Third Part Imports
use anyhow::Result;
use http_types::{mime::JSON, StatusCode};
use once_cell::sync::Lazy;
use pretty_assertions::assert_str_eq;
use rstest::{fixture, rstest};
use wiremock::{
http::{Method as HttpMethod, Url},
Match, Mock, MockServer, Request, Respond, ResponseTemplate,
};
// Crate-Level Imports
use briteverify_rs::{errors::BriteVerifyClientError, BriteVerifyClient};
use utils::BriteVerifyRequest;
//
const AUTH_KEY_ERROR: &str =
r#"{"errors":{"user":"not authorized or over daily test limit for untrusted domains"}}"#;
const RATE_LIMIT_BODY: &str = "wouldn't that bring about chaos?";
static REQUEST_COUNTS: Lazy>>> =
Lazy::new(|| Arc::new(Mutex::new(HashMap::new())));
//
//
#[fixture]
/// An unregistered `Mock` that will respond to any request lacking
/// the proper `Authorization` header with the official response body
/// from the BriteVerify API's published Postman collection / documentation
fn mock_auth_error() -> Mock {
Mock::given(is_unauthorized_request).respond_with(unauthorized_response)
}
//
//
//
/// Check if the supplied request is not authorized
/// [[ref](https://docs.briteverify.com/#8fce7493-92a4-43f0-bd0d-bd2dfdb65bf5)]
pub fn is_unauthorized_request(request: &Request) -> bool {
!request.has_valid_api_key()
&& [HttpMethod::Get, HttpMethod::Post, HttpMethod::Delete].contains(&request.method)
}
//
//
/// Return an error response indicating that the supplied API key
/// was either invalid, expired, or otherwise not properly authorized
/// [[ref](https://docs.briteverify.com/#8fce7493-92a4-43f0-bd0d-bd2dfdb65bf5)]
pub fn unauthorized_response(_: &Request) -> ResponseTemplate {
ResponseTemplate::new(StatusCode::Unauthorized).set_body_raw(AUTH_KEY_ERROR, &JSON.to_string())
}
//
//
//
#[derive(Debug)]
struct StatefulRateLimit(pub Arc);
impl Match for StatefulRateLimit {
fn matches(&self, request: &Request) -> bool {
let url = &request.url;
let mut count_map = REQUEST_COUNTS.lock().unwrap();
let call_count = count_map
.entry(url.clone())
.and_modify(|count| *count += 1)
.or_insert(1)
.to_owned();
self.0.store(call_count, Ordering::SeqCst);
url.to_string().ends_with("/auto-retry")
}
}
impl Respond for StatefulRateLimit {
fn respond(&self, request: &Request) -> ResponseTemplate {
let call_count = self.0.load(Ordering::SeqCst);
if call_count < 2u8 {
ResponseTemplate::new(StatusCode::TooManyRequests).insert_header("retry-after", "1")
} else {
REQUEST_COUNTS
.lock()
.unwrap()
.insert(request.url.clone(), 0);
ResponseTemplate::new(StatusCode::Ok).set_body_raw(RATE_LIMIT_BODY, &JSON.to_string())
}
}
}
//
//
#[rstest]
#[test_log::test(tokio::test)]
/// Test that the [`BriteVerifyClient`](BriteVerifyClient)
/// behaves as expected when a request is met with an authorization
/// error response (per the official BriteVerify API Postman collection)
async fn errors_with_bad_api_keys(#[from(mock_auth_error)] mock: Mock) -> Result<()> {
let (client, server) =
utils::client_and_server(Some(r#"what's dwarven for "friend" again?"#), None).await;
#[allow(unused_variables)]
let guard = mock.mount_as_scoped(&server).await;
let url = format!("{}://{}/auth-check", "http", server.address());
let response = client.build_and_send(client.get(url)).await;
Ok(assert!(response
.expect_err("Client method was expected to return an error but did not")
.to_string()
.contains("Invalid or unauthorized BriteVerify API key")))
}
#[rstest]
#[test_log::test(tokio::test)]
/// Test that the [`BriteVerifyClient`](BriteVerifyClient)
/// behaves as expected when auto-retry is enabled and a
/// request cannot be cloned
/// ___
/// **NOTE:** This can't currently happen "in the real world", as no
/// client method will create a request with an unclonable body. This
/// test currently exists exclusively because there is a branch in the
/// client's `_build_and_send` method that handles the case.
/// ___
async fn errors_with_unclonable_requests() -> Result<()> {
// Create a `BriteVerifyClient` instance with auto-retry enabled
let client = BriteVerifyClient::builder()
.api_key("fear is the true enemy, the only enemy")
.retry_enabled(true)
.build()?;
// Per the reqwest documentation,`None` is only returned
// by `Request::try_clone` in the case that the request's
// body is unclonable.
// [[ref](https://docs.rs/reqwest/latest/reqwest/struct.RequestBuilder.html#method.try_clone)]
//
// This example is taken directly from the associated documentation
// [[ref](https://docs.rs/reqwest/latest/reqwest/struct.Body.html#method.wrap_stream)]
// as a way to create a request body that is unclonable, and
// will therefore take the branch in `_build_and_send` that
// handles that specific case.
let body_stream = futures_util::stream::iter(Vec::>::from([
Ok("flair"),
Ok("is"),
Ok("what"),
Ok("marks"),
Ok("the"),
Ok("difference"),
Ok("between"),
Ok("artistry"),
Ok("and"),
Ok("mere"),
Ok("competence"),
]));
let request_body = reqwest::Body::wrap_stream(body_stream);
let response = client
.build_and_send(client.post("https://example.com").body(request_body))
.await;
Ok(assert!(
response
.as_ref()
.is_err_and(|error| matches!(error, BriteVerifyClientError::UnclonableRequest)),
"Expected Err(BriteVerifyClientError), got: {:#?}",
response
))
}
#[rstest]
#[test_log::test(tokio::test)]
/// Test that the [`BriteVerifyClient`](BriteVerifyClient)
/// behaves as expected when auto-retry is enabled and the
/// BriteVerify API responds with a request with an error
/// due to rate limit exhaustion
async fn handles_rate_limit_responses() -> Result<()> {
let server = MockServer::start().await;
// Create a `BriteVerifyClient` instance with auto-retry enabled
let client = utils::client_for_server(&server, Some("computer, belay that order"), true);
let call_count = Arc::new(AtomicU8::from(0u8));
#[allow(unused_variables)]
let mock = Mock::given(StatefulRateLimit(Arc::clone(&call_count)))
.respond_with(StatefulRateLimit(Arc::clone(&call_count)))
.mount_as_scoped(&server)
.await;
let url = format!("{}://{}/auto-retry", "http", server.address()).parse::()?;
let response = client.build_and_send(client.get(url)).await;
assert!(
response.as_ref().is_ok(),
"Expected Ok(response), got: {:#?}",
response
);
let response = response.unwrap();
assert_eq!(
mock.received_requests().await.len() as u64,
call_count.load(Ordering::SeqCst) as u64,
);
assert_eq!(response.status(), reqwest::StatusCode::OK);
Ok(assert_str_eq!(
RATE_LIMIT_BODY,
response.text().await.unwrap_or("error".to_string())
))
}
//