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
24 changes: 24 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
version: 2
updates:
- package-ecosystem: "uv"
directory: "/"
schedule:
interval: "weekly"
groups:
python-minor-patch:
update-types:
- "minor"
- "patch"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
groups:
actions:
update-types:
- "minor"
- "patch"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v7
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v9.0.0
- run: uv sync
- run: uv run ruff check .
- run: uv run ruff format --check .

test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v7
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v9.0.0
- run: uv sync
- run: uv run pytest

Expand All @@ -48,8 +48,8 @@ jobs:
env:
DATABASE_URL: postgresql+asyncpg://postgres:postgres@localhost:5432/criticalbit_auth
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v7
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v9.0.0
- run: uv sync
- run: uv run alembic upgrade head
- run: uv run alembic downgrade base
Expand Down
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
fail_fast: true
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.6
rev: v0.16.0
hooks:
- id: ruff
- id: ruff-check
args: [--fix]
- id: ruff-format
- repo: local
Expand Down
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.12
3.14
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
FROM python:3.12-slim
FROM python:3.14-slim

WORKDIR /app

# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
COPY --from=ghcr.io/astral-sh/uv:0.11.32 /uv /usr/local/bin/uv

# Copy dependency files
COPY pyproject.toml uv.lock* ./
Expand Down
4 changes: 2 additions & 2 deletions app/auth/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

async def get_user_db(
session: AsyncSession = Depends(get_async_session),
) -> AsyncGenerator[SQLAlchemyUserDatabase, None]:
) -> AsyncGenerator[SQLAlchemyUserDatabase]:
yield SQLAlchemyUserDatabase(session, User, OAuthAccount)


Expand Down Expand Up @@ -218,7 +218,7 @@ async def on_after_request_verify(self, user: User, token: str, request=None):

async def get_user_manager(
user_db: SQLAlchemyUserDatabase = Depends(get_user_db),
) -> AsyncGenerator[UserManager, None]:
) -> AsyncGenerator[UserManager]:
yield UserManager(user_db)


Expand Down
2 changes: 1 addition & 1 deletion app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def cors_origin_regex(self) -> str | None:
return r"https://([a-z0-9-]+\.)?criticalbit\.gg"

@model_validator(mode="after")
def validate_production_settings(self) -> "Settings":
def validate_production_settings(self) -> Settings:
if not self.is_development:
weak_secrets = {"change-me-in-production", "dev-secret-key-change-in-production", ""}
if self.secret_key in weak_secrets or len(self.secret_key) < 32:
Expand Down
2 changes: 1 addition & 1 deletion app/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def _pool_kwargs(database_url: str) -> dict:
)


async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
async def get_async_session() -> AsyncGenerator[AsyncSession]:
"""Dependency that provides an async database session."""
async with async_session_maker() as session:
yield session
2 changes: 1 addition & 1 deletion app/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class User(SQLAlchemyBaseUserTableUUID, Base):
# DELETEs and rely on the DB's ``ON DELETE CASCADE`` instead, which
# is true in prod (Postgres) but false in the SQLite test harness
# without ``PRAGMA foreign_keys=ON``, masking regressions.
oauth_accounts: Mapped[list["OAuthAccount"]] = relationship( # noqa: F821
oauth_accounts: Mapped[list[OAuthAccount]] = relationship( # noqa: F821
"OAuthAccount",
lazy="joined",
cascade="all, delete",
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "criticalbit-auth-api"
version = "0.1.0"
description = "Shared authentication service for criticalbit.gg — user management, JWT issuance, SSO"
readme = "README.md"
requires-python = ">=3.12"
requires-python = ">=3.14"
dependencies = [
"fastapi>=0.135.0",
"uvicorn[standard]>=0.41.0",
Expand Down Expand Up @@ -43,7 +43,7 @@ testpaths = ["tests"]
asyncio_default_fixture_loop_scope = "function"

[tool.ruff]
target-version = "py312"
target-version = "py314"
line-length = 100

[tool.ruff.lint]
Expand Down
10 changes: 5 additions & 5 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ async def setup_database():
await conn.run_sync(Base.metadata.drop_all)


async def override_get_async_session() -> AsyncGenerator[AsyncSession, None]:
async def override_get_async_session() -> AsyncGenerator[AsyncSession]:
async with async_session_maker() as session:
yield session

Expand All @@ -36,7 +36,7 @@ async def override_get_async_session() -> AsyncGenerator[AsyncSession, None]:


@pytest.fixture
async def client() -> AsyncGenerator[AsyncClient, None]:
async def client() -> AsyncGenerator[AsyncClient]:
"""Async HTTP client for testing."""
async with AsyncClient(
transport=ASGITransport(app=app),
Expand All @@ -46,7 +46,7 @@ async def client() -> AsyncGenerator[AsyncClient, None]:


@pytest.fixture
async def session() -> AsyncGenerator[AsyncSession, None]:
async def session() -> AsyncGenerator[AsyncSession]:
"""Direct database session for test setup."""
async with async_session_maker() as session:
yield session
Expand Down Expand Up @@ -104,7 +104,7 @@ def superuser() -> User:


@pytest.fixture
async def auth_client(client: AsyncClient, test_user: User) -> AsyncGenerator[AsyncClient, None]:
async def auth_client(client: AsyncClient, test_user: User) -> AsyncGenerator[AsyncClient]:
"""Client that authenticates as test_user via dependency override."""
app.dependency_overrides[current_active_user] = lambda: test_user
try:
Expand All @@ -114,7 +114,7 @@ async def auth_client(client: AsyncClient, test_user: User) -> AsyncGenerator[As


@pytest.fixture
async def admin_client(client: AsyncClient, admin_user: User) -> AsyncGenerator[AsyncClient, None]:
async def admin_client(client: AsyncClient, admin_user: User) -> AsyncGenerator[AsyncClient]:
"""Client that authenticates as admin_user via dependency override."""
app.dependency_overrides[current_active_user] = lambda: admin_user
try:
Expand Down
24 changes: 9 additions & 15 deletions tests/test_cookie_names.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
_session_maker = async_sessionmaker(_engine, class_=AsyncSession, expire_on_commit=False)


async def _override_session() -> AsyncGenerator[AsyncSession, None]:
async def _override_session() -> AsyncGenerator[AsyncSession]:
async with _session_maker() as session:
yield session

Expand Down Expand Up @@ -74,7 +74,7 @@ def _set_cookies_by_name(headers: list[str]) -> dict[str, dict[str, str]]:


@pytest.fixture
async def shared_db(monkeypatch: pytest.MonkeyPatch) -> AsyncGenerator[None, None]:
async def shared_db(monkeypatch: pytest.MonkeyPatch) -> AsyncGenerator[None]:
"""Create tables on the shared in-memory DB and wire overrides/patches.

Also resets the process-wide slowapi rate limiter state so per-test
Expand Down Expand Up @@ -110,7 +110,7 @@ async def shared_db(monkeypatch: pytest.MonkeyPatch) -> AsyncGenerator[None, Non


@pytest.fixture
async def api(shared_db: None) -> AsyncGenerator[AsyncClient, None]:
async def api(shared_db: None) -> AsyncGenerator[AsyncClient]:
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://test",
Expand Down Expand Up @@ -367,16 +367,10 @@ def test_oauth_csrf_cookie_subprocess() -> None:

from app.main import app # noqa: E402

# The unified provider router uses a dynamic path
# (/auth/{provider_name}/authorize). Match the templated form OR a
# statically-mounted equivalent so this test survives both wirings.
has_authorize = any(
getattr(r, "path", "") in (
"/auth/google/authorize",
"/auth/{provider_name}/authorize",
)
for r in app.routes
)
# Registration is proven behaviorally (the request below returning
# 200 rather than 404). FastAPI >=0.137 materializes included
# routers' routes lazily, so scanning `app.routes` at import time
# no longer reflects what is actually registered and served.

async def _run():
async with AsyncClient(
Expand All @@ -388,7 +382,6 @@ async def _run():

status, headers = asyncio.run(_run())
print("OAUTH_SUBPROC_RESULT", json.dumps({
"has_authorize": has_authorize,
"status": status,
"set_cookie_headers": headers,
}))
Expand Down Expand Up @@ -416,7 +409,8 @@ async def _run():

print("OAUTH subprocess result:", payload)

assert payload["has_authorize"], "authorize route not registered despite env vars"
# 200 (not 404) is the registration proof — see the note in the script
# about FastAPI's lazy route materialization.
assert payload["status"] == 200, payload
raw = payload["set_cookie_headers"]
cookies = _set_cookies_by_name(raw)
Expand Down
Loading
Loading