Skip to content

Commit ebd98c8

Browse files
committed
test: add targeted coverage for optional _from_protobuf branches to reach 92% patch coverage
Signed-off-by: Mounil Kanakhara <mounilkankhara@gmail.com>
1 parent ffeef74 commit ebd98c8

15 files changed

Lines changed: 563 additions & 0 deletions

tests/unit/account_create_transaction_test.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,3 +577,37 @@ def test_from_bytes_without_auto_renew_period(mock_account_ids):
577577
assert isinstance(reconstructed, AccountCreateTransaction)
578578
assert reconstructed.initial_balance == 1000
579579
assert reconstructed.auto_renew_period is None
580+
581+
582+
def test_from_bytes_with_staked_account_id(mock_account_ids):
583+
"""staked_account_id branch in _from_protobuf is covered."""
584+
operator_id, node_account_id = mock_account_ids
585+
586+
tx = AccountCreateTransaction()
587+
tx.set_staked_account_id(AccountId(0, 0, 7))
588+
tx.transaction_id = generate_transaction_id(operator_id)
589+
tx.node_account_id = node_account_id
590+
tx.freeze()
591+
592+
reconstructed = Transaction.from_bytes(tx.to_bytes())
593+
594+
assert isinstance(reconstructed, AccountCreateTransaction)
595+
assert reconstructed.staked_account_id == AccountId(0, 0, 7)
596+
assert reconstructed.staked_node_id is None
597+
598+
599+
def test_from_bytes_with_staked_node_id(mock_account_ids):
600+
"""staked_node_id branch in _from_protobuf is covered."""
601+
operator_id, node_account_id = mock_account_ids
602+
603+
tx = AccountCreateTransaction()
604+
tx.set_staked_node_id(3)
605+
tx.transaction_id = generate_transaction_id(operator_id)
606+
tx.node_account_id = node_account_id
607+
tx.freeze()
608+
609+
reconstructed = Transaction.from_bytes(tx.to_bytes())
610+
611+
assert isinstance(reconstructed, AccountCreateTransaction)
612+
assert reconstructed.staked_node_id == 3
613+
assert reconstructed.staked_account_id is None

tests/unit/account_update_transaction_test.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,3 +842,28 @@ def test_from_bytes(mock_account_ids):
842842
assert reconstructed.decline_staking_reward == False
843843
assert reconstructed.receiver_signature_required == False
844844
assert reconstructed.max_automatic_token_associations == 10
845+
846+
847+
def test_from_bytes_with_key_expiration_and_staked_account(mock_account_ids):
848+
"""Covers key, expiration_time, and staked_account_id branches in _from_protobuf."""
849+
operator_id, _, node_account_id, _, _ = mock_account_ids
850+
851+
key = PrivateKey.generate().public_key()
852+
expiry = Timestamp(seconds=9_999_999_999, nanos=0)
853+
854+
tx = AccountUpdateTransaction()
855+
tx.set_account_id(AccountId(0, 0, 1))
856+
tx.set_key(key)
857+
tx.set_expiration_time(expiry)
858+
tx.set_staked_account_id(AccountId(0, 0, 9))
859+
tx.transaction_id = TransactionId.generate(operator_id)
860+
tx.node_account_id = node_account_id
861+
tx.freeze()
862+
863+
reconstructed = Transaction.from_bytes(tx.to_bytes())
864+
865+
assert isinstance(reconstructed, AccountUpdateTransaction)
866+
assert reconstructed.key.to_bytes_raw() == key.to_bytes_raw()
867+
assert reconstructed.expiration_time.seconds == expiry.seconds
868+
assert reconstructed.staked_account_id == AccountId(0, 0, 9)
869+
assert reconstructed.staked_node_id is None

tests/unit/contract_create_transaction_test.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,3 +449,54 @@ def test_from_bytes(mock_account_ids):
449449
assert reconstructed.gas == 100_000
450450
assert reconstructed.bytecode == b"\x60\x80\x60\x40"
451451
assert reconstructed.contract_memo == "hello"
452+
453+
454+
def test_from_bytes_with_optional_fields(mock_account_ids):
455+
"""Covers adminKey, proxyAccountID, auto_renew_account_id, fileID, staked_account_id, staked_node_id."""
456+
from hiero_sdk_python.transaction.transaction import Transaction
457+
from hiero_sdk_python.transaction.transaction_id import TransactionId
458+
459+
operator_id, _, node_account_id, _, _ = mock_account_ids
460+
461+
admin_key = PrivateKey.generate().public_key()
462+
463+
tx = ContractCreateTransaction()
464+
tx.set_admin_key(admin_key)
465+
tx.set_bytecode_file_id(FileId(0, 0, 42))
466+
tx.set_gas(1000)
467+
tx.set_auto_renew_account_id(AccountId(0, 0, 5))
468+
tx.set_staked_account_id(AccountId(0, 0, 6))
469+
tx.transaction_id = TransactionId.generate(operator_id)
470+
tx.node_account_id = node_account_id
471+
tx.freeze()
472+
473+
reconstructed = Transaction.from_bytes(tx.to_bytes())
474+
475+
assert isinstance(reconstructed, ContractCreateTransaction)
476+
assert reconstructed.admin_key is not None
477+
assert reconstructed.bytecode_file_id == FileId(0, 0, 42)
478+
assert reconstructed.auto_renew_account_id == AccountId(0, 0, 5)
479+
assert reconstructed.staked_account_id == AccountId(0, 0, 6)
480+
assert reconstructed.staked_node_id is None
481+
482+
483+
def test_from_bytes_with_staked_node_id(mock_account_ids):
484+
"""Covers staked_node_id branch in _from_protobuf."""
485+
from hiero_sdk_python.transaction.transaction import Transaction
486+
from hiero_sdk_python.transaction.transaction_id import TransactionId
487+
488+
operator_id, _, node_account_id, _, _ = mock_account_ids
489+
490+
tx = ContractCreateTransaction()
491+
tx.set_bytecode(b"\x00")
492+
tx.set_gas(1000)
493+
tx.set_staked_node_id(7)
494+
tx.transaction_id = TransactionId.generate(operator_id)
495+
tx.node_account_id = node_account_id
496+
tx.freeze()
497+
498+
reconstructed = Transaction.from_bytes(tx.to_bytes())
499+
500+
assert isinstance(reconstructed, ContractCreateTransaction)
501+
assert reconstructed.staked_node_id == 7
502+
assert reconstructed.staked_account_id is None

tests/unit/contract_delete_transaction_test.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,3 +453,25 @@ def test_from_bytes_with_permanent_removal(mock_account_ids):
453453
assert reconstructed.permanent_removal is True
454454
assert reconstructed.transfer_account_id is None
455455
assert reconstructed.transfer_contract_id is None
456+
457+
458+
def test_from_bytes_with_transfer_contract_id(mock_account_ids):
459+
"""Covers transferContractID branch in _from_protobuf."""
460+
from hiero_sdk_python.transaction.transaction import Transaction
461+
from hiero_sdk_python.transaction.transaction_id import TransactionId
462+
463+
operator_id, _, node_account_id, _, _ = mock_account_ids
464+
465+
tx = ContractDeleteTransaction()
466+
tx.set_contract_id(ContractId(0, 0, 10))
467+
tx.set_transfer_contract_id(ContractId(0, 0, 20))
468+
tx.transaction_id = TransactionId.generate(operator_id)
469+
tx.node_account_id = node_account_id
470+
tx.freeze()
471+
472+
reconstructed = Transaction.from_bytes(tx.to_bytes())
473+
474+
assert isinstance(reconstructed, ContractDeleteTransaction)
475+
assert reconstructed.contract_id == ContractId(0, 0, 10)
476+
assert reconstructed.transfer_contract_id == ContractId(0, 0, 20)
477+
assert reconstructed.transfer_account_id is None

tests/unit/contract_update_transaction_test.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,3 +405,61 @@ def test_from_bytes(mock_account_ids, contract_id):
405405
assert reconstructed.contract_id == contract_id
406406
assert reconstructed.contract_memo == "updated memo"
407407
assert reconstructed.auto_renew_period.seconds == 7_776_000
408+
409+
410+
def test_from_bytes_with_optional_fields(mock_account_ids, contract_id):
411+
"""Covers expiration_time, adminKey, max_auto_token_assoc, auto_renew_account, decline_reward, staked."""
412+
from hiero_sdk_python.timestamp import Timestamp
413+
from hiero_sdk_python.transaction.transaction import Transaction
414+
from hiero_sdk_python.transaction.transaction_id import TransactionId
415+
416+
operator_id, _, node_account_id, _, _ = mock_account_ids
417+
418+
admin_key = PrivateKey.generate().public_key()
419+
expiry = Timestamp(seconds=9_999_999_999, nanos=0)
420+
421+
tx = ContractUpdateTransaction()
422+
tx.set_contract_id(contract_id)
423+
tx.set_expiration_time(expiry)
424+
tx.set_admin_key(admin_key)
425+
tx.set_max_automatic_token_associations(5)
426+
tx.set_auto_renew_account_id(AccountId(0, 0, 8))
427+
tx.set_decline_reward(True)
428+
tx.set_staked_account_id(AccountId(0, 0, 9))
429+
tx.transaction_id = TransactionId.generate(operator_id)
430+
tx.node_account_id = node_account_id
431+
tx.freeze()
432+
433+
reconstructed = Transaction.from_bytes(tx.to_bytes())
434+
435+
assert isinstance(reconstructed, ContractUpdateTransaction)
436+
assert reconstructed.expiration_time.seconds == expiry.seconds
437+
assert reconstructed.admin_key is not None
438+
assert reconstructed.max_automatic_token_associations == 5
439+
assert reconstructed.auto_renew_account_id == AccountId(0, 0, 8)
440+
assert reconstructed.decline_reward is True
441+
assert reconstructed.staked_account_id == AccountId(0, 0, 9)
442+
443+
444+
def test_from_bytes_with_staked_node_id_and_old_memo(mock_account_ids, contract_id):
445+
"""Covers staked_node_id and old-format memo field in _from_protobuf."""
446+
from hiero_sdk_python.hapi.services import contract_update_pb2, transaction_pb2
447+
448+
operator_id, _, node_account_id, _, _ = mock_account_ids
449+
450+
body = contract_update_pb2.ContractUpdateTransactionBody(
451+
contractID=contract_id._to_proto(),
452+
staked_node_id=4,
453+
memo="old memo style",
454+
)
455+
tx_body = transaction_pb2.TransactionBody()
456+
tx_body.contractUpdateInstance.CopyFrom(body)
457+
458+
from hiero_sdk_python.contract.contract_update_transaction import ContractUpdateTransaction
459+
460+
result = ContractUpdateTransaction._from_protobuf(tx_body, b"", None)
461+
462+
assert isinstance(result, ContractUpdateTransaction)
463+
assert result.staked_node_id == 4
464+
assert result.contract_memo == "old memo style"
465+
assert result.staked_account_id is None

tests/unit/custom_fee_test.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,3 +293,38 @@ def test_set_amount_in_tinybars_deprecation():
293293
# Verify the method still works correctly
294294
assert fee.amount == 100
295295
assert fee.denominating_token_id is None
296+
297+
298+
def test_custom_fixed_fee_from_proto_raises_without_fixed_fee():
299+
"""_from_proto raises ValueError when proto has no fixed_fee field."""
300+
from hiero_sdk_python.hapi.services.custom_fees_pb2 import CustomFee as CustomFeeProto
301+
302+
proto = CustomFeeProto()
303+
with pytest.raises(ValueError, match="fixed_fee is required"):
304+
CustomFixedFee._from_proto(proto)
305+
306+
307+
def test_custom_fixed_fee_from_topic_fee_proto():
308+
"""_from_topic_fee_proto deserializes a FixedCustomFee proto correctly."""
309+
310+
fee = CustomFixedFee(
311+
amount=250,
312+
denominating_token_id=TokenId(0, 0, 55),
313+
fee_collector_account_id=AccountId(0, 0, 77),
314+
)
315+
proto = fee._to_topic_fee_proto()
316+
result = CustomFixedFee._from_topic_fee_proto(proto)
317+
318+
assert isinstance(result, CustomFixedFee)
319+
assert result.amount == 250
320+
assert result.denominating_token_id == TokenId(0, 0, 55)
321+
assert result.fee_collector_account_id == AccountId(0, 0, 77)
322+
323+
324+
def test_custom_fixed_fee_from_topic_fee_proto_raises_without_fixed_fee():
325+
"""_from_topic_fee_proto raises ValueError when proto has no fixed_fee."""
326+
from hiero_sdk_python.hapi.services.custom_fees_pb2 import FixedCustomFee
327+
328+
proto = FixedCustomFee()
329+
with pytest.raises(ValueError, match="fixed_fee is required"):
330+
CustomFixedFee._from_topic_fee_proto(proto)

tests/unit/node_create_transaction_test.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,3 +362,27 @@ def test_from_bytes(mock_account_ids):
362362
assert reconstructed.service_endpoints[0].get_domain_name() == "service.example.com"
363363
assert reconstructed.gossip_ca_certificate == b"test-ca-cert"
364364
assert reconstructed.grpc_certificate_hash == b"test-cert-hash"
365+
366+
367+
def test_from_bytes_with_admin_key(mock_account_ids):
368+
"""Covers admin_key branch in _from_protobuf for NodeCreateTransaction."""
369+
from hiero_sdk_python.crypto.private_key import PrivateKey
370+
from hiero_sdk_python.transaction.transaction import Transaction
371+
from hiero_sdk_python.transaction.transaction_id import TransactionId
372+
373+
operator_id, _, node_account_id, _, _ = mock_account_ids
374+
375+
admin_key = PrivateKey.generate_ed25519().public_key()
376+
377+
tx = NodeCreateTransaction()
378+
tx.set_account_id(AccountId(0, 0, 99))
379+
tx.set_admin_key(admin_key)
380+
tx.transaction_id = TransactionId.generate(operator_id)
381+
tx.node_account_id = node_account_id
382+
tx.freeze()
383+
384+
reconstructed = Transaction.from_bytes(tx.to_bytes())
385+
386+
assert isinstance(reconstructed, NodeCreateTransaction)
387+
assert reconstructed.admin_key is not None
388+
assert reconstructed.admin_key.to_bytes_raw() == admin_key.to_bytes_raw()

tests/unit/node_update_transaction_test.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,3 +385,27 @@ def test_from_bytes(mock_account_ids):
385385
assert reconstructed.service_endpoints[0].get_domain_name() == "service.example.com"
386386
assert reconstructed.gossip_ca_certificate == b"test-ca-cert"
387387
assert reconstructed.grpc_certificate_hash == b"test-cert-hash"
388+
389+
390+
def test_from_bytes_with_admin_key(mock_account_ids):
391+
"""Covers admin_key branch in _from_protobuf for NodeUpdateTransaction."""
392+
from hiero_sdk_python.crypto.private_key import PrivateKey
393+
from hiero_sdk_python.transaction.transaction import Transaction
394+
from hiero_sdk_python.transaction.transaction_id import TransactionId
395+
396+
operator_id, _, node_account_id, _, _ = mock_account_ids
397+
398+
admin_key = PrivateKey.generate_ed25519().public_key()
399+
400+
tx = NodeUpdateTransaction()
401+
tx.set_node_id(2)
402+
tx.set_admin_key(admin_key)
403+
tx.transaction_id = TransactionId.generate(operator_id)
404+
tx.node_account_id = node_account_id
405+
tx.freeze()
406+
407+
reconstructed = Transaction.from_bytes(tx.to_bytes())
408+
409+
assert isinstance(reconstructed, NodeUpdateTransaction)
410+
assert reconstructed.admin_key is not None
411+
assert reconstructed.admin_key.to_bytes_raw() == admin_key.to_bytes_raw()

tests/unit/schedule_create_transaction_test.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,3 +294,30 @@ def test_from_bytes(mock_account_ids):
294294
assert reconstructed.payer_account_id == account_id_sender
295295
assert reconstructed.schedulable_body == schedulable_body
296296
assert reconstructed.schedulable_body.HasField("tokenGrantKyc")
297+
298+
299+
def test_from_bytes_with_admin_key_and_expiration(mock_account_ids):
300+
"""Covers admin_key and expiration_time branches in _from_protobuf."""
301+
account_id_sender, _, node_account_id, token_id_1, _ = mock_account_ids
302+
303+
admin_key = PrivateKey.generate_ed25519().public_key()
304+
expiry = Timestamp(seconds=9_999_999_999, nanos=0)
305+
306+
inner_tx = TokenGrantKycTransaction()
307+
inner_tx.set_token_id(token_id_1)
308+
inner_tx.set_account_id(account_id_sender)
309+
schedulable_body = inner_tx.build_scheduled_body()
310+
311+
tx = ScheduleCreateTransaction()
312+
tx.set_admin_key(admin_key)
313+
tx.set_expiration_time(expiry)
314+
tx._set_schedulable_body(schedulable_body)
315+
tx.transaction_id = TransactionId.generate(account_id_sender)
316+
tx.node_account_id = node_account_id
317+
tx.freeze()
318+
319+
reconstructed = Transaction.from_bytes(tx.to_bytes())
320+
321+
assert isinstance(reconstructed, ScheduleCreateTransaction)
322+
assert reconstructed.admin_key is not None
323+
assert reconstructed.expiration_time.seconds == expiry.seconds

tests/unit/token_airdrop_transaction_test.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,3 +447,62 @@ def test_from_bytes_nft(mock_account_ids):
447447
assert nft_transfers[0].receiver_id == receiver
448448
assert nft_transfers[0].serial_number == 7
449449
assert not nft_transfers[0].is_approved
450+
451+
452+
def test_from_protobuf_skips_transfer_without_token():
453+
"""Covers continue branch when token field is missing from a TokenTransferList."""
454+
from hiero_sdk_python.hapi.services import transaction_pb2
455+
from hiero_sdk_python.hapi.services.basic_types_pb2 import TokenTransferList
456+
from hiero_sdk_python.hapi.services.token_airdrop_pb2 import TokenAirdropTransactionBody
457+
458+
body = TokenAirdropTransactionBody()
459+
body.token_transfers.append(TokenTransferList())
460+
tx_body = transaction_pb2.TransactionBody()
461+
tx_body.tokenAirdrop.CopyFrom(body)
462+
463+
result = TokenAirdropTransaction._from_protobuf(tx_body, b"", None)
464+
465+
assert isinstance(result, TokenAirdropTransaction)
466+
assert len(result.token_transfers) == 0
467+
468+
469+
def test_from_protobuf_skips_fungible_transfer_without_account_id(mock_account_ids):
470+
"""Covers continue branch when accountID is missing from a fungible transfer."""
471+
from hiero_sdk_python.hapi.services import transaction_pb2
472+
from hiero_sdk_python.hapi.services.basic_types_pb2 import AccountAmount, TokenTransferList
473+
from hiero_sdk_python.hapi.services.token_airdrop_pb2 import TokenAirdropTransactionBody
474+
475+
_, _, _, token_id_1, _ = mock_account_ids
476+
477+
body = TokenAirdropTransactionBody()
478+
transfer_list = TokenTransferList(token=token_id_1._to_proto())
479+
transfer_list.transfers.append(AccountAmount(amount=100))
480+
body.token_transfers.append(transfer_list)
481+
tx_body = transaction_pb2.TransactionBody()
482+
tx_body.tokenAirdrop.CopyFrom(body)
483+
484+
result = TokenAirdropTransaction._from_protobuf(tx_body, b"", None)
485+
486+
assert isinstance(result, TokenAirdropTransaction)
487+
assert len(result.token_transfers[token_id_1]) == 0
488+
489+
490+
def test_from_protobuf_skips_nft_transfer_without_sender_or_receiver(mock_account_ids):
491+
"""Covers continue branch when senderAccountID or receiverAccountID is missing from NFT transfer."""
492+
from hiero_sdk_python.hapi.services import transaction_pb2
493+
from hiero_sdk_python.hapi.services.basic_types_pb2 import NftTransfer, TokenTransferList
494+
from hiero_sdk_python.hapi.services.token_airdrop_pb2 import TokenAirdropTransactionBody
495+
496+
_, _, _, token_id_1, _ = mock_account_ids
497+
498+
body = TokenAirdropTransactionBody()
499+
transfer_list = TokenTransferList(token=token_id_1._to_proto())
500+
transfer_list.nftTransfers.append(NftTransfer(serialNumber=1))
501+
body.token_transfers.append(transfer_list)
502+
tx_body = transaction_pb2.TransactionBody()
503+
tx_body.tokenAirdrop.CopyFrom(body)
504+
505+
result = TokenAirdropTransaction._from_protobuf(tx_body, b"", None)
506+
507+
assert isinstance(result, TokenAirdropTransaction)
508+
assert len(result.nft_transfers[token_id_1]) == 0

0 commit comments

Comments
 (0)