feat: add transaction hash search to blockchain explorer and cleanup settlement storage
Blockchain Explorer: - Add transaction hash search support (64-char hex pattern validation) - Fetch and display transaction details in modal (hash, type, from/to, amount, fee, block) - Fix regex escape sequence in block height validation - Update search placeholder text to mention both search types - Add blank lines between function definitions for PEP 8 compliance Settlement Storage: - Add timedelta import for future
This commit is contained in:
@@ -1,393 +0,0 @@
|
||||
"""
|
||||
End-to-end tests for real user scenarios
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
import time
|
||||
from datetime import datetime
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
class TestUserOnboarding:
|
||||
"""Test complete user onboarding flow"""
|
||||
|
||||
def test_new_user_registration_and_first_job(self, browser, base_url):
|
||||
"""Test new user registering and creating their first job"""
|
||||
# 1. Navigate to application
|
||||
browser.get(f"{base_url}/")
|
||||
|
||||
# 2. Click register button
|
||||
register_btn = browser.find_element(By.ID, "register-btn")
|
||||
register_btn.click()
|
||||
|
||||
# 3. Fill registration form
|
||||
browser.find_element(By.ID, "email").send_keys("test@example.com")
|
||||
browser.find_element(By.ID, "password").send_keys("SecurePass123!")
|
||||
browser.find_element(By.ID, "confirm-password").send_keys("SecurePass123!")
|
||||
browser.find_element(By.ID, "organization").send_keys("Test Org")
|
||||
|
||||
# 4. Submit registration
|
||||
browser.find_element(By.ID, "submit-register").click()
|
||||
|
||||
# 5. Verify email confirmation page
|
||||
WebDriverWait(browser, 10).until(
|
||||
EC.presence_of_element_located((By.ID, "confirmation-message"))
|
||||
)
|
||||
assert "Check your email" in browser.page_source
|
||||
|
||||
# 6. Simulate email confirmation (via API)
|
||||
# In real test, would parse email and click confirmation link
|
||||
|
||||
# 7. Login after confirmation
|
||||
browser.get(f"{base_url}/login")
|
||||
browser.find_element(By.ID, "email").send_keys("test@example.com")
|
||||
browser.find_element(By.ID, "password").send_keys("SecurePass123!")
|
||||
browser.find_element(By.ID, "login-btn").click()
|
||||
|
||||
# 8. Verify dashboard
|
||||
WebDriverWait(browser, 10).until(
|
||||
EC.presence_of_element_located((By.ID, "dashboard"))
|
||||
)
|
||||
assert "Welcome" in browser.page_source
|
||||
|
||||
# 9. Create first job
|
||||
browser.find_element(By.ID, "create-job-btn").click()
|
||||
browser.find_element(By.ID, "job-type").send_keys("AI Inference")
|
||||
browser.find_element(By.ID, "model-select").send_keys("GPT-4")
|
||||
browser.find_element(By.ID, "prompt-input").send_keys("Write a poem about AI")
|
||||
|
||||
# 10. Submit job
|
||||
browser.find_element(By.ID, "submit-job").click()
|
||||
|
||||
# 11. Verify job created
|
||||
WebDriverWait(browser, 10).until(
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "job-card"))
|
||||
)
|
||||
assert "AI Inference" in browser.page_source
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
class TestMinerWorkflow:
|
||||
"""Test miner registration and job execution"""
|
||||
|
||||
def test_miner_setup_and_job_execution(self, browser, base_url):
|
||||
"""Test miner setting up and executing jobs"""
|
||||
# 1. Navigate to miner portal
|
||||
browser.get(f"{base_url}/miner")
|
||||
|
||||
# 2. Register as miner
|
||||
browser.find_element(By.ID, "miner-register").click()
|
||||
browser.find_element(By.ID, "miner-id").send_keys("miner-test-123")
|
||||
browser.find_element(By.ID, "endpoint").send_keys("http://localhost:9000")
|
||||
browser.find_element(By.ID, "gpu-memory").send_keys("16")
|
||||
browser.find_element(By.ID, "cpu-cores").send_keys("8")
|
||||
|
||||
# Select capabilities
|
||||
browser.find_element(By.ID, "cap-ai").click()
|
||||
browser.find_element(By.ID, "cap-image").click()
|
||||
|
||||
browser.find_element(By.ID, "submit-miner").click()
|
||||
|
||||
# 3. Verify miner registered
|
||||
WebDriverWait(browser, 10).until(
|
||||
EC.presence_of_element_located((By.ID, "miner-dashboard"))
|
||||
)
|
||||
assert "Miner Dashboard" in browser.page_source
|
||||
|
||||
# 4. Start miner daemon (simulated)
|
||||
browser.find_element(By.ID, "start-miner").click()
|
||||
|
||||
# 5. Wait for job assignment
|
||||
time.sleep(2) # Simulate waiting
|
||||
|
||||
# 6. Accept job
|
||||
WebDriverWait(browser, 10).until(
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "job-assignment"))
|
||||
)
|
||||
browser.find_element(By.ID, "accept-job").click()
|
||||
|
||||
# 7. Execute job (simulated)
|
||||
browser.find_element(By.ID, "execute-job").click()
|
||||
|
||||
# 8. Submit results
|
||||
browser.find_element(By.ID, "result-input").send_keys("Generated poem about AI...")
|
||||
browser.find_element(By.ID, "submit-result").click()
|
||||
|
||||
# 9. Verify job completed
|
||||
WebDriverWait(browser, 10).until(
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "completion-status"))
|
||||
)
|
||||
assert "Completed" in browser.page_source
|
||||
|
||||
# 10. Check earnings
|
||||
browser.find_element(By.ID, "earnings-tab").click()
|
||||
assert browser.find_element(By.ID, "total-earnings").text != "0"
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
class TestWalletOperations:
|
||||
"""Test wallet creation and operations"""
|
||||
|
||||
def test_wallet_creation_and_transactions(self, browser, base_url):
|
||||
"""Test creating wallet and performing transactions"""
|
||||
# 1. Login and navigate to wallet
|
||||
browser.get(f"{base_url}/login")
|
||||
browser.find_element(By.ID, "email").send_keys("wallet@example.com")
|
||||
browser.find_element(By.ID, "password").send_keys("WalletPass123!")
|
||||
browser.find_element(By.ID, "login-btn").click()
|
||||
|
||||
# 2. Go to wallet section
|
||||
browser.find_element(By.ID, "wallet-link").click()
|
||||
|
||||
# 3. Create new wallet
|
||||
browser.find_element(By.ID, "create-wallet").click()
|
||||
browser.find_element(By.ID, "wallet-name").send_keys("My Test Wallet")
|
||||
browser.find_element(By.ID, "create-wallet-btn").click()
|
||||
|
||||
# 4. Secure wallet (backup phrase)
|
||||
WebDriverWait(browser, 10).until(
|
||||
EC.presence_of_element_located((By.ID, "backup-phrase"))
|
||||
)
|
||||
phrase = browser.find_element(By.ID, "backup-phrase").text
|
||||
assert len(phrase.split()) == 12 # 12-word mnemonic
|
||||
|
||||
# 5. Confirm backup
|
||||
browser.find_element(By.ID, "confirm-backup").click()
|
||||
|
||||
# 6. View wallet address
|
||||
address = browser.find_element(By.ID, "wallet-address").text
|
||||
assert address.startswith("0x")
|
||||
|
||||
# 7. Fund wallet (testnet faucet)
|
||||
browser.find_element(By.ID, "fund-wallet").click()
|
||||
browser.find_element(By.ID, "request-funds").click()
|
||||
|
||||
# 8. Wait for funding
|
||||
time.sleep(3)
|
||||
|
||||
# 9. Check balance
|
||||
balance = browser.find_element(By.ID, "wallet-balance").text
|
||||
assert float(balance) > 0
|
||||
|
||||
# 10. Send transaction
|
||||
browser.find_element(By.ID, "send-btn").click()
|
||||
browser.find_element(By.ID, "recipient").send_keys("0x1234567890abcdef")
|
||||
browser.find_element(By.ID, "amount").send_keys("1.0")
|
||||
browser.find_element(By.ID, "send-tx").click()
|
||||
|
||||
# 11. Confirm transaction
|
||||
browser.find_element(By.ID, "confirm-send").click()
|
||||
|
||||
# 12. Verify transaction sent
|
||||
WebDriverWait(browser, 10).until(
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "tx-success"))
|
||||
)
|
||||
assert "Transaction sent" in browser.page_source
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
class TestMarketplaceInteraction:
|
||||
"""Test marketplace interactions"""
|
||||
|
||||
def test_service_provider_workflow(self, browser, base_url):
|
||||
"""Test service provider listing and managing services"""
|
||||
# 1. Login as provider
|
||||
browser.get(f"{base_url}/login")
|
||||
browser.find_element(By.ID, "email").send_keys("provider@example.com")
|
||||
browser.find_element(By.ID, "password").send_keys("ProviderPass123!")
|
||||
browser.find_element(By.ID, "login-btn").click()
|
||||
|
||||
# 2. Go to marketplace
|
||||
browser.find_element(By.ID, "marketplace-link").click()
|
||||
|
||||
# 3. List new service
|
||||
browser.find_element(By.ID, "list-service").click()
|
||||
browser.find_element(By.ID, "service-name").send_keys("Premium AI Inference")
|
||||
browser.find_element(By.ID, "service-desc").send_keys("High-performance AI inference with GPU acceleration")
|
||||
|
||||
# Set pricing
|
||||
browser.find_element(By.ID, "price-per-token").send_keys("0.0001")
|
||||
browser.find_element(By.ID, "price-per-minute").send_keys("0.05")
|
||||
|
||||
# Set capabilities
|
||||
browser.find_element(By.ID, "capability-text").click()
|
||||
browser.find_element(By.ID, "capability-image").click()
|
||||
browser.find_element(By.ID, "capability-video").click()
|
||||
|
||||
browser.find_element(By.ID, "submit-service").click()
|
||||
|
||||
# 4. Verify service listed
|
||||
WebDriverWait(browser, 10).until(
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "service-card"))
|
||||
)
|
||||
assert "Premium AI Inference" in browser.page_source
|
||||
|
||||
# 5. Receive booking notification
|
||||
time.sleep(2) # Simulate booking
|
||||
|
||||
# 6. View bookings
|
||||
browser.find_element(By.ID, "bookings-tab").click()
|
||||
bookings = browser.find_elements(By.CLASS_NAME, "booking-item")
|
||||
assert len(bookings) > 0
|
||||
|
||||
# 7. Accept booking
|
||||
browser.find_element(By.ID, "accept-booking").click()
|
||||
|
||||
# 8. Mark as completed
|
||||
browser.find_element(By.ID, "complete-booking").click()
|
||||
browser.find_element(By.ID, "completion-notes").send_keys("Job completed successfully")
|
||||
browser.find_element(By.ID, "submit-completion").click()
|
||||
|
||||
# 9. Receive payment
|
||||
WebDriverWait(browser, 10).until(
|
||||
EC.presence_of_element_located((By.ID, "payment-received"))
|
||||
)
|
||||
assert "Payment received" in browser.page_source
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
class TestMultiTenantScenario:
|
||||
"""Test multi-tenant scenarios"""
|
||||
|
||||
def test_tenant_isolation(self, browser, base_url):
|
||||
"""Test that tenant data is properly isolated"""
|
||||
# 1. Login as Tenant A
|
||||
browser.get(f"{base_url}/login")
|
||||
browser.find_element(By.ID, "email").send_keys("tenant-a@example.com")
|
||||
browser.find_element(By.ID, "password").send_keys("TenantAPass123!")
|
||||
browser.find_element(By.ID, "login-btn").click()
|
||||
|
||||
# 2. Create jobs for Tenant A
|
||||
for i in range(3):
|
||||
browser.find_element(By.ID, "create-job").click()
|
||||
browser.find_element(By.ID, "job-name").send_keys(f"Tenant A Job {i}")
|
||||
browser.find_element(By.ID, "submit-job").click()
|
||||
time.sleep(0.5)
|
||||
|
||||
# 3. Verify Tenant A sees only their jobs
|
||||
jobs = browser.find_elements(By.CLASS_NAME, "job-item")
|
||||
assert len(jobs) == 3
|
||||
for job in jobs:
|
||||
assert "Tenant A Job" in job.text
|
||||
|
||||
# 4. Logout
|
||||
browser.find_element(By.ID, "logout").click()
|
||||
|
||||
# 5. Login as Tenant B
|
||||
browser.find_element(By.ID, "email").send_keys("tenant-b@example.com")
|
||||
browser.find_element(By.ID, "password").send_keys("TenantBPass123!")
|
||||
browser.find_element(By.ID, "login-btn").click()
|
||||
|
||||
# 6. Verify Tenant B cannot see Tenant A's jobs
|
||||
jobs = browser.find_elements(By.CLASS_NAME, "job-item")
|
||||
assert len(jobs) == 0
|
||||
|
||||
# 7. Create job for Tenant B
|
||||
browser.find_element(By.ID, "create-job").click()
|
||||
browser.find_element(By.ID, "job-name").send_keys("Tenant B Job")
|
||||
browser.find_element(By.ID, "submit-job").click()
|
||||
|
||||
# 8. Verify Tenant B sees only their job
|
||||
jobs = browser.find_elements(By.CLASS_NAME, "job-item")
|
||||
assert len(jobs) == 1
|
||||
assert "Tenant B Job" in jobs[0].text
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
class TestErrorHandling:
|
||||
"""Test error handling in user flows"""
|
||||
|
||||
def test_network_error_handling(self, browser, base_url):
|
||||
"""Test handling of network errors"""
|
||||
# 1. Start a job
|
||||
browser.get(f"{base_url}/login")
|
||||
browser.find_element(By.ID, "email").send_keys("user@example.com")
|
||||
browser.find_element(By.ID, "password").send_keys("UserPass123!")
|
||||
browser.find_element(By.ID, "login-btn").click()
|
||||
|
||||
browser.find_element(By.ID, "create-job").click()
|
||||
browser.find_element(By.ID, "job-name").send_keys("Test Job")
|
||||
browser.find_element(By.ID, "submit-job").click()
|
||||
|
||||
# 2. Simulate network error (disconnect network)
|
||||
# In real test, would use network simulation tool
|
||||
|
||||
# 3. Try to update job
|
||||
browser.find_element(By.ID, "edit-job").click()
|
||||
browser.find_element(By.ID, "job-name").clear()
|
||||
browser.find_element(By.ID, "job-name").send_keys("Updated Job")
|
||||
browser.find_element(By.ID, "save-job").click()
|
||||
|
||||
# 4. Verify error message
|
||||
WebDriverWait(browser, 10).until(
|
||||
EC.presence_of_element_located((By.ID, "error-message"))
|
||||
)
|
||||
assert "Network error" in browser.page_source
|
||||
|
||||
# 5. Verify retry option
|
||||
assert browser.find_element(By.ID, "retry-btn").is_displayed()
|
||||
|
||||
# 6. Retry after network restored
|
||||
browser.find_element(By.ID, "retry-btn").click()
|
||||
|
||||
# 7. Verify success
|
||||
WebDriverWait(browser, 10).until(
|
||||
EC.presence_of_element_located((By.ID, "success-message"))
|
||||
)
|
||||
assert "Updated successfully" in browser.page_source
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
class TestMobileResponsiveness:
|
||||
"""Test mobile responsiveness"""
|
||||
|
||||
def test_mobile_workflow(self, mobile_browser, base_url):
|
||||
"""Test complete workflow on mobile device"""
|
||||
# 1. Open on mobile
|
||||
mobile_browser.get(f"{base_url}")
|
||||
|
||||
# 2. Verify mobile layout
|
||||
assert mobile_browser.find_element(By.ID, "mobile-menu").is_displayed()
|
||||
|
||||
# 3. Navigate using mobile menu
|
||||
mobile_browser.find_element(By.ID, "mobile-menu").click()
|
||||
mobile_browser.find_element(By.ID, "mobile-jobs").click()
|
||||
|
||||
# 4. Create job on mobile
|
||||
mobile_browser.find_element(By.ID, "mobile-create-job").click()
|
||||
mobile_browser.find_element(By.ID, "job-type-mobile").send_keys("AI Inference")
|
||||
mobile_browser.find_element(By.ID, "prompt-mobile").send_keys("Mobile test prompt")
|
||||
mobile_browser.find_element(By.ID, "submit-mobile").click()
|
||||
|
||||
# 5. Verify job created
|
||||
WebDriverWait(mobile_browser, 10).until(
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "mobile-job-card"))
|
||||
)
|
||||
|
||||
# 6. Check mobile wallet
|
||||
mobile_browser.find_element(By.ID, "mobile-menu").click()
|
||||
mobile_browser.find_element(By.ID, "mobile-wallet").click()
|
||||
|
||||
# 7. Verify wallet balance displayed
|
||||
assert mobile_browser.find_element(By.ID, "mobile-balance").is_displayed()
|
||||
|
||||
# 8. Send payment on mobile
|
||||
mobile_browser.find_element(By.ID, "mobile-send").click()
|
||||
mobile_browser.find_element(By.ID, "recipient-mobile").send_keys("0x123456")
|
||||
mobile_browser.find_element(By.ID, "amount-mobile").send_keys("1.0")
|
||||
mobile_browser.find_element(By.ID, "send-mobile").click()
|
||||
|
||||
# 9. Confirm with mobile PIN
|
||||
mobile_browser.find_element(By.ID, "pin-1").click()
|
||||
mobile_browser.find_element(By.ID, "pin-2").click()
|
||||
mobile_browser.find_element(By.ID, "pin-3").click()
|
||||
mobile_browser.find_element(By.ID, "pin-4").click()
|
||||
|
||||
# 10. Verify success
|
||||
WebDriverWait(mobile_browser, 10).until(
|
||||
EC.presence_of_element_located((By.ID, "mobile-success"))
|
||||
)
|
||||
@@ -1,625 +0,0 @@
|
||||
"""
|
||||
End-to-end tests for AITBC Wallet Daemon
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import requests
|
||||
from cryptography.hazmat.primitives.asymmetric import ed25519
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
|
||||
from packages.py.aitbc_crypto import sign_receipt, verify_receipt
|
||||
from packages.py.aitbc_sdk import AITBCClient
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
class TestWalletDaemonE2E:
|
||||
"""End-to-end tests for wallet daemon functionality"""
|
||||
|
||||
@pytest.fixture
|
||||
def wallet_base_url(self):
|
||||
"""Wallet daemon base URL"""
|
||||
return "http://localhost:8002"
|
||||
|
||||
@pytest.fixture
|
||||
def coordinator_base_url(self):
|
||||
"""Coordinator API base URL"""
|
||||
return "http://localhost:8001"
|
||||
|
||||
@pytest.fixture
|
||||
def test_wallet_data(self, temp_directory):
|
||||
"""Create test wallet data"""
|
||||
wallet_path = Path(temp_directory) / "test_wallet.json"
|
||||
wallet_data = {
|
||||
"id": "test-wallet-123",
|
||||
"name": "Test Wallet",
|
||||
"created_at": datetime.utcnow().isoformat(),
|
||||
"accounts": [
|
||||
{
|
||||
"address": "0x1234567890abcdef",
|
||||
"public_key": "test-public-key",
|
||||
"encrypted_private_key": "encrypted-key-here",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
with open(wallet_path, "w") as f:
|
||||
json.dump(wallet_data, f)
|
||||
|
||||
return wallet_path
|
||||
|
||||
def test_wallet_creation_flow(self, wallet_base_url, temp_directory):
|
||||
"""Test complete wallet creation flow"""
|
||||
# Step 1: Create new wallet
|
||||
create_data = {
|
||||
"name": "E2E Test Wallet",
|
||||
"password": "test-password-123",
|
||||
"keystore_path": str(temp_directory),
|
||||
}
|
||||
|
||||
response = requests.post(f"{wallet_base_url}/v1/wallets", json=create_data)
|
||||
assert response.status_code == 201
|
||||
|
||||
wallet = response.json()
|
||||
assert wallet["name"] == "E2E Test Wallet"
|
||||
assert "id" in wallet
|
||||
assert "accounts" in wallet
|
||||
assert len(wallet["accounts"]) == 1
|
||||
|
||||
account = wallet["accounts"][0]
|
||||
assert "address" in account
|
||||
assert "public_key" in account
|
||||
assert "encrypted_private_key" not in account # Should not be exposed
|
||||
|
||||
# Step 2: List wallets
|
||||
response = requests.get(f"{wallet_base_url}/v1/wallets")
|
||||
assert response.status_code == 200
|
||||
|
||||
wallets = response.json()
|
||||
assert any(w["id"] == wallet["id"] for w in wallets)
|
||||
|
||||
# Step 3: Get wallet details
|
||||
response = requests.get(f"{wallet_base_url}/v1/wallets/{wallet['id']}")
|
||||
assert response.status_code == 200
|
||||
|
||||
wallet_details = response.json()
|
||||
assert wallet_details["id"] == wallet["id"]
|
||||
assert len(wallet_details["accounts"]) == 1
|
||||
|
||||
def test_wallet_unlock_flow(self, wallet_base_url, test_wallet_data):
|
||||
"""Test wallet unlock and session management"""
|
||||
# Step 1: Unlock wallet
|
||||
unlock_data = {
|
||||
"password": "test-password-123",
|
||||
"keystore_path": str(test_wallet_data),
|
||||
}
|
||||
|
||||
response = requests.post(f"{wallet_base_url}/v1/wallets/unlock", json=unlock_data)
|
||||
assert response.status_code == 200
|
||||
|
||||
unlock_result = response.json()
|
||||
assert "session_token" in unlock_result
|
||||
assert "expires_at" in unlock_result
|
||||
|
||||
session_token = unlock_result["session_token"]
|
||||
|
||||
# Step 2: Use session for signing
|
||||
headers = {"Authorization": f"Bearer {session_token}"}
|
||||
|
||||
sign_data = {
|
||||
"message": "Test message to sign",
|
||||
"account_address": "0x1234567890abcdef",
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f"{wallet_base_url}/v1/sign",
|
||||
json=sign_data,
|
||||
headers=headers
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
signature = response.json()
|
||||
assert "signature" in signature
|
||||
assert "public_key" in signature
|
||||
|
||||
# Step 3: Lock wallet
|
||||
response = requests.post(
|
||||
f"{wallet_base_url}/v1/wallets/lock",
|
||||
headers=headers
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Step 4: Verify session is invalid
|
||||
response = requests.post(
|
||||
f"{wallet_base_url}/v1/sign",
|
||||
json=sign_data,
|
||||
headers=headers
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_receipt_verification_flow(self, wallet_base_url, coordinator_base_url, signed_receipt):
|
||||
"""Test receipt verification workflow"""
|
||||
# Step 1: Submit receipt to wallet for verification
|
||||
verify_data = {
|
||||
"receipt": signed_receipt,
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f"{wallet_base_url}/v1/receipts/verify",
|
||||
json=verify_data
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
verification = response.json()
|
||||
assert "valid" in verification
|
||||
assert verification["valid"] is True
|
||||
assert "verifications" in verification
|
||||
|
||||
# Check verification details
|
||||
verifications = verification["verifications"]
|
||||
assert "miner_signature" in verifications
|
||||
assert "coordinator_signature" in verifications
|
||||
assert verifications["miner_signature"]["valid"] is True
|
||||
assert verifications["coordinator_signature"]["valid"] is True
|
||||
|
||||
# Step 2: Get receipt history
|
||||
response = requests.get(f"{wallet_base_url}/v1/receipts")
|
||||
assert response.status_code == 200
|
||||
|
||||
receipts = response.json()
|
||||
assert len(receipts) > 0
|
||||
assert any(r["id"] == signed_receipt["id"] for r in receipts)
|
||||
|
||||
def test_cross_component_integration(self, wallet_base_url, coordinator_base_url):
|
||||
"""Test integration between wallet and coordinator"""
|
||||
# Step 1: Create job via coordinator
|
||||
job_data = {
|
||||
"job_type": "ai_inference",
|
||||
"parameters": {
|
||||
"model": "gpt-3.5-turbo",
|
||||
"prompt": "Test prompt",
|
||||
},
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f"{coordinator_base_url}/v1/jobs",
|
||||
json=job_data,
|
||||
headers={"X-Tenant-ID": "test-tenant"}
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
job = response.json()
|
||||
job_id = job["id"]
|
||||
|
||||
# Step 2: Mock job completion and receipt creation
|
||||
# In real test, this would involve actual miner execution
|
||||
receipt_data = {
|
||||
"id": f"receipt-{job_id}",
|
||||
"job_id": job_id,
|
||||
"miner_id": "test-miner",
|
||||
"coordinator_id": "test-coordinator",
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"result": {"output": "Test result"},
|
||||
}
|
||||
|
||||
# Sign receipt
|
||||
private_key = ed25519.Ed25519PrivateKey.generate()
|
||||
receipt_json = json.dumps({k: v for k, v in receipt_data.items() if k != "signature"})
|
||||
signature = private_key.sign(receipt_json.encode())
|
||||
receipt_data["signature"] = signature.hex()
|
||||
|
||||
# Step 3: Submit receipt to coordinator
|
||||
response = requests.post(
|
||||
f"{coordinator_base_url}/v1/receipts",
|
||||
json=receipt_data
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
# Step 4: Fetch and verify receipt via wallet
|
||||
response = requests.get(
|
||||
f"{wallet_base_url}/v1/receipts/{receipt_data['id']}"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
fetched_receipt = response.json()
|
||||
assert fetched_receipt["id"] == receipt_data["id"]
|
||||
assert fetched_receipt["job_id"] == job_id
|
||||
|
||||
def test_error_handling_flows(self, wallet_base_url):
|
||||
"""Test error handling in various scenarios"""
|
||||
# Test invalid password
|
||||
unlock_data = {
|
||||
"password": "wrong-password",
|
||||
"keystore_path": "/nonexistent/path",
|
||||
}
|
||||
|
||||
response = requests.post(f"{wallet_base_url}/v1/wallets/unlock", json=unlock_data)
|
||||
assert response.status_code == 400
|
||||
assert "error" in response.json()
|
||||
|
||||
# Test invalid session token
|
||||
headers = {"Authorization": "Bearer invalid-token"}
|
||||
|
||||
sign_data = {
|
||||
"message": "Test",
|
||||
"account_address": "0x123",
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f"{wallet_base_url}/v1/sign",
|
||||
json=sign_data,
|
||||
headers=headers
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
# Test invalid receipt format
|
||||
response = requests.post(
|
||||
f"{wallet_base_url}/v1/receipts/verify",
|
||||
json={"receipt": {"invalid": "data"}}
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_concurrent_operations(self, wallet_base_url, test_wallet_data):
|
||||
"""Test concurrent wallet operations"""
|
||||
import threading
|
||||
import queue
|
||||
|
||||
# Unlock wallet first
|
||||
unlock_data = {
|
||||
"password": "test-password-123",
|
||||
"keystore_path": str(test_wallet_data),
|
||||
}
|
||||
|
||||
response = requests.post(f"{wallet_base_url}/v1/wallets/unlock", json=unlock_data)
|
||||
session_token = response.json()["session_token"]
|
||||
headers = {"Authorization": f"Bearer {session_token}"}
|
||||
|
||||
# Concurrent signing operations
|
||||
results = queue.Queue()
|
||||
|
||||
def sign_message(message_id):
|
||||
sign_data = {
|
||||
"message": f"Test message {message_id}",
|
||||
"account_address": "0x1234567890abcdef",
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f"{wallet_base_url}/v1/sign",
|
||||
json=sign_data,
|
||||
headers=headers
|
||||
)
|
||||
results.put((message_id, response.status_code, response.json()))
|
||||
|
||||
# Start 10 concurrent signing operations
|
||||
threads = []
|
||||
for i in range(10):
|
||||
thread = threading.Thread(target=sign_message, args=(i,))
|
||||
threads.append(thread)
|
||||
thread.start()
|
||||
|
||||
# Wait for all threads to complete
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
|
||||
# Verify all operations succeeded
|
||||
success_count = 0
|
||||
while not results.empty():
|
||||
msg_id, status, result = results.get()
|
||||
assert status == 200, f"Message {msg_id} failed"
|
||||
success_count += 1
|
||||
|
||||
assert success_count == 10
|
||||
|
||||
def test_performance_limits(self, wallet_base_url, test_wallet_data):
|
||||
"""Test performance limits and rate limiting"""
|
||||
# Unlock wallet
|
||||
unlock_data = {
|
||||
"password": "test-password-123",
|
||||
"keystore_path": str(test_wallet_data),
|
||||
}
|
||||
|
||||
response = requests.post(f"{wallet_base_url}/v1/wallets/unlock", json=unlock_data)
|
||||
session_token = response.json()["session_token"]
|
||||
headers = {"Authorization": f"Bearer {session_token}"}
|
||||
|
||||
# Test rapid signing requests
|
||||
start_time = time.time()
|
||||
success_count = 0
|
||||
|
||||
for i in range(100):
|
||||
sign_data = {
|
||||
"message": f"Performance test {i}",
|
||||
"account_address": "0x1234567890abcdef",
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f"{wallet_base_url}/v1/sign",
|
||||
json=sign_data,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
success_count += 1
|
||||
elif response.status_code == 429:
|
||||
# Rate limited
|
||||
break
|
||||
|
||||
elapsed_time = time.time() - start_time
|
||||
|
||||
# Should handle at least 50 requests per second
|
||||
assert success_count > 50
|
||||
assert success_count / elapsed_time > 50
|
||||
|
||||
def test_wallet_backup_and_restore(self, wallet_base_url, temp_directory):
|
||||
"""Test wallet backup and restore functionality"""
|
||||
# Step 1: Create wallet with multiple accounts
|
||||
create_data = {
|
||||
"name": "Backup Test Wallet",
|
||||
"password": "backup-password-123",
|
||||
"keystore_path": str(temp_directory),
|
||||
}
|
||||
|
||||
response = requests.post(f"{wallet_base_url}/v1/wallets", json=create_data)
|
||||
wallet = response.json()
|
||||
|
||||
# Add additional account
|
||||
unlock_data = {
|
||||
"password": "backup-password-123",
|
||||
"keystore_path": str(temp_directory),
|
||||
}
|
||||
|
||||
response = requests.post(f"{wallet_base_url}/v1/wallets/unlock", json=unlock_data)
|
||||
session_token = response.json()["session_token"]
|
||||
headers = {"Authorization": f"Bearer {session_token}"}
|
||||
|
||||
response = requests.post(
|
||||
f"{wallet_base_url}/v1/accounts",
|
||||
headers=headers
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
# Step 2: Create backup
|
||||
backup_path = Path(temp_directory) / "wallet_backup.json"
|
||||
|
||||
response = requests.post(
|
||||
f"{wallet_base_url}/v1/wallets/{wallet['id']}/backup",
|
||||
json={"backup_path": str(backup_path)},
|
||||
headers=headers
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Verify backup exists
|
||||
assert backup_path.exists()
|
||||
|
||||
# Step 3: Restore wallet to new location
|
||||
restore_dir = Path(temp_directory) / "restored"
|
||||
restore_dir.mkdir()
|
||||
|
||||
response = requests.post(
|
||||
f"{wallet_base_url}/v1/wallets/restore",
|
||||
json={
|
||||
"backup_path": str(backup_path),
|
||||
"restore_path": str(restore_dir),
|
||||
"new_password": "restored-password-456",
|
||||
}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
restored_wallet = response.json()
|
||||
assert len(restored_wallet["accounts"]) == 2
|
||||
|
||||
# Step 4: Verify restored wallet works
|
||||
unlock_data = {
|
||||
"password": "restored-password-456",
|
||||
"keystore_path": str(restore_dir),
|
||||
}
|
||||
|
||||
response = requests.post(f"{wallet_base_url}/v1/wallets/unlock", json=unlock_data)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
class TestWalletSecurityE2E:
|
||||
"""End-to-end security tests for wallet daemon"""
|
||||
|
||||
def test_session_security(self, wallet_base_url, test_wallet_data):
|
||||
"""Test session token security"""
|
||||
# Unlock wallet to get session
|
||||
unlock_data = {
|
||||
"password": "test-password-123",
|
||||
"keystore_path": str(test_wallet_data),
|
||||
}
|
||||
|
||||
response = requests.post(f"{wallet_base_url}/v1/wallets/unlock", json=unlock_data)
|
||||
session_token = response.json()["session_token"]
|
||||
|
||||
# Test session expiration
|
||||
# In real test, this would wait for actual expiration
|
||||
# For now, test invalid token format
|
||||
invalid_tokens = [
|
||||
"",
|
||||
"invalid",
|
||||
"Bearer invalid",
|
||||
"Bearer ",
|
||||
"Bearer " + "A" * 1000, # Too long
|
||||
]
|
||||
|
||||
for token in invalid_tokens:
|
||||
headers = {"Authorization": token}
|
||||
response = requests.get(f"{wallet_base_url}/v1/wallets", headers=headers)
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_input_validation(self, wallet_base_url):
|
||||
"""Test input validation and sanitization"""
|
||||
# Test malicious inputs
|
||||
malicious_inputs = [
|
||||
{"name": "<script>alert('xss')</script>"},
|
||||
{"password": "../../etc/passwd"},
|
||||
{"keystore_path": "/etc/shadow"},
|
||||
{"message": "\x00\x01\x02\x03"},
|
||||
{"account_address": "invalid-address"},
|
||||
]
|
||||
|
||||
for malicious_input in malicious_inputs:
|
||||
response = requests.post(
|
||||
f"{wallet_base_url}/v1/wallets",
|
||||
json=malicious_input
|
||||
)
|
||||
# Should either reject or sanitize
|
||||
assert response.status_code in [400, 422]
|
||||
|
||||
def test_rate_limiting(self, wallet_base_url):
|
||||
"""Test rate limiting on sensitive operations"""
|
||||
# Test unlock rate limiting
|
||||
unlock_data = {
|
||||
"password": "test",
|
||||
"keystore_path": "/nonexistent",
|
||||
}
|
||||
|
||||
# Send rapid requests
|
||||
rate_limited = False
|
||||
for i in range(100):
|
||||
response = requests.post(f"{wallet_base_url}/v1/wallets/unlock", json=unlock_data)
|
||||
if response.status_code == 429:
|
||||
rate_limited = True
|
||||
break
|
||||
|
||||
assert rate_limited, "Rate limiting should be triggered"
|
||||
|
||||
def test_encryption_strength(self, wallet_base_url, temp_directory):
|
||||
"""Test wallet encryption strength"""
|
||||
# Create wallet with strong password
|
||||
create_data = {
|
||||
"name": "Security Test Wallet",
|
||||
"password": "VeryStr0ngP@ssw0rd!2024#SpecialChars",
|
||||
"keystore_path": str(temp_directory),
|
||||
}
|
||||
|
||||
response = requests.post(f"{wallet_base_url}/v1/wallets", json=create_data)
|
||||
assert response.status_code == 201
|
||||
|
||||
# Verify keystore file is encrypted
|
||||
keystore_path = Path(temp_directory) / "security-test-wallet.json"
|
||||
assert keystore_path.exists()
|
||||
|
||||
with open(keystore_path, "r") as f:
|
||||
keystore_data = json.load(f)
|
||||
|
||||
# Check that private keys are encrypted
|
||||
for account in keystore_data.get("accounts", []):
|
||||
assert "encrypted_private_key" in account
|
||||
encrypted_key = account["encrypted_private_key"]
|
||||
# Should not contain plaintext key material
|
||||
assert "BEGIN PRIVATE KEY" not in encrypted_key
|
||||
assert "-----END" not in encrypted_key
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
@pytest.mark.slow
|
||||
class TestWalletPerformanceE2E:
|
||||
"""Performance tests for wallet daemon"""
|
||||
|
||||
def test_large_wallet_performance(self, wallet_base_url, temp_directory):
|
||||
"""Test performance with large number of accounts"""
|
||||
# Create wallet
|
||||
create_data = {
|
||||
"name": "Large Wallet Test",
|
||||
"password": "test-password-123",
|
||||
"keystore_path": str(temp_directory),
|
||||
}
|
||||
|
||||
response = requests.post(f"{wallet_base_url}/v1/wallets", json=create_data)
|
||||
wallet = response.json()
|
||||
|
||||
# Unlock wallet
|
||||
unlock_data = {
|
||||
"password": "test-password-123",
|
||||
"keystore_path": str(temp_directory),
|
||||
}
|
||||
|
||||
response = requests.post(f"{wallet_base_url}/v1/wallets/unlock", json=unlock_data)
|
||||
session_token = response.json()["session_token"]
|
||||
headers = {"Authorization": f"Bearer {session_token}"}
|
||||
|
||||
# Create 100 accounts
|
||||
start_time = time.time()
|
||||
|
||||
for i in range(100):
|
||||
response = requests.post(
|
||||
f"{wallet_base_url}/v1/accounts",
|
||||
headers=headers
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
creation_time = time.time() - start_time
|
||||
|
||||
# Should create accounts quickly
|
||||
assert creation_time < 10.0, f"Account creation too slow: {creation_time}s"
|
||||
|
||||
# Test listing performance
|
||||
start_time = time.time()
|
||||
|
||||
response = requests.get(
|
||||
f"{wallet_base_url}/v1/wallets/{wallet['id']}",
|
||||
headers=headers
|
||||
)
|
||||
|
||||
listing_time = time.time() - start_time
|
||||
assert response.status_code == 200
|
||||
|
||||
wallet_data = response.json()
|
||||
assert len(wallet_data["accounts"]) == 101
|
||||
assert listing_time < 1.0, f"Wallet listing too slow: {listing_time}s"
|
||||
|
||||
def test_concurrent_wallet_operations(self, wallet_base_url, temp_directory):
|
||||
"""Test concurrent operations on multiple wallets"""
|
||||
import concurrent.futures
|
||||
|
||||
def create_and_use_wallet(wallet_id):
|
||||
wallet_dir = Path(temp_directory) / f"wallet_{wallet_id}"
|
||||
wallet_dir.mkdir()
|
||||
|
||||
# Create wallet
|
||||
create_data = {
|
||||
"name": f"Concurrent Wallet {wallet_id}",
|
||||
"password": f"password-{wallet_id}",
|
||||
"keystore_path": str(wallet_dir),
|
||||
}
|
||||
|
||||
response = requests.post(f"{wallet_base_url}/v1/wallets", json=create_data)
|
||||
assert response.status_code == 201
|
||||
|
||||
# Unlock and sign
|
||||
unlock_data = {
|
||||
"password": f"password-{wallet_id}",
|
||||
"keystore_path": str(wallet_dir),
|
||||
}
|
||||
|
||||
response = requests.post(f"{wallet_base_url}/v1/wallets/unlock", json=unlock_data)
|
||||
session_token = response.json()["session_token"]
|
||||
headers = {"Authorization": f"Bearer {session_token}"}
|
||||
|
||||
sign_data = {
|
||||
"message": f"Message from wallet {wallet_id}",
|
||||
"account_address": "0x1234567890abcdef",
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f"{wallet_base_url}/v1/sign",
|
||||
json=sign_data,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
return response.status_code == 200
|
||||
|
||||
# Run 20 concurrent wallet operations
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
|
||||
futures = [executor.submit(create_and_use_wallet, i) for i in range(20)]
|
||||
results = [future.result() for future in concurrent.futures.as_completed(futures)]
|
||||
|
||||
# All operations should succeed
|
||||
assert all(results), "Some concurrent wallet operations failed"
|
||||
Reference in New Issue
Block a user