mirror of
https://github.com/blackboxprogramming/BlackRoad-Operating-System.git
synced 2026-03-18 05:33:59 -05:00
Enforce positive blockchain transactions
This commit is contained in:
@@ -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"""
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user