correlate 0.3.0

correlate is a standalone server that listens for Stripe webhook events and sends notification emails about successful orders.
use crate::{AppConfig, MailConfig, ProductsConfig, StripeConfig};

// Mock configuration for testing
fn get_test_config() -> AppConfig {
    let products = vec![
        crate::products::ProductVariant {
            name: "Test Product".to_string(),
            sku: "test-sku".to_string(),
            price: "price_123456".to_string(),
        },
        crate::products::ProductVariant {
            name: "Another Product".to_string(),
            sku: "another-sku".to_string(),
            price: "price_654321".to_string(),
        },
    ];

    AppConfig {
        email: MailConfig {
            sender: "test@example.com".to_string(),
            recipient: "admin@example.com".to_string(),
            smtp_relay_host: Some("smtp.example.com".to_string()),
            smtp_relay_port: 25,
            smtp_username: Some("testuser".to_string()),
            smtp_password: Some("testpass".to_string()),
        },
        stripe: Some(StripeConfig {
            // not a valid test key
            secret: "sk_test_123456".to_string(),
        }),
        products: ProductsConfig { products },
    }
}

// We'll use a JSON string instead of constructing the object directly
// to avoid serialization issues
fn create_test_checkout_session_json() -> String {
    r#"{
        "id": "cs_test_123456",
        "object": "checkout.session",
        "amount_subtotal": 1000,
        "amount_total": 1000,
        "created": 1234567890,
        "currency": "usd",
        "mode": "payment",
        "payment_status": "paid",
        "status": "complete",
        "customer_details": {
            "email": "customer@example.com",
            "name": "Test Customer"
        },
        "url": "https://example.com/checkout"
    }"#
    .to_string()
}

#[cfg(test)]
mod integration_tests {
    use crate::VERSION;

    use super::*;
    use pretty_assertions::assert_eq;
    use rocket::http::{ContentType, Status};
    use rocket::local::blocking::Client;

    fn client() -> Client {
        // initialize();
        Client::tracked(
            rocket::build()
                .mount(
                    "/",
                    crate::routes![crate::index, crate::create_checkout, crate::webhook_event],
                )
                .manage(get_test_config()),
        )
        .expect("not valid rocket instance")
    }

    #[test]
    fn test_index_endpoint() {
        let client = client();
        let response = client.get("/").dispatch();

        assert_eq!(response.status(), Status::Ok);
        let body = response
            .into_string()
            .expect("failed to get response as string");

        // Just check that it contains the description, since the version may be different in tests
        assert!(body.contains("cor·re·late"));
        assert!(body.contains(VERSION));
    }

    #[test]
    fn test_webhook_event_valid_payload() {
        let client = client();
        let valid_checkout_event = format!(
            r#"{{
            "id": "evt_123456",
            "created": 1234567890,
            "data": {{
                "object": {}
            }}
        }}"#,
            create_test_checkout_session_json()
        );

        let response = client
            .post("/")
            .header(ContentType::JSON)
            .body(valid_checkout_event)
            .dispatch();

        // In tests the webhook processing might fail due to missing integrations,
        // but we can at least check that the request is accepted
        assert!(
            response.status() == rocket::http::Status::Ok
                || response.status() == rocket::http::Status::BadRequest
        );
    }

    #[test]
    fn test_webhook_event_invalid_payload() {
        let client = client();

        // Create an invalid event with missing required fields
        let invalid_event = r#"{
            "id": "evt_invalid"
            // Missing required fields
        }"#;

        let response = client
            .post("/")
            .header(ContentType::JSON)
            .body(invalid_event)
            .dispatch();

        assert_eq!(response.status(), rocket::http::Status::BadRequest);
    }

    #[test]
    fn test_checkout_endpoint_valid_payload() {
        let client = client();

        let checkout_request = r#"{
            "successUrl": "https://example.com/success",
            "submissionUrl": "https://example.com/submission",
            "items": [
                {
                    "name": "Test Product",
                    "sku": "test-sku",
                    "quantity": 1
                }
            ]
        }"#;

        let response = client
            .post("/checkout")
            .header(ContentType::JSON)
            .body(checkout_request)
            .dispatch();

        // We expect either Ok or BadRequest since we're using fake API keys
        assert!(
            response.status() == rocket::http::Status::Ok
                || response.status() == rocket::http::Status::BadRequest,
            "Expected Ok or BadRequest, got {:?}",
            response.status()
        );
    }

    #[test]
    fn test_checkout_endpoint_invalid_product() {
        let client = client();

        let checkout_request = r#"{
            "successUrl": "https://example.com/success",
            "submissionUrl": "https://example.com/submission",
            "items": [
                {
                    "name": "Non-existent Product",
                    "sku": "invalid-sku",
                    "quantity": 1
                }
            ]
        }"#;

        let response = client
            .post("/checkout")
            .header(ContentType::JSON)
            .body(checkout_request)
            .dispatch();

        assert_eq!(response.status(), rocket::http::Status::BadRequest);
    }

    #[test]
    fn test_checkout_endpoint_missing_fields() {
        let client = client();

        // Missing required successUrl
        let checkout_request = r#"{
            "submissionUrl": "https://example.com/submission",
            "items": [
                {
                    "name": "Test Product",
                    "sku": "test-sku",
                    "quantity": 1
                }
            ]
        }"#;

        let response = client
            .post("/checkout")
            .header(ContentType::JSON)
            .body(checkout_request)
            .dispatch();

        assert_eq!(response.status(), rocket::http::Status::UnprocessableEntity);
    }

    #[test]
    fn test_checkout_endpoint_empty_items() {
        let client = client();

        let checkout_request = r#"{
            "successUrl": "https://example.com/success",
            "submissionUrl": "https://example.com/submission",
            "items": []
        }"#;

        let response = client
            .post("/checkout")
            .header(ContentType::JSON)
            .body(checkout_request)
            .dispatch();

        // This should be a BadRequest since there are no items
        assert_eq!(response.status(), rocket::http::Status::BadRequest);
    }
}