Skip to content
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
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: CI

on:
pull_request:
branches:
- main
push:
branches:
- main

jobs:
backend-tests:
runs-on: ubuntu-latest

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

- name: Run backend tests in Docker
run: docker compose run --rm --no-deps --build bench-processor python -m pytest -q tests
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,18 @@ docker compose exec bench-processor python -m alembic -c alembic.ini revision --
Check alembic/versions newly created migration, then apply migration:
```
docker compose exec bench-processor python -m alembic -c alembic.ini upgrade head
```
```

## Tests

Run backend tests inside the Docker container:

```bash
docker compose run --rm --no-deps bench-processor python -m pytest -q tests
```

If you want to run tests against the latest image rebuild:

```bash
docker compose run --rm --no-deps --build bench-processor python -m pytest -q tests
```
4 changes: 3 additions & 1 deletion bench_processor/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ fastapi==0.115.0
uvicorn[standard]==0.30.6
sqlalchemy==2.0.32
psycopg[binary]==3.2.2
alembic==1.13.2
alembic==1.13.2
pytest==8.4.2
httpx==0.27.2
45 changes: 45 additions & 0 deletions bench_processor/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import sys
from pathlib import Path

import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool

PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))

from app.db import Base, get_db
from app.main import app


@pytest.fixture(scope="module")
def test_db_session() -> Session:
engine = create_engine(
"sqlite+pysqlite:///:memory:",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
testing_session_local = sessionmaker(bind=engine, autoflush=False, autocommit=False)
Base.metadata.create_all(bind=engine)

db = testing_session_local()
try:
yield db
finally:
db.close()
Base.metadata.drop_all(bind=engine)
engine.dispose()


@pytest.fixture(scope="module")
def client(test_db_session: Session):
def override_get_db():
yield test_db_session

app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as test_client:
yield test_client
app.dependency_overrides.clear()
5 changes: 5 additions & 0 deletions bench_processor/tests/test_health_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
def test_health_returns_ok(client) -> None:
response = client.get("/api/health")

assert response.status_code == 200
assert response.json() == {"status": "ok"}
40 changes: 40 additions & 0 deletions bench_processor/tests/test_log_stool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import pytest
from sqlalchemy.orm import Session

from app.db.models import StoolRecord, User


@pytest.fixture(scope="module", autouse=True)
def seed_default_user(test_db_session: Session) -> None:
test_db_session.add(User(user_id=1, name="Test User", email="test@example.com"))
test_db_session.commit()


def test_insert_stool_record_writes_row(client, test_db_session: Session) -> None:
payload = {"wetness": 0.67, "experience": 1.0}
response = client.post("/api/1/stool", json=payload)

assert response.status_code == 200
assert response.json() == "created"

rows = test_db_session.query(StoolRecord).all()
assert len(rows) == 1
assert rows[0].user_id == 1
assert rows[0].wetness_rating == 0.67
assert rows[0].experience_rating == 1.0


def test_insert_stool_record_returns_validation_error_when_user_missing(client) -> None:
payload = {"wetness": 0.67, "experience": 1.0}
response = client.post("/api/2/stool", json=payload)

assert response.status_code == 400
assert response.json() == {"message": "validation error: user_id 2 not found"}


def test_insert_stool_record_rejects_out_of_range_values(client) -> None:
payload = {"wetness": 1.2, "experience": -0.1}
response = client.post("/api/1/stool", json=payload)

assert response.status_code == 422
assert response.json()["message"].startswith("validation error:")
Loading