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
45 changes: 45 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Coverage Configuration for PropChain Contracts

[run]
# Source files to include
source = ["contracts"]

# Source files to exclude
omit = [
"*/target/*",
"*/tests/*",
"*/test_utils.rs",
"*/e2e_tests.rs",
]

# Branch coverage
branch = true

# Show missing lines
show_missing = true

# Precision for coverage percentage
precision = 2

[report]
# Exclude lines from coverage
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise AssertionError",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
"if False:",
"class .*\\bProtocol\\):",
"@(abc\\.)?abstractmethod",
]

# Minimum coverage percentage
fail_under = 95

[html]
directory = "coverage/html"

[xml]
output = "coverage/coverage.xml"
69 changes: 69 additions & 0 deletions .github/workflows/test-coverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Test Coverage

on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]

jobs:
coverage:
name: Test Coverage Report
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy

- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

- name: Install cargo-tarpaulin
run: cargo install cargo-tarpaulin

- name: Run tests with coverage
run: |
cargo tarpaulin \
--out Xml \
--out Html \
--output-dir coverage \
--exclude-files '*/tests/*' \
--exclude-files '*/target/*' \
--timeout 120 \
--all-features \
--workspace

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage/coverage.xml
flags: unittests
name: codecov-umbrella
fail_ci_if_error: false

- name: Check coverage threshold
run: |
COVERAGE=$(grep -oP 'coverage: \K[0-9.]+' coverage/tarpaulin-report.html | head -1 || echo "0")
THRESHOLD=95.0
echo "Current coverage: ${COVERAGE}%"
echo "Required threshold: ${THRESHOLD}%"
if (( $(echo "$COVERAGE < $THRESHOLD" | bc -l) )); then
echo "ERROR: Coverage ${COVERAGE}% is below threshold ${THRESHOLD}%"
exit 1
fi

- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
273 changes: 273 additions & 0 deletions contracts/property-token/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1679,5 +1679,278 @@ mod property_token {
.expect("Compliance info should exist after verification");
assert!(compliance_info.verified);
}

// ============================================================================
// EDGE CASE TESTS
// ============================================================================

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

let result = contract.transfer_from(accounts.alice, accounts.bob, 999);
assert_eq!(result, Err(Error::TokenNotFound));
}

#[ink::test]
fn test_transfer_from_unauthorized_caller() {
let mut contract = setup_contract();
let accounts = test::default_accounts::<DefaultEnvironment>();
test::set_caller::<DefaultEnvironment>(accounts.alice);

let metadata = PropertyMetadata {
location: String::from("123 Main St"),
size: 1000,
legal_description: String::from("Sample property"),
valuation: 500000,
documents_url: String::from("ipfs://sample-docs"),
};

let token_id = contract
.register_property_with_token(metadata)
.expect("Token registration should succeed in test");

// Bob tries to transfer Alice's token without approval
test::set_caller::<DefaultEnvironment>(accounts.bob);
let result = contract.transfer_from(accounts.alice, accounts.bob, token_id);
assert_eq!(result, Err(Error::Unauthorized));
}

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

let result = contract.approve(accounts.bob, 999);
assert_eq!(result, Err(Error::TokenNotFound));
}

#[ink::test]
fn test_approve_unauthorized_caller() {
let mut contract = setup_contract();
let accounts = test::default_accounts::<DefaultEnvironment>();
test::set_caller::<DefaultEnvironment>(accounts.alice);

let metadata = PropertyMetadata {
location: String::from("123 Main St"),
size: 1000,
legal_description: String::from("Sample property"),
valuation: 500000,
documents_url: String::from("ipfs://sample-docs"),
};

let token_id = contract
.register_property_with_token(metadata)
.expect("Token registration should succeed in test");

// Bob tries to approve without being owner or operator
test::set_caller::<DefaultEnvironment>(accounts.bob);
let result = contract.approve(accounts.charlie, token_id);
assert_eq!(result, Err(Error::Unauthorized));
}

#[ink::test]
fn test_owner_of_nonexistent_token() {
let contract = setup_contract();

assert_eq!(contract.owner_of(0), None);
assert_eq!(contract.owner_of(1), None);
assert_eq!(contract.owner_of(u64::MAX), None);
}

#[ink::test]
fn test_balance_of_nonexistent_account() {
let contract = setup_contract();
let nonexistent = AccountId::from([0xFF; 32]);

assert_eq!(contract.balance_of(nonexistent), 0);
}

#[ink::test]
fn test_attach_document_to_nonexistent_token() {
let mut contract = setup_contract();
let doc_hash = Hash::from([1u8; 32]);

let result = contract.attach_legal_document(999, doc_hash, "Deed".to_string());
assert_eq!(result, Err(Error::TokenNotFound));
}

#[ink::test]
fn test_attach_document_unauthorized() {
let mut contract = setup_contract();
let accounts = test::default_accounts::<DefaultEnvironment>();
test::set_caller::<DefaultEnvironment>(accounts.alice);

let metadata = PropertyMetadata {
location: String::from("123 Main St"),
size: 1000,
legal_description: String::from("Sample property"),
valuation: 500000,
documents_url: String::from("ipfs://sample-docs"),
};

let token_id = contract
.register_property_with_token(metadata)
.expect("Token registration should succeed in test");

// Bob tries to attach document
test::set_caller::<DefaultEnvironment>(accounts.bob);
let doc_hash = Hash::from([1u8; 32]);
let result = contract.attach_legal_document(token_id, doc_hash, "Deed".to_string());
assert_eq!(result, Err(Error::Unauthorized));
}

#[ink::test]
fn test_verify_compliance_nonexistent_token() {
let mut contract = setup_contract();
let accounts = test::default_accounts::<DefaultEnvironment>();
test::set_caller::<DefaultEnvironment>(accounts.alice);

let result = contract.verify_compliance(999, true);
assert_eq!(result, Err(Error::TokenNotFound));
}

#[ink::test]
fn test_initiate_bridge_invalid_chain() {
let mut contract = setup_contract();
let accounts = test::default_accounts::<DefaultEnvironment>();
test::set_caller::<DefaultEnvironment>(accounts.alice);

let metadata = PropertyMetadata {
location: String::from("123 Main St"),
size: 1000,
legal_description: String::from("Sample property"),
valuation: 500000,
documents_url: String::from("ipfs://sample-docs"),
};

let token_id = contract
.register_property_with_token(metadata)
.expect("Token registration should succeed in test");

// Try to bridge to unsupported chain
let result = contract.initiate_bridge_multisig(
token_id,
999, // Invalid chain ID
accounts.bob,
2, // required_signatures
None, // timeout_blocks
);

assert_eq!(result, Err(Error::InvalidChain));
}

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

let result = contract.initiate_bridge_multisig(
999, // nonexistent token_id
2, // destination_chain
accounts.bob, // recipient
2, // required_signatures
None, // timeout_blocks
);

assert_eq!(result, Err(Error::TokenNotFound));
}

#[ink::test]
fn test_sign_bridge_request_nonexistent() {
let mut contract = setup_contract();
let _accounts = test::default_accounts::<DefaultEnvironment>();

let result = contract.sign_bridge_request(999, true);
assert_eq!(result, Err(Error::InvalidRequest));
}

#[ink::test]
fn test_register_multiple_properties_increments_ids() {
let mut contract = setup_contract();
let accounts = test::default_accounts::<DefaultEnvironment>();
test::set_caller::<DefaultEnvironment>(accounts.alice);

for i in 1..=10 {
let metadata = PropertyMetadata {
location: format!("Property {}", i),
size: 1000 + i,
legal_description: format!("Description {}", i),
valuation: 100_000 + (i as u128 * 1000),
documents_url: format!("ipfs://prop{}", i),
};

let token_id = contract
.register_property_with_token(metadata)
.expect("Token registration should succeed in test");
assert_eq!(token_id, i);
assert_eq!(contract.total_supply(), i);
}
}

#[ink::test]
fn test_transfer_preserves_total_supply() {
let mut contract = setup_contract();
let accounts = test::default_accounts::<DefaultEnvironment>();
test::set_caller::<DefaultEnvironment>(accounts.alice);

let metadata = PropertyMetadata {
location: String::from("123 Main St"),
size: 1000,
legal_description: String::from("Sample property"),
valuation: 500000,
documents_url: String::from("ipfs://sample-docs"),
};

let token_id = contract
.register_property_with_token(metadata)
.expect("Token registration should succeed in test");

let initial_supply = contract.total_supply();

contract
.transfer_from(accounts.alice, accounts.bob, token_id)
.expect("Transfer should succeed");

// Total supply should remain constant
assert_eq!(contract.total_supply(), initial_supply);
}

#[ink::test]
fn test_balance_of_batch_empty_vectors() {
let contract = setup_contract();

let result = contract.balance_of_batch(Vec::new(), Vec::new());
assert_eq!(result, Vec::<u128>::new());
}

#[ink::test]
fn test_get_error_count_nonexistent() {
let contract = setup_contract();
let accounts = test::default_accounts::<DefaultEnvironment>();

let count = contract.get_error_count(accounts.alice, "NONEXISTENT".to_string());
assert_eq!(count, 0);
}

#[ink::test]
fn test_get_error_rate_nonexistent() {
let contract = setup_contract();

let rate = contract.get_error_rate("NONEXISTENT".to_string());
assert_eq!(rate, 0);
}

#[ink::test]
fn test_get_recent_errors_unauthorized() {
let contract = setup_contract();
let accounts = test::default_accounts::<DefaultEnvironment>();

// Non-admin tries to get errors
test::set_caller::<DefaultEnvironment>(accounts.bob);
let errors = contract.get_recent_errors(10);
assert_eq!(errors, Vec::new());
}
}
}
Loading
Loading