Skip to content

Commit 7be80bc

Browse files
Add unit test and more coverage (#56)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Refactor** - Restructured member and video functionality into `MemberService` and `VideoService` classes for improved modularity and separation of concerns. - **New Features** - Introduced enhanced video URL validation to ensure a minimum length. - **Tests** - Added comprehensive tests for video and member services, validating error handling, validation, and functionality. - **Chores** - Added pylint check using `poetry run pylint src` to the CircleCI configuration for static code analysis. - Updated imports and instantiation in the `main.py` file for improved efficiency. - Introduced a new test case `test_main_startup` in `test_main.py` for testing startup behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent be09a83 commit 7be80bc

File tree

21 files changed

+760
-215
lines changed

21 files changed

+760
-215
lines changed

.circleci/config.yml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ jobs:
3131
pkg-manager: poetry
3232
- run: poetry run isort . --check
3333
- run: poetry run black . --check
34-
- run: poetry run mypy -p src.bss_web_file_server
34+
- run: poetry run pylint src
35+
- run: poetry run mypy -p src
3536
verify-requirements:
3637
executor: python
3738
steps:
@@ -50,11 +51,22 @@ jobs:
5051
- github-cli/setup:
5152
version: 2.43.1
5253
- run: gh release create << pipeline.git.tag >> -t << pipeline.git.tag >> --generate-notes
54+
coverage:
55+
executor: python
56+
steps:
57+
- checkout
58+
- python/install-packages:
59+
pkg-manager: poetry
60+
- run: poetry run pytest --cov=src --cov-report=html --cov-fail-under=100
61+
- store_artifacts:
62+
path: htmlcov
5363
workflows:
5464
Build:
5565
jobs:
5666
- lint:
5767
name: Lint
68+
- coverage:
69+
name: Coverage
5870
- verify-requirements:
5971
name: Verify requirements.txt
6072
- python/test:

poetry.lock

Lines changed: 84 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ packages = [{include = "bss_web_file_server", from = "src"}]
88

99
[tool.poetry.dependencies]
1010
python = "^3.12"
11-
fastapi = {extras=["all"], version="0.110.0"}
12-
uvicorn = {extras = ["standard"], version = "0.28.0"}
11+
fastapi = {version="0.110.0", extras=["all"]}
12+
uvicorn = {version = "0.28.0", extras = ["standard"]}
1313
pillow = "10.2.0"
1414
pillow-avif-plugin = "1.4.3"
1515
python-multipart = "0.0.9"
@@ -25,6 +25,7 @@ types-Pillow = "10.2.0.20240311"
2525
httpx = "^0.23.0"
2626
pytest = "^8.0.0"
2727
pytest-mock = "^3.10.0"
28+
pytest-cov = "^4.1.0"
2829
mypy = "1.9.0"
2930

3031
[tool.isort]

src/bss_web_file_server/main.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
"""Main module for the FastAPI application."""
22

3+
from contextlib import asynccontextmanager
4+
35
from fastapi import FastAPI
46

57
from .routers import health, member, video
6-
from .services.member import create_member_base_path
7-
from .services.video import create_video_base_path
8+
from .services.member import MemberService
9+
from .services.video import VideoService
10+
11+
member_service = MemberService()
12+
video_service = VideoService()
13+
14+
15+
@asynccontextmanager
16+
async def lifespan(api: FastAPI): # pylint: disable=unused-argument
17+
"""Create the base paths for the video and member folders on startup."""
18+
video_service.create_base_path()
19+
member_service.create_base_path()
20+
yield
821

9-
app = FastAPI()
22+
23+
app = FastAPI(lifespan=lifespan)
1024

1125
app.include_router(health.router)
1226
app.include_router(video.router)
1327
app.include_router(member.router)
14-
15-
16-
@app.on_event("startup")
17-
async def startup_event():
18-
"""Create the base paths for the video and member folders on startup."""
19-
create_video_base_path()
20-
create_member_base_path()
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
""" A module for storing the video model. """
22

3+
from typing import Annotated
34
from uuid import UUID
45

6+
from annotated_types import Len
57
from pydantic import BaseModel
68

79

810
class Video(BaseModel):
911
"""A class for storing video data."""
1012

1113
id: UUID
12-
urls: list[str]
14+
urls: Annotated[list[str], Len(min_length=1)]
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
"""Health check endpoints."""
22

33
from fastapi import APIRouter
4+
from fastapi.responses import PlainTextResponse
45

56
router = APIRouter(tags=["Health"])
67

78

8-
@router.get("/health", response_model=str)
9+
@router.get("/health", response_class=PlainTextResponse)
910
async def health():
11+
"""Health check endpoint."""
1012
return "UP"
1113

1214

13-
@router.get("/ping", response_model=str)
15+
@router.get("/ping", response_class=PlainTextResponse)
1416
async def ping():
17+
"""Ping check endpoint."""
1518
return "PONG"

src/bss_web_file_server/routers/member.py

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,10 @@
66
from fastapi import APIRouter, Response, UploadFile, status
77

88
from ..models.member import Member
9-
from ..services.member import (
10-
create_folder_structure,
11-
create_profile_picture,
12-
to_id_path,
13-
update_symlink,
14-
)
9+
from ..services.member import MemberService
1510

1611
router = APIRouter(tags=["Member"], prefix="/api/v1/member")
12+
service: MemberService = MemberService()
1713

1814

1915
@router.post("", response_model=Member)
@@ -23,7 +19,7 @@ def create_member_folder(member: Member):
2319
:param member: Member object
2420
:return: 200 and the original member object
2521
"""
26-
create_folder_structure(member)
22+
service.create_folder_structure(member)
2723
return member
2824

2925

@@ -35,9 +31,9 @@ def update_member_folder(member: Member):
3531
:param member: Member object
3632
:return: 200 and the original member object
3733
"""
38-
if not to_id_path(member.id).exists():
34+
if not service.to_id_path(member.id).exists():
3935
return Response(status_code=status.HTTP_404_NOT_FOUND)
40-
update_symlink(member)
36+
service.update_symlink(member)
4137
return member
4238

4339

@@ -52,15 +48,15 @@ async def upload_member_picture(member_id: UUID, file: UploadFile):
5248
:param file: the image file
5349
:return: 200 and the original member_id
5450
"""
55-
if not to_id_path(member_id).exists():
56-
return Response(status_code=status.HTTP_404_NOT_FOUND)
5751
# pylint: disable=duplicate-code
52+
if not service.to_id_path(member_id).exists():
53+
return Response(status_code=status.HTTP_404_NOT_FOUND)
5854
if file.content_type is not None and not re.match("image/.+", file.content_type):
5955
return Response(
6056
content="Mime is not an image format",
6157
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
6258
)
63-
# pylint: enable=duplicate-code
6459
file_content = await file.read()
65-
create_profile_picture(file_content, member_id)
60+
# pylint: enable=duplicate-code
61+
service.create_profile_picture(file_content, member_id)
6662
return member_id

src/bss_web_file_server/routers/video.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,11 @@
55

66
from fastapi import APIRouter, Response, UploadFile, status
77

8-
from ..services.video import (
9-
Video,
10-
create_folder_structure,
11-
create_thumbnails,
12-
to_id_path,
13-
update_symlinks,
14-
)
8+
from ..models.video import Video
9+
from ..services.video import VideoService
1510

1611
router = APIRouter(tags=["Video"], prefix="/api/v1/video")
12+
service: VideoService = VideoService()
1713

1814

1915
@router.post("", response_model=Video)
@@ -23,7 +19,7 @@ def create_video_folder(video: Video):
2319
:param video: Video object
2420
:return: 200 and the original video object
2521
"""
26-
create_folder_structure(video)
22+
service.create_folder_structure(video)
2723
return video
2824

2925

@@ -35,9 +31,9 @@ def update_video_folder(video: Video):
3531
:param video: Video object
3632
:return: 200 and the original video object
3733
"""
38-
if not to_id_path(video.id).exists():
34+
if not service.to_id_path(video.id).exists():
3935
return Response(status_code=status.HTTP_404_NOT_FOUND)
40-
update_symlinks(video)
36+
service.update_symlinks(video)
4137
return video
4238

4339

@@ -52,15 +48,15 @@ async def upload_video_poster(video_id: UUID, file: UploadFile):
5248
:param file: the image file
5349
:return: 200 and the original video_id
5450
"""
55-
if not to_id_path(video_id).exists():
56-
return Response(status_code=status.HTTP_404_NOT_FOUND)
5751
# pylint: disable=duplicate-code
52+
if not service.to_id_path(video_id).exists():
53+
return Response(status_code=status.HTTP_404_NOT_FOUND)
5854
if file.content_type is not None and not re.match("image/.+", file.content_type):
5955
return Response(
6056
content="Mime is not an image format",
6157
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
6258
)
63-
# pylint: enable=duplicate-code
6459
file_content = await file.read()
65-
create_thumbnails(file_content, video_id)
60+
# pylint: enable=duplicate-code
61+
service.create_thumbnails(file_content, video_id)
6662
return video_id

0 commit comments

Comments
 (0)