open-standards/openpay/crates/op-ffi-jni/src/tests.rs
Transaction Science 70ecd6caa0 Initial import — three open standards under one shelf
Transaction Science open standards monorepo. Each subdirectory is a
self-contained reference protocol with its own README and licence.

- openpay/    Apache-2.0  payment-acceptance stack (Rust, 26 crates)
- smart-byte/ CC-BY-4.0   content-addressed value carrier (spec)
- eoc/        CC-BY-4.0   energy-optimised compute (spec)

The wire format and the right to fork are public. The optional hosted
services are the offer; the protocols are not.
2026-05-23 07:58:09 -04:00

120 lines
4 KiB
Rust

//! Cross-surface integration tests.
//!
//! The JNI surface can't be exercised without a JVM, so these tests
//! focus on the C ABI and on shared utilities. The JNI handle/error
//! paths are covered by unit tests inside `jni_bridge.rs`.
use core::ffi::{CStr, c_char};
use crate::c_api::{
OpTokenizationPolicy, op_card_data_first_six, op_card_data_free, op_card_data_last_four,
op_card_data_new, op_last_error, op_scorer_free, op_scorer_name, op_scorer_new_heuristic,
op_string_free, op_vault_free, op_vault_new_ephemeral, op_vault_ref_as_string,
op_vault_ref_free, op_vault_tokenize,
};
use crate::error::FfiError;
const VALID_VISA: &[u8] = b"4242424242424242\0";
const NAME: &[u8] = b"jni-cross\0";
unsafe fn take_c_str(p: *mut c_char) -> String {
assert!(!p.is_null());
let s = unsafe { CStr::from_ptr(p) }.to_str().unwrap().to_owned();
unsafe { op_string_free(p) };
s
}
#[test]
fn c_abi_and_jni_share_the_same_error_discriminants() {
// The C ABI returns i32 codes; the JNI side throws typed
// exceptions whose class names embed the same discriminants in
// the exception_class method. Verify they line up.
let invalid = FfiError::InvalidInput;
assert_eq!(invalid.as_i32(), 1);
assert!(invalid.exception_class().ends_with("InvalidInput"));
let lookup = FfiError::VaultLookupFailed;
assert_eq!(lookup.as_i32(), 2);
assert!(lookup.exception_class().ends_with("VaultLookupFailed"));
}
#[test]
fn c_abi_full_lifecycle_works_in_isolation() {
unsafe {
let vault = op_vault_new_ephemeral(NAME.as_ptr() as *const c_char);
let card = op_card_data_new(VALID_VISA.as_ptr() as *const c_char, 12, 2030);
let policy = OpTokenizationPolicy {
format: 0,
lifetime: 0,
ttl_seconds: 0,
};
let token = op_vault_tokenize(vault, card, policy);
assert!(!token.is_null());
let s = take_c_str(op_vault_ref_as_string(token));
assert!(s.starts_with("tok_v7_"));
op_vault_ref_free(token);
op_vault_free(vault);
}
}
#[test]
fn c_abi_strings_freeable_in_loop() {
unsafe {
for _ in 0..100 {
let card = op_card_data_new(VALID_VISA.as_ptr() as *const c_char, 12, 2030);
let f6 = op_card_data_first_six(card);
let l4 = op_card_data_last_four(card);
op_string_free(f6);
op_string_free(l4);
op_card_data_free(card);
}
}
}
#[test]
fn scorer_name_consistent_via_c_abi() {
unsafe {
let s = op_scorer_new_heuristic();
let name = take_c_str(op_scorer_name(s));
assert_eq!(name, "heuristic-v1");
op_scorer_free(s);
}
}
#[test]
fn c_abi_thread_local_error_is_per_thread() {
use std::thread;
let a = thread::spawn(|| unsafe {
let bad = b"bad\0";
let _ = op_card_data_new(bad.as_ptr() as *const c_char, 12, 2030);
op_last_error()
});
let b = thread::spawn(|| unsafe {
// Different thread: trigger same error category.
let bad = b"also-bad\0";
let _ = op_card_data_new(bad.as_ptr() as *const c_char, 12, 2030);
op_last_error()
});
assert_eq!(a.join().unwrap(), FfiError::InvalidInput.as_i32());
assert_eq!(b.join().unwrap(), FfiError::InvalidInput.as_i32());
}
#[test]
fn ffi_error_discriminants_match_phase_8_swift_exactly() {
// The whole point of using identical discriminants across iOS and
// Android is observability: a single dashboard maps error codes
// to a unified taxonomy. Verify the contract.
assert_eq!(FfiError::Ok.as_i32(), 0);
assert_eq!(FfiError::InvalidInput.as_i32(), 1);
assert_eq!(FfiError::VaultLookupFailed.as_i32(), 2);
assert_eq!(FfiError::TokenExpired.as_i32(), 3);
assert_eq!(FfiError::TokenAlreadyConsumed.as_i32(), 4);
assert_eq!(FfiError::FraudDeclined.as_i32(), 5);
assert_eq!(FfiError::FraudReviewRequired.as_i32(), 6);
assert_eq!(FfiError::Backend.as_i32(), 7);
assert_eq!(FfiError::Internal.as_i32(), 8);
assert_eq!(FfiError::Capacity.as_i32(), 9);
}