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
8039c84
update toml for add test libs
Baptiste-Ferrand Jul 18, 2025
64cba5d
create a admin in mock data
Baptiste-Ferrand Jul 18, 2025
4bbb422
init the admin data
Baptiste-Ferrand Jul 18, 2025
0de3418
create conf test and some test for profil still in progress
Baptiste-Ferrand Jul 18, 2025
bea75f8
add test in dep cause fuck u
Baptiste-Ferrand Jul 22, 2025
a7c0354
update uv lock
Baptiste-Ferrand Jul 22, 2025
5147f18
create find all in repo inmemory for debug
Baptiste-Ferrand Jul 22, 2025
a71e89a
rename it cause -_-
Baptiste-Ferrand Jul 22, 2025
54d4f05
remove the older conftest cause is what shit
Baptiste-Ferrand Jul 22, 2025
4f1361b
remove odler test and create some test for profile (maybe new fiew more)
Baptiste-Ferrand Jul 22, 2025
41f9f95
update schema for optional data for coachprofilread
Baptiste-Ferrand Jul 23, 2025
ebef58f
create scenario profil test
Baptiste-Ferrand Jul 23, 2025
c2445d2
update inmemory group for using profils inmemory repo
Baptiste-Ferrand Jul 24, 2025
b59238d
fix the probleme when remove member groups 404
Baptiste-Ferrand Jul 24, 2025
4cde959
create 26 test for testing groups still in progress
Baptiste-Ferrand Jul 24, 2025
576ea85
remove info files... was a mistake
Baptiste-Ferrand Jul 24, 2025
3ddb341
remove s in profile files
Baptiste-Ferrand Jul 24, 2025
e94c35b
fix in router group form user leave groups convert is uuid (as string…
Baptiste-Ferrand Jul 24, 2025
9948426
fix endpoint remove "/" for create and get all exercices cause no need
Baptiste-Ferrand Jul 24, 2025
7dd53e4
update conftest for using fixture score session 'test_state'
Baptiste-Ferrand Jul 24, 2025
b94848a
create two more test for delete groups, and using the fixture from co…
Baptiste-Ferrand Jul 24, 2025
13b18aa
create exercices test using test_state fixture to have tokend and uid…
Baptiste-Ferrand Jul 24, 2025
0b8b6b9
create training test
Baptiste-Ferrand Jul 30, 2025
9bf6a63
fix diet service when update mealplan
Baptiste-Ferrand Jul 30, 2025
be9fd08
create diet test scenario
Baptiste-Ferrand Jul 30, 2025
158f218
create pipeline for testing executing when pr open update or reopen
Baptiste-Ferrand Jul 30, 2025
5a71cb4
test 3.12 version
Baptiste-Ferrand Jul 30, 2025
41a4ffa
downgrate requirement python version
Baptiste-Ferrand Jul 30, 2025
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
60 changes: 60 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Run Tests

on:
pull_request:
types: [opened, synchronize, reopened]
branches: [main, develop]

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.12'

- name: Cache pip dependencies
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-

- name: Create and activate virtual environment
run: |
python -m venv venv
source venv/bin/activate

- name: Install dependencies
run: |
source venv/bin/activate
pip install --upgrade pip
pip install .

- name: Run tests
env:
ENV: test
SECRET_KEY: test_secret_key
ACCESS_TOKEN_EXPIRE_MINUTES: 60
run: |
source venv/bin/activate
uvicorn src.main:app --host 0.0.0.0 --port 8000 --reload &
pytest src/entrypoints/api/tests/profile.py \
src/entrypoints/api/tests/group.py \
src/entrypoints/api/tests/exercise.py \
src/entrypoints/api/tests/training.py \
src/entrypoints/api/tests/diet.py \
-v \
--tb=short

- name: Test Summary
if: always()
run: |
echo "Tests completed!"
echo "Check the logs above for details."
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@
# PYTHON
__pycache__/

# Info file
tracknatrainapi.egg-info/
21 changes: 16 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
[project]
name = "tracknatrainapi"
version = "0.4.0"
requires-python = ">=3.13"
dependencies = [ "annotated-types==0.7.0", "anyio==4.9.0", "bcrypt==4.3.0", "boto3==1.37.37", "botocore==1.37.37", "cffi==1.17.1", "click==8.1.8", "cryptography==44.0.2", "dnspython==2.7.0", "ecdsa==0.19.1", "email-validator==2.2.0", "exceptiongroup==1.2.2", "fastapi==0.115.12", "greenlet==3.1.1", "h11==0.14.0", "idna==3.10", "jmespath==1.0.1", "passlib[bcrypt]>=1.7.4", "psycopg2-binary==2.9.10", "pyasn1==0.4.8", "pycparser==2.22", "pydantic==2.11.3", "pydantic-core==2.33.1", "python-dateutil==2.9.0.post0", "python-dotenv==1.1.0", "python-jose==3.4.0", "python-multipart==0.0.20", "rsa==4.9", "s3transfer==0.11.5", "six==1.17.0", "sniffio==1.3.1", "sqlalchemy==2.0.40", "starlette==0.46.2", "typing-extensions==4.13.2", "typing-inspection==0.4.0", "urllib3==2.4.0", "uvicorn==0.34.1",]
requires-python = ">=3.12"
dependencies = [ "annotated-types==0.7.0", "anyio==4.9.0", "bcrypt==4.3.0", "boto3==1.37.37", "botocore==1.37.37", "cffi==1.17.1", "click==8.1.8", "cryptography==44.0.2", "dnspython==2.7.0", "ecdsa==0.19.1", "email-validator==2.2.0", "exceptiongroup==1.2.2", "fastapi==0.115.12", "greenlet==3.1.1", "h11==0.14.0", "idna==3.10", "jmespath==1.0.1", "passlib[bcrypt]>=1.7.4", "psycopg2-binary==2.9.10", "pyasn1==0.4.8", "pycparser==2.22", "pydantic==2.11.3", "pydantic-core==2.33.1", "python-dateutil==2.9.0.post0", "python-dotenv==1.1.0", "python-jose==3.4.0", "python-multipart==0.0.20", "rsa==4.9", "s3transfer==0.11.5", "six==1.17.0", "sniffio==1.3.1", "sqlalchemy==2.0.40", "starlette==0.46.2", "typing-extensions==4.13.2", "typing-inspection==0.4.0", "urllib3==2.4.0", "uvicorn==0.34.1", "pytest>=7.0",
"pytest-asyncio>=0.20",
"httpx>=0.24",
"pytest-cov>=4.0",
"coverage>=6.0" ]

[project.optional-dependencies]
testing = [
"pytest>=7.0",
"pytest-asyncio>=0.20",
"httpx>=0.24",
"pytest-cov>=4.0",
"coverage>=6.0"
]


[build-system]
requires = [ "setuptools>=42", "wheel",]
build-backend = "setuptools.build_meta"

[tool.setuptools.packages.find]
include = [ "app*", "alembic*",]
4 changes: 2 additions & 2 deletions src/adapters/inmemory/repositories/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
from src.adapters.inmemory.repositories.profile import InMemoryProfileRepository

class InMemoryGroupRepository(GroupRepository):
def __init__(self):
def __init__(self, profile_repo: InMemoryProfileRepository):
self._groups: dict[UUID, DomainGroup] = {}
self._members: dict[UUID, List[UUID]] = {}
self._profile_repo = InMemoryProfileRepository()
self._profile_repo = profile_repo

def find_by_id(self, id: UUID) -> Optional[DomainGroup]:
return self._groups.get(id)
Expand Down
8 changes: 7 additions & 1 deletion src/adapters/inmemory/repositories/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@


class InMemoryProfileRepository(ProfileRepository):
def __init__(self):
def __init__(self, initial: list[DomainProfile] | None = None):
self._data: dict[UUID, DomainProfile] = {}
if initial:
for profile in initial:
self._data[profile.id] = profile

def find_by_email(self, email: str) -> Optional[DomainProfile]:
for profile in self._data.values():
Expand Down Expand Up @@ -41,3 +44,6 @@ def find_all_users(self) -> List[DomainProfile]:

def find_all_coachs(self) -> List[DomainProfile]:
return [p for p in self._data.values() if "coach" in (p.roles or [])]

def find_all(self) -> List[DomainProfile]:
return list(self._data.values())
22 changes: 20 additions & 2 deletions src/container.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
from uuid import uuid4, UUID

from src.domain.lib.security import BcryptPasswordHasher
from src.domain.services.profile import ProfileService
Expand All @@ -13,13 +14,30 @@ def __init__(self, env: str | None = None):
self.hasher = BcryptPasswordHasher()

if self.env in ("dev", "test"):
from src.domain.model.profile import Profile as DomainProfile
plain_pw = "123456789"
hashed_pw = self.hasher.hash(plain_pw)
admin = DomainProfile(
id=uuid4(),
email="admin@mail.fr",
password=hashed_pw,
name="Admin",
sex=None,
age=None,
contact=None,
pricing=None,
description=None,
legacy=False,
roles=["admin"],
created_at=None,
)
from src.adapters.inmemory.repositories.profile import InMemoryProfileRepository
from src.adapters.inmemory.repositories.group import InMemoryGroupRepository
from src.adapters.inmemory.repositories.training import InMemoryTrainingRepository
from src.adapters.inmemory.repositories.exercise import InMemoryExerciseRepository
from src.adapters.inmemory.repositories.diet import InMemoryDietRepository
self.profile_repo = InMemoryProfileRepository()
self.group_repo = InMemoryGroupRepository()
self.profile_repo = InMemoryProfileRepository(initial=[admin])
self.group_repo = InMemoryGroupRepository(self.profile_repo)
self.training_repo = InMemoryTrainingRepository()
self.exercise_repo = InMemoryExerciseRepository()
self.diet_repo = InMemoryDietRepository()
Expand Down
10 changes: 6 additions & 4 deletions src/domain/services/diet.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,14 +158,16 @@ def update_meal_plan(
) -> DomainMealPlan:
mp = self.get_meal_plan_by_id(plan_id)

domain_meals = [DomainMealItem(**m.model_dump()) for m in meals]
if not domain_meals:
updated_name = name if name is not None else mp.name
updated_meals = [DomainMealItem(**m.model_dump()) for m in meals] if meals is not None else mp.meals

if not updated_meals:
raise ValueError("Meal Plan must have at least one meal")

if name is not None:
mp.name = name
mp.name = updated_name
if meals is not None:
mp.meals = domain_meals
mp.meals = updated_meals
return self._repo.update_meal_plan(mp)

def delete_meal_plan(self, plan_id: UUID) -> None:
Expand Down
4 changes: 3 additions & 1 deletion src/domain/services/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ def remove_member(self, group_id: UUID, user_id: UUID) -> None:
if not group:
raise NotFoundError(f"Group with id {group_id} not found")

if user_id not in self._repo.list_members(group_id):
profiles = self._repo.list_members(group_id)
member_ids = [p.id for p in profiles]
if user_id not in member_ids:
raise NotFoundError(f"User with id {user_id} is not a member of group {group_id}")

self._repo.remove_member(group_id, user_id)
Expand Down
2 changes: 1 addition & 1 deletion src/entrypoints/api/deps/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def require_owner_or_admin(
if str(user_id) != str(profile_id) and "admin" not in roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Forbidden access"
detail="Access forbidden"
)
return user

Expand Down
4 changes: 2 additions & 2 deletions src/entrypoints/api/routers/exercise.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

router = APIRouter(prefix="/exercises", tags=["exercises"])

@router.post("/", response_model=ExerciseRead, status_code=201, dependencies=[Depends(require_roles("admin", "coach"))])
@router.post("", response_model=ExerciseRead, status_code=201, dependencies=[Depends(require_roles("admin", "coach"))])
def create_exercise(
dto: ExerciseCreate,
user=Depends(get_current_user)
Expand All @@ -30,7 +30,7 @@ def create_exercise(

return ExerciseRead.model_validate(exercise)

@router.get("/", response_model=List[ExerciseRead], dependencies=[Depends(get_current_user)])
@router.get("", response_model=List[ExerciseRead], dependencies=[Depends(get_current_user)])
def get_exercises(
user=Depends(get_current_user)
):
Expand Down
2 changes: 1 addition & 1 deletion src/entrypoints/api/routers/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def list_owner_groups(owner_id: UUID):
def leave_group(group_id: UUID, user=Depends(get_current_user)):
service = container.get_group_service()
try:
service.remove_member(group_id, user["sub"])
service.remove_member(group_id, UUID(user["sub"]))
except NotFoundError as e:
raise HTTPException(404, str(e))

14 changes: 7 additions & 7 deletions src/entrypoints/api/schemas/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ class ProfileRead(BaseModel):

class CoachProfileRead(BaseModel):
id: UUID
name: str
sex: str
age: int
contact: str
pricing: float
description: str
legacy: str
name: Optional[str] = None
sex: Optional[str] = None
age: Optional[int] = None
contact: Optional[str] = None
pricing: Optional[float] = None
description: Optional[str] = None
legacy: Optional[str] = None

model_config = {
"from_attributes": True
Expand Down
36 changes: 36 additions & 0 deletions src/entrypoints/api/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import pytest
import os
from httpx import AsyncClient, ASGITransport
from src.main import app
from src.container import Container
import pytest_asyncio



@pytest.fixture(scope="session", autouse=True)
def set_test_env():
os.environ["ENV"] = "test"
yield


@pytest.fixture(scope="module", autouse=True)
def container():
c = Container(env="test")

assert os.getenv("ENV") == "test", "L'environnement n'est pas correctement configuré sur 'test'."

return c


@pytest_asyncio.fixture
async def client(container):

transport = ASGITransport(app=app)

async with AsyncClient(transport=transport, base_url="http://testserver") as ac:
yield ac

@pytest.fixture(scope="session")
def test_state():

return {}
Loading