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

Update backup setup + tests #37

Merged
merged 9 commits into from
Oct 7, 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
13 changes: 8 additions & 5 deletions backend-app/app/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@


def authenticate_user(email: str, password: str, db: Session = Depends(get_db)):
user = get_user_by_email(db, email)
if not user:
return False
if not verify_password(password, user.hashed_password):
"""Authenticates a user using their email address and password."""
try:
user = get_user_by_email(db, email)
if not verify_password(password, user.hashed_password):
return False
return user
except Exception as e:
print(f"Error occurred while authenticating user: {e}")
return False
return user


def create_access_token(data: dict, expires_delta: timedelta | None = None):
Expand Down
6 changes: 3 additions & 3 deletions backend-app/app/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ class UserCreate(UserBase):
password: str


# TODO: Needed?
# class UserInDB(UserBase):
# hashed_password: str
# only for testing
class UserInDB(UserCreate):
hashed_password: str


class User(UserBase):
Expand Down
58 changes: 56 additions & 2 deletions backend-app/tests/unit/conftest.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
from io import BytesIO
from unittest.mock import MagicMock, patch

import pandas as pd
import pytest
from fastapi.testclient import TestClient
from jose import jwt
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from sqlalchemy.orm import Session, sessionmaker

from app.api.auth import get_current_active_admin, get_current_active_user, get_user_by_email
from app.main import app
from app.models import user as api_m
from app.models.database import User as db_user
from app.models.file_db import create_file_table_class, update_schema
from app.models.user import UserCreate
from app.models.user import UserCreate, UserInDB
from app.sql_db.crud import create_user, get_db, update_is_active, update_is_admin
from app.sql_db.database import Base
from app.sql_db.file_crud import create_update_table, insert_data
Expand Down Expand Up @@ -179,3 +181,55 @@ def files_bad_column():
)
}
return files


@pytest.fixture
def mock_db():
"""Fixture for mocking the database session."""
db = MagicMock(spec=Session)
return db


@pytest.fixture
def mock_user():
"""Fixture for mocking a user."""
user = UserInDB(email="[email protected]", password="test1", hashed_password="test1fake_hash")
return user


@pytest.fixture
def mock_get_user_by_email_success(mock_user):
"""Fixture for mocking get_user_by_email to return the mock user."""
mock_function = MagicMock()
mock_function.return_value = mock_user
return mock_function


@pytest.fixture
def mock_get_user_by_email_none():
"""Fixture for mocking get_user_by_email to retuern None (user not found)."""

mock_function = MagicMock()
mock_function.return_value = None

return mock_function


@pytest.fixture
def valid_token_payload():
return {"sub": "[email protected]"}


@pytest.fixture
def valid_token(valid_token_payload):
return jwt.encode(
valid_token_payload,
"SECRET",
algorithm="HS256", # TODO: secret and "HS256" should be configurable
) # TODO: secret and "HS256" should be configurable


@pytest.fixture
def mock_jwt_decode(valid_token_payload):
"""Mock jwt.decode to return a valid payload."""
return MagicMock(return_value=valid_token_payload)
197 changes: 197 additions & 0 deletions backend-app/tests/unit/test_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
from datetime import datetime, timedelta, timezone

import pytest
from fastapi.exceptions import HTTPException
from jose import JWTError, jwt

from app.api.auth import authenticate_user, create_access_token, get_current_user


def test_authenticate_user_success(mock_db, mock_get_user_by_email_success, mock_user, monkeypatch):
"""Test successful user authentication."""

def mock_verify_password(password, hashed_password):
return True

# Monkeypatching external dependencies
monkeypatch.setattr("app.api.auth.get_user_by_email", mock_get_user_by_email_success)
monkeypatch.setattr("app.api.auth.verify_password", mock_verify_password)

# Call the function
result = authenticate_user("[email protected]", "test1", db=mock_db)
print(result)

# Assert that authentication was successful and returns the user
assert result == mock_user


def test_authenticate_user_wrong_password(mock_db, mock_get_user_by_email_success, monkeypatch):
"""Test user authentication with wrong password."""

def mock_verify_password(password, hashed_password):
return False

# Monkeypatching external dependencies
monkeypatch.setattr("app.api.auth.get_user_by_email", mock_get_user_by_email_success)
monkeypatch.setattr("app.api.auth.verify_password", mock_verify_password)

# Call the function
result = authenticate_user("[email protected]", "wrong_password", db=mock_db)

# Assert that authentication fails
assert result is False


def test_authenticate_user_no_user_found(mock_db, mock_get_user_by_email_none, monkeypatch):
"""Test user authentication when no user is found."""

# Monkeypatching external dependencies
monkeypatch.setattr("app.api.auth.get_user_by_email", mock_get_user_by_email_none)

# Call the function
result = authenticate_user("[email protected]", "password", db=mock_db)

# Assert that authentication fails
assert result is False


def test_create_access_token(mock_user, monkeypatch):
TEST_SECRET_KEY = "SECRET"
monkeypatch.setattr("app.api.auth.SECRET_KEY", TEST_SECRET_KEY)

expires_delta = timedelta(minutes=1)
access_token = create_access_token(data={"sub": mock_user.email}, expires_delta=expires_delta)
decoded_token = jwt.decode(access_token, TEST_SECRET_KEY, algorithms=["HS256"])

# Assert the sub claim is correct
assert decoded_token["sub"] == mock_user.email

# Assert the expiration time is correct and within a reasonable range
exp_time = datetime.fromtimestamp(decoded_token["exp"], tz=timezone.utc)
now_time = datetime.now(timezone.utc)

# Token should expire in approximately the given expiration delta
assert now_time < exp_time <= (now_time + expires_delta + timedelta(seconds=2))


def test_create_access_token_default_expiration(mock_user, monkeypatch):
TEST_SECRET_KEY = "SECRET"
monkeypatch.setattr("app.api.auth.SECRET_KEY", TEST_SECRET_KEY)

# Call create_access_token without specifying expires_delta
access_token = create_access_token(data={"sub": mock_user.email})

decoded_token = jwt.decode(access_token, TEST_SECRET_KEY, algorithms=["HS256"])

# Assert the sub claim is correct
assert decoded_token["sub"] == mock_user.email

# Assert the default expiration is 15 minutes
exp_time = datetime.fromtimestamp(decoded_token["exp"], tz=timezone.utc)
now_time = datetime.now(timezone.utc)
assert now_time < exp_time <= (now_time + timedelta(minutes=15) + timedelta(seconds=2))


def test_create_access_token_expired_token(mock_user, monkeypatch):
TEST_SECRET_KEY = "SECRET"
monkeypatch.setattr("app.api.auth.SECRET_KEY", TEST_SECRET_KEY)

# Create an expired token by setting expires_delta to -1 minute
access_token = create_access_token(
data={"sub": mock_user.email}, expires_delta=timedelta(minutes=-1)
)
with pytest.raises(jwt.ExpiredSignatureError):
jwt.decode(access_token, TEST_SECRET_KEY, algorithms=["HS256"])


@pytest.mark.asyncio
async def test_get_current_user_valid_token(
mock_jwt_decode,
valid_token,
mock_user,
mock_db,
mock_get_user_by_email_success,
monkeypatch,
):
TEST_SECRET_KEY = "SECRET"

# Mock jwt.decode and get_user_by_email using monkeypatch
monkeypatch.setattr("app.api.auth.jwt.decode", mock_jwt_decode)
monkeypatch.setattr("app.api.auth.get_user_by_email", mock_get_user_by_email_success)
monkeypatch.setattr("app.api.auth.SECRET_KEY", TEST_SECRET_KEY)

# Call the function
user = await get_current_user(token=valid_token, db=mock_db)

# Assertions
assert user.email == mock_user.email
mock_jwt_decode.assert_called_once_with(valid_token, TEST_SECRET_KEY, algorithms=["HS256"])
mock_get_user_by_email_success.assert_called_once_with(
mock_db, email=mock_jwt_decode.return_value.get("sub")
)


@pytest.mark.asyncio
async def test_get_current_user_missing_email(mock_jwt_decode, valid_token, mock_db, monkeypatch):
TEST_SECRET_KEY = "SECRET"

# Mock jwt.decode to return a payload without "sub" (missing email)
mock_jwt_decode.return_value = {}

monkeypatch.setattr("app.api.auth.jwt.decode", mock_jwt_decode)
monkeypatch.setattr("app.api.auth.SECRET_KEY", TEST_SECRET_KEY)

# Expect the function to raise an HTTPException when email is missing
with pytest.raises(HTTPException) as exc_info:
await get_current_user(token=valid_token, db=mock_db)

assert exc_info.value.status_code == 401
assert exc_info.value.detail == "Could not validate credentials"
mock_jwt_decode.assert_called_once_with(valid_token, TEST_SECRET_KEY, algorithms=["HS256"])


@pytest.mark.asyncio
async def test_get_current_user_jwt_error(mock_jwt_decode, valid_token, mock_db, monkeypatch):
TEST_SECRET_KEY = "SECRET"

# Mock jwt.decode to raise JWTError
mock_jwt_decode.side_effect = JWTError

monkeypatch.setattr("app.api.auth.jwt.decode", mock_jwt_decode)
monkeypatch.setattr("app.api.auth.SECRET_KEY", TEST_SECRET_KEY)

# Expect the function to raise an HTTPException when JWTError occurs
with pytest.raises(HTTPException) as exc_info:
await get_current_user(token=valid_token, db=mock_db)

assert exc_info.value.status_code == 401
assert exc_info.value.detail == "Could not validate credentials"
mock_jwt_decode.assert_called_once_with(valid_token, TEST_SECRET_KEY, algorithms=["HS256"])


@pytest.mark.asyncio
async def test_get_current_user_user_not_found(
mock_jwt_decode,
valid_token_payload,
valid_token,
mock_db,
mock_get_user_by_email_none,
monkeypatch,
):
TEST_SECRET_KEY = "SECRET"

# Mock jwt.decode to return a valid payload with email
mock_jwt_decode.return_value = valid_token_payload

monkeypatch.setattr("app.api.auth.jwt.decode", mock_jwt_decode)
monkeypatch.setattr("app.api.auth.get_user_by_email", mock_get_user_by_email_none)
monkeypatch.setattr("app.api.auth.SECRET_KEY", TEST_SECRET_KEY)

# Expect the function to raise an HTTPException when user is not found
with pytest.raises(HTTPException) as exc_info:
await get_current_user(token=valid_token, db=mock_db)

assert exc_info.value.status_code == 401
assert exc_info.value.detail == "Could not validate credentials"
mock_jwt_decode.assert_called_once_with(valid_token, TEST_SECRET_KEY, algorithms=["HS256"])
mock_get_user_by_email_none.assert_called_once_with(mock_db, email=valid_token_payload["sub"])
6 changes: 3 additions & 3 deletions backend-app/tests/unit/test_crud.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import pytest
from unittest.mock import MagicMock
from sqlalchemy import create_engine
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import sessionmaker
from app.sql_db import crud

from app.models.database import User as db_user
from app.models.user import UserCreate
from app.sql_db import crud
from app.sql_db.database import Base
from app.models.user import UserCreate, User


@pytest.fixture(scope="session")
Expand Down
2 changes: 0 additions & 2 deletions backend-app/tests/unit/test_filedb_crud.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import logging

import pytest

from app.sql_db import file_crud


Expand Down
2 changes: 0 additions & 2 deletions backend-app/tests/unit/test_users.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from fastapi.testclient import TestClient
from app.api import users
from app.sql_db import crud


Expand Down
Loading