crates/monetize-server/src/signup/tests.rs
//! The store, the checks and the engine, on the host. The webhook and the gRPC
//! service are driven over their real wires in `tests/paid_signup.rs`.
use std::collections::BTreeMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use monetize_pay::{Currency, Money};
use super::*;
use crate::config::SignupPlan;
pub const PLAN: &str = "gunnar/10gb-1m";
/// The plan table a test deployment sells, in the shape `monetize.toml` writes:
/// `[signups.gunnar.plans."gunnar/10gb-1m"] amount_minor = 10000, currency = "Sek"`.
pub fn plans() -> BTreeMap<String, SignupPlan> {
BTreeMap::from([(
PLAN.to_string(),
SignupPlan {
amount_minor: 10_000,
currency: Currency::Sek,
cap_bytes: 10_737_418_240,
period_days: 30,
},
)])
}
pub fn a_signup(session: &str, username: &str) -> PaidSignup {
PaidSignup {
vendor: "mockpay".into(),
session: session.into(),
reference: "pi_1".into(),
email: "bob@example.com".into(),
username: username.into(),
product: "gunnar".into(),
plan: PLAN.into(),
amount: Money {
minor: 10_000,
currency: Currency::Sek,
},
// No tax registration when this fixture was written: the vendor took the
// plan price and nothing on top, so gross and net are the same number.
tax: Money {
minor: 0,
currency: Currency::Sek,
},
paid_at_unix_ms: 1_788_436_800_000,
customer_country: None,
}
}
/// A sink that counts, so "created once" is a measured number and not a guess.
pub struct CountingSink {
/// **What `name_verdict` says**, and it is a list of names this sink holds
/// rather than a flag: the whole question `Signup.CheckName` and
/// `Signup.Rename` ask is about ONE name, and a sink that refused every name
/// or none could not show that a rename to a free one works.
pub taken: Mutex<Vec<String>>,
/// Whether the sink can even be ASKED. A name check that cannot reach the
/// product is not "taken", and the tests hold that apart.
pub reachable: Mutex<bool>,
pub calls: AtomicU64,
pub answer: Mutex<Result<SignupOutcome, SignupError>>,
}
impl CountingSink {
pub fn creating() -> Arc<CountingSink> {
Arc::new(CountingSink {
taken: Mutex::new(Vec::new()),
reachable: Mutex::new(true),
calls: AtomicU64::new(0),
answer: Mutex::new(Ok(SignupOutcome::Created {
namespace: "bob".into(),
username: "bob".into(),
})),
})
}
pub fn answering(a: Result<SignupOutcome, SignupError>) -> Arc<CountingSink> {
Arc::new(CountingSink {
taken: Mutex::new(Vec::new()),
reachable: Mutex::new(true),
calls: AtomicU64::new(0),
answer: Mutex::new(a),
})
}
/// This deployment already has these names.
pub fn holding(names: &[&str]) -> Arc<CountingSink> {
let sink = CountingSink::creating();
*sink.taken.lock().unwrap() = names.iter().map(|n| (*n).to_owned()).collect();
sink
}
}
impl SignupSink for CountingSink {
fn paid(&self, _s: &PaidSignup) -> Result<SignupOutcome, SignupError> {
self.calls.fetch_add(1, Ordering::SeqCst);
self.answer.lock().unwrap().clone()
}
fn name_verdict(&self, username: &str) -> Result<NameVerdict, SignupError> {
if !*self.reachable.lock().unwrap() {
return Err(SignupError::Unavailable(
"the product is not answering".into(),
));
}
if self.taken.lock().unwrap().iter().any(|n| n == username) {
return Ok(NameVerdict::Taken(format!(
"username {username:?} is taken on this appliance"
)));
}
Ok(NameVerdict::Free)
}
}
/// An `AppState` whose clock a test can move, with the sink and the plans it asks for.
pub fn state_with(sink: Arc<dyn SignupSink>, now: Arc<AtomicU64>) -> Arc<crate::state::AppState> {
let mut state = crate::state::AppState::in_memory(crate::registry::Registry::new());
state.signup_sink = sink;
state.signup_plans = plans().into();
let tick = Arc::clone(&now);
state.clock = Box::new(move || tick.load(Ordering::SeqCst));
Arc::new(state)
}
#[test]
fn the_second_webhook_for_a_session_creates_nothing_and_answers_what_the_first_did() {
let sink = CountingSink::creating();
let now = Arc::new(AtomicU64::new(1_788_436_800_000));
let state = state_with(sink.clone(), now);
let s = a_signup("cs_1", "bob");
let first = settle(&state, &s);
assert!(
matches!(&first, SignupState::Created { username, namespace, .. } if username == "bob" && namespace == "bob"),
"{first:?}"
);
// The vendor retries. It must be a no-op, and it must still say Created.
let second = settle(&state, &s);
assert_eq!(first, second, "the retry answered something else");
assert_eq!(
sink.calls.load(Ordering::SeqCst),
1,
"the sink ran twice for one payment"
);
assert_eq!(
state.signups.list(0).unwrap().len(),
1,
"one payment, one row"
);
}
#[test]
fn a_username_that_is_not_a_name_is_refused_before_the_sink_is_asked() {
let sink = CountingSink::creating();
let now = Arc::new(AtomicU64::new(2));
let state = state_with(sink.clone(), now);
let out = settle(&state, &a_signup("cs_2", "Bob!"));
let SignupState::Refused { reason, .. } = out else {
panic!("{out:?}")
};
assert!(reason.contains("Bob!"), "{reason}");
assert!(
reason.contains(monetize_pay::signup::USERNAME_RULE),
"{reason}"
);
assert_eq!(
sink.calls.load(Ordering::SeqCst),
0,
"a refused name still reached the sink"
);
}
#[test]
fn a_plan_the_config_does_not_name_is_refused_by_name() {
let sink = CountingSink::creating();
let state = state_with(sink.clone(), Arc::new(AtomicU64::new(3)));
let mut s = a_signup("cs_3", "bob");
s.plan = "gunnar/1tb-forever".into();
let out = settle(&state, &s);
let SignupState::Refused { reason, .. } = out else {
panic!("{out:?}")
};
assert!(reason.contains("gunnar/1tb-forever"), "{reason}");
assert!(
reason.contains(PLAN),
"the refusal did not say what IS sold: {reason}"
);
assert_eq!(sink.calls.load(Ordering::SeqCst), 0);
}
#[test]
fn an_amount_that_is_not_the_plans_price_is_refused_with_both_numbers() {
let sink = CountingSink::creating();
let state = state_with(sink.clone(), Arc::new(AtomicU64::new(4)));
let mut s = a_signup("cs_4", "bob");
s.amount = Money {
minor: 1,
currency: Currency::Sek,
};
let out = settle(&state, &s);
let SignupState::Refused { reason, .. } = out else {
panic!("{out:?}")
};
assert!(
reason.contains("100.00 SEK") && reason.contains("paid 0.01 SEK"),
"both amounts, in kronor: {reason}"
);
assert_eq!(sink.calls.load(Ordering::SeqCst), 0);
// The same number in the wrong currency is a mismatch too, never converted.
let mut s = a_signup("cs_5", "bob");
s.amount = Money {
minor: 10_000,
currency: Currency::Eur,
};
let out = settle(&state, &s);
assert!(matches!(out, SignupState::Refused { .. }), "{out:?}");
}
/// **The refusal that would have taken a customer's money and given them
/// nothing.** A checkout switched to collect 25 % VAT hands up a total a quarter
/// larger than the plan price. Measured against the GROSS that reads as a page
/// whose price drifted — and `SignupError::Refused` is final and never retried,
/// so the charge stands and no account is ever made. The plan price is a NET
/// price, and the net is what it is compared against.
#[test]
fn a_signup_that_collected_vat_is_measured_on_its_net_and_not_its_total() {
let plans = plans();
// 100,00 SEK + 25 % moms: 125,00 taken, 25,00 of it the state's.
let mut taxed = a_signup("cs_vat", "bob");
taxed.amount = Money {
minor: 12_500,
currency: Currency::Sek,
};
taxed.tax = Money {
minor: 2_500,
currency: Currency::Sek,
};
assert_eq!(
refusal(&plans, &taxed),
None,
"the net is the plan price; the tax was never the deployment's"
);
// The drift check still bites underneath the tax: 90,00 + moms is still a
// page whose price drifted, and the refusal names the net it measured.
let mut drifted = a_signup("cs_drift", "bob");
drifted.amount = Money {
minor: 11_250,
currency: Currency::Sek,
};
drifted.tax = Money {
minor: 2_250,
currency: Currency::Sek,
};
let (code, reason) =
refusal(&plans, &drifted).expect("a net of 90.00 SEK is not the plan's 100.00 SEK");
assert!(
reason.contains("90.00 SEK"),
"the refusal names the net it measured, in kronor: {reason}"
);
assert_eq!(
code,
Refusal::Final,
"a drifted price is not something another username fixes"
);
}
/// **Every amount in the refusal is in major units with its currency**
/// (owner, live 2026-10-12: the console read "costs 11000 SEK here and the
/// vendor took 12500 SEK" for a 125.00 SEK payment — öre printed as kronor).
#[test]
fn a_price_refusal_says_every_amount_in_kronor_never_in_ore() {
let mut plans = plans();
plans.get_mut("gunnar/10gb-1m").unwrap().amount_minor = 11_000;
let mut kicka = a_signup("cs_kicka", "kicka");
kicka.amount = Money {
minor: 12_500,
currency: Currency::Sek,
};
kicka.tax = Money {
minor: 2_500,
currency: Currency::Sek,
};
let (_, reason) = refusal(&plans, &kicka).expect("a net of 100.00 is not the plan's 110.00");
assert_eq!(
reason,
"paid 125.00 SEK (25.00 SEK of it tax, 100.00 SEK net), but plan \"gunnar/10gb-1m\" costs 110.00 SEK here",
"major units, the currency beside each figure"
);
for ore in ["11000", "12500", "2500", "10000"] {
assert!(
!reason.contains(ore),
"no öre figure in the words: {reason}"
);
}
}
/// The rows written before the fix are re-said in kronor when the console
/// reads them; any other reason is untouched.
#[test]
fn a_price_refusal_stored_in_ore_is_shown_in_kronor() {
let old = "plan \"gunnar/10gb-1m\" costs 11000 SEK here and the vendor took 12500 SEK (2500 of it tax), a net of 10000 SEK";
assert_eq!(
crate::signup::legacy_price_refusal(old).as_deref(),
Some("paid 125.00 SEK (25.00 SEK of it tax, 100.00 SEK net), but plan \"gunnar/10gb-1m\" costs 110.00 SEK here")
);
assert_eq!(
crate::signup::legacy_price_refusal("username \"Bob!\" contains '!'"),
None
);
}
#[test]
fn a_sink_that_refuses_by_name_stores_the_name_and_is_not_retried() {
let sink =
CountingSink::answering(Err(SignupError::Refused("gunnar already has a bob".into())));
let state = state_with(sink.clone(), Arc::new(AtomicU64::new(5)));
let out = settle(&state, &a_signup("cs_6", "bob"));
let SignupState::Refused { reason, .. } = out else {
panic!("{out:?}")
};
assert_eq!(reason, "gunnar already has a bob");
// A refusal is final: the poll reads the row and calls nobody.
let again = status_with_retry(&state, "mockpay", "cs_6")
.unwrap()
.unwrap();
assert_eq!(
again.state,
SignupState::Refused {
reason: "gunnar already has a bob".into(),
at_unix_ms: 5,
refusal: Refusal::Final,
words: Some(words::COULD_NOT_CREATE.into()),
owed: Owed::RefundAndTheNameMayBeSpent,
}
);
assert_eq!(sink.calls.load(Ordering::SeqCst), 1);
}
#[test]
fn unavailable_keeps_it_pending_and_the_next_poll_tries_again() {
let sink = CountingSink::answering(Err(SignupError::Unavailable(
"gunnar is not answering".into(),
)));
let now = Arc::new(AtomicU64::new(1_000));
let state = state_with(sink.clone(), Arc::clone(&now));
let s = a_signup("cs_7", "bob");
assert_eq!(settle(&state, &s), SignupState::Pending);
// The return page polls. That is what drives the retry — nothing else will.
let polled = status_with_retry(&state, "mockpay", "cs_7")
.unwrap()
.unwrap();
assert_eq!(polled.state, SignupState::Pending);
assert_eq!(
sink.calls.load(Ordering::SeqCst),
2,
"the poll did not retry"
);
// The product comes back. The next poll creates the customer, through the
// same code the webhook would have.
*sink.answer.lock().unwrap() = Ok(SignupOutcome::Created {
namespace: "bob".into(),
username: "bob".into(),
});
now.store(2_000, Ordering::SeqCst);
let polled = status_with_retry(&state, "mockpay", "cs_7")
.unwrap()
.unwrap();
assert_eq!(
polled.state,
SignupState::Created {
namespace: "bob".into(),
username: "bob".into(),
at_unix_ms: 2_000
}
);
// And it stayed created: the poll after that calls nobody.
let calls = sink.calls.load(Ordering::SeqCst);
status_with_retry(&state, "mockpay", "cs_7").unwrap();
assert_eq!(sink.calls.load(Ordering::SeqCst), calls);
}
#[test]
fn a_signup_pending_for_a_day_becomes_a_named_refusal() {
let sink = CountingSink::answering(Err(SignupError::Unavailable(
"gunnar is not answering".into(),
)));
let now = Arc::new(AtomicU64::new(1_000));
let state = state_with(sink, Arc::clone(&now));
assert_eq!(
settle(&state, &a_signup("cs_8", "bob")),
SignupState::Pending
);
// A minute short of the ceiling it is still a retry.
now.store(1_000 + PENDING_CEILING_MS as u64 - 60_000, Ordering::SeqCst);
assert!(status_with_retry(&state, "mockpay", "cs_8")
.unwrap()
.unwrap()
.state
.is_pending());
// Past it, the customer is owed an answer, and it names why.
now.store(1_000 + PENDING_CEILING_MS as u64, Ordering::SeqCst);
let out = status_with_retry(&state, "mockpay", "cs_8")
.unwrap()
.unwrap();
let SignupState::Refused { reason, .. } = out.state else {
panic!("{out:?}")
};
assert!(reason.starts_with(NEVER_ANSWERED), "{reason}");
assert!(
reason.contains("gunnar is not answering"),
"the outage's own words were dropped: {reason}"
);
}
#[test]
fn a_session_nobody_has_seen_is_absent_and_not_an_error() {
let state = state_with(CountingSink::creating(), Arc::new(AtomicU64::new(9)));
assert_eq!(
status_with_retry(&state, "mockpay", "cs_never").unwrap(),
None
);
}
#[test]
fn the_loud_sink_creates_nothing_and_answers_created() {
let s = a_signup("cs_10", "bob");
let out = LoudSink.paid(&s).expect("the open sink answers");
assert_eq!(
out,
SignupOutcome::Created {
namespace: "bob".into(),
username: "bob".into()
}
);
}
#[test]
fn the_store_lists_newest_first_and_keeps_the_email_off_nothing_but_itself() {
let state = state_with(CountingSink::creating(), Arc::new(AtomicU64::new(100)));
settle(&state, &a_signup("cs_a", "bob"));
let rows = state.signups.list(0).unwrap();
assert_eq!(rows.len(), 1);
// The record keeps it, because a retry needs it. What goes on the wire is
// `services::signup`'s business and is asserted there.
assert_eq!(rows[0].signup.email, "bob@example.com");
assert_eq!(rows[0].recorded_at_unix_ms, 100);
}
/// ★ **A settled paid signup is a sale the tax return counts — exactly once.**
/// 125.00 SEK taken incl. 25 % moms on 2026-09-15: 100.00 net and 25.00 VAT in
/// 2026-Q3, however many times the vendor and the return page hand it up.
#[test]
fn a_created_signup_is_one_tax_sale_in_its_quarter_and_a_replay_adds_nothing() {
let state = state_with(
CountingSink::creating(),
Arc::new(AtomicU64::new(1_789_468_200_000)),
);
let mut paid = a_signup("cs_live_q3", "bob");
paid.amount = Money {
minor: 12_500,
currency: Currency::Sek,
};
paid.tax = Money {
minor: 2_500,
currency: Currency::Sek,
};
paid.paid_at_unix_ms = 1_789_468_154_000; // 2026-09-15T10:29:14Z
assert!(matches!(settle(&state, &paid), SignupState::Created { .. }));
// The replay: a second webhook, a return-page poll.
settle(&state, &paid);
assert!(matches!(
status_with_retry(&state, "mockpay", "cs_live_q3")
.unwrap()
.unwrap()
.state,
SignupState::Created { .. }
));
// A refused signup is money owed back, not a sale.
settle(&state, &a_signup("cs_refused", "Bob!"));
let books = monetize::Books {
home_country: "SE".into(),
supplier_countries: BTreeMap::new(),
};
let sales = tax_sales(&state.signups, &books).unwrap();
assert_eq!(sales.len(), 1, "one payment, one sale: {sales:?}");
let s = &sales[0];
assert_eq!(s.reference, "signup:mockpay:cs_live_q3");
assert_eq!(
(
s.net.minor,
s.tax.minor,
s.basis_points,
s.customer_country.as_str()
),
(10_000, 2_500, 2_500, "SE")
);
let view = monetize::tax::LedgerView {
home_country: "SE".into(),
sales,
};
let quarters = monetize::tax::PeriodKind::Quarterly.periods(2026);
assert_eq!(view.in_period(&quarters[2]).count(), 1, "2026-Q3 holds it");
assert_eq!(
quarters
.iter()
.map(|q| view.in_period(q).count())
.sum::<usize>(),
1,
"and no other quarter does"
);
}
/// A sink that creates and records every welcome resend it is asked for.
struct MailSink {
resent: Mutex<Vec<(String, String, bool)>>,
}
impl SignupSink for MailSink {
fn paid(&self, s: &PaidSignup) -> Result<SignupOutcome, SignupError> {
// The product settles on a name that differs from the one asked for,
// so the test can see which one the welcome is sent with.
Ok(SignupOutcome::Created {
namespace: format!("{}-ns", s.username),
username: format!("{}-made", s.username),
})
}
fn name_verdict(&self, _: &str) -> Result<NameVerdict, SignupError> {
Ok(NameVerdict::Free)
}
fn resend_welcome(&self, s: &PaidSignup, force: bool) -> Result<String, SignupError> {
self.resent
.lock()
.unwrap()
.push((s.username.clone(), s.email.clone(), force));
Ok(format!("sent to {}", s.email))
}
}
/// **The resend door: only a CREATED signup, with the name the product made.**
#[test]
fn resend_welcome_reaches_the_sink_only_for_a_created_signup_with_its_settled_name() {
let sink = Arc::new(MailSink {
resent: Mutex::new(Vec::new()),
});
let state = state_with(sink.clone(), Arc::new(AtomicU64::new(100)));
assert_eq!(
resend_welcome(&state, "mockpay", "cs_none", false),
Err(ResendRefusal::Unknown)
);
settle(&state, &a_signup("cs_ok", "bob"));
let out = resend_welcome(&state, "mockpay", "cs_ok", true)
.expect("a created signup can be welcomed again");
assert_eq!(out, "sent to bob@example.com");
assert_eq!(
*sink.resent.lock().unwrap(),
vec![("bob-made".to_owned(), "bob@example.com".to_owned(), true)],
"the settled name, the stored address, the operator's force"
);
// Refused over the shape rule: nothing was created, nothing to welcome.
settle(&state, &a_signup("cs_bad", "Bob!"));
assert!(
matches!(resend_welcome(&state, "mockpay", "cs_bad", false), Err(ResendRefusal::NotSendable(why)) if why.contains("not CREATED"))
);
assert_eq!(
sink.resent.lock().unwrap().len(),
1,
"the sink was asked about a signup with no account"
);
}
/// The open sink sends no mail and says so by name.
#[test]
fn the_default_resend_is_a_refusal_by_name_never_a_quiet_sent() {
let state = state_with(CountingSink::creating(), Arc::new(AtomicU64::new(100)));
settle(&state, &a_signup("cs_ok", "bob"));
assert!(
matches!(resend_welcome(&state, "mockpay", "cs_ok", false), Err(ResendRefusal::NotSendable(why)) if why.contains("sends no welcome mail"))
);
}
#[test]
fn refusal_checks_the_three_assertions_a_stranger_made() {
let plans = plans();
assert_eq!(refusal(&plans, &a_signup("cs", "bob")), None);
assert!(refusal(&BTreeMap::new(), &a_signup("cs", "bob"))
.unwrap()
.1
.contains("not sold here"));
}
/// ★ **The refusal says whether another name would fix it, as a VALUE.**
///
/// The page that has to choose between offering one text field and offering the
/// support address reads this, and nothing else. Before it existed the only
/// evidence was the prose, which is a thing somebody improves.
#[test]
fn a_refusal_records_whether_another_name_would_fix_it() {
// The name: the shape rule, and a sink that says the name is held.
let state = state_with(CountingSink::creating(), Arc::new(AtomicU64::new(1)));
let out = settle(&state, &a_signup("cs_shape", "Bob!"));
assert!(
matches!(
out,
SignupState::Refused {
refusal: Refusal::Name,
..
}
),
"{out:?}"
);
let taken = CountingSink::answering(Err(SignupError::NameTaken(
"username \"bob\" is taken on this appliance".into(),
)));
let state = state_with(taken, Arc::new(AtomicU64::new(1)));
let out = settle(&state, &a_signup("cs_taken", "bob"));
assert!(
matches!(
out,
SignupState::Refused {
refusal: Refusal::Name,
..
}
),
"{out:?}"
);
// The order: a plan nobody sells. Nothing the customer types changes it.
let state = state_with(CountingSink::creating(), Arc::new(AtomicU64::new(1)));
let mut s = a_signup("cs_plan", "bob");
s.plan = "gunnar/1tb-forever".into();
let out = settle(&state, &s);
assert!(
matches!(
out,
SignupState::Refused {
refusal: Refusal::Final,
..
}
),
"{out:?}"
);
// And a sink that refuses for its own reasons is final too.
let state = state_with(
CountingSink::answering(Err(SignupError::Refused(
"gunnar refused the entitlement".into(),
))),
Arc::new(AtomicU64::new(1)),
);
let out = settle(&state, &a_signup("cs_ent", "bob"));
assert!(
matches!(
out,
SignupState::Refused {
refusal: Refusal::Final,
..
}
),
"{out:?}"
);
assert!(Refusal::Name.renameable());
assert!(!Refusal::Final.renameable());
// A row written before the field existed. Not renameable: nobody knows what
// it was about, and re-running a sink on a guess spends somebody's money.
assert!(!Refusal::Unknown.renameable());
}
/// **A row this store wrote before the refusal field existed still parses**, and
/// reads as `Unknown`. The table IS the idempotency key: a store that refused
/// its own history would run a sink again for a payment already answered.
#[test]
fn a_refused_row_from_before_the_field_existed_reads_as_unknown() {
let old = serde_json::json!({ "Refused": { "reason": "username \"Bob!\" contains '!'", "at_unix_ms": 7 } });
let state: SignupState = serde_json::from_value(old).expect("the old shape still parses");
assert!(
matches!(
state,
SignupState::Refused {
refusal: Refusal::Unknown,
..
}
),
"{state:?}"
);
}
/// ★ **The race, closed without a refund.** A name free when it was checked,
/// taken by the time the card cleared: the customer types another one and the
/// SAME money makes the account.
#[test]
fn a_signup_refused_over_a_taken_name_is_finished_by_another_name() {
// The appliance already has a bob, and says so the way a real sink does.
let sink = CountingSink::holding(&["bob"]);
*sink.answer.lock().unwrap() = Err(SignupError::NameTaken(
"username \"bob\" is taken on this appliance".into(),
));
let state = state_with(sink.clone(), Arc::new(AtomicU64::new(10)));
let first = settle(&state, &a_signup("cs_r", "bob"));
assert!(
matches!(
first,
SignupState::Refused {
refusal: Refusal::Name,
..
}
),
"{first:?}"
);
// The customer types another one, and the same money makes the account.
*sink.answer.lock().unwrap() = Ok(SignupOutcome::Created {
namespace: "bob2".into(),
username: "bob2".into(),
});
let out = rename(&state, "mockpay", "cs_r", "bob2").expect("a free name finishes it");
assert!(matches!(out.state, SignupState::Created { .. }), "{out:?}");
// **The row carries the new name**, or the console shows an operator a name
// nobody has and a retry asks the sink for it again.
assert_eq!(out.signup.username, "bob2");
assert_eq!(
state
.signups
.get("mockpay", "cs_r")
.unwrap()
.unwrap()
.signup
.username,
"bob2"
);
// The money did not move: the record is the same payment, same session.
assert_eq!(out.signup.reference, "pi_1");
assert_eq!(
out.recorded_at_unix_ms, 10,
"the rename rewrote when the webhook landed"
);
}
/// **A second name that is also taken is the ordinary case**, and it leaves the
/// record renameable so the page can simply ask again.
#[test]
fn a_rename_to_a_name_that_is_also_taken_is_refused_and_asks_again() {
let sink = CountingSink::holding(&["bob", "bob2"]);
*sink.answer.lock().unwrap() = Err(SignupError::NameTaken(
"username \"bob\" is taken on this appliance".into(),
));
let state = state_with(sink, Arc::new(AtomicU64::new(11)));
settle(&state, &a_signup("cs_r2", "bob"));
let err = rename(&state, "mockpay", "cs_r2", "bob2").expect_err("also taken");
assert!(
matches!(err, RenameRefusal::Name(ref why) if why.contains("bob2")),
"{err:?}"
);
// Untouched: still refused over the NAME, so the page offers the field again.
let held = state.signups.get("mockpay", "cs_r2").unwrap().unwrap();
assert!(
matches!(
held.state,
SignupState::Refused {
refusal: Refusal::Name,
..
}
),
"{held:?}"
);
assert_eq!(
held.signup.username, "bob",
"a refused rename rewrote the row"
);
// And a name that is not a name at all never reaches the sink's list.
assert!(matches!(
rename(&state, "mockpay", "cs_r2", "Bob!"),
Err(RenameRefusal::Name(_))
));
}
/// ★ **A FINAL refusal is not renameable**, and the refusal it answers with is
/// the original one — which is what the page shows instead of a text field.
#[test]
fn a_signup_refused_over_the_order_is_not_renameable() {
let state = state_with(CountingSink::creating(), Arc::new(AtomicU64::new(12)));
let mut s = a_signup("cs_f", "bob");
s.plan = "gunnar/1tb-forever".into();
settle(&state, &s);
let err = rename(&state, "mockpay", "cs_f", "bob2")
.expect_err("a plan nobody sells is not a name problem");
let RenameRefusal::NotRenameable(why) = err else {
panic!("{err:?}")
};
assert!(
why.contains("gunnar/1tb-forever"),
"the page shows the ORIGINAL refusal: {why}"
);
// Nothing ran and nothing changed.
assert_eq!(
state
.signups
.get("mockpay", "cs_f")
.unwrap()
.unwrap()
.signup
.username,
"bob"
);
}
/// **Idempotent per session.** A return page that posts twice — a double tap, a
/// retry over a dropped connection — must not push a second name at a sink that
/// has already made an account.
#[test]
fn renaming_a_created_signup_runs_nothing_and_answers_what_it_became() {
let sink = CountingSink::creating();
let state = state_with(sink.clone(), Arc::new(AtomicU64::new(13)));
settle(&state, &a_signup("cs_done", "bob"));
let calls = sink.calls.load(Ordering::SeqCst);
let out = rename(&state, "mockpay", "cs_done", "somebodyelse").expect("answers what it became");
assert!(matches!(out.state, SignupState::Created { .. }));
assert_eq!(
out.signup.username, "bob",
"a created signup was renamed out from under its owner"
);
assert_eq!(
sink.calls.load(Ordering::SeqCst),
calls,
"the sink ran again for a signup already created"
);
}
/// A session nobody has heard of, and one still pending: two different
/// sentences, because a page answers them differently.
#[test]
fn a_rename_of_an_unknown_or_pending_signup_is_refused_by_name() {
let sink = CountingSink::answering(Err(SignupError::Unavailable(
"gunnar is not answering".into(),
)));
let state = state_with(sink, Arc::new(AtomicU64::new(14)));
assert_eq!(
rename(&state, "mockpay", "cs_nope", "bob"),
Err(RenameRefusal::Unknown)
);
settle(&state, &a_signup("cs_pend", "bob"));
let err =
rename(&state, "mockpay", "cs_pend", "bob2").expect_err("nothing has been refused yet");
assert!(matches!(err, RenameRefusal::NotRenameable(_)), "{err:?}");
}
/// ★ **A sink that cannot be asked is not "taken".**
///
/// A console that said "choose another username" because the appliance was
/// rebooting would be lying to somebody whose name is free — and they would
/// spend it on a second-choice name they keep forever.
#[test]
fn a_name_check_against_an_unreachable_sink_is_an_outage_and_not_a_refusal() {
let sink = CountingSink::holding(&["bob"]);
assert_eq!(sink.name_verdict("olle"), Ok(NameVerdict::Free));
assert!(matches!(
sink.name_verdict("bob"),
Ok(NameVerdict::Taken(_))
));
*sink.reachable.lock().unwrap() = false;
assert!(matches!(
sink.name_verdict("olle"),
Err(SignupError::Unavailable(_))
));
// And a rename through an unreachable sink leaves the record alone.
*sink.reachable.lock().unwrap() = true;
*sink.answer.lock().unwrap() = Err(SignupError::NameTaken(
"username \"bob\" is taken on this appliance".into(),
));
let state = state_with(sink.clone(), Arc::new(AtomicU64::new(15)));
settle(&state, &a_signup("cs_u", "bob"));
*sink.reachable.lock().unwrap() = false;
assert!(matches!(
rename(&state, "mockpay", "cs_u", "olle"),
Err(RenameRefusal::Unavailable(_))
));
let held = state.signups.get("mockpay", "cs_u").unwrap().unwrap();
assert_eq!(held.signup.username, "bob", "an outage rewrote the row");
}
/// **The open build's sink holds nothing, so it invents no occupant.** It
/// creates no principal; answering "taken" for a well-formed name would be a
/// deployment refusing a customer over a name nobody has.
#[test]
fn the_open_sink_answers_the_shape_rule_and_nothing_more() {
assert_eq!(LoudSink.name_verdict("bob"), Ok(NameVerdict::Free));
// Even the names a real deployment reserves: that list is the deployment's,
// and a self-hoster's is their own.
assert_eq!(LoudSink.name_verdict("admin"), Ok(NameVerdict::Free));
let taken = LoudSink
.name_verdict("Bob!")
.expect("the shape rule needs nobody");
assert!(
matches!(taken, NameVerdict::Taken(ref why) if why.contains('!')),
"{taken:?}"
);
}
// ── 2026-09-14: the pending signup nobody is watching, and what the customer reads ──
/// The words a customer must never read, checked case-blind.
fn assert_customer_safe(sentence: &str) {
let lower = sentence.to_lowercase();
for word in words::NEVER_SHOWN {
assert!(
!lower.contains(word),
"a customer sentence carries {word:?}: {sentence:?}"
);
}
}
/// gunnar's own refusal for a valid key on a skewed clock, as the sink reports
/// it once it has classified it transient.
const SKEW: &str = "gunnar Access.ListPrincipals -> Unauthenticated: the credential is 412000 ms away from this server's clock; the window is ±300000 ms";
/// **A pending signup's customer words say it is being set up, and nothing about the
/// clock** (PAID-WORDS: never maintenance for a skew; it is not the product saying so).
/// The operator's copy of the skew is on `waiting_on` (which `SignupAdmin.List` carries)
/// and nowhere a customer reads.
#[test]
fn a_skew_pending_signup_tells_the_customer_setting_up_and_the_operator_the_skew() {
let sink = CountingSink::answering(Err(SignupError::Unavailable(SKEW.into())));
let state = state_with(sink, Arc::new(AtomicU64::new(1_000)));
let s = a_signup("cs_skew", "bob");
assert_eq!(
settle(&state, &s),
SignupState::Pending,
"a skew is not a refusal"
);
let rec = state.signups.get("mockpay", "cs_skew").unwrap().unwrap();
let (said, reason) = customer_words(&state.signups, &rec, state.now() as i64);
assert_eq!(said, words::BEING_CREATED);
assert!(reason.is_empty(), "a pending signup has no refusal to show");
assert_customer_safe(&said);
assert_eq!(
state.signups.waiting_on("mockpay", "cs_skew").as_deref(),
Some(SKEW),
"the operator keeps the real reason"
);
// Before the sink has said "not now", the page says the account is being made.
let fresh = state_with(CountingSink::creating(), Arc::new(AtomicU64::new(1)));
fresh
.signups
.record_pending(&a_signup("cs_fresh", "olle"), 1)
.unwrap();
let rec = fresh.signups.get("mockpay", "cs_fresh").unwrap().unwrap();
assert_eq!(
customer_words(&fresh.signups, &rec, fresh.now() as i64).0,
words::BEING_CREATED
);
}
/// ★ **PAID-WORDS: a "not now" that is not the product saying maintenance is a settle**, in the
/// buyer's words: a product that does not answer, a timeout, a queue. Never "maintenance".
#[test]
fn a_generic_not_now_after_paying_reads_setting_up_never_maintenance() {
for why in [
"gunnar is not answering",
"status: Unavailable, message: \"tcp connect error\"",
"deadline has elapsed",
"queued: gunnar.rs's main creates \"bob\" when it next calls monetize",
] {
let state = state_with(
CountingSink::answering(Err(SignupError::Unavailable(why.into()))),
Arc::new(AtomicU64::new(1_000)),
);
assert_eq!(
settle(&state, &a_signup("cs_blip", "bob")),
SignupState::Pending
);
let rec = state.signups.get("mockpay", "cs_blip").unwrap().unwrap();
let said = customer_words(&state.signups, &rec, state.now() as i64).0;
assert!(
!said.to_lowercase().contains("maintenance"),
"{why:?} said maintenance: {said:?}"
);
assert!(
said.starts_with("Payment received. Setting up your account"),
"{why:?}: {said:?}"
);
assert_customer_safe(&said);
}
}
/// ★ **The pending sentence, as a pure choice**: maintenance only by name; past the bound,
/// taking longer (the mail promised only where a sign-in link is mailed); else setting up.
#[test]
fn pending_words_say_maintenance_only_by_name_and_taking_longer_past_the_bound() {
let bound = SETTLE_BOUND_MS;
assert_eq!(pending_words(false, 0, true), words::BEING_CREATED);
assert_eq!(pending_words(false, bound - 1, false), words::BEING_CREATED);
assert_eq!(pending_words(true, 0, true), words::MAINTENANCE);
assert_eq!(
pending_words(true, bound * 10, true),
words::MAINTENANCE,
"the product's own word holds"
);
assert_eq!(
pending_words(false, bound, true),
words::TAKING_LONGER_MAILED
);
assert_eq!(pending_words(false, bound, false), words::TAKING_LONGER);
assert!(
words::TAKING_LONGER_MAILED.contains("e-mail") && !words::TAKING_LONGER.contains("mail"),
"a mail is promised only where one is sent"
);
}
/// ★ **"Down for maintenance" only when the product says so by name**: its refusal is
/// `SignupError::Maintenance` (RA4-20), and the operator keeps the product's own words.
#[test]
fn a_product_that_says_it_is_in_maintenance_reads_maintenance() {
let state = state_with(
CountingSink::answering(Err(SignupError::Maintenance(
"the shop resizes gunnar.rs".into(),
))),
Arc::new(AtomicU64::new(1_000)),
);
assert_eq!(
settle(&state, &a_signup("cs_maint", "bob")),
SignupState::Pending
);
let rec = state.signups.get("mockpay", "cs_maint").unwrap().unwrap();
assert_eq!(
customer_words(&state.signups, &rec, state.now() as i64).0,
words::MAINTENANCE
);
assert_eq!(
state.signups.waiting_on("mockpay", "cs_maint").as_deref(),
Some("the shop resizes gunnar.rs"),
"the operator keeps the product's words"
);
// Past the bound the product's own word still holds.
let later = state_with(
CountingSink::answering(Err(SignupError::Maintenance("resizing".into()))),
Arc::new(AtomicU64::new(1_000)),
);
assert_eq!(
settle(&later, &a_signup("cs_maint2", "bob")),
SignupState::Pending
);
let rec = later.signups.get("mockpay", "cs_maint2").unwrap().unwrap();
assert_eq!(
customer_words(&later.signups, &rec, 1_000 + 10 * SETTLE_BOUND_MS).0,
words::MAINTENANCE
);
}
/// ★ **Words that merely begin "maintenance: " are a settle, not maintenance** (RA4-20): the
/// refusal is by name, never by a prefix of `Unavailable`'s words.
#[test]
fn an_unavailable_that_begins_maintenance_is_a_settle() {
let state = state_with(
CountingSink::answering(Err(SignupError::Unavailable(
"maintenance: the shop resizes gunnar.rs".into(),
))),
Arc::new(AtomicU64::new(1_000)),
);
assert_eq!(
settle(&state, &a_signup("cs_prefix", "bob")),
SignupState::Pending
);
let rec = state.signups.get("mockpay", "cs_prefix").unwrap().unwrap();
assert_eq!(
customer_words(&state.signups, &rec, state.now() as i64).0,
words::BEING_CREATED
);
}
/// ★ **RA4-19: a pending answer sends AHEAD the sentence for the shop's silence**, due at the
/// settle bound from the payment: the page keeps no bound and no words of its own.
#[test]
fn a_pending_answer_sends_ahead_its_sentence_for_the_shops_silence() {
let now = Arc::new(AtomicU64::new(1_000));
let state = state_with(
CountingSink::answering(Err(SignupError::Unavailable(
"gunnar is not answering".into(),
))),
Arc::clone(&now),
);
assert_eq!(
settle(&state, &a_signup("cs_ahead", "bob")),
SignupState::Pending
);
let rec = state.signups.get("mockpay", "cs_ahead").unwrap().unwrap();
let t0 = rec.recorded_at_unix_ms;
assert_eq!(
words_ahead(&state.signups, &rec, t0),
(words::TAKING_LONGER.to_owned(), SETTLE_BOUND_MS)
);
assert_eq!(
words_ahead(&state.signups, &rec, t0 + 30_000),
(words::TAKING_LONGER.to_owned(), SETTLE_BOUND_MS - 30_000)
);
assert_eq!(
words_ahead(&state.signups, &rec, t0 + 10 * SETTLE_BOUND_MS),
(words::TAKING_LONGER.to_owned(), 0)
);
let maint = state_with(
CountingSink::answering(Err(SignupError::Maintenance("resizing".into()))),
Arc::new(AtomicU64::new(1_000)),
);
assert_eq!(
settle(&maint, &a_signup("cs_ahead_m", "bob")),
SignupState::Pending
);
let rec = maint.signups.get("mockpay", "cs_ahead_m").unwrap().unwrap();
assert_eq!(
words_ahead(&maint.signups, &rec, rec.recorded_at_unix_ms).0,
words::MAINTENANCE
);
}
/// ★ **A settle past its bound says so honestly**: taking longer than usual, the payment safe,
/// and nothing to pay again; never maintenance.
#[test]
fn a_settle_past_its_bound_says_taking_longer_and_the_payment_is_safe() {
let now = Arc::new(AtomicU64::new(1_000));
let state = state_with(
CountingSink::answering(Err(SignupError::Unavailable(
"gunnar is not answering".into(),
))),
Arc::clone(&now),
);
assert_eq!(
settle(&state, &a_signup("cs_long", "bob")),
SignupState::Pending
);
now.store(1_000 + 10 * 60 * 1000, Ordering::SeqCst);
assert_eq!(
settle(&state, &a_signup("cs_long", "bob")),
SignupState::Pending
);
let rec = state.signups.get("mockpay", "cs_long").unwrap().unwrap();
let said = customer_words(&state.signups, &rec, state.now() as i64).0;
assert!(
said.contains("This is taking longer than usual") && said.contains("your payment is safe"),
"{said:?}"
);
assert!(
said.contains("you do not need to pay again")
&& !said.to_lowercase().contains("maintenance"),
"{said:?}"
);
assert_customer_safe(&said);
}
/// **Every fixed customer sentence passes the same check**, and a product's
/// refusal reaches the customer as those words while the operator keeps the
/// product's own. Twin: a name refusal is written for the customer and passes
/// through untouched, because the page needs to say WHICH name.
#[test]
fn the_product_s_own_refusal_never_reaches_the_customer_but_a_name_refusal_does() {
for w in [
words::BEING_CREATED,
words::TAKING_LONGER,
words::TAKING_LONGER_MAILED,
words::NOT_ANSWERING_PAID,
words::MAINTENANCE,
words::READY,
words::COULD_NOT_CREATE,
words::SHOP_DOWN,
] {
assert_customer_safe(w);
}
let raw = "gunnar Access.AddPrincipal -> Unauthenticated: credential did not verify; the sink's identity key is not registered on this gunnar";
let state = state_with(
CountingSink::answering(Err(SignupError::Refused(raw.into()))),
Arc::new(AtomicU64::new(1)),
);
let out = settle(&state, &a_signup("cs_key", "bob"));
assert!(
matches!(&out, SignupState::Refused { reason, refusal: Refusal::Final, .. } if reason == raw),
"the operator's row keeps it verbatim: {out:?}"
);
let rec = state.signups.get("mockpay", "cs_key").unwrap().unwrap();
let (said, reason) = customer_words(&state.signups, &rec, state.now() as i64);
assert_eq!(
(said.as_str(), reason.as_str()),
(words::COULD_NOT_CREATE, words::COULD_NOT_CREATE)
);
let named = "username \"bob\" is taken on this appliance";
let state = state_with(
CountingSink::answering(Err(SignupError::NameTaken(named.into()))),
Arc::new(AtomicU64::new(1)),
);
settle(&state, &a_signup("cs_name", "bob"));
let rec = state.signups.get("mockpay", "cs_name").unwrap().unwrap();
assert_eq!(
customer_words(&state.signups, &rec, state.now() as i64).1,
named
);
}
/// **Past the ceiling a skew outage becomes a refusal — with customer words.**
/// What `PENDING_CEILING_MS` did before, it still does; the customer reads that
/// they need not pay again, and not `sink never answered: … clock`.
#[test]
fn a_skew_outage_longer_than_the_ceiling_is_refused_in_customer_words() {
let now = Arc::new(AtomicU64::new(1_000));
let state = state_with(
CountingSink::answering(Err(SignupError::Unavailable(SKEW.into()))),
Arc::clone(&now),
);
settle(&state, &a_signup("cs_long", "bob"));
now.store(1_000 + PENDING_CEILING_MS as u64, Ordering::SeqCst);
assert_eq!(settle_pending(&state), 1, "the background pass decides it");
let rec = state.signups.get("mockpay", "cs_long").unwrap().unwrap();
assert!(
matches!(&rec.state, SignupState::Refused { reason, .. } if reason.starts_with(NEVER_ANSWERED) && reason.contains("clock")),
"{rec:?}"
);
let (said, _) = customer_words(&state.signups, &rec, state.now() as i64);
assert_eq!(said, words::COULD_NOT_CREATE);
assert_customer_safe(&said);
}
/// **The background pass settles a pending signup with no `Signup.Status`
/// call.** RED before: nothing but the return page's poll moved it, so a
/// customer who closed the tab stayed PENDING until somebody opened that URL.
#[test]
fn a_pending_signup_is_settled_in_the_background_once_the_product_answers() {
let sink = CountingSink::answering(Err(SignupError::Unavailable(SKEW.into())));
let now = Arc::new(AtomicU64::new(1_000));
let state = state_with(sink.clone(), Arc::clone(&now));
assert_eq!(
settle(&state, &a_signup("cs_bg", "bob")),
SignupState::Pending
);
// Still skewed: the pass asks, decides nothing, and the row stays pending.
assert_eq!(settle_pending(&state), 0);
assert_eq!(sink.calls.load(Ordering::SeqCst), 2);
// The clocks agree. No page is open; the poller's tick finishes it.
*sink.answer.lock().unwrap() = Ok(SignupOutcome::Created {
namespace: "bob".into(),
username: "bob".into(),
});
now.store(301_000, Ordering::SeqCst);
let tick = crate::poller::tick(&state);
assert_eq!(tick.signups_settled, 1, "{tick:?}");
let rec = state.signups.get("mockpay", "cs_bg").unwrap().unwrap();
assert_eq!(
rec.state,
SignupState::Created {
namespace: "bob".into(),
username: "bob".into(),
at_unix_ms: 301_000
}
);
assert_eq!(
state.signups.waiting_on("mockpay", "cs_bg"),
None,
"a decided signup is not waiting on anything"
);
// And a decided signup is not asked again by the next pass.
assert_eq!(settle_pending(&state), 0);
assert_eq!(sink.calls.load(Ordering::SeqCst), 3);
}
/// A sink whose `paid` blocks until the test lets it go — so two callers can be
/// made to overlap on one session, deterministically.
struct GateSink {
entered: std::sync::mpsc::SyncSender<()>,
release: Mutex<std::sync::mpsc::Receiver<()>>,
calls: AtomicU64,
}
impl SignupSink for GateSink {
fn paid(&self, s: &PaidSignup) -> Result<SignupOutcome, SignupError> {
// Only the FIRST call is held, so a second caller that wrongly reaches
// the sink fails the assertion below instead of hanging the test.
if self.calls.fetch_add(1, Ordering::SeqCst) == 0 {
let _ = self.entered.send(());
let _ = self.release.lock().unwrap().recv();
}
Ok(SignupOutcome::Created {
namespace: s.username.clone(),
username: s.username.clone(),
})
}
fn name_verdict(&self, _username: &str) -> Result<NameVerdict, SignupError> {
Ok(NameVerdict::Free)
}
}
/// **A background settle and a status poll on the same session create the
/// account once.** The background pass is held inside the sink; the poll that
/// arrives meanwhile answers PENDING and calls nothing, and once the pass is
/// released both read CREATED. RED before the flight: the poll ran the sink a
/// second time for the same payment.
#[test]
fn a_background_settle_and_a_status_poll_on_one_session_create_the_account_once() {
let (entered_tx, entered_rx) = std::sync::mpsc::sync_channel(4);
let (release_tx, release_rx) = std::sync::mpsc::channel();
let sink = Arc::new(GateSink {
entered: entered_tx,
release: Mutex::new(release_rx),
calls: AtomicU64::new(0),
});
let state = state_with(sink.clone(), Arc::new(AtomicU64::new(1_000)));
let s = a_signup("cs_race", "bob");
// Pending, as a webhook that met an outage leaves it.
state.signups.record_pending(&s, 1_000).unwrap();
let bg_state = Arc::clone(&state);
let background = std::thread::spawn(move || settle_pending(&bg_state));
entered_rx
.recv_timeout(std::time::Duration::from_secs(10))
.expect("the background pass reaches the sink");
// The customer's page polls while the pass is inside the sink.
let polled = status_with_retry(&state, "mockpay", "cs_race")
.unwrap()
.unwrap();
assert_eq!(
polled.state,
SignupState::Pending,
"the poll must not run the sink beside the pass"
);
assert_eq!(sink.calls.load(Ordering::SeqCst), 1);
release_tx.send(()).unwrap();
assert_eq!(background.join().unwrap(), 1);
let polled = status_with_retry(&state, "mockpay", "cs_race")
.unwrap()
.unwrap();
assert!(
matches!(polled.state, SignupState::Created { .. }),
"{polled:?}"
);
assert_eq!(
sink.calls.load(Ordering::SeqCst),
1,
"one payment, one account, one sink call"
);
assert!(
state.signups.fly("mockpay", "cs_race").is_some(),
"the flight is released when the call ends"
);
}
/// A state that links gunnar (so `Entitlement` and `Ledger.Sales` answer for
/// it), with the counting sink and the test plans.
fn linked_state(sink: Arc<dyn SignupSink>, now: u64) -> Arc<crate::state::AppState> {
let mut state = crate::state::AppState::in_memory(
crate::registry::Registry::new().product(monetize::testing::MockProduct::gunnar()),
);
state.signup_sink = sink;
state.signup_plans = plans().into();
state.clock = Box::new(move || now);
Arc::new(state)
}
/// (r) **A paid signup's tenant shows its cap.** Measured on the live front
/// 2026-09-15: `elvira` paid, the sink pushed pack_bytes = 10 GiB for 30 days
/// and gunnar accepted it, and the console listed her as "no cap" — a signup
/// writes no ledger row, and `Entitlement.List` read the ledger alone.
#[tokio::test]
async fn a_created_signup_is_an_entitlement_with_its_cap_and_a_replay_adds_no_second() {
use monetize_proto::entitlement_server::Entitlement as _;
let now = 1_788_436_900_000;
let sink = CountingSink::answering(Ok(SignupOutcome::Created {
namespace: "elvira".into(),
username: "elvira".into(),
}));
let state = linked_state(sink, now);
let s = a_signup("cs_elvira", "elvira");
assert!(matches!(settle(&state, &s), SignupState::Created { .. }));
settle(&state, &s); // the vendor re-delivers
let svc = crate::services::entitlement::EntitlementService(state.clone());
let facts = svc
.list(tonic::Request::new(
monetize_proto::ListEntitlementsRequest {
product: "gunnar".into(),
},
))
.await
.unwrap()
.into_inner()
.facts;
assert_eq!(facts.len(), 1, "one signup, one fact: {facts:?}");
let f = &facts[0];
assert_eq!(f.tenant, "elvira");
assert_eq!(
f.caps.get("pack_bytes"),
Some(&10_737_418_240),
"the plan's cap: {f:?}"
);
assert_eq!(
f.paid_until_unix_ms,
Some(s.paid_at_unix_ms as u64 + 30 * 24 * 60 * 60 * 1000)
);
assert_eq!(f.state(), monetize_proto::State::Paid);
let rust = crate::convert::fact_from_pb(f).unwrap();
assert!(
monetize::embed::signing::verify_fact(&rust, &state.signer_for("gunnar").verifying_key())
.is_ok(),
"signed by monetize's own key"
);
let got = svc
.get(tonic::Request::new(monetize_proto::GetEntitlementRequest {
product: "gunnar".into(),
tenant: "elvira".into(),
}))
.await
.unwrap()
.into_inner();
assert_eq!(
got.fact.and_then(|f| f.caps.get("pack_bytes").copied()),
Some(10_737_418_240)
);
// A fact the LEDGER holds for the tenant wins: a later purchase or an
// operator's hand replaced the signup's.
let mut later = rust.clone();
later.caps = BTreeMap::from([("pack_bytes".to_string(), 20_u64 << 30)]);
state.ledger.set_entitlement("gunnar", &later).unwrap();
let facts = svc
.list(tonic::Request::new(
monetize_proto::ListEntitlementsRequest {
product: "gunnar".into(),
},
))
.await
.unwrap()
.into_inner()
.facts;
assert_eq!(facts.len(), 1);
assert_eq!(facts[0].caps.get("pack_bytes"), Some(&(20_u64 << 30)));
}
/// (r) **"Recently bought" includes a paid signup**, one line per vendor
/// session, and a REFUSED signup is not a sale. RED on the live front: "Nothing
/// bought yet" beside a paying tenant.
#[tokio::test]
async fn a_created_signup_is_recently_bought_once_and_a_refused_one_is_not() {
use monetize_proto::ledger_server::Ledger as _;
let sink = CountingSink::answering(Ok(SignupOutcome::Created {
namespace: "elvira".into(),
username: "elvira".into(),
}));
let state = linked_state(sink.clone(), 1_788_436_900_000);
let mut s = a_signup("cs_elvira", "elvira");
s.amount = Money {
minor: 12_500,
currency: Currency::Sek,
};
s.tax = Money {
minor: 2_500,
currency: Currency::Sek,
};
settle(&state, &s);
settle(&state, &s);
*sink.answer.lock().unwrap() = Err(SignupError::Refused("the product refused".into()));
settle(&state, &a_signup("cs_refused", "bjorn"));
let svc = crate::services::ledger::LedgerService(state.clone());
let sales = svc
.sales(tonic::Request::new(monetize_proto::SalesRequest {
product: "gunnar".into(),
limit: 10,
}))
.await
.unwrap()
.into_inner()
.sales;
assert_eq!(
sales.len(),
1,
"one CREATED session, one line — replay and refusal add none: {sales:?}"
);
let sale = &sales[0];
assert_eq!(sale.reference, "signup:mockpay:cs_elvira");
assert_eq!(
(
sale.tenant.as_str(),
sale.outcome.as_str(),
sale.drawn,
sale.needs_you
),
("elvira", "granted", true, false)
);
assert_eq!(
sale.price.as_ref().map(|m| m.minor),
Some(10_000),
"net of the vendor's tax"
);
assert_eq!(sale.caps.get("pack_bytes"), Some(&10_737_418_240));
assert!(sale.fee.is_none(), "not reported is not zero");
assert_eq!(
sale.livemode, None,
"the mock vendor's session carries no mode marker: not live, not test — not said"
);
// The all-products ledger page lists it once too.
let all = svc
.sales(tonic::Request::new(monetize_proto::SalesRequest {
product: String::new(),
limit: 0,
}))
.await
.unwrap()
.into_inner()
.sales;
assert_eq!(all.len(), 1);
}
/// (r) **MONEY TAKEN, ACCOUNT NOT CREATED, RECORDED AS SUCH — and the job
/// named.**
///
/// The shape this exists for, measured against `monetize-signup-impl`'s own
/// order of work: the sink creates the gunnar principal at step (b) and pushes
/// the entitlement at step (c), so a `ProductError::Refused` at (c) is a
/// customer whose card cleared, whose name is spent on the appliance, and whose
/// signup is `Refusal::Final` — never retried (`settle`), not renameable
/// (`rename`), and nothing calls `reclaim::purge_tenant`. Before this the only
/// record of it was prose in `reason`, `tax_sales` counted the CREATED rows
/// alone, and its comment said the rest was "owed back by hand" without there
/// being a hand to count.
///
/// Asserted on the ARM and on the remedy's substance, not on a bool: a queue
/// that said "refund" for a signup whose name is burnt would send an operator
/// to do half the job.
#[test]
fn money_taken_for_an_account_that_was_never_created_is_recorded_as_owed_back_with_the_job_named() {
let sink = CountingSink::answering(Err(SignupError::Refused(
"Entitlement.Set: quota rejected".into(),
)));
let state = state_with(sink.clone(), Arc::new(AtomicU64::new(7_000)));
let out = settle(&state, &a_signup("cs_owed", "bob"));
let SignupState::Refused { owed, refusal, .. } = &out else {
panic!("{out:?}")
};
assert_eq!(
*refusal,
Refusal::Final,
"the pre-existing shape: nothing the customer types fixes it"
);
assert_eq!(
*owed,
Owed::RefundAndTheNameMayBeSpent,
"the sink had already made the principal when it refused"
);
let remedy = owed.remedy().to_lowercase();
assert!(
remedy.contains("refund"),
"the operator is not told to refund: {remedy}"
);
assert!(
remedy.contains("name"),
"the burnt username is not mentioned: {remedy}"
);
// THE QUEUE. One row, and it carries the buyer and the money — a list that
// said only "1 owed" would be a number nobody can act on.
let owed_back = state.signups.owed_back().unwrap();
assert_eq!(owed_back.len(), 1);
assert_eq!(owed_back[0].signup.session, "cs_owed");
assert_eq!(owed_back[0].signup.email, "bob@example.com");
assert_eq!(
owed_back[0].signup.amount,
Money {
minor: 10_000,
currency: Currency::Sek
}
);
assert_eq!(state.signups.owed_back_count().unwrap(), 1);
// A CREATED signup is not on it, and neither is a PENDING one: the first is
// the account and the second may still become one.
*sink.answer.lock().unwrap() = Ok(SignupOutcome::Created {
namespace: "ada".into(),
username: "ada".into(),
});
settle(&state, &a_signup("cs_made", "ada"));
*sink.answer.lock().unwrap() = Err(SignupError::Unavailable("gunnar is restarting".into()));
settle(&state, &a_signup("cs_waiting", "eva"));
assert_eq!(
state.signups.owed_back_count().unwrap(),
1,
"only the refusal is owed back"
);
// And the OTHER arms are reachable and different, so the queue is not one
// verdict wearing four names. A plan nobody sells is refused before the
// sink runs: nothing was created, so the name is not spent.
let mut wrong_plan = a_signup("cs_plan", "carl");
wrong_plan.plan = "gunnar/1tb-forever".into();
assert_eq!(settle(&state, &wrong_plan).owed(), Some(Owed::Refund));
// A name refusal is the one the customer can still finish themselves.
assert_eq!(
settle(&state, &a_signup("cs_shape", "Bob!")).owed(),
Some(Owed::RenameOrRefund)
);
let arms: Vec<&str> = state
.signups
.owed_back()
.unwrap()
.iter()
.filter_map(|r| r.state.owed())
.map(|o| o.name())
.collect();
assert_eq!(
arms,
[
"refund_and_the_name_may_be_spent",
"refund",
"rename_or_refund"
],
"oldest first, one arm each"
);
}
/// (r) **The owed-back queue survives a restart**, because the rows do — and a
/// row written before the field existed reads as `unrecorded` rather than as
/// nothing owed.
///
/// A queue that lived in memory would empty itself on every deploy, which for
/// this particular list means a customer's hundred kronor quietly ceasing to
/// exist. The pre-field row is the other half: `Owed` is `#[serde(default)]`,
/// and the default must put the row IN the queue with "go and look", never
/// leave it out.
#[test]
fn the_owed_back_queue_is_still_there_after_the_store_is_closed_and_reopened() {
let dir = std::env::temp_dir().join(format!("monetize-owed-back-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("signups.redb");
let _ = std::fs::remove_file(&path);
{
let store = SignupStore::open(&path).unwrap();
let s = a_signup("cs_restart", "bob");
store.record_pending(&s, 1_000).unwrap();
store
.set(
"mockpay",
"cs_restart",
SignupState::Refused {
reason: "Entitlement.Set: quota rejected".into(),
at_unix_ms: 1_001,
refusal: Refusal::Final,
words: Some(words::COULD_NOT_CREATE.into()),
owed: Owed::RefundAndTheNameMayBeSpent,
},
)
.unwrap();
assert_eq!(store.owed_back_count().unwrap(), 1);
}
// A row written by a monetize from BEFORE this field: the JSON has no
// `owed` key at all. Poked in through the store's own restore path shape —
// the point is the bytes, which is what a live signups.redb holds.
let store = SignupStore::open(&path).unwrap();
assert_eq!(
store.owed_back_count().unwrap(),
1,
"the refusal did not survive the reopen"
);
let rec = store.get("mockpay", "cs_restart").unwrap().unwrap();
assert_eq!(rec.state.owed(), Some(Owed::RefundAndTheNameMayBeSpent));
let old_json = serde_json::json!({
"signup": serde_json::to_value(a_signup("cs_ancient", "eva")).unwrap(),
"state": { "Refused": { "reason": "plan \"gunnar/1tb\" is not sold here", "at_unix_ms": 900 } },
"recorded_at_unix_ms": 900,
});
let ancient: SignupRecord =
serde_json::from_value(old_json).expect("a row written before the field still parses");
assert_eq!(
ancient.state.owed(),
Some(Owed::Unrecorded),
"a row from before the field is IN the queue, not out of it"
);
assert!(
ancient
.state
.owed()
.unwrap()
.remedy()
.to_lowercase()
.contains("unknown"),
"{}",
ancient.state.owed().unwrap().remedy()
);
let _ = std::fs::remove_file(&path);
}
/// ★ **A FULL SHOP IS NOT MAINTENANCE — the second half of the 2026-09-20 fix.**
///
/// `monetize::transaction::words` stopped blaming maintenance for a store that
/// was simply too small; this constant kept doing it for one more repository,
/// because its doc pointed at that sibling as the justification. Maintenance
/// ends by itself. A shop with no room left does not — somebody has to act —
/// so the one word that must never appear here is the one that tells a
/// customer to wait.
///
/// It also asserts what the sentence DOES carry, because "does not say
/// maintenance" is satisfied by a blank string: the true half a frightened
/// buyer needs first, and who acts next.
#[test]
fn the_no_room_sentence_never_blames_maintenance() {
let s = words::NO_ROOM;
assert!(
!s.to_lowercase().contains("maintenance"),
"the full-shop refusal still blames maintenance: {s:?}"
);
assert!(
s.contains("Nothing has been charged"),
"the half that is always true is missing: {s:?}"
);
assert!(
s.contains("ask us"),
"nothing tells the customer that somebody has to act: {s:?}"
);
assert_customer_safe(s);
// The control: the one sentence that is HONESTLY about maintenance (the product said
// so by name) keeps the word, so this test cannot be passing because the needle is
// wrong. A generic "not now" (SHOP_DOWN) never says it (PAID-WORDS).
assert!(words::MAINTENANCE.to_lowercase().contains("maintenance"));
assert!(!words::SHOP_DOWN.to_lowercase().contains("maintenance"));
}
/// **The address a tenant signed up with is the payer of its later orders**
/// (owner 2026-09-28: Stripe's page asked a logged-in tenant on test2 for
/// "E-postadress" again). Only a signup that CREATED the namespace counts; a
/// pending one, another product's, or another namespace's does not.
#[test]
fn the_email_of_a_tenant_is_the_one_its_created_signup_carried() {
let store = SignupStore::in_memory().unwrap();
let mut alice = a_signup("cs_alice", "alice");
alice.email = " alice@example.com ".into();
store.record_pending(&alice, 1).unwrap();
assert_eq!(
store.email_of("gunnar", "alice").unwrap(),
None,
"a pending signup made nobody yet"
);
store
.set(
"mockpay",
"cs_alice",
SignupState::Created {
namespace: "alice".into(),
username: "alice".into(),
at_unix_ms: 2,
},
)
.unwrap();
assert_eq!(
store.email_of("gunnar", "alice").unwrap().as_deref(),
Some("alice@example.com")
);
assert_eq!(
store.email_of("holger", "alice").unwrap(),
None,
"another product's tenant"
);
assert_eq!(
store.email_of("gunnar", "bob").unwrap(),
None,
"a namespace no signup made (an operator's account)"
);
}
/// ★ **g-nonpay / g-renewal: the front's ONE sink delivers a customer notice
/// through the HOST's relay when the sink itself sends no mail.** On the gunnar
/// front the sink is a signup GUEST (or none), which sends nothing — so until
/// this existed every nonpay warning and renewal reminder there was recorded
/// "not sent". The relay is the host's: `secret.smtp_url` / `secret.smtp_from`
/// in the secrets dir (fed by the installer), the env names as the fallback.
/// Asked of a relay nobody listens on, the answer is `Unavailable` — the relay
/// WAS asked — where before it was the sink's refusal.
#[test]
fn a_notice_goes_through_the_hosts_relay_when_the_sink_sends_no_mail() {
let n = monetize_pay::CustomerNotice {
product: "gunnar".into(),
tenant: "bob".into(),
to: "bob@example.com".into(),
kind: "nonpay.day0".into(),
subject: "s".into(),
body: "b".into(),
};
let dir = std::env::temp_dir().join(format!("monetize-host-relay-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let hot = HotSink::per_product(BTreeMap::from([(
"gunnar".to_string(),
Arc::new(NoSink) as Sink,
)]));
hot.mail_from(&dir);
// No relay anywhere: the refusal says so, by name.
let e = hot.notify(&n).unwrap_err();
assert!(
matches!(&e, SignupError::Refused(why) if why.contains("smtp_url")),
"{e:?}"
);
// A fed relay, down: the host's relay was ASKED.
let dead = std::net::TcpListener::bind("127.0.0.1:0")
.unwrap()
.local_addr()
.unwrap()
.port();
monetize_product_wasm::secrets::write(
&dir,
"smtp_url",
format!("smtp://u:p@127.0.0.1:{dead}").as_bytes(),
)
.unwrap();
monetize_product_wasm::secrets::write(&dir, "smtp_from", b"gunnar.rs <no-reply@gunnar.rs>")
.unwrap();
let e = hot.notify(&n).unwrap_err();
assert!(
matches!(&e, SignupError::Unavailable(why) if why.contains("nonpay.day0")),
"{e:?}"
);
let _ = std::fs::remove_dir_all(&dir);
}
/// ★ Review #11: a signup row that will not parse is a store ERROR on every
/// read and write, never "no such signup": read as absent, `settle` would call
/// the sink again and a reversal would be answered "not ours".
#[test]
fn a_signup_row_that_will_not_parse_is_an_error_never_absent() {
let store = SignupStore::in_memory().unwrap();
let s = a_signup("cs_broken", "alice");
store.record_pending(&s, 1).unwrap();
{
let txn = store.db.begin_write().unwrap();
txn.open_table(SIGNUPS)
.unwrap()
.insert(
(s.vendor.as_str(), s.session.as_str()),
b"{not json".as_slice(),
)
.unwrap();
txn.commit().unwrap();
}
assert!(
store.get(&s.vendor, &s.session).is_err(),
"get read a broken row as absent"
);
assert!(
store
.set(&s.vendor, &s.session, SignupState::Pending)
.is_err(),
"set read a broken row as absent"
);
assert!(
store
.rewrite(&s.vendor, &s.session, &s, SignupState::Pending)
.is_err(),
"rewrite read a broken row as absent"
);
}
/// ★ **G4: each product's signup answers through its OWN sink** (one monetize,
/// gunnar and holger, 2026-10-10). The name check, the paid signup and "does
/// it create accounts" go to the sink of the product they name; a product
/// with no `[signups.<product>]` is refused by name, never handed to another
/// product's sink; a name asked with no product is refused.
#[test]
fn the_hot_sink_answers_each_product_with_its_own_sink() {
let gunnar = CountingSink::holding(&["rust-dev"]);
let holger = CountingSink::creating();
let hot = HotSink::per_product(BTreeMap::from([
("gunnar".to_string(), gunnar.clone() as Sink),
("holger".to_string(), holger.clone() as Sink),
]));
assert!(
matches!(
hot.name_verdict_for("gunnar", "rust-dev"),
Ok(NameVerdict::Taken(_))
),
"gunnar holds the name"
);
assert!(
matches!(
hot.name_verdict_for("holger", "rust-dev"),
Ok(NameVerdict::Free)
),
"holger's names are holger's"
);
assert!(
matches!(hot.name_verdict_for("idun", "rust-dev"), Err(SignupError::Refused(why)) if why.contains("idun"))
);
assert!(
matches!(hot.name_verdict("rust-dev"), Err(SignupError::Refused(_))),
"a name is checked FOR a product"
);
assert!(
hot.creates_accounts_for("gunnar")
&& hot.creates_accounts_for("holger")
&& !hot.creates_accounts_for("idun")
);
let mut s = a_signup("cs_h", "rust-dev");
s.product = "holger".into();
s.plan = "holger/base-se".into();
assert!(hot.paid(&s).is_ok());
assert_eq!(
(
gunnar.calls.load(Ordering::SeqCst),
holger.calls.load(Ordering::SeqCst)
),
(0, 1),
"holger's signup went to holger's sink"
);
s.product = "idun".into();
assert!(
matches!(hot.paid(&s), Err(SignupError::Refused(why)) if why.contains("[signups.idun]"))
);
hot.replace("holger", Arc::new(NoSink));
assert!(
!hot.creates_accounts_for("holger") && hot.creates_accounts_for("gunnar"),
"one product's sink is replaced alone"
);
}
/// ★ **G4: each product's plans and meter** — gunnar's caps land on
/// `pack_bytes`, holger's on `disk_bytes`, and a product's plans are its own.
#[test]
fn each_product_has_its_own_plans_and_its_own_meter() {
let mut all = plans();
all.insert(
"holger/base-se".into(),
SignupPlan {
amount_minor: 10_000,
currency: Currency::Sek,
cap_bytes: 5 << 30,
period_days: 30,
},
);
let p = Plans::of(
all,
BTreeMap::from([("holger".to_string(), "disk_bytes".to_string())]),
);
assert_eq!(p.cap_meter_of("holger"), "disk_bytes");
assert_eq!(p.cap_meter_of("gunnar"), SIGNUP_CAP_METER);
assert_eq!(
p.snapshot_of("holger")
.keys()
.map(String::as_str)
.collect::<Vec<_>>(),
["holger/base-se"]
);
assert_eq!(
p.snapshot_of("gunnar")
.keys()
.map(String::as_str)
.collect::<Vec<_>>(),
[PLAN]
);
}
/// ★ **A signup's tax line is in the BUYER's country, never the currency's**
/// (stinky monetize-server #15, 5.4): a German buyer paying in EUR was booked
/// under Finnish VAT because EUR's market is FI. The vendor's billing country
/// wins; only a row that names none (written before the field, or a vendor
/// that does not say) falls back to the currency's market. The reconciler's
/// copy of a payment carries it the same way.
#[test]
fn a_signups_tax_line_is_in_the_buyers_billing_country_and_never_the_currencys() {
let eur_plans = BTreeMap::from([(
PLAN.to_string(),
SignupPlan {
amount_minor: 10_000,
currency: Currency::Eur,
cap_bytes: 10_737_418_240,
period_days: 30,
},
)]);
let state = {
let mut state = crate::state::AppState::in_memory(crate::registry::Registry::new());
state.signup_sink = CountingSink::creating();
state.signup_plans = eur_plans.clone().into();
state.clock = Box::new(|| 1_789_468_200_000);
Arc::new(state)
};
let eur = |session: &str, user: &str, country: Option<&str>| {
let mut p = a_signup(session, user);
p.amount = Money {
minor: 11_900,
currency: Currency::Eur,
};
p.tax = Money {
minor: 1_900,
currency: Currency::Eur,
};
p.customer_country = country.map(str::to_owned);
p
};
assert!(matches!(
settle(&state, &eur("cs_de", "bob", Some("DE"))),
SignupState::Created { .. }
));
assert!(matches!(
settle(&state, &eur("cs_old", "eve", None)),
SignupState::Created { .. }
));
let books = monetize::Books {
home_country: "SE".into(),
supplier_countries: BTreeMap::new(),
};
let sales = tax_sales(&state.signups, &books).unwrap();
let country = |r: &str| {
sales
.iter()
.find(|s| s.reference == r)
.map(|s| s.customer_country.clone())
.unwrap()
};
assert_eq!(
country("signup:mockpay:cs_de"),
"DE",
"the buyer's billing country: {sales:?}"
);
assert_eq!(
country("signup:mockpay:cs_old"),
"FI",
"no country said: the currency's market, as before"
);
let fact = monetize_pay::reconcile::PaidAtVendor {
vendor: "mockpay".into(),
session: "cs_rec".into(),
reference: "pi_rec".into(),
paid_at_unix_ms: 1_789_468_154_000,
amount_minor: 11_900,
tax_minor: 1_900,
currency: "eur".into(),
livemode: false,
metadata: BTreeMap::from([
("product".into(), "gunnar".into()),
("plan".into(), PLAN.into()),
("username".into(), "olle".into()),
]),
email: String::new(),
customer_country: Some("DE".into()),
};
let s = crate::reconcile::signup_of(&fact, &eur_plans).unwrap();
assert_eq!(s.customer_country.as_deref(), Some("DE"), "{s:?}");
}
/// The "stays pending" line is said once per change, not on every tick.
#[test]
fn stays_pending_is_said_once_per_change() {
assert!(
SignupStore::says_pending(None, "sink down"),
"the first pending pass says it"
);
assert!(
!SignupStore::says_pending(Some("sink down"), "sink down"),
"the same reason again is quiet"
);
assert!(
SignupStore::says_pending(Some("sink down"), "sink slow"),
"a new reason is said"
);
}