Skip to content

Commit

Permalink
tests(auth): simplify auth service tests with wiremock (#1514)
Browse files Browse the repository at this point in the history
* tests(auth): simplify auth service tests with wiremock

* tests(auth): use stripe mock consts directly in tests

this makes the tests more readable, since you don't need to know which index each mock has
  • Loading branch information
oddgrd authored and jonaro00 committed Jan 24, 2024
1 parent 2ac440d commit 0fe67e5
Show file tree
Hide file tree
Showing 13 changed files with 182 additions and 233 deletions.
64 changes: 64 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions auth/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ portpicker = { workspace = true }
serde_json = { workspace = true }
shuttle-common-tests = { workspace = true }
tower = { workspace = true, features = ["util"] }
wiremock = "0.5"
135 changes: 35 additions & 100 deletions auth/tests/api/helpers.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
use crate::stripe::MOCKED_SUBSCRIPTIONS;
use axum::{body::Body, extract::Path, extract::State, response::Response, routing::get, Router};
use axum::{body::Body, response::Response, Router};
use http::{header::CONTENT_TYPE, StatusCode};
use hyper::{
http::{header::AUTHORIZATION, Request},
Server,
};
use hyper::http::{header::AUTHORIZATION, Request};
use once_cell::sync::Lazy;
use serde_json::Value;
use shuttle_auth::{pgpool_init, ApiBuilder};
Expand All @@ -14,14 +10,16 @@ use shuttle_common::{
};
use shuttle_common_tests::postgres::DockerInstance;
use sqlx::query;
use std::{
net::SocketAddr,
str::FromStr,
sync::{Arc, Mutex},
};
use tower::ServiceExt;
use wiremock::{
matchers::{bearer_token, method, path},
Mock, MockServer, ResponseTemplate,
};

/// Admin user API key.
pub(crate) const ADMIN_KEY: &str = "ndh9z58jttoes3qv";
/// Stripe test API key.
pub(crate) const STRIPE_TEST_KEY: &str = "sk_test_123";

static PG: Lazy<DockerInstance> = Lazy::new(DockerInstance::default);
#[ctor::dtor]
Expand All @@ -31,14 +29,15 @@ fn cleanup() {

pub(crate) struct TestApp {
pub router: Router,
pub mocked_stripe_server: MockedStripeServer,
pub mock_server: MockServer,
}

/// Initialize a router with an in-memory sqlite database for each test.
pub(crate) async fn app() -> TestApp {
let pg_pool = pgpool_init(PG.get_unique_uri().as_str()).await.unwrap();

let mocked_stripe_server = MockedStripeServer::default();
let mock_server = MockServer::start().await;

// Insert an admin user for the tests.
query("INSERT INTO users (account_name, key, account_tier) VALUES ($1, $2, $3)")
.bind("admin")
Expand All @@ -52,15 +51,15 @@ pub(crate) async fn app() -> TestApp {
.with_pg_pool(pg_pool)
.with_sessions()
.with_stripe_client(stripe::Client::from_url(
mocked_stripe_server.uri.to_string().as_str(),
"",
mock_server.uri().as_str(),
STRIPE_TEST_KEY,
))
.with_jwt_signing_private_key("LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1DNENBUUF3QlFZREsyVndCQ0lFSUR5V0ZFYzhKYm05NnA0ZGNLTEwvQWNvVUVsbUF0MVVKSTU4WTc4d1FpWk4KLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo=".to_string())
.into_router();

TestApp {
router,
mocked_stripe_server,
mock_server,
}
}

Expand Down Expand Up @@ -149,91 +148,27 @@ impl TestApp {

Claim::from_token(token, &public_key).unwrap()
}
}

#[derive(Clone)]
pub(crate) struct MockedStripeServer {
uri: http::Uri,
router: Router,
}

#[derive(Clone)]
pub(crate) struct RouterState {
subscription_cancel_side_effect_toggle: Arc<Mutex<bool>>,
}

impl MockedStripeServer {
async fn subscription_retrieve_handler(
Path(subscription_id): Path<String>,
State(state): State<RouterState>,
) -> axum::response::Response<String> {
let is_sub_cancelled = state
.subscription_cancel_side_effect_toggle
.lock()
.unwrap()
.to_owned();

if subscription_id == "sub_123" {
if is_sub_cancelled {
return Response::new(MOCKED_SUBSCRIPTIONS[3].to_string());
} else {
let mut toggle = state.subscription_cancel_side_effect_toggle.lock().unwrap();
*toggle = true;
return Response::new(MOCKED_SUBSCRIPTIONS[2].to_string());
}
}

let sessions = MOCKED_SUBSCRIPTIONS
.iter()
.filter(|sub| sub.contains(format!("\"id\": \"{}\"", subscription_id).as_str()))
.map(|sub| serde_json::from_str(sub).unwrap())
.collect::<Vec<Value>>();
if sessions.len() == 1 {
return Response::new(sessions[0].to_string());
}

Response::builder()
.status(http::StatusCode::NOT_FOUND)
.body("subscription id not found".to_string())
.unwrap()
}

pub(crate) async fn serve(self) {
let address = &SocketAddr::from_str(
format!("{}:{}", self.uri.host().unwrap(), self.uri.port().unwrap()).as_str(),
)
.unwrap();
println!("serving on: {}", address);
Server::bind(address)
.serve(self.router.into_make_service())
.await
.unwrap_or_else(|_| panic!("Failed to bind to address: {}", self.uri));
}
}

impl Default for MockedStripeServer {
fn default() -> MockedStripeServer {
let router_state = RouterState {
subscription_cancel_side_effect_toggle: Arc::new(Mutex::new(false)),
};

let router = Router::new()
.route(
"/v1/subscriptions/:subscription_id",
get(MockedStripeServer::subscription_retrieve_handler),
)
.with_state(router_state);

MockedStripeServer {
uri: http::Uri::from_str(
format!(
"http://127.0.0.1:{}",
portpicker::pick_unused_port().unwrap()
)
.as_str(),
/// A test util to get a user with a subscription, mocking the response from Stripe. A key part
/// of this util is `mount_as_scoped`, since getting a user with a subscription can be done
/// several times in a test, if they're not scoped the first mock would always apply.
pub async fn get_user_with_mocked_stripe(
&self,
subscription_id: &str,
response_body: &str,
account_name: &str,
) -> Response {
// This mock will apply until the end of this function scope.
let _mock_guard = Mock::given(method("GET"))
.and(bearer_token(STRIPE_TEST_KEY))
.and(path(format!("/v1/subscriptions/{subscription_id}")))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(serde_json::from_str::<Value>(response_body).unwrap()),
)
.unwrap(),
router,
}
.mount_as_scoped(&self.mock_server)
.await;

self.get_user(account_name).await
}
}
2 changes: 1 addition & 1 deletion auth/tests/api/stripe/active_subscription.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pub(crate) const MOCKED_ACTIVE_SUBSCRIPTION: &str = r#"{
pub const MOCKED_ACTIVE_SUBSCRIPTION: &str = r#"{
"id": "sub_1Nw8xOD8t1tt0S3DtwAuOVp6",
"object": "subscription",
"application": null,
Expand Down
2 changes: 1 addition & 1 deletion auth/tests/api/stripe/cancelledpro_checkout_session.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pub(crate) const MOCKED_CANCELLEDPRO_CHECKOUT_SESSION: &str = r#"{
pub const MOCKED_CANCELLEDPRO_CHECKOUT_SESSION: &str = r#"{
"id": "cs_test",
"object": "checkout.session",
"after_expiration": null,
Expand Down
2 changes: 1 addition & 1 deletion auth/tests/api/stripe/cancelledpro_subscription_active.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pub(crate) const MOCKED_CANCELLEDPRO_SUBSCRIPTION_ACTIVE: &str = r#"{
pub const MOCKED_CANCELLEDPRO_SUBSCRIPTION_ACTIVE: &str = r#"{
"id": "sub_123",
"object": "subscription",
"application": null,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pub(crate) const MOCKED_CANCELLEDPRO_SUBSCRIPTION_CANCELLED: &str = r#"{
pub const MOCKED_CANCELLEDPRO_SUBSCRIPTION_CANCELLED: &str = r#"{
"id": "sub_123",
"object": "subscription",
"application": null,
Expand Down
2 changes: 1 addition & 1 deletion auth/tests/api/stripe/completed_checkout_session.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pub(crate) const MOCKED_COMPLETED_CHECKOUT_SESSION: &str = r#"{
pub const MOCKED_COMPLETED_CHECKOUT_SESSION: &str = r#"{
"id": "cs_test_a1nmf3TXSDqYScpNLEroolP1ugCtk8Rx7kivUjYHLUdmjyJoociglcbN8q",
"object": "checkout.session",
"after_expiration": null,
Expand Down
2 changes: 1 addition & 1 deletion auth/tests/api/stripe/incomplete_checkout_session.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pub(crate) const MOCKED_INCOMPLETE_CHECKOUT_SESSION: &str = r#"{
pub const MOCKED_INCOMPLETE_CHECKOUT_SESSION: &str = r#"{
"id": "cs_test_a11rHy7qRTwFZuj4lBHso3Frq7CMZheZYcYqNXEFBV4oddxXFLx7bT911p",
"object": "checkout.session",
"after_expiration": null,
Expand Down
34 changes: 10 additions & 24 deletions auth/tests/api/stripe/mod.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
use self::{
active_subscription::MOCKED_ACTIVE_SUBSCRIPTION,
cancelledpro_checkout_session::MOCKED_CANCELLEDPRO_CHECKOUT_SESSION,
cancelledpro_subscription_active::MOCKED_CANCELLEDPRO_SUBSCRIPTION_ACTIVE,
cancelledpro_subscription_cancelled::MOCKED_CANCELLEDPRO_SUBSCRIPTION_CANCELLED,
completed_checkout_session::MOCKED_COMPLETED_CHECKOUT_SESSION,
incomplete_checkout_session::MOCKED_INCOMPLETE_CHECKOUT_SESSION,
overdue_payment_checkout_session::MOCKED_OVERDUE_PAYMENT_CHECKOUT_SESSION,
past_due_subscription::MOCKED_PAST_DUE_SUBSCRIPTION,
};

mod active_subscription;
mod cancelledpro_checkout_session;
mod cancelledpro_subscription_active;
Expand All @@ -18,16 +7,13 @@ mod incomplete_checkout_session;
mod overdue_payment_checkout_session;
mod past_due_subscription;

pub(crate) const MOCKED_SUBSCRIPTIONS: &[&str] = &[
MOCKED_ACTIVE_SUBSCRIPTION,
MOCKED_PAST_DUE_SUBSCRIPTION,
MOCKED_CANCELLEDPRO_SUBSCRIPTION_ACTIVE,
MOCKED_CANCELLEDPRO_SUBSCRIPTION_CANCELLED,
];

pub(crate) const MOCKED_CHECKOUT_SESSIONS: &[&str] = &[
MOCKED_COMPLETED_CHECKOUT_SESSION,
MOCKED_INCOMPLETE_CHECKOUT_SESSION,
MOCKED_OVERDUE_PAYMENT_CHECKOUT_SESSION,
MOCKED_CANCELLEDPRO_CHECKOUT_SESSION,
];
pub use {
active_subscription::MOCKED_ACTIVE_SUBSCRIPTION,
cancelledpro_checkout_session::MOCKED_CANCELLEDPRO_CHECKOUT_SESSION,
cancelledpro_subscription_active::MOCKED_CANCELLEDPRO_SUBSCRIPTION_ACTIVE,
cancelledpro_subscription_cancelled::MOCKED_CANCELLEDPRO_SUBSCRIPTION_CANCELLED,
completed_checkout_session::MOCKED_COMPLETED_CHECKOUT_SESSION,
incomplete_checkout_session::MOCKED_INCOMPLETE_CHECKOUT_SESSION,
overdue_payment_checkout_session::MOCKED_OVERDUE_PAYMENT_CHECKOUT_SESSION,
past_due_subscription::MOCKED_PAST_DUE_SUBSCRIPTION,
};
2 changes: 1 addition & 1 deletion auth/tests/api/stripe/overdue_payment_checkout_session.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// This is a synthetic checkout session. It is used to simplify the code path for downgrading to `PendingPaymentPro` tier
// when user payment is overdue.

pub(crate) const MOCKED_OVERDUE_PAYMENT_CHECKOUT_SESSION: &str = r#"{
pub const MOCKED_OVERDUE_PAYMENT_CHECKOUT_SESSION: &str = r#"{
"id": "cs_test_a11rHy7qRTwFZuj4lBHso3Frq7CMZheZYcYqNXEFBV4oddxXFLx7bT911p",
"object": "checkout.session",
"after_expiration": null,
Expand Down
2 changes: 1 addition & 1 deletion auth/tests/api/stripe/past_due_subscription.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pub(crate) const MOCKED_PAST_DUE_SUBSCRIPTION: &str = r#"{
pub const MOCKED_PAST_DUE_SUBSCRIPTION: &str = r#"{
"id": "sub_1NwObED8t1tt0S3Dq0IYOEsa",
"object": "subscription",
"application": null,
Expand Down
Loading

0 comments on commit 0fe67e5

Please sign in to comment.