Skip to content

Commit

Permalink
linting
Browse files Browse the repository at this point in the history
  • Loading branch information
mmiqball committed Nov 3, 2024
1 parent a564d75 commit c547769
Show file tree
Hide file tree
Showing 7 changed files with 81 additions and 54 deletions.
8 changes: 5 additions & 3 deletions backend/app/routes/user.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session

from app.schemas.user import UserCreate, UserInDB
from app.services.implementations.user_service import UserService
from app.utilities.db_utils import get_db
from sqlalchemy.orm import Session

router = APIRouter(
prefix="/users",
tags=["users"],
)

# to do:
# to do:
# send email verification via auth_service
# allow signup methods other than email (like sign up w Google)??


@router.post("/", response_model=UserInDB)
async def create_user(user: UserCreate, db: Session = Depends(get_db)):
user_service = UserService(db)
Expand All @@ -22,4 +24,4 @@ async def create_user(user: UserCreate, db: Session = Depends(get_db)):
except HTTPException as http_ex:
raise http_ex
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
raise HTTPException(status_code=500, detail=str(e))
25 changes: 14 additions & 11 deletions backend/app/schemas/user.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,41 @@
from pydantic import BaseModel, EmailStr, Field, field_validator, ConfigDict
from enum import Enum
from uuid import UUID

from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator


class UserRole(str, Enum):
PARTICIPANT = "participant"
VOLUNTEER = "volunteer"
ADMIN = "admin"

@classmethod
def to_role_id(cls, role: "UserRole") -> int:
role_map = {
cls.PARTICIPANT: 1,
cls.VOLUNTEER: 2,
cls.ADMIN: 3
}
role_map = {cls.PARTICIPANT: 1, cls.VOLUNTEER: 2, cls.ADMIN: 3}
return role_map[role]


class UserBase(BaseModel):
first_name: str = Field(..., min_length=1, max_length=50)
last_name: str = Field(..., min_length=1, max_length=50)
email: EmailStr
role: UserRole


class UserCreate(UserBase):
password: str = Field(..., min_length=8)

@field_validator('password')
@field_validator("password")
def password_complexity(cls, v):
if not any(char.isdigit() for char in v):
raise ValueError('Password must contain at least one digit')
raise ValueError("Password must contain at least one digit")
if not any(char.isupper() for char in v):
raise ValueError('Password must contain at least one uppercase letter')
raise ValueError("Password must contain at least one uppercase letter")
if not any(char.islower() for char in v):
raise ValueError('Password must contain at least one lowercase letter')
raise ValueError("Password must contain at least one lowercase letter")
return v


class UserInDB(BaseModel):
id: UUID
first_name: str
Expand All @@ -45,15 +46,17 @@ class UserInDB(BaseModel):

model_config = ConfigDict(from_attributes=True)


class User(UserBase):
id: UUID

model_config = ConfigDict(from_attributes=True)


class UserUpdate(BaseModel):
first_name: str | None = Field(None, min_length=1, max_length=50)
last_name: str | None = Field(None, min_length=1, max_length=50)
email: EmailStr | None = None
role: UserRole | None = None

model_config = ConfigDict(from_attributes=True)
model_config = ConfigDict(from_attributes=True)
2 changes: 1 addition & 1 deletion backend/app/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from .routes import user
from .utilities.firebase_init import initialize_firebase


load_dotenv()

log = logging.getLogger("uvicorn")
Expand All @@ -29,6 +28,7 @@ async def lifespan(_: FastAPI):
app = FastAPI(lifespan=lifespan)
app.include_router(user.router)


@app.get("/")
def read_root():
return {"Hello": "World"}
Expand Down
19 changes: 11 additions & 8 deletions backend/app/services/implementations/user_service.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import logging

import firebase_admin.auth
from fastapi import HTTPException
from sqlalchemy.orm import Session
from app.models import Role, User

from app.models import User
from app.schemas.user import UserCreate, UserInDB, UserRole
from app.services.interfaces.user_service import IUserService
from firebase_admin.exceptions import FirebaseError
import logging


class UserService(IUserService):
def __init__(self, db: Session):
Expand All @@ -17,8 +19,7 @@ async def create_user(self, user: UserCreate) -> UserInDB:
try:
# Create user in Firebase
firebase_user = firebase_admin.auth.create_user(
email=user.email,
password=user.password
email=user.email, password=user.password
)

role_id = UserRole.to_role_id(user.role)
Expand All @@ -29,15 +30,15 @@ async def create_user(self, user: UserCreate) -> UserInDB:
last_name=user.last_name,
email=user.email,
role_id=role_id,
auth_id=firebase_user.uid
auth_id=firebase_user.uid,
)

self.db.add(db_user)
self.db.commit()
self.db.refresh(db_user)

return UserInDB.model_validate(db_user)

except firebase_admin.exceptions.FirebaseError as firebase_error:
self.logger.error(f"Firebase error: {str(firebase_error)}")
if isinstance(firebase_error, firebase_admin.auth.EmailAlreadyExistsError):
Expand All @@ -49,7 +50,9 @@ async def create_user(self, user: UserCreate) -> UserInDB:
try:
firebase_admin.auth.delete_user(firebase_user.uid)
except firebase_admin.auth.AuthError as firebase_error:
self.logger.error(f"Failed to delete Firebase user after database insertion failed. Firebase UID: {firebase_user.uid}. Error: {str(firebase_error)}")
self.logger.error(
f"Failed to delete Firebase user after database insertion failed. Firebase UID: {firebase_user.uid}. Error: {str(firebase_error)}"
)

self.db.rollback()
self.logger.error(f"Error creating user: {str(e)}")
Expand Down
7 changes: 4 additions & 3 deletions backend/app/utilities/db_utils.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
from app.models.Base import Base
import os

from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker

DATABASE_URL = os.getenv("POSTGRES_DATABASE_URL")
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)


def get_db() -> Session:
db = SessionLocal()
try:
Expand Down
34 changes: 21 additions & 13 deletions backend/app/utilities/firebase_init.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
import os

import firebase_admin
from firebase_admin import credentials


def initialize_firebase():
private_key = os.getenv("FIREBASE_SVC_ACCOUNT_PRIVATE_KEY")
if private_key:
private_key = private_key.replace("\\n", "\n")

cred = credentials.Certificate({
"type": "service_account",
"project_id": os.getenv("FIREBASE_PROJECT_ID"),
"private_key_id": os.getenv("FIREBASE_SVC_ACCOUNT_PRIVATE_KEY_ID"),
"private_key": private_key,
"client_email": os.getenv("FIREBASE_SVC_ACCOUNT_CLIENT_EMAIL"),
"client_id": os.getenv("FIREBASE_SVC_ACCOUNT_CLIENT_ID"),
"auth_uri": os.getenv("FIREBASE_SVC_ACCOUNT_AUTH_URI"),
"token_uri": os.getenv("FIREBASE_SVC_ACCOUNT_TOKEN_URI"),
"auth_provider_x509_cert_url": os.getenv("FIREBASE_SVC_ACCOUNT_AUTH_PROVIDER_X509_CERT_URL"),
"client_x509_cert_url": os.getenv("FIREBASE_SVC_ACCOUNT_CLIENT_X509_CERT_URL"),
})
cred = credentials.Certificate(
{
"type": "service_account",
"project_id": os.getenv("FIREBASE_PROJECT_ID"),
"private_key_id": os.getenv("FIREBASE_SVC_ACCOUNT_PRIVATE_KEY_ID"),
"private_key": private_key,
"client_email": os.getenv("FIREBASE_SVC_ACCOUNT_CLIENT_EMAIL"),
"client_id": os.getenv("FIREBASE_SVC_ACCOUNT_CLIENT_ID"),
"auth_uri": os.getenv("FIREBASE_SVC_ACCOUNT_AUTH_URI"),
"token_uri": os.getenv("FIREBASE_SVC_ACCOUNT_TOKEN_URI"),
"auth_provider_x509_cert_url": os.getenv(
"FIREBASE_SVC_ACCOUNT_AUTH_PROVIDER_X509_CERT_URL"
),
"client_x509_cert_url": os.getenv(
"FIREBASE_SVC_ACCOUNT_CLIENT_X509_CERT_URL"
),
}
)

firebase_admin.initialize_app(cred)
firebase_admin.initialize_app(cred)
40 changes: 25 additions & 15 deletions backend/tests/unit/test_models.py
Original file line number Diff line number Diff line change
@@ -1,73 +1,82 @@
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from app.models import Role
from app.models.Base import Base
from app.models.User import User
from app.models import Role
from app.schemas.user import UserCreate, UserRole, UserInDB
from app.schemas.user import UserCreate, UserInDB, UserRole
from app.services.implementations.user_service import UserService

# Test DB Configuration
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False}
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)


class MockFirebaseUser:
"""Mock Firebase user response"""

def __init__(self):
self.uid = "test_firebase_uid"
self.email = "[email protected]"


class MockFirebaseError(Exception):
"""Mock Firebase error"""

pass


class MockAuthError(MockFirebaseError):
"""Mock Firebase auth error"""

def __init__(self, code, message):
self.code = code
self.message = message
super().__init__(f"{code}: {message}")


@pytest.fixture
def mock_firebase_auth(monkeypatch):
"""Mock Firebase authentication service"""

class MockAuth:
def create_user(self, email, password):
return MockFirebaseUser()

def get_user_by_email(self, email):
return MockFirebaseUser()

def delete_user(self, uid):
pass

AuthError = MockAuthError

mock_auth = MockAuth()
monkeypatch.setattr('firebase_admin.auth', mock_auth)
monkeypatch.setattr("firebase_admin.auth", mock_auth)
return mock_auth


@pytest.fixture(scope="function")
def db_session():
"""Provide a clean database session for each test"""
Base.metadata.create_all(bind=engine)
session = TestingSessionLocal()

try:
# Clean up any existing data first
session.query(User).delete()
session.query(Role).delete()
session.commit()

# Create test role
test_role = Role(id=1, name=UserRole.PARTICIPANT)
session.add(test_role)
session.commit()

yield session
finally:
session.rollback()
Expand All @@ -80,6 +89,7 @@ def db_session():
session.close()
Base.metadata.drop_all(bind=engine)


@pytest.mark.asyncio
async def test_create_user_service(mock_firebase_auth, db_session):
"""Test user creation flow including Firebase auth and database storage"""
Expand All @@ -91,7 +101,7 @@ async def test_create_user_service(mock_firebase_auth, db_session):
last_name="User",
email="[email protected]",
password="TestPass@123",
role=UserRole.PARTICIPANT
role=UserRole.PARTICIPANT,
)

# Act
Expand All @@ -110,8 +120,8 @@ async def test_create_user_service(mock_firebase_auth, db_session):
assert db_user is not None
assert db_user.auth_id == "test_firebase_uid"
assert db_user.role_id == 1

db_session.commit() # Commit successful test
except Exception:
db_session.rollback() # Rollback on error
raise
raise

0 comments on commit c547769

Please sign in to comment.