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() {
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();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn export_content_blocks_writes_liquid_files() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/content_blocks/list"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"content_blocks": [
{"content_block_id": "id-promo", "name": "promo"},
{"content_block_id": "id-header", "name": "shared_header"}
],
"message": "success"
})))
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/content_blocks/info"))
.and(wiremock::matchers::query_param(
"content_block_id",
"id-promo",
))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"content_block_id": "id-promo",
"name": "promo",
"description": "Promo banner",
"content": "Hello {{ user.${first_name} }}",
"tags": ["pr"],
"message": "success"
})))
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/content_blocks/info"))
.and(wiremock::matchers::query_param(
"content_block_id",
"id-header",
))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"content_block_id": "id-header",
"name": "shared_header",
"content": "<header>shared</header>",
"tags": [],
"message": "success"
})))
.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", "content_block"])
.assert()
.success();
})
.await
.unwrap();
let promo = tmp_path.join("content_blocks/promo.liquid");
let header = tmp_path.join("content_blocks/shared_header.liquid");
assert!(promo.exists(), "promo.liquid should exist");
assert!(header.exists(), "shared_header.liquid should exist");
let promo_text = fs::read_to_string(&promo).unwrap();
assert!(promo_text.starts_with("---\n"));
assert!(promo_text.contains("name: promo"));
assert!(promo_text.contains("description: Promo banner"));
assert!(promo_text.contains("Hello {{ user.${first_name} }}"));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn export_content_block_with_name_filter_only_fetches_matching_info() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/content_blocks/list"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"content_blocks": [
{"content_block_id": "id-promo", "name": "promo"},
{"content_block_id": "id-header", "name": "shared_header"}
]
})))
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/content_blocks/info"))
.and(wiremock::matchers::query_param(
"content_block_id",
"id-header",
))
.respond_with(ResponseTemplate::new(500))
.expect(0)
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/content_blocks/info"))
.and(wiremock::matchers::query_param(
"content_block_id",
"id-promo",
))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"name": "promo",
"content": "x",
"tags": []
})))
.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", "content_block", "--name", "promo"])
.assert()
.success();
})
.await
.unwrap();
assert!(tmp_path.join("content_blocks/promo.liquid").exists());
assert!(!tmp_path
.join("content_blocks/shared_header.liquid")
.exists());
}