use std::fs;
use assert_cmd::Command;
use predicates::prelude::*;
use serde_json::Value;
use tempfile::tempdir;
use uselesskey_test_support::{TestResult, ensure_eq, require_ok, require_some};
const WEBHOOK_SEED: &str = "webhook-profile-integration-seed";
const WEBHOOK_LABEL: &str = "webhook-integration";
fn run_bundle(bundle_dir: &std::path::Path) -> TestResult<()> {
let mut cmd = require_ok(Command::cargo_bin("uselesskey"), "bin exists")?;
cmd.args([
"bundle",
"--profile",
"webhook",
"--seed",
WEBHOOK_SEED,
"--label",
WEBHOOK_LABEL,
"--out",
require_some(bundle_dir.to_str(), "utf-8 path")?,
]);
cmd.assert().success();
Ok(())
}
#[test]
fn webhook_bundle_emits_expected_layout() -> TestResult<()> {
let dir = require_ok(tempdir(), "tempdir")?;
let bundle_dir = dir.path().join("webhook");
run_bundle(&bundle_dir)?;
for relative in [
"requests/valid.json",
"requests/negative-tampered-body.json",
"requests/negative-wrong-secret.json",
"requests/negative-stale-timestamp.json",
"requests/negative-missing-signature.json",
"requests/negative-malformed-signature.json",
"evidence/webhook-profile.md",
"receipts/materialization.json",
"receipts/audit-surface.json",
"manifest.json",
] {
let path = bundle_dir.join(relative);
assert!(
path.exists(),
"expected bundle file missing: {}",
path.display()
);
let meta = require_ok(fs::metadata(&path), "stat fixture")?;
assert!(meta.len() > 0, "fixture {relative} should not be empty");
}
let manifest = read_json(&bundle_dir.join("manifest.json"))?;
ensure_eq!(manifest["profile"], "webhook");
ensure_eq!(manifest["seed"], WEBHOOK_SEED);
ensure_eq!(manifest["label"], WEBHOOK_LABEL);
ensure_eq!(manifest["files"][0], "requests/valid.json");
ensure_eq!(manifest["files"][6], "evidence/webhook-profile.md");
let artifacts = require_some(manifest["artifacts"].as_array(), "artifacts array")?;
ensure_eq!(artifacts.len(), 7);
assert!(
artifacts
.iter()
.all(|artifact| artifact["profile"] == "webhook")
);
assert!(
artifacts
.iter()
.filter(|artifact| artifact["kind"] == "webhook" && artifact["scanner_safe"] == false)
.count()
>= 6
);
let audit = read_json(&bundle_dir.join("receipts/audit-surface.json"))?;
ensure_eq!(audit["profile"], "webhook");
ensure_eq!(audit["scanner_safe"], false);
ensure_eq!(audit["runtime_material_count"], 6);
Ok(())
}
#[test]
fn webhook_request_fixtures_record_stable_rejection_classes() -> TestResult<()> {
let dir = require_ok(tempdir(), "tempdir")?;
let bundle_dir = dir.path().join("webhook");
run_bundle(&bundle_dir)?;
for (relative, expected_result, rejection_class) in [
("requests/valid.json", "accept", "valid"),
(
"requests/negative-tampered-body.json",
"reject",
"tampered_body",
),
(
"requests/negative-wrong-secret.json",
"reject",
"wrong_secret",
),
(
"requests/negative-stale-timestamp.json",
"reject",
"stale_timestamp",
),
(
"requests/negative-missing-signature.json",
"reject",
"missing_signature",
),
(
"requests/negative-malformed-signature.json",
"reject",
"malformed_signature",
),
] {
let fixture = read_json(&bundle_dir.join(relative))?;
ensure_eq!(fixture["method"], "POST");
ensure_eq!(fixture["path"], "/webhooks/uselesskey");
ensure_eq!(fixture["expected_result"], expected_result);
ensure_eq!(fixture["rejection_class"], rejection_class);
ensure_eq!(fixture["profile"], "webhook");
assert!(fixture["body"].is_string());
assert!(fixture["headers"].is_object());
assert!(fixture["verifier_secret"].is_string());
}
let missing = read_json(&bundle_dir.join("requests/negative-missing-signature.json"))?;
assert!(missing["headers"].get("Stripe-Signature").is_none());
let malformed = read_json(&bundle_dir.join("requests/negative-malformed-signature.json"))?;
ensure_eq!(
malformed["headers"]["Stripe-Signature"],
format!(
"t={},v1=not-a-hex-signature",
require_some(malformed["timestamp"].as_i64(), "timestamp")?
)
);
Ok(())
}
#[test]
fn webhook_bundle_is_deterministic_and_verifiable() -> TestResult<()> {
let first = require_ok(tempdir(), "tempdir1")?;
let second = require_ok(tempdir(), "tempdir2")?;
let first_dir = first.path().join("webhook");
let second_dir = second.path().join("webhook");
run_bundle(&first_dir)?;
run_bundle(&second_dir)?;
for relative in [
"requests/valid.json",
"requests/negative-tampered-body.json",
"requests/negative-wrong-secret.json",
"requests/negative-stale-timestamp.json",
"requests/negative-missing-signature.json",
"requests/negative-malformed-signature.json",
"evidence/webhook-profile.md",
"receipts/materialization.json",
"receipts/audit-surface.json",
"manifest.json",
] {
let a = require_ok(fs::read(first_dir.join(relative)), "read first")?;
let b = require_ok(fs::read(second_dir.join(relative)), "read second")?;
ensure_eq!(a, b);
}
let mut verify = require_ok(Command::cargo_bin("uselesskey"), "bin exists")?;
verify.args([
"verify-bundle",
"--path",
require_some(first_dir.to_str(), "utf-8 path")?,
]);
verify
.assert()
.success()
.stdout(predicate::str::contains("\"status\": \"ok\""));
Ok(())
}
#[test]
fn webhook_evidence_markdown_lists_all_rejection_classes() -> TestResult<()> {
let dir = require_ok(tempdir(), "tempdir")?;
let bundle_dir = dir.path().join("webhook");
run_bundle(&bundle_dir)?;
let evidence = require_ok(
fs::read_to_string(bundle_dir.join("evidence/webhook-profile.md")),
"read evidence",
)?;
assert!(evidence.contains("# Webhook contract-pack profile evidence"));
assert!(evidence.contains("uselesskey bundle --profile webhook"));
assert!(evidence.contains("| File | Expected result | Rejection class |"));
for rejection_class in [
"valid",
"tampered_body",
"wrong_secret",
"stale_timestamp",
"missing_signature",
"malformed_signature",
] {
assert!(
evidence.contains(rejection_class),
"evidence must include {rejection_class}"
);
}
assert!(evidence.contains("does not prove provider compatibility"));
Ok(())
}
fn read_json(path: &std::path::Path) -> TestResult<Value> {
let bytes = require_ok(fs::read(path), format!("read {}", path.display()))?;
require_ok(
serde_json::from_slice(&bytes),
format!("parse {}", path.display()),
)
}