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

nordisk/monetize

crates/monetize-pay/tests/redirect.rs

//! **A charge's page URL (`Payment::status` of `Of::Page`, read through `Vendor::page_url`), across the vendors that need no socket.**
//!
//! `invoice` answering `None` is the assertion that is worth nothing on its own:
//! `None` is also what a method nobody wrote would return, and it is what a
//! `redirect` that ignored its argument would return for everything. So the two
//! vendors are asserted TOGETHER, in the same test, over the same handles — one
//! that has a checkout page and one that cannot have one. A single
//! implementation cannot satisfy both halves by accident.
//!
//! Both are feature-gated vendors, so this file only exists when both are
//! compiled in (`--all-features`, which the suite runs).

#![cfg(all(feature = "invoice", feature = "sandbox"))]

use monetize_pay::invoice::{InvoiceConfig, InvoiceProvider};
use monetize_pay::sandbox::{SandboxConfig, SandboxProvider};
use monetize_pay::{ChargeHandle, ChargeRequest, Currency, Market, Money, Payment, Vendor};

/// The page URL as the host reads it: through the ten ops.
fn page_url(p: &dyn Payment, h: &ChargeHandle) -> Option<String> {
    Vendor::new("t", &[], p).page_url(h)
}

fn request(reference: &str) -> ChargeRequest {
    ChargeRequest {
        reference: reference.into(),
        amount: Money {
            minor: 12_500,
            currency: Currency::Sek,
        },
        market: Market::Se,
        description: "gunnar pro 2026-09".into(),
        payer: None,
    }
}

/// (r) A vendor WITH a checkout page and a vendor WITHOUT one, side by side.
///
/// The sandbox mints a URL per order, the way Klarna and Stripe do; the invoice
/// has none, because a bankgiro reference is paid from the customer's own bank
/// and there is no page to send anyone to. Before this method existed monetize
/// built both from the same `checkout_url` template, which meant every invoice
/// sale put a dead link in front of an operator.
#[test]
fn a_vendor_with_a_checkout_page_answers_and_one_without_says_so() {
    let sandbox = SandboxProvider::new(SandboxConfig {
        callback_secret: "s3cret".into(),
        state_path: None,
    });
    let invoice = InvoiceProvider::new(InvoiceConfig {
        import_token: "import-secret".into(),
        bankgiro: Some("5050-1055".into()),
    });

    let sandbox_handle = sandbox.reserve(&request("gunnar/acme/2026-09")).unwrap();
    let invoice_handle = invoice.reserve(&request("gunnar/acme/2026-09")).unwrap();

    let url = page_url(&sandbox, &sandbox_handle)
        .expect("the sandbox rehearses the checkout step, so it has a link");
    assert!(
        url.ends_with(&sandbox_handle.id),
        "the link names THIS order, not a constant: {url}"
    );
    assert!(
        url.starts_with("https://checkout.sandbox.invalid/"),
        "and it is under the RFC 2606 `.invalid` TLD, which can never be delegated — a rehearsal must not be able to send a real \
         customer anywhere real: {url}"
    );

    assert_eq!(
        page_url(&invoice, &invoice_handle),
        None,
        "a bankgiro invoice has no checkout page; the customer pays the reference from their own bank"
    );

    // …and the two are not simply agreeing by accident: the SAME reference gave
    // one a link and the other none, and neither answers for a handle it did not
    // mint or for another vendor's.
    assert_ne!(
        page_url(&sandbox, &sandbox_handle),
        page_url(&invoice, &invoice_handle)
    );
    assert_eq!(
        page_url(
            &sandbox,
            &ChargeHandle {
                vendor: "sandbox".into(),
                id: "sbx_never".into()
            }
        ),
        None,
        "an order it never minted"
    );
    assert_eq!(
        page_url(
            &sandbox,
            &ChargeHandle {
                vendor: "klarna".into(),
                id: sandbox_handle.id.clone()
            }
        ),
        None,
        "not our vendor"
    );
    assert_eq!(
        page_url(
            &invoice,
            &ChargeHandle {
                vendor: "invoice".into(),
                id: "nonsense".into()
            }
        ),
        None
    );
}

/// (r) The sandbox's link keeps working across the whole money path — reserve,
/// capture, release — because a console re-renders an outcome whenever an
/// operator opens the page, and a link that vanished after capture would look
/// like a bug in the sale.
#[test]
fn the_sandbox_link_is_stable_for_the_life_of_the_charge() {
    let sandbox = SandboxProvider::new(SandboxConfig {
        callback_secret: "s3cret".into(),
        state_path: None,
    });
    let a = sandbox.reserve(&request("gunnar/one/2026-09")).unwrap();
    let b = sandbox.reserve(&request("gunnar/two/2026-09")).unwrap();
    assert_ne!(a.id, b.id, "two references, two charges");

    let before = page_url(&sandbox, &a).unwrap();
    assert_ne!(
        Some(before.clone()),
        page_url(&sandbox, &b),
        "two charges, two links"
    );
    sandbox.capture(&a).unwrap();
    assert_eq!(
        page_url(&sandbox, &a),
        Some(before.clone()),
        "still the same link after capture"
    );
    sandbox.release(&b).unwrap();
    assert!(
        page_url(&sandbox, &b).is_some(),
        "and a released charge still says where it had been"
    );
}