botserver/src/billing/testing.rs
Rodrigo Rodriguez (Pragmatismo) b674d85583 Fix SafeCommand to allow shell scripts with redirects and command chaining
- Add shell_script_arg() method for bash/sh/cmd -c scripts
- Allow > < redirects in shell scripts (blocked in regular args)
- Allow && || command chaining in shell scripts
- Update safe_sh_command functions to use shell_script_arg
- Update run_commands, start, and LLM server commands
- Block dangerous patterns: backticks, path traversal
- Fix struct field mismatches and type errors
2026-01-08 23:50:38 -03:00

907 lines
29 KiB
Rust

use chrono::{Duration, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MockStripeCustomer {
pub id: String,
pub email: String,
pub name: Option<String>,
pub metadata: HashMap<String, String>,
pub created: i64,
pub default_source: Option<String>,
pub invoice_settings: MockInvoiceSettings,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MockInvoiceSettings {
pub default_payment_method: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MockStripeSubscription {
pub id: String,
pub customer: String,
pub status: SubscriptionStatus,
pub current_period_start: i64,
pub current_period_end: i64,
pub items: MockSubscriptionItems,
pub metadata: HashMap<String, String>,
pub cancel_at_period_end: bool,
pub canceled_at: Option<i64>,
pub trial_start: Option<i64>,
pub trial_end: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum SubscriptionStatus {
Active,
PastDue,
Unpaid,
Canceled,
Incomplete,
IncompleteExpired,
Trialing,
Paused,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MockSubscriptionItems {
pub data: Vec<MockSubscriptionItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MockSubscriptionItem {
pub id: String,
pub price: MockPrice,
pub quantity: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MockPrice {
pub id: String,
pub product: String,
pub unit_amount: i64,
pub currency: String,
pub recurring: Option<MockRecurring>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MockRecurring {
pub interval: String,
pub interval_count: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MockStripeInvoice {
pub id: String,
pub customer: String,
pub subscription: Option<String>,
pub status: InvoiceStatus,
pub amount_due: i64,
pub amount_paid: i64,
pub amount_remaining: i64,
pub currency: String,
pub created: i64,
pub due_date: Option<i64>,
pub paid: bool,
pub lines: MockInvoiceLines,
pub hosted_invoice_url: Option<String>,
pub invoice_pdf: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum InvoiceStatus {
Draft,
Open,
Paid,
Uncollectible,
Void,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MockInvoiceLines {
pub data: Vec<MockInvoiceLine>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MockInvoiceLine {
pub id: String,
pub amount: i64,
pub currency: String,
pub description: Option<String>,
pub quantity: u32,
pub price: MockPrice,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MockStripePaymentIntent {
pub id: String,
pub amount: i64,
pub currency: String,
pub status: PaymentIntentStatus,
pub customer: Option<String>,
pub payment_method: Option<String>,
pub client_secret: String,
pub created: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PaymentIntentStatus {
RequiresPaymentMethod,
RequiresConfirmation,
RequiresAction,
Processing,
RequiresCapture,
Canceled,
Succeeded,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MockStripePaymentMethod {
pub id: String,
pub customer: Option<String>,
pub payment_type: String,
pub card: Option<MockCard>,
pub created: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MockCard {
pub brand: String,
pub last4: String,
pub exp_month: u32,
pub exp_year: u32,
pub funding: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MockStripeEvent {
pub id: String,
pub event_type: String,
pub created: i64,
pub data: MockEventData,
pub livemode: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MockEventData {
pub object: serde_json::Value,
}
pub struct MockStripeClient {
customers: Arc<RwLock<HashMap<String, MockStripeCustomer>>>,
subscriptions: Arc<RwLock<HashMap<String, MockStripeSubscription>>>,
invoices: Arc<RwLock<HashMap<String, MockStripeInvoice>>>,
payment_intents: Arc<RwLock<HashMap<String, MockStripePaymentIntent>>>,
payment_methods: Arc<RwLock<HashMap<String, MockStripePaymentMethod>>>,
events: Arc<RwLock<Vec<MockStripeEvent>>>,
prices: Arc<RwLock<HashMap<String, MockPrice>>>,
should_fail: Arc<RwLock<bool>>,
failure_code: Arc<RwLock<Option<String>>>,
}
impl MockStripeClient {
pub fn new() -> Self {
let client = Self {
customers: Arc::new(RwLock::new(HashMap::new())),
subscriptions: Arc::new(RwLock::new(HashMap::new())),
invoices: Arc::new(RwLock::new(HashMap::new())),
payment_intents: Arc::new(RwLock::new(HashMap::new())),
payment_methods: Arc::new(RwLock::new(HashMap::new())),
events: Arc::new(RwLock::new(Vec::new())),
prices: Arc::new(RwLock::new(HashMap::new())),
should_fail: Arc::new(RwLock::new(false)),
failure_code: Arc::new(RwLock::new(None)),
};
tokio::spawn({
let prices = client.prices.clone();
async move {
let mut p = prices.write().await;
p.insert(
"price_free".to_string(),
MockPrice {
id: "price_free".to_string(),
product: "prod_free".to_string(),
unit_amount: 0,
currency: "usd".to_string(),
recurring: Some(MockRecurring {
interval: "month".to_string(),
interval_count: 1,
}),
},
);
p.insert(
"price_starter".to_string(),
MockPrice {
id: "price_starter".to_string(),
product: "prod_starter".to_string(),
unit_amount: 2900,
currency: "usd".to_string(),
recurring: Some(MockRecurring {
interval: "month".to_string(),
interval_count: 1,
}),
},
);
p.insert(
"price_pro".to_string(),
MockPrice {
id: "price_pro".to_string(),
product: "prod_pro".to_string(),
unit_amount: 4900,
currency: "usd".to_string(),
recurring: Some(MockRecurring {
interval: "month".to_string(),
interval_count: 1,
}),
},
);
p.insert(
"price_enterprise".to_string(),
MockPrice {
id: "price_enterprise".to_string(),
product: "prod_enterprise".to_string(),
unit_amount: 19900,
currency: "usd".to_string(),
recurring: Some(MockRecurring {
interval: "month".to_string(),
interval_count: 1,
}),
},
);
}
});
client
}
pub async fn set_should_fail(&self, should_fail: bool, code: Option<String>) {
let mut fail = self.should_fail.write().await;
*fail = should_fail;
let mut failure_code = self.failure_code.write().await;
*failure_code = code;
}
async fn check_failure(&self) -> Result<(), MockStripeError> {
let should_fail = *self.should_fail.read().await;
if should_fail {
let code = self.failure_code.read().await.clone();
return Err(MockStripeError::ApiError(
code.unwrap_or_else(|| "card_declined".to_string()),
));
}
Ok(())
}
pub async fn create_customer(
&self,
email: &str,
name: Option<&str>,
metadata: HashMap<String, String>,
) -> Result<MockStripeCustomer, MockStripeError> {
self.check_failure().await?;
let customer = MockStripeCustomer {
id: format!("cus_{}", generate_stripe_id()),
email: email.to_string(),
name: name.map(|s| s.to_string()),
metadata,
created: Utc::now().timestamp(),
default_source: None,
invoice_settings: MockInvoiceSettings {
default_payment_method: None,
},
};
let mut customers = self.customers.write().await;
customers.insert(customer.id.clone(), customer.clone());
Ok(customer)
}
pub async fn get_customer(&self, customer_id: &str) -> Result<MockStripeCustomer, MockStripeError> {
self.check_failure().await?;
let customers = self.customers.read().await;
customers
.get(customer_id)
.cloned()
.ok_or_else(|| MockStripeError::NotFound(format!("Customer {customer_id} not found")))
}
pub async fn update_customer(
&self,
customer_id: &str,
email: Option<&str>,
name: Option<&str>,
metadata: Option<HashMap<String, String>>,
) -> Result<MockStripeCustomer, MockStripeError> {
self.check_failure().await?;
let mut customers = self.customers.write().await;
let customer = customers
.get_mut(customer_id)
.ok_or_else(|| MockStripeError::NotFound(format!("Customer {customer_id} not found")))?;
if let Some(e) = email {
customer.email = e.to_string();
}
if let Some(n) = name {
customer.name = Some(n.to_string());
}
if let Some(m) = metadata {
customer.metadata = m;
}
Ok(customer.clone())
}
pub async fn delete_customer(&self, customer_id: &str) -> Result<(), MockStripeError> {
self.check_failure().await?;
let mut customers = self.customers.write().await;
customers
.remove(customer_id)
.ok_or_else(|| MockStripeError::NotFound(format!("Customer {customer_id} not found")))?;
Ok(())
}
pub async fn create_subscription(
&self,
customer_id: &str,
price_id: &str,
metadata: HashMap<String, String>,
trial_days: Option<u32>,
) -> Result<MockStripeSubscription, MockStripeError> {
self.check_failure().await?;
let customers = self.customers.read().await;
if !customers.contains_key(customer_id) {
return Err(MockStripeError::NotFound(format!(
"Customer {customer_id} not found"
)));
}
drop(customers);
let prices = self.prices.read().await;
let price = prices
.get(price_id)
.cloned()
.ok_or_else(|| MockStripeError::NotFound(format!("Price {price_id} not found")))?;
drop(prices);
let now = Utc::now();
let (trial_start, trial_end, status) = if let Some(days) = trial_days {
let ts = now.timestamp();
let te = (now + Duration::days(days as i64)).timestamp();
(Some(ts), Some(te), SubscriptionStatus::Trialing)
} else {
(None, None, SubscriptionStatus::Active)
};
let period_end = now + Duration::days(30);
let subscription = MockStripeSubscription {
id: format!("sub_{}", generate_stripe_id()),
customer: customer_id.to_string(),
status,
current_period_start: now.timestamp(),
current_period_end: period_end.timestamp(),
items: MockSubscriptionItems {
data: vec![MockSubscriptionItem {
id: format!("si_{}", generate_stripe_id()),
price,
quantity: 1,
}],
},
metadata,
cancel_at_period_end: false,
canceled_at: None,
trial_start,
trial_end,
};
let mut subscriptions = self.subscriptions.write().await;
subscriptions.insert(subscription.id.clone(), subscription.clone());
self.emit_event("customer.subscription.created", &subscription).await;
Ok(subscription)
}
pub async fn get_subscription(
&self,
subscription_id: &str,
) -> Result<MockStripeSubscription, MockStripeError> {
self.check_failure().await?;
let subscriptions = self.subscriptions.read().await;
subscriptions
.get(subscription_id)
.cloned()
.ok_or_else(|| MockStripeError::NotFound(format!("Subscription {subscription_id} not found")))
}
pub async fn update_subscription(
&self,
subscription_id: &str,
price_id: Option<&str>,
cancel_at_period_end: Option<bool>,
) -> Result<MockStripeSubscription, MockStripeError> {
self.check_failure().await?;
let mut subscriptions = self.subscriptions.write().await;
let subscription = subscriptions.get_mut(subscription_id).ok_or_else(|| {
MockStripeError::NotFound(format!("Subscription {subscription_id} not found"))
})?;
if let Some(pid) = price_id {
let prices = self.prices.read().await;
let price = prices
.get(pid)
.cloned()
.ok_or_else(|| MockStripeError::NotFound(format!("Price {pid} not found")))?;
subscription.items.data[0].price = price;
}
if let Some(cancel) = cancel_at_period_end {
subscription.cancel_at_period_end = cancel;
}
let result = subscription.clone();
drop(subscriptions);
self.emit_event("customer.subscription.updated", &result).await;
Ok(result)
}
pub async fn cancel_subscription(
&self,
subscription_id: &str,
immediately: bool,
) -> Result<MockStripeSubscription, MockStripeError> {
self.check_failure().await?;
let mut subscriptions = self.subscriptions.write().await;
let subscription = subscriptions.get_mut(subscription_id).ok_or_else(|| {
MockStripeError::NotFound(format!("Subscription {subscription_id} not found"))
})?;
if immediately {
subscription.status = SubscriptionStatus::Canceled;
subscription.canceled_at = Some(Utc::now().timestamp());
} else {
subscription.cancel_at_period_end = true;
}
let result = subscription.clone();
drop(subscriptions);
self.emit_event("customer.subscription.deleted", &result).await;
Ok(result)
}
pub async fn create_invoice(
&self,
customer_id: &str,
subscription_id: Option<&str>,
amount: i64,
) -> Result<MockStripeInvoice, MockStripeError> {
self.check_failure().await?;
let customers = self.customers.read().await;
if !customers.contains_key(customer_id) {
return Err(MockStripeError::NotFound(format!(
"Customer {customer_id} not found"
)));
}
drop(customers);
let invoice_id = format!("in_{}", generate_stripe_id());
let invoice = MockStripeInvoice {
id: invoice_id.clone(),
customer: customer_id.to_string(),
subscription: subscription_id.map(|s| s.to_string()),
status: InvoiceStatus::Draft,
amount_due: amount,
amount_paid: 0,
amount_remaining: amount,
currency: "usd".to_string(),
created: Utc::now().timestamp(),
due_date: Some((Utc::now() + Duration::days(30)).timestamp()),
paid: false,
lines: MockInvoiceLines {
data: vec![MockInvoiceLine {
id: format!("il_{}", generate_stripe_id()),
amount,
currency: "usd".to_string(),
description: Some("Subscription".to_string()),
quantity: 1,
price: MockPrice {
id: "price_auto".to_string(),
product: "prod_auto".to_string(),
unit_amount: amount,
currency: "usd".to_string(),
recurring: None,
},
}],
},
hosted_invoice_url: Some(format!("https://invoice.stripe.com/i/{invoice_id}")),
invoice_pdf: Some(format!("https://invoice.stripe.com/i/{invoice_id}/pdf")),
};
let mut invoices = self.invoices.write().await;
invoices.insert(invoice.id.clone(), invoice.clone());
Ok(invoice)
}
pub async fn get_invoice(&self, invoice_id: &str) -> Result<MockStripeInvoice, MockStripeError> {
self.check_failure().await?;
let invoices = self.invoices.read().await;
invoices
.get(invoice_id)
.cloned()
.ok_or_else(|| MockStripeError::NotFound(format!("Invoice {invoice_id} not found")))
}
pub async fn finalize_invoice(&self, invoice_id: &str) -> Result<MockStripeInvoice, MockStripeError> {
self.check_failure().await?;
let mut invoices = self.invoices.write().await;
let invoice = invoices
.get_mut(invoice_id)
.ok_or_else(|| MockStripeError::NotFound(format!("Invoice {invoice_id} not found")))?;
invoice.status = InvoiceStatus::Open;
let result = invoice.clone();
drop(invoices);
self.emit_event("invoice.finalized", &result).await;
Ok(result)
}
pub async fn pay_invoice(&self, invoice_id: &str) -> Result<MockStripeInvoice, MockStripeError> {
self.check_failure().await?;
let mut invoices = self.invoices.write().await;
let invoice = invoices
.get_mut(invoice_id)
.ok_or_else(|| MockStripeError::NotFound(format!("Invoice {invoice_id} not found")))?;
invoice.status = InvoiceStatus::Paid;
invoice.paid = true;
invoice.amount_paid = invoice.amount_due;
invoice.amount_remaining = 0;
let result = invoice.clone();
drop(invoices);
self.emit_event("invoice.paid", &result).await;
Ok(result)
}
pub async fn void_invoice(&self, invoice_id: &str) -> Result<MockStripeInvoice, MockStripeError> {
self.check_failure().await?;
let mut invoices = self.invoices.write().await;
let invoice = invoices
.get_mut(invoice_id)
.ok_or_else(|| MockStripeError::NotFound(format!("Invoice {invoice_id} not found")))?;
invoice.status = InvoiceStatus::Void;
let result = invoice.clone();
drop(invoices);
self.emit_event("invoice.voided", &result).await;
Ok(result)
}
pub async fn create_payment_intent(
&self,
amount: i64,
currency: &str,
customer_id: Option<&str>,
) -> Result<MockStripePaymentIntent, MockStripeError> {
self.check_failure().await?;
let payment_intent = MockStripePaymentIntent {
id: format!("pi_{}", generate_stripe_id()),
amount,
currency: currency.to_string(),
status: PaymentIntentStatus::RequiresPaymentMethod,
customer: customer_id.map(|s| s.to_string()),
payment_method: None,
client_secret: format!("pi_{}_secret_{}", generate_stripe_id(), generate_stripe_id()),
created: Utc::now().timestamp(),
};
let mut payment_intents = self.payment_intents.write().await;
payment_intents.insert(payment_intent.id.clone(), payment_intent.clone());
Ok(payment_intent)
}
pub async fn confirm_payment_intent(
&self,
payment_intent_id: &str,
payment_method_id: &str,
) -> Result<MockStripePaymentIntent, MockStripeError> {
self.check_failure().await?;
let mut payment_intents = self.payment_intents.write().await;
let pi = payment_intents.get_mut(payment_intent_id).ok_or_else(|| {
MockStripeError::NotFound(format!("PaymentIntent {payment_intent_id} not found"))
})?;
pi.payment_method = Some(payment_method_id.to_string());
pi.status = PaymentIntentStatus::Succeeded;
let result = pi.clone();
drop(payment_intents);
self.emit_event("payment_intent.succeeded", &result).await;
Ok(result)
}
pub async fn create_payment_method(
&self,
card_brand: &str,
last4: &str,
exp_month: u32,
exp_year: u32,
) -> Result<MockStripePaymentMethod, MockStripeError> {
self.check_failure().await?;
let pm = MockStripePaymentMethod {
id: format!("pm_{}", generate_stripe_id()),
customer: None,
payment_type: "card".to_string(),
card: Some(MockCard {
brand: card_brand.to_string(),
last4: last4.to_string(),
exp_month,
exp_year,
funding: "credit".to_string(),
}),
created: Utc::now().timestamp(),
};
let mut payment_methods = self.payment_methods.write().await;
payment_methods.insert(pm.id.clone(), pm.clone());
Ok(pm)
}
pub async fn attach_payment_method(
&self,
payment_method_id: &str,
customer_id: &str,
) -> Result<MockStripePaymentMethod, MockStripeError> {
self.check_failure().await?;
let customers = self.customers.read().await;
if !customers.contains_key(customer_id) {
return Err(MockStripeError::NotFound(format!(
"Customer {customer_id} not found"
)));
}
drop(customers);
let mut payment_methods = self.payment_methods.write().await;
let pm = payment_methods.get_mut(payment_method_id).ok_or_else(|| {
MockStripeError::NotFound(format!("PaymentMethod {payment_method_id} not found"))
})?;
pm.customer = Some(customer_id.to_string());
Ok(pm.clone())
}
pub async fn get_events(&self, limit: usize) -> Vec<MockStripeEvent> {
let events = self.events.read().await;
events.iter().rev().take(limit).cloned().collect()
}
async fn emit_event<T: Serialize>(&self, event_type: &str, data: &T) {
let event = MockStripeEvent {
id: format!("evt_{}", generate_stripe_id()),
event_type: event_type.to_string(),
created: Utc::now().timestamp(),
data: MockEventData {
object: serde_json::to_value(data).unwrap_or(serde_json::Value::Null),
},
livemode: false,
};
let mut events = self.events.write().await;
events.push(event);
if events.len() > 1000 {
events.drain(0..100);
}
}
}
impl Default for MockStripeClient {
fn default() -> Self {
Self::new()
}
}
fn generate_stripe_id() -> String {
let uuid = Uuid::new_v4();
uuid.to_string().replace('-', "")[..24].to_string()
}
#[derive(Debug, Clone)]
pub enum MockStripeError {
NotFound(String),
ApiError(String),
InvalidRequest(String),
AuthenticationError,
RateLimitError,
NetworkError(String),
}
impl std::fmt::Display for MockStripeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotFound(msg) => write!(f, "Not found: {msg}"),
Self::ApiError(code) => write!(f, "API error: {code}"),
Self::InvalidRequest(msg) => write!(f, "Invalid request: {msg}"),
Self::AuthenticationError => write!(f, "Authentication failed"),
Self::RateLimitError => write!(f, "Rate limit exceeded"),
Self::NetworkError(msg) => write!(f, "Network error: {msg}"),
}
}
}
impl std::error::Error for MockStripeError {}
#[derive(Debug, Clone)]
pub struct BillingTestScenario {
pub name: String,
pub description: String,
pub steps: Vec<TestStep>,
}
#[derive(Debug, Clone)]
pub struct TestStep {
pub action: TestAction,
pub expected_result: ExpectedResult,
pub delay_ms: Option<u64>,
}
#[derive(Debug, Clone)]
pub enum TestAction {
CreateCustomer { email: String, name: Option<String> },
CreateSubscription { plan: String, trial_days: Option<u32> },
UpgradeSubscription { new_plan: String },
DowngradeSubscription { new_plan: String },
CancelSubscription { immediately: bool },
SimulatePaymentFailure,
SimulatePaymentSuccess,
ProcessInvoice,
ApplyDiscount { code: String },
RecordUsage { metric: String, quantity: i64 },
CheckQuota { metric: String },
}
#[derive(Debug, Clone)]
pub enum ExpectedResult {
Success,
SubscriptionStatus(SubscriptionStatus),
InvoiceStatus(InvoiceStatus),
QuotaExceeded,
QuotaAvailable { remaining: i64 },
Error { code: String },
}
pub fn create_standard_test_scenarios() -> Vec<BillingTestScenario> {
vec![
BillingTestScenario {
name: "New Customer Signup".to_string(),
description: "Test new customer creating account and subscribing".to_string(),
steps: vec![
TestStep {
action: TestAction::CreateCustomer {
email: "test@example.com".to_string(),
name: Some("Test User".to_string()),
},
expected_result: ExpectedResult::Success,
delay_ms: None,
},
TestStep {
action: TestAction::CreateSubscription {
plan: "starter".to_string(),
trial_days: Some(14),
},
expected_result: ExpectedResult::SubscriptionStatus(SubscriptionStatus::Trialing),
delay_ms: None,
},
],
},
BillingTestScenario {
name: "Plan Upgrade".to_string(),
description: "Test customer upgrading from starter to pro".to_string(),
steps: vec![
TestStep {
action: TestAction::CreateCustomer {
email: "upgrade@example.com".to_string(),
name: None,
},
expected_result: ExpectedResult::Success,
delay_ms: None,
},
TestStep {
action: TestAction::CreateSubscription {
plan: "starter".to_string(),
trial_days: None,
},
expected_result: ExpectedResult::SubscriptionStatus(SubscriptionStatus::Active),
delay_ms: None,
},
TestStep {
action: TestAction::UpgradeSubscription {
new_plan: "pro".to_string(),
},
expected_result: ExpectedResult::Success,
delay_ms: Some(100),
},
],
},
BillingTestScenario {
name: "Payment Failure Recovery".to_string(),
description: "Test handling payment failure and recovery".to_string(),
steps: vec![
TestStep {
action: TestAction::CreateCustomer {
email: "failure@example.com".to_string(),
name: None,
},
expected_result: ExpectedResult::Success,
delay_ms: None,
},
TestStep {
action: TestAction::CreateSubscription {
plan: "pro".to_string(),
trial_days: None,
},
expected_result: ExpectedResult::SubscriptionStatus(SubscriptionStatus::Active),
delay_ms: None,
},
TestStep {
action: TestAction::SimulatePaymentFailure,
expected_result: ExpectedResult::SubscriptionStatus(SubscriptionStatus::PastDue),
delay_ms: Some(50),
},
TestStep {
action: TestAction::SimulatePaymentSuccess,
expected_result: ExpectedResult::SubscriptionStatus(SubscriptionStatus::Active),
delay_ms: Some(50),
},
],
},
]
}