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
84 changes: 59 additions & 25 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,30 +1,64 @@
name: Test

on:
push:
branches: ["**"]
pull_request:
branches: ["**"]
push:
branches: [ "**" ]
pull_request:
branches: [ "**" ]

jobs:
pytest:
name: Pytest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v5

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install dependencies
working-directory: example/config-app
run: uv sync

- name: Run tests
working-directory: example/config-app
run: uv run pytest tests/ -v
tests:
name: Pytest
runs-on: ubuntu-latest

env:
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_DATABASE: database_app_test
DB_USERNAME: app
DB_PASSWORD: secret

steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v5

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"

- name: Start MySQL
run: docker compose -f docker-compose.test.yml up -d --wait

# ── fastapi_startkit package ──────────────────────────────────────────
- name: Install dependencies (fastapi_startkit)
working-directory: fastapi_startkit
run: uv sync --group dev

- name: Run tests (fastapi_startkit)
working-directory: fastapi_startkit
run: uv run pytest tests/ -v

# ── example/config-app ────────────────────────────────────────────────
- name: Install dependencies (config-app)
working-directory: example/config-app
run: uv sync

- name: Run tests (config-app)
working-directory: example/config-app
run: uv run pytest tests/ -v

# ── example/database-app ──────────────────────────────────────────────
- name: Install dependencies (database-app)
working-directory: example/database-app
run: uv sync

- name: Run tests (database-app)
working-directory: example/database-app
run: uv run pytest tests/ -v

- name: Stop MySQL
if: always()
run: docker compose -f docker-compose.test.yml down
36 changes: 36 additions & 0 deletions bin/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
set -e

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"

# ── Start MySQL via Docker Compose ────────────────────────────────────────────
echo "Starting test services..."
docker compose -f "$ROOT/docker-compose.yml" up -d --wait

# Ensure MySQL is torn down on exit (even if tests fail)
trap 'echo "Stopping test services..."; docker compose -f "$ROOT/docker-compose.test.yml" down' EXIT

# ── Common DB env vars (match docker-compose.test.yml) ───────────────────────
export DB_HOST=127.0.0.1
export DB_PORT=3306
export DB_DATABASE=database_app_test
export DB_USERNAME=app
export DB_PASSWORD=secret

echo ""
echo "============================================================"
echo " Running: fastapi_startkit package tests"
echo "============================================================"
(cd "$ROOT/fastapi_startkit" && uv run pytest)

echo ""
echo "============================================================"
echo " Running: example/config-app tests"
echo "============================================================"
(cd "$ROOT/example/config-app" && uv run pytest)

echo ""
echo "============================================================"
echo " Running: example/database-app tests"
echo "============================================================"
(cd "$ROOT/example/database-app" && uv run pytest)
15 changes: 15 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: database_app_test
MYSQL_USER: app
MYSQL_PASSWORD: secret
ports:
- "3306:3306"
healthcheck:
test: [ "CMD", "mysqladmin", "ping", "--silent" ]
interval: 5s
timeout: 5s
retries: 10
2 changes: 2 additions & 0 deletions example/database-app/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
APP_NAME="Fastapi starter Kit Database example app"

DB_HOST=localhost
DB_DATABASE=postgres
DB_USER=postgres
Expand Down
4 changes: 2 additions & 2 deletions example/database-app/.env.testing
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
APP_NAME="Masonite Testing"
APP_NAME="Fastapi starter Kit Database example app"
APP_ENV=testing

DB_HOST=127.0.0.1
Expand All @@ -7,4 +7,4 @@ DB_USERNAME=app
DB_PASSWORD=secret
DB_PORT=3306

LOG_CHANNEL=syslog
LOG_CHANNEL=terminal
2 changes: 2 additions & 0 deletions example/database-app/app/students/controllers/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def login():
pass
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,3 @@ async def register(request: StudentRegistrationRequest):
await profile.save()

return {"message": "Student registered successfully", "user_id": user.id}


def login():
pass
16 changes: 0 additions & 16 deletions example/database-app/docker-compose.yml

This file was deleted.

17 changes: 0 additions & 17 deletions example/database-app/docker/mysql/init.sql

This file was deleted.

1 change: 1 addition & 0 deletions example/database-app/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"aiomysql>=0.3.2",
"aiosqlite>=0.22.1",
"cryptography>=47.0.0",
"fastapi-startkit[database,fastapi,postgres]",
]
Expand Down
3 changes: 1 addition & 2 deletions example/database-app/pytest.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
[pytest]
asyncio_mode = auto
asyncio_default_fixture_loop_scope = function
asyncio_default_test_loop_scope = function
pythonpath = .

2 changes: 0 additions & 2 deletions example/database-app/routes/api.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
from fastapi_startkit.fastapi import Router

from app.students.controllers import auth_controller as student_auth
from app.http.controllers.auth_controller import AuthController

public = Router()

public.post("/register/student", student_auth.register)
public.post("/register/teacher", AuthController.register_teacher)
7 changes: 4 additions & 3 deletions example/database-app/routes/student.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from fastapi_startkit.fastapi import Router

from app.students.controllers import auth_controller
from app.students.controllers.registration import register
from app.students.controllers.auth import login

router = Router()

router.post("/students/register", auth_controller.register)
router.get("/students/login", auth_controller.login)
router.post("/students/register", register)
router.get("/students/login", login)
Empty file.
Empty file.
Empty file.
9 changes: 6 additions & 3 deletions example/database-app/tests/features/students/test_register.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from fastapi_startkit.fastapi.testing import HttpTestCase
from fastapi_startkit.masoniteorm.testing import RefreshDatabase

from app.models.user import User
from tests.test_case import TestCase, RefreshDatabase
from tests.test_case import TestCase


class TestRegister(TestCase, RefreshDatabase):
class TestRegister(TestCase, HttpTestCase, RefreshDatabase):
async def test_user_can_register(self):
response = await self.post("/students/register", json={
"name": "John Doe",
Expand Down Expand Up @@ -40,7 +43,7 @@ async def test_user_cannot_register_with_invalid_data(self):
})
assert response.status_code == 422

# name too short
# name too shorts
response = await self.post("/students/register", json={
"name": "J",
"email": "john@example.com",
Expand Down
Empty file.
20 changes: 20 additions & 0 deletions example/database-app/tests/features/teachers/test_register.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from fastapi_startkit.masoniteorm.testing import RefreshDatabase, DatabaseTransaction

from app.models.user import User
from tests.test_case import TestCase


class TestRegister(RefreshDatabase, TestCase):
async def test_register(self):
user = User(name="Teacher", email="teacher@example.com", password="password123", role="teacher")
await user.save()

found = await User.where("email", "teacher@example.com").first()
assert found is not None
assert found.role == "teacher"


class TestTableIsClean(DatabaseTransaction, TestCase):
async def test_table_is_clean(self):
users = await User.all()
assert len(users) == 0
79 changes: 9 additions & 70 deletions example/database-app/tests/test_case.py
Original file line number Diff line number Diff line change
@@ -1,74 +1,13 @@
import gc
import pytest
from httpx import AsyncClient, ASGITransport
from bootstrap.application import app
from abc import ABC
from typing import TYPE_CHECKING

class TestCase:
@pytest.fixture(autouse=True)
async def setup_client(self):
async with AsyncClient(
transport=ASGITransport(app=app.fastapi), base_url="http://test"
) as client:
self.client = client
yield
from fastapi_startkit.testing import TestCase as BaseTestCase

async def get(self, url, **kwargs):
return await self.client.get(url, **kwargs)
if TYPE_CHECKING:
from fastapi_startkit.application import Application

async def post(self, url, **kwargs):
return await self.client.post(url, **kwargs)

async def put(self, url, **kwargs):
return await self.client.put(url, **kwargs)

async def delete(self, url, **kwargs):
return await self.client.delete(url, **kwargs)


class RefreshDatabase:
migrated = False

@staticmethod
async def migrate_database():
from fastapi_startkit.masoniteorm.migrations import Migration

if not RefreshDatabase.migrated:
migration = Migration(migration_directory="databases/migrations")
await migration.fresh(ignore_fk=True)
RefreshDatabase.migrated = True

@pytest.fixture(autouse=True)
async def refresh_database(self):
await RefreshDatabase.migrate_database()

from fastapi_startkit.masoniteorm.models import Model
db_connection = Model.db_manager.connection(None)
original_engine = db_connection.conn

async with original_engine.connect() as conn:
transaction = await conn.begin()

original_commit = conn.sync_connection.commit
conn.sync_connection.commit = lambda: None

class PatchedEngine:
def connect(self):
return YieldConn()

class YieldConn:
async def __aenter__(self):
return conn

async def __aexit__(self, *args):
pass

db_connection.conn = PatchedEngine()

yield

db_connection.conn = original_engine
conn.sync_connection.commit = original_commit
await transaction.rollback()

await original_engine.dispose()
gc.collect()
class TestCase(BaseTestCase, ABC):
def get_application(self) -> 'Application':
from bootstrap.application import app
return app
Loading
Loading