"""Offline tests for the Dragonchain Python SDK. No network access required. The HMAC test independently recomputes the expected Authorization header to guard the byte-for-byte contract with the prime-node authorizer. """ import base64 import hashlib import hmac import json import pytest from prime_sdk import ( Block, DragonchainAPIError, DragonchainSDK, InterchainTrace, SmartContractCreateRequest, Transaction, TransactionCreateRequest, ) from prime_sdk.client import Client from prime_sdk.credentials import load_config_from_string PUBLIC_ID = "test-public-id" AUTH_KEY_ID = "test-key-id" AUTH_KEY = "test-secret-key" BASE_URL = "https://chains.dragonchain.com" def _expected_auth_header(method, path, timestamp, content_type, body: bytes) -> str: """Recompute the expected header from scratch, independent of the SDK.""" b64_content = base64.b64encode(hashlib.sha256(body).digest()).decode() message = "\n".join([method.upper(), path, PUBLIC_ID, timestamp, content_type, b64_content]) digest = hmac.new(AUTH_KEY.encode(), message.encode(), hashlib.sha256).digest() return f"DC1-HMAC-SHA256 {AUTH_KEY_ID}:{base64.b64encode(digest).decode()}" # --------------------------------------------------------------------------- # # HMAC signing # --------------------------------------------------------------------------- # def test_hmac_message_format(): c = Client(PUBLIC_ID, AUTH_KEY_ID, AUTH_KEY, BASE_URL) msg = c._hmac_message("post", "/api/v1/transaction", "1700000000", "application/json", b"hello") lines = msg.split("\n") assert len(lines) == 6 assert lines[0] == "POST" # method uppercased assert lines[1] == "/api/v1/transaction" assert lines[2] == PUBLIC_ID assert lines[3] == "1700000000" assert lines[4] == "application/json" assert lines[5] == base64.b64encode(hashlib.sha256(b"hello").digest()).decode() assert not msg.endswith("\n") # no trailing newline def test_auth_header_matches_independent_computation(): c = Client(PUBLIC_ID, AUTH_KEY_ID, AUTH_KEY, BASE_URL) body = b'{"txn_type":"x","payload":"y"}' got = c._generate_auth_header("POST", "/api/v1/transaction", "1700000000", "application/json", body) want = _expected_auth_header("POST", "/api/v1/transaction", "1700000000", "application/json", body) assert got == want def test_empty_body_signs_sha256_of_empty(): c = Client(PUBLIC_ID, AUTH_KEY_ID, AUTH_KEY, BASE_URL) got = c._generate_auth_header("GET", "/api/v1/status", "1700000000", "", b"") want = _expected_auth_header("GET", "/api/v1/status", "1700000000", "", b"") assert got == want def test_base_url_trailing_slash_stripped(): c = Client(PUBLIC_ID, AUTH_KEY_ID, AUTH_KEY, BASE_URL + "/") assert c.endpoint() == BASE_URL # --------------------------------------------------------------------------- # # Request plumbing (with a fake session) # --------------------------------------------------------------------------- # class _FakeResponse: def __init__(self, status_code, text): self.status_code = status_code self.text = text class _FakeSession: def __init__(self, response): self.response = response self.calls = [] def request(self, method, url, data=None, headers=None, timeout=None, allow_redirects=None): self.calls.append( { "method": method, "url": url, "data": data, "headers": headers, "allow_redirects": allow_redirects, } ) return self.response def test_create_sends_correct_request_and_parses_response(): sdk = DragonchainSDK(PUBLIC_ID, AUTH_KEY_ID, AUTH_KEY, BASE_URL) fake = _FakeSession(_FakeResponse(200, json.dumps({"transaction_id": "txn-123"}))) sdk.get_client()._session = fake resp = sdk.transaction.create( TransactionCreateRequest(txn_type="my-type", payload='{"a":1}', tag="t") ) assert resp.transaction_id == "txn-123" call = fake.calls[0] assert call["method"] == "POST" assert call["url"] == BASE_URL + "/api/v1/transaction" # headers present and consistent headers = call["headers"] assert headers["Dragonchain"] == PUBLIC_ID assert headers["Content-Type"] == "application/json" assert headers["Authorization"].startswith(f"DC1-HMAC-SHA256 {AUTH_KEY_ID}:") # the bytes sent are exactly the bytes that were signed sent = call["data"] expected_auth = _expected_auth_header( "POST", "/api/v1/transaction", headers["Timestamp"], "application/json", sent ) assert headers["Authorization"] == expected_auth # omitempty: version dropped, tag kept body = json.loads(sent) assert body == {"txn_type": "my-type", "payload": '{"a":1}', "tag": "t"} def test_error_status_raises(): sdk = DragonchainSDK(PUBLIC_ID, AUTH_KEY_ID, AUTH_KEY, BASE_URL) sdk.get_client()._session = _FakeSession(_FakeResponse(404, " not found ")) with pytest.raises(DragonchainAPIError) as exc: sdk.transaction.get("missing") assert exc.value.status_code == 404 assert exc.value.body == "not found" def test_get_sends_no_content_type_header(): sdk = DragonchainSDK(PUBLIC_ID, AUTH_KEY_ID, AUTH_KEY, BASE_URL) fake = _FakeSession(_FakeResponse(200, json.dumps({"id": "chain", "level": 1}))) sdk.get_client()._session = fake sdk.system.status() headers = fake.calls[0]["headers"] assert "Content-Type" not in headers assert fake.calls[0]["data"] == b"" # --------------------------------------------------------------------------- # # Model (de)serialization # --------------------------------------------------------------------------- # def test_transaction_from_dict_nested(): raw = { "version": "1", "header": {"txn_id": "abc", "block_id": "42", "txn_type": "t"}, "proof": {"full": "ff", "stripped": "ss"}, "payload": "{}", } txn = Transaction.from_dict(raw) assert txn.header.txn_id == "abc" assert txn.header.block_id == "42" assert txn.proof.full == "ff" assert txn.payload == "{}" def test_block_from_dict_nested_header(): # Real server shape: id/prev/timestamp nested under "header" with camelCase # keys; proof carries only "proof" on a trust-scheme chain. raw = { "version": "1", "header": { "blockId": "69569983", "dcId": "chain-xyz", "prevId": "69186326", "prevProof": "MEUCIQ...", "timestamp": "1780088135", }, "proof": {"proof": "MEQCIF..."}, "transactions": ["{...}", "{...}", "{...}"], } blk = Block.from_dict(raw) assert blk.version == "1" assert blk.header.block_id == "69569983" assert blk.header.dc_id == "chain-xyz" assert blk.header.prev_id == "69186326" assert blk.header.prev_proof == "MEUCIQ..." assert blk.header.timestamp == "1780088135" assert blk.proof.proof == "MEQCIF..." assert blk.proof.scheme == "" # absent on trust-scheme chain assert len(blk.transactions) == 3 def test_interchain_trace_from_dict_nested(): # Server shape: validatorBlocks + interchainTransactions arrays, camelCase. raw = { "blockId": "69636602", "validatorBlocks": [ { "version": "1", "primeChainId": "zDYr", "primeBlockId": "69636602", "verifierPublicKey": "02c4...", "verifierSignature": "MEUC...", } ], "interchainTransactions": [ { "id": 19, "chainId": "1", "transHash": "0xd46e", "validatorBlocks": ["69636602"], "coveredPrimeChainIds": ["zDYr"], } ], } trace = InterchainTrace.from_dict(raw) assert trace.block_id == "69636602" assert len(trace.validator_blocks) == 1 assert trace.validator_blocks[0].prime_block_id == "69636602" assert trace.validator_blocks[0].verifier_public_key == "02c4..." assert len(trace.interchain_transactions) == 1 assert trace.interchain_transactions[0].id == 19 assert trace.interchain_transactions[0].trans_hash == "0xd46e" assert trace.interchain_transactions[0].chain_id == "1" assert trace.interchain_transactions[0].validator_blocks == ["69636602"] assert trace.interchain_transactions[0].covered_prime_chain_ids == ["zDYr"] def test_contract_create_request_omitempty(): req = SmartContractCreateRequest( environment="python3.8", transaction_type="my-type", execution_order="parallel", ) d = req.to_dict() assert d == { "environment": "python3.8", "transactionType": "my-type", "executionOrder": "parallel", } # remote False is dropped; envvars/secrets absent assert "remote" not in d assert "environmentVariables" not in d assert "secret" not in d def test_contract_create_request_with_optionals(): req = SmartContractCreateRequest( environment="python3.8", transaction_type="my-type", execution_order="serial", environment_variables={"K": "V"}, secrets={"S": "v"}, remote=True, ) d = req.to_dict() assert d["environmentVariables"] == {"K": "V"} assert d["secret"] == {"S": "v"} assert d["remote"] is True def test_credentials_round_trip(): yaml_text = """ default: pub-1 chains: - name: alpha publicId: pub-1 authKeyId: kid-1 authKey: secret-1 endpoint: https://chains.dragonchain.com - name: beta publicId: pub-2 authKeyId: kid-2 authKey: secret-2 endpoint: https://dev-chains.dragonchain.com """ cfg = load_config_from_string(yaml_text) assert cfg.default == "pub-1" assert cfg.list_chains() == ["alpha", "beta"] default = cfg.get_default_chain() assert default is not None and default.auth_key_id == "kid-1" assert cfg.get_chain_by_public_id("pub-2").endpoint.endswith("dev-chains.dragonchain.com") with pytest.raises(ValueError): cfg.delete_chain("pub-1") # default cfg.delete_chain("pub-2") assert cfg.list_chains() == ["alpha"]