Initial commit: WealthWise financial analytics platform

This commit is contained in:
2026-02-14 21:16:57 +05:30
commit b8588df583
171 changed files with 29048 additions and 0 deletions

View File

@@ -0,0 +1 @@
# Tests package for WealthWise Backend

246
backend/tests/test_auth.py Normal file
View File

@@ -0,0 +1,246 @@
"""Tests for authentication layer.
This module tests JWT validation and authentication dependencies.
"""
import time
from datetime import datetime, timedelta, timezone
import pytest
from fastapi import HTTPException
from fastapi.testclient import TestClient
from jose import jwt
from app.api.deps import get_current_user, validate_supabase_token
from app.core.config import get_settings
from app.main import app
from app.schemas.token import TokenPayload, UserContext
settings = get_settings()
class TestJWTValidation:
"""Test cases for JWT token validation."""
def create_test_token(
self,
secret: str = None,
expired: bool = False,
wrong_audience: bool = False,
missing_claims: bool = False,
algorithm: str = "HS256",
) -> str:
"""Create a test JWT token with specified properties.
Args:
secret: JWT signing secret (defaults to settings.SUPABASE_JWT_SECRET)
expired: Whether the token should be expired
wrong_audience: Whether to use wrong audience
missing_claims: Whether to omit required claims
algorithm: Signing algorithm
Returns:
Encoded JWT string
"""
secret = secret or settings.SUPABASE_JWT_SECRET or settings.SECRET_KEY
# Base payload
payload = {
"sub": "550e8400-e29b-41d4-a716-446655440000",
"email": "test@wealthwise.app",
"aud": "wrong-audience" if wrong_audience else "authenticated",
"role": "authenticated",
"app_metadata": {},
"user_metadata": {},
}
if not missing_claims:
# Set expiration
if expired:
payload["exp"] = int(time.time()) - 3600 # 1 hour ago
else:
payload["exp"] = int(time.time()) + 3600 # 1 hour from now
return jwt.encode(payload, secret, algorithm=algorithm)
def test_validate_valid_token(self):
"""Test validation of a valid JWT token."""
token = self.create_test_token()
payload = validate_supabase_token(token)
assert isinstance(payload, TokenPayload)
assert payload.sub == "550e8400-e29b-41d4-a716-446655440000"
assert payload.email == "test@wealthwise.app"
assert payload.aud == "authenticated"
def test_validate_expired_token(self):
"""Test that expired tokens are rejected."""
token = self.create_test_token(expired=True)
with pytest.raises(HTTPException) as exc_info:
validate_supabase_token(token)
assert exc_info.value.status_code == 401
assert "Could not validate credentials" in exc_info.value.detail
def test_validate_wrong_audience(self):
"""Test that tokens with wrong audience are rejected."""
token = self.create_test_token(wrong_audience=True)
with pytest.raises(HTTPException) as exc_info:
validate_supabase_token(token)
assert exc_info.value.status_code == 401
def test_validate_invalid_signature(self):
"""Test that tokens with invalid signature are rejected."""
token = self.create_test_token(secret="wrong-secret")
with pytest.raises(HTTPException) as exc_info:
validate_supabase_token(token)
assert exc_info.value.status_code == 401
def test_validate_missing_exp(self):
"""Test that tokens without expiration are rejected."""
token = self.create_test_token(missing_claims=True)
with pytest.raises(HTTPException) as exc_info:
validate_supabase_token(token)
assert exc_info.value.status_code == 401
class TestGetCurrentUser:
"""Test cases for get_current_user dependency."""
def test_get_current_user_valid_token(self):
"""Test that valid token returns UserContext."""
# Create a valid token
payload = {
"sub": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@wealthwise.app",
"aud": "authenticated",
"exp": int(time.time()) + 3600,
"role": "authenticated",
"app_metadata": {},
"user_metadata": {},
}
token = jwt.encode(
payload,
settings.SUPABASE_JWT_SECRET or settings.SECRET_KEY,
algorithm="HS256",
)
# Since get_current_user is async, we need to run it in an event loop
import asyncio
async def test():
return await get_current_user(token)
user = asyncio.run(test())
assert isinstance(user, UserContext)
assert user.id == "550e8400-e29b-41d4-a716-446655440000"
assert user.email == "user@wealthwise.app"
assert user.role == "authenticated"
def test_get_current_user_no_token(self):
"""Test that missing token raises 401."""
import asyncio
async def test():
return await get_current_user(None)
with pytest.raises(HTTPException) as exc_info:
asyncio.run(test())
assert exc_info.value.status_code == 401
def test_get_current_user_invalid_token(self):
"""Test that invalid token raises 401."""
import asyncio
async def test():
return await get_current_user("invalid-token")
with pytest.raises(HTTPException) as exc_info:
asyncio.run(test())
assert exc_info.value.status_code == 401
class TestUserContext:
"""Test cases for UserContext model."""
def test_from_token_payload(self):
"""Test conversion from TokenPayload to UserContext."""
payload = TokenPayload(
sub="550e8400-e29b-41d4-a716-446655440000",
email="test@wealthwise.app",
aud="authenticated",
role="authenticated",
app_metadata={"role": "admin"},
)
context = UserContext.from_token_payload(payload)
assert context.id == "550e8400-e29b-41d4-a716-446655440000"
assert context.email == "test@wealthwise.app"
assert context.role == "admin" # From app_metadata
def test_from_token_payload_no_app_metadata_role(self):
"""Test fallback to role claim when app_metadata has no role."""
payload = TokenPayload(
sub="550e8400-e29b-41d4-a716-446655440000",
email="test@wealthwise.app",
aud="authenticated",
role="authenticated",
app_metadata={},
)
context = UserContext.from_token_payload(payload)
assert context.role == "authenticated" # From role claim
class TestProtectedEndpoints:
"""Integration tests for protected endpoints."""
def test_me_endpoint_without_auth(self):
"""Test that /me endpoint requires authentication."""
client = TestClient(app)
response = client.get("/api/v1/users/me")
assert response.status_code == 401
def test_me_endpoint_with_valid_auth(self):
"""Test that /me endpoint works with valid token."""
# Create valid token
payload = {
"sub": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@wealthwise.app",
"aud": "authenticated",
"exp": int(time.time()) + 3600,
"role": "authenticated",
"app_metadata": {},
"user_metadata": {},
}
token = jwt.encode(
payload,
settings.SUPABASE_JWT_SECRET or settings.SECRET_KEY,
algorithm="HS256",
)
client = TestClient(app)
response = client.get(
"/api/v1/users/me",
headers={"Authorization": f"Bearer {token}"},
)
assert response.status_code == 200
data = response.json()
assert data["id"] == "550e8400-e29b-41d4-a716-446655440000"
assert data["email"] == "user@wealthwise.app"

View File

@@ -0,0 +1,193 @@
"""Tests for self-hosted authentication system.
This module tests the JWT-based authentication with OAuth2 Password Flow.
"""
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import select
from sqlmodel.ext.asyncio.session import AsyncSession
from app.core.security import create_access_token, get_password_hash, verify_password
from app.main import app
from app.models import User
from app.schemas.user import UserCreate
client = TestClient(app)
class TestSecurityUtilities:
"""Test cases for security utilities."""
def test_password_hashing(self):
"""Test password hashing and verification."""
password = "testpassword123"
hashed = get_password_hash(password)
# Hash should be different from plain password
assert hashed != password
# Verification should succeed
assert verify_password(password, hashed) is True
# Wrong password should fail
assert verify_password("wrongpassword", hashed) is False
def test_create_access_token(self):
"""Test JWT token creation."""
data = {"sub": "123e4567-e89b-12d3-a456-426614174000"}
token = create_access_token(data)
assert token is not None
assert isinstance(token, str)
# JWT tokens have 3 parts separated by dots
assert len(token.split(".")) == 3
class TestAuthEndpoints:
"""Test cases for authentication endpoints."""
def test_register_success(self):
"""Test successful user registration."""
response = client.post(
"/api/v1/auth/register",
json={
"email": "test@wealthwise.app",
"password": "testpassword123",
},
)
assert response.status_code == 201
data = response.json()
assert data["email"] == "test@wealthwise.app"
assert "id" in data
assert "hashed_password" not in data
assert data["is_active"] is True
def test_register_duplicate_email(self):
"""Test registration with duplicate email."""
# First registration
client.post(
"/api/v1/auth/register",
json={
"email": "duplicate@wealthwise.app",
"password": "testpassword123",
},
)
# Second registration with same email
response = client.post(
"/api/v1/auth/register",
json={
"email": "duplicate@wealthwise.app",
"password": "testpassword123",
},
)
assert response.status_code == 400
assert "email already registered" in response.json()["detail"].lower()
def test_login_success(self):
"""Test successful login."""
# Register first
client.post(
"/api/v1/auth/register",
json={
"email": "login@wealthwise.app",
"password": "testpassword123",
},
)
# Login
response = client.post(
"/api/v1/auth/token",
data={
"username": "login@wealthwise.app",
"password": "testpassword123",
},
)
assert response.status_code == 200
data = response.json()
assert "access_token" in data
assert data["token_type"] == "bearer"
def test_login_invalid_credentials(self):
"""Test login with invalid credentials."""
response = client.post(
"/api/v1/auth/token",
data={
"username": "nonexistent@wealthwise.app",
"password": "wrongpassword",
},
)
assert response.status_code == 401
def test_login_wrong_password(self):
"""Test login with wrong password."""
# Register first
client.post(
"/api/v1/auth/register",
json={
"email": "wrongpass@wealthwise.app",
"password": "testpassword123",
},
)
# Login with wrong password
response = client.post(
"/api/v1/auth/token",
data={
"username": "wrongpass@wealthwise.app",
"password": "wrongpassword",
},
)
assert response.status_code == 401
class TestProtectedEndpoints:
"""Test cases for protected endpoints."""
def test_me_without_auth(self):
"""Test accessing /me without authentication."""
response = client.get("/api/v1/users/me")
assert response.status_code == 401
def test_me_with_valid_token(self):
"""Test accessing /me with valid token."""
# Register and login
register_response = client.post(
"/api/v1/auth/register",
json={
"email": "protected@wealthwise.app",
"password": "testpassword123",
},
)
login_response = client.post(
"/api/v1/auth/token",
data={
"username": "protected@wealthwise.app",
"password": "testpassword123",
},
)
token = login_response.json()["access_token"]
# Access protected endpoint
response = client.get(
"/api/v1/users/me",
headers={"Authorization": f"Bearer {token}"},
)
assert response.status_code == 200
data = response.json()
assert data["email"] == "protected@wealthwise.app"
def test_me_with_invalid_token(self):
"""Test accessing /me with invalid token."""
response = client.get(
"/api/v1/users/me",
headers={"Authorization": "Bearer invalid-token"},
)
assert response.status_code == 401

View File

@@ -0,0 +1,55 @@
"""Tests for core configuration module."""
import os
import pytest
from app.core.config import Settings, get_settings
class TestSettings:
"""Test cases for Settings configuration."""
def test_default_values(self):
"""Test that default values are set correctly."""
settings = Settings()
assert settings.PROJECT_NAME == "WealthWise"
assert settings.API_V1_STR == "/api/v1"
assert settings.VERSION == "1.0.0"
assert settings.DB_POOL_SIZE == 20
assert settings.DB_MAX_OVERFLOW == 10
assert settings.DEBUG is False
def test_database_url_validation(self):
"""Test that database URL is validated and converted to asyncpg."""
# Test conversion from postgresql:// to postgresql+asyncpg://
settings = Settings(DATABASE_URL="postgresql://user:pass@localhost:5432/db")
assert settings.DATABASE_URL.startswith("postgresql+asyncpg://")
# Test that asyncpg URL is preserved
asyncpg_url = "postgresql+asyncpg://user:pass@localhost:6543/postgres"
settings2 = Settings(DATABASE_URL=asyncpg_url)
assert settings2.DATABASE_URL == asyncpg_url
def test_cors_origins_parsing(self):
"""Test CORS origins parsing from string."""
settings = Settings(CORS_ORIGINS="http://localhost:5173,https://example.com")
assert "http://localhost:5173" in settings.CORS_ORIGINS
assert "https://example.com" in settings.CORS_ORIGINS
# Test list input
settings2 = Settings(CORS_ORIGINS=["http://localhost:3000"])
assert settings2.CORS_ORIGINS == ["http://localhost:3000"]
class TestGetSettings:
"""Test cases for get_settings function."""
def test_get_settings_returns_singleton(self):
"""Test that get_settings returns a cached singleton."""
settings1 = get_settings()
settings2 = get_settings()
# Should be the same object (cached)
assert settings1 is settings2

View File

@@ -0,0 +1,40 @@
"""Tests for health check endpoint."""
import pytest
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
class TestHealthCheck:
"""Test cases for health check endpoint."""
def test_health_endpoint_exists(self):
"""Test that health endpoint is accessible."""
response = client.get("/api/v1/health")
assert response.status_code in [200, 503] # 200 if DB connected, 503 if not
def test_health_response_structure(self):
"""Test that health response has expected structure."""
response = client.get("/api/v1/health")
data = response.json()
assert "status" in data
assert "database" in data
assert "version" in data
def test_readiness_probe(self):
"""Test readiness probe endpoint."""
response = client.get("/api/v1/health/ready")
assert response.status_code == 200
assert response.json() == {"ready": True}
def test_liveness_probe(self):
"""Test liveness probe endpoint."""
response = client.get("/api/v1/health/live")
assert response.status_code == 200
assert response.json() == {"alive": True}