hostbasket/actix_mvc_app/tests/payment_tests.rs
Mahmoud-Emad d3a66d4fc8 feat: Add initial production deployment support
- Add .env.example file for environment variable setup
- Add .gitignore to manage sensitive files and directories
- Add Dockerfile.prod for production-ready Docker image
- Add PRODUCTION_CHECKLIST.md for pre/post deployment steps
- Add PRODUCTION_DEPLOYMENT.md for deployment instructions
- Add STRIPE_SETUP.md for Stripe payment configuration
- Add config/default.toml for default configuration settings
- Add config/local.toml.example for local configuration template
2025-06-25 18:32:20 +03:00

363 lines
13 KiB
Rust

use actix_mvc_app::controllers::payment::CompanyRegistrationData;
use actix_mvc_app::db::payment as payment_db;
use actix_mvc_app::db::registration as registration_db;
use actix_mvc_app::utils::stripe_security::StripeWebhookVerifier;
use actix_mvc_app::validators::CompanyRegistrationValidator;
use heromodels::models::biz::PaymentStatus;
use hmac::{Hmac, Mac};
use sha2::Sha256;
#[cfg(test)]
mod payment_flow_tests {
use super::*;
fn create_valid_registration_data() -> CompanyRegistrationData {
CompanyRegistrationData {
company_name: "Test Company Ltd".to_string(),
company_type: "Single FZC".to_string(),
company_email: Some("test@example.com".to_string()),
company_phone: Some("+1234567890".to_string()),
company_website: Some("https://example.com".to_string()),
company_address: Some("123 Test Street, Test City".to_string()),
company_industry: Some("Technology".to_string()),
company_purpose: Some("Software development".to_string()),
fiscal_year_end: Some("December".to_string()),
shareholders: r#"[{"name": "John Doe", "percentage": 100}]"#.to_string(),
payment_plan: "monthly".to_string(),
}
}
#[test]
fn test_registration_data_validation_success() {
let data = create_valid_registration_data();
let result = CompanyRegistrationValidator::validate(&data);
assert!(
result.is_valid,
"Valid registration data should pass validation"
);
assert!(result.errors.is_empty(), "Valid data should have no errors");
}
#[test]
fn test_registration_data_validation_failures() {
// Test empty company name
let mut data = create_valid_registration_data();
data.company_name = "".to_string();
let result = CompanyRegistrationValidator::validate(&data);
assert!(!result.is_valid);
assert!(result.errors.iter().any(|e| e.field == "company_name"));
// Test invalid email
let mut data = create_valid_registration_data();
data.company_email = Some("invalid-email".to_string());
let result = CompanyRegistrationValidator::validate(&data);
assert!(!result.is_valid);
assert!(result.errors.iter().any(|e| e.field == "company_email"));
// Test invalid phone
let mut data = create_valid_registration_data();
data.company_phone = Some("123".to_string());
let result = CompanyRegistrationValidator::validate(&data);
assert!(!result.is_valid);
assert!(result.errors.iter().any(|e| e.field == "company_phone"));
// Test invalid website
let mut data = create_valid_registration_data();
data.company_website = Some("not-a-url".to_string());
let result = CompanyRegistrationValidator::validate(&data);
assert!(!result.is_valid);
assert!(result.errors.iter().any(|e| e.field == "company_website"));
// Test invalid shareholders JSON
let mut data = create_valid_registration_data();
data.shareholders = "invalid json".to_string();
let result = CompanyRegistrationValidator::validate(&data);
assert!(!result.is_valid);
assert!(result.errors.iter().any(|e| e.field == "shareholders"));
// Test invalid payment plan
let mut data = create_valid_registration_data();
data.payment_plan = "invalid_plan".to_string();
let result = CompanyRegistrationValidator::validate(&data);
assert!(!result.is_valid);
assert!(result.errors.iter().any(|e| e.field == "payment_plan"));
}
#[test]
fn test_registration_data_storage_and_retrieval() {
let payment_intent_id = "pi_test_123456".to_string();
let data = create_valid_registration_data();
// Store registration data
let store_result =
registration_db::store_registration_data(payment_intent_id.clone(), data.clone());
assert!(
store_result.is_ok(),
"Should successfully store registration data"
);
// Retrieve registration data
let retrieve_result = registration_db::get_registration_data(&payment_intent_id);
assert!(
retrieve_result.is_ok(),
"Should successfully retrieve registration data"
);
let retrieved_data = retrieve_result.unwrap();
assert!(
retrieved_data.is_some(),
"Should find stored registration data"
);
let stored_data = retrieved_data.unwrap();
assert_eq!(stored_data.company_name, data.company_name);
assert_eq!(stored_data.company_email, data.company_email.unwrap());
assert_eq!(stored_data.payment_plan, data.payment_plan);
// Clean up
let _ = registration_db::delete_registration_data(&payment_intent_id);
}
#[test]
fn test_payment_creation_and_status_updates() {
let payment_intent_id = "pi_test_payment_123".to_string();
// Create a payment
let create_result = payment_db::create_new_payment(
payment_intent_id.clone(),
0, // Temporary company_id
"monthly".to_string(),
20.0, // setup_fee
20.0, // monthly_fee
40.0, // total_amount
);
assert!(create_result.is_ok(), "Should successfully create payment");
let (payment_id, payment) = create_result.unwrap();
assert_eq!(payment.payment_intent_id, payment_intent_id);
assert_eq!(payment.status, PaymentStatus::Pending);
// Update payment status to completed
let update_result =
payment_db::update_payment_status(&payment_intent_id, PaymentStatus::Completed);
assert!(
update_result.is_ok(),
"Should successfully update payment status"
);
let updated_payment = update_result.unwrap();
assert!(updated_payment.is_some(), "Should return updated payment");
assert_eq!(updated_payment.unwrap().status, PaymentStatus::Completed);
// Test updating company ID
let company_id = 123u32;
let link_result = payment_db::update_payment_company_id(&payment_intent_id, company_id);
assert!(
link_result.is_ok(),
"Should successfully link payment to company"
);
let linked_payment = link_result.unwrap();
assert!(linked_payment.is_some(), "Should return linked payment");
assert_eq!(linked_payment.unwrap().company_id, company_id);
}
#[test]
fn test_payment_queries() {
// Test getting pending payments
let pending_result = payment_db::get_pending_payments();
assert!(
pending_result.is_ok(),
"Should successfully get pending payments"
);
// Test getting failed payments
let failed_result = payment_db::get_failed_payments();
assert!(
failed_result.is_ok(),
"Should successfully get failed payments"
);
// Test getting payment by intent ID
let get_result = payment_db::get_payment_by_intent_id("nonexistent_payment");
assert!(
get_result.is_ok(),
"Should handle nonexistent payment gracefully"
);
assert!(
get_result.unwrap().is_none(),
"Should return None for nonexistent payment"
);
}
#[test]
fn test_pricing_calculations() {
// Test pricing calculation logic
fn calculate_total_amount(setup_fee: f64, monthly_fee: f64, payment_plan: &str) -> f64 {
match payment_plan {
"monthly" => setup_fee + monthly_fee,
"yearly" => setup_fee + (monthly_fee * 12.0 * 0.8), // 20% discount
"two_year" => setup_fee + (monthly_fee * 24.0 * 0.6), // 40% discount
_ => setup_fee + monthly_fee,
}
}
// Test monthly pricing
let monthly_total = calculate_total_amount(20.0, 20.0, "monthly");
assert_eq!(
monthly_total, 40.0,
"Monthly total should be setup + monthly fee"
);
// Test yearly pricing (20% discount)
let yearly_total = calculate_total_amount(20.0, 20.0, "yearly");
let expected_yearly = 20.0 + (20.0 * 12.0 * 0.8); // Setup + discounted yearly
assert_eq!(
yearly_total, expected_yearly,
"Yearly total should include 20% discount"
);
// Test two-year pricing (40% discount)
let two_year_total = calculate_total_amount(20.0, 20.0, "two_year");
let expected_two_year = 20.0 + (20.0 * 24.0 * 0.6); // Setup + discounted two-year
assert_eq!(
two_year_total, expected_two_year,
"Two-year total should include 40% discount"
);
}
#[test]
fn test_company_type_mapping() {
let test_cases = vec![
("Single FZC", "Single"),
("Startup FZC", "Starter"),
("Growth FZC", "Global"),
("Global FZC", "Global"),
("Cooperative FZC", "Coop"),
("Twin FZC", "Twin"),
];
for (input, expected) in test_cases {
// This would test the business type mapping in create_company_from_form_data
// We'll need to expose this logic or test it indirectly
assert!(true, "Company type mapping test placeholder for {}", input);
}
}
}
#[cfg(test)]
mod webhook_security_tests {
use super::*;
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
fn test_webhook_signature_verification_valid() {
let payload = b"test payload";
let webhook_secret = "whsec_test_secret";
let current_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
// Create a valid signature
let signed_payload = format!("{}.{}", current_time, std::str::from_utf8(payload).unwrap());
let mut mac = Hmac::<Sha256>::new_from_slice(webhook_secret.as_bytes()).unwrap();
mac.update(signed_payload.as_bytes());
let signature = hex::encode(mac.finalize().into_bytes());
let signature_header = format!("t={},v1={}", current_time, signature);
let result = StripeWebhookVerifier::verify_signature(
payload,
&signature_header,
webhook_secret,
Some(300),
);
assert!(result.is_ok(), "Valid signature should verify successfully");
assert!(result.unwrap(), "Valid signature should return true");
}
#[test]
fn test_webhook_signature_verification_invalid() {
let payload = b"test payload";
let webhook_secret = "whsec_test_secret";
let current_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
// Create an invalid signature
let signature_header = format!("t={},v1=invalid_signature", current_time);
let result = StripeWebhookVerifier::verify_signature(
payload,
&signature_header,
webhook_secret,
Some(300),
);
assert!(result.is_ok(), "Invalid signature should not cause error");
assert!(!result.unwrap(), "Invalid signature should return false");
}
#[test]
fn test_webhook_signature_verification_expired() {
let payload = b"test payload";
let webhook_secret = "whsec_test_secret";
let old_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
- 400; // 400 seconds ago (beyond 300s tolerance)
// Create a signature with old timestamp
let signed_payload = format!("{}.{}", old_time, std::str::from_utf8(payload).unwrap());
let mut mac = Hmac::<Sha256>::new_from_slice(webhook_secret.as_bytes()).unwrap();
mac.update(signed_payload.as_bytes());
let signature = hex::encode(mac.finalize().into_bytes());
let signature_header = format!("t={},v1={}", old_time, signature);
let result = StripeWebhookVerifier::verify_signature(
payload,
&signature_header,
webhook_secret,
Some(300),
);
assert!(result.is_err(), "Expired signature should return error");
assert!(
result.unwrap_err().contains("too old"),
"Error should mention timestamp age"
);
}
#[test]
fn test_webhook_signature_verification_malformed_header() {
let payload = b"test payload";
let webhook_secret = "whsec_test_secret";
// Test various malformed headers
let malformed_headers = vec![
"invalid_header",
"t=123",
"v1=signature",
"t=invalid_timestamp,v1=signature",
"",
];
for header in malformed_headers {
let result =
StripeWebhookVerifier::verify_signature(payload, header, webhook_secret, Some(300));
assert!(
result.is_err(),
"Malformed header '{}' should return error",
header
);
}
}
}