From 0fe67e5b79cdc4e05d13062bf6b9dbe4f01bfcd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oddbj=C3=B8rn=20Gr=C3=B8dem?= <29732646+oddgrd@users.noreply.github.com> Date: Mon, 15 Jan 2024 10:54:05 +0100 Subject: [PATCH] tests(auth): simplify auth service tests with wiremock (#1514) * 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 --- Cargo.lock | 64 +++++++ auth/Cargo.toml | 1 + auth/tests/api/helpers.rs | 135 ++++---------- auth/tests/api/stripe/active_subscription.rs | 2 +- .../stripe/cancelledpro_checkout_session.rs | 2 +- .../cancelledpro_subscription_active.rs | 2 +- .../cancelledpro_subscription_cancelled.rs | 2 +- .../api/stripe/completed_checkout_session.rs | 2 +- .../api/stripe/incomplete_checkout_session.rs | 2 +- auth/tests/api/stripe/mod.rs | 34 ++-- .../overdue_payment_checkout_session.rs | 2 +- .../tests/api/stripe/past_due_subscription.rs | 2 +- auth/tests/api/users.rs | 165 +++++++----------- 13 files changed, 182 insertions(+), 233 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2761dc1719..3ac4574025 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -189,6 +189,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "assert_cmd" version = "2.0.12" @@ -1790,6 +1800,25 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +[[package]] +name = "deadpool" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "421fe0f90f2ab22016f32a9881be5134fdd71c65298917084b0c7477cbc3856e" +dependencies = [ + "async-trait", + "deadpool-runtime", + "num_cpus", + "retain_mut", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63dfa964fe2a66f3fde91fc70b267fe193d822c7e603e2a675a49a7f46ad3f49" + [[package]] name = "debugid" version = "0.8.0" @@ -2292,6 +2321,12 @@ version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + [[package]] name = "futures-util" version = "0.3.29" @@ -5190,6 +5225,12 @@ dependencies = [ "quick-error", ] +[[package]] +name = "retain_mut" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0" + [[package]] name = "retry-policies" version = "0.2.1" @@ -5825,6 +5866,7 @@ dependencies = [ "tracing", "tracing-opentelemetry", "tracing-subscriber", + "wiremock", ] [[package]] @@ -8690,6 +8732,28 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "wiremock" +version = "0.5.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13a3a53eaf34f390dd30d7b1b078287dd05df2aa2e21a589ccb80f5c7253c2e9" +dependencies = [ + "assert-json-diff", + "async-trait", + "base64 0.21.5", + "deadpool", + "futures", + "futures-timer", + "http-types", + "hyper", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "wit-parser" version = "0.11.3" diff --git a/auth/Cargo.toml b/auth/Cargo.toml index 9945479504..06dfa06b01 100644 --- a/auth/Cargo.toml +++ b/auth/Cargo.toml @@ -43,3 +43,4 @@ portpicker = { workspace = true } serde_json = { workspace = true } shuttle-common-tests = { workspace = true } tower = { workspace = true, features = ["util"] } +wiremock = "0.5" diff --git a/auth/tests/api/helpers.rs b/auth/tests/api/helpers.rs index e2c00e4a0a..b9ec305cd4 100644 --- a/auth/tests/api/helpers.rs +++ b/auth/tests/api/helpers.rs @@ -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}; @@ -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 = Lazy::new(DockerInstance::default); #[ctor::dtor] @@ -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") @@ -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, } } @@ -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>, -} - -impl MockedStripeServer { - async fn subscription_retrieve_handler( - Path(subscription_id): Path, - State(state): State, - ) -> axum::response::Response { - 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::>(); - 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::(response_body).unwrap()), ) - .unwrap(), - router, - } + .mount_as_scoped(&self.mock_server) + .await; + + self.get_user(account_name).await } } diff --git a/auth/tests/api/stripe/active_subscription.rs b/auth/tests/api/stripe/active_subscription.rs index 5f956fa2b0..7be21ff7c6 100644 --- a/auth/tests/api/stripe/active_subscription.rs +++ b/auth/tests/api/stripe/active_subscription.rs @@ -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, diff --git a/auth/tests/api/stripe/cancelledpro_checkout_session.rs b/auth/tests/api/stripe/cancelledpro_checkout_session.rs index 3192af25ac..b270c74e40 100644 --- a/auth/tests/api/stripe/cancelledpro_checkout_session.rs +++ b/auth/tests/api/stripe/cancelledpro_checkout_session.rs @@ -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, diff --git a/auth/tests/api/stripe/cancelledpro_subscription_active.rs b/auth/tests/api/stripe/cancelledpro_subscription_active.rs index 4491c5287d..a8a30c72a3 100644 --- a/auth/tests/api/stripe/cancelledpro_subscription_active.rs +++ b/auth/tests/api/stripe/cancelledpro_subscription_active.rs @@ -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, diff --git a/auth/tests/api/stripe/cancelledpro_subscription_cancelled.rs b/auth/tests/api/stripe/cancelledpro_subscription_cancelled.rs index fb18326768..0a284ab2cb 100644 --- a/auth/tests/api/stripe/cancelledpro_subscription_cancelled.rs +++ b/auth/tests/api/stripe/cancelledpro_subscription_cancelled.rs @@ -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, diff --git a/auth/tests/api/stripe/completed_checkout_session.rs b/auth/tests/api/stripe/completed_checkout_session.rs index 01e55bd6e0..5eef09bce6 100644 --- a/auth/tests/api/stripe/completed_checkout_session.rs +++ b/auth/tests/api/stripe/completed_checkout_session.rs @@ -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, diff --git a/auth/tests/api/stripe/incomplete_checkout_session.rs b/auth/tests/api/stripe/incomplete_checkout_session.rs index cf62c96076..8ca36a2d77 100644 --- a/auth/tests/api/stripe/incomplete_checkout_session.rs +++ b/auth/tests/api/stripe/incomplete_checkout_session.rs @@ -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, diff --git a/auth/tests/api/stripe/mod.rs b/auth/tests/api/stripe/mod.rs index eda0426f0a..2043296c58 100644 --- a/auth/tests/api/stripe/mod.rs +++ b/auth/tests/api/stripe/mod.rs @@ -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; @@ -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, +}; diff --git a/auth/tests/api/stripe/overdue_payment_checkout_session.rs b/auth/tests/api/stripe/overdue_payment_checkout_session.rs index 43f5f98760..f18178fbec 100644 --- a/auth/tests/api/stripe/overdue_payment_checkout_session.rs +++ b/auth/tests/api/stripe/overdue_payment_checkout_session.rs @@ -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, diff --git a/auth/tests/api/stripe/past_due_subscription.rs b/auth/tests/api/stripe/past_due_subscription.rs index b282095a43..920f0cf710 100644 --- a/auth/tests/api/stripe/past_due_subscription.rs +++ b/auth/tests/api/stripe/past_due_subscription.rs @@ -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, diff --git a/auth/tests/api/users.rs b/auth/tests/api/users.rs index d5062aca33..ae756cc6da 100644 --- a/auth/tests/api/users.rs +++ b/auth/tests/api/users.rs @@ -1,9 +1,12 @@ mod needs_docker { - use std::time::Duration; - use crate::{ helpers::{self, app}, - stripe::{MOCKED_CHECKOUT_SESSIONS, MOCKED_SUBSCRIPTIONS}, + stripe::{ + MOCKED_ACTIVE_SUBSCRIPTION, MOCKED_CANCELLEDPRO_CHECKOUT_SESSION, + MOCKED_CANCELLEDPRO_SUBSCRIPTION_ACTIVE, MOCKED_CANCELLEDPRO_SUBSCRIPTION_CANCELLED, + MOCKED_COMPLETED_CHECKOUT_SESSION, MOCKED_INCOMPLETE_CHECKOUT_SESSION, + MOCKED_OVERDUE_PAYMENT_CHECKOUT_SESSION, MOCKED_PAST_DUE_SUBSCRIPTION, + }, }; use axum::body::Body; use hyper::http::{header::AUTHORIZATION, Request, StatusCode}; @@ -114,10 +117,6 @@ mod needs_docker { async fn successful_upgrade_to_pro() { let app = app().await; - // Wait for the mocked Stripe server to start. - tokio::task::spawn(app.mocked_stripe_server.clone().serve()); - tokio::time::sleep(Duration::from_secs(1)).await; - // POST user first so one exists in the database. let response = app.post_user("test-user", "basic").await; @@ -126,47 +125,45 @@ mod needs_docker { let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); let expected_user: Value = serde_json::from_slice(&body).unwrap(); + // PUT /users/test-user/pro with a completed checkout session to upgrade a user to pro. let response = app - .put_user("test-user", "pro", MOCKED_CHECKOUT_SESSIONS[0]) + .put_user("test-user", "pro", MOCKED_COMPLETED_CHECKOUT_SESSION) .await; assert_eq!(response.status(), StatusCode::OK); - let response = app.get_user("test-user").await; + // Next we're going to fetch the user, which will trigger a sync of the users tier. It will + // fetch the subscription from stripe using the subscription ID from the previous checkout + // session. This should return an active subscription, meaning the users tier should remain + // pro. + let response = app + .get_user_with_mocked_stripe( + "sub_1Nw8xOD8t1tt0S3DtwAuOVp6", + MOCKED_ACTIVE_SUBSCRIPTION, + "test-user", + ) + .await; + assert_eq!(response.status(), StatusCode::OK); let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); let actual_user: Value = serde_json::from_slice(&body).unwrap(); assert_eq!( - expected_user.as_object().unwrap().get("name").unwrap(), - actual_user.as_object().unwrap().get("name").unwrap() + expected_user.get("name").unwrap(), + actual_user.get("name").unwrap() ); assert_eq!( - expected_user.as_object().unwrap().get("key").unwrap(), - actual_user.as_object().unwrap().get("key").unwrap() + expected_user.get("key").unwrap(), + actual_user.get("key").unwrap() ); - assert_eq!( - actual_user - .as_object() - .unwrap() - .get("account_tier") - .unwrap(), - "pro" - ); + assert_eq!(actual_user.get("account_tier").unwrap(), "pro"); - let mocked_subscription_obj: Value = serde_json::from_str(MOCKED_SUBSCRIPTIONS[0]).unwrap(); + let mocked_subscription_obj: Value = + serde_json::from_str(MOCKED_ACTIVE_SUBSCRIPTION).unwrap(); assert_eq!( - actual_user - .as_object() - .unwrap() - .get("subscription_id") - .unwrap(), - mocked_subscription_obj - .as_object() - .unwrap() - .get("id") - .unwrap() + actual_user.get("subscription_id").unwrap(), + mocked_subscription_obj.get("id").unwrap() ); } @@ -174,10 +171,6 @@ mod needs_docker { async fn unsuccessful_upgrade_to_pro() { let app = app().await; - // Wait for the mocked Stripe server to start. - tokio::task::spawn(app.mocked_stripe_server.clone().serve()); - tokio::time::sleep(Duration::from_secs(1)).await; - // POST user first so one exists in the database. let response = app.post_user("test-user", "basic").await; assert_eq!(response.status(), StatusCode::OK); @@ -188,7 +181,7 @@ mod needs_docker { // Test upgrading to pro with an incomplete checkout session object. let response = app - .put_user("test-user", "pro", MOCKED_CHECKOUT_SESSIONS[1]) + .put_user("test-user", "pro", MOCKED_INCOMPLETE_CHECKOUT_SESSION) .await; assert_eq!(response.status(), StatusCode::BAD_REQUEST); } @@ -197,32 +190,33 @@ mod needs_docker { async fn downgrade_in_case_subscription_due_payment() { let app = app().await; - // Wait for the mocked Stripe server to start. - tokio::task::spawn(app.mocked_stripe_server.clone().serve()); - tokio::time::sleep(Duration::from_secs(1)).await; - // POST user first so one exists in the database. let response = app.post_user("test-user", "basic").await; assert_eq!(response.status(), StatusCode::OK); // Test upgrading to pro with a checkout session that points to a due session. let response = app - .put_user("test-user", "pro", MOCKED_CHECKOUT_SESSIONS[2]) + .put_user("test-user", "pro", MOCKED_OVERDUE_PAYMENT_CHECKOUT_SESSION) .await; assert_eq!(response.status(), StatusCode::OK); - // This get_user request should check the subscription status and return an accurate tier. - let response = app.get_user("test-user").await; + // The auth service should call stripe to fetch the subscription with the sub id from the + // checkout session, and return a subscription that is pending payment. + let response = app + .get_user_with_mocked_stripe( + "sub_1NwObED8t1tt0S3Dq0IYOEsa", + MOCKED_PAST_DUE_SUBSCRIPTION, + "test-user", + ) + .await; + assert_eq!(response.status(), StatusCode::OK); + let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); let actual_user: Value = serde_json::from_slice(&body).unwrap(); assert_eq!( - actual_user - .as_object() - .unwrap() - .get("account_tier") - .unwrap(), + actual_user.get("account_tier").unwrap(), "pendingpaymentpro" ); } @@ -255,82 +249,51 @@ mod needs_docker { async fn downgrade_from_cancelledpro() { let app = app().await; - // Wait for the mocked Stripe server to start. - tokio::task::spawn(app.mocked_stripe_server.clone().serve()); - tokio::time::sleep(Duration::from_secs(1)).await; - // Create user with basic tier let response = app.post_user("test-user", "basic").await; assert_eq!(response.status(), StatusCode::OK); // Upgrade user to pro let response = app - .put_user("test-user", "pro", MOCKED_CHECKOUT_SESSIONS[3]) + .put_user("test-user", "pro", MOCKED_CANCELLEDPRO_CHECKOUT_SESSION) .await; assert_eq!(response.status(), StatusCode::OK); - // Cancel subscription + // Cancel subscription, this will be called by the console. let response = app.put_user("test-user", "cancelledpro", "").await; assert_eq!(response.status(), StatusCode::OK); - // Trigger status change to canceled. This call has a side effect because the user has a - // subscription that is handled in a specific way by the MockedStripeServer, which changes - // the subscription state to cancelled. - let response = app.get_user("test-user").await; - assert_eq!(response.status(), StatusCode::OK); - - let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); - let user: Value = serde_json::from_slice(&body).unwrap(); - assert_eq!( - user.as_object().unwrap().get("account_tier").unwrap(), - "cancelledpro" - ); + // Fetch the user to trigger a sync of the account tier to cancelled. The account should not + // be downgraded to basic right away, since when we cancel subscriptions we pass in the + // "cancel_at_period_end" end flag. + let response = app + .get_user_with_mocked_stripe( + "sub_123", + MOCKED_CANCELLEDPRO_SUBSCRIPTION_ACTIVE, + "test-user", + ) + .await; - // Check if user is downgraded to basic - let response = app.get_user("test-user").await; assert_eq!(response.status(), StatusCode::OK); let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); let user: Value = serde_json::from_slice(&body).unwrap(); + assert_eq!(user.get("account_tier").unwrap(), "cancelledpro"); - assert_eq!( - user.as_object().unwrap().get("account_tier").unwrap(), - "basic" - ); - } - - #[tokio::test] - async fn retain_cancelledpro_status() { - let app = app().await; - - // Wait for the mocked Stripe server to start. - tokio::task::spawn(app.mocked_stripe_server.clone().serve()); - tokio::time::sleep(Duration::from_secs(1)).await; - - // Create user with basic tier - let response = app.post_user("test-user", "basic").await; - assert_eq!(response.status(), StatusCode::OK); - - // Upgrade user to pro + // When called again at some later time, the subscription returned from stripe should be + // cancelled. let response = app - .put_user("test-user", "pro", MOCKED_CHECKOUT_SESSIONS[3]) + .get_user_with_mocked_stripe( + "sub_123", + MOCKED_CANCELLEDPRO_SUBSCRIPTION_CANCELLED, + "test-user", + ) .await; assert_eq!(response.status(), StatusCode::OK); - // Cancel subscription - let response = app.put_user("test-user", "cancelledpro", "").await; - assert_eq!(response.status(), StatusCode::OK); - - // Check if user has cancelledpro status - let response = app.get_user("test-user").await; - assert_eq!(response.status(), StatusCode::OK); - let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); let user: Value = serde_json::from_slice(&body).unwrap(); - assert_eq!( - user.as_object().unwrap().get("account_tier").unwrap(), - "cancelledpro" - ); + assert_eq!(user.get("account_tier").unwrap(), "basic"); } }