Add authentication to dispute endpoints and improve test coverage infrastructure
Some checks failed
Blockchain Synchronization Verification / sync-verification (push) Has been cancelled
CLI Tests / test-cli (push) Has been cancelled
Contract Performance Benchmarks / benchmark-gas-usage (push) Has been cancelled
Contract Performance Benchmarks / benchmark-execution-time (push) Has been cancelled
Contract Performance Benchmarks / benchmark-throughput (push) Has been cancelled
Coverage Phase 1 (70% Target) / test-coverage-70 (push) Has been cancelled
Coverage Phase 2 (85% Target) / test-coverage-85 (push) Has been cancelled
Cross-Chain Functionality Tests / test-cross-chain-sync (push) Has been cancelled
Cross-Chain Functionality Tests / test-cross-chain-transactions (push) Has been cancelled
Cross-Chain Functionality Tests / test-multi-chain-consensus (push) Has been cancelled
Deploy to Testnet / deploy-testnet (push) Has been cancelled
Documentation Validation / validate-docs (push) Has been cancelled
Documentation Validation / validate-policies-strict (push) Has been cancelled
Integration Tests / test-service-integration (push) Has been cancelled
Multi-Chain Island Architecture Tests / test-multi-chain-island (push) Has been cancelled
Multi-Node Blockchain Health Monitoring / health-check (push) Has been cancelled
Node Failover Simulation / failover-test (push) Has been cancelled
P2P Network Verification / p2p-verification (push) Has been cancelled
Package Tests / Python package - aitbc-agent-sdk (push) Has been cancelled
Package Tests / Python package - aitbc-core (push) Has been cancelled
Package Tests / Python package - aitbc-crypto (push) Has been cancelled
Package Tests / Python package - aitbc-sdk (push) Has been cancelled
Package Tests / JavaScript package - aitbc-sdk-js (push) Has been cancelled
Package Tests / JavaScript package - aitbc-token (push) Has been cancelled
Python Tests / test-python (push) Has been cancelled
Security Scanning / security-scan (push) Has been cancelled
Smart Contract Tests / test-solidity (map[name:aitbc-contracts path:contracts]) (push) Has been cancelled
Smart Contract Tests / test-solidity (map[name:aitbc-token path:packages/solidity/aitbc-token]) (push) Has been cancelled
Smart Contract Tests / test-foundry (push) Has been cancelled
Smart Contract Tests / lint-solidity (push) Has been cancelled
Smart Contract Tests / deploy-contracts (push) Has been cancelled
Staking Tests / test-staking-service (push) Has been cancelled
Contract Performance Benchmarks / compare-benchmarks (push) Has been cancelled
Cross-Chain Functionality Tests / aggregate-results (push) Has been cancelled
Staking Tests / test-staking-integration (push) Has been cancelled
Staking Tests / test-staking-contract (push) Has been cancelled
Staking Tests / run-staking-test-runner (push) Has been cancelled
Multi-Node Stress Testing / stress-test (push) Successful in 3s
Cross-Node Transaction Testing / transaction-test (push) Successful in 3s

- Add get_authenticated_address() helper to extract wallet address from X-Wallet-Address header or JWT token
- Add authentication to dispute filing, evidence submission, verification, voting, and arbitrator authorization endpoints
- Replace hardcoded zero addresses with authenticated addresses from request headers
- Add DEV_MODE fallback for development without authentication
- Add --mock flag to experimental resource
This commit is contained in:
aitbc
2026-05-22 23:13:47 +02:00
parent a6868e5836
commit 494bd962b4
12 changed files with 581 additions and 27 deletions

View File

@@ -0,0 +1,98 @@
name: Coverage Phase 1 (70% Target)
on:
push:
branches: [main, develop]
paths:
- 'apps/**/*.py'
- 'packages/py/**'
- 'tests/**'
- 'pyproject.toml'
- 'requirements.txt'
- '.gitea/workflows/coverage-phase-1.yml'
pull_request:
branches: [main, develop]
workflow_dispatch:
concurrency:
group: coverage-phase-1-${{ github.ref }}
cancel-in-progress: true
jobs:
test-coverage-70:
runs-on: debian
timeout-minutes: 20
env:
WORKSPACE: /var/lib/aitbc-workspaces/coverage-phase-1
steps:
- name: Clone repository
run: |
rm -rf "${{ env.WORKSPACE }}"
mkdir -p "${{ env.WORKSPACE }}"
cd "${{ env.WORKSPACE }}"
git clone --depth 1 http://gitea.bubuit.net:3000/oib/aitbc.git repo
- name: Initialize job logging
run: |
cd "${{ env.WORKSPACE }}/repo"
bash scripts/ci/setup-job-logging.sh
- name: Setup Python environment
run: |
cd "${{ env.WORKSPACE }}/repo"
rm -rf venv
mkdir -p /var/lib/aitbc/data /var/lib/aitbc/keystore /etc/aitbc /var/log/aitbc
bash scripts/ci/setup-python-venv.sh \
--repo-dir "$PWD" \
--venv-dir "$PWD/venv" \
--skip-requirements \
--mode copy \
--extra-packages "pytest pytest-cov pytest-mock pytest-timeout pytest-asyncio locust pydantic-settings fastapi uvicorn aiohttp>=3.12.14 sqlmodel>=0.0.38 PyJWT"
- name: Install packages
run: |
cd "${{ env.WORKSPACE }}/repo"
venv/bin/python -m pip install -e packages/py/aitbc-crypto/
venv/bin/python -m pip install -e packages/py/aitbc-sdk/
venv/bin/python -m pip install -e packages/py/aitbc-agent-sdk/
- name: Run tests with 70% coverage gate
run: |
cd "${{ env.WORKSPACE }}/repo"
export PYTHONPATH="$PWD/apps/coordinator-api/src:$PWD/apps/blockchain-node/src:$PWD/apps/wallet/src:$PWD/packages/py/aitbc-crypto/src:$PWD/packages/py/aitbc-sdk/src:$PWD/packages/py/aitbc-agent-sdk/src:$PWD:$PYTHONPATH"
venv/bin/python -m pytest tests/ \
-c /dev/null --rootdir "$PWD" --import-mode=importlib \
--tb=short -q --timeout=30 \
-o asyncio_mode=auto \
--cov=apps --cov=packages --cov=cli \
--cov-report=term-missing --cov-report=html \
--cov-report=json:coverage.json \
--cov-fail-under=70
- name: Upload coverage artifact
run: |
cd "${{ env.WORKSPACE }}/repo"
mkdir -p coverage-artifacts
cp coverage.json coverage-artifacts/
cp htmlcov/index.html coverage-artifacts/coverage-summary.html
echo "Coverage report uploaded to artifacts"
- name: Count TODOs
run: |
cd "${{ env.WORKSPACE }}/repo"
TODO_COUNT=$(rg -c "TODO|FIXME" apps/ packages/py/ cli/ --type py 2>/dev/null | awk -F: '{sum+=$2} END {print sum+0}')
echo "TODO_COUNT=${TODO_COUNT}" >> $GITHUB_ENV
echo "Found ${TODO_COUNT} TODO/FIXME comments"
- name: Publish coverage metrics
run: |
cd "${{ env.WORKSPACE }}/repo"
COVERAGE=$(python -c "import json; print(json.load(open('coverage.json')).get('totals', {}).get('percent_covered', 0))")
echo "Coverage: ${COVERAGE}%"
echo "TODOs: ${TODO_COUNT}"

View File

@@ -0,0 +1,98 @@
name: Coverage Phase 2 (85% Target)
on:
push:
branches: [main, develop]
paths:
- 'apps/**/*.py'
- 'packages/py/**'
- 'tests/**'
- 'pyproject.toml'
- 'requirements.txt'
- '.gitea/workflows/coverage-phase-2.yml'
pull_request:
branches: [main, develop]
workflow_dispatch:
concurrency:
group: coverage-phase-2-${{ github.ref }}
cancel-in-progress: true
jobs:
test-coverage-85:
runs-on: debian
timeout-minutes: 25
env:
WORKSPACE: /var/lib/aitbc-workspaces/coverage-phase-2
steps:
- name: Clone repository
run: |
rm -rf "${{ env.WORKSPACE }}"
mkdir -p "${{ env.WORKSPACE }}"
cd "${{ env.WORKSPACE }}"
git clone --depth 1 http://gitea.bubuit.net:3000/oib/aitbc.git repo
- name: Initialize job logging
run: |
cd "${{ env.WORKSPACE }}/repo"
bash scripts/ci/setup-job-logging.sh
- name: Setup Python environment
run: |
cd "${{ env.WORKSPACE }}/repo"
rm -rf venv
mkdir -p /var/lib/aitbc/data /var/lib/aitbc/keystore /etc/aitbc /var/log/aitbc
bash scripts/ci/setup-python-venv.sh \
--repo-dir "$PWD" \
--venv-dir "$PWD/venv" \
--skip-requirements \
--mode copy \
--extra-packages "pytest pytest-cov pytest-mock pytest-timeout pytest-asyncio locust pydantic-settings fastapi uvicorn aiohttp>=3.12.14 sqlmodel>=0.0.38 PyJWT"
- name: Install packages
run: |
cd "${{ env.WORKSPACE }}/repo"
venv/bin/python -m pip install -e packages/py/aitbc-crypto/
venv/bin/python -m pip install -e packages/py/aitbc-sdk/
venv/bin/python -m pip install -e packages/py/aitbc-agent-sdk/
- name: Run tests with 85% coverage gate
run: |
cd "${{ env.WORKSPACE }}/repo"
export PYTHONPATH="$PWD/apps/coordinator-api/src:$PWD/apps/blockchain-node/src:$PWD/apps/wallet/src:$PWD/packages/py/aitbc-crypto/src:$PWD/packages/py/aitbc-sdk/src:$PWD/packages/py/aitbc-agent-sdk/src:$PWD:$PYTHONPATH"
venv/bin/python -m pytest tests/ \
-c /dev/null --rootdir "$PWD" --import-mode=importlib \
--tb=short -q --timeout=30 \
-o asyncio_mode=auto \
--cov=apps --cov=packages --cov=cli \
--cov-report=term-missing --cov-report=html \
--cov-report=json:coverage.json \
--cov-fail-under=85
- name: Upload coverage artifact
run: |
cd "${{ env.WORKSPACE }}/repo"
mkdir -p coverage-artifacts
cp coverage.json coverage-artifacts/
cp htmlcov/index.html coverage-artifacts/coverage-summary.html
echo "Coverage report uploaded to artifacts"
- name: Count TODOs
run: |
cd "${{ env.WORKSPACE }}/repo"
TODO_COUNT=$(rg -c "TODO|FIXME" apps/ packages/py/ cli/ --type py 2>/dev/null | awk -F: '{sum+=$2} END {print sum+0}')
echo "TODO_COUNT=${TODO_COUNT}" >> $GITHUB_ENV
echo "Found ${TODO_COUNT} TODO/FIXME comments"
- name: Publish coverage metrics
run: |
cd "${{ env.WORKSPACE }}/repo"
COVERAGE=$(python -c "import json; print(json.load(open('coverage.json')).get('totals', {}).get('percent_covered', 0))")
echo "Coverage: ${COVERAGE}%"
echo "TODOs: ${TODO_COUNT}"

View File

@@ -84,7 +84,9 @@ jobs:
-c /dev/null --rootdir "$PWD" --import-mode=importlib \
--tb=short -q --timeout=30 \
-o asyncio_mode=auto \
--cov=apps --cov=packages --cov=cli --cov-append
--cov=apps --cov=packages --cov=cli --cov-append \
--cov-report=term-missing --cov-report=html \
--cov-fail-under=50
- name: Run app and package tests with coverage
run: |

View File

@@ -32,7 +32,7 @@
### CLI & Tools
- **Unified CLI** with 50+ command groups
- **100% test coverage** for CLI commands
- **Test coverage** for CLI commands (Current: 50%, Target: 85%)
- **Modular handler architecture** for extensibility
- **Bridge commands** for blockchain event bridging
- **Account management** commands
@@ -45,9 +45,10 @@
- **Encrypted keystores** for secure key management
### Testing & CI/CD
- **Comprehensive test suite** with 100% success rate
- **Comprehensive test suite** with 50% minimum coverage (Target: 85%)
- **Standardized venv caching** with corruption detection
- **Automated CI/CD** with Gitea workflows
- **Phased quality gates** (50% → 70% → 85%+)
- **Security scanning** optimized for changed files
- **Cross-node verification tests**

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
import asyncio
import hashlib
import json
import os
import re
import time
import uuid
@@ -10,6 +11,7 @@ from typing import Any, Dict, Optional, List
from datetime import datetime, timezone, timedelta
from fastapi import APIRouter, HTTPException, status, Request
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel, Field, model_validator
from sqlmodel import select, delete
@@ -29,6 +31,63 @@ from aitbc.rate_limiting import rate_limit
_logger = get_logger(__name__)
# Security scheme for authentication
security = HTTPBearer(auto_error=False)
def get_authenticated_address(request: Request, credentials: Optional[HTTPAuthorizationCredentials] = None) -> str:
"""
Extract authenticated wallet address from request headers or JWT token.
Priority order:
1. X-Wallet-Address header (for API key auth)
2. JWT Bearer token (if provided)
3. Development mode fallback (if DEV_MODE=true)
Returns:
str: The authenticated wallet address
Raises:
HTTPException: If authentication fails and not in development mode
"""
# Check for X-Wallet-Address header (API key authentication)
wallet_address = request.headers.get("X-Wallet-Address")
if wallet_address:
# Validate address format (basic check)
if not wallet_address.startswith("0x") or len(wallet_address) != 42:
_logger.warning(f"Invalid wallet address format in X-Wallet-Address header: {wallet_address}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid wallet address format"
)
_logger.debug(f"Authenticated via X-Wallet-Address header: {wallet_address}")
return wallet_address
# Check for JWT Bearer token
if credentials and credentials.scheme == "Bearer":
# In a full implementation, this would validate the JWT token
# For now, we'll extract a wallet address from the token if present
# This is a placeholder for proper JWT validation
token = credentials.credentials
_logger.debug(f"JWT token provided (validation not yet implemented)")
# TODO: Implement proper JWT validation and address extraction
# For now, raise an error to require proper implementation
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="JWT authentication not yet implemented. Use X-Wallet-Address header."
)
# Development mode fallback
if os.getenv("DEV_MODE", "false").lower() == "true":
_logger.warning("Using development mode fallback for authentication - returning zero address")
return "0x0000000000000000000000000000000000000000"
# No valid authentication found
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authentication required. Provide X-Wallet-Address header or valid JWT token.",
headers={"WWW-Authenticate": "Bearer"}
)
router = APIRouter()
# Global rate limiter for importBlock
@@ -1554,12 +1613,19 @@ class GetArbitrationVotesResponse(BaseModel):
@router.post("/disputes/file", summary="File a new dispute")
async def file_dispute(request: FileDisputeRequest) -> FileDisputeResponse:
async def file_dispute(
request: FileDisputeRequest,
http_request: Request,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)
) -> FileDisputeResponse:
"""
File a new dispute for a marketplace transaction.
This interacts with the DisputeResolution smart contract.
"""
try:
# Get authenticated address from request
sender_address = get_authenticated_address(http_request, credentials)
# Use dispute resolution service
result = dispute_resolution_service.file_dispute(
agreement_id=request.agreement_id,
@@ -1567,7 +1633,7 @@ async def file_dispute(request: FileDisputeRequest) -> FileDisputeResponse:
dispute_type=request.dispute_type,
reason=request.reason,
evidence_hash=request.evidence_hash,
sender_address="0x0000000000000000000000000000000000000000" # TODO: Get from auth
sender_address=sender_address
)
if not result.get("success"):
@@ -1587,17 +1653,24 @@ async def file_dispute(request: FileDisputeRequest) -> FileDisputeResponse:
@router.post("/disputes/evidence", summary="Submit evidence for a dispute")
async def submit_evidence(request: SubmitEvidenceRequest) -> SubmitEvidenceResponse:
async def submit_evidence(
request: SubmitEvidenceRequest,
http_request: Request,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)
) -> SubmitEvidenceResponse:
"""
Submit evidence for a dispute.
This interacts with the DisputeResolution smart contract.
"""
try:
# Get authenticated address from request
submitter_address = get_authenticated_address(http_request, credentials)
result = dispute_resolution_service.submit_evidence(
dispute_id=request.dispute_id,
evidence_type=request.evidence_type,
evidence_data=request.evidence_data,
submitter_address="0x0000000000000000000000000000000000000000" # TODO: Get from auth
submitter_address=submitter_address
)
if not result.get("success"):
@@ -1617,18 +1690,25 @@ async def submit_evidence(request: SubmitEvidenceRequest) -> SubmitEvidenceRespo
@router.post("/disputes/verify-evidence", summary="Verify evidence (arbitrator only)")
async def verify_evidence(request: VerifyEvidenceRequest) -> VerifyEvidenceResponse:
async def verify_evidence(
request: VerifyEvidenceRequest,
http_request: Request,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)
) -> VerifyEvidenceResponse:
"""
Verify evidence submitted in a dispute.
This can only be called by authorized arbitrators.
"""
try:
# Get authenticated address from request
arbitrator_address = get_authenticated_address(http_request, credentials)
result = dispute_resolution_service.verify_evidence(
dispute_id=request.dispute_id,
evidence_id=request.evidence_id,
is_valid=request.is_valid,
verification_score=request.verification_score,
arbitrator_address="0x0000000000000000000000000000000000000000" # TODO: Get from auth
arbitrator_address=arbitrator_address
)
if not result.get("success"):
@@ -1647,13 +1727,23 @@ async def verify_evidence(request: VerifyEvidenceRequest) -> VerifyEvidenceRespo
@router.post("/disputes/vote", summary="Submit arbitration vote (arbitrator only)")
async def submit_arbitration_vote(request: SubmitArbitrationVoteRequest) -> SubmitArbitrationVoteResponse:
async def submit_arbitration_vote(
request: SubmitArbitrationVoteRequest,
http_request: Request,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)
) -> SubmitArbitrationVoteResponse:
"""
Submit an arbitration vote for a dispute.
This can only be called by authorized arbitrators assigned to the dispute.
"""
try:
# Get authenticated address from request
arbitrator_address = get_authenticated_address(http_request, credentials)
# TODO: Implement actual smart contract interaction with arbitrator authorization check
# For now, validate that we have a real address (not zero address unless in dev mode)
if arbitrator_address == "0x0000000000000000000000000000000000000000":
_logger.warning("Vote submission attempted with zero address - may be in dev mode")
return SubmitArbitrationVoteResponse(
success=True,
@@ -1666,16 +1756,23 @@ async def submit_arbitration_vote(request: SubmitArbitrationVoteRequest) -> Subm
@router.post("/disputes/arbitrators/authorize", summary="Authorize an arbitrator (admin only)")
async def authorize_arbitrator(request: AuthorizeArbitratorRequest) -> AuthorizeArbitratorResponse:
async def authorize_arbitrator(
request: AuthorizeArbitratorRequest,
http_request: Request,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)
) -> AuthorizeArbitratorResponse:
"""
Authorize a new arbitrator.
This can only be called by the contract owner.
"""
try:
# Get authenticated address from request
owner_address = get_authenticated_address(http_request, credentials)
result = dispute_resolution_service.authorize_arbitrator(
arbitrator_address=request.arbitrator,
reputation_score=request.reputation_score,
owner_address="0x0000000000000000000000000000000000000000" # TODO: Get from auth
owner_address=owner_address
)
if not result.get("success"):

View File

@@ -13,7 +13,7 @@ from ..utils import error, success
@click.group()
def resource():
"""Resource management commands"""
"""Resource management commands (EXPERIMENTAL - use --mock for testing)"""
pass
@@ -21,20 +21,33 @@ def resource():
@click.option('--resource-type', required=True, help='Type of resource (gpu, cpu, storage)')
@click.option('--quantity', type=int, required=True, help='Quantity of resources')
@click.option('--priority', type=click.Choice(['low', 'medium', 'high']), default='medium', help='Allocation priority')
def allocate(resource_type: str, quantity: int, priority: str):
"""Allocate resources"""
@click.option('--mock', is_flag=True, help='Use mock data for experimental command')
def allocate(resource_type: str, quantity: int, priority: str, mock: bool):
"""Allocate resources (EXPERIMENTAL)"""
if not mock:
error("[EXPERIMENTAL] This command uses placeholder logic. Use --mock for testing.")
click.echo("To proceed with mock data, run: aitbc resource allocate --mock")
return 1
success(f"Allocate {quantity} {resource_type} with {priority} priority")
# TODO: Implement actual resource allocation via coordinator API
click.echo(f"Allocation ID: alloc_{int(time.time())}")
click.echo(f"Status: Allocated")
click.echo(f"Cost per hour: 25 AIT")
return 0
@resource.command()
@click.option('--resource-id', help='Specific resource ID')
@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format')
def list(resource_id: Optional[str], format: str):
"""List allocated resources"""
@click.option('--mock', is_flag=True, help='Use mock data for experimental command')
def list(resource_id: Optional[str], format: str, mock: bool):
"""List allocated resources (EXPERIMENTAL)"""
if not mock:
error("[EXPERIMENTAL] This command uses placeholder logic. Use --mock for testing.")
click.echo("To proceed with mock data, run: aitbc resource list --mock")
return 1
success("Allocated resources:")
resources = [
{"type": "gpu", "allocated": 4, "available": 8, "efficiency": "78.5%"},
@@ -47,21 +60,35 @@ def list(resource_id: Optional[str], format: str):
else:
for res in resources:
click.echo(f" - {res['type'].upper()}: {res['allocated']} allocated, {res['available']} available ({res['efficiency']})")
return 0
@resource.command()
@click.argument('resource_id')
def release(resource_id: str):
"""Release allocated resources"""
@click.option('--mock', is_flag=True, help='Use mock data for experimental command')
def release(resource_id: str, mock: bool):
"""Release allocated resources (EXPERIMENTAL)"""
if not mock:
error("[EXPERIMENTAL] This command uses placeholder logic. Use --mock for testing.")
click.echo("To proceed with mock data, run: aitbc resource release <id> --mock")
return 1
success(f"Release resource {resource_id}")
# TODO: Implement actual resource release via coordinator API
click.echo("Status: Released")
return 0
@resource.command()
@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format')
def utilization(format: str):
"""Get resource utilization metrics"""
@click.option('--mock', is_flag=True, help='Use mock data for experimental command')
def utilization(format: str, mock: bool):
"""Get resource utilization metrics (EXPERIMENTAL)"""
if not mock:
error("[EXPERIMENTAL] This command uses placeholder logic. Use --mock for testing.")
click.echo("To proceed with mock data, run: aitbc resource utilization --mock")
return 1
success("Resource utilization:")
metrics = {
"cpu_utilization": "45.2%",
@@ -77,13 +104,20 @@ def utilization(format: str):
else:
for key, value in metrics.items():
click.echo(f" {key}: {value}")
return 0
@resource.command()
@click.option('--target', default='all', help='Optimization target (all, cpu, gpu, memory)')
@click.option('--agent-id', help='Specific agent ID')
def optimize(target: str, agent_id: Optional[str]):
"""Optimize resource allocation"""
@click.option('--mock', is_flag=True, help='Use mock data for experimental command')
def optimize(target: str, agent_id: Optional[str], mock: bool):
"""Optimize resource allocation (EXPERIMENTAL)"""
if not mock:
error("[EXPERIMENTAL] This command uses placeholder logic. Use --mock for testing.")
click.echo("To proceed with mock data, run: aitbc resource optimize --mock")
return 1
success(f"Optimize resources for target: {target}")
if agent_id:
click.echo(f"Agent: {agent_id}")
@@ -91,3 +125,4 @@ def optimize(target: str, agent_id: Optional[str]):
click.echo("Optimization score: 85.2%")
click.echo("Improvement: 12.5%")
click.echo("Status: Optimized")
return 0

View File

@@ -5,10 +5,14 @@ Node client for multi-chain operations
import asyncio
import httpx
import json
import os
import logging
from typing import Dict, List, Optional, Any
from core.config import NodeConfig
from models.chain import ChainInfo, ChainType, ChainStatus, ConsensusAlgorithm
logger = logging.getLogger(__name__)
class NodeClient:
"""Client for communicating with AITBC nodes"""
@@ -16,6 +20,8 @@ class NodeClient:
self.config = node_config
self._client: Optional[httpx.AsyncClient] = None
self._session_id: Optional[str] = None
self._mock_fallback_count = 0
self._dev_mocks_enabled = os.getenv("DEV_MOCKS_ENABLED", "false").lower() == "true"
async def __aenter__(self):
"""Async context manager entry"""
@@ -45,7 +51,11 @@ class NodeClient:
self._session_id = data.get("session_id")
except Exception as e:
# For development, we'll continue without authentication
pass # print(f"Warning: Could not authenticate with node {self.config.id}: {e}")
if self._dev_mocks_enabled:
logger.warning(f"[DEV_MODE] Authentication failed for node {self.config.id}: {e}")
else:
logger.error(f"Authentication failed for node {self.config.id}: {e}")
raise
async def get_node_info(self) -> Dict[str, Any]:
"""Get node information"""
@@ -57,7 +67,13 @@ class NodeClient:
raise Exception(f"Node info request failed: {response.status_code}")
except Exception as e:
# Return mock data for development
if self._dev_mocks_enabled:
self._mock_fallback_count += 1
logger.warning(f"[DEV_MODE] Using mock node info for {self.config.id} (fallback #{self._mock_fallback_count})")
return self._get_mock_node_info()
else:
logger.error(f"Failed to get node info for {self.config.id}: {e}")
raise
async def get_hosted_chains(self) -> List[ChainInfo]:
"""Get all chains hosted by this node"""

View File

@@ -6,7 +6,7 @@ authors = [
{name = "AITBC Team", email = "team@aitbc.dev"}
]
readme = "README.md"
requires-python = ">=3.13.5,<3.14"
requires-python = ">=3.11,<3.14"
dependencies = [
"cryptography>=46.0.0",
"pynacl>=1.5.0"

View File

@@ -7,7 +7,7 @@ authors = ["AITBC Team"]
[tool.poetry.dependencies]
python = ">=3.13,<3.14"
python = ">=3.11,<3.14"
# Core Web Framework
fastapi = ">=0.115.6"
uvicorn = {extras = ["standard"], version = ">=0.34.0"}

View File

@@ -0,0 +1,207 @@
"""
Negative authentication tests for dispute endpoints.
Tests for missing authentication, unauthorized access, and invalid tokens.
"""
import pytest
import os
from httpx import AsyncClient, ASGITransport
from fastapi import status
@pytest.mark.asyncio
class TestDisputeAuthentication:
"""Test authentication requirements for dispute endpoints"""
@pytest.fixture
async def client(self):
"""Create test client for blockchain node RPC"""
from apps.blockchain_node.src.aitbc_chain.rpc.router import router
from fastapi import FastAPI
app = FastAPI()
app.include_router(router, prefix="/rpc")
# Set DEV_MODE to false for production-like testing
original_dev_mode = os.getenv("DEV_MODE")
os.environ["DEV_MODE"] = "false"
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac
# Restore original DEV_MODE
if original_dev_mode is None:
os.environ.pop("DEV_MODE", None)
else:
os.environ["DEV_MODE"] = original_dev_mode
async def test_file_dispute_missing_auth(self, client):
"""Test that filing a dispute without authentication returns 401"""
response = await client.post(
"/rpc/disputes/file",
json={
"agreement_id": "test_agreement_1",
"respondent": "0x1234567890123456789012345678901234567890",
"dispute_type": "payment_dispute",
"reason": "Test dispute",
"evidence_hash": "0xabcdef"
}
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
assert "Authentication required" in response.json()["detail"]
async def test_file_dispute_with_invalid_wallet_address(self, client):
"""Test that filing a dispute with invalid wallet address format returns 401"""
response = await client.post(
"/rpc/disputes/file",
json={
"agreement_id": "test_agreement_1",
"respondent": "0x1234567890123456789012345678901234567890",
"dispute_type": "payment_dispute",
"reason": "Test dispute",
"evidence_hash": "0xabcdef"
},
headers={"X-Wallet-Address": "invalid_address_format"}
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
assert "Invalid wallet address format" in response.json()["detail"]
async def test_submit_evidence_missing_auth(self, client):
"""Test that submitting evidence without authentication returns 401"""
response = await client.post(
"/rpc/disputes/evidence",
json={
"dispute_id": 1,
"evidence_type": "transaction_proof",
"evidence_data": "test_evidence_data"
}
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
assert "Authentication required" in response.json()["detail"]
async def test_verify_evidence_missing_auth(self, client):
"""Test that verifying evidence without authentication returns 401"""
response = await client.post(
"/rpc/disputes/verify-evidence",
json={
"dispute_id": 1,
"evidence_id": 1,
"is_valid": True,
"verification_score": 95
}
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
assert "Authentication required" in response.json()["detail"]
async def test_authorize_arbitrator_missing_auth(self, client):
"""Test that authorizing an arbitrator without authentication returns 401"""
response = await client.post(
"/rpc/disputes/arbitrators/authorize",
json={
"arbitrator": "0x1234567890123456789012345678901234567890",
"reputation_score": 85
}
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
assert "Authentication required" in response.json()["detail"]
async def test_submit_vote_missing_auth(self, client):
"""Test that submitting a vote without authentication returns 401"""
response = await client.post(
"/rpc/disputes/vote",
json={
"dispute_id": 1,
"vote_in_favor_of_initiator": True,
"confidence": 90,
"reasoning": "Test reasoning"
}
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
assert "Authentication required" in response.json()["detail"]
async def test_file_dispute_with_valid_wallet_address(self, client):
"""Test that filing a dispute with valid wallet address header succeeds (or returns expected error)"""
response = await client.post(
"/rpc/disputes/file",
json={
"agreement_id": "test_agreement_1",
"respondent": "0x1234567890123456789012345678901234567890",
"dispute_type": "payment_dispute",
"reason": "Test dispute",
"evidence_hash": "0xabcdef"
},
headers={"X-Wallet-Address": "0x1234567890123456789012345678901234567890"}
)
# Should not be 401 (authentication passed)
# May be 500 if dispute service is not available, which is acceptable
assert response.status_code != status.HTTP_401_UNAUTHORIZED
async def test_jwt_token_not_implemented(self, client):
"""Test that JWT token authentication returns 501 (not yet implemented)"""
response = await client.post(
"/rpc/disputes/file",
json={
"agreement_id": "test_agreement_1",
"respondent": "0x1234567890123456789012345678901234567890",
"dispute_type": "payment_dispute",
"reason": "Test dispute",
"evidence_hash": "0xabcdef"
},
headers={"Authorization": "Bearer test_token"}
)
assert response.status_code == status.HTTP_501_NOT_IMPLEMENTED
assert "JWT authentication not yet implemented" in response.json()["detail"]
@pytest.mark.asyncio
class TestDisputeAuthDevMode:
"""Test authentication behavior in development mode"""
@pytest.fixture
async def dev_client(self):
"""Create test client with DEV_MODE enabled"""
from apps.blockchain_node.src.aitbc_chain.rpc.router import router
from fastapi import FastAPI
app = FastAPI()
app.include_router(router, prefix="/rpc")
# Set DEV_MODE to true
original_dev_mode = os.getenv("DEV_MODE")
os.environ["DEV_MODE"] = "true"
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac
# Restore original DEV_MODE
if original_dev_mode is None:
os.environ.pop("DEV_MODE", None)
else:
os.environ["DEV_MODE"] = original_dev_mode
async def test_file_dispute_dev_mode_fallback(self, dev_client):
"""Test that in dev mode, missing auth uses zero address fallback"""
response = await dev_client.post(
"/rpc/disputes/file",
json={
"agreement_id": "test_agreement_1",
"respondent": "0x1234567890123456789012345678901234567890",
"dispute_type": "payment_dispute",
"reason": "Test dispute",
"evidence_hash": "0xabcdef"
}
)
# In dev mode, should not return 401 (uses zero address fallback)
# May return 500 if dispute service is not available
assert response.status_code != status.HTTP_401_UNAUTHORIZED