diff --git a/databases/gophertalk-backend-flavours/fastapi/src/dto/post_dto.py b/databases/gophertalk-backend-flavours/fastapi/src/dto/post_dto.py index 6c963af..6dc04a5 100644 --- a/databases/gophertalk-backend-flavours/fastapi/src/dto/post_dto.py +++ b/databases/gophertalk-backend-flavours/fastapi/src/dto/post_dto.py @@ -34,4 +34,4 @@ class PostReadDTO(BaseModel): views_count: int class Config: - orm_mode = True + from_attributes = True diff --git a/databases/gophertalk-backend-flavours/fastapi/src/dto/user_dto.py b/databases/gophertalk-backend-flavours/fastapi/src/dto/user_dto.py index 3210fd9..9cca24c 100644 --- a/databases/gophertalk-backend-flavours/fastapi/src/dto/user_dto.py +++ b/databases/gophertalk-backend-flavours/fastapi/src/dto/user_dto.py @@ -65,4 +65,4 @@ class ReadUserDTO(BaseModel): status: int class Config: - orm_mode = True \ No newline at end of file + from_attributes = True \ No newline at end of file diff --git a/databases/gophertalk-backend-flavours/fastapi/tests/controllers/test_post_controller.py b/databases/gophertalk-backend-flavours/fastapi/tests/controllers/test_post_controller.py new file mode 100644 index 0000000..12181c4 --- /dev/null +++ b/databases/gophertalk-backend-flavours/fastapi/tests/controllers/test_post_controller.py @@ -0,0 +1,216 @@ +import pytest +from fastapi.testclient import TestClient +from unittest.mock import patch +from app import app + +client = TestClient(app) + + +@pytest.fixture +def mock_token_header(): + return {"Authorization": "Bearer mockToken"} + +@pytest.fixture +def mock_user_dto(): + return { + "id": 1, + "user_name": "test_user", + "first_name": "John", + "last_name": "Doe", + "status": 1 + } + +@pytest.fixture +def mock_update_dto(): + return { + "id": 1, + "user_name": "test_user", + "first_name": "Jane", + "last_name": "Smith", + "status": 1 + } + + +@pytest.fixture +def mock_post_dto(): + return { + "id": 1, + "user_id": 1, + "content": "Test post content", + "reply_to_id": None, + "created_at": "2024-01-01T10:00:00", + "likes_count": 0, + "views_count": 0 + } + + +@pytest.fixture +def mock_post_create_dto(): + return { + "content": "Test post content", + "reply_to_id": None + } + + +@pytest.fixture +def mock_posts_list(mock_post_dto): + return [mock_post_dto] + + +def test_get_all_posts_success(mock_token_header, mock_posts_list, mock_user): + with patch("controllers.post_controller.get_current_user", return_value=mock_user), \ + patch("services.post_service.get_all_posts", return_value=mock_posts_list): + response = client.get("/api/posts?limit=10&offset=0", headers=mock_token_header) + + assert response.status_code == 200 + assert response.json() == mock_posts_list + + +def test_get_all_posts_with_filters_success(mock_token_header, mock_posts_list, mock_user): + with patch("controllers.post_controller.get_current_user", return_value=mock_user), \ + patch("services.post_service.get_all_posts", return_value=mock_posts_list): + response = client.get( + "/api/posts?limit=5&offset=10&reply_to_id=2&owner_id=3&search=test", + headers=mock_token_header + ) + + assert response.status_code == 200 + assert response.json() == mock_posts_list + + +def test_get_all_posts_failure(mock_token_header, mock_user): + with patch("controllers.post_controller.get_current_user", return_value=mock_user), \ + patch("services.post_service.get_all_posts", side_effect=Exception("Service error")): + response = client.get("/api/posts", headers=mock_token_header) + + assert response.status_code == 400 + assert response.json() == {"detail": "Service error"} + + +def test_create_post_success(mock_token_header, mock_post_create_dto, mock_post_dto, mock_user): + with patch("controllers.post_controller.get_current_user", return_value=mock_user), \ + patch("services.post_service.create_post", return_value=mock_post_dto): + response = client.post("/api/posts", headers=mock_token_header, json=mock_post_create_dto) + + assert response.status_code == 201 + assert response.json() == mock_post_dto + + +def test_create_post_failure(mock_token_header, mock_post_create_dto, mock_user): + with patch("controllers.post_controller.get_current_user", return_value=mock_user), \ + patch("services.post_service.create_post", side_effect=Exception("Service error")): + response = client.post("/api/posts", headers=mock_token_header, json=mock_post_create_dto) + + assert response.status_code == 400 + assert response.json() == {"detail": "Service error"} + + +def test_create_post_validation_failure(mock_token_header, mock_user): + invalid_dto = {"content": ""} # пустой контент + with patch("controllers.post_controller.get_current_user", return_value=mock_user): + response = client.post("/api/posts", headers=mock_token_header, json=invalid_dto) + + assert response.status_code == 422 + + +def test_delete_post_success(mock_token_header, mock_user): + with patch("controllers.post_controller.get_current_user", return_value=mock_user), \ + patch("services.post_service.delete_post", return_value=None): + response = client.delete("/api/posts/1", headers=mock_token_header) + + assert response.status_code == 204 + + +def test_delete_post_invalid_id(mock_token_header, mock_user): + with patch("controllers.post_controller.get_current_user", return_value=mock_user): + response = client.delete("/api/posts/abc", headers=mock_token_header) + + assert response.status_code == 422 + + +def test_delete_post_not_found(mock_token_header, mock_user): + with patch("controllers.post_controller.get_current_user", return_value=mock_user), \ + patch("services.post_service.delete_post", side_effect=Exception("Post not found")): + response = client.delete("/api/posts/999", headers=mock_token_header) + + assert response.status_code == 404 + assert response.json() == {"detail": "Post not found"} + + +def test_delete_post_zero_id(mock_token_header, mock_user): + with patch("controllers.post_controller.get_current_user", return_value=mock_user): + response = client.delete("/api/posts/0", headers=mock_token_header) + + assert response.status_code == 422 + + +def test_view_post_success(mock_token_header, mock_user): + with patch("controllers.post_controller.get_current_user", return_value=mock_user), \ + patch("services.post_service.view_post", return_value=None): + response = client.post("/api/posts/1/view", headers=mock_token_header) + + assert response.status_code == 201 + + +def test_view_post_invalid_id(mock_token_header, mock_user): + with patch("controllers.post_controller.get_current_user", return_value=mock_user): + response = client.post("/api/posts/abc/view", headers=mock_token_header) + + assert response.status_code == 422 + + +def test_view_post_not_found(mock_token_header, mock_user): + with patch("controllers.post_controller.get_current_user", return_value=mock_user), \ + patch("services.post_service.view_post", side_effect=Exception("Post not found")): + response = client.post("/api/posts/999/view", headers=mock_token_header) + + assert response.status_code == 404 + assert response.json() == {"detail": "Post not found"} + + +def test_like_post_success(mock_token_header, mock_user): + with patch("controllers.post_controller.get_current_user", return_value=mock_user), \ + patch("services.post_service.like_post", return_value=None): + response = client.post("/api/posts/1/like", headers=mock_token_header) + + assert response.status_code == 201 + + +def test_like_post_invalid_id(mock_token_header, mock_user): + with patch("controllers.post_controller.get_current_user", return_value=mock_user): + response = client.post("/api/posts/abc/like", headers=mock_token_header) + + assert response.status_code == 422 + + +def test_like_post_not_found(mock_token_header, mock_user): + with patch("controllers.post_controller.get_current_user", return_value=mock_user), \ + patch("services.post_service.like_post", side_effect=Exception("Post not found")): + response = client.post("/api/posts/999/like", headers=mock_token_header) + + assert response.status_code == 404 + assert response.json() == {"detail": "Post not found"} + + +def test_dislike_post_success(mock_token_header, mock_user): + with patch("controllers.post_controller.get_current_user", return_value=mock_user), \ + patch("services.post_service.dislike_post", return_value=None): + response = client.delete("/api/posts/1/like", headers=mock_token_header) + + assert response.status_code == 204 + + +def test_dislike_post_invalid_id(mock_token_header, mock_user): + with patch("controllers.post_controller.get_current_user", return_value=mock_user): + response = client.delete("/api/posts/abc/like", headers=mock_token_header) + + assert response.status_code == 422 + + +def test_dislike_post_not_found(mock_token_header, mock_user): + with patch("controllers.post_controller.get_current_user", return_value=mock_user), \ + patch("services.post_service.dislike_post", side_effect=Exception("Post not found")): + response = client.delete("/api/posts/999/like", headers=mock_token_header) + + assert response.status_code == 404 + assert response.json() == {"detail": "Post not found"} \ No newline at end of file diff --git a/docs/databases/development/python/coding/controller.md b/docs/databases/development/python/coding/controller.md index 2803b62..e843d45 100644 --- a/docs/databases/development/python/coding/controller.md +++ b/docs/databases/development/python/coding/controller.md @@ -139,7 +139,7 @@ DTO (Data Transfer Object) — это простой объект, которы Создайте в папке `src/dto` (DTO - data transfer object) файл `auth_dto.py` и добавьте в него следующий код: ```python -from pydantic import BaseModel, Field, validator +from pydantic import BaseModel, Field, field_validator, model_validator import regex as re def username_validator(value: str) -> str: @@ -163,23 +163,40 @@ class LoginDTO(BaseModel): user_name: str = Field(..., min_length=5, max_length=30) password: str = Field(..., min_length=5, max_length=30) - _validate_username = validator("user_name", allow_reuse=True)(username_validator) - _validate_password = validator("password", allow_reuse=True)(password_validator) + @field_validator("user_name") + @classmethod + def validate_username(cls, value: str) -> str: + return username_validator(value) + @field_validator("password") + @classmethod + def validate_password(cls, value: str) -> str: + return password_validator(value) class RegisterDTO(LoginDTO): password_confirm: str = Field(..., min_length=5, max_length=30) first_name: str = Field(..., min_length=1, max_length=30) last_name: str = Field(..., min_length=1, max_length=30) - _validate_password_confirm = validator("password_confirm", allow_reuse=True)(password_validator) - _validate_first_name = validator("first_name", allow_reuse=True)(name_validator) - _validate_last_name = validator("last_name", allow_reuse=True)(name_validator) + @field_validator("password_confirm") + @classmethod + def validate_password_confirm(cls, value: str) -> str: + return password_validator(value) + + @field_validator("first_name") + @classmethod + def validate_first_name(cls, value: str) -> str: + return name_validator(value) + + @field_validator("last_name") + @classmethod + def validate_last_name(cls, value: str) -> str: + return name_validator(value) - @validator("password_confirm") - def passwords_match(cls, v, values): - if "password" in values and v != values["password"]: + @model_validator(mode='after') + def passwords_match(self): + if self.password != self.password_confirm: raise ValueError("Passwords must match") - return v + return self ``` Этот файл содержит схемы валидации для тела запроса (`request.body`) при авторизации пользователей. @@ -217,8 +234,14 @@ class LoginDTO(BaseModel): user_name: str = Field(..., min_length=5, max_length=30) password: str = Field(..., min_length=5, max_length=30) - _validate_username = validator("user_name", allow_reuse=True)(username_validator) - _validate_password = validator("password", allow_reuse=True)(password_validator) + @field_validator("user_name") + @classmethod + def validate_username(cls, value: str) -> str: + return username_validator(value) + @field_validator("password") + @classmethod + def validate_password(cls, value: str) -> str: + return password_validator(value) ``` Проверяет `user_name` и `password` при логине. @@ -229,15 +252,26 @@ class RegisterDTO(LoginDTO): first_name: str = Field(..., min_length=1, max_length=30) last_name: str = Field(..., min_length=1, max_length=30) - _validate_password_confirm = validator("password_confirm", allow_reuse=True)(password_validator) - _validate_first_name = validator("first_name", allow_reuse=True)(name_validator) - _validate_last_name = validator("last_name", allow_reuse=True)(name_validator) + @field_validator("password_confirm") + @classmethod + def validate_password_confirm(cls, value: str) -> str: + return password_validator(value) + + @field_validator("first_name") + @classmethod + def validate_first_name(cls, value: str) -> str: + return name_validator(value) + + @field_validator("last_name") + @classmethod + def validate_last_name(cls, value: str) -> str: + return name_validator(value) - @validator("password_confirm") - def passwords_match(cls, v, values): - if "password" in values and v != values["password"]: + @model_validator(mode='after') + def passwords_match(self): + if self.password != self.password_confirm: raise ValueError("Passwords must match") - return v + return self ``` Проверяет: @@ -245,7 +279,7 @@ class RegisterDTO(LoginDTO): - `user_name`, `password`, `password_confirm` (по тем же схемам, что и при логине). - `first_name` и `last_name` — строки длиной от `1` до `30` символов, только буквенные символы, включая буквы любых алфавитов (`\p{L}`). -Дополнительная проверка реализована через `@validator("password_confirm")`: +Дополнительная проверка реализована через `@model_validator(mode='after')`: - `password` и `password_confirm` должны совпадать, иначе будет выброшена ошибка "Passwords must match". @@ -583,7 +617,7 @@ class ReadUserDTO(BaseModel): status: int class Config: - orm_mode = True + from_attributes = True ``` Далее необходимо обновить `app.py`, добавив две строки (выделены цветом): @@ -920,282 +954,231 @@ def dislike_post_handler(post_id: int = Path(..., gt=0), user=Depends(get_curren ``` -В каталоге `src/validators` создайте файл `postValidators.js` и поместите туда код: +После этого нужно запустить сервер. Если все сделано правильно, он запустится без ошибок: -```python -import { z } from "zod"; - -export const createPostValidator = z.object({ - text: z.string().min(1).max(280), - reply_to_id: z - .number() - .optional() - .nullable() - .refine(val => val === undefined || val > 0, { - message: "ReplyToID must be greater than 0", - }), -}); - -export const filterPostValidator = z.object({ - search: z.string().optional(), - owner_id: z.string().regex(/^\d+$/).optional(), - user_id: z.string().regex(/^\d+$/).optional(), - reply_to_id: z.string().regex(/^\d+$/).optional(), - limit: z.string().regex(/^\d+$/).optional(), - offset: z.string().regex(/^\d+$/).optional(), -}); +```bash +python3 app.py ``` -Далее добавим маршруты. В каталоге `src/routes` создайте файл `postRoutes.js` и поместите туда код: +Самостоятельно проверьте эндпоинты из папки `posts` в Postman: + +- `get all` - получить все посты +- `delete` - удалить пост (можно удалить только свой пост; проверьте, что произойдет с записью поста в базе данных) +- `create` - создать пост +- `like` - поставить лайк +- `dislike` - удалить лайк +- `view` - просмотреть пост + +## Тестирование контроллера постов + +В каталоге `tests/controllers` создайте файл `test_post_controller.py` и поместите в него следующий код: + +::: details Unit тесты `post_controller` ```python -import express from "express"; -import { PostController } from "../controllers/postController.js"; -import { validate } from "../middleware/validate.js"; -import { requestAuth, requestAuthSameId } from "../middleware/auth.js"; -import { createPostValidator } from "../validators/postValidators.js"; +import pytest +from fastapi.testclient import TestClient +from unittest.mock import patch, MagicMock +from app import app + +client = TestClient(app) + + +@pytest.fixture +def mock_token_header(): + return {"Authorization": "Bearer mockToken"} -const router = express.Router(); -router.get("/", requestAuth(process.env.ACCESS_TOKEN_SECRET), PostController.getAllPosts); +@pytest.fixture +def mock_user(): + user = MagicMock() + user.sub = 1 + return user -router.post( - "/", - requestAuth(process.env.ACCESS_TOKEN_SECRET), - validate(createPostValidator), - PostController.createPost -); -router.delete("/:id", requestAuthSameId(process.env.ACCESS_TOKEN_SECRET), PostController.deletePost); +@pytest.fixture +def mock_post_dto(): + return { + "id": 1, + "user_id": 1, + "content": "Test post content", + "reply_to_id": None, + "created_at": "2024-01-01T10:00:00", + "likes_count": 0, + "views_count": 0 + } -router.post("/:id/view", requestAuth(process.env.ACCESS_TOKEN_SECRET), PostController.viewPost); -router.post("/:id/like", requestAuth(process.env.ACCESS_TOKEN_SECRET), PostController.likePost); +@pytest.fixture +def mock_post_create_dto(): + return { + "content": "Test post content", + "reply_to_id": None + } -router.post("/:id/dislike", requestAuth(process.env.ACCESS_TOKEN_SECRET), PostController.dislikePost); -export default router; -``` +@pytest.fixture +def mock_posts_list(mock_post_dto): + return [mock_post_dto] -Далее необходимо обновить `app.js`, добавив две строки (выделены зеленым цветом): -```python -import dotenv from "dotenv"; -import express from "express"; -import { pool } from "./config/db.js"; -import authRoutes from "./routes/authRoutes.js"; -import postRoutes from "./routes/postRoutes.js"; // [!code ++] -import userRoutes from "./routes/userRoutes.js"; - -dotenv.config(); - -const app = express(); -const PORT = process.env.PORT || 3000; - -app.use(express.json()); - -app.use("/api/auth", authRoutes); -app.use("/api/posts", postRoutes); // [!code ++] -app.use("/api/users", userRoutes); - -app.get("/api/health-check", async (req, res) => { - try { - await pool.query("SELECT 1"); - res.status(200).send("OK"); - } catch (err) { - res.status(500).send("DB connection failed"); - } -}); - -app.listen(PORT, () => { - console.log(`Server is running on port ${PORT}`); -}); -``` +def test_get_all_posts_success(mock_token_header, mock_posts_list, mock_user): + with patch("dependencies.auth.get_current_user", return_value=mock_user), \ + patch("controllers.post_controller.get_all_posts", return_value=mock_posts_list): + response = client.get("/api/posts?limit=10&offset=0", headers=mock_token_header) -После этого нужно запустить сервер. Если все сделано правильно, он запустится без ошибок: + assert response.status_code == 200 + assert response.json() == mock_posts_list -```bash -npm run dev -> gophertalk-backend-express@0.1.0 dev -> nodemon src/app.js +def test_get_all_posts_with_filters_success(mock_token_header, mock_posts_list, mock_user): + with patch("dependencies.auth.get_current_user", return_value=mock_user), \ + patch("controllers.post_controller.get_all_posts", return_value=mock_posts_list): + response = client.get( + "/api/posts?limit=5&offset=10&reply_to_id=2&owner_id=3&search=test", + headers=mock_token_header + ) -[nodemon] 3.1.9 -[nodemon] to restart at any time, enter `rs` -[nodemon] watching path(s): *.* -[nodemon] watching extensions: js,mjs,cjs,json -[nodemon] starting `node src/app.js` -Server is running on port 3000 -``` + assert response.status_code == 200 + assert response.json() == mock_posts_list -Самостоятельно проверьте эндпоинты из папки `users` в Postman: -- `get all` - получить все посты -- `delete` - удалить пост (можно удалить только свой пост; проверьте, что произойдет с записью поста в базе данных) -- `create` - создать пост -- `like` - поставить лайк -- `dislike` - удалить лайк -- `view` - просмотреть пост +def test_get_all_posts_failure(mock_token_header, mock_user): + with patch("dependencies.auth.get_current_user", return_value=mock_user), \ + patch("controllers.post_controller.get_all_posts", side_effect=Exception("Service error")): + response = client.get("/api/posts", headers=mock_token_header) -## Тестирование контроллера постов + assert response.status_code == 400 + assert response.json() == {"detail": "Service error"} -В каталоге `__tests__/controllers` создайте файл `postController.test.js` и поместите в него следующий код: -::: details Unit тесты postController +def test_create_post_success(mock_token_header, mock_post_create_dto, mock_post_dto, mock_user): + with patch("dependencies.auth.get_current_user", return_value=mock_user), \ + patch("controllers.post_controller.create_post", return_value=mock_post_dto): + response = client.post("/api/posts", headers=mock_token_header, json=mock_post_create_dto) -```python -import { expect, jest } from "@jest/globals"; -import dotenv from "dotenv"; -import express from "express"; -import jwt from "jsonwebtoken"; -import request from "supertest"; -import { PostController } from "../../src/controllers/postController.js"; -import { requestAuth } from "../../src/middleware/auth.js"; -import { validate } from "../../src/middleware/validate.js"; -import { PostService } from "../../src/services/postService.js"; -import { createPostValidator } from "../../src/validators/postValidators.js"; + assert response.status_code == 201 + assert response.json() == mock_post_dto + + +def test_create_post_failure(mock_token_header, mock_post_create_dto, mock_user): + with patch("dependencies.auth.get_current_user", return_value=mock_user), \ + patch("controllers.post_controller.create_post", side_effect=Exception("Service error")): + response = client.post("/api/posts", headers=mock_token_header, json=mock_post_create_dto) -dotenv.config(); + assert response.status_code == 400 + assert response.json() == {"detail": "Service error"} -const app = express(); -app.use(express.json()); -app.use((req, res, next) => { - const token = jwt.sign({ sub: "1" }, process.env.ACCESS_TOKEN_SECRET); - req.headers.authorization = `Bearer ${token}`; - requestAuth(process.env.ACCESS_TOKEN_SECRET)(req, res, next); -}); +def test_create_post_validation_failure(mock_token_header, mock_user): + invalid_dto = {"content": ""} # пустой контент + with patch("dependencies.auth.get_current_user", return_value=mock_user): + response = client.post("/api/posts", headers=mock_token_header, json=invalid_dto) -app.get("/api/posts", PostController.getAllPosts); -app.post("/api/posts", validate(createPostValidator), PostController.createPost); -app.delete("/api/posts/:id", PostController.deletePost); -app.post("/api/posts/:id/view", PostController.viewPost); -app.post("/api/posts/:id/like", PostController.likePost); -app.delete("/api/posts/:id/like", PostController.dislikePost); + assert response.status_code == 422 -describe("PostController", () => { - afterEach(() => { - jest.clearAllMocks(); - }); - describe("GET /api/posts", () => { - it("should fetch posts successfully", async () => { - const posts = [{ id: 1, text: "Test post" }]; - jest.spyOn(PostService, "getAllPosts").mockResolvedValueOnce(posts); +def test_delete_post_success(mock_token_header, mock_user): + with patch("dependencies.auth.get_current_user", return_value=mock_user), \ + patch("controllers.post_controller.delete_post", return_value=None): + response = client.delete("/api/posts/1", headers=mock_token_header) - const res = await request(app).get("/api/posts?limit=10&offset=0"); + assert response.status_code == 204 - expect(res.status).toBe(200); - expect(res.body).toEqual(posts); - expect(PostService.getAllPosts).toHaveBeenCalled(); - }); - it("should handle service error", async () => { - jest.spyOn(PostService, "getAllPosts").mockRejectedValueOnce(new Error("Service error")); +def test_delete_post_invalid_id(mock_token_header, mock_user): + with patch("dependencies.auth.get_current_user", return_value=mock_user): + response = client.delete("/api/posts/abc", headers=mock_token_header) - const res = await request(app).get("/api/posts?limit=10&offset=0"); + assert response.status_code == 422 - expect(res.status).toBe(400); - expect(PostService.getAllPosts).toHaveBeenCalled(); - }); - }); - describe("POST /api/posts", () => { - it("should create a post successfully", async () => { - const post = { id: 1, text: "New post" }; - jest.spyOn(PostService, "createPost").mockResolvedValueOnce(post); +def test_delete_post_not_found(mock_token_header, mock_user): + with patch("dependencies.auth.get_current_user", return_value=mock_user), \ + patch("controllers.post_controller.delete_post", side_effect=Exception("Post not found")): + response = client.delete("/api/posts/999", headers=mock_token_header) - const res = await request(app).post("/api/posts").send({ text: "New post" }); + assert response.status_code == 404 + assert response.json() == {"detail": "Post not found"} - expect(res.status).toBe(201); - expect(res.body).toEqual(post); - expect(PostService.createPost).toHaveBeenCalled(); - }); - it("should handle validation error", async () => { - const res = await request(app).post("/api/posts").send({}); +def test_delete_post_zero_id(mock_token_header, mock_user): + with patch("dependencies.auth.get_current_user", return_value=mock_user): + response = client.delete("/api/posts/0", headers=mock_token_header) - expect(res.status).toBe(422); - }); + assert response.status_code == 422 - it("should handle service error", async () => { - jest.spyOn(PostService, "createPost").mockRejectedValueOnce(new Error("Service error")); - const res = await request(app).post("/api/posts").send({ text: "New post" }); +def test_view_post_success(mock_token_header, mock_user): + with patch("dependencies.auth.get_current_user", return_value=mock_user), \ + patch("controllers.post_controller.view_post", return_value=None): + response = client.post("/api/posts/1/view", headers=mock_token_header) + + assert response.status_code == 201 - expect(res.status).toBe(400); - expect(PostService.createPost).toHaveBeenCalled(); - }); - }); - describe("DELETE /api/posts/:id", () => { - it("should delete post successfully", async () => { - jest.spyOn(PostService, "deletePost").mockResolvedValueOnce(); +def test_view_post_invalid_id(mock_token_header, mock_user): + with patch("dependencies.auth.get_current_user", return_value=mock_user): + response = client.post("/api/posts/abc/view", headers=mock_token_header) - const res = await request(app).delete("/api/posts/1"); + assert response.status_code == 422 - expect(res.status).toBe(204); - expect(PostService.deletePost).toHaveBeenCalled(); - }); - it("should handle invalid id", async () => { - const res = await request(app).delete("/api/posts/abc"); +def test_view_post_not_found(mock_token_header, mock_user): + with patch("dependencies.auth.get_current_user", return_value=mock_user), \ + patch("controllers.post_controller.view_post", side_effect=Exception("Post not found")): + response = client.post("/api/posts/999/view", headers=mock_token_header) - expect(res.status).toBe(404); - }); - }); + assert response.status_code == 404 + assert response.json() == {"detail": "Post not found"} + + +def test_like_post_success(mock_token_header, mock_user): + with patch("dependencies.auth.get_current_user", return_value=mock_user), \ + patch("controllers.post_controller.like_post", return_value=None): + response = client.post("/api/posts/1/like", headers=mock_token_header) + + assert response.status_code == 201 - describe("POST /api/posts/:id/view", () => { - it("should view post successfully", async () => { - jest.spyOn(PostService, "viewPost").mockResolvedValueOnce(); - const res = await request(app).post("/api/posts/1/view"); +def test_like_post_invalid_id(mock_token_header, mock_user): + with patch("dependencies.auth.get_current_user", return_value=mock_user): + response = client.post("/api/posts/abc/like", headers=mock_token_header) - expect(res.status).toBe(201); - expect(PostService.viewPost).toHaveBeenCalled(); - }); + assert response.status_code == 422 - it("should handle invalid id", async () => { - const res = await request(app).post("/api/posts/abc/view"); - expect(res.status).toBe(404); - }); - }); +def test_like_post_not_found(mock_token_header, mock_user): + with patch("dependencies.auth.get_current_user", return_value=mock_user), \ + patch("controllers.post_controller.like_post", side_effect=Exception("Post not found")): + response = client.post("/api/posts/999/like", headers=mock_token_header) - describe("POST /api/posts/:id/like", () => { - it("should like post successfully", async () => { - jest.spyOn(PostService, "likePost").mockResolvedValueOnce(); + assert response.status_code == 404 + assert response.json() == {"detail": "Post not found"} - const res = await request(app).post("/api/posts/1/like"); - expect(res.status).toBe(201); - expect(PostService.likePost).toHaveBeenCalled(); - }); +def test_dislike_post_success(mock_token_header, mock_user): + with patch("dependencies.auth.get_current_user", return_value=mock_user), \ + patch("controllers.post_controller.dislike_post", return_value=None): + response = client.delete("/api/posts/1/like", headers=mock_token_header) - it("should handle invalid id", async () => { - const res = await request(app).post("/api/posts/abc/like"); + assert response.status_code == 204 - expect(res.status).toBe(404); - }); - }); - describe("DELETE /api/posts/:id/like", () => { - it("should dislike post successfully", async () => { - jest.spyOn(PostService, "dislikePost").mockResolvedValueOnce(); +def test_dislike_post_invalid_id(mock_token_header, mock_user): + with patch("dependencies.auth.get_current_user", return_value=mock_user): + response = client.delete("/api/posts/abc/like", headers=mock_token_header) - const res = await request(app).delete("/api/posts/1/like"); + assert response.status_code == 422 - expect(res.status).toBe(204); - expect(PostService.dislikePost).toHaveBeenCalled(); - }); - it("should handle invalid id", async () => { - const res = await request(app).delete("/api/posts/abc/like"); +def test_dislike_post_not_found(mock_token_header, mock_user): + with patch("dependencies.auth.get_current_user", return_value=mock_user), \ + patch("controllers.post_controller.dislike_post", side_effect=Exception("Post not found")): + response = client.delete("/api/posts/999/like", headers=mock_token_header) - expect(res.status).toBe(404); - }); - }); -}); + assert response.status_code == 404 + assert response.json() == {"detail": "Post not found"} ``` ::: diff --git a/docs/databases/development/python/coding/repository.md b/docs/databases/development/python/coding/repository.md index 2fbb84e..1f398aa 100644 --- a/docs/databases/development/python/coding/repository.md +++ b/docs/databases/development/python/coding/repository.md @@ -56,6 +56,7 @@ erDiagram - Используйте типы данных и ограничения согласно описанию. - Настройте первичные и внешние ключи. +- Сделайте проставление `created_at` автоматическим. - Создайте уникальный индекс по `user_name`, но только для не удалённых пользователей (`deleted_at IS NULL`). - Убедитесь, что `status` может быть только `0` или `1`. @@ -597,7 +598,7 @@ def test_create_user_error(mock_conn): ) def test_get_all_users_success(mock_conn): - now = datetime.utcnow() + now = datetime.now() expected = [{ "id": 1, "user_name": "john", @@ -634,7 +635,7 @@ def test_get_all_users_error(mock_conn): def test_get_user_by_id_success(mock_conn): - now = datetime.utcnow() + now = datetime.now() expected = { "user_name": "john", "first_name": "John", @@ -687,7 +688,7 @@ def test_get_user_by_username_not_found(mock_conn): def test_update_user_success(mock_conn): - now = datetime.utcnow() + now = datetime.now() earlier = now - timedelta(hours=1) dto = { @@ -754,7 +755,7 @@ def test_delete_user_not_found(mock_conn): После этого выполните команду ```bash -pytest-v +pytest -v ``` Если вы все сделали правильно, все тесты пройдены. @@ -1212,7 +1213,7 @@ def like_post(post_id: int, user_id: int) -> None: try: with pool.connection() as conn: with conn.cursor() as cur: - cur.execute(query, (post_id, user_id, post_id)) + cur.execute(query, (post_id, user_id)) if cur.rowcount == 0: raise ValueError("Post not found") except UniqueViolation as err: @@ -1237,7 +1238,7 @@ def dislike_post(post_id: int, user_id: int) -> None: with pool.connection() as conn: with conn.cursor() as cur: - cur.execute(query, (post_id, user_id, post_id)) + cur.execute(query, (post_id, user_id)) if cur.rowcount == 0: raise ValueError("Post not found") ``` @@ -1283,7 +1284,7 @@ def test_create_post_success(mock_conn): expected = { "id": 1, "text": dto["text"], - "created_at": datetime.utcnow(), + "created_at": datetime.now(), "reply_to_id": None, } @@ -1336,7 +1337,7 @@ def test_create_post_success(mock_conn): expected = { "id": 1, "text": dto["text"], - "created_at": datetime.utcnow(), + "created_at": datetime.now(), "reply_to_id": None, } @@ -1502,7 +1503,7 @@ def test_get_all_posts_error(mock_conn): def test_get_post_by_id_success(mock_conn): user_id = 1 post_id = 1 - now = datetime.utcnow() + now = datetime.now() row = { "post_id": post_id, @@ -1668,7 +1669,7 @@ def test_like_post_success(mock_conn): assert "insert into likes (post_id, user_id)" in normalized_sql params = mock_cursor.execute.call_args[0][1] - assert params == (post_id, user_id, post_id) + assert params == (post_id, user_id) def test_like_post_error(mock_conn): @@ -1687,7 +1688,7 @@ def test_like_post_error(mock_conn): assert "insert into likes (post_id, user_id)" in normalized_sql params = mock_cursor.execute.call_args[0][1] - assert params == (post_id, user_id, post_id) + assert params == (post_id, user_id) def test_like_post_already_liked(mock_conn): @@ -1718,7 +1719,7 @@ def test_dislike_post_success(mock_conn): assert "delete from likes where post_id = %s and user_id = %s" in normalized_sql params = mock_cursor.execute.call_args[0][1] - assert params == (post_id, user_id, post_id) + assert params == (post_id, user_id) def test_dislike_post_error(mock_conn): @@ -1737,7 +1738,7 @@ def test_dislike_post_error(mock_conn): assert "delete from likes where post_id = %s and user_id = %s" in normalized_sql params = mock_cursor.execute.call_args[0][1] - assert params == (post_id, user_id, post_id) + assert params == (post_id, user_id) def test_dislike_post_not_found(mock_conn): @@ -1756,7 +1757,7 @@ def test_dislike_post_not_found(mock_conn): assert "delete from likes where post_id = %s and user_id = %s" in normalized_sql params = mock_cursor.execute.call_args[0][1] - assert params == (post_id, user_id, post_id) + assert params == (post_id, user_id) ``` ::: diff --git a/docs/databases/development/python/design/database-connection.md b/docs/databases/development/python/design/database-connection.md index f08612d..0074461 100644 --- a/docs/databases/development/python/design/database-connection.md +++ b/docs/databases/development/python/design/database-connection.md @@ -20,6 +20,28 @@ Благодаря этому мы сможем использовать **всю мощь PostgreSQL прямо из кода на Python** — просто, эффективно и без необходимости использовать ORM. +## Установка библиотеки + +Прежде чем приступить к изучению возможностей адаптера `psycopg`, необходимо установить соответствующую библиотеку для Python. + +Как указано в официальной документации, сначала убедитесь, что версия менеджера пакетов `pip` не ниже 20.3: + +```bash +pip install --upgrade pip +``` + +Затем установите модуль `psycopg[binary]`, который не требует наличия системных зависимостей: + +```bash +pip install "psycopg[binary]" +``` + +Установите `psycopg_pool`: + +```bash +pip install psycopg_pool +``` + ## Основы API библиотеки `psycopg` Библиотека `psycopg` предоставляет несколько ключевых интерфейсов для работы с PostgreSQL в соответствии со стандартом [**Python DB API 2.0**](https://peps.python.org/pep-0249/). @@ -61,30 +83,25 @@ conn.close() - По умолчанию `psycopg` использует неавтоматические транзакции: нужно вызывать `conn.commit()` или `conn.rollback()`. Параметры подключения: - -```python -psycopg.connect( - host="localhost", - port=5432, - user="postgres", - password="secret", - dbname="mydb", - connect_timeout=10, - application_name="myapp" -) - -``` +- **host** — адрес сервера PostgreSQL (Например, `localhost` для локального подключения); +- **port** — порт, на котором работает PostgreSQL (По умолчанию - 5432); +- **user** — имя пользователя PostgreSQL; +- **password** — пароль пользователя; +- **dbname** — имя базы данных, к которой осуществляется подключение; +- **connect_timeout** (необязательный параметр) — таймаут в секундах при установке соединения. Также можно использовать DSN-строку: ```python -psycopg.connect("postgresql://postgres:secret@localhost:5432/mydb") +psycopg.connect(conninfo="postgresql://postgres:secret@localhost:5432/mydb") ``` ### 2. Пул подключений (рекомендуемый способ) Объект `ConnectionPool` из модуля `psycopg_pool` управляет множеством соединений с базой данных. Это более производительный и устойчивый способ подключения в реальных приложениях. +pip install psycopg_pool + #### Пример: ```python @@ -277,14 +294,12 @@ pool = ConnectionPool( ```python import os - from dotenv import load_dotenv from fastapi import FastAPI, Response, status +from config.db import pool load_dotenv() -from config.db import pool - app = FastAPI() port = int(os.getenv("PORT", 3000)) diff --git a/package-lock.json b/package-lock.json index fed451b..876b49d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "courses", + "name": "shekshuev.github.io", "lockfileVersion": 3, "requires": true, "packages": {