diff --git a/src/application/use_cases/comment_use_cases.py b/src/application/use_cases/comment_use_cases.py new file mode 100644 index 0000000..9ee4d6e --- /dev/null +++ b/src/application/use_cases/comment_use_cases.py @@ -0,0 +1,67 @@ +import datetime +from typing import List, Optional +from src.domain.entities.comment import Comment +from src.domain.exceptions import EntityNotFound, CommentNotFound +from src.domain.repositories.comment_repository import CommentRepository +from src.application.use_cases.log_use_cases import LogUseCase + +class CreateCommentUseCase: + def __init__(self, comment_repository: CommentRepository, log_use_case: LogUseCase): + self.comment_repository = comment_repository + self.log_use_case = log_use_case + + async def execute(self, user_id: int, content: str) -> Comment: + comment = Comment(post_id=None, user_id=user_id, content=content) + result = await self.comment_repository.create(comment) + + if result: + await self.log_use_case.log_action(user_id=user_id, action="create_comment", + timestamp=datetime.datetime.utcnow().isoformat()) + return result + +class GetCommentUseCase: + def __init__(self, comment_repository: CommentRepository): + self.comment_repository = comment_repository + + async def execute(self, + page: int = 1, + limit: int = 10, + sort: str = "created_at", + order: str = "desc", + ) -> list[Comment]: + result = await self.comment_repository.get_with_pagination( + page=page, + limit=limit, + sort=sort, + order=order) + if not result or not result.get("comments"): + raise CommentNotFound(f"Comment not found") + return result + +class GetAllCommentsUseCase: + def __init__(self, comment_repository: CommentRepository): + self.comment_repository = comment_repository + + async def execute(self) -> List[Comment]: + comments = await self.comment_repository.get_all_comments() + return comments + +class UpdateCommentUseCase: + def __init__(self, comment_repository: CommentRepository, log_use_case: LogUseCase): + self.comment_repository = comment_repository + self.log_use_case = log_use_case + + async def execute(self, post_id: int, content: Optional[str] = None) -> Comment: + existing_comment = await self.comment_repository.get_by_id(post_id=post_id) + if not existing_comment: + raise EntityNotFound(f"Entity not found") + + if content: + existing_comment.content = content + await self.log_use_case.log_action(user_id=existing_comment.user_id, action="update_comment", + timestamp=datetime.datetime.utcnow().isoformat()) + + updated_comment = await self.comment_repository.update(existing_comment) + if not updated_comment: + raise EntityNotFound(f"Entity not found") + return updated_comment \ No newline at end of file diff --git a/src/application/use_cases/log_use_cases.py b/src/application/use_cases/log_use_cases.py new file mode 100644 index 0000000..9cb993d --- /dev/null +++ b/src/application/use_cases/log_use_cases.py @@ -0,0 +1,17 @@ +import datetime +from typing import Optional + +from src.domain.entities.log import LogEntry +from src.domain.repositories.log_repository import LogRepository + +class LogUseCase: + def __init__(self, log_repository: LogRepository): + self.log_repository = log_repository + + async def log_action(self, user_id: Optional[int], action: str, timestamp: Optional[str] = None) -> None: + log = LogEntry( + user_id=user_id, + action=action, + timestamp=timestamp or datetime.datetime.utcnow().isoformat() + ) + await self.log_repository.create(log) \ No newline at end of file diff --git a/src/domain/entities/comment.py b/src/domain/entities/comment.py new file mode 100644 index 0000000..ead74b8 --- /dev/null +++ b/src/domain/entities/comment.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass +from datetime import datetime +from typing import Optional + +@dataclass +class Comment: + post_id: Optional[int] + user_id: Optional[int] + content: str + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None \ No newline at end of file diff --git a/src/domain/entities/log.py b/src/domain/entities/log.py new file mode 100644 index 0000000..8e5ca12 --- /dev/null +++ b/src/domain/entities/log.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass +from datetime import datetime +from typing import Optional + +@dataclass +class LogEntry: + user_id: int + action: str + timestamp: Optional[datetime] \ No newline at end of file diff --git a/src/domain/exceptions.py b/src/domain/exceptions.py index 356962a..7c8ff81 100644 --- a/src/domain/exceptions.py +++ b/src/domain/exceptions.py @@ -13,3 +13,7 @@ class EntityAlreadyExists(DomainException): class ValidationError(DomainException): pass + +class CommentNotFound(DomainException): + pass + diff --git a/src/domain/repositories/comment_repository.py b/src/domain/repositories/comment_repository.py new file mode 100644 index 0000000..4fff375 --- /dev/null +++ b/src/domain/repositories/comment_repository.py @@ -0,0 +1,30 @@ +from abc import ABC, abstractmethod +from typing import List, Optional +from src.domain.entities.comment import Comment + +class CommentRepository(ABC): + @abstractmethod + async def create(self, comment: Comment) -> Optional[Comment]: + pass + + @abstractmethod + async def get_by_id(self, post_id: int) -> Optional[Comment]: + pass + + @abstractmethod + async def get_all_comments(self) -> List[Comment]: + pass + + @abstractmethod + async def get_with_pagination(self, + page: int, + limit: int, + sort: str, + order: str + ) -> List[Comment]: + pass + + + @abstractmethod + async def update(self, comment: Comment) -> Optional[Comment]: + pass \ No newline at end of file diff --git a/src/domain/repositories/log_repository.py b/src/domain/repositories/log_repository.py new file mode 100644 index 0000000..97c073f --- /dev/null +++ b/src/domain/repositories/log_repository.py @@ -0,0 +1,9 @@ +from abc import ABC, abstractmethod +from src.domain.entities.log import LogEntry + +class LogRepository(ABC): + + @abstractmethod + async def create(self, log: LogEntry): + pass + diff --git a/src/infrastructure/database/connection.py b/src/infrastructure/database/connection.py index dd32f22..6971daf 100644 --- a/src/infrastructure/database/connection.py +++ b/src/infrastructure/database/connection.py @@ -1,26 +1,36 @@ +import os import asyncpg +import logging from typing import Optional from src.infrastructure.config import settings +logger = logging.getLogger(__name__) + class DatabaseConnection: def __init__(self): self.pool: Optional[asyncpg.Pool] = None + self.dsn = os.getenv('DATABASE_URL') async def connect(self): if not self.pool: - self.pool = await asyncpg.create_pool( - host=settings.database_host, - port=settings.database_port, - database=settings.database_name, - user=settings.database_user, - password=settings.database_password, - min_size=1, - max_size=20, - timeout=30.0, - command_timeout=60.0, - ) + try: + logger.info("Попытка подключения к БД") + self.pool = await asyncpg.create_pool( + host=settings.database_host, + port=settings.database_port, + database=settings.database_name, + user=settings.database_user, + password=settings.database_password, + min_size=1, + max_size=20, + timeout=30.0, + command_timeout=60.0, + ) + logger.info("Подключение к БД успешно установлено") + except (ConnectionError, OSError, Exception) as e: + logger.error(f"Ошибка подключения к БД: {e}") async def disconnect(self): if self.pool: diff --git a/src/infrastructure/database/migration_runner.py b/src/infrastructure/database/migration_runner.py index 2377ff6..5e22636 100644 --- a/src/infrastructure/database/migration_runner.py +++ b/src/infrastructure/database/migration_runner.py @@ -2,10 +2,14 @@ import asyncpg from pathlib import Path from src.infrastructure.config import settings +import platform class MigrationRunner: - def __init__(self, migrations_dir: str = "src/infrastructure/database/migrations"): + def __init__(self, migrations_dir: str = None): + base_dir = Path(__file__).resolve().parent + if migrations_dir is None: + migrations_dir = base_dir / "migrations" self.migrations_dir = Path(migrations_dir) self.migrations_dir.mkdir(parents=True, exist_ok=True) @@ -23,12 +27,15 @@ async def _get_applied_migrations(self, conn): async def _get_pending_migrations(self, conn): applied = await self._get_applied_migrations(conn) - all_migrations = sorted([ - f.stem for f in self.migrations_dir.glob("*.sql") - ]) + all_migrations = sorted([f.stem for f in self.migrations_dir.glob("*.sql")]) + print(f"🔍 Found migrations: {all_migrations}") return [m for m in all_migrations if m not in applied] async def migrate(self): + print( + f"🔌 Connecting to {settings.database_name}@" + f"{settings.database_host}:{settings.database_port}" + f" as {settings.database_user}") conn = await asyncpg.connect( host=settings.database_host, port=settings.database_port, @@ -36,7 +43,8 @@ async def migrate(self): user=settings.database_user, password=settings.database_password, ) - + db_name = await conn.fetchval("SELECT current_database()") + print(f"📡 Connected to database: {db_name}") try: await self._ensure_migrations_table(conn) pending = await self._get_pending_migrations(conn) @@ -76,7 +84,10 @@ async def status(self): user=settings.database_user, password=settings.database_password, ) - + + db_name = await conn.fetchval("SELECT current_database()") + print(f"📡 Connected to database: {db_name}") + try: await self._ensure_migrations_table(conn) applied = await self._get_applied_migrations(conn) @@ -112,6 +123,8 @@ async def main(): else: await runner.migrate() +if platform.system() == "Windows": + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) if __name__ == "__main__": asyncio.run(main()) diff --git a/src/infrastructure/database/migrations/002_create_comments_table.sql b/src/infrastructure/database/migrations/002_create_comments_table.sql new file mode 100644 index 0000000..295ce1a --- /dev/null +++ b/src/infrastructure/database/migrations/002_create_comments_table.sql @@ -0,0 +1,7 @@ +CREATE TABLE if not exists comments ( + post_id serial PRIMARY KEY, + user_id INTEGER references users(id) ON DELETE CASCADE, + content TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/src/infrastructure/database/migrations/003_create_logs_table.sql b/src/infrastructure/database/migrations/003_create_logs_table.sql new file mode 100644 index 0000000..dc3f857 --- /dev/null +++ b/src/infrastructure/database/migrations/003_create_logs_table.sql @@ -0,0 +1,6 @@ +CREATE TABLE if not exists logs ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + action VARCHAR(255) NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/src/infrastructure/database/migrations/002_create_products_table.sql.example b/src/infrastructure/database/migrations/004_create_products_table.sql.example similarity index 100% rename from src/infrastructure/database/migrations/002_create_products_table.sql.example rename to src/infrastructure/database/migrations/004_create_products_table.sql.example diff --git a/src/infrastructure/database/migrations/003_add_user_avatar.sql.example b/src/infrastructure/database/migrations/005_add_user_avatar.sql.example similarity index 100% rename from src/infrastructure/database/migrations/003_add_user_avatar.sql.example rename to src/infrastructure/database/migrations/005_add_user_avatar.sql.example diff --git a/src/infrastructure/repositories/postgres_comment_repository.py b/src/infrastructure/repositories/postgres_comment_repository.py new file mode 100644 index 0000000..75e4600 --- /dev/null +++ b/src/infrastructure/repositories/postgres_comment_repository.py @@ -0,0 +1,97 @@ +from typing import Optional, Any, Coroutine + +from src.domain.entities.comment import Comment +from src.domain.repositories.comment_repository import CommentRepository +from src.infrastructure.database.connection import DatabaseConnection + +class PostgresCommentRepository(CommentRepository): + def __init__(self, db: DatabaseConnection): + self.db = db + + def _map_row_to_comment(self, row) -> Comment | None: + if not row: + return None + return Comment( + post_id=row['post_id'], + user_id=row['user_id'], + content=row['content'], + created_at=row['created_at'], + updated_at=row['updated_at'] + ) + + async def create(self, comment: Comment) -> Comment: + row = await self.db.fetchrow( + """ + insert into comments (user_id, content) + values ($1, $2) + returning post_id, user_id, content, created_at, updated_at + """, + comment.user_id, comment.content + ) + return self._map_row_to_comment(row) + + async def get_by_id(self, post_id: int) -> Optional[Comment]: + row = await self.db.fetchrow( + """ + select post_id, user_id, content, created_at, updated_at + from comments + where post_id = $1 + """, + post_id + ) + return self._map_row_to_comment(row) + + async def get_all_comments(self) -> list[Comment]: + rows = await self.db.fetch( + """ + select post_id, user_id, content, created_at, updated_at + from comments + """ + ) + return [self._map_row_to_comment(row) for row in rows] + + async def update(self, comment: Comment) -> Optional[Comment]: + row = await self.db.fetchrow( + """ + update comments + set content = $1, updated_at = current_timestamp + where post_id = $2 + returning post_id, user_id, content, created_at, updated_at + """, + comment.content, comment.post_id + ) + return self._map_row_to_comment(row) + + async def get_with_pagination(self, + page: int = 1, + limit: int = 10, + sort: str = "created_at", + order: str = "desc" + ) -> dict[str, list[Comment | None] | int | Any]: + offset = (page - 1) * limit + + if sort not in {"created_at", "updated_at"}: + sort = "created_at" + if order.lower() not in {"asc", "desc"}: + order = "desc" + + query = f""" + SELECT post_id, user_id, content, created_at, updated_at + FROM comments + ORDER BY {sort} {order} + LIMIT $1 offset $2 + """ + rows = await self.db.fetch( + query, + limit, offset + ) + + total_row = await self.db.fetchrow("SELECT COUNT(*) AS total FROM comments") + total = total_row["total"] if total_row else 0 + + comments = [self._map_row_to_comment(row) for row in rows] + + return { + "comments": comments, "total": total, + "page": page, "limit": limit + } \ No newline at end of file diff --git a/src/infrastructure/repositories/postgres_log_repository.py b/src/infrastructure/repositories/postgres_log_repository.py new file mode 100644 index 0000000..3e785d7 --- /dev/null +++ b/src/infrastructure/repositories/postgres_log_repository.py @@ -0,0 +1,26 @@ +from typing import Optional +from src.domain.entities.log import LogEntry +from src.domain.repositories.log_repository import LogRepository +from src.infrastructure.database.connection import DatabaseConnection + +class LogRepositoryImpl(LogRepository): + def __init__(self, db_connection: DatabaseConnection): + self.db_connection = db_connection + + async def create(self, log: LogEntry) -> Optional[LogEntry]: + row = await self.db_connection.fetchrow( + """ + INSERT INTO logs (user_id, action) + VALUES ($1, $2) + RETURNING user_id, action, timestamp + """, + log.user_id, + log.action, + ) + if row: + return LogEntry( + user_id=row["user_id"], + action=row["action"], + timestamp=row["timestamp"] + ) + return None \ No newline at end of file diff --git a/src/presentation/api/app.py b/src/presentation/api/app.py index 451b7e9..25bd6a6 100644 --- a/src/presentation/api/app.py +++ b/src/presentation/api/app.py @@ -4,6 +4,7 @@ from src.infrastructure.database.connection import db_connection from src.presentation.api.routes.users import router as users_router +from src.presentation.api.routes.comments import router as comments_router @asynccontextmanager @@ -30,6 +31,7 @@ def create_app() -> FastAPI: ) app.include_router(users_router) + app.include_router(comments_router) @app.get("/health") async def health_check(): diff --git a/src/presentation/api/dependencies.py b/src/presentation/api/dependencies.py index c98a365..0d65542 100644 --- a/src/presentation/api/dependencies.py +++ b/src/presentation/api/dependencies.py @@ -5,10 +5,24 @@ UpdateUserUseCase, DeleteUserUseCase, ) +from src.application.use_cases.comment_use_cases import ( + CreateCommentUseCase, + UpdateCommentUseCase, + GetAllCommentsUseCase, + GetCommentUseCase +) +from src.application.use_cases.log_use_cases import LogUseCase + + +from src.application.use_cases.log_use_cases import LogUseCase from src.infrastructure.database.connection import db_connection from src.infrastructure.repositories.postgres_user_repository import PostgresUserRepository +from src.infrastructure.repositories.postgres_comment_repository import PostgresCommentRepository +from src.infrastructure.repositories.postgres_log_repository import LogRepositoryImpl +''' User related dependencies ''' + def get_user_repository(): return PostgresUserRepository(db_connection) @@ -32,3 +46,34 @@ def get_update_user_use_case(): def get_delete_user_use_case(): return DeleteUserUseCase(get_user_repository()) + +''' Comment related dependencies ''' + +def get_comment_repository(): + return PostgresCommentRepository(db_connection) + + +def get_create_comment_use_case(): + return CreateCommentUseCase(get_comment_repository(), get_log_use_case()) + + +def get_get_comment_use_case(): + return GetCommentUseCase(get_comment_repository()) + + +def get_get_all_comments_use_case(): + return GetAllCommentsUseCase(get_comment_repository()) + + +def get_update_comment_use_case(): + return UpdateCommentUseCase(get_comment_repository(), get_log_use_case()) + + +''' Log related dependencies ''' + +def get_log_repository(): + return LogRepositoryImpl(db_connection) + + +def get_log_use_case(): + return LogUseCase(get_log_repository()) \ No newline at end of file diff --git a/src/presentation/api/routes/comments.py b/src/presentation/api/routes/comments.py new file mode 100644 index 0000000..85060f2 --- /dev/null +++ b/src/presentation/api/routes/comments.py @@ -0,0 +1,127 @@ +from fastapi import APIRouter, Depends, HTTPException, status + +from src.application.use_cases.comment_use_cases import ( + CreateCommentUseCase, + UpdateCommentUseCase, + GetAllCommentsUseCase, + GetCommentUseCase +) + +from src.domain.exceptions import EntityAlreadyExists, EntityNotFound, ValidationError + +from src.presentation.api.dependencies import ( + get_create_comment_use_case, + get_update_comment_use_case, + get_get_all_comments_use_case, + get_get_comment_use_case +) +from src.presentation.schemas.comment_schemas import ( + CommentCreateRequest, + CommentUpdateRequest, + CommentResponse, + GetAllCommentsResponse, PaginatedCommentsResponse +) + + +router = APIRouter(prefix="/comments", tags=["comments"]) + + +@router.post("/", response_model=CommentResponse, status_code=status.HTTP_201_CREATED) +async def create_comment( + request: CommentCreateRequest, + use_case: CreateCommentUseCase = Depends(get_create_comment_use_case), +): + try: + comment = await use_case.execute( + user_id=request.user_id, + content=request.content, + ) + return CommentResponse( + post_id=comment.post_id, + user_id=comment.user_id, + content=comment.content, + created_at=comment.created_at, + updated_at=comment.updated_at, + ) + except EntityAlreadyExists as e: + raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e)) + except ValidationError as e: + raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e)) + + +@router.get("/all", response_model=GetAllCommentsResponse) +async def get_all_comments( + use_case: GetAllCommentsUseCase = Depends(get_get_all_comments_use_case) +): + try: + comments = await use_case.execute() + return GetAllCommentsResponse( + comments=[ + CommentResponse( + post_id=comment.post_id, + user_id=comment.user_id, + content=comment.content, + created_at=comment.created_at, + updated_at=comment.updated_at, + ) for comment in comments + ] + ) + except EntityNotFound as e: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) + +@router.get("/", response_model=PaginatedCommentsResponse) +async def get_comments_with_pagination( + page: int = 1, + limit: int = 10, + sort: str = "created_at", + order: str = "desc", + use_case: GetCommentUseCase = Depends(get_get_comment_use_case) +): + if page < 1: + raise HTTPException(status_code=400, detail="page must be >= 1") + if not (1 <= limit <= 100): + raise HTTPException(status_code=400, detail="limit must be between 1 and 100") + try: + result = await use_case.execute( + page=page, + limit=limit, + sort=sort, + order=order + ) + return PaginatedCommentsResponse( + comments=[ + CommentResponse( + post_id=comment.post_id, + user_id=comment.user_id, + content=comment.content, + created_at=comment.created_at, + updated_at=comment.updated_at, + ) for comment in result["comments"] + ], + total=result["total"], + page=page, + limit=limit, + ) + except EntityNotFound as e: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) + +@router.put("/{post_id}", response_model=CommentResponse) +async def update_comment( + post_id: int, + request: CommentUpdateRequest, + use_case: UpdateCommentUseCase = Depends(get_update_comment_use_case) +): + try: + comment = await use_case.execute( + post_id=post_id, + content=request.content, + ) + return CommentResponse( + post_id=comment.post_id, + user_id=comment.user_id, + content=comment.content, + created_at=comment.created_at, + updated_at=comment.updated_at, + ) + except EntityNotFound as e: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) \ No newline at end of file diff --git a/src/presentation/schemas/comment_schemas.py b/src/presentation/schemas/comment_schemas.py new file mode 100644 index 0000000..59ec331 --- /dev/null +++ b/src/presentation/schemas/comment_schemas.py @@ -0,0 +1,32 @@ +from datetime import datetime +from typing import Optional +from pydantic import BaseModel, ConfigDict + +class CommentCreateRequest(BaseModel): + user_id: int + content: str + + +class CommentResponse(BaseModel): + model_config = ConfigDict(from_attributes=True) + + post_id : int + user_id: int + content: str + created_at: datetime + updated_at: datetime + + +class GetAllCommentsResponse(BaseModel): + comments: list[CommentResponse] + + +class PaginatedCommentsResponse(BaseModel): + comments: list[CommentResponse] + total: int + page: int + limit: int + + +class CommentUpdateRequest(BaseModel): + content: Optional[str] = None \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index be57ec7..a4f0294 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,7 @@ from src.infrastructure.database.connection import db_connection from src.presentation.api.routes.users import router as users_router +from src.presentation.api.routes.comments import router as comments_router @pytest_asyncio.fixture(scope="function") @@ -15,10 +16,13 @@ async def client(): app = FastAPI(title="Test App") app.include_router(users_router) + app.include_router(comments_router) pool = db_connection.pool async with pool.acquire() as conn: await conn.execute("truncate table users cascade;") + await conn.execute("truncate table comments cascade;") + await conn.execute("truncate table logs cascade;") async with AsyncClient( transport=ASGITransport(app=app), @@ -28,4 +32,6 @@ async def client(): async with pool.acquire() as conn: await conn.execute("truncate table users cascade;") + await conn.execute("truncate table comments cascade;") + await conn.execute("truncate table logs cascade;") diff --git a/tests/test_comments.py b/tests/test_comments.py new file mode 100644 index 0000000..751d955 --- /dev/null +++ b/tests/test_comments.py @@ -0,0 +1,96 @@ +from os.path import defpath + +from httpx import AsyncClient + + +async def test_create_comment(client: AsyncClient): + user_data = { + "email": "test@example.com", + "name": "Test User" + } + user_response = await client.post("/users/", json=user_data) + user_id = user_response.json()["id"] + + comment_data = { + "user_id": user_id, + "content": "This is a test comment." + } + response = await client.post("/comments/", json=comment_data) + assert response.status_code == 201 + + data = response.json() + assert data["user_id"] == user_id + assert data["content"] == comment_data["content"] + assert "post_id" in data + assert "created_at" in data + assert "updated_at" in data + + +async def test_get_all_comments(client: AsyncClient): + user_data = {"email": "test@example.com", "name": "Test User"} + user_response = await client.post("/users/", json=user_data) + user_id = user_response.json()["id"] + + for i in range(3): + await client.post("/comments/", json={ + "user_id": user_id, + "content": f"Comment {i}" + }) + + response = await client.get("/comments/all") + assert response.status_code == 200 + + data = response.json() + assert "comments" in data + assert isinstance(data["comments"], list) + assert len(data["comments"]) >= 3 + assert all("content" in c for c in data["comments"]) + + +async def test_get_comments_pagination(client: AsyncClient): + user_data = {"email": "test@example.com", "name": "Tester"} + user_response = await client.post("/users/", json=user_data) + user_id = user_response.json()["id"] + + # Создаём 15 комментариев + for i in range(15): + await client.post("/comments/", json={"user_id": user_id, "content": f"Comment {i}"}) + + response = await client.get("/comments/?page=2&limit=5&order=desc") + assert response.status_code == 200 + + data = response.json() + assert "comments" in data + assert data["page"] == 2 + assert data["limit"] == 5 + assert isinstance(data["comments"], list) + assert len(data["comments"]) == 5 + + +async def test_update_comment(client: AsyncClient): + user_data = { + "email": "test@example.com", + "name": "Test User" + } + user_response = await client.post("/users/", json=user_data) + user_id = user_response.json()["id"] + + comment_data = { + "user_id": user_id, + "content": "This is a test comment." + } + create_response = await client.post("/comments/", json=comment_data) + post_id = create_response.json()["post_id"] + + update_data = { + "content": "This is an updated test comment." + } + response = await client.put(f"/comments/{post_id}", json=update_data) + assert response.status_code == 200 + + data = response.json() + assert data["post_id"] == post_id + assert data["user_id"] == user_id + assert data["content"] == update_data["content"] + assert "created_at" in data + assert "updated_at" in data \ No newline at end of file