flowglad 0.1.1

(Unofficial) Rust SDK for FlowGlad - Open source billing infrastructure
Documentation
//! Integration tests for the FlowGlad SDK
//!
//! These tests run against the real FlowGlad API using a test key.
//! They verify that the SDK correctly interacts with the actual API.
//!
//! # Setup
//!
//! To run these tests, set the FLOWGLAD_API_KEY environment variable:
//!
//! ```bash
//! FLOWGLAD_API_KEY=sk_test_... cargo test --test integration_tests
//! ```
//!
//! Or create a `.env` file in the project root:
//!
//! ```
//! FLOWGLAD_API_KEY=sk_test_your_key_here
//! ```

use flowglad::types::customer::{CreateCustomer, UpdateCustomer};
use flowglad::{Client, Config};
use once_cell::sync::Lazy;

/// Shared test client using the FlowGlad test API key from environment
static TEST_CLIENT: Lazy<Client> = Lazy::new(|| {
    // Load .env file if it exists (for local development)
    let _ = dotenvy::dotenv();

    let api_key = std::env::var("FLOWGLAD_API_KEY")
        .expect("FLOWGLAD_API_KEY environment variable must be set to run integration tests");

    let config = Config::new(api_key);
    Client::new(config).expect("Failed to create test client")
});

/// Generate a unique test email to avoid conflicts
fn unique_email(prefix: &str) -> String {
    use std::time::{SystemTime, UNIX_EPOCH};
    let timestamp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_secs();
    format!("{}+{}@flowglad-test.com", prefix, timestamp)
}

/// Generate a unique test name
fn unique_name(prefix: &str) -> String {
    use std::time::{SystemTime, UNIX_EPOCH};
    let timestamp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_secs();
    format!("{} {}", prefix, timestamp)
}

/// Generate a unique external ID
fn unique_external_id(prefix: &str) -> String {
    use std::time::{SystemTime, UNIX_EPOCH};
    let timestamp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_secs();
    format!("{}_{}", prefix, timestamp)
}

// Customer API Tests

#[tokio::test]
async fn test_create_customer() {
    let external_id = unique_external_id("test_create");
    let email = unique_email("create");
    let name = unique_name("Create Test");

    let result = TEST_CLIENT
        .customers()
        .create(CreateCustomer::new(&external_id, &name).email(&email))
        .await;

    assert!(
        result.is_ok(),
        "Failed to create customer: {:?}",
        result.err()
    );

    let customer = result.unwrap();
    assert_eq!(customer.email.as_deref(), Some(email.as_str()));
    assert_eq!(customer.name.as_deref(), Some(name.as_str()));
    assert!(!customer.id.as_str().is_empty());

    println!("✓ Created customer: {} ({})", customer.id, email);
}

#[tokio::test]
async fn test_create_customer_with_metadata() {
    let external_id = unique_external_id("test_metadata");
    let email = unique_email("metadata");

    let result = TEST_CLIENT
        .customers()
        .create(
            CreateCustomer::new(&external_id, "Metadata Test")
                .email(&email)
                .metadata("test_key", serde_json::json!("test_value"))
                .metadata("tier", serde_json::json!(3)),
        )
        .await;

    assert!(
        result.is_ok(),
        "Failed to create customer: {:?}",
        result.err()
    );

    let customer = result.unwrap();
    // Note: The API may not return metadata in the response, so we just verify the customer was created
    assert!(!customer.id.as_str().is_empty());

    println!("✓ Created customer with metadata: {}", customer.id);
}

#[tokio::test]
async fn test_get_customer() {
    let external_id = unique_external_id("test_get");
    let email = unique_email("get");
    let created = TEST_CLIENT
        .customers()
        .create(CreateCustomer::new(&external_id, "Get Test").email(&email))
        .await
        .expect("Failed to create customer");

    // API uses external_id for GET, not the internal customer id
    let result = TEST_CLIENT.customers().get(&external_id).await;

    assert!(result.is_ok(), "Failed to get customer: {:?}", result.err());

    let retrieved = result.unwrap();
    assert_eq!(created.id, retrieved.id);
    assert_eq!(created.email, retrieved.email);
    assert_eq!(created.name, retrieved.name);

    println!("✓ Retrieved customer: {}", retrieved.id);
}

#[tokio::test]
async fn test_get_nonexistent_customer() {
    let fake_external_id = "nonexistent_external_id_12345";
    let result = TEST_CLIENT.customers().get(fake_external_id).await;

    assert!(result.is_err(), "Expected error for nonexistent customer");

    let error = result.unwrap_err();
    assert!(
        error.to_string().contains("404")
            || error.to_string().contains("not found")
            || error.to_string().contains("NOT_FOUND"),
        "Expected 404 error, got: {}",
        error
    );

    println!("✓ Correctly handled nonexistent customer");
}

#[tokio::test]
async fn test_list_customers() {
    let external_id = unique_external_id("test_list");
    let email = unique_email("list");
    let _ = TEST_CLIENT
        .customers()
        .create(CreateCustomer::new(&external_id, "List Test").email(&email))
        .await
        .expect("Failed to create customer");

    let result = TEST_CLIENT.customers().list().await;

    assert!(
        result.is_ok(),
        "Failed to list customers: {:?}",
        result.err()
    );

    let list = result.unwrap();
    assert!(!list.data.is_empty(), "Expected at least one customer");
    assert!(!list.is_empty());
    assert!(list.len() > 0);

    println!("✓ Listed {} customers", list.len());
}

#[tokio::test]
async fn test_update_customer() {
    let external_id = unique_external_id("test_update");
    let email = unique_email("update");
    let created = TEST_CLIENT
        .customers()
        .create(CreateCustomer::new(&external_id, "Original Name").email(&email))
        .await
        .expect("Failed to create customer");

    let new_email = unique_email("updated");
    let result = TEST_CLIENT
        .customers()
        .update(
            &external_id,
            UpdateCustomer::new()
                .email(&new_email)
                .name("Updated Name")
                .phone("+1-555-9999"),
        )
        .await;

    assert!(
        result.is_ok(),
        "Failed to update customer: {:?}",
        result.err()
    );

    let updated = result.unwrap();
    assert_eq!(updated.id, created.id);
    assert_eq!(updated.email.as_deref(), Some(new_email.as_str()));
    assert_eq!(updated.name.as_deref(), Some("Updated Name"));
    // Note: API doesn't return phone field in response
    // assert_eq!(updated.phone.as_deref(), Some("+1-555-9999"));

    println!("✓ Updated customer: {}", updated.id);
}

#[tokio::test]
async fn test_get_billing_details() {
    let external_id = unique_external_id("test_billing");
    let email = unique_email("billing");
    let created = TEST_CLIENT
        .customers()
        .create(CreateCustomer::new(&external_id, "Billing Test").email(&email))
        .await
        .expect("Failed to create customer");

    let result = TEST_CLIENT.customers().get_billing(&external_id).await;

    assert!(
        result.is_ok(),
        "Failed to get billing details: {:?}",
        result.err()
    );

    let billing = result.unwrap();
    assert_eq!(billing.customer.id, created.id);
    // Billing details should contain subscriptions (at least the free plan)
    assert!(!billing.subscriptions.is_empty() || !billing.current_subscriptions.is_empty());

    println!(
        "✓ Retrieved billing details for customer: {}",
        billing.customer.id
    );
}

#[tokio::test]
async fn test_customer_with_phone() {
    let external_id = unique_external_id("test_phone");
    let email = unique_email("phone");

    let result = TEST_CLIENT
        .customers()
        .create(
            CreateCustomer::new(&external_id, "Phone Test")
                .email(&email)
                .phone("+1-555-1234"),
        )
        .await;

    assert!(
        result.is_ok(),
        "Failed to create customer: {:?}",
        result.err()
    );

    let customer = result.unwrap();
    // Note: API doesn't return phone field in create response, but accepts it as input
    assert!(!customer.id.as_str().is_empty());

    println!("✓ Created customer with phone: {}", customer.id);
}