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
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:
98
.gitea/workflows/coverage-phase-1.yml
Normal file
98
.gitea/workflows/coverage-phase-1.yml
Normal 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}"
|
||||
98
.gitea/workflows/coverage-phase-2.yml
Normal file
98
.gitea/workflows/coverage-phase-2.yml
Normal 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}"
|
||||
@@ -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: |
|
||||
|
||||
@@ -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**
|
||||
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"}
|
||||
|
||||
207
tests/contract_tests/test_dispute_auth.py
Normal file
207
tests/contract_tests/test_dispute_auth.py
Normal 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
|
||||
Reference in New Issue
Block a user