Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

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)]
    );
}