crates/monetize-server/src/estate_order/tests/dns_takeover.rs
//! **The operator site's own DNS is taken over** (owner 2026-10-22, live case holger-rs): the
//! ONE box laid the operator site holger-rs and parked, "holger.rs already has a A record this
//! writer did not make", because the apex and www. still pointed at 95.111.208.165, an old
//! Holger box long deleted at the provider. Monetize must handle that by itself:
//!
//! * an OPERATOR site order takes over the A/AAAA/CNAME records at exactly the names it writes
//! (apex, www., git.), replaces them and marks them as its writer's;
//! * the run's history and an ALERT say which old records were replaced (name, type, target);
//! * MX, TXT and the rest at the apex, and every record on another name, are left as they are;
//! * a SOLD estate's write still refuses a record its writer did not make.
use super::*;
use monetize_cloud::dns::{Replaced, Written, TAKE_OVER_KINDS};
/// **A fake Cloudflare zone** with the plugin's rules: every record it writes carries the
/// mark; a plain write refuses a record at the same name and kind without it; a takeover
/// replaces the unmarked address records at the names it is given, and only those.
#[derive(Default)]
struct FakeCloudflare {
/// (record, made by this writer)
rows: Mutex<Vec<(DnsRecord, bool)>>,
/// Every `take_over` list the writer was handed.
asked: Mutex<Vec<Vec<String>>>,
}
fn row(name: &str, kind: &str, value: &str) -> (DnsRecord, bool) {
(
DnsRecord {
name: name.into(),
kind: kind.into(),
value: value.into(),
proxied: false,
},
false,
)
}
impl FakeCloudflare {
fn with(rows: Vec<(DnsRecord, bool)>) -> Arc<FakeCloudflare> {
Arc::new(FakeCloudflare {
rows: Mutex::new(rows),
..Default::default()
})
}
fn at(&self, name: &str, kind: &str) -> Vec<(String, bool)> {
self.rows
.lock()
.unwrap()
.iter()
.filter(|(r, _)| r.name == name && r.kind == kind)
.map(|(r, ours)| (r.value.clone(), *ours))
.collect()
}
}
impl EstateDns for FakeCloudflare {
/// The fake zone is gunnar.rs (RA1-5: a writer names its zone, or writes nothing).
fn zone(&self) -> Result<Option<String>, String> {
Ok(Some("gunnar.rs".into()))
}
fn write(&self, records: &[DnsRecord]) -> Result<String, String> {
let mut rows = self.rows.lock().unwrap();
for r in records {
if rows
.iter()
.any(|(l, ours)| !ours && l.name == r.name && l.kind == r.kind)
{
return Err(format!("cloudflare: {} already has a {} record this writer did not make (no comment \"monetize-estate\"); it is left as it is and nothing was written", r.name, r.kind));
}
}
rows.retain(|(l, _)| !records.iter().any(|r| r.name == l.name && r.kind == l.kind));
rows.extend(records.iter().cloned().map(|r| (r, true)));
Ok(format!("cloudflare: {} record(s) written", records.len()))
}
fn write_taking_over(
&self,
records: &[DnsRecord],
take_over: &[String],
) -> Result<Written, String> {
self.asked.lock().unwrap().push(take_over.to_vec());
let mut replaced = Vec::new();
self.rows.lock().unwrap().retain(|(l, ours)| {
let taken =
!ours && take_over.contains(&l.name) && TAKE_OVER_KINDS.contains(&l.kind.as_str());
if taken {
replaced.push(Replaced {
name: l.name.clone(),
kind: l.kind.clone(),
value: l.value.clone(),
});
}
!taken
});
let line = self.write(records)?;
Ok(Written { line, replaced })
}
fn remove(&self, names: &[String]) -> Result<String, String> {
self.rows
.lock()
.unwrap()
.retain(|(l, ours)| !(*ours && names.contains(&l.name)));
Ok("removed".into())
}
fn listed(&self) -> Result<Vec<DnsRecord>, String> {
Ok(self
.rows
.lock()
.unwrap()
.iter()
.filter(|(_, ours)| *ours)
.map(|(r, _)| r.clone())
.collect())
}
}
/// The site rig (operator site `gunnar-rs` → gunnar.rs) over the fake Cloudflare zone.
fn site_rig_over(name: &str, zone: Arc<FakeCloudflare>) -> (Arc<AppState>, HoldPay) {
let dir = tmp(name);
let pay = HoldPay::default();
let cloud = Arc::new(MockCloud::new());
let kit = FakeKit::new("PASS");
*kit.ticket_key.lock().unwrap() = a_ticket_key();
let site =
|vendor: &str, site: &str, owner: &str, operator_site: bool| crate::config::EstateConfig {
product: "gunnar".into(),
vendor: vendor.into(),
site: site.into(),
owner: owner.into(),
operator_site,
..Default::default()
};
let reg = Registry::new()
.sells("gunnar", pay.clone())
.vendor(SitePay("sitepay-kunde"))
.cloud(cloud)
.shared_product(shop_product())
.home("gunnar", "gunnar-rs")
.estate("gunnar-rs", site("holdpay", "gunnar.rs", "ignalina", true))
.estate(
"kunde-se",
site("sitepay-kunde", "kunde.se", "kunde", false),
);
let dns: Arc<dyn EstateDns> = zone;
let (state, ..) = build_in(
reg,
&dir,
EstateOrderConfig {
vendor: String::new(),
apex: String::new(),
..cfg()
},
kit,
Some(dns),
None,
);
(state, pay)
}
const OLD_BOX: &str = "95.111.208.165";
/// ★ **The operator site takes over its own address records, and nothing else** — the live
/// holger-rs case: the apex's and www.'s A records (and an AAAA and a CNAME) were set by hand
/// for an old box; the lay replaces them, marks them, says which, and the mail records at the
/// apex and the records on other names stand.
#[test]
fn an_operator_site_order_takes_over_its_own_address_records_and_leaves_the_rest() {
let zone = FakeCloudflare::with(vec![
row("gunnar.rs", "A", OLD_BOX),
row("gunnar.rs", "AAAA", "2a04:3540::dead"),
row("www.gunnar.rs", "A", OLD_BOX),
row("git.gunnar.rs", "CNAME", "old.gunnar.rs"),
row("gunnar.rs", "MX", "mail.protonmail.ch"),
row("gunnar.rs", "TXT", "protonmail-verification=abc"),
row("gunnar.rs", "CAA", "0 issue \"letsencrypt.org\""),
row("_dmarc.gunnar.rs", "TXT", "v=DMARC1; p=quarantine"),
row("mail.gunnar.rs", "A", OLD_BOX),
]);
let (state, pay) = site_rig_over("dns-takeover-site", zone.clone());
let run = site::start_site(
&state,
"gunnar-rs",
"ops@example.invalid",
&EstateSizes::default(),
)
.expect("the operator's site order is taken");
pay.authorize(&run.handle.clone().unwrap().id);
drive(&state, "gunnarrs");
let run = state.estates.get("gunnarrs").unwrap();
assert!(
!run.why.contains("did not make"),
"the lay is not stuck on the old records: {}",
run.why
);
assert!(
phases(&run).contains(&"installing".to_string()),
"{:#?}",
run.history
);
let front = run
.dns
.first()
.map(|r| r.value.clone())
.expect("the run names its records");
assert_ne!(front, OLD_BOX);
// Exactly the names it writes were handed over for takeover.
assert_eq!(
zone.asked.lock().unwrap().first().cloned(),
Some(vec![
"gunnar.rs".to_string(),
"www.gunnar.rs".to_string(),
"git.gunnar.rs".to_string()
])
);
// Its own names: the new front, marked as the writer's; the old AAAA and CNAME gone.
for name in ["gunnar.rs", "www.gunnar.rs", "git.gunnar.rs"] {
assert_eq!(zone.at(name, "A"), vec![(front.clone(), true)], "{name}");
}
assert!(
zone.at("gunnar.rs", "AAAA").is_empty() && zone.at("git.gunnar.rs", "CNAME").is_empty()
);
// The rest stands: the mail records, the CAA, other names.
assert_eq!(
zone.at("gunnar.rs", "MX"),
vec![("mail.protonmail.ch".to_string(), false)]
);
assert_eq!(
zone.at("gunnar.rs", "TXT"),
vec![("protonmail-verification=abc".to_string(), false)]
);
assert_eq!(zone.at("gunnar.rs", "CAA").len(), 1);
assert_eq!(zone.at("_dmarc.gunnar.rs", "TXT").len(), 1);
assert_eq!(
zone.at("mail.gunnar.rs", "A"),
vec![(OLD_BOX.to_string(), false)],
"another name is never taken over"
);
// The history says what was replaced: name, type, old target.
let said: Vec<&str> = run
.history
.iter()
.map(|t| t.note.as_str())
.filter(|n| n.contains("took over"))
.collect();
assert!(
!said.is_empty(),
"the history names the takeover: {:#?}",
run.history
);
for old in [
format!("A gunnar.rs -> {OLD_BOX}"),
format!("A www.gunnar.rs -> {OLD_BOX}"),
"AAAA gunnar.rs -> 2a04:3540::dead".into(),
"CNAME git.gunnar.rs -> old.gunnar.rs".into(),
] {
assert!(said.iter().any(|n| n.contains(&old)), "{old} in {said:?}");
}
assert!(
!said
.iter()
.any(|n| n.contains("MX") || n.contains("mail.gunnar.rs")),
"{said:?}"
);
}
/// ★ **A SOLD estate still refuses a record its writer did not make** — nothing is taken over,
/// the lay waits, the hand-made record stands.
#[test]
fn a_sold_estate_still_refuses_a_record_its_writer_did_not_make() {
let zone = FakeCloudflare::with(vec![row("acme.gunnar.rs", "A", OLD_BOX)]);
let dir = tmp("dns-takeover-sold");
let (pay, cloud) = (HoldPay::default(), Arc::new(MockCloud::new()));
let dns: Arc<dyn EstateDns> = zone.clone();
let (state, ..) = build_with(
&dir,
cfg(),
pay.clone(),
cloud,
FakeKit::new("PASS"),
Some(dns),
None,
);
start(
&state,
"acme",
"buyer@example.com",
"",
&EstateSizes::default(),
)
.unwrap();
pay.authorize(&state.estates.get("acme").unwrap().handle.unwrap().id);
drive(&state, "acme");
let run = state.estates.get("acme").unwrap();
assert_eq!(run.phase, phase::LAYING, "{:#?}", run.history);
assert!(
run.why.contains("did not make") && run.why.contains("acme.gunnar.rs"),
"{}",
run.why
);
assert!(
zone.asked.lock().unwrap().is_empty(),
"a sold estate never asks a takeover"
);
assert_eq!(
zone.at("acme.gunnar.rs", "A"),
vec![(OLD_BOX.to_string(), false)]
);
}