app-store-server-library 4.3.0

The Rust server library for the App Store Server API, App Store Server Notifications and Advanced Commerce API
Documentation
mod common;
use common::transport_mock::{MockTransport, RequestVerifier};
use app_store_server_library::api_client::api::advanced_commerce_api::AdvancedCommerceApiClient;
use app_store_server_library::api_client::error::ConfigurationError;
use app_store_server_library::primitives::environment::Environment;
use app_store_server_library::primitives::advanced_commerce::subscription_cancel_request::SubscriptionCancelRequest;
use app_store_server_library::primitives::advanced_commerce::subscription_revoke_request::SubscriptionRevokeRequest;
use app_store_server_library::primitives::advanced_commerce::request_refund_request::RequestRefundRequest;
use app_store_server_library::primitives::advanced_commerce::subscription_change_metadata_request::SubscriptionChangeMetadataRequest;
use app_store_server_library::primitives::advanced_commerce::subscription_price_change_request::SubscriptionPriceChangeRequest;
use app_store_server_library::primitives::advanced_commerce::subscription_migrate_request::SubscriptionMigrateRequest;
use app_store_server_library::primitives::advanced_commerce::request_info::RequestInfo;
use app_store_server_library::primitives::advanced_commerce::refund_reason::RefundReason;
use app_store_server_library::primitives::advanced_commerce::refund_type::RefundType;
use http::{Method, StatusCode};
use serde_json::Value;
use std::collections::HashMap;
use std::fs;
use uuid::Uuid;

#[tokio::test]
async fn test_cancel_subscription() {
    let client = advanced_commerce_api_client_with_body_from_file(
        "tests/resources/models/subscriptionCancelResponse.json",
        StatusCode::OK,
        Some(Box::new(|req, body| {
            assert_eq!(&Method::POST, req.method());
            assert_eq!(
                "https://local-testing-base-url/advancedCommerce/v1/subscription/cancel/test_transaction_id",
                req.uri().to_string()
            );

            let decoded_json: HashMap<&str, Value> = serde_json::from_slice(body).unwrap();
            assert!(decoded_json.contains_key("requestInfo"));
        })),
    );

    let request = SubscriptionCancelRequest::new(Uuid::new_v4())
        .with_storefront("test_storefront".to_string());

    let response = client
        .cancel_subscription("test_transaction_id", &request)
        .await
        .unwrap();

    assert!(!response.signed_transaction_info.is_empty());
    assert!(!response.signed_renewal_info.is_empty());
}

#[tokio::test]
async fn test_revoke_subscription() {
    let client = advanced_commerce_api_client_with_body_from_file(
        "tests/resources/models/subscriptionRevokeResponse.json",
        StatusCode::OK,
        Some(Box::new(|req, body| {
            assert_eq!(&Method::POST, req.method());
            assert_eq!(
                "https://local-testing-base-url/advancedCommerce/v1/subscription/revoke/test_transaction_id",
                req.uri().to_string()
            );

            let decoded_json: HashMap<&str, Value> = serde_json::from_slice(body).unwrap();
            assert!(decoded_json.contains_key("requestInfo"));
            assert!(decoded_json.contains_key("refundReason"));
            assert!(decoded_json.contains_key("refundType"));
        })),
    );

    let request = SubscriptionRevokeRequest::new(
        Uuid::new_v4(),
        RefundReason::UnsatisfiedWithPurchase,
        "LOW_ENGAGEMENT".to_string(),
        RefundType::Full,
    );

    let response = client
        .revoke_subscription("test_transaction_id", &request)
        .await
        .unwrap();

    assert!(!response.signed_transaction_info.is_empty());
    assert!(!response.signed_renewal_info.is_empty());
}

#[tokio::test]
async fn test_request_transaction_refund() {
    let client = advanced_commerce_api_client_with_body_from_file(
        "tests/resources/models/requestRefundResponse.json",
        StatusCode::OK,
        Some(Box::new(|req, body| {
            assert_eq!(&Method::POST, req.method());
            assert_eq!(
                "https://local-testing-base-url/advancedCommerce/v1/transaction/requestRefund/test_transaction_id",
                req.uri().to_string()
            );

            let decoded_json: HashMap<&str, Value> = serde_json::from_slice(body).unwrap();
            assert!(decoded_json.contains_key("requestInfo"));
            assert!(decoded_json.contains_key("items"));
        })),
    );

    let request = RequestRefundRequest {
        request_info: RequestInfo::new(Uuid::new_v4()),
        currency: None,
        items: vec![],
        refund_risking_preference: false,
        storefront: None,
    };

    let response = client
        .request_transaction_refund("test_transaction_id", &request)
        .await
        .unwrap();

    assert!(!response.signed_transaction_info.is_empty());
}

#[tokio::test]
async fn test_change_subscription_metadata() {
    let client = advanced_commerce_api_client_with_body_from_file(
        "tests/resources/models/subscriptionChangeMetadataResponse.json",
        StatusCode::OK,
        Some(Box::new(|req, body| {
            assert_eq!(&Method::POST, req.method());
            assert_eq!(
                "https://local-testing-base-url/advancedCommerce/v1/subscription/changeMetadata/test_transaction_id",
                req.uri().to_string()
            );

            let decoded_json: HashMap<&str, Value> = serde_json::from_slice(body).unwrap();
            assert!(decoded_json.contains_key("requestInfo"));
            assert!(decoded_json.contains_key("items"));
        })),
    );

    let request = SubscriptionChangeMetadataRequest::new(Uuid::new_v4())
        .with_items(vec![]);

    let response = client
        .change_subscription_metadata("test_transaction_id", &request)
        .await
        .unwrap();

    assert!(!response.signed_transaction_info.is_empty());
    assert!(!response.signed_renewal_info.is_empty());
}

#[tokio::test]
async fn test_change_subscription_price() {
    let client = advanced_commerce_api_client_with_body_from_file(
        "tests/resources/models/subscriptionPriceChangeResponse.json",
        StatusCode::OK,
        Some(Box::new(|req, body| {
            assert_eq!(&Method::POST, req.method());
            assert_eq!(
                "https://local-testing-base-url/advancedCommerce/v1/subscription/changePrice/test_transaction_id",
                req.uri().to_string()
            );

            let decoded_json: HashMap<&str, Value> = serde_json::from_slice(body).unwrap();
            assert!(decoded_json.contains_key("requestInfo"));
            assert!(decoded_json.contains_key("items"));
        })),
    );

    let request = SubscriptionPriceChangeRequest::new(
        "test_storefront".to_string(),
        vec![],
        Uuid::new_v4(),
    );

    let response = client
        .change_subscription_price("test_transaction_id", &request)
        .await
        .unwrap();

    assert!(!response.signed_transaction_info.is_empty());
    assert!(!response.signed_renewal_info.is_empty());
}

#[tokio::test]
async fn test_migrate_subscription() {
    let client = advanced_commerce_api_client_with_body_from_file(
        "tests/resources/models/subscriptionMigrateResponse.json",
        StatusCode::OK,
        Some(Box::new(|req, body| {
            assert_eq!(&Method::POST, req.method());
            assert_eq!(
                "https://local-testing-base-url/advancedCommerce/v1/subscription/migrate/test_transaction_id",
                req.uri().to_string()
            );

            let decoded_json: HashMap<&str, Value> = serde_json::from_slice(body).unwrap();
            assert!(decoded_json.contains_key("requestInfo"));
            assert!(decoded_json.contains_key("items"));
        })),
    );

    let request = SubscriptionMigrateRequest::new(
        Uuid::new_v4(),
        vec![],
        "target_product".to_string(),
        "tax_code".to_string(),
    );

    let response = client
        .migrate_subscription("test_transaction_id", &request)
        .await
        .unwrap();

    assert!(!response.signed_transaction_info.is_empty());
    assert!(!response.signed_renewal_info.is_empty());
}

#[test]
fn test_xcode_environment_is_not_supported() {
    let mock_transport = MockTransport::new(
        String::new(),
        StatusCode::OK,
        None
    );

    let result = AdvancedCommerceApiClient::new(
        vec![],
        "test_key_id",
        "test_issuer_id",
        "com.test.app",
        Environment::Xcode,
        mock_transport,
    );

    assert!(result.is_err());
    match result {
        Err(ConfigurationError::InvalidEnvironment(msg)) => {
            assert!(msg.contains("Xcode environment is not supported"));
        }
        _ => panic!("Expected InvalidEnvironment error"),
    }
}

#[test]
fn test_sandbox_environment_is_accepted() {
    let mock_transport = MockTransport::new(
        String::new(),
        StatusCode::OK,
        None
    );

    let result = AdvancedCommerceApiClient::new(
        vec![],
        "test_key_id",
        "test_issuer_id",
        "com.test.app",
        Environment::Sandbox,
        mock_transport,
    );

    assert!(result.is_ok());
}

#[test]
fn test_production_environment_is_accepted() {
    let mock_transport = MockTransport::new(
        String::new(),
        StatusCode::OK,
        None
    );

    let result = AdvancedCommerceApiClient::new(
        vec![],
        "test_key_id",
        "test_issuer_id",
        "com.test.app",
        Environment::Production,
        mock_transport,
    );

    assert!(result.is_ok());
}

fn advanced_commerce_api_client_with_body_from_file(
    path: &str,
    status: StatusCode,
    request_verifier: Option<RequestVerifier>,
) -> AdvancedCommerceApiClient<MockTransport> {
    let body = fs::read_to_string(path).expect("Failed to read file");
    advanced_commerce_api_client(body, status, request_verifier)
}

fn advanced_commerce_api_client(
    body: String,
    status: StatusCode,
    request_verifier: Option<RequestVerifier>,
) -> AdvancedCommerceApiClient<MockTransport> {
    let key = fs::read("tests/resources/certs/testSigningKey.p8").expect("Failed to read file");

    let mock_transport = MockTransport::new(body, status, request_verifier);

    AdvancedCommerceApiClient::new(
        key,
        "keyId",
        "issuerId",
        "com.example",
        Environment::LocalTesting,
        mock_transport,
    )
    .expect("Error creating advanced commerce client")
}