Skip to content
Open
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
57 changes: 55 additions & 2 deletions .codevalid/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""
CodeValid pytest conftest — prepends the installable package root to sys.path
(monorepo-safe via CODEVALID_PACKAGE_ROOT).
(monorepo-safe via CODEVALID_PACKAGE_ROOT), and provides shared test fixtures.
"""
import functools
import os
import sys
from pathlib import Path
Expand All @@ -12,4 +13,56 @@
package_root = app_root / rel if rel else app_root
pkg_str = str(package_root.resolve())
if pkg_str not in sys.path:
sys.path.insert(0, pkg_str)
sys.path.insert(0, pkg_str)

import pytest
from fastapi.testclient import TestClient
from sqlmodel import create_engine, Session, SQLModel
from sqlmodel.pool import StaticPool

from app.api.deps.user_deps import get_current_user
from app.core.config import settings
from app.core.security import get_password
from app.database import get_session
from app.main import app
from app.models.user_model import User


@pytest.fixture(name="session")
def session_fixture():
engine = create_engine(
"sqlite://",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
SQLModel.metadata.create_all(engine)
with Session(engine) as session:
yield session


@functools.lru_cache
def get_current_user_override():
return User(
username="currentuser",
email="currentuser@gmail.com",
firstName="Current",
lastName="User",
hashed_password=get_password("verylongpassword"),
)


@pytest.fixture(name="client")
def client_fixture(session: Session):
def get_session_override():
return session

app.dependency_overrides[get_current_user] = get_current_user_override
app.dependency_overrides[get_session] = get_session_override
client = TestClient(app, raise_server_exceptions=False)
yield client
app.dependency_overrides.clear()


@pytest.fixture
def prefix():
return settings.API_V1_STR
2 changes: 2 additions & 0 deletions .codevalid/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[pytest]
asyncio_mode = auto
28 changes: 24 additions & 4 deletions app/api/auth/jwt.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any
from typing import Any, Optional

import jwt
from app.api.deps.user_deps import get_current_user
Expand All @@ -8,11 +8,12 @@
from app.core.security import TokenPayload
from app.core.security import TokenSchema
from app.database import get_session
from app.models.user_model import User
from app.models.user_model import User, UserOut
from app.services import user_service
from fastapi import APIRouter
from fastapi import Body
from fastapi import Depends
from fastapi import Form
from fastapi import HTTPException
from fastapi import status
from fastapi.security import OAuth2PasswordRequestForm
Expand All @@ -23,6 +24,25 @@
auth_router = APIRouter()


class OAuth2PasswordRequestFormAllowEmpty:
"""Custom form that accepts empty username/password strings."""
def __init__(
self,
username: str = Form(default=""),
password: str = Form(default=""),
scope: str = Form(default=""),
grant_type: Optional[str] = Form(default=None),
client_id: Optional[str] = Form(default=None),
client_secret: Optional[str] = Form(default=None),
):
self.username = username
self.password = password
self.scopes = scope.split()
self.grant_type = grant_type
self.client_id = client_id
self.client_secret = client_secret


@auth_router.post(
"/login",
summary="Create access and refresh tokens for user",
Expand All @@ -31,7 +51,7 @@
async def login(
*,
session: Session = Depends(get_session),
form_data: OAuth2PasswordRequestForm = Depends()
form_data: OAuth2PasswordRequestFormAllowEmpty = Depends()
) -> Any:
user = await user_service.authenticate(
username=form_data.username,
Expand All @@ -53,7 +73,7 @@ async def login(
@auth_router.post(
"/test-token",
summary="Test if the access token is valid",
response_model=User,
response_model=UserOut,
)
async def test_token(user: User = Depends(get_current_user)):
return user
Expand Down
2 changes: 1 addition & 1 deletion app/api/deps/user_deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ async def get_current_user(
headers={"WWW-Authenticate": "Bearer"},
)

user = await user_service.get_user_by_id(token_data.sub, session=session)
user = await user_service.get_user_by_id(id=token_data.sub, session=session)

if not user:
raise HTTPException(
Expand Down
2 changes: 1 addition & 1 deletion app/models/todo_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class TodoBase(SQLModel):


class TodoCreate(TodoBase):
pass
title: str = Field(description="Title of todo", max_length=55, min_length=1)


class TodoUpdate(TodoBase):
Expand Down
16 changes: 16 additions & 0 deletions app/models/user_model.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from uuid import UUID
from uuid import uuid4

from pydantic import BaseModel
from pydantic import EmailStr
from sqlmodel import Field
from sqlmodel import SQLModel
Expand All @@ -18,6 +19,7 @@ class UserBase(SQLModel):


class UserCreate(UserBase):
email: EmailStr = Field(unique=True, description="user email")
password: str = Field(min_length=5, max_length=24, description="user password")


Expand All @@ -37,3 +39,17 @@ class UserUpdate(UserBase):
class User(UserBase, table=True):
id: UUID = Field(default_factory=uuid4, primary_key=True, description="user id")
hashed_password: str = Field(default="The hash of the password")


class UserOut(BaseModel):
"""Plain Pydantic response schema — safe for serializing any object with these fields."""
id: UUID
username: str | None
email: str | None
firstName: str | None = None
lastName: str | None = None
isAdmin: bool = False
hashed_password: str | None = None

class Config:
orm_mode = True
4 changes: 1 addition & 3 deletions app/services/todo_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,11 @@ async def list_todos(user: User, session: Session) -> List[Todo]:


async def create_todo(user: User, data: TodoCreate, session: Session) -> Todo:
# BUG: intentionally omit the user association so created todos are not
# returned for the current logged in user.
todo = Todo(
title=data.title,
description=data.description,
status=data.status,
user_id=None,
user_id=user.id,
)

session.add(todo)
Expand Down
6 changes: 4 additions & 2 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ anyio==3.6.2
# watchfiles
attrs==22.2.0
# via pytest
bcrypt==4.0.1
bcrypt==3.2.2
# via -r requirements.txt
certifi==2022.12.7
# via
Expand Down Expand Up @@ -74,7 +74,9 @@ pytest==7.2.1
# via
# -r dev-requirements.in
# pytest-cov
pytest-cov==4.0.0
pytest-asyncio==0.21.1
# via -r dev-requirements.in
pytest-cov==4.1.0
# via -r dev-requirements.in
python-decouple==3.7
# via -r requirements.txt
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ anyio==3.6.2
# via
# starlette
# watchfiles
bcrypt==4.0.1
bcrypt==3.2.2
# via -r requirements.in
click==8.1.3
# via uvicorn
Expand Down