Lines
100 %
Functions
Branches
//! Integration tests for the `scripting-sdk` entity wrappers. The SDK
//! is consumed by guest WASM scripts (rust + C++); none of these
//! accessors had direct unit coverage before — `cargo tarpaulin`
//! reported 0/106 on `entity.rs`. The tests below pin every accessor
//! plus the entity-type discrimination + OOB branches so a future
//! refactor of the wire format catches the change before guest
//! scripts break.
use scripting_format::{
AccountData, CommodityData, ENTITY_HEADER_SIZE, EntityHeader, EntityType, Operation,
SPLIT_DATA_SIZE, SplitData, TRANSACTION_DATA_SIZE, TagData, TransactionData,
};
use scripting_sdk::entity::EntityRef;
use scripting_sdk::error::Error;
fn empty_header(entity_type: u8, operation: u8) -> EntityHeader {
EntityHeader {
entity_type,
operation,
flags: 0,
reserved: [0; 1],
id: [7u8; 16],
parent_idx: -1,
data_offset: 0,
data_size: 0,
}
fn tx_data() -> [u8; TRANSACTION_DATA_SIZE] {
TransactionData {
post_date: 1000,
enter_date: 2000,
split_count: 3,
tag_count: 1,
is_multi_currency: 1,
reserved: [0; 23],
.to_bytes()
fn split_data() -> [u8; SPLIT_DATA_SIZE] {
SplitData {
account_id: [10u8; 16],
commodity_id: [20u8; 16],
value_num: -5000,
value_denom: 100,
reconcile_state: 2,
reserved: [0; 7],
reconcile_date: 12345,
account_name_offset: 0,
account_name_len: 0,
#[test]
fn entity_type_resolves_each_known_value() {
for (raw, expected) in [
(0u8, EntityType::Transaction),
(1u8, EntityType::Split),
(2u8, EntityType::Tag),
(3u8, EntityType::Account),
(4u8, EntityType::Commodity),
] {
let header = empty_header(raw, 0);
let entity = EntityRef {
header,
data: &[],
strings_pool: &[],
assert_eq!(entity.entity_type().unwrap(), expected);
fn entity_type_rejects_unknown_byte() {
let header = empty_header(0x55, 0);
assert_eq!(entity.entity_type().unwrap_err(), Error::InvalidEntityType);
fn operation_resolves_each_known_value() {
(0u8, Operation::Nop),
(1u8, Operation::Create),
(2u8, Operation::Update),
(3u8, Operation::Delete),
(4u8, Operation::Link),
(5u8, Operation::Unlink),
let header = empty_header(0, raw);
assert_eq!(entity.operation().unwrap(), expected);
fn operation_rejects_unknown_byte() {
let header = empty_header(0, 0x7F);
assert_eq!(entity.operation().unwrap_err(), Error::InvalidOperation);
fn id_and_parent_idx_pass_through_header() {
let mut header = empty_header(0, 0);
header.id = [0xAB; 16];
header.parent_idx = 42;
assert_eq!(entity.id(), [0xAB; 16]);
assert_eq!(entity.parent_idx(), 42);
fn as_transaction_returns_typed_view() {
let data = tx_data();
header: empty_header(0, 2),
data: &data,
let tx = entity.as_transaction().unwrap();
assert_eq!(tx.id(), [7u8; 16]);
assert_eq!(tx.post_date(), 1000);
assert_eq!(tx.enter_date(), 2000);
assert_eq!(tx.split_count(), 3);
assert_eq!(tx.tag_count(), 1);
assert!(tx.is_multi_currency());
fn as_transaction_rejects_wrong_entity_type() {
header: empty_header(1, 0),
data: &tx_data(),
assert!(matches!(
entity.as_transaction(),
Err(Error::InvalidEntityType)
));
fn as_transaction_rejects_truncated_data() {
let short = [0u8; TRANSACTION_DATA_SIZE - 1];
header: empty_header(0, 0),
data: &short,
assert!(matches!(entity.as_transaction(), Err(Error::OutOfBounds)));
fn as_split_returns_typed_view() {
let data = split_data();
let split = entity.as_split().unwrap();
assert_eq!(split.account_id(), [10u8; 16]);
assert_eq!(split.commodity_id(), [20u8; 16]);
assert_eq!(split.value_num(), -5000);
assert_eq!(split.value_denom(), 100);
assert_eq!(split.reconcile_state(), 2);
assert_eq!(split.reconcile_date(), 12345);
assert_eq!(split.parent_idx(), -1);
fn as_split_rejects_wrong_entity_type() {
data: &split_data(),
assert!(matches!(entity.as_split(), Err(Error::InvalidEntityType)));
fn as_split_rejects_truncated_data() {
let short = [0u8; SPLIT_DATA_SIZE - 1];
assert!(matches!(entity.as_split(), Err(Error::OutOfBounds)));
fn as_tag_reads_strings_from_pool() {
let pool = b"namevalue";
let tag_data = TagData {
name_offset: 0,
value_offset: 4,
name_len: 4,
value_len: 5,
reserved: [0; 4],
.to_bytes();
header: empty_header(2, 0),
data: &tag_data,
strings_pool: pool,
let tag = entity.as_tag().unwrap();
assert_eq!(tag.name().unwrap(), "name");
assert_eq!(tag.value().unwrap(), "value");
assert_eq!(tag.parent_idx(), -1);
fn as_tag_rejects_wrong_entity_type() {
value_offset: 0,
name_len: 0,
value_len: 0,
assert!(matches!(entity.as_tag(), Err(Error::InvalidEntityType)));
fn tag_name_oob_returns_out_of_bounds_error() {
let pool = b"short";
name_len: 100,
assert_eq!(tag.name().unwrap_err(), Error::OutOfBounds);
fn tag_name_invalid_utf8_returns_utf8_error() {
let pool = &[0xC0u8, 0xC1u8];
name_len: 2,
assert_eq!(tag.name().unwrap_err(), Error::Utf8Error);
fn as_account_reads_name_and_path() {
let pool = b"Walletassets:wallet";
let acct_data = AccountData {
parent_account_id: [99u8; 16],
path_offset: 6,
tag_count: 0,
name_len: 6,
path_len: 13,
reserved: [0; 16],
header: empty_header(3, 0),
data: &acct_data,
let acct = entity.as_account().unwrap();
assert_eq!(acct.parent_account_id(), [99u8; 16]);
assert_eq!(acct.name().unwrap(), "Wallet");
assert_eq!(acct.path().unwrap(), "assets:wallet");
assert_eq!(acct.tag_count(), 0);
fn as_account_rejects_wrong_entity_type() {
parent_account_id: [0u8; 16],
path_offset: 0,
path_len: 0,
assert!(matches!(entity.as_account(), Err(Error::InvalidEntityType)));
fn account_path_oob_returns_out_of_bounds_error() {
let pool = b"abc";
path_len: 50,
assert_eq!(acct.path().unwrap_err(), Error::OutOfBounds);
fn as_commodity_reads_symbol_and_name() {
let pool = b"USDDollar";
let cmd_data = CommodityData {
symbol_offset: 0,
name_offset: 3,
tag_count: 2,
symbol_len: 3,
header: empty_header(4, 0),
data: &cmd_data,
let cmd = entity.as_commodity().unwrap();
assert_eq!(cmd.symbol().unwrap(), "USD");
assert_eq!(cmd.name().unwrap(), "Dollar");
assert_eq!(cmd.tag_count(), 2);
fn as_commodity_rejects_wrong_entity_type() {
symbol_len: 0,
entity.as_commodity(),
fn commodity_symbol_oob_returns_out_of_bounds_error() {
let pool = b"x";
symbol_len: 10,
assert_eq!(cmd.symbol().unwrap_err(), Error::OutOfBounds);
/// Round-trip sanity: header bytes serialize via `to_bytes` and can
/// be reconstructed via `from_bytes` — the test catches alignment /
/// repr drift between the SDK and the wire format crate.
fn entity_header_bytes_round_trip() {
let header = EntityHeader {
entity_type: 1,
operation: 2,
id: [42u8; 16],
parent_idx: 3,
data_offset: 100,
data_size: 64,
let bytes = header.to_bytes();
assert_eq!(bytes.len(), ENTITY_HEADER_SIZE);
let parsed = EntityHeader::from_bytes(&bytes).unwrap();
assert_eq!(parsed.entity_type, 1);
assert_eq!(parsed.operation, 2);
assert_eq!(parsed.id, [42u8; 16]);
assert_eq!(parsed.parent_idx, 3);
assert_eq!(parsed.data_offset, 100);
assert_eq!(parsed.data_size, 64);