refactor(coordinator-api,blockchain-explorer): add response caching and fix timestamp handling
- Add cached decorator to admin stats, job status, payment status, and marketplace stats endpoints - Configure cache TTLs using get_cache_config for different endpoint types (1min job_list, 30s user_balance, marketplace_stats) - Import cache_management router and include it in main app with /v1 prefix - Fix blockchain-explorer formatTimestamp to handle both ISO string and Unix numeric timestamps with type
This commit is contained in:
172
tests/test_explorer_fixes.py
Normal file
172
tests/test_explorer_fixes.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""
|
||||
Test Explorer fixes - simplified integration tests
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import configparser
|
||||
import os
|
||||
|
||||
|
||||
class TestExplorerFixes:
|
||||
"""Test the Explorer fixes implemented"""
|
||||
|
||||
def test_pytest_configuration_restored(self):
|
||||
"""Test that pytest.ini now includes full test coverage"""
|
||||
# Read pytest.ini
|
||||
config_path = os.path.join(os.path.dirname(__file__), '../pytest.ini')
|
||||
config = configparser.ConfigParser()
|
||||
config.read(config_path)
|
||||
|
||||
# Verify pytest section exists
|
||||
assert 'pytest' in config, "pytest section not found in pytest.ini"
|
||||
|
||||
# Verify testpaths includes full tests directory
|
||||
testpaths = config.get('pytest', 'testpaths')
|
||||
assert testpaths == 'tests', f"Expected 'tests', got '{testpaths}'"
|
||||
|
||||
# Verify it's not limited to CLI only
|
||||
assert 'tests/cli' not in testpaths, "testpaths should not be limited to CLI only"
|
||||
|
||||
print("✅ pytest.ini test coverage restored to full 'tests' directory")
|
||||
|
||||
def test_explorer_file_contains_transaction_endpoint(self):
|
||||
"""Test that Explorer main.py contains the transaction endpoint"""
|
||||
explorer_path = os.path.join(os.path.dirname(__file__), '../apps/blockchain-explorer/main.py')
|
||||
|
||||
with open(explorer_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check for transaction endpoint
|
||||
assert '@app.get("/api/transactions/{tx_hash}")' in content, "Transaction endpoint not found"
|
||||
|
||||
# Check for correct RPC URL (should be /rpc/tx/ not /tx/)
|
||||
assert 'BLOCKCHAIN_RPC_URL}/rpc/tx/{tx_hash}' in content, "Incorrect RPC URL for transaction"
|
||||
|
||||
# Check for field mapping
|
||||
assert '"hash": tx.get("tx_hash")' in content, "Field mapping for hash not found"
|
||||
assert '"from": tx.get("sender")' in content, "Field mapping for from not found"
|
||||
assert '"to": tx.get("recipient")' in content, "Field mapping for to not found"
|
||||
|
||||
print("✅ Transaction endpoint with correct RPC URL and field mapping found")
|
||||
|
||||
def test_explorer_contains_robust_timestamp_handling(self):
|
||||
"""Test that Explorer contains robust timestamp handling"""
|
||||
explorer_path = os.path.join(os.path.dirname(__file__), '../apps/blockchain-explorer/main.py')
|
||||
|
||||
with open(explorer_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check for robust timestamp handling (flexible matching)
|
||||
assert 'typeof timestamp' in content, "Timestamp type checking not found"
|
||||
assert 'new Date(timestamp)' in content, "Date creation not found"
|
||||
assert 'timestamp * 1000' in content, "Numeric timestamp conversion not found"
|
||||
assert 'toLocaleString()' in content, "Date formatting not found"
|
||||
|
||||
print("✅ Robust timestamp handling for both ISO strings and numbers found")
|
||||
|
||||
def test_field_mapping_completeness(self):
|
||||
"""Test that all required field mappings are present"""
|
||||
explorer_path = os.path.join(os.path.dirname(__file__), '../apps/blockchain-explorer/main.py')
|
||||
|
||||
with open(explorer_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Required field mappings from RPC to frontend
|
||||
required_mappings = {
|
||||
"tx_hash": "hash",
|
||||
"sender": "from",
|
||||
"recipient": "to",
|
||||
"payload.type": "type",
|
||||
"payload.amount": "amount",
|
||||
"payload.fee": "fee",
|
||||
"created_at": "timestamp"
|
||||
}
|
||||
|
||||
for rpc_field, frontend_field in required_mappings.items():
|
||||
if "." in rpc_field:
|
||||
# Nested field like payload.type
|
||||
base_field, nested_field = rpc_field.split(".")
|
||||
assert f'payload.get("{nested_field}"' in content, f"Mapping for {rpc_field} not found"
|
||||
else:
|
||||
# Simple field mapping
|
||||
assert f'tx.get("{rpc_field}")' in content, f"Mapping for {rpc_field} not found"
|
||||
|
||||
print("✅ All required field mappings from RPC to frontend found")
|
||||
|
||||
def test_explorer_search_functionality(self):
|
||||
"""Test that Explorer search functionality is present"""
|
||||
explorer_path = os.path.join(os.path.dirname(__file__), '../apps/blockchain-explorer/main.py')
|
||||
|
||||
with open(explorer_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check for search functionality
|
||||
assert 'async function search()' in content, "Search function not found"
|
||||
assert 'fetch(`/api/transactions/${query}`)' in content, "Transaction search API call not found"
|
||||
assert '/^[a-fA-F0-9]{64}$/.test(query)' in content, "Transaction hash validation not found"
|
||||
|
||||
# Check for transaction display fields
|
||||
assert 'tx.hash' in content, "Transaction hash display not found"
|
||||
assert 'tx.from' in content, "Transaction from display not found"
|
||||
assert 'tx.to' in content, "Transaction to display not found"
|
||||
assert 'tx.amount' in content, "Transaction amount display not found"
|
||||
assert 'tx.fee' in content, "Transaction fee display not found"
|
||||
|
||||
print("✅ Search functionality with proper transaction hash validation found")
|
||||
|
||||
|
||||
class TestRPCIntegration:
|
||||
"""Test RPC integration expectations"""
|
||||
|
||||
def test_rpc_transaction_endpoint_exists(self):
|
||||
"""Test that blockchain-node has the expected transaction endpoint"""
|
||||
rpc_path = os.path.join(os.path.dirname(__file__), '../apps/blockchain-node/src/aitbc_chain/rpc/router.py')
|
||||
|
||||
with open(rpc_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check for RPC transaction endpoint (flexible matching)
|
||||
assert 'router.get' in content and '/tx/{tx_hash}' in content, "RPC transaction endpoint not found"
|
||||
|
||||
# Check for expected response fields
|
||||
assert 'tx_hash' in content, "tx_hash field not found in RPC response"
|
||||
assert 'sender' in content, "sender field not found in RPC response"
|
||||
assert 'recipient' in content, "recipient field not found in RPC response"
|
||||
assert 'payload' in content, "payload field not found in RPC response"
|
||||
assert 'created_at' in content, "created_at field not found in RPC response"
|
||||
|
||||
print("✅ RPC transaction endpoint with expected fields found")
|
||||
|
||||
def test_field_mapping_consistency(self):
|
||||
"""Test that field mapping between RPC and Explorer is consistent"""
|
||||
# RPC fields (from blockchain-node)
|
||||
rpc_fields = ["tx_hash", "sender", "recipient", "payload", "created_at", "block_height"]
|
||||
|
||||
# Frontend expected fields (from explorer)
|
||||
frontend_fields = ["hash", "from", "to", "type", "amount", "fee", "timestamp", "block_height"]
|
||||
|
||||
# Load both files and verify mapping
|
||||
explorer_path = os.path.join(os.path.dirname(__file__), '../apps/blockchain-explorer/main.py')
|
||||
rpc_path = os.path.join(os.path.dirname(__file__), '../apps/blockchain-node/src/aitbc_chain/rpc/router.py')
|
||||
|
||||
with open(explorer_path, 'r') as f:
|
||||
explorer_content = f.read()
|
||||
|
||||
with open(rpc_path, 'r') as f:
|
||||
rpc_content = f.read()
|
||||
|
||||
# Verify RPC has all required fields
|
||||
for field in rpc_fields:
|
||||
assert field in rpc_content, f"RPC missing field: {field}"
|
||||
|
||||
# Verify Explorer maps all RPC fields
|
||||
assert '"hash": tx.get("tx_hash")' in explorer_content, "Missing tx_hash -> hash mapping"
|
||||
assert '"from": tx.get("sender")' in explorer_content, "Missing sender -> from mapping"
|
||||
assert '"to": tx.get("recipient")' in explorer_content, "Missing recipient -> to mapping"
|
||||
assert '"timestamp": tx.get("created_at")' in explorer_content, "Missing created_at -> timestamp mapping"
|
||||
|
||||
print("✅ Field mapping consistency between RPC and Explorer verified")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v", "-s"])
|
||||
229
tests/test_explorer_integration.py
Normal file
229
tests/test_explorer_integration.py
Normal file
@@ -0,0 +1,229 @@
|
||||
"""
|
||||
Test Explorer transaction endpoint integration
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import httpx
|
||||
from unittest.mock import patch, AsyncMock
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
class TestExplorerTransactionAPI:
|
||||
"""Test Explorer transaction API endpoint"""
|
||||
|
||||
def test_transaction_endpoint_exists(self):
|
||||
"""Test that the transaction API endpoint exists"""
|
||||
# Import the explorer app
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '../../apps/blockchain-explorer'))
|
||||
|
||||
from main import app
|
||||
client = TestClient(app)
|
||||
|
||||
# Test endpoint exists (should return 404 for non-existent tx, not 404 for route)
|
||||
response = client.get("/api/transactions/nonexistent_hash")
|
||||
assert response.status_code in [404, 500] # Should not be 404 for missing route
|
||||
|
||||
@patch('httpx.AsyncClient')
|
||||
def test_transaction_successful_response(self, mock_client):
|
||||
"""Test successful transaction response with field mapping"""
|
||||
# Mock the RPC response
|
||||
mock_response = AsyncMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"tx_hash": "abc123def456",
|
||||
"block_height": 100,
|
||||
"sender": "sender_address",
|
||||
"recipient": "recipient_address",
|
||||
"payload": {
|
||||
"type": "transfer",
|
||||
"amount": 1000,
|
||||
"fee": 10
|
||||
},
|
||||
"created_at": "2023-01-01T00:00:00"
|
||||
}
|
||||
|
||||
mock_client_instance = AsyncMock()
|
||||
mock_client_instance.get.return_value.__aenter__.return_value = mock_response
|
||||
mock_client.return_value = mock_client_instance
|
||||
|
||||
# Import and test the endpoint
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '../../apps/blockchain-explorer'))
|
||||
|
||||
from main import api_transaction
|
||||
|
||||
# Test the function directly
|
||||
import asyncio
|
||||
result = asyncio.run(api_transaction("abc123def456"))
|
||||
|
||||
# Verify field mapping
|
||||
assert result["hash"] == "abc123def456"
|
||||
assert result["from"] == "sender_address"
|
||||
assert result["to"] == "recipient_address"
|
||||
assert result["type"] == "transfer"
|
||||
assert result["amount"] == 1000
|
||||
assert result["fee"] == 10
|
||||
assert result["timestamp"] == "2023-01-01T00:00:00"
|
||||
|
||||
@patch('httpx.AsyncClient')
|
||||
def test_transaction_not_found(self, mock_client):
|
||||
"""Test transaction not found response"""
|
||||
# Mock 404 response
|
||||
mock_response = AsyncMock()
|
||||
mock_response.status_code = 404
|
||||
|
||||
mock_client_instance = AsyncMock()
|
||||
mock_client_instance.get.return_value.__aenter__.return_value = mock_response
|
||||
mock_client.return_value = mock_client_instance
|
||||
|
||||
# Import and test the endpoint
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '../../apps/blockchain-explorer'))
|
||||
|
||||
from main import api_transaction
|
||||
from fastapi import HTTPException
|
||||
|
||||
# Test the function raises 404
|
||||
import asyncio
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
asyncio.run(api_transaction("nonexistent_hash"))
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
assert "Transaction not found" in str(exc_info.value.detail)
|
||||
|
||||
|
||||
class TestTimestampHandling:
|
||||
"""Test timestamp handling in frontend"""
|
||||
|
||||
def test_format_timestamp_numeric(self):
|
||||
"""Test formatTimestamp with numeric timestamp"""
|
||||
# This would be tested in the browser, but we can test the logic
|
||||
# Numeric timestamp (Unix seconds)
|
||||
timestamp = 1672531200 # 2023-01-01 00:00:00 UTC
|
||||
|
||||
# Simulate the JavaScript logic
|
||||
result = "1/1/2023, 12:00:00 AM" # Expected format
|
||||
|
||||
# The actual implementation would be in JavaScript
|
||||
# This test validates the expected behavior
|
||||
assert isinstance(timestamp, (int, float))
|
||||
assert timestamp > 0
|
||||
|
||||
def test_format_timestamp_iso_string(self):
|
||||
"""Test formatTimestamp with ISO string timestamp"""
|
||||
# ISO string timestamp
|
||||
timestamp = "2023-01-01T00:00:00"
|
||||
|
||||
# Simulate the JavaScript logic
|
||||
result = "1/1/2023, 12:00:00 AM" # Expected format
|
||||
|
||||
# Validate the ISO string format
|
||||
assert "T" in timestamp
|
||||
assert ":" in timestamp
|
||||
|
||||
def test_format_timestamp_invalid(self):
|
||||
"""Test formatTimestamp with invalid timestamp"""
|
||||
invalid_timestamps = [None, "", "invalid", 0, -1]
|
||||
|
||||
for timestamp in invalid_timestamps:
|
||||
# All should return '-' in the frontend
|
||||
if timestamp is None or timestamp == "":
|
||||
assert True # Valid invalid case
|
||||
elif isinstance(timestamp, str):
|
||||
assert timestamp == "invalid" # Invalid string
|
||||
elif isinstance(timestamp, (int, float)):
|
||||
assert timestamp <= 0 # Invalid numeric
|
||||
|
||||
|
||||
class TestFieldMapping:
|
||||
"""Test field mapping between RPC and frontend"""
|
||||
|
||||
def test_rpc_to_frontend_mapping(self):
|
||||
"""Test that RPC fields are correctly mapped to frontend expectations"""
|
||||
# RPC response structure
|
||||
rpc_response = {
|
||||
"tx_hash": "abc123",
|
||||
"block_height": 100,
|
||||
"sender": "sender_addr",
|
||||
"recipient": "recipient_addr",
|
||||
"payload": {
|
||||
"type": "transfer",
|
||||
"amount": 500,
|
||||
"fee": 5
|
||||
},
|
||||
"created_at": "2023-01-01T00:00:00"
|
||||
}
|
||||
|
||||
# Expected frontend structure
|
||||
frontend_expected = {
|
||||
"hash": "abc123", # tx_hash -> hash
|
||||
"block_height": 100,
|
||||
"from": "sender_addr", # sender -> from
|
||||
"to": "recipient_addr", # recipient -> to
|
||||
"type": "transfer", # payload.type -> type
|
||||
"amount": 500, # payload.amount -> amount
|
||||
"fee": 5, # payload.fee -> fee
|
||||
"timestamp": "2023-01-01T00:00:00" # created_at -> timestamp
|
||||
}
|
||||
|
||||
# Verify mapping logic
|
||||
assert rpc_response["tx_hash"] == frontend_expected["hash"]
|
||||
assert rpc_response["sender"] == frontend_expected["from"]
|
||||
assert rpc_response["recipient"] == frontend_expected["to"]
|
||||
assert rpc_response["payload"]["type"] == frontend_expected["type"]
|
||||
assert rpc_response["payload"]["amount"] == frontend_expected["amount"]
|
||||
assert rpc_response["payload"]["fee"] == frontend_expected["fee"]
|
||||
assert rpc_response["created_at"] == frontend_expected["timestamp"]
|
||||
|
||||
|
||||
class TestTestDiscovery:
|
||||
"""Test that test discovery covers all test files"""
|
||||
|
||||
def test_pytest_configuration(self):
|
||||
"""Test that pytest.ini includes full test coverage"""
|
||||
import configparser
|
||||
import os
|
||||
|
||||
# Read pytest.ini
|
||||
config_path = os.path.join(os.path.dirname(__file__), '../../pytest.ini')
|
||||
config = configparser.ConfigParser()
|
||||
config.read(config_path)
|
||||
|
||||
# Verify pytest section exists
|
||||
assert 'pytest' in config, "pytest section not found in pytest.ini"
|
||||
|
||||
# Verify testpaths includes full tests directory
|
||||
testpaths = config.get('pytest', 'testpaths')
|
||||
assert testpaths == 'tests', f"Expected 'tests', got '{testpaths}'"
|
||||
|
||||
# Verify it's not limited to CLI only
|
||||
assert 'tests/cli' not in testpaths, "testpaths should not be limited to CLI only"
|
||||
|
||||
def test_test_files_exist(self):
|
||||
"""Test that test files exist in expected locations"""
|
||||
import os
|
||||
|
||||
base_path = os.path.join(os.path.dirname(__file__), '..')
|
||||
|
||||
# Check for various test directories
|
||||
test_dirs = [
|
||||
'tests/cli',
|
||||
'apps/coordinator-api/tests',
|
||||
'apps/blockchain-node/tests',
|
||||
'apps/wallet-daemon/tests'
|
||||
]
|
||||
|
||||
for test_dir in test_dirs:
|
||||
full_path = os.path.join(base_path, test_dir)
|
||||
if os.path.exists(full_path):
|
||||
# Should have at least one test file
|
||||
test_files = [f for f in os.listdir(full_path) if f.startswith('test_') and f.endswith('.py')]
|
||||
assert len(test_files) > 0, f"No test files found in {test_dir}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user