Skip to content

Commit 86e2de0

Browse files
committed
test: added pytest for the create blog route
1 parent b3e962a commit 86e2de0

File tree

7 files changed

+169
-6
lines changed

7 files changed

+169
-6
lines changed

api/db/database.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
#!/usr/bin/env python3
22
"""The database module"""
33

4-
from sqlalchemy.ext.declarative import declarative_base
5-
from sqlalchemy.orm import sessionmaker, scoped_session
4+
from sqlalchemy.orm import declarative_base, sessionmaker, scoped_session
65
from sqlalchemy import create_engine
76
from api.utils.settings import settings, BASE_DIR
87

api/utils/logging_config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@
3030
"level": "DEBUG",
3131
"propagate": True,
3232
},
33+
"api": {
34+
"handlers": ["console", "file"],
35+
"level": "INFO",
36+
"propagate": False,
37+
},
3338
},
3439
}
3540

api/v1/models/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
from sqlalchemy.ext.declarative import declarative_base
1+
from sqlalchemy.orm import declarative_base
22

33
Base = declarative_base()

api/v1/routes/blog.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
blog = APIRouter(prefix="/blogs", tags=["blog"])
1010

11-
logger = logging.getLogger(__name__)
11+
logger = logging.getLogger("api")
1212

1313

1414
@blog.post(
@@ -20,6 +20,7 @@ def create_blog(blog: BlogCreateSchema, db: Session = Depends(get_db)):
2020
try:
2121
existing_blog = db.query(Blog).filter(Blog.title == blog.title).first()
2222
if existing_blog:
23+
logger.warning(f"Blog post with title '{blog.title}' already exists.")
2324
raise HTTPException(
2425
status_code=status.HTTP_409_CONFLICT,
2526
detail="A blog post with this title already exists.",

api/v1/schemas/blog.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ class BlogResponseSchema(BaseModel):
1515
created_at: datetime
1616
updated_at: datetime
1717

18-
class Config:
19-
from_attributes = True
18+
model_config = {"from_attributes": True}
2019

2120

2221
class BlogCreateSchema(BaseModel):
@@ -28,6 +27,7 @@ class BlogCreateSchema(BaseModel):
2827

2928
title: str = Field(
3029
...,
30+
min_length=1,
3131
max_length=255,
3232
description="Title of the blog post",
3333
)

tests/v1/blog/__init__.py

Whitespace-only changes.

tests/v1/blog/test_create_blog.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
from datetime import datetime, timezone
2+
import pytest
3+
from fastapi.testclient import TestClient
4+
from unittest.mock import MagicMock
5+
from main import app
6+
from api.db.database import get_db
7+
from api.v1.models.blog import Blog
8+
9+
client = TestClient(app)
10+
11+
12+
@pytest.fixture
13+
def db_session_mock():
14+
return MagicMock()
15+
16+
17+
@pytest.fixture(autouse=True)
18+
def override_get_db(db_session_mock):
19+
app.dependency_overrides[get_db] = lambda: db_session_mock
20+
yield
21+
app.dependency_overrides[get_db] = None
22+
23+
24+
def test_create_blog_success(db_session_mock):
25+
"""Checks if a new blog post can be created successfully and
26+
validates the response data."""
27+
new_blog_data = {
28+
"title": "New Blog Post",
29+
"excerpt": "A summary of the blog post...",
30+
"content": "The content of the blog post...",
31+
"image_url": "image-url-link",
32+
}
33+
34+
# Mocking the behavior of the database session
35+
db_session_mock.query.return_value.filter.return_value.first.return_value = None
36+
db_session_mock.add.side_effect = lambda x: setattr(x, "id", 1)
37+
db_session_mock.commit.side_effect = lambda: None
38+
db_session_mock.refresh.side_effect = lambda blog: setattr(
39+
blog, "created_at", datetime.now(timezone.utc)
40+
) or setattr(blog, "updated_at", datetime.now(timezone.utc))
41+
42+
# Creating the mock blog object
43+
blog_mock = Blog(**new_blog_data)
44+
blog_mock.id = 1
45+
blog_mock.created_at = datetime.now(timezone.utc)
46+
blog_mock.updated_at = datetime.now(timezone.utc)
47+
48+
db_session_mock.query.return_value.filter.return_value.first.return_value = None
49+
db_session_mock.add.side_effect = lambda x: setattr(x, "id", blog_mock.id)
50+
db_session_mock.commit.side_effect = lambda: None
51+
db_session_mock.refresh.side_effect = lambda x: setattr(
52+
x, "created_at", blog_mock.created_at
53+
) or setattr(x, "updated_at", blog_mock.updated_at)
54+
55+
response = client.post("/api/v1/blogs", json=new_blog_data)
56+
57+
assert response.status_code == 201
58+
response_data = response.json()
59+
assert response_data["title"] == new_blog_data["title"]
60+
assert response_data["excerpt"] == new_blog_data["excerpt"]
61+
assert response_data["content"] == new_blog_data["content"]
62+
assert response_data["image_url"] == new_blog_data["image_url"]
63+
assert "created_at" in response_data
64+
assert "updated_at" in response_data
65+
66+
67+
def test_create_blog_conflict(db_session_mock):
68+
"""Simulates a conflict scenario by creating a blog post with a title that
69+
already exists, then checks for the correct status code and error message."""
70+
# Arrange
71+
new_blog_data = {
72+
"title": "Existing Blog Post",
73+
"excerpt": "A summary of the blog post...",
74+
"content": "The content of the blog post...",
75+
"image_url": "image-url-link",
76+
}
77+
78+
# Mock the database query for checking existing blog
79+
db_session_mock.query.return_value.filter.return_value.first.return_value = Blog(
80+
title="Existing Blog Post", excerpt="...", content="...", image_url="..."
81+
)
82+
83+
# Act
84+
response = client.post("/api/v1/blogs", json=new_blog_data)
85+
86+
# Assert
87+
assert response.status_code == 409
88+
assert response.json()["detail"] == "A blog post with this title already exists."
89+
90+
91+
def test_create_blog_internal_server_error(db_session_mock):
92+
"""Simulates an internal server error and checks for the
93+
correct status code and error message."""
94+
new_blog_data = {
95+
"title": "New Blog Post",
96+
"excerpt": "A summary of the blog post...",
97+
"content": "The content of the blog post...",
98+
"image_url": "image-url-link",
99+
}
100+
101+
db_session_mock.query.side_effect = Exception("Unexpected error")
102+
103+
response = client.post("/api/v1/blogs", json=new_blog_data)
104+
105+
assert response.status_code == 500
106+
assert response.json()["detail"] == "Internal server error."
107+
108+
109+
def test_create_blog_invalid_data(db_session_mock):
110+
"""Sends invalid data (e.g., empty title) and checks for validation errors."""
111+
invalid_blog_data = {
112+
"title": "", # Title is required and cannot be empty
113+
"excerpt": "A summary of the blog post...",
114+
"content": "The content of the blog post...",
115+
"image_url": "image-url-link",
116+
}
117+
118+
response = client.post("/api/v1/blogs", json=invalid_blog_data)
119+
120+
# Unprocessable Entity (validation error)
121+
assert response.status_code == 422
122+
response_data = response.json()
123+
assert "detail" in response_data
124+
125+
126+
def test_create_blog_boundary_testing(db_session_mock):
127+
"""Tests the maximum length constraints for the title and excerpt fields,
128+
ensuring the API handles boundary conditions correctly."""
129+
130+
boundary_blog_data = {
131+
"title": "T" * 255, # Maximum allowed length for title
132+
"excerpt": "E" * 300, # Maximum allowed length for excerpt
133+
"content": "Content of the blog post...",
134+
"image_url": "image-url-link",
135+
}
136+
137+
# Mocking the behavior of the database session
138+
db_session_mock.query.return_value.filter.return_value.first.return_value = None
139+
db_session_mock.add.side_effect = lambda x: setattr(x, "id", 1)
140+
db_session_mock.commit.side_effect = lambda: None
141+
db_session_mock.refresh.side_effect = lambda x: setattr(
142+
x, "created_at", datetime.now(timezone.utc)
143+
) or setattr(x, "updated_at", datetime.now(timezone.utc))
144+
145+
response = client.post("/api/v1/blogs", json=boundary_blog_data)
146+
147+
assert response.status_code == 201
148+
response_data = response.json()
149+
assert response_data["title"] == boundary_blog_data["title"]
150+
assert response_data["excerpt"] == boundary_blog_data["excerpt"]
151+
assert response_data["content"] == boundary_blog_data["content"]
152+
assert response_data["image_url"] == boundary_blog_data["image_url"]
153+
assert "created_at" in response_data
154+
assert "updated_at" in response_data
155+
assert isinstance(
156+
response_data["created_at"], str
157+
) # Check if it's a string representation of a datetime
158+
assert isinstance(response_data["updated_at"], str)

0 commit comments

Comments
 (0)