Lines
99.23 %
Functions
100 %
Branches
use chrono::Utc;
use finance::split::Split;
use finance::transaction::Transaction;
use scripting::executor::ScriptExecutor;
use scripting::format::{
ContextType, ENTITY_HEADER_SIZE, EntityType, GLOBAL_HEADER_SIZE, Operation,
};
use scripting::parser::EntityData;
use scripting::serializer::MemorySerializer;
use uuid::Uuid;
const TEST_WASM: &[u8] = include_bytes!("../../web/static/wasm/groceries_markup.wasm");
const TAG_SYNC_WASM: &[u8] = include_bytes!("../../web/static/wasm/tag_sync.wasm");
#[test]
fn test_groceries_script_tags_splits() {
let executor = ScriptExecutor::new();
let tx = Transaction {
id: Uuid::new_v4(),
post_date: Utc::now(),
enter_date: Utc::now(),
let account1_id = Uuid::new_v4();
let account2_id = Uuid::new_v4();
let commodity_id = Uuid::new_v4();
let split1 = Split {
tx_id: tx.id,
account_id: account1_id,
commodity_id,
value_num: -5000,
value_denom: 100,
reconcile_state: None,
reconcile_date: None,
lot_id: None,
let split2 = Split {
account_id: account2_id,
value_num: 5000,
let mut serializer = MemorySerializer::new();
serializer.set_context(ContextType::EntityCreate, EntityType::Transaction);
// Transaction with 2 splits and 1 tag
let tx_idx = serializer.add_transaction_from(&tx, true, 2, 1, false);
serializer.set_primary(tx_idx);
let split1_idx = serializer.add_split_from(&split1, tx_idx as i32);
let split2_idx = serializer.add_split_from(&split2, tx_idx as i32);
// Add "note" = "groceries" tag to transaction
serializer.add_tag(
Uuid::new_v4().into_bytes(),
tx_idx as i32,
false,
"note",
"groceries",
);
let input = serializer.finalize(4096);
println!("Input size: {} bytes", input.len());
let entities = executor
.execute(TEST_WASM, &input, Some(4096))
.expect("Execution failed");
println!("Output entities: {entities:?}");
assert_eq!(
entities.len(),
2,
"Expected 2 output entities (category tags for each split)"
for (i, entity) in entities.iter().enumerate() {
assert_eq!(entity.entity_type, EntityType::Tag);
assert_eq!(entity.operation, Operation::Create);
if let EntityData::Tag { name, value } = &entity.data {
assert_eq!(name, "category", "Tag {i} name mismatch");
assert_eq!(value, "groceries", "Tag {i} value mismatch");
} else {
panic!("Expected Tag entity data, got {:?}", entity.data);
}
let parent_indices: Vec<i32> = entities.iter().map(|e| e.parent_idx).collect();
assert!(
parent_indices.contains(&(split1_idx as i32)),
"Missing tag for split1"
parent_indices.contains(&(split2_idx as i32)),
"Missing tag for split2"
fn test_groceries_script_skips_non_groceries() {
// Transaction with tag "note" = "other" (not groceries)
let tx_idx = serializer.add_transaction_from(&tx, true, 0, 1, false);
"other",
entities.is_empty(),
"Expected 0 output entities for non-groceries transaction"
fn test_groceries_script_skips_non_transaction() {
serializer.set_context(ContextType::EntityCreate, EntityType::Account);
let account_id = [1u8; 16];
let parent_account_id = [0u8; 16];
let account_idx = serializer.add_account(
account_id,
-1,
true,
parent_account_id,
"Test Account",
"Assets:Test Account",
0,
serializer.set_primary(account_idx);
"Expected 0 output entities for Account"
fn test_groceries_script_skips_no_note_tag() {
// Transaction without any tags
let tx_idx = serializer.add_transaction_from(&tx, true, 0, 0, false);
"Expected 0 output entities for transaction without note tag"
fn test_tag_sync_copies_user_tags_to_splits() {
account_id: Uuid::new_v4(),
let tx_idx = serializer.add_transaction_from(&tx, true, 2, 2, false);
// "note" is a system tag — excluded from user tag count
// "category" is a user tag
"category",
"food",
.execute(TAG_SYNC_WASM, &input, Some(4096))
.expect("tag_sync execution failed");
"Expected 2 output entities (category tag copied to each split)"
for entity in &entities {
assert_eq!(name, "category");
assert_eq!(value, "food");
fn test_tag_sync_copies_split_tags_to_transaction() {
// Transaction with only a "note" tag (no user tags)
serializer.add_split_from(&split2, tx_idx as i32);
// Split1 has a user tag
split1_idx as i32,
1,
"Expected 1 output entity (category tag copied to transaction)"
let entity = &entities[0];
assert_eq!(entity.parent_idx, tx_idx as i32);
fn test_tag_sync_noop_when_note_only_no_split_tags() {
serializer.add_split_from(&split1, tx_idx as i32);
// Only "note" on tx, no tags on splits — nothing to sync
"Expected 0 output entities when only note tag on tx and no split tags"
fn build_valid_transaction_input() -> Vec<u8> {
serializer.finalize(4096)
fn test_script_handles_truncated_input_without_panic() {
let input = build_valid_transaction_input();
let truncated = &input[..GLOBAL_HEADER_SIZE + ENTITY_HEADER_SIZE];
let result = executor.execute(TEST_WASM, truncated, Some(4096));
result.is_ok() || result.is_err(),
"Must return a result, not panic"
fn test_script_handles_corrupted_entity_data_offset_without_panic() {
let mut input = build_valid_transaction_input();
let entity_start = GLOBAL_HEADER_SIZE;
let data_offset_field = entity_start + 24;
if data_offset_field + 4 <= input.len() {
input[data_offset_field..data_offset_field + 4].copy_from_slice(&u32::MAX.to_le_bytes());
let result = executor.execute(TEST_WASM, &input, Some(4096));
fn test_script_handles_corrupted_entity_data_size_without_panic() {
let data_size_field = entity_start + 28;
if data_size_field + 4 <= input.len() {
input[data_size_field..data_size_field + 4].copy_from_slice(&u32::MAX.to_le_bytes());
fn test_tag_sync_script_handles_truncated_input_without_panic() {
let result = executor.execute(TAG_SYNC_WASM, truncated, Some(4096));
fn test_tag_sync_script_handles_corrupted_entity_offset_without_panic() {
let result = executor.execute(TAG_SYNC_WASM, &input, Some(4096));