Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
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
63 changes: 62 additions & 1 deletion .github/workflows/python-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,65 @@ jobs:

- name: Run isort
working-directory: backend
run: uv run isort . --check-only --diff
run: uv run isort . --check-only --diff

run-tests:
runs-on: ubuntu-latest
container: node:20-bookworm-slim
needs:
- run-checks

steps:
- uses: actions/checkout@v6

- uses: actions/setup-python@v6
with:
python-version-file: "backend/.python-version"

- name: Setup UV
uses: astral-sh/setup-uv@v7

- name: Install dependencies
working-directory: backend
run: uv sync --locked --all-extras --dev

- name: Run python tests
working-directory: backend
run: uv run pytest --cov=backlog_app --cov-report=xml:coverage.xml
env:
BACKLOG__DB__CONNECTION__USERNAME: test
BACKLOG__DB__CONNECTION__PASSWORD: test
BACKLOG__DB__CONNECTION__NAME: backlog
BACKLOG__DB__CONNECTION__HOST: localhost
BACKLOG__DB__CONNECTION__PORT: 5432
BACKLOG__SUPERUSER__EMAIL: admin@site.com
BACKLOG__SUPERUSER__PASSWORD: admin
BACKLOG__ACCESS_TOKEN_DB__RESET_PASSWORD_TOKEN_SECRET: secret1
BACKLOG__ACCESS_TOKEN_DB__VERIFICATION_TOKEN_SECRET: secret2

- name: Upload artefacts
uses: actions/upload-artifact@v4
with:
name: coverage-reports
path: |
coverage.xml
retention-days: 1

upload-report-to-codcov:
runs-on: ubuntu-latest
needs:
- run-tests
steps:
- uses: actions/checkout@v4

- name: Download artifact
uses: actions/download-artifact@v4
with:
name: coverage-reports

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
fail_ci_if_error: 'true'
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ repos:
rev: 25.12.0
hooks:
- id: black
files: ^backend/app/.*\.py$
files: ^backend/
args:
- --diff

- repo: https://github.com/pycqa/isort
rev: 7.0.0
hooks:
- id: isort
files: ^backend/app/.*\.py$
files: ^backend/
args:
- --diff
14 changes: 7 additions & 7 deletions backend/backlog_app/_helpers/create_super_user.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import asyncio
import contextlib

from config import settings
from dependencies.authentification.user_manager import get_user_manager
from dependencies.authentification.users import get_user_db
from models import User
from schemas.user import UserCreate
from servicies.authentification import UserManager
from storages.database import get_async_session
from backlog_app.config import settings
from backlog_app.dependencies.authentification.user_manager import get_user_manager
from backlog_app.dependencies.authentification.users import get_user_db
from backlog_app.models import User
from backlog_app.schemas.user import UserCreate
from backlog_app.servicies.authentification import UserManager
from backlog_app.storages.database import get_async_session

get_async_session_context = contextlib.asynccontextmanager(get_async_session)
get_user_db_context = contextlib.asynccontextmanager(get_user_db)
Expand Down
2 changes: 1 addition & 1 deletion backend/backlog_app/api/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
async def create_movie(
db: AsyncSession, movie_in: MovieCreate, user: User
) -> MovieRead:
movie = Movie(**movie_in.model_dump(), user_id=user.id)
movie = Movie(**movie_in.model_dump(), user=user)
db.add(movie)
await db.commit()
await db.refresh(movie)
Expand Down
2 changes: 1 addition & 1 deletion backend/backlog_app/api/view/movie_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ async def add_movie(


@router.get("/", response_model=List[MovieRead])
async def list_movies(
async def get_movie_list(
db: Annotated[AsyncSession, Depends(get_async_session)],
user: Annotated[User, Depends(current_active_user)],
only_mine: bool = False,
Expand Down
17 changes: 9 additions & 8 deletions backend/backlog_app/schemas/movie.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from datetime import datetime
from typing import Annotated

from pydantic import AnyHttpUrl, BaseModel, Field
from annotated_types import Len
from pydantic import BaseModel, Field


class MovieBase(BaseModel):
title: str
description: str
title: Annotated[str, Len(min_length=3, max_length=255)]
description: Annotated[str, Len(min_length=20, max_length=1000)]
year: int
rating: float
watch_link: str | None = None
Expand All @@ -14,15 +16,14 @@ class MovieBase(BaseModel):


class MovieCreate(MovieBase):
title: str
description: str | None = None
description: Annotated[str, Len(min_length=20, max_length=1000)] | None = None
year: int | None = None
rating: float | None = Field(default=None, ge=1.0, le=10.0)


class MovieUpdate(MovieBase):
title: str | None = None
description: str | None = None
title: Annotated[str, Len(min_length=3, max_length=255)] | None = None
description: Annotated[str, Len(min_length=20, max_length=1000)] | None = None
year: int | None = None
watched: bool | None = None
rating: float | None = Field(default=None, ge=1.0, le=10.0)
Expand All @@ -31,7 +32,7 @@ class MovieUpdate(MovieBase):
class MovieRead(MovieBase):
id: int
user: str
description: str | None
description: Annotated[str, Len(min_length=20, max_length=1000)] | None
year: int | None
watched: bool
rating: float | None
Expand Down
6 changes: 4 additions & 2 deletions backend/backlog_app/schemas/user.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import uuid
from typing import Annotated

from annotated_types import Len
from fastapi_users import schemas


Expand All @@ -8,8 +10,8 @@ class UserRead(schemas.BaseUser[uuid.UUID]):


class UserCreate(schemas.BaseUserCreate):
pass
password: Annotated[str, Len(min_length=8)]


class UserUpdate(schemas.BaseUserUpdate):
pass
password: Annotated[str, Len(min_length=8)]
35 changes: 35 additions & 0 deletions backend/codecov.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
codecov:
require_ci_to_pass: true
branch: main

coverage:
precision: 2
round: down
range: 70..80
status:
project:
default:
target: 80%
threshold: 5%
if_ci_failed: error
patch:
default:
target: 80%
threshold: 5%
informational: true

comment:
layout: "diff, flags, files"
behavior: default
require_changes: false
require_base: false
require_head: true
hide_project_coverage: false

parsers:
gcov:
branch_detection:
conditional: yes
loop: yes
method: no
macro: no
7 changes: 7 additions & 0 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,14 @@ dependencies = [

[dependency-groups]
dev = [
"aiosqlite>=0.22.1",
"black>=26.1.0",
"coverage>=7.13.3",
"isort>=7.0.0",
"pre-commit>=4.5.1",
"pytest>=9.0.2",
"pytest-asyncio>=1.3.0",
"pytest-cov>=7.0.0",
]

[tool.black]
Expand All @@ -31,3 +36,5 @@ line-length = 88
[tool.isort]
profile = "black"

[tool.pytest.ini_options]
asyncio_mode = "auto"
Empty file added backend/tests/__init__.py
Empty file.
87 changes: 87 additions & 0 deletions backend/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import contextlib
import os
from typing import Any, AsyncGenerator, Generator

import pytest
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine

from backlog_app._helpers.create_super_user import create_user
from backlog_app.api.crud import create_movie, delete_movie
from backlog_app.dependencies.authentification.user_manager import get_user_manager
from backlog_app.dependencies.authentification.users import get_user_db
from backlog_app.models import Base, Movie, User
from backlog_app.schemas.movie import MovieCreate, MovieRead
from backlog_app.schemas.user import UserCreate

DB_PATH = "test.db"
DATABASE_URL = f"sqlite+aiosqlite:///{DB_PATH}"

engine_test = create_async_engine(DATABASE_URL, echo=False)
AsyncSessionTest = async_sessionmaker(
engine_test,
expire_on_commit=False,
)


@pytest.fixture(scope="session")
async def init_db():
if os.path.exists(DB_PATH):
os.remove(DB_PATH)

async with engine_test.begin() as connection:
await connection.run_sync(Base.metadata.create_all)
yield

if os.path.exists(DB_PATH):
os.remove(DB_PATH)


@pytest.fixture
async def session(init_db):
async with AsyncSessionTest() as session:
yield session


@pytest.fixture
async def user_test(session) -> AsyncGenerator[User, None]:
get_user_db_context = contextlib.asynccontextmanager(lambda: get_user_db(session))
get_user_manager_context = contextlib.asynccontextmanager(get_user_manager)

user_create = UserCreate(
email="test_user@test.com",
password="testpassword",
is_active=True,
is_superuser=False,
is_verified=True,
)

async with get_user_db_context() as user_db:
async with get_user_manager_context(user_db) as user_manager:
user = await create_user(user_manager=user_manager, user_create=user_create)
yield user
await user_manager.delete(user)


def build_movie_create(
title: str, rating: float, watch_link: str, description: str
) -> MovieCreate:
return MovieCreate(
title=title,
description=description,
rating=rating,
imdb_id=123456789,
watch_link=watch_link,
)


@pytest.fixture
async def movie(session, user_test) -> AsyncGenerator[MovieRead, None]:
title = "Interstellar"
description = "Interstellar" * 20
rating = 9.5
watch_link = "https://example.com"
movie_in = build_movie_create(title, rating, watch_link, description)

movie = await create_movie(session, movie_in, user_test)
yield movie
await delete_movie(session, movie.id, user_test)
Empty file.
38 changes: 38 additions & 0 deletions backend/tests/test_api/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from typing import Generator

import pytest
from starlette.testclient import TestClient

from backlog_app.main import app

TEST_USERNAME = "test_user@example.com"
TEST_PASSWORD = "testuser"


@pytest.fixture
def client():
return TestClient(app)


@pytest.fixture
def access_token(client) -> str:
response = client.post(
"/api/auth/login",
data={
"username": TEST_USERNAME,
"password": TEST_PASSWORD,
},
headers={
"Content-Type": "application/x-www-form-urlencoded",
},
)

assert response.status_code == 200
return response.json()["access_token"]


@pytest.fixture
def auth_client(access_token: str) -> Generator[TestClient, None, None]:
with TestClient(app) as client:
client.headers.update({"Authorization": f"Bearer {access_token}"})
yield client
7 changes: 7 additions & 0 deletions backend/tests/test_api/test_main_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from fastapi import status
from fastapi.testclient import TestClient


def test_root(client: TestClient) -> None:
response = client.get("/")
assert response.status_code == status.HTTP_200_OK, response.text
Loading
Loading