#![expect(clippy::expect_used, reason = "test harness: a panic here names the setup or assertion failure precisely")]
#[path = "behavior/common/mod.rs"]
mod common;
use common::*;
use backbone_mailing::application::service::ab_test_write_service::AbTestWriteService;
use backbone_mailing::application::service::mailing_write_service::{
MailingUpsertCommand, MailingWriteService,
};
use uuid::Uuid;
fn variant_cmd(audience_id: Uuid, campaign_id: Uuid, subject: &str) -> MailingUpsertCommand {
MailingUpsertCommand {
subject: subject.into(),
preview: None,
body_html: format!("<p>{subject}</p>"),
email_from: "ab@example.id".into(),
reply_to: None,
mailing_domain_raw: serde_json::json!([
["mailing_audience_id", "=", audience_id.to_string()]
]),
target_model: "mailing_contact".into(),
schedule_type: "immediate".into(),
schedule_date: None,
use_exclusion_list: true,
campaign_id: Some(campaign_id),
source_id: None,
ab_test_id: None,
ab_testing_enabled: false,
ab_testing_pc: 0,
}
}
#[tokio::test]
async fn the_sampling_seed_is_minted_once_and_persists() {
let Some(db) = TestDb::new("seed").await else {
return skipped("seed");
};
let svc = AbTestWriteService::new(db.pool.clone());
let campaign = Uuid::new_v4();
let far = chrono::Utc::now() + chrono::Duration::hours(1);
let id = svc
.create_ab_test(campaign, "opened_ratio", Some(far))
.await
.expect("create");
let view = svc.view(id).await.expect("view");
assert_eq!(view.sampling_seed.len(), 32, "128 bits as 32 hex chars");
assert!(view.sampling_seed.chars().all(|c| c.is_ascii_hexdigit()));
let again = svc.view(id).await.expect("view again");
assert_eq!(view.sampling_seed, again.sampling_seed);
let err = svc
.create_ab_test(campaign, "clicks_ratio", Some(far))
.await
.expect_err("second test must refuse");
assert_eq!(err.http_status(), 409);
db.dispose().await;
}
#[tokio::test]
async fn variants_partition_the_audience_with_no_overlap() {
let Some(db) = TestDb::new("split").await else {
return skipped("split");
};
let (audience_id, emails) = seed_audience(&db.pool, "split", 200).await;
let campaign = Uuid::new_v4();
let ab = AbTestWriteService::new(db.pool.clone());
let engine = MailingWriteService::new(db.pool.clone());
let far = chrono::Utc::now() + chrono::Duration::hours(1);
let test_id = ab
.create_ab_test(campaign, "opened_ratio", Some(far))
.await
.expect("test");
let a = engine
.create_mailing(&variant_cmd(audience_id, campaign, "Variant A"))
.await
.expect("A");
let b = engine
.create_mailing(&variant_cmd(audience_id, campaign, "Variant B"))
.await
.expect("B");
ab.bind_variant(a, test_id, 50).await.expect("bind A");
ab.bind_variant(b, test_id, 100).await.expect("bind B");
engine.launch(a, "immediate", None).await.expect("launch A");
let first = engine.send_queue_sweep().await.expect("sweep A");
assert_eq!(first.claimed, 1);
assert_eq!(first.completed, 1);
engine.launch(b, "immediate", None).await.expect("launch B");
let second = engine.send_queue_sweep().await.expect("sweep B");
assert_eq!(second.claimed, 1);
assert_eq!(second.completed, 1);
let per_recipient: Vec<(String, i64)> = sqlx::query_as(
r#"SELECT lower(recipient_email), count(*) FROM mailing.mailing_traces
WHERE trace_status <> 'cancel' AND (metadata->>'deleted_at') IS NULL
GROUP BY 1"#,
)
.fetch_all(&db.pool)
.await
.expect("grouping");
assert!(
per_recipient.iter().all(|(_, n)| *n == 1),
"no recipient carries two variant traces"
);
assert_eq!(
per_recipient.len(),
emails.len(),
"the fragments cover the whole audience, disjointly"
);
let a_traces: i64 = sqlx::query_scalar(
"SELECT count(*) FROM mailing.mailing_traces WHERE mailing_id = $1",
)
.bind(a)
.fetch_one(&db.pool)
.await
.expect("A traces");
let b_traces: i64 = sqlx::query_scalar(
"SELECT count(*) FROM mailing.mailing_traces WHERE mailing_id = $1",
)
.bind(b)
.fetch_one(&db.pool)
.await
.expect("B traces");
assert!(
(70..=130).contains(&a_traces),
"variant A fragment was {a_traces}"
);
assert_eq!(b_traces, 200 - a_traces, "B gets exactly the remainder");
db.dispose().await;
}
#[tokio::test]
async fn due_test_promotes_the_ranked_winner_idempotently() {
let Some(db) = TestDb::new("promo").await else {
return skipped("promo");
};
let (audience_id, emails) = seed_audience(&db.pool, "promo", 40).await;
let campaign = Uuid::new_v4();
let ab = AbTestWriteService::new(db.pool.clone());
let engine = MailingWriteService::new(db.pool.clone());
let future = chrono::Utc::now() + chrono::Duration::hours(1);
let test_id = ab
.create_ab_test(campaign, "opened_ratio", Some(future))
.await
.expect("test");
let a = engine
.create_mailing(&variant_cmd(audience_id, campaign, "Winner bait"))
.await
.expect("A");
let b = engine
.create_mailing(&variant_cmd(audience_id, campaign, "Loser bait"))
.await
.expect("B");
ab.bind_variant(a, test_id, 100).await.expect("bind A");
ab.bind_variant(b, test_id, 0).await.expect("bind B");
engine.launch(a, "immediate", None).await.expect("launch A");
engine.launch(b, "immediate", None).await.expect("launch B");
engine.send_queue_sweep().await.expect("sweep 1");
sqlx::query(
r#"UPDATE mailing.mailing_traces SET trace_status = 'open',
open_datetime = now(), sent_datetime = now()
WHERE mailing_id = $1"#,
)
.bind(a)
.execute(&db.pool)
.await
.expect("opens");
sqlx::query("UPDATE mailing.mailing_ab_tests SET promote_at = now() - interval '1 minute' WHERE id = $1")
.bind(test_id)
.execute(&db.pool)
.await
.expect("backdate promote_at");
let out = engine.send_queue_sweep().await.expect("sweep 2");
assert_eq!(out.ab_promotions, 1, "the due test promotes its winner");
let view = ab.view(test_id).await.expect("view");
assert!(view.completed);
assert_eq!(view.winner_mailing_id, Some(a), "opened_ratio ranks A first");
let promoted: Vec<(Uuid, String, bool, i32)> = sqlx::query_as(
r#"SELECT id, state::text, ab_testing_enabled, ab_testing_pc
FROM mailing.mailings
WHERE ab_test_id = $1 AND NOT ab_testing_enabled"#,
)
.bind(test_id)
.fetch_all(&db.pool)
.await
.expect("promoted row");
assert_eq!(promoted.len(), 1);
assert_eq!(promoted[0].1, "in_queue");
assert!(!promoted[0].2, "the copy does not sample again");
let third = engine.send_queue_sweep().await.expect("sweep 3");
assert_eq!(third.ab_promotions, 0);
assert_eq!(third.claimed, 1, "the winner copy is driven");
assert_eq!(
third.minted, 0,
"the winner copy re-sends nobody the campaign already covered"
);
assert_eq!(third.completed, 1);
let _ = emails;
db.dispose().await;
}
#[tokio::test]
async fn manual_tests_wait_for_a_human_winner() {
let Some(db) = TestDb::new("manual").await else {
return skipped("manual");
};
let (audience_id, _) = seed_audience(&db.pool, "manual", 10).await;
let campaign = Uuid::new_v4();
let ab = AbTestWriteService::new(db.pool.clone());
let engine = MailingWriteService::new(db.pool.clone());
let test_id = ab
.create_ab_test(campaign, "manual", None)
.await
.expect("manual test");
let a = engine
.create_mailing(&variant_cmd(audience_id, campaign, "Manual A"))
.await
.expect("A");
ab.bind_variant(a, test_id, 100).await.expect("bind");
engine.launch(a, "immediate", None).await.expect("launch");
engine.send_queue_sweep().await.expect("sweep");
sqlx::query("UPDATE mailing.mailing_ab_tests SET promote_at = now() - interval '1 hour' WHERE id = $1")
.bind(test_id)
.execute(&db.pool)
.await
.expect("backdate");
let out = engine.send_queue_sweep().await.expect("sweep 2");
assert_eq!(out.ab_promotions, 0, "manual selection is human-only");
let promoted = ab.select_winner(test_id, a).await.expect("select");
assert!(promoted.is_some());
let err = ab
.select_winner(test_id, a)
.await
.expect_err("second selection refuses");
assert_eq!(err.http_status(), 409);
db.dispose().await;
}