mirror of
https://github.com/blackboxprogramming/BlackRoad-Operating-System.git
synced 2026-03-18 05:33:59 -05:00
Fix miner wallet queries and add tests
This commit is contained in:
@@ -6,6 +6,7 @@ from sqlalchemy import select
|
|||||||
|
|
||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
|
from app.models.blockchain import Wallet
|
||||||
from app.schemas.user import UserCreate, UserResponse, Token, UserLogin
|
from app.schemas.user import UserCreate, UserResponse, Token, UserLogin
|
||||||
from app.auth import (
|
from app.auth import (
|
||||||
verify_password,
|
verify_password,
|
||||||
@@ -38,7 +39,7 @@ async def register(user_data: UserCreate, db: AsyncSession = Depends(get_db)):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Generate wallet
|
# Generate wallet
|
||||||
wallet_address, private_key = BlockchainService.generate_wallet_address()
|
wallet_address, private_key, public_key = BlockchainService.generate_wallet_address()
|
||||||
|
|
||||||
# Create user
|
# Create user
|
||||||
user = User(
|
user = User(
|
||||||
@@ -53,6 +54,19 @@ async def register(user_data: UserCreate, db: AsyncSession = Depends(get_db)):
|
|||||||
)
|
)
|
||||||
|
|
||||||
db.add(user)
|
db.add(user)
|
||||||
|
await db.flush()
|
||||||
|
|
||||||
|
wallet = Wallet(
|
||||||
|
user_id=user.id,
|
||||||
|
address=wallet_address,
|
||||||
|
private_key=private_key,
|
||||||
|
public_key=public_key,
|
||||||
|
balance=user.balance,
|
||||||
|
label="Primary Wallet",
|
||||||
|
is_primary=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
db.add(wallet)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(user)
|
await db.refresh(user)
|
||||||
|
|
||||||
@@ -115,3 +129,10 @@ async def get_current_user_info(
|
|||||||
async def logout():
|
async def logout():
|
||||||
"""Logout (client should delete token)"""
|
"""Logout (client should delete token)"""
|
||||||
return {"message": "Successfully logged out"}
|
return {"message": "Successfully logged out"}
|
||||||
|
|
||||||
|
|
||||||
|
async def get_current_user(
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
) -> User:
|
||||||
|
"""Reusable dependency that returns the authenticated user."""
|
||||||
|
return current_user
|
||||||
|
|||||||
@@ -138,22 +138,37 @@ async def get_miner_stats(
|
|||||||
)
|
)
|
||||||
wallet = wallet_result.scalar_one_or_none()
|
wallet = wallet_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not wallet:
|
||||||
|
return MinerStats(
|
||||||
|
blocks_mined=0,
|
||||||
|
roadcoins_earned=0.0,
|
||||||
|
current_hashrate_mhs=miner_state.hashrate_mhs if miner_state.is_mining else 0.0,
|
||||||
|
average_hashrate_mhs=0.0,
|
||||||
|
total_shares=miner_state.shares_submitted,
|
||||||
|
accepted_shares=miner_state.shares_accepted,
|
||||||
|
rejected_shares=miner_state.shares_submitted - miner_state.shares_accepted,
|
||||||
|
last_block_time=None,
|
||||||
|
mining_since=miner_state.started_at,
|
||||||
|
)
|
||||||
|
|
||||||
|
miner_filter = Block.miner_address == wallet.address
|
||||||
|
|
||||||
# Count blocks mined by this user
|
# Count blocks mined by this user
|
||||||
blocks_count_result = await db.execute(
|
blocks_count_result = await db.execute(
|
||||||
select(func.count(Block.id)).filter(Block.miner == wallet.address if wallet else None)
|
select(func.count(Block.id)).filter(miner_filter)
|
||||||
)
|
)
|
||||||
blocks_mined = blocks_count_result.scalar() or 0
|
blocks_mined = blocks_count_result.scalar() or 0
|
||||||
|
|
||||||
# Sum rewards earned
|
# Sum rewards earned
|
||||||
rewards_result = await db.execute(
|
rewards_result = await db.execute(
|
||||||
select(func.sum(Block.reward)).filter(Block.miner == wallet.address if wallet else None)
|
select(func.sum(Block.reward)).filter(miner_filter)
|
||||||
)
|
)
|
||||||
roadcoins_earned = rewards_result.scalar() or 0.0
|
roadcoins_earned = rewards_result.scalar() or 0.0
|
||||||
|
|
||||||
# Get last block mined
|
# Get last block mined
|
||||||
last_block_result = await db.execute(
|
last_block_result = await db.execute(
|
||||||
select(Block)
|
select(Block)
|
||||||
.filter(Block.miner == wallet.address if wallet else None)
|
.filter(miner_filter)
|
||||||
.order_by(desc(Block.timestamp))
|
.order_by(desc(Block.timestamp))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
)
|
)
|
||||||
@@ -197,7 +212,7 @@ async def get_recent_blocks(
|
|||||||
# Get recent blocks
|
# Get recent blocks
|
||||||
blocks_result = await db.execute(
|
blocks_result = await db.execute(
|
||||||
select(Block)
|
select(Block)
|
||||||
.filter(Block.miner == wallet.address)
|
.filter(Block.miner_address == wallet.address)
|
||||||
.order_by(desc(Block.timestamp))
|
.order_by(desc(Block.timestamp))
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -127,12 +127,12 @@ class BlockchainService:
|
|||||||
return new_block
|
return new_block
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def generate_wallet_address() -> tuple[str, str]:
|
def generate_wallet_address() -> tuple[str, str, str]:
|
||||||
"""Generate a new wallet address and private key"""
|
"""Generate a new wallet address, private key and public key"""
|
||||||
private_key = secrets.token_hex(32)
|
private_key = secrets.token_hex(32)
|
||||||
public_key = hashlib.sha256(private_key.encode()).hexdigest()
|
public_key = hashlib.sha256(private_key.encode()).hexdigest()
|
||||||
address = "RD" + hashlib.sha256(public_key.encode()).hexdigest()[:38]
|
address = "RD" + hashlib.sha256(public_key.encode()).hexdigest()[:38]
|
||||||
return address, private_key
|
return address, private_key, public_key
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def create_transaction(
|
async def create_transaction(
|
||||||
|
|||||||
2
backend/pytest.ini
Normal file
2
backend/pytest.ini
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[pytest]
|
||||||
|
asyncio_mode = auto
|
||||||
@@ -1,16 +1,21 @@
|
|||||||
"""Pytest configuration and fixtures"""
|
"""Pytest configuration and fixtures"""
|
||||||
import pytest
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import os
|
||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator
|
||||||
from httpx import AsyncClient
|
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
import pytest
|
||||||
|
from httpx import ASGITransport, AsyncClient
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||||
|
|
||||||
from app.main import app
|
from app.main import app
|
||||||
from app.database import get_db, Base
|
from app.database import get_db, Base
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
|
|
||||||
# Test database URL
|
# Test database URL
|
||||||
TEST_DATABASE_URL = "postgresql+asyncpg://blackroad:password@localhost:5432/blackroad_test"
|
TEST_DATABASE_URL = os.getenv(
|
||||||
|
"TEST_DATABASE_URL",
|
||||||
|
"sqlite+aiosqlite:///./test.db"
|
||||||
|
)
|
||||||
|
|
||||||
# Create test engine
|
# Create test engine
|
||||||
test_engine = create_async_engine(TEST_DATABASE_URL, echo=False)
|
test_engine = create_async_engine(TEST_DATABASE_URL, echo=False)
|
||||||
@@ -46,7 +51,9 @@ async def client(db_session: AsyncSession) -> AsyncGenerator[AsyncClient, None]:
|
|||||||
|
|
||||||
app.dependency_overrides[get_db] = override_get_db
|
app.dependency_overrides[get_db] = override_get_db
|
||||||
|
|
||||||
async with AsyncClient(app=app, base_url="http://test") as client:
|
transport = ASGITransport(app=app)
|
||||||
|
|
||||||
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||||
yield client
|
yield client
|
||||||
|
|
||||||
app.dependency_overrides.clear()
|
app.dependency_overrides.clear()
|
||||||
|
|||||||
130
backend/tests/test_miner.py
Normal file
130
backend/tests/test_miner.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
"""Miner integration tests"""
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from httpx import AsyncClient
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.models.blockchain import Block
|
||||||
|
|
||||||
|
|
||||||
|
async def _create_block(
|
||||||
|
db_session: AsyncSession,
|
||||||
|
*,
|
||||||
|
index: int,
|
||||||
|
timestamp: datetime,
|
||||||
|
miner_id: int,
|
||||||
|
miner_address: str,
|
||||||
|
reward: float,
|
||||||
|
) -> None:
|
||||||
|
"""Helper to insert a block for tests."""
|
||||||
|
block = Block(
|
||||||
|
index=index,
|
||||||
|
timestamp=timestamp,
|
||||||
|
nonce=index,
|
||||||
|
previous_hash=f"prev-{index}",
|
||||||
|
hash=f"hash-{index}-{miner_address}",
|
||||||
|
miner_id=miner_id,
|
||||||
|
miner_address=miner_address,
|
||||||
|
difficulty=4,
|
||||||
|
reward=reward,
|
||||||
|
transaction_count=0,
|
||||||
|
is_valid=True,
|
||||||
|
)
|
||||||
|
db_session.add(block)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_miner_stats_respects_wallet(
|
||||||
|
client: AsyncClient,
|
||||||
|
auth_headers,
|
||||||
|
db_session: AsyncSession,
|
||||||
|
test_user,
|
||||||
|
):
|
||||||
|
"""Ensure /api/miner/stats reports only the authenticated user's blocks."""
|
||||||
|
wallet_address = test_user["wallet_address"]
|
||||||
|
user_id = test_user["id"]
|
||||||
|
now = datetime.utcnow()
|
||||||
|
|
||||||
|
await _create_block(
|
||||||
|
db_session,
|
||||||
|
index=1,
|
||||||
|
timestamp=now - timedelta(minutes=5),
|
||||||
|
miner_id=user_id,
|
||||||
|
miner_address=wallet_address,
|
||||||
|
reward=40.0,
|
||||||
|
)
|
||||||
|
await _create_block(
|
||||||
|
db_session,
|
||||||
|
index=2,
|
||||||
|
timestamp=now - timedelta(minutes=1),
|
||||||
|
miner_id=user_id,
|
||||||
|
miner_address=wallet_address,
|
||||||
|
reward=60.0,
|
||||||
|
)
|
||||||
|
await _create_block(
|
||||||
|
db_session,
|
||||||
|
index=3,
|
||||||
|
timestamp=now - timedelta(minutes=2),
|
||||||
|
miner_id=user_id + 100,
|
||||||
|
miner_address="RDOTHER000000000000000000000000000000", # Different miner
|
||||||
|
reward=75.0,
|
||||||
|
)
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
response = await client.get("/api/miner/stats", headers=auth_headers)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert data["blocks_mined"] == 2
|
||||||
|
assert pytest.approx(data["roadcoins_earned"], rel=1e-3) == 100.0
|
||||||
|
assert data["last_block_time"] is not None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_miner_blocks_endpoint_returns_only_user_blocks(
|
||||||
|
client: AsyncClient,
|
||||||
|
auth_headers,
|
||||||
|
db_session: AsyncSession,
|
||||||
|
test_user,
|
||||||
|
):
|
||||||
|
"""Ensure /api/miner/blocks only returns the authenticated user's blocks."""
|
||||||
|
wallet_address = test_user["wallet_address"]
|
||||||
|
user_id = test_user["id"]
|
||||||
|
now = datetime.utcnow()
|
||||||
|
|
||||||
|
await _create_block(
|
||||||
|
db_session,
|
||||||
|
index=5,
|
||||||
|
timestamp=now - timedelta(minutes=10),
|
||||||
|
miner_id=user_id,
|
||||||
|
miner_address=wallet_address,
|
||||||
|
reward=25.0,
|
||||||
|
)
|
||||||
|
await _create_block(
|
||||||
|
db_session,
|
||||||
|
index=6,
|
||||||
|
timestamp=now - timedelta(minutes=3),
|
||||||
|
miner_id=user_id,
|
||||||
|
miner_address=wallet_address,
|
||||||
|
reward=30.0,
|
||||||
|
)
|
||||||
|
await _create_block(
|
||||||
|
db_session,
|
||||||
|
index=7,
|
||||||
|
timestamp=now - timedelta(minutes=1),
|
||||||
|
miner_id=user_id + 200,
|
||||||
|
miner_address="RDANOTHER0000000000000000000000000000",
|
||||||
|
reward=55.0,
|
||||||
|
)
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
response = await client.get("/api/miner/blocks", headers=auth_headers)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert len(data) == 2
|
||||||
|
returned_indexes = {block["block_index"] for block in data}
|
||||||
|
assert returned_indexes == {5, 6}
|
||||||
|
# Ensure results are sorted by timestamp desc (latest first)
|
||||||
|
assert data[0]["block_index"] == 6
|
||||||
Reference in New Issue
Block a user