style: apply ruff formatting to source and test files
This commit is contained in:
@@ -27,9 +27,7 @@ class CreatePostUseCase:
|
|||||||
|
|
||||||
# Check if slug already exists
|
# Check if slug already exists
|
||||||
if await self._post_repo.slug_exists(slug.value):
|
if await self._post_repo.slug_exists(slug.value):
|
||||||
raise AlreadyExistsException(
|
raise AlreadyExistsException(f"Post with slug '{slug.value}' already exists")
|
||||||
f"Post with slug '{slug.value}' already exists"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create domain entity
|
# Create domain entity
|
||||||
post = Post.create(
|
post = Post.create(
|
||||||
|
|||||||
@@ -38,9 +38,7 @@ class ListPostsUseCase:
|
|||||||
offset: int | None = None,
|
offset: int | None = None,
|
||||||
) -> list[PostResponseDTO]:
|
) -> list[PostResponseDTO]:
|
||||||
"""Get posts by author."""
|
"""Get posts by author."""
|
||||||
posts = await self._post_repo.get_by_author(
|
posts = await self._post_repo.get_by_author(author_id, limit=limit, offset=offset)
|
||||||
author_id, limit=limit, offset=offset
|
|
||||||
)
|
|
||||||
return [self._map_to_dto(post) for post in posts]
|
return [self._map_to_dto(post) for post in posts]
|
||||||
|
|
||||||
async def by_tag(
|
async def by_tag(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime, timezone
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
@@ -12,8 +12,8 @@ class BaseEntity(ABC):
|
|||||||
"""Base class for all domain entities."""
|
"""Base class for all domain entities."""
|
||||||
|
|
||||||
id: UUID = field(default_factory=uuid4)
|
id: UUID = field(default_factory=uuid4)
|
||||||
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||||||
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
updated_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
||||||
|
|
||||||
def __eq__(self, other: object) -> bool:
|
def __eq__(self, other: object) -> bool:
|
||||||
if not isinstance(other, BaseEntity):
|
if not isinstance(other, BaseEntity):
|
||||||
@@ -25,7 +25,7 @@ class BaseEntity(ABC):
|
|||||||
|
|
||||||
def touch(self) -> None:
|
def touch(self) -> None:
|
||||||
"""Update the updated_at timestamp."""
|
"""Update the updated_at timestamp."""
|
||||||
self.updated_at = datetime.now(timezone.utc)
|
self.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def to_dict(self) -> dict[str, Any]:
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
"""Role-based access control definitions."""
|
"""Role-based access control definitions."""
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Any, Callable
|
from typing import Any
|
||||||
|
|
||||||
from app.domain.exceptions import ForbiddenException
|
from app.domain.exceptions import ForbiddenException
|
||||||
|
|
||||||
|
|||||||
@@ -19,9 +19,7 @@ class Slug(ValueObject[str]):
|
|||||||
if len(self.value) > self.MAX_LENGTH:
|
if len(self.value) > self.MAX_LENGTH:
|
||||||
raise ValueError(f"Slug must be at most {self.MAX_LENGTH} characters")
|
raise ValueError(f"Slug must be at most {self.MAX_LENGTH} characters")
|
||||||
if not re.match(self.SLUG_PATTERN, self.value):
|
if not re.match(self.SLUG_PATTERN, self.value):
|
||||||
raise ValueError(
|
raise ValueError("Slug must contain only lowercase letters, numbers, and hyphens")
|
||||||
"Slug must contain only lowercase letters, numbers, and hyphens"
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_title(cls, title: str) -> "Slug":
|
def from_title(cls, title: str) -> "Slug":
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Database connection and session management."""
|
"""Database connection and session management."""
|
||||||
|
|
||||||
|
from collections.abc import AsyncGenerator
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from typing import AsyncGenerator
|
|
||||||
|
|
||||||
from sqlalchemy.ext.asyncio import (
|
from sqlalchemy.ext.asyncio import (
|
||||||
AsyncEngine,
|
AsyncEngine,
|
||||||
@@ -38,7 +38,7 @@ AsyncSessionLocal = async_sessionmaker(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
async def get_session() -> AsyncGenerator[AsyncSession]:
|
||||||
"""Get database session."""
|
"""Get database session."""
|
||||||
async with AsyncSessionLocal() as session:
|
async with AsyncSessionLocal() as session:
|
||||||
try:
|
try:
|
||||||
@@ -48,7 +48,7 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
|||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def get_session_context() -> AsyncGenerator[AsyncSession, None]:
|
async def get_session_context() -> AsyncGenerator[AsyncSession]:
|
||||||
"""Get database session as context manager."""
|
"""Get database session as context manager."""
|
||||||
async with AsyncSessionLocal() as session:
|
async with AsyncSessionLocal() as session:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""SQLAlchemy ORM models."""
|
"""SQLAlchemy ORM models."""
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import UTC, datetime
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from sqlalchemy import JSON, Boolean, DateTime, String, Text
|
from sqlalchemy import JSON, Boolean, DateTime, String, Text
|
||||||
@@ -14,27 +14,21 @@ class PostORM(Base): # type: ignore[valid-type,misc]
|
|||||||
|
|
||||||
__tablename__ = "posts"
|
__tablename__ = "posts"
|
||||||
|
|
||||||
id: Mapped[str] = mapped_column(
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid4()))
|
||||||
String(36), primary_key=True, default=lambda: str(uuid4())
|
|
||||||
)
|
|
||||||
title: Mapped[str] = mapped_column(String(200), nullable=False)
|
title: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||||
content: Mapped[str] = mapped_column(Text, nullable=False)
|
content: Mapped[str] = mapped_column(Text, nullable=False)
|
||||||
slug: Mapped[str] = mapped_column(
|
slug: Mapped[str] = mapped_column(String(200), nullable=False, unique=True, index=True)
|
||||||
String(200), nullable=False, unique=True, index=True
|
|
||||||
)
|
|
||||||
author_id: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
|
author_id: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
|
||||||
published: Mapped[bool] = mapped_column(
|
published: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, index=True)
|
||||||
Boolean, default=False, nullable=False, index=True
|
|
||||||
)
|
|
||||||
tags: Mapped[list[str]] = mapped_column(JSON, default=list)
|
tags: Mapped[list[str]] = mapped_column(JSON, default=list)
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
DateTime(timezone=True),
|
DateTime(timezone=True),
|
||||||
default=lambda: datetime.now(timezone.utc),
|
default=lambda: datetime.now(UTC),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
)
|
)
|
||||||
updated_at: Mapped[datetime] = mapped_column(
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
DateTime(timezone=True),
|
DateTime(timezone=True),
|
||||||
default=lambda: datetime.now(timezone.utc),
|
default=lambda: datetime.now(UTC),
|
||||||
onupdate=lambda: datetime.now(timezone.utc),
|
onupdate=lambda: datetime.now(UTC),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Dishka providers for dependency injection."""
|
"""Dishka providers for dependency injection."""
|
||||||
|
|
||||||
from typing import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
|
|
||||||
from dishka import Provider, Scope, provide
|
from dishka import Provider, Scope, provide
|
||||||
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
|
||||||
@@ -30,7 +30,7 @@ class DatabaseProvider(Provider):
|
|||||||
return engine
|
return engine
|
||||||
|
|
||||||
@provide(scope=Scope.REQUEST)
|
@provide(scope=Scope.REQUEST)
|
||||||
async def get_session(self) -> AsyncGenerator[AsyncSession, None]:
|
async def get_session(self) -> AsyncGenerator[AsyncSession]:
|
||||||
"""Provide database session per request."""
|
"""Provide database session per request."""
|
||||||
async with AsyncSessionLocal() as session:
|
async with AsyncSessionLocal() as session:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Exception handling middleware."""
|
"""Exception handling middleware."""
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
@@ -33,9 +33,7 @@ def get_status_code(exc: DomainException) -> int:
|
|||||||
return 500
|
return 500
|
||||||
|
|
||||||
|
|
||||||
async def domain_exception_handler(
|
async def domain_exception_handler(request: Request, exc: DomainException) -> JSONResponse:
|
||||||
request: Request, exc: DomainException
|
|
||||||
) -> JSONResponse:
|
|
||||||
"""Handle domain exceptions."""
|
"""Handle domain exceptions."""
|
||||||
status_code = get_status_code(exc)
|
status_code = get_status_code(exc)
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
@@ -43,22 +41,20 @@ async def domain_exception_handler(
|
|||||||
content={
|
content={
|
||||||
"error": exc.__class__.__name__,
|
"error": exc.__class__.__name__,
|
||||||
"message": exc.message,
|
"message": exc.message,
|
||||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
"timestamp": datetime.now(UTC).isoformat(),
|
||||||
"path": str(request.url.path),
|
"path": str(request.url.path),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def http_exception_handler(
|
async def http_exception_handler(request: Request, exc: StarletteHTTPException) -> JSONResponse:
|
||||||
request: Request, exc: StarletteHTTPException
|
|
||||||
) -> JSONResponse:
|
|
||||||
"""Handle HTTP exceptions."""
|
"""Handle HTTP exceptions."""
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=exc.status_code,
|
status_code=exc.status_code,
|
||||||
content={
|
content={
|
||||||
"error": "HTTPException",
|
"error": "HTTPException",
|
||||||
"message": str(exc.detail),
|
"message": str(exc.detail),
|
||||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
"timestamp": datetime.now(UTC).isoformat(),
|
||||||
"path": str(request.url.path),
|
"path": str(request.url.path),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -71,7 +67,7 @@ async def generic_exception_handler(request: Request, exc: Exception) -> JSONRes
|
|||||||
content={
|
content={
|
||||||
"error": "InternalServerError",
|
"error": "InternalServerError",
|
||||||
"message": "An unexpected error occurred",
|
"message": "An unexpected error occurred",
|
||||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
"timestamp": datetime.now(UTC).isoformat(),
|
||||||
"path": str(request.url.path),
|
"path": str(request.url.path),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -47,9 +47,7 @@ class SQLAlchemyPostRepository(PostRepository):
|
|||||||
|
|
||||||
async def get_by_id(self, entity_id: UUID) -> Post | None:
|
async def get_by_id(self, entity_id: UUID) -> Post | None:
|
||||||
"""Get post by ID."""
|
"""Get post by ID."""
|
||||||
result = await self._session.execute(
|
result = await self._session.execute(select(PostORM).where(PostORM.id == str(entity_id)))
|
||||||
select(PostORM).where(PostORM.id == str(entity_id))
|
|
||||||
)
|
|
||||||
orm = result.scalar_one_or_none()
|
orm = result.scalar_one_or_none()
|
||||||
return self._to_domain(orm) if orm else None
|
return self._to_domain(orm) if orm else None
|
||||||
|
|
||||||
@@ -67,9 +65,7 @@ class SQLAlchemyPostRepository(PostRepository):
|
|||||||
|
|
||||||
async def update(self, entity: Post) -> None:
|
async def update(self, entity: Post) -> None:
|
||||||
"""Update existing post."""
|
"""Update existing post."""
|
||||||
result = await self._session.execute(
|
result = await self._session.execute(select(PostORM).where(PostORM.id == str(entity.id)))
|
||||||
select(PostORM).where(PostORM.id == str(entity.id))
|
|
||||||
)
|
|
||||||
orm = result.scalar_one()
|
orm = result.scalar_one()
|
||||||
|
|
||||||
orm.title = entity.title.value
|
orm.title = entity.title.value
|
||||||
@@ -83,25 +79,19 @@ class SQLAlchemyPostRepository(PostRepository):
|
|||||||
|
|
||||||
async def delete(self, entity_id: UUID) -> None:
|
async def delete(self, entity_id: UUID) -> None:
|
||||||
"""Delete post by ID."""
|
"""Delete post by ID."""
|
||||||
result = await self._session.execute(
|
result = await self._session.execute(select(PostORM).where(PostORM.id == str(entity_id)))
|
||||||
select(PostORM).where(PostORM.id == str(entity_id))
|
|
||||||
)
|
|
||||||
orm = result.scalar_one_or_none()
|
orm = result.scalar_one_or_none()
|
||||||
if orm:
|
if orm:
|
||||||
await self._session.delete(orm)
|
await self._session.delete(orm)
|
||||||
|
|
||||||
async def exists(self, entity_id: UUID) -> bool:
|
async def exists(self, entity_id: UUID) -> bool:
|
||||||
"""Check if post exists."""
|
"""Check if post exists."""
|
||||||
result = await self._session.execute(
|
result = await self._session.execute(select(PostORM).where(PostORM.id == str(entity_id)))
|
||||||
select(PostORM).where(PostORM.id == str(entity_id))
|
|
||||||
)
|
|
||||||
return result.scalar_one_or_none() is not None
|
return result.scalar_one_or_none() is not None
|
||||||
|
|
||||||
async def get_by_slug(self, slug: str) -> Post | None:
|
async def get_by_slug(self, slug: str) -> Post | None:
|
||||||
"""Get post by slug."""
|
"""Get post by slug."""
|
||||||
result = await self._session.execute(
|
result = await self._session.execute(select(PostORM).where(PostORM.slug == slug))
|
||||||
select(PostORM).where(PostORM.slug == slug)
|
|
||||||
)
|
|
||||||
orm = result.scalar_one_or_none()
|
orm = result.scalar_one_or_none()
|
||||||
return self._to_domain(orm) if orm else None
|
return self._to_domain(orm) if orm else None
|
||||||
|
|
||||||
@@ -154,9 +144,7 @@ class SQLAlchemyPostRepository(PostRepository):
|
|||||||
|
|
||||||
async def slug_exists(self, slug: str) -> bool:
|
async def slug_exists(self, slug: str) -> bool:
|
||||||
"""Check if slug exists."""
|
"""Check if slug exists."""
|
||||||
result = await self._session.execute(
|
result = await self._session.execute(select(PostORM).where(PostORM.slug == slug))
|
||||||
select(PostORM).where(PostORM.slug == slug)
|
|
||||||
)
|
|
||||||
return result.scalar_one_or_none() is not None
|
return result.scalar_one_or_none() is not None
|
||||||
|
|
||||||
async def search(
|
async def search(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Application entry point with DDD architecture."""
|
"""Application entry point with DDD architecture."""
|
||||||
|
|
||||||
|
from collections.abc import AsyncGenerator
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from typing import AsyncGenerator
|
|
||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from dishka import make_async_container
|
from dishka import make_async_container
|
||||||
@@ -21,7 +21,7 @@ from app.presentation import router
|
|||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
|
||||||
"""Application lifespan manager."""
|
"""Application lifespan manager."""
|
||||||
# Startup
|
# Startup
|
||||||
await init_db()
|
await init_db()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""API test fixtures."""
|
"""API test fixtures."""
|
||||||
|
|
||||||
from typing import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -25,7 +25,7 @@ def mock_keycloak_client() -> MagicMock:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
async def client(mock_keycloak_client: MagicMock) -> AsyncGenerator[AsyncClient, None]:
|
async def client(mock_keycloak_client: MagicMock) -> AsyncGenerator[AsyncClient]:
|
||||||
"""Create async HTTP client for API testing."""
|
"""Create async HTTP client for API testing."""
|
||||||
with patch(
|
with patch(
|
||||||
"app.presentation.api.deps.KeycloakAuthClient",
|
"app.presentation.api.deps.KeycloakAuthClient",
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
# E2E test fixtures
|
# E2E test fixtures
|
||||||
# Provides: full application state, end-to-end workflows, cleanup
|
# Provides: full application state, end-to-end workflows, cleanup
|
||||||
|
|
||||||
from typing import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
async def e2e_app() -> AsyncGenerator[FastAPI, None]:
|
async def e2e_app() -> AsyncGenerator[FastAPI]:
|
||||||
"""Create full application instance for E2E testing."""
|
"""Create full application instance for E2E testing."""
|
||||||
from app.main import app_factory
|
from app.main import app_factory
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Integration test fixtures."""
|
"""Integration test fixtures."""
|
||||||
|
|
||||||
from typing import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from sqlalchemy.ext.asyncio import (
|
from sqlalchemy.ext.asyncio import (
|
||||||
@@ -39,7 +39,7 @@ def session_factory(engine: AsyncEngine) -> async_sessionmaker[AsyncSession]:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
async def setup_db(engine: AsyncEngine) -> AsyncGenerator[None, None]:
|
async def setup_db(engine: AsyncEngine) -> AsyncGenerator[None]:
|
||||||
"""Setup database tables for each test."""
|
"""Setup database tables for each test."""
|
||||||
async with engine.begin() as conn:
|
async with engine.begin() as conn:
|
||||||
await conn.run_sync(Base.metadata.create_all)
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
@@ -51,7 +51,7 @@ async def setup_db(engine: AsyncEngine) -> AsyncGenerator[None, None]:
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
async def db_session(
|
async def db_session(
|
||||||
session_factory: async_sessionmaker[AsyncSession],
|
session_factory: async_sessionmaker[AsyncSession],
|
||||||
) -> AsyncGenerator[AsyncSession, None]:
|
) -> AsyncGenerator[AsyncSession]:
|
||||||
"""Create database session for testing."""
|
"""Create database session for testing."""
|
||||||
async with session_factory() as session:
|
async with session_factory() as session:
|
||||||
yield session
|
yield session
|
||||||
|
|||||||
@@ -123,9 +123,7 @@ class TestKeycloakAuthClient:
|
|||||||
"""Create Keycloak client."""
|
"""Create Keycloak client."""
|
||||||
return KeycloakAuthClient(settings)
|
return KeycloakAuthClient(settings)
|
||||||
|
|
||||||
def test_client_initialization(
|
def test_client_initialization(self, client: KeycloakAuthClient, settings: Settings) -> None:
|
||||||
self, client: KeycloakAuthClient, settings: Settings
|
|
||||||
) -> None:
|
|
||||||
"""Test client initialization."""
|
"""Test client initialization."""
|
||||||
assert client._settings == settings
|
assert client._settings == settings
|
||||||
assert client._base_url == "http://localhost:8080/realms/test-realm"
|
assert client._base_url == "http://localhost:8080/realms/test-realm"
|
||||||
@@ -144,10 +142,7 @@ class TestKeycloakAuthClient:
|
|||||||
def test_get_userinfo_url(self, client: KeycloakAuthClient) -> None:
|
def test_get_userinfo_url(self, client: KeycloakAuthClient) -> None:
|
||||||
"""Test userinfo URL generation."""
|
"""Test userinfo URL generation."""
|
||||||
url = client._get_userinfo_url()
|
url = client._get_userinfo_url()
|
||||||
assert (
|
assert url == "http://localhost:8080/realms/test-realm/protocol/openid-connect/userinfo"
|
||||||
url
|
|
||||||
== "http://localhost:8080/realms/test-realm/protocol/openid-connect/userinfo"
|
|
||||||
)
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_introspect_token_success(self, client: KeycloakAuthClient) -> None:
|
async def test_introspect_token_success(self, client: KeycloakAuthClient) -> None:
|
||||||
@@ -196,18 +191,14 @@ class TestKeycloakAuthClient:
|
|||||||
assert result.is_valid is False
|
assert result.is_valid is False
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_introspect_token_http_error(
|
async def test_introspect_token_http_error(self, client: KeycloakAuthClient) -> None:
|
||||||
self, client: KeycloakAuthClient
|
|
||||||
) -> None:
|
|
||||||
"""Test introspection with HTTP error."""
|
"""Test introspection with HTTP error."""
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
mock_async_client = AsyncMock()
|
mock_async_client = AsyncMock()
|
||||||
mock_async_client.__aenter__ = AsyncMock(return_value=mock_async_client)
|
mock_async_client.__aenter__ = AsyncMock(return_value=mock_async_client)
|
||||||
mock_async_client.__aexit__ = AsyncMock(return_value=None)
|
mock_async_client.__aexit__ = AsyncMock(return_value=None)
|
||||||
mock_async_client.post = AsyncMock(
|
mock_async_client.post = AsyncMock(side_effect=httpx.HTTPError("Connection error"))
|
||||||
side_effect=httpx.HTTPError("Connection error")
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch("httpx.AsyncClient", return_value=mock_async_client):
|
with patch("httpx.AsyncClient", return_value=mock_async_client):
|
||||||
result = await client.introspect_token("test-token")
|
result = await client.introspect_token("test-token")
|
||||||
@@ -216,9 +207,7 @@ class TestKeycloakAuthClient:
|
|||||||
assert result.is_valid is False
|
assert result.is_valid is False
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_introspect_token_uses_cache(
|
async def test_introspect_token_uses_cache(self, client: KeycloakAuthClient) -> None:
|
||||||
self, client: KeycloakAuthClient
|
|
||||||
) -> None:
|
|
||||||
"""Test that token introspection uses cache."""
|
"""Test that token introspection uses cache."""
|
||||||
mock_response = Mock()
|
mock_response = Mock()
|
||||||
mock_response.json.return_value = {
|
mock_response.json.return_value = {
|
||||||
@@ -283,9 +272,7 @@ class TestKeycloakAuthClient:
|
|||||||
mock_async_client = AsyncMock()
|
mock_async_client = AsyncMock()
|
||||||
mock_async_client.__aenter__ = AsyncMock(return_value=mock_async_client)
|
mock_async_client.__aenter__ = AsyncMock(return_value=mock_async_client)
|
||||||
mock_async_client.__aexit__ = AsyncMock(return_value=None)
|
mock_async_client.__aexit__ = AsyncMock(return_value=None)
|
||||||
mock_async_client.get = AsyncMock(
|
mock_async_client.get = AsyncMock(side_effect=httpx.HTTPError("Connection error"))
|
||||||
side_effect=httpx.HTTPError("Connection error")
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch("httpx.AsyncClient", return_value=mock_async_client):
|
with patch("httpx.AsyncClient", return_value=mock_async_client):
|
||||||
result = await client.get_userinfo("test-token")
|
result = await client.get_userinfo("test-token")
|
||||||
@@ -293,9 +280,7 @@ class TestKeycloakAuthClient:
|
|||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_introspect_token_no_realm_roles(
|
async def test_introspect_token_no_realm_roles(self, client: KeycloakAuthClient) -> None:
|
||||||
self, client: KeycloakAuthClient
|
|
||||||
) -> None:
|
|
||||||
"""Test introspection without realm_access roles."""
|
"""Test introspection without realm_access roles."""
|
||||||
mock_response = Mock()
|
mock_response = Mock()
|
||||||
mock_response.json.return_value = {
|
mock_response.json.return_value = {
|
||||||
|
|||||||
@@ -129,10 +129,7 @@ class TestSettings:
|
|||||||
security=SecurityConfig(secret_key="test"),
|
security=SecurityConfig(secret_key="test"),
|
||||||
kc=KCConfig(client_secret="test"),
|
kc=KCConfig(client_secret="test"),
|
||||||
)
|
)
|
||||||
assert (
|
assert s.database_url == "postgresql+asyncpg://admin:secret@db.example.com:5433/mydb"
|
||||||
s.database_url
|
|
||||||
== "postgresql+asyncpg://admin:secret@db.example.com:5433/mydb"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_database_url_override(self) -> None:
|
def test_database_url_override(self) -> None:
|
||||||
"""Test that explicit database URL overrides auto-building."""
|
"""Test that explicit database URL overrides auto-building."""
|
||||||
|
|||||||
Reference in New Issue
Block a user