Skip to content
Open
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
60 changes: 35 additions & 25 deletions node/machine_passport_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,24 @@ def get_optional_json_object():
return data, None


def get_required_json_object():
"""Return a required JSON object body or an error response."""
data = request.get_json(silent=True)
if not data:
return None, (jsonify({
'ok': False,
'error': 'invalid_request',
'message': 'JSON body required',
}), 400)
if not isinstance(data, dict):
return None, (jsonify({
'ok': False,
'error': 'invalid_request',
'message': 'JSON object required',
}), 400)
return data, None


# === Public Read Endpoints ===

@machine_passport_bp.route('/<machine_id>', methods=['GET'])
Expand Down Expand Up @@ -210,13 +228,9 @@ def create_passport():
'message': 'Admin key required',
}), 401

data = request.get_json()
if not data:
return jsonify({
'ok': False,
'error': 'invalid_request',
'message': 'JSON body required',
}), 400
data, error = get_required_json_object()
if error:
return error

# Validate required fields
required = ['name', 'owner_miner_id']
Expand Down Expand Up @@ -304,13 +318,9 @@ def update_passport(machine_id: str):
'message': 'Admin key required',
}), 401

data = request.get_json()
if not data:
return jsonify({
'ok': False,
'error': 'invalid_request',
'message': 'JSON body required',
}), 400
data, error = get_required_json_object()
if error:
return error

success, msg = ledger.update_passport(machine_id, data)

Expand Down Expand Up @@ -346,8 +356,10 @@ def add_repair_entry(machine_id: str):
if not passport:
return jsonify({'ok': False, 'error': 'passport_not_found'}), 404

data = request.get_json()
if not data or 'repair_type' not in data or 'description' not in data:
data, error = get_required_json_object()
if error:
return error
if 'repair_type' not in data or 'description' not in data:
return jsonify({
'ok': False,
'error': 'missing_field',
Expand Down Expand Up @@ -479,8 +491,10 @@ def add_lineage_note(machine_id: str):
if not passport:
return jsonify({'ok': False, 'error': 'passport_not_found'}), 404

data = request.get_json()
if not data or 'event_type' not in data:
data, error = get_required_json_object()
if error:
return error
if 'event_type' not in data:
return jsonify({
'ok': False,
'error': 'missing_field',
Expand Down Expand Up @@ -614,13 +628,9 @@ def compute_machine_id_endpoint():

Request Body: Hardware fingerprint data (same as attestation)
"""
data = request.get_json()
if not data:
return jsonify({
'ok': False,
'error': 'invalid_request',
'message': 'JSON body required',
}), 400
data, error = get_required_json_object()
if error:
return error

machine_id = compute_machine_id(data)

Expand Down
21 changes: 21 additions & 0 deletions tests/test_machine_passport_event_json_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,24 @@ def test_benchmark_route_accepts_object_json(client, ledger):
assert response.status_code == 200
assert ledger.benchmark_payload["compute_score"] == 1250.0
assert ledger.benchmark_payload["memory_bandwidth"] == 3200.5


@pytest.mark.parametrize(
("method", "path", "payload"),
(
("post", "/api/machine-passport", ["name", "owner_miner_id"]),
("put", "/api/machine-passport/machine-1", ["name"]),
("post", "/api/machine-passport/machine-1/repair-log", ["repair_type", "description"]),
("post", "/api/machine-passport/machine-1/lineage", ["event_type"]),
("post", "/api/machine-passport/compute-machine-id", ["not", "object"]),
),
)
def test_required_machine_passport_routes_reject_non_object_json(client, method, path, payload):
response = getattr(client, method)(path, json=payload)

assert response.status_code == 400
assert response.get_json() == {
"ok": False,
"error": "invalid_request",
"message": "JSON object required",
}
16 changes: 7 additions & 9 deletions tests/test_wallet_show_regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,20 @@ def test_balance_response_parsing(self):
balance = resp.get("amount_rtc", resp.get("balance_rtc", resp.get("balance", 0)))
assert isinstance(balance, (int, float))

@patch('urllib.request.urlopen')
@patch('rustchain_cli.urlopen')
def test_wallet_show_handles_network_error_gracefully(self, mock_urlopen):
"""Test that wallet show handles network errors without crashing."""
import urllib.error

# Simulate network timeout
mock_urlopen.side_effect = urllib.error.URLError("timeout")

# Should not raise exception, should handle gracefully
# This is the behavior we want to preserve
try:
# Test the balance fetch logic directly
result = fetch_api("/wallet/balance?miner_id=test")
except Exception as e:
# Expected to fail with network error
assert "timeout" in str(e).lower() or "network" in str(e).lower()
# The CLI reports a controlled network error and exits instead of
# leaking a urllib traceback.
with pytest.raises(SystemExit) as exc_info:
fetch_api("/wallet/balance?miner_id=test")

assert exc_info.value.code == 1

def test_balance_endpoint_returns_valid_json(self):
"""Integration test: verify /wallet/balance returns valid JSON."""
Expand Down
Loading