feat: add the 'tests' module to the project
Some checks failed
/ test (push) Has been cancelled

This commit is contained in:
wilson 2026-04-07 07:55:57 +01:00
parent acadf77e2e
commit 2cae5d9445
6 changed files with 200 additions and 0 deletions

View file

@ -0,0 +1,24 @@
on:
push:
branches:
- "main"
pull_request:
types: [opened, synchronize, reopened]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install pytest
run: pip install pytest
- name: Run tests
working-directory: test
run: docker-compose up -d && pytest -v

59
docker-compose.test.yml Normal file
View file

@ -0,0 +1,59 @@
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: langlearn_test
POSTGRES_PASSWORD: testpassword
POSTGRES_DB: langlearn_test
tmpfs:
- /var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U langlearn_test"]
interval: 5s
timeout: 5s
retries: 10
storage:
image: minio/minio:latest
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: langlearn_test
MINIO_ROOT_PASSWORD: testpassword123
tmpfs:
- /data
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:9000/minio/health/live || exit 1"]
interval: 5s
timeout: 5s
retries: 10
api:
build: ./api
ports:
- "18000:8000"
environment:
DATABASE_URL: postgresql+asyncpg://langlearn_test:testpassword@db:5432/langlearn_test
JWT_SECRET: test-jwt-secret-not-for-production
ANTHROPIC_API_KEY: test-key
DEEPL_API_KEY: test-key
DEEPGRAM_API_KEY: test-key
GEMINI_API_KEY: test-key
ADMIN_USER_EMAILS: admin@test.com
API_BASE_URL: http://localhost:18000
STORAGE_ENDPOINT_URL: http://storage:9000
STORAGE_ACCESS_KEY: langlearn_test
STORAGE_SECRET_KEY: testpassword123
STORAGE_BUCKET: langlearn-test
depends_on:
db:
condition: service_healthy
storage:
condition: service_healthy
healthcheck:
test:
- "CMD-SHELL"
- "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8000/health')\""
interval: 5s
timeout: 5s
retries: 20
start_period: 10s

5
tests/README.md Normal file
View file

@ -0,0 +1,5 @@
# Tests
This module contains system-level tests for the Langauge Learning App, e.g. end-to-end (e2e) or API-level tests.
Because the whole system relies on multiple components (api, frontend, storage, db), it can be efficient to centralise these tests into a single module, rather than having each module re-create them. Additionally, it is not the responsibility of any single feature module to assert the correct behaviour of the entire cohort.

42
tests/conftest.py Normal file
View file

@ -0,0 +1,42 @@
"""
Session-scoped fixtures that spin up and tear down the test stack.
The test stack uses docker-compose.test.yml which:
- Runs on port 18000 (won't collide with the dev stack on 8000)
- Uses tmpfs for all storage (no data survives after `down`)
- Uses project name "langlearn-test" to stay isolated from dev containers
"""
import pathlib
import subprocess
import httpx
import pytest
PROJECT_ROOT = pathlib.Path(__file__).parent.parent
COMPOSE_FILE = str(PROJECT_ROOT / "docker-compose.test.yml")
COMPOSE_PROJECT = "langlearn-test"
API_BASE_URL = "http://localhost:18000"
def _compose(*args: str) -> None:
subprocess.run(
["docker", "compose", "-p", COMPOSE_PROJECT, "-f", COMPOSE_FILE, *args],
cwd=PROJECT_ROOT,
check=True,
)
@pytest.fixture(scope="session", autouse=True)
def docker_stack():
"""Bring the test stack up before the session; tear it down (including volumes) after."""
_compose("up", "--build", "--wait", "-d")
yield
_compose("down", "-v")
@pytest.fixture
def client() -> httpx.Client:
"""A plain httpx client pointed at the test API. Not authenticated."""
with httpx.Client(base_url=API_BASE_URL) as c:
yield c

11
tests/pyproject.toml Normal file
View file

@ -0,0 +1,11 @@
[project]
name = "language-learning-api-tests"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"pytest>=8.0.0",
"httpx>=0.28.1",
]
[tool.pytest.ini_options]
testpaths = ["."]

59
tests/test_auth.py Normal file
View file

@ -0,0 +1,59 @@
import httpx
import pytest
def test_register_creates_account(client: httpx.Client):
response = client.post(
"/auth/register",
json={"email": "newuser@example.com", "password": "securepassword123"},
)
assert response.status_code == 201
body = response.json()
assert body["email"] == "newuser@example.com"
assert "id" in body
def test_register_duplicate_email_returns_409(client: httpx.Client):
payload = {"email": "duplicate@example.com", "password": "securepassword123"}
client.post("/auth/register", json=payload)
response = client.post("/auth/register", json=payload)
assert response.status_code == 409
def test_login_returns_token(client: httpx.Client):
credentials = {"email": "loginuser@example.com", "password": "securepassword123"}
client.post("/auth/register", json=credentials)
response = client.post("/auth/login", json=credentials)
assert response.status_code == 200
body = response.json()
assert "access_token" in body
assert body["token_type"] == "bearer"
assert len(body["access_token"]) > 0
def test_login_wrong_password_returns_401(client: httpx.Client):
client.post(
"/auth/register",
json={"email": "wrongpass@example.com", "password": "correctpassword"},
)
response = client.post(
"/auth/login",
json={"email": "wrongpass@example.com", "password": "wrongpassword"},
)
assert response.status_code == 401
def test_login_unknown_email_returns_401(client: httpx.Client):
response = client.post(
"/auth/login",
json={"email": "nobody@example.com", "password": "doesntmatter"},
)
assert response.status_code == 401