Enforce positive blockchain transactions

This commit is contained in:
Alexa Amundson
2025-11-16 01:56:18 -06:00
parent b2f933762a
commit 509747d4d9
4 changed files with 113 additions and 9 deletions

View File

@@ -1,8 +1,8 @@
"""Authentication routes""" """Authentication routes"""
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status, Form
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
from typing import Optional
from app.database import get_db from app.database import get_db
from app.models.user import User from app.models.user import User
@@ -19,6 +19,29 @@ from datetime import datetime
router = APIRouter(prefix="/api/auth", tags=["Authentication"]) router = APIRouter(prefix="/api/auth", tags=["Authentication"])
# Backwards compatibility for modules importing get_current_user from this router
get_current_user = get_current_active_user
class SimpleOAuth2PasswordRequestForm:
"""Minimal form parser compatible with OAuth2 password flow"""
def __init__(
self,
grant_type: Optional[str] = Form(default=None),
username: str = Form(...),
password: str = Form(...),
scope: str = Form(default=""),
client_id: Optional[str] = Form(default=None),
client_secret: Optional[str] = Form(default=None)
):
self.grant_type = grant_type
self.username = username
self.password = password
self.scopes = scope.split()
self.client_id = client_id
self.client_secret = client_secret
@router.post("/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED) @router.post("/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def register(user_data: UserCreate, db: AsyncSession = Depends(get_db)): async def register(user_data: UserCreate, db: AsyncSession = Depends(get_db)):
@@ -61,7 +84,7 @@ async def register(user_data: UserCreate, db: AsyncSession = Depends(get_db)):
@router.post("/login", response_model=Token) @router.post("/login", response_model=Token)
async def login( async def login(
form_data: OAuth2PasswordRequestForm = Depends(), form_data: SimpleOAuth2PasswordRequestForm = Depends(),
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
"""Login and get access token""" """Login and get access token"""

View File

@@ -3,7 +3,7 @@ from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_, or_, desc, func from sqlalchemy import select, and_, or_, desc, func
from typing import List, Optional from typing import List, Optional
from pydantic import BaseModel from pydantic import BaseModel, Field
from datetime import datetime from datetime import datetime
from app.database import get_db from app.database import get_db
@@ -15,9 +15,12 @@ from app.services.blockchain import BlockchainService
router = APIRouter(prefix="/api/blockchain", tags=["Blockchain"]) router = APIRouter(prefix="/api/blockchain", tags=["Blockchain"])
MIN_TRANSACTION_AMOUNT = 0.0001
class TransactionCreate(BaseModel): class TransactionCreate(BaseModel):
to_address: str to_address: str
amount: float amount: float = Field(gt=0, description="Amount to transfer; must be positive")
message: Optional[str] = None message: Optional[str] = None
@@ -92,6 +95,18 @@ async def create_transaction(
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
"""Create a new transaction""" """Create a new transaction"""
if tx_data.amount <= 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Transaction amount must be greater than zero"
)
if tx_data.amount < MIN_TRANSACTION_AMOUNT:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Transactions must be at least {MIN_TRANSACTION_AMOUNT} tokens"
)
# Check balance # Check balance
if current_user.balance < tx_data.amount: if current_user.balance < tx_data.amount:
raise HTTPException( raise HTTPException(
@@ -121,6 +136,11 @@ async def create_transaction(
) )
# Update balances (simplified - in production would be done on block confirmation) # Update balances (simplified - in production would be done on block confirmation)
if tx_data.amount <= 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Transaction amount must be greater than zero"
)
current_user.balance -= tx_data.amount current_user.balance -= tx_data.amount
recipient.balance += tx_data.amount recipient.balance += tx_data.amount

View File

@@ -1,5 +1,6 @@
"""Pytest configuration and fixtures""" """Pytest configuration and fixtures"""
import pytest import pytest
import pytest_asyncio
import asyncio import asyncio
from typing import AsyncGenerator from typing import AsyncGenerator
from httpx import AsyncClient from httpx import AsyncClient
@@ -25,7 +26,7 @@ def event_loop():
loop.close() loop.close()
@pytest.fixture(scope="function") @pytest_asyncio.fixture(scope="function")
async def db_session() -> AsyncGenerator[AsyncSession, None]: async def db_session() -> AsyncGenerator[AsyncSession, None]:
"""Create test database session""" """Create test database session"""
async with test_engine.begin() as conn: async with test_engine.begin() as conn:
@@ -38,7 +39,7 @@ async def db_session() -> AsyncGenerator[AsyncSession, None]:
await conn.run_sync(Base.metadata.drop_all) await conn.run_sync(Base.metadata.drop_all)
@pytest.fixture(scope="function") @pytest_asyncio.fixture(scope="function")
async def client(db_session: AsyncSession) -> AsyncGenerator[AsyncClient, None]: async def client(db_session: AsyncSession) -> AsyncGenerator[AsyncClient, None]:
"""Create test client""" """Create test client"""
async def override_get_db(): async def override_get_db():
@@ -52,7 +53,7 @@ async def client(db_session: AsyncSession) -> AsyncGenerator[AsyncClient, None]:
app.dependency_overrides.clear() app.dependency_overrides.clear()
@pytest.fixture @pytest_asyncio.fixture
async def test_user(client: AsyncClient): async def test_user(client: AsyncClient):
"""Create a test user""" """Create a test user"""
user_data = { user_data = {
@@ -67,7 +68,7 @@ async def test_user(client: AsyncClient):
return response.json() return response.json()
@pytest.fixture @pytest_asyncio.fixture
async def auth_headers(client: AsyncClient, test_user): async def auth_headers(client: AsyncClient, test_user):
"""Get authentication headers""" """Get authentication headers"""
login_data = { login_data = {

View File

@@ -1,8 +1,24 @@
"""Blockchain tests""" """Blockchain tests"""
import pytest import pytest
import pytest_asyncio
from httpx import AsyncClient from httpx import AsyncClient
@pytest_asyncio.fixture
async def recipient_user(client: AsyncClient):
"""Create a secondary user to receive transactions"""
user_data = {
"username": "recipient",
"email": "recipient@example.com",
"password": "recipientpassword",
"full_name": "Recipient User"
}
response = await client.post("/api/auth/register", json=user_data)
assert response.status_code == 201
return response.json()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_wallet(client: AsyncClient, auth_headers): async def test_get_wallet(client: AsyncClient, auth_headers):
"""Test getting user wallet""" """Test getting user wallet"""
@@ -49,3 +65,47 @@ async def test_mine_block(client: AsyncClient, auth_headers):
assert "hash" in data assert "hash" in data
assert "reward" in data assert "reward" in data
assert data["reward"] == 50.0 # Mining reward assert data["reward"] == 50.0 # Mining reward
@pytest.mark.asyncio
async def test_create_transaction_rejects_zero_amount(
client: AsyncClient,
auth_headers,
recipient_user
):
"""Transaction creation should fail fast for zero amounts"""
tx_data = {
"to_address": recipient_user["wallet_address"],
"amount": 0,
"message": "Invalid zero"
}
response = await client.post(
"/api/blockchain/transactions",
json=tx_data,
headers=auth_headers
)
assert response.status_code == 422
@pytest.mark.asyncio
async def test_create_transaction_rejects_negative_amount(
client: AsyncClient,
auth_headers,
recipient_user
):
"""Transaction creation should fail fast for negative amounts"""
tx_data = {
"to_address": recipient_user["wallet_address"],
"amount": -10,
"message": "Invalid negative"
}
response = await client.post(
"/api/blockchain/transactions",
json=tx_data,
headers=auth_headers
)
assert response.status_code == 422