Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ref, test: some files refactored and test inits for user added. #12

Merged
merged 1 commit into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 5 additions & 30 deletions chatApp/config/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,16 @@
from typing import Any

from fastapi import Depends
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from passlib.context import CryptContext

from chatApp.config.config import get_settings
from chatApp.config.logs import logger
from chatApp.models import user as user_model
from chatApp.utils import hasher
from chatApp.utils.exceptions import credentials_exception

settings = get_settings()

# Password hashing context
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# OAuth2 scheme
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token")

# JWT settings
SECRET_KEY = settings.jwt_secret_key.get_secret_value()
Expand All @@ -26,27 +20,6 @@
REFRESH_TOKEN_EXPIRE_DAYS = settings.refresh_token_expire_days


def verify_password(plain_password: str, hashed_password: str) -> bool:
"""
Verify if the provided password matches the stored hashed password.

:param plain_password: The plain text password.
:param hashed_password: The hashed password stored in the database.
:return: True if passwords match, otherwise False.
"""
return pwd_context.verify(plain_password, hashed_password)


def get_password_hash(password: str) -> str:
"""
Hash the given password using the password hashing context.

:param password: The plain text password to hash.
:return: The hashed password.
"""
return pwd_context.hash(password)


def create_token(
data: dict[str, Any],
token_type: str,
Expand Down Expand Up @@ -128,7 +101,7 @@ def validate_token(token: str) -> bool:


async def get_current_user(
token: str = Depends(oauth2_scheme),
token: str = Depends(hasher.oauth2_scheme),
) -> user_model.UserInDB:
"""
Retrieve the current user from the database using the provided JWT token.
Expand Down Expand Up @@ -165,7 +138,9 @@ async def authenticate_user(
)

# Return None if no user was found or if password verification fails
if user is None or not verify_password(password, user.hashed_password):
if user is None or not hasher.verify_password(
password, user.hashed_password
):
return None

return user
3 changes: 3 additions & 0 deletions chatApp/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ class Settings(BaseSettings):
database_name: str = Field(default="chat_app")
max_pool_size: int = 10
min_pool_size: int = 1
test_database_url: str = Field(default="mongodb://localhost:27017")
test_database_name: str = Field(default="test_chat_app")
test_mode: bool = Field(default=False)

# jwt settings
jwt_secret_key: SecretStr = Field(default="your-secret-key")
Expand Down
36 changes: 26 additions & 10 deletions chatApp/config/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,46 @@


class MongoDB:
def __init__(self) -> None:
def __init__(self, test_db: bool = False) -> None:
self.db_client: AsyncIOMotorClient | None = None
self.db: AsyncIOMotorDatabase | None = None
self.users_collection: AsyncIOMotorCollection | None = None
self.messages_collection: AsyncIOMotorCollection | None = None
self.public_rooms_collection: AsyncIOMotorCollection | None = None
self.private_rooms_collection: AsyncIOMotorCollection | None = None
self.test_db: bool = test_db

async def connect_to_mongodb(self) -> None:
try:
db_url = (
settings.test_database_url
if self.test_db
else settings.database_url
)
db_name = (
settings.test_database_name
if self.test_db
else settings.database_name
)

self.db_client = AsyncIOMotorClient(
settings.database_url,
db_url,
maxPoolSize=settings.max_pool_size,
minPoolSize=settings.min_pool_size,
)

assert self.db_client is not None
self.db = self.db_client[settings.database_name]
self.db = self.db_client[db_name]
assert self.db is not None

# Define collections and schema validations
await self.create_collections()

# Ping the server to validate the connection
await self.db_client.admin.command("ismaster")
logger.info("Connected to MongoDB")
logger.info(
f"Connected to MongoDB {'test' if self.test_db else ''} database"
)
except Exception as e:
logger.error(f"Could not connect to MongoDB: {e}")
raise
Expand Down Expand Up @@ -196,21 +210,23 @@ async def close_mongodb_connection(self) -> None:
self.db_client.close()
logger.info("Closed MongoDB connection")

async def drop_database(self) -> None:
if self.db_client and self.db is not None:
await self.db_client.drop_database(self.db.name)
logger.info(f"Dropped database {self.db.name}")


mongo_db = None


async def init_mongo_db():
async def init_mongo_db(test_db: bool = False) -> MongoDB:
global mongo_db
mongo_db = MongoDB()
mongo_db = MongoDB(test_db=test_db)
await mongo_db.connect_to_mongodb()
return mongo_db


async def shutdown_mongo_db():
"""
Close the MongoDB connection.
"""
async def shutdown_mongo_db() -> None:
global mongo_db
if mongo_db is not None:
await mongo_db.close_mongodb_connection()
Expand Down
16 changes: 14 additions & 2 deletions chatApp/main.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager

import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
Expand All @@ -13,13 +16,22 @@
settings = get_settings()


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
# This function will be called on startup and shutdown
await init_mongo_db(test_db=settings.test_mode)
try:
yield
finally:
await shutdown_mongo_db()


# Create a FastAPI app instance
app = FastAPI(
title="FastAPI Chat App",
description="A chat application built with FastAPI and socket.io",
version="1.0.0",
on_startup=[init_mongo_db],
on_shutdown=[shutdown_mongo_db],
lifespan=lifespan,
)

### Add middlewares ###
Expand Down
4 changes: 2 additions & 2 deletions chatApp/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
from motor.motor_asyncio import AsyncIOMotorCursor
from pydantic import BaseModel, Field

from chatApp.config import auth
from chatApp.config.database import get_users_collection
from chatApp.utils import hasher
from chatApp.utils.object_id import PydanticObjectId


Expand Down Expand Up @@ -65,7 +65,7 @@ async def create_user(user_dict: dict[str, Any]) -> UserInDB:
user_dict["created_at"] = datetime.now()
user_dict["updated_at"] = datetime.now()
user_dict["last_login"] = datetime.now()
user_dict["hashed_password"] = auth.get_password_hash(
user_dict["hashed_password"] = hasher.get_password_hash(
user_dict["password"]
)

Expand Down
33 changes: 33 additions & 0 deletions chatApp/utils/hasher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from fastapi.security import OAuth2PasswordBearer
from passlib.context import CryptContext

from chatApp.config.config import get_settings

settings = get_settings()

# Password hashing context
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# OAuth2 scheme
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token")


def verify_password(plain_password: str, hashed_password: str) -> bool:
"""
Verify if the provided password matches the stored hashed password.

:param plain_password: The plain text password.
:param hashed_password: The hashed password stored in the database.
:return: True if passwords match, otherwise False.
"""
return pwd_context.verify(plain_password, hashed_password)


def get_password_hash(password: str) -> str:
"""
Hash the given password using the password hashing context.

:param password: The plain text password to hash.
:return: The hashed password.
"""
return pwd_context.hash(password)
1 change: 1 addition & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
[pytest]
asyncio_mode = auto
addopts = -p no:warnings -vv
64 changes: 64 additions & 0 deletions tests/unit/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import pytest

from chatApp.config import database
from chatApp.config.database import mongo_db


@pytest.fixture(scope="session")
async def db():
# Initialize the test database
global mongo_db
print(f"mongodb is {mongo_db}")
mongo_db = await database.init_mongo_db(test_db=True)
yield mongo_db
# Clean up the test database
await database.shutdown_mongo_db()


@pytest.fixture
async def users_collection(db):
return db.users_collection


@pytest.fixture
async def messages_collection(db):
return db.messages_collection


@pytest.fixture
async def public_rooms_collection(db):
return db.public_rooms_collection


@pytest.fixture
async def private_rooms_collection(db):
return db.private_rooms_collection


@pytest.fixture
async def test_user():
return {
"username": "test_user",
"email": "test@test.com",
"password": "test_password",
}


@pytest.fixture
async def test_room():
return {"name": "test_room"}


@pytest.fixture
async def test_message():
return {"sender": "test_user", "text": "test_message"}


@pytest.fixture
async def test_private_room():
return {"name": "test_private_room", "users": ["test_user"]}


@pytest.fixture
async def test_public_room():
return {"name": "test_public_room"}
Empty file added tests/unit/test_user.py
Empty file.
Loading