braze-sync 0.1.0

GitOps CLI for managing Braze configuration as code
Documentation
//! Integration tests for `braze-sync export` (Catalog Schema).
//!
//! Each test stands up a wiremock server, writes a temporary
//! braze-sync.config.yaml that points the api_endpoint at the mock,
//! invokes the real binary via assert_cmd, and asserts on the resulting
//! filesystem state and exit code.
//!
//! Tests use `flavor = "multi_thread"` so wiremock can serve HTTP
//! requests on a worker thread while the test thread is parked in
//! `spawn_blocking` waiting on the subprocess. Single-threaded
//! `#[tokio::test]` would deadlock because the blocking subprocess wait
//! would hold the only worker.

mod common;

use assert_cmd::Command;
use common::write_config;
use serde_json::json;
use std::fs;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn export_catalog_schemas_writes_files_and_exits_zero() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/catalogs"))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
            "catalogs": [
                {
                    "name": "cardiology",
                    "description": "Cardiology catalog",
                    "fields": [
                        {"name": "id", "type": "string"},
                        {"name": "score", "type": "number"}
                    ]
                },
                {
                    "name": "dermatology",
                    "fields": [
                        {"name": "id", "type": "string"}
                    ]
                }
            ]
        })))
        .mount(&server)
        .await;

    let tmp = tempfile::tempdir().unwrap();
    let config_path = write_config(tmp.path(), &server.uri());
    let tmp_path = tmp.path().to_path_buf();

    tokio::task::spawn_blocking(move || {
        Command::cargo_bin("braze-sync")
            .unwrap()
            .env("BRAZE_API_KEY", "test-key")
            .args(["--config", config_path.to_str().unwrap()])
            .args(["export", "--resource", "catalog_schema"])
            .assert()
            .success();
    })
    .await
    .unwrap();

    let cardiology = tmp_path.join("catalogs/cardiology/schema.yaml");
    let dermatology = tmp_path.join("catalogs/dermatology/schema.yaml");
    assert!(cardiology.exists(), "cardiology schema should exist");
    assert!(dermatology.exists(), "dermatology schema should exist");

    let content = fs::read_to_string(&cardiology).unwrap();
    assert!(content.contains("name: cardiology"));
    assert!(content.contains("- name: id"));
    assert!(content.contains("- name: score"));
    assert!(content.starts_with("# Generated by braze-sync."));
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn export_with_name_filter_uses_get_endpoint() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/catalogs/cardiology"))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
            "catalogs": [
                {"name": "cardiology", "fields": [{"name": "id", "type": "string"}]}
            ]
        })))
        .mount(&server)
        .await;

    let tmp = tempfile::tempdir().unwrap();
    let config_path = write_config(tmp.path(), &server.uri());
    let tmp_path = tmp.path().to_path_buf();

    tokio::task::spawn_blocking(move || {
        Command::cargo_bin("braze-sync")
            .unwrap()
            .env("BRAZE_API_KEY", "test-key")
            .args(["--config", config_path.to_str().unwrap()])
            .args([
                "export",
                "--resource",
                "catalog_schema",
                "--name",
                "cardiology",
            ])
            .assert()
            .success();
    })
    .await
    .unwrap();

    assert!(
        tmp_path.join("catalogs/cardiology/schema.yaml").exists(),
        "cardiology schema should exist after --name export"
    );
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn unauthorized_braze_response_yields_exit_code_4() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/catalogs"))
        .respond_with(ResponseTemplate::new(401).set_body_string("invalid api key"))
        .mount(&server)
        .await;

    let tmp = tempfile::tempdir().unwrap();
    let config_path = write_config(tmp.path(), &server.uri());

    tokio::task::spawn_blocking(move || {
        Command::cargo_bin("braze-sync")
            .unwrap()
            .env("BRAZE_API_KEY", "wrong-key")
            .args(["--config", config_path.to_str().unwrap()])
            .args(["export", "--resource", "catalog_schema"])
            .assert()
            .failure()
            .code(4);
    })
    .await
    .unwrap();
}

#[test]
fn missing_config_file_yields_exit_code_3() {
    Command::cargo_bin("braze-sync")
        .unwrap()
        .env("BRAZE_API_KEY", "anything")
        .args(["--config", "/nonexistent/braze-sync.config.yaml"])
        .args(["export"])
        .assert()
        .failure()
        .code(3);
}

#[test]
fn invalid_args_name_without_resource_yields_exit_code_3() {
    // clap rejects --name without --resource at parse time. We expect
    // exit 3 (config / argument error).
    Command::cargo_bin("braze-sync")
        .unwrap()
        .args(["export", "--name", "x"])
        .assert()
        .failure()
        .code(3);
}

#[test]
fn help_flag_exits_zero() {
    Command::cargo_bin("braze-sync")
        .unwrap()
        .arg("--help")
        .assert()
        .success();
}