Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion contracts/compliance_registry/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
use propchain_traits::{ComplianceChecker, *};

#[ink::contract]
mod compliance_registry {
pub mod compliance_registry {
use ink::prelude::vec::Vec;
use ink::storage::Mapping;
use propchain_traits::ComplianceOperation;
Expand Down
1 change: 1 addition & 0 deletions contracts/database/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,7 @@ pub mod propchain_database {
// UNIT TESTS
// ========================================================================

// Unit tests extracted to tests.rs (Issue #101)
// Include unit tests (extracted to tests.rs per Issue #101)
#[cfg(test)]
include!("tests.rs");
Expand Down
119 changes: 119 additions & 0 deletions contracts/database/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,123 @@ mod tests {
let list = contract.get_indexer_list();
assert_eq!(list.len(), 1);
}

// ========================================================================
// Export-request lifecycle (Issue #1016)
// ========================================================================

fn new_admin_contract() -> DatabaseIntegration {
let accounts = ink::env::test::default_accounts::<ink::env::DefaultEnvironment>();
ink::env::test::set_caller::<ink::env::DefaultEnvironment>(accounts.alice);
DatabaseIntegration::new()
}

#[ink::test]
fn request_data_export_is_admin_only() {
let accounts = ink::env::test::default_accounts::<ink::env::DefaultEnvironment>();
let mut contract = new_admin_contract();

// Non-admin is rejected
ink::env::test::set_caller::<ink::env::DefaultEnvironment>(accounts.bob);
let forbidden = contract.request_data_export(DataType::Properties, 1, 100, 0, 1000);
assert_eq!(forbidden, Err(Error::Unauthorized));

// Admin succeeds
ink::env::test::set_caller::<ink::env::DefaultEnvironment>(accounts.alice);
let batch = contract
.request_data_export(DataType::Properties, 1, 100, 0, 1000)
.expect("Admin should request an export");
assert_eq!(batch, 1);
}

#[ink::test]
fn inverted_id_range_with_equal_blocks_is_rejected() {
let mut contract = new_admin_contract();
let result = contract.request_data_export(DataType::Transfers, 100, 1, 500, 500);
assert_eq!(result, Err(Error::InvalidDataRange));
}

#[ink::test]
fn inverted_block_range_with_equal_ids_is_rejected() {
let mut contract = new_admin_contract();
let result = contract.request_data_export(DataType::Escrows, 42, 42, 900, 100);
assert_eq!(result, Err(Error::InvalidDataRange));
}

#[ink::test]
fn fully_inverted_ranges_are_rejected() {
let mut contract = new_admin_contract();
let result = contract.request_data_export(DataType::Valuations, 100, 1, 900, 100);
assert_eq!(result, Err(Error::InvalidDataRange));
}

#[ink::test]
fn equal_boundaries_are_accepted_as_valid_range() {
let mut contract = new_admin_contract();
let result =
contract.request_data_export(DataType::Compliance, 7, 7, 1234, 1234);
assert!(
result.is_ok(),
"from == to on both axes must be a valid single-record range"
);
}

#[ink::test]
fn valid_request_stores_exact_fields_and_increments_batch_ids() {
let accounts = ink::env::test::default_accounts::<ink::env::DefaultEnvironment>();
let mut contract = new_admin_contract();

let first = contract
.request_data_export(DataType::Properties, 10, 20, 100, 200)
.expect("first export");
let second = contract
.request_data_export(DataType::Tokens, 30, 40, 300, 400)
.expect("second export");

assert_eq!(first, 1, "batch ids increment sequentially");
assert_eq!(second, 2);

let stored = contract.get_export_request(first).unwrap();
assert_eq!(stored.batch_id, first);
assert_eq!(stored.data_type, DataType::Properties);
assert_eq!(stored.from_id, 10);
assert_eq!(stored.to_id, 20);
assert_eq!(stored.from_block, 100);
assert_eq!(stored.to_block, 200);
assert_eq!(stored.requested_by, accounts.alice);
assert!(!stored.completed);
assert_eq!(stored.export_checksum, None);
}

#[ink::test]
fn completion_sets_completed_flag_and_checksum() {
let checksum = Hash::from([0xAB; 32]);
let mut contract = new_admin_contract();

let batch = contract
.request_data_export(DataType::Analytics, 1, 9, 5, 50)
.expect("export request");
assert!(!contract.get_export_request(batch).unwrap().completed);

contract
.complete_data_export(batch, checksum)
.expect("Admin completes the export");

let completed = contract.get_export_request(batch).unwrap();
assert!(completed.completed);
assert_eq!(completed.export_checksum, Some(checksum));
}

#[ink::test]
fn completing_unknown_batch_returns_export_not_found() {
let mut contract = new_admin_contract();
let result = contract.complete_data_export(999, Hash::from([0x01; 32]));
assert_eq!(result, Err(Error::ExportNotFound));
}

#[ink::test]
fn get_export_request_returns_none_for_unknown_batch() {
let contract = new_admin_contract();
assert!(contract.get_export_request(12345).is_none());
}
}
175 changes: 175 additions & 0 deletions contracts/monitoring/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -793,5 +793,180 @@ pub mod monitoring {
c.transfer_admin(new_admin).unwrap();
assert_eq!(c.get_admin(), new_admin);
}

// ─────────────────────────────────────────────────────────────────
// Issue #1015: subscriber lifecycle & snapshot coverage
// ─────────────────────────────────────────────────────────────────

fn new_contract_with_admin(admin: AccountId) -> MonitoringContract {
ink::env::test::set_caller::<ink::env::DefaultEnvironment>(admin);
MonitoringContract::new()
}

#[ink::test]
fn subscribe_alerts_registers_subscriber_once() {
let accounts = ink::env::test::default_accounts::<ink::env::DefaultEnvironment>();
let mut c = new_contract_with_admin(accounts.alice);

ink::env::test::set_caller::<ink::env::DefaultEnvironment>(accounts.alice);
c.subscribe_alerts(accounts.bob).expect("first subscribe");

// Duplicate subscribe is a silent no-op, not an error
c.subscribe_alerts(accounts.bob)
.expect("duplicate subscribe must succeed silently");
let subs = c.get_alert_subscribers();
assert_eq!(subs.len(), 1, "duplicate subscribe must not add twice");
assert_eq!(subs[0], accounts.bob);
}

#[ink::test]
fn unsubscribe_unknown_subscriber_returns_error() {
let accounts = ink::env::test::default_accounts::<ink::env::DefaultEnvironment>();
let mut c = new_contract_with_admin(accounts.alice);

ink::env::test::set_caller::<ink::env::DefaultEnvironment>(accounts.alice);
let result = c.unsubscribe_alerts(accounts.django);
assert_eq!(result, Err(MonitoringError::SubscriberNotFound));
}

#[ink::test]
fn get_alert_subscribers_reflects_content_and_removal_order() {
let accounts = ink::env::test::default_accounts::<ink::env::DefaultEnvironment>();
let mut c = new_contract_with_admin(accounts.alice);

ink::env::test::set_caller::<ink::env::DefaultEnvironment>(accounts.alice);
c.subscribe_alerts(accounts.bob).unwrap();
c.subscribe_alerts(accounts.charlie).unwrap();
c.subscribe_alerts(accounts.django).unwrap();

assert_eq!(
c.get_alert_subscribers(),
vec![accounts.bob, accounts.charlie, accounts.django],
"subscribers must appear in registration order"
);

// Removing the middle entry swaps the last one into its place
c.unsubscribe_alerts(accounts.charlie).unwrap();
assert_eq!(
c.get_alert_subscribers(),
vec![accounts.bob, accounts.django],
"swap_remove moves the last subscriber into the freed slot"
);
}

#[ink::test]
fn non_admin_cannot_manage_subscribers() {
let accounts = ink::env::test::default_accounts::<ink::env::DefaultEnvironment>();
let mut c = new_contract_with_admin(accounts.alice);

ink::env::test::set_caller::<ink::env::DefaultEnvironment>(accounts.bob);
assert_eq!(
c.subscribe_alerts(accounts.charlie),
Err(MonitoringError::Unauthorized)
);
assert_eq!(
c.unsubscribe_alerts(accounts.charlie),
Err(MonitoringError::Unauthorized)
);
assert!(c.get_alert_subscribers().is_empty());
}

#[ink::test]
fn snapshot_round_trip_records_expected_aggregates_from_reporter() {
let accounts = ink::env::test::default_accounts::<ink::env::DefaultEnvironment>();
let mut c = new_contract_with_admin(accounts.alice);

// Admin authorizes bob as a reporter
ink::env::test::set_caller::<ink::env::DefaultEnvironment>(accounts.alice);
c.add_reporter(accounts.bob).unwrap();
assert!(c.is_authorized_reporter(accounts.bob));

// Authorized reporter records operations: 3 successes + 1 failure
ink::env::test::set_caller::<ink::env::DefaultEnvironment>(accounts.bob);
c.record_operation(OperationType::RegisterProperty, true)
.unwrap();
c.record_operation(OperationType::TransferProperty, true)
.unwrap();
c.record_operation(OperationType::GovernanceVote, true)
.unwrap();
c.record_operation(OperationType::BridgeTransfer, false)
.unwrap();

// Snapshot lands at slot 0
c.take_metrics_snapshot().unwrap();
let snap = c
.get_metrics_snapshot(0)
.expect("first snapshot must be at slot 0");
assert_eq!(snap.snapshot_id, 0);
assert_eq!(snap.total_calls, 4);
assert_eq!(snap.total_errors, 1);
assert_eq!(snap.error_rate_bips, 2_500); // 25%
}

#[ink::test]
fn consecutive_snapshots_use_distinct_slots_and_ids() {
let accounts = ink::env::test::default_accounts::<ink::env::DefaultEnvironment>();
let mut c = new_contract_with_admin(accounts.alice);

ink::env::test::set_caller::<ink::env::DefaultEnvironment>(accounts.alice);
c.record_operation(OperationType::Generic, true).unwrap();
c.take_metrics_snapshot().unwrap();

c.record_operation(OperationType::Stake, false).unwrap();
c.take_metrics_snapshot().unwrap();

let first = c.get_metrics_snapshot(0).expect("slot 0");
let second = c.get_metrics_snapshot(1).expect("second snapshot at slot 1");
assert_eq!(first.snapshot_id, 0);
assert_eq!(second.snapshot_id, 1);
assert_eq!(second.total_calls, 2);
assert_eq!(second.total_errors, 1);
assert_eq!(second.error_rate_bips, 5_000); // 50%
}

#[ink::test]
fn snapshot_buffer_wraps_and_overwrites_oldest_slot() {
let accounts = ink::env::test::default_accounts::<ink::env::DefaultEnvironment>();
let mut c = new_contract_with_admin(accounts.alice);

let max = constants::MONITORING_MAX_SNAPSHOTS;
let extra = 3u64;
let total_snapshots = max + extra;

ink::env::test::set_caller::<ink::env::DefaultEnvironment>(accounts.alice);
for _ in 0..total_snapshots {
c.take_metrics_snapshot().unwrap();
}
assert_eq!(c.snapshot_count, total_snapshots);

// Slot reuse: slot 0 first held snapshot 0, now holds the newest
// snapshot whose id maps onto slot 0 (id = max).
let wrapped = c.get_metrics_snapshot(0).expect("slot 0 rewritten");
assert_eq!(
wrapped.snapshot_id,
max,
"oldest snapshot in slot 0 must be overwritten"
);
// Slot 1 now holds id max+1, slot 2 holds max+2
assert_eq!(c.get_metrics_snapshot(1).unwrap().snapshot_id, max + 1);
assert_eq!(c.get_metrics_snapshot(2).unwrap().snapshot_id, max + 2);

// Slots untouched during the second pass keep their original data
assert_eq!(c.get_metrics_snapshot(extra).unwrap().snapshot_id, extra);
}

#[ink::test]
fn snapshot_beyond_written_count_returns_none() {
let mut c = new_contract();
assert!(c.get_metrics_snapshot(0).is_none(), "empty buffer");

c.take_metrics_snapshot().unwrap();
// Slot indices are bounded by MONITORING_MAX_SNAPSHOTS; anything
// at or above it was never written.
assert!(
c.get_metrics_snapshot(constants::MONITORING_MAX_SNAPSHOTS)
.is_none()
);
}
}
}
2 changes: 1 addition & 1 deletion contracts/third-party/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use ink::prelude::vec::Vec;
use ink::storage::Mapping;

#[ink::contract]
mod propchain_third_party {
pub mod propchain_third_party {
use super::*;

// Data types extracted to types.rs (Issue #101)
Expand Down
2 changes: 2 additions & 0 deletions tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ staking = { path = "../contracts/staking", default-features = false }
propchain-bridge = { path = "../contracts/bridge", default-features = false }
oracle = { path = "../contracts/oracle", default-features = false }
propchain-insurance = { path = "../contracts/insurance", default-features = false }
propchain-third-party = { path = "../contracts/third-party", default-features = false }
# Issue #1010: mock oracle integration coverage (no "mock" feature — prices
# must come from explicit pushes only)
mock-oracle = { path = "../contracts/mock-oracle", default-features = false }
Expand Down Expand Up @@ -83,6 +84,7 @@ std = [
"staking/std",
"propchain-bridge/std",
"propchain-insurance/std",
"propchain-third-party/std",
"mock-oracle/std",
"propchain-gdpr/std",
"propchain-factory/std",
Expand Down
Loading
Loading