crates/lake-product/src/signed.rs
//! **The signed guests' axes of the Lake bench** (`--cloud upcloud|scaleway`,
//! `--vendor stripe|klarna|swish`): monetize-impl's signed cloud and payment guests, loaded by
//! the ONE box's own registry from the blocks [`blocks`] writes, against their fakes:
//!
//! | axis | the guest (signed here, under the run's CA) | its fake |
//! |---|---|---|
//! | `upcloud` | `guests/upcloud` | `mock-upcloud`, in process (`mock_upcloud::serve`) |
//! | `scaleway` | `guests/scaleway` | `mock-scaleway`, in process (`Mock::serve_on`) |
//! | `stripe` | `guests/stripe` | `fake_stripe_serve`, a process; its control door is the buyer |
//! | `klarna` | `guests/klarna` | `fake_klarna_serve`, a process; its control door is the buyer |
//! | `swish` | `guests/swish` | `fake_swish_serve`, a process; its control door is the payer |
//!
//! The open repo holds none of them: `cargo xtask bench lake` builds them in the sibling
//! `../monetize-impl` at run time and hands this bench ONE directory ([`Signed`]) with
//! `<axis>.wasm`, `fake_<vendor>_serve` and, for Swish, `swish-client.pem` (the sibling's test
//! certificate, not a secret). A missing file is refused by name.
//!
//! The money flows for real through the guest: the fake's page is paid by its control door,
//! the vendor's signed callback is posted to the ONE box's `/callback/<kind>/lake`, the core
//! sees the hold through the guest's `status` and captures it through the guest's `capture`.
use std::collections::BTreeMap;
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::say;
/// The signed clouds and vendors.
pub const CLOUDS: [&str; 2] = ["upcloud", "scaleway"];
pub const VENDORS: [&str; 3] = ["stripe", "klarna", "swish"];
/// The vendors' instance in the bench's config (`[vendors.<kind>.lake]`).
pub const INSTANCE: &str = "lake";
/// The test credentials the fakes are started with and the secrets hold (never real ones).
pub const STRIPE_KEY: &str = "sk_test_LAKEbenchNEVERlive";
pub const STRIPE_WEBHOOK: &str = "whsec_test_LAKEbench";
pub const KLARNA_USER: &str = "PK_test_lake_bench";
pub const KLARNA_PASSWORD: &str = "lake-bench-klarna-test-password";
pub const KLARNA_CALLBACK: &str = "lakecb:lake-bench-callback-test";
pub const SWISH_PAYEE: &str = "1234679304";
pub const SWISH_CALLBACK: &str = "LAKEbenchSWISHcallbackIDENTIFIER0001";
pub const UPCLOUD_TOKEN: &str = "ucat_mock_lake_bench_not_a_real_token";
/// The zones the signed clouds lay in.
pub const UPCLOUD_ZONE: &str = "se-sto1";
pub const SCALEWAY_ZONE: &str = "fr-par-1";
/// Where the vendors are told the ONE box takes their callbacks (the bench posts them itself).
pub const SHOP: &str = "https://one.mock.invalid";
/// Is `name` one of the signed axes?
pub fn is_signed(name: &str) -> bool {
CLOUDS.contains(&name) || VENDORS.contains(&name)
}
/// **The directory the xtask hands the bench**: the built guests and fakes, by name.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Signed(pub PathBuf);
impl Signed {
/// The guest module of `axis` (`<axis>.wasm`), refused by name when it is not there.
pub fn guest(&self, axis: &str) -> Result<PathBuf, String> {
self.file(&format!("{axis}.wasm"), &format!("the signed {axis} guest"))
}
/// The fake of `vendor` (`fake_<vendor>_serve`).
pub fn fake(&self, vendor: &str) -> Result<PathBuf, String> {
self.file(
&format!("fake_{vendor}_serve"),
&format!("the fake {vendor} (monetize-pay-impl's example fake_{vendor}_serve)"),
)
}
/// Swish's test client certificate (`swish-client.pem`, monetize-impl's test fixture).
pub fn swish_pem(&self) -> Result<PathBuf, String> {
self.file("swish-client.pem", "Swish's test client certificate")
}
fn file(&self, name: &str, what: &str) -> Result<PathBuf, String> {
let p = self.0.join(name);
if p.is_file() {
Ok(p)
} else {
Err(format!("{what} is not at {}: `cargo xtask bench lake` builds it from the sibling ../monetize-impl", p.display()))
}
}
/// Every file `cloud` and `vendor` need, refused by the first missing name. A dummy axis
/// needs nothing.
pub fn check(&self, cloud: &str, vendor: &str) -> Result<(), String> {
if CLOUDS.contains(&cloud) {
self.guest(cloud)?;
}
if VENDORS.contains(&vendor) {
self.guest(vendor)?;
self.fake(vendor)?;
if vendor == "swish" {
self.swish_pem()?;
}
}
Ok(())
}
}
/// The vendor's name in the registry (`<kind>/lake`).
pub fn vendor_name(vendor: &str) -> String {
format!("{vendor}/{INSTANCE}")
}
/// The sale's currency a vendor takes: Swish takes SEK only, the others EUR.
pub fn vendor_currency(vendor: &str) -> &'static str {
if vendor == "swish" {
"SEK"
} else {
"EUR"
}
}
fn free_port() -> Result<u16, String> {
Ok(std::net::TcpListener::bind("127.0.0.1:0")
.and_then(|l| l.local_addr())
.map_err(|e| e.to_string())?
.port())
}
fn listening(port: u16) -> bool {
std::net::TcpStream::connect(("127.0.0.1", port)).is_ok()
}
/// A guest copied into the run and signed under the run's CA.
fn sign_into(
dir: &Path,
from: &Path,
name: &str,
signer: &dyn guest_host::verify::sign::ArchiveSigner,
) -> Result<PathBuf, String> {
let module = dir.join(format!("{name}.wasm"));
std::fs::copy(from, &module).map_err(|e| format!("{}: {e}", from.display()))?;
guest_host::verify::sign::sign_module_file(&module, signer)?;
Ok(module)
}
// ── the clouds ─────────────────────────────────────────────────────────────────────────────
/// **A signed cloud's fake, in this process**, and the media the shop lays from (the
/// release's storages or snapshots at the mock, by kind and release).
pub enum CloudFake {
UpCloud {
mock: Arc<mock_upcloud::Mock>,
base: String,
},
Scaleway {
mock: Arc<mock_scaleway::Mock>,
},
}
impl CloudFake {
/// Started on loopback, with `media` (`lake-r1`, `lake-keep-r1`, …) made at the mock.
pub async fn start(
cloud: &str,
media: &[String],
) -> Result<(CloudFake, BTreeMap<String, String>), String> {
let mut ids = BTreeMap::new();
match cloud {
"upcloud" => {
let mock = mock_upcloud::Mock::recording(mock_upcloud::estate::Estate::new(
mock_upcloud::Clock::new(1_000_000),
mock_upcloud::Faults::none(),
11,
));
{
let mut e = mock.estate.lock().unwrap();
for m in media {
ids.insert(
m.clone(),
e.create_storage(m, 1, "maxiops", UPCLOUD_ZONE, Vec::new())
.map_err(|r| {
format!("the UpCloud mock refused the medium {m}: {r:?}")
})?,
);
}
}
let (port, _task) = mock_upcloud::serve(mock.clone(), "127.0.0.1:0")
.await
.map_err(|e| format!("mock-upcloud: {e}"))?;
say(
"bench",
"cloud",
"bench",
&format!(
"mock-upcloud at 127.0.0.1:{port}, {} medium storage(s)",
ids.len()
),
);
Ok((
CloudFake::UpCloud {
mock,
base: format!("http://127.0.0.1:{port}"),
},
ids,
))
}
"scaleway" => {
let mock = mock_scaleway::Mock::new(mock_scaleway::Clock::new(1_000_000));
let port = mock
.serve_on("127.0.0.1:0")
.map_err(|e| format!("mock-scaleway: {e}"))?;
// A release medium at Scaleway is a SNAPSHOT: a volume made, then snapshotted.
for m in media {
let (project, tok) = (mock.project(), mock.token());
let body = serde_json::json!({ "name": m, "project": project, "volume_type": "b_ssd", "size": 20_000_000_000u64 });
let vol = mock.with(|a| {
match a.handle(
"POST",
&format!("/instance/v1/zones/{SCALEWAY_ZONE}/volumes"),
&[],
Some(&tok),
Some("application/json"),
&serde_json::to_vec(&body).unwrap_or_default(),
) {
mock_scaleway::Answer::Json { body, .. } => {
body["volume"]["id"].as_str().map(str::to_string)
}
_ => None,
}
});
let vol = vol.ok_or_else(|| {
format!("the Scaleway mock made no volume for the medium {m}")
})?;
let snap = mock.snapshot_of(&vol, &[]).ok_or_else(|| {
format!("the Scaleway mock made no snapshot for the medium {m}")
})?;
ids.insert(m.clone(), snap);
}
say(
"bench",
"cloud",
"bench",
&format!(
"mock-scaleway at 127.0.0.1:{port}, {} medium snapshot(s)",
ids.len()
),
);
Ok((CloudFake::Scaleway { mock }, ids))
}
other => Err(format!("{other} is not a signed cloud ({CLOUDS:?})")),
}
}
pub fn name(&self) -> &'static str {
match self {
CloudFake::UpCloud { .. } => "upcloud",
CloudFake::Scaleway { .. } => "scaleway",
}
}
pub fn zone(&self) -> &'static str {
match self {
CloudFake::UpCloud { .. } => UPCLOUD_ZONE,
CloudFake::Scaleway { .. } => SCALEWAY_ZONE,
}
}
/// The servers the mock itself still holds (its own word, beside the guest's inventory).
pub fn servers_left(&self) -> usize {
match self {
CloudFake::UpCloud { mock, .. } => mock
.estate
.lock()
.map(|e| e.servers_matching(&[]).len())
.unwrap_or(usize::MAX),
CloudFake::Scaleway { mock } => mock.servers().len(),
}
}
/// **The keep medium, played** (Scaleway): a machine booted from a keep medium installs and
/// powers itself off, which mock-upcloud models and mock-scaleway does not. While `run` is
/// set, a running server whose boot volume is a keep clone (`…-keep…`) is powered off, as
/// its medium would. UpCloud: nothing to play.
pub fn play_keep_media(
&self,
run: Arc<std::sync::atomic::AtomicBool>,
) -> Option<std::thread::JoinHandle<()>> {
let CloudFake::Scaleway { mock } = self else {
return None;
};
let mock = mock.clone();
Some(std::thread::spawn(move || {
while run.load(std::sync::atomic::Ordering::SeqCst) {
for s in mock.servers() {
let boot = s["volumes"]["0"]["name"].as_str().unwrap_or_default();
if s["state"] == "running" && boot.contains("-keep") {
let (id, tok) = (
s["id"].as_str().unwrap_or_default().to_string(),
mock.token(),
);
let zone = s["zone"].as_str().unwrap_or(SCALEWAY_ZONE).to_string();
mock.with(|a| {
a.handle(
"POST",
&format!("/instance/v1/zones/{zone}/servers/{id}/action"),
&[],
Some(&tok),
Some("application/json"),
br#"{"action":"poweroff"}"#,
)
});
say(
"bench",
"keep",
"bench",
&format!(
"the keep medium of {} installed and powered it off",
s["name"].as_str().unwrap_or(&id)
),
);
}
}
std::thread::sleep(Duration::from_millis(200));
}
}))
}
/// `[clouds.<name>]`, the guest signed into `dir`, its key a secret in `secrets`.
fn block(
&self,
dir: &Path,
signed: &Signed,
signer: &dyn guest_host::verify::sign::ArchiveSigner,
root: &Path,
secrets: &Path,
) -> Result<String, String> {
let module = sign_into(dir, &signed.guest(self.name())?, self.name(), signer)?;
Ok(match self {
CloudFake::UpCloud { base, .. } => {
write_secret(secrets, "upcloud_token", UPCLOUD_TOKEN)?;
cloud_block(
"upcloud",
&module,
root,
base,
UPCLOUD_ZONE,
"Bearer ",
"",
"upcloud_token",
"country = \"FI\"\nallow_grow = true\n",
)
}
CloudFake::Scaleway { mock } => {
write_secret(secrets, "scaleway_token", &mock.token())?;
cloud_block(
"scaleway",
&module,
root,
&mock.base_url(),
SCALEWAY_ZONE,
"",
"X-Auth-Token",
"scaleway_token",
&format!(
"country = \"FR\"\nproject = {:?}\npoll_ms = 5\nwait_s = 60\n",
mock.project()
),
)
}
})
}
}
/// Pure: **a signed cloud's block** — the guest, its trust root, its grants and hosts, the key
/// the HOST presents (`http_auth`), and a spend cap.
#[allow(clippy::too_many_arguments)]
pub fn cloud_block(
name: &str,
module: &Path,
root: &Path,
base: &str,
zone: &str,
prefix: &str,
header: &str,
secret: &str,
extra: &str,
) -> String {
let header = if header.is_empty() {
String::new()
} else {
format!("header = {header:?}\n")
};
format!(
"\n[clouds.{name}]\nenabled = true\nguest = {:?}\nguest_trust_roots = [{:?}]\ngrants = [\"http\", \"state\"]\nhttp_hosts = [{base:?}]\napi_base = {base:?}\nzone = {zone:?}\ncap = {{ monthly_minor = 100000000, currency = \"Eur\" }}\n{extra}[[clouds.{name}.http_auth]]\nhost = {base:?}\n{header}prefix = {prefix:?}\nsecret = {secret:?}\n",
module.display().to_string(),
root.display().to_string()
)
}
// ── the vendors ────────────────────────────────────────────────────────────────────────────
/// **A signed vendor's fake, as a process** (only this child is ever stopped): its API, which
/// the guest dials, and its control door, which is the buyer.
pub struct VendorFake {
pub kind: String,
child: Child,
pub api: u16,
pub control: u16,
}
impl VendorFake {
pub fn start(kind: &str, signed: &Signed) -> Result<VendorFake, String> {
let bin = signed.fake(kind)?;
let (api, control) = (free_port()?, free_port()?);
let (a, c) = (api.to_string(), control.to_string());
let args: Vec<&str> = match kind {
"stripe" => vec![
"--port",
&a,
"--control",
&c,
"--key",
STRIPE_KEY,
"--webhook",
STRIPE_WEBHOOK,
"--ids-like-stripe",
"true",
],
"klarna" => vec![
"--port",
&a,
"--control",
&c,
"--user",
KLARNA_USER,
"--password",
KLARNA_PASSWORD,
],
"swish" => vec!["--port", &a, "--control", &c, "--payee", SWISH_PAYEE],
other => return Err(format!("{other} is not a signed vendor ({VENDORS:?})")),
};
let child = Command::new(&bin)
.args(&args)
.stdout(Stdio::null())
.stderr(Stdio::inherit())
.spawn()
.map_err(|e| format!("{}: {e}", bin.display()))?;
let mut fake = VendorFake {
kind: kind.into(),
child,
api,
control,
};
let until = Instant::now() + Duration::from_secs(20);
while !(listening(api) && listening(control)) {
if let Ok(Some(st)) = fake.child.try_wait() {
return Err(format!("the fake {kind} ended before it listened: {st}"));
}
if Instant::now() > until {
return Err(format!(
"the fake {kind} did not listen on {api}/{control} in 20 s"
));
}
std::thread::sleep(Duration::from_millis(50));
}
say(
"bench",
"vendor",
"bench",
&format!(
"fake {kind} (pid {}) at 127.0.0.1:{api}, its buyer's door at 127.0.0.1:{control}",
fake.child.id()
),
);
Ok(fake)
}
pub fn base(&self) -> String {
format!("http://127.0.0.1:{}", self.api)
}
fn control(&self, method: &str, path: &str) -> Result<serde_json::Value, String> {
let addr: SocketAddr = ([127, 0, 0, 1], self.control).into();
// The fakes' control doors read the request line only and close: a reset while the rest
// of the request is still being written is asked again.
let mut tries = 0;
let answer = loop {
match crate::http::call(addr, method, path, "") {
Err(e) if tries < 5 && (e.contains("reset") || e.contains("Broken pipe")) => {
tries += 1;
std::thread::sleep(Duration::from_millis(100));
}
other => break other?,
}
};
match answer {
(200, b) => serde_json::from_str(&b)
.map_err(|e| format!("the fake {}'s {method} {path}: {e}: {b}", self.kind)),
(s, b) => Err(format!(
"the fake {}'s page refused {method} {path}: {s} {b}",
self.kind
)),
}
}
/// **The buyer pays the page of `handle`** (`checkout` is the page's URL) at the fake, and
/// the vendor's signed callback is delivered to the ONE box at `one`. Answers what happened.
pub fn pay(
&self,
one: SocketAddr,
handle: &monetize_pay::ChargeHandle,
checkout: &str,
) -> Result<String, String> {
let (path, headers, body, what) = match self.kind.as_str() {
"stripe" => {
let cs = session_of(checkout);
let ev = self.control("POST", &format!("/pay/{cs}"))?;
let sig = ev["signature"].as_str().unwrap_or_default().to_string();
(
"/callback/stripe/lake",
vec![("Stripe-Signature".to_string(), sig)],
ev["body"].as_str().unwrap_or_default().to_string(),
format!("paid {cs} at the fake Stripe's page"),
)
}
"klarna" => {
let ev = self.control("POST", &format!("/authorize/{}", handle.id))?;
let auth = ev["authorization"].as_str().unwrap_or_default().to_string();
(
"/callback/klarna/lake",
vec![("Authorization".to_string(), auth)],
ev["body"].as_str().unwrap_or_default().to_string(),
format!(
"authorized {} at the fake Klarna's page (order {})",
handle.id,
ev["order"].as_str().unwrap_or("?")
),
)
}
"swish" => {
let ev = self.control("POST", &format!("/pay/{}", handle.id))?;
let ci = ev["callbackIdentifier"]
.as_str()
.unwrap_or_default()
.to_string();
(
"/callback/swish/lake",
vec![("callbackIdentifier".to_string(), ci)],
ev["body"].as_str().unwrap_or_default().to_string(),
format!("paid {} in the fake Swish app", handle.id),
)
}
other => return Err(format!("{other} is not a signed vendor")),
};
let mut h = headers;
h.push(("Content-Type".into(), "application/json".into()));
match crate::http::call_with(one, "POST", path, &h, &body)? {
(200..=299, b) => {
say(
"vendor",
"callback",
&self.kind,
&format!("{path} answered {}", b.trim()),
);
Ok(format!("{what}; its callback taken at {path}"))
}
(s, b) => Err(format!(
"{what}; the ONE box refused its callback at {path}: {s} {b}"
)),
}
}
/// The fake's own word on the page's money, where its control door says it (Stripe's
/// `GET /calls/<cs>`: what it was asked to capture, cancel or expire).
pub fn calls_word(&self, checkout: &str) -> Option<String> {
if self.kind != "stripe" {
return None;
}
self.control("GET", &format!("/calls/{}", session_of(checkout)))
.ok()
.map(|v| format!("the fake Stripe was asked {v}"))
}
/// `[vendors.<kind>.lake]`, the guest signed into `dir`, its secrets in `secrets`.
fn block(
&self,
dir: &Path,
signed: &Signed,
signer: &dyn guest_host::verify::sign::ArchiveSigner,
root: &Path,
secrets: &Path,
) -> Result<String, String> {
let module = sign_into(dir, &signed.guest(&self.kind)?, &self.kind, signer)?;
let base = self.base();
Ok(match self.kind.as_str() {
"stripe" => {
write_secret(secrets, "stripe_lake_key", STRIPE_KEY)?;
write_secret(secrets, "stripe_lake_webhook", STRIPE_WEBHOOK)?;
vendor_block("stripe", &module, root, &base, "stripe_lake_webhook", &format!("country = \"IE\"\nbase_url = {base:?}\nsuccess_url = \"https://lake.mock.invalid/paid/{{reference}}\"\ncancel_url = \"https://lake.mock.invalid/cancel/{{reference}}\"\nlivemode = false\nwebhook_secret = \"stripe_lake_webhook\"\n"), "prefix = \"Bearer \"\nsecret = \"stripe_lake_key\"\n")
}
"klarna" => {
write_secret(secrets, "klarna_user", KLARNA_USER)?;
write_secret(secrets, "klarna_password", KLARNA_PASSWORD)?;
write_secret(secrets, "klarna_callback", KLARNA_CALLBACK)?;
vendor_block("klarna", &module, root, &base, "klarna_callback", &format!("country = \"FI\"\nbase_url = {base:?}\ncallback_url = \"{SHOP}/callback/klarna/lake\"\nreturn_url = \"https://lake.mock.invalid/paid/{{reference}}\"\n"), "basic_user_secret = \"klarna_user\"\nsecret = \"klarna_password\"\nbody_secrets = [\"klarna_callback\"]\n")
}
"swish" => {
let pem = std::fs::read(signed.swish_pem()?).map_err(|e| e.to_string())?;
write_secret(secrets, "swish_client", &String::from_utf8_lossy(&pem))?;
write_secret(secrets, "swish_callback", SWISH_CALLBACK)?;
vendor_block("swish", &module, root, &base, "swish_callback", &format!("country = \"SE\"\nbase_url = \"{base}/swish-cpcapi\"\npayee_alias = {SWISH_PAYEE:?}\ncallback_url = \"{SHOP}/callback/swish/lake\"\ncallback_secret = \"swish_callback\"\n"), "client_cert_secret = \"swish_client\"\nbody_secrets = [\"swish_callback\"]\n")
}
other => return Err(format!("{other} is not a signed vendor")),
})
}
}
impl Drop for VendorFake {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
/// Pure: **a signed vendor's block** — the guest, its trust root, its grants and host, the
/// secret its callbacks are checked with on the host (`hmac_secrets`), and the credential the
/// HOST presents (`http_auth`).
pub fn vendor_block(
kind: &str,
module: &Path,
root: &Path,
base: &str,
hmac: &str,
settings: &str,
auth: &str,
) -> String {
format!(
"\n[vendors.{kind}.{INSTANCE}]\nenabled = true\nguest = {:?}\nguest_trust_roots = [{:?}]\ngrants = [\"http\", \"hmac\", \"state\"]\nhttp_hosts = [{base:?}]\nhmac_secrets = [{hmac:?}]\n{settings}[[vendors.{kind}.{INSTANCE}.http_auth]]\nhost = {base:?}\n{auth}",
module.display().to_string(),
root.display().to_string()
)
}
/// The checkout session of a Stripe page URL (`…/c/pay/cs_test_…#…`).
pub fn session_of(url: &str) -> String {
url.rsplit('/')
.next()
.unwrap_or_default()
.split('#')
.next()
.unwrap_or_default()
.split('?')
.next()
.unwrap_or_default()
.to_owned()
}
fn write_secret(dir: &Path, name: &str, value: &str) -> Result<(), String> {
monetize_product_wasm::secrets::write(dir, name, value.as_bytes()).map(|_| ())
}
/// **The signed blocks of the run**: a CA minted for it, every chosen guest signed under it,
/// the blocks for `monetize.toml` (to be appended) and the secrets written owner-only into
/// `secrets`.
pub fn blocks(
dir: &Path,
signed: &Signed,
cloud: Option<&CloudFake>,
vendor: Option<&VendorFake>,
secrets: &Path,
) -> Result<String, String> {
use guest_host::verify::sign::dev;
let (ca_key, ca_der) = dev::mint_ca("lake-bench-signed-root").map_err(|e| e.to_string())?;
let root = dir.join("signed-root.der");
std::fs::write(&root, &ca_der).map_err(|e| e.to_string())?;
let signer = dev::new_ed25519_signer(&ca_key, &ca_der, "lake-bench-signed-signing")
.map_err(|e| e.to_string())?;
let mut t = String::new();
if let Some(c) = cloud {
t.push_str(&c.block(dir, signed, &signer, &root, secrets)?);
}
if let Some(v) = vendor {
t.push_str(&v.block(dir, signed, &signer, &root, secrets)?);
}
Ok(t)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_signed_axes_are_named_and_a_missing_file_is_refused_by_name() {
for a in ["upcloud", "scaleway", "stripe", "klarna", "swish"] {
assert!(is_signed(a), "{a}");
}
assert!(!is_signed("mock") && !is_signed("sandbox"));
let dir = std::env::temp_dir().join(format!("lake-signed-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let s = Signed(dir.clone());
assert_eq!(s.check("mock", "sandbox"), Ok(()), "a dummy needs nothing");
let e = s.check("upcloud", "mock").unwrap_err();
assert!(
e.contains("the signed upcloud guest") && e.contains("../monetize-impl"),
"{e}"
);
std::fs::write(dir.join("upcloud.wasm"), b"x").unwrap();
std::fs::write(dir.join("swish.wasm"), b"x").unwrap();
let e = s.check("upcloud", "swish").unwrap_err();
assert!(e.contains("fake_swish_serve"), "{e}");
std::fs::write(dir.join("fake_swish_serve"), b"x").unwrap();
let e = s.check("upcloud", "swish").unwrap_err();
assert!(e.contains("swish-client.pem"), "{e}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_cloud_block_names_its_guest_its_root_its_grants_and_the_key_the_host_presents() {
let b = cloud_block(
"scaleway",
Path::new("/r/scaleway.wasm"),
Path::new("/r/root.der"),
"http://127.0.0.1:9",
SCALEWAY_ZONE,
"",
"X-Auth-Token",
"scaleway_token",
"project = \"p\"\n",
);
let t: toml::Value = toml::from_str(&b).unwrap();
let c = &t["clouds"]["scaleway"];
assert_eq!(c["guest"].as_str(), Some("/r/scaleway.wasm"));
assert_eq!(c["guest_trust_roots"][0].as_str(), Some("/r/root.der"));
assert_eq!(c["grants"].as_array().unwrap().len(), 2);
assert_eq!(c["http_hosts"][0].as_str(), Some("http://127.0.0.1:9"));
assert_eq!(c["http_auth"][0]["header"].as_str(), Some("X-Auth-Token"));
assert_eq!(c["http_auth"][0]["secret"].as_str(), Some("scaleway_token"));
assert!(c["cap"]["monthly_minor"].as_integer().unwrap() > 0);
let u: toml::Value = toml::from_str(&cloud_block(
"upcloud",
Path::new("/u.wasm"),
Path::new("/root.der"),
"http://127.0.0.1:8",
UPCLOUD_ZONE,
"Bearer ",
"",
"upcloud_token",
"",
))
.unwrap();
assert!(
u["clouds"]["upcloud"]["http_auth"][0]
.get("header")
.is_none(),
"UpCloud's key is the Authorization header"
);
}
#[test]
fn a_vendor_block_names_its_guest_its_hmac_secret_and_its_credential() {
let b = vendor_block(
"klarna",
Path::new("/k.wasm"),
Path::new("/root.der"),
"http://127.0.0.1:7",
"klarna_callback",
"base_url = \"http://127.0.0.1:7\"\n",
"basic_user_secret = \"klarna_user\"\nsecret = \"klarna_password\"\n",
);
let t: toml::Value = toml::from_str(&b).unwrap();
let v = &t["vendors"]["klarna"]["lake"];
assert_eq!(v["guest"].as_str(), Some("/k.wasm"));
assert_eq!(
v["grants"]
.as_array()
.unwrap()
.iter()
.map(|g| g.as_str().unwrap())
.collect::<Vec<_>>(),
["http", "hmac", "state"]
);
assert_eq!(v["hmac_secrets"][0].as_str(), Some("klarna_callback"));
assert_eq!(
v["http_auth"][0]["basic_user_secret"].as_str(),
Some("klarna_user")
);
assert_eq!(vendor_name("klarna"), "klarna/lake");
assert_eq!(
(vendor_currency("swish"), vendor_currency("stripe")),
("SEK", "EUR")
);
assert_eq!(
session_of("https://pay.mock.invalid/c/pay/cs_test_7#frag"),
"cs_test_7"
);
}
}