use super::es256_test_keys::{ES256_PRIVATE_KEY, ES256_PUBLIC_KEY};
use super::*;
use crate::vm::Vm;
use std::sync::Arc;
fn vm() -> Vm {
let mut vm = Vm::new();
register_crypto_builtins(&mut vm);
vm
}
fn call(vm: &mut Vm, name: &str, args: Vec<VmValue>) -> Result<VmValue, VmError> {
let f = vm.builtins.get(name).unwrap().clone();
let mut out = String::new();
f(&args, &mut out)
}
fn s(v: &str) -> VmValue {
VmValue::String(arcstr::ArcStr::from(v))
}
fn jwt_claims() -> VmValue {
VmValue::dict(crate::value::DictMap::from_iter([
(crate::value::intern_key("exp"), VmValue::Int(4_102_444_800)),
(crate::value::intern_key("iat"), VmValue::Int(1_700_000_000)),
(crate::value::intern_key("iss"), s("12345")),
]))
}
fn dict(items: &[(&str, VmValue)]) -> VmValue {
VmValue::dict(
items
.iter()
.map(|(key, value)| (crate::value::intern_key(key), value.clone()))
.collect::<crate::value::DictMap>(),
)
}
#[test]
fn encode_and_hash_bytes_input_is_lossless_past_preview_cap() {
let mut vm = vm();
let raw = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcd";
assert!(raw.len() > 32);
let bytes_val = VmValue::Bytes(std::sync::Arc::new(raw.as_bytes().to_vec()));
for name in [
"base64_encode",
"base64url_encode",
"base32_encode",
"hex_encode",
"sha256",
"sha224",
"sha384",
"sha512",
"sha512_256",
"md5",
"bytes_to_base64url",
"sha256_base64url",
] {
let from_bytes = call(&mut vm, name, vec![bytes_val.clone()]).unwrap();
let from_string = call(&mut vm, name, vec![s(raw)]).unwrap();
assert_eq!(
from_bytes.display(),
from_string.display(),
"{name}: bytes and string inputs must agree (no truncation)"
);
}
let hex = call(&mut vm, "hex_encode", vec![bytes_val]).unwrap();
assert_eq!(
hex.display().len(),
raw.len() * 2,
"hex must cover all bytes"
);
}
#[test]
fn base64_empty_string() {
let mut vm = vm();
let encoded = call(&mut vm, "base64_encode", vec![s("")]).unwrap();
assert_eq!(encoded.display(), "");
let decoded = call(&mut vm, "base64_decode", vec![encoded]).unwrap();
assert_eq!(decoded.display(), "");
}
#[test]
fn base64_decode_invalid_input() {
let mut vm = vm();
let result = call(&mut vm, "base64_decode", vec![s("not-valid-base64!!!")]);
assert!(result.is_err());
}
#[test]
fn base64_binary_content() {
let mut vm = vm();
let encoded = call(&mut vm, "base64_encode", vec![s("\x00\x01\x02")]).unwrap();
let decoded = call(&mut vm, "base64_decode", vec![encoded]).unwrap();
assert_eq!(decoded.display(), "\x00\x01\x02");
}
#[test]
fn base64url_omits_padding() {
let mut vm = vm();
let encoded = call(&mut vm, "base64url_encode", vec![s("f")]).unwrap();
assert_eq!(encoded.display(), "Zg");
}
#[test]
fn base64url_decode_invalid_input() {
let mut vm = vm();
let result = call(&mut vm, "base64url_decode", vec![s("not+url/safe")]);
assert!(result.is_err());
}
#[test]
fn base32_decode_invalid_input() {
let mut vm = vm();
let result = call(&mut vm, "base32_decode", vec![s("INVALID-BASE32")]);
assert!(result.is_err());
}
#[test]
fn hex_round_trip_control_bytes() {
let mut vm = vm();
let encoded = call(&mut vm, "hex_encode", vec![s("\x00\x01\x02")]).unwrap();
assert_eq!(encoded.display(), "000102");
let decoded = call(&mut vm, "hex_decode", vec![encoded]).unwrap();
assert_eq!(decoded.display(), "\x00\x01\x02");
}
#[test]
fn hex_decode_invalid_input() {
let mut vm = vm();
let result = call(&mut vm, "hex_decode", vec![s("abc")]);
assert!(result.is_err());
}
#[test]
fn base64_encode_accepts_bytes() {
let mut vm = vm();
let encoded = call(
&mut vm,
"base64_encode",
vec![VmValue::Bytes(std::sync::Arc::new(vec![0, 1, 2]))],
)
.unwrap();
assert_eq!(encoded.display(), "AAEC");
}
#[test]
fn url_encode_handles_utf8() {
let mut vm = vm();
let result = call(&mut vm, "url_encode", vec![s("café")]).unwrap();
assert!(result.display().contains("%C3%A9"));
}
#[test]
fn url_decode_invalid_percent_passthrough() {
let mut vm = vm();
let result = call(&mut vm, "url_decode", vec![s("100%ZZ")]).unwrap();
assert_eq!(result.display(), "100%ZZ");
}
#[test]
fn url_decode_is_byte_based_and_boundary_safe() {
let mut vm = vm();
let raw = call(&mut vm, "url_decode", vec![s("%日x")]).unwrap();
assert_eq!(raw.display(), "%日x");
let multi = call(&mut vm, "url_decode", vec![s("%E6%97%A5+%F0%9F%99%82")]).unwrap();
assert_eq!(multi.display(), "日 🙂");
let signed = call(&mut vm, "url_decode", vec![s("a%+Ab")]).unwrap();
assert_eq!(signed.display(), "a% Ab");
}
#[test]
fn url_round_trip() {
let mut vm = vm();
let original = "key=hello world&foo=bar/baz";
let encoded = call(&mut vm, "url_encode", vec![s(original)]).unwrap();
let decoded = call(&mut vm, "url_decode", vec![encoded]).unwrap();
assert_eq!(decoded.display(), original);
}
#[test]
fn sha224_known_vector() {
let mut vm = vm();
let result = call(&mut vm, "sha224", vec![s("test")]).unwrap();
assert_eq!(
result.display(),
"90a3ed9e32b2aaf4c61c410eb925426119e1a9dc53d4286ade99a809"
);
}
#[test]
fn sha384_known_vector() {
let mut vm = vm();
let result = call(&mut vm, "sha384", vec![s("test")]).unwrap();
assert_eq!(
result.display(),
"768412320f7b0aa5812fce428dc4706b3cae50e02a64caa16a782249bfe8efc4b7ef1ccb126255d196047dfedf17a0a9"
);
}
#[test]
fn sha512_known_vector() {
let mut vm = vm();
let result = call(&mut vm, "sha512", vec![s("test")]).unwrap();
assert_eq!(
result.display(),
"ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff"
);
}
#[test]
fn sha512_256_known_vector() {
let mut vm = vm();
let result = call(&mut vm, "sha512_256", vec![s("test")]).unwrap();
assert_eq!(
result.display(),
"3d37fe58435e0d87323dee4a2c1b339ef954de63716ee79f5747f94d974f913f"
);
}
#[test]
fn md5_known_vector() {
let mut vm = vm();
let result = call(&mut vm, "md5", vec![s("")]).unwrap();
assert_eq!(result.display(), "d41d8cd98f00b204e9800998ecf8427e");
}
#[test]
fn hash_value_different_inputs() {
let mut vm = vm();
let a = call(&mut vm, "hash_value", vec![s("foo")]).unwrap();
let b = call(&mut vm, "hash_value", vec![s("bar")]).unwrap();
assert_ne!(a.display(), b.display());
}
#[test]
fn hash_value_nil() {
let mut vm = vm();
let result = call(&mut vm, "hash_value", vec![VmValue::Nil]).unwrap();
assert!(matches!(result, VmValue::Int(_)));
}
#[test]
fn hash_value_uses_mutation_stable_atomic_and_resource_keys() {
use crate::value::{value_structural_hash_key, values_equal, VmAtomicHandle, VmResourceHandle};
use std::sync::atomic::{AtomicI64, Ordering};
let mut vm = vm();
let mut hash = |value| call(&mut vm, "hash_value", vec![value]).unwrap().display();
let contents = Arc::new(AtomicI64::new(1));
let atomic = VmValue::atomic(VmAtomicHandle {
value: contents.clone(),
});
let nested = |value| VmValue::set([VmValue::dict([("members", VmValue::set([value]))])]);
let resident = nested(atomic.clone());
let before = hash(resident.clone());
let fingerprint = value_structural_hash_key(&resident);
contents.store(2, Ordering::SeqCst);
let equal = nested(VmValue::atomic(VmAtomicHandle {
value: Arc::new(AtomicI64::new(2)),
}));
assert!(values_equal(&resident, &equal));
assert_eq!(hash(resident.clone()), hash(equal));
assert_eq!(hash(resident.clone()), before);
assert_eq!(value_structural_hash_key(&resident), fingerprint);
let different = VmValue::atomic(VmAtomicHandle {
value: Arc::new(AtomicI64::new(3)),
});
assert!(!values_equal(&atomic, &different));
assert_eq!(hash(atomic), hash(different));
let payload = Arc::new(7);
let first = VmValue::resource(VmResourceHandle::from_arc("first", payload.clone()));
let second = VmValue::resource(VmResourceHandle::from_arc("second", payload));
assert!(values_equal(&first, &second));
assert_eq!(hash(first), hash(second));
}
#[test]
fn hmac_sha256_empty_inputs() {
let mut vm = vm();
let result = call(&mut vm, "hmac_sha256", vec![s(""), s("")]).unwrap();
assert_eq!(
result.display(),
"b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad"
);
}
#[test]
fn signed_url_canonicalizes_query_and_uses_url_safe_signature() {
let mut vm = vm();
let signed = call(
&mut vm,
"signed_url",
vec![
s("https://example.test/receipt/abc?b=two+words&a=1"),
dict(&[("z", s("slash/value")), ("a2", s("!"))]),
s("secret"),
VmValue::Int(1_700_000_600),
],
)
.unwrap()
.display();
assert!(signed.starts_with("https://example.test/receipt/abc?"));
assert!(signed.contains("a=1"));
assert!(signed.contains("a2=%21"));
assert!(signed.contains("b=two%20words"));
assert!(signed.contains("z=slash%2Fvalue"));
let sig = signed
.split('&')
.find_map(|part| part.strip_prefix("sig="))
.expect("signature param");
assert!(!sig.contains('+'));
assert!(!sig.contains('/'));
assert!(!sig.contains('='));
}
#[test]
fn verify_signed_url_accepts_valid_path_and_returns_claims() {
let mut vm = vm();
let signed = call(
&mut vm,
"signed_url",
vec![
s("/artifacts/run 1?kind=trace"),
dict(&[("receipt", s("r_123"))]),
s("secret"),
VmValue::Int(200),
],
)
.unwrap();
assert!(signed.display().starts_with("/artifacts/run%201?"));
let verified = call(
&mut vm,
"verify_signed_url",
vec![signed, s("secret"), VmValue::Int(199)],
)
.unwrap();
let VmValue::Dict(result) = verified else {
panic!("expected verification dict");
};
assert!(matches!(result.get("valid"), Some(VmValue::Bool(true))));
assert!(matches!(
result.get("signature_valid"),
Some(VmValue::Bool(true))
));
assert!(matches!(result.get("expired"), Some(VmValue::Bool(false))));
assert_eq!(result.get("reason").unwrap().display(), "ok");
let claims = result.get("claims").unwrap().as_dict().unwrap();
assert_eq!(claims.get("kind").unwrap().display(), "trace");
assert_eq!(claims.get("receipt").unwrap().display(), "r_123");
}
#[test]
fn jwt_sign_es256_produces_verifiable_compact_jws() {
let mut vm = vm();
let token = call(
&mut vm,
"jwt_sign",
vec![s("ES256"), jwt_claims(), s(ES256_PRIVATE_KEY)],
)
.unwrap()
.display();
let parts: Vec<&str> = token.split('.').collect();
assert_eq!(parts.len(), 3);
let mut validation = jsonwebtoken::Validation::new(Algorithm::ES256);
validation.validate_exp = false;
let decoded = jsonwebtoken::decode::<serde_json::Value>(
&token,
&jsonwebtoken::DecodingKey::from_ec_pem(ES256_PUBLIC_KEY.as_bytes()).unwrap(),
&validation,
)
.unwrap();
assert_eq!(decoded.header.alg, Algorithm::ES256);
assert_eq!(decoded.claims["iss"], "12345");
assert_eq!(decoded.claims["iat"], 1_700_000_000);
}
#[test]
fn jwt_sign_rejects_unsupported_algorithm() {
let mut vm = vm();
let result = call(
&mut vm,
"jwt_sign",
vec![s("HS256"), jwt_claims(), s("secret")],
);
let Err(VmError::Runtime(message)) = result else {
panic!("expected runtime error");
};
assert!(message.contains("unsupported algorithm `HS256`"));
}
#[test]
fn jwt_sign_requires_dict_claims() {
let mut vm = vm();
let result = call(
&mut vm,
"jwt_sign",
vec![s("ES256"), s("not a dict"), s(ES256_PRIVATE_KEY)],
);
let Err(VmError::Runtime(message)) = result else {
panic!("expected runtime error");
};
assert!(message.contains("claims must be a dict"));
}