Skip to content

Commit 8cd0fc5

Browse files
committed
feat: Blog List API Endpoint [Issue #3]
1 parent 86e2de0 commit 8cd0fc5

File tree

2 files changed

+181
-3
lines changed

2 files changed

+181
-3
lines changed

api/v1/routes/blog.py

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
from fastapi import APIRouter, Depends, HTTPException, status
1+
from typing import Any
2+
from fastapi import APIRouter, Depends, HTTPException, Query, status
23
from sqlalchemy.orm import Session
34
from sqlalchemy.exc import SQLAlchemyError
5+
from sqlalchemy import not_
46
from api.v1.models.blog import Blog
57
from api.v1.schemas.blog import BlogCreateSchema, BlogResponseSchema
68
from api.db.database import get_db
79
import logging
810

9-
blog = APIRouter(prefix="/blogs", tags=["blog"])
11+
blog = APIRouter(prefix="/blogs", tags=["Blog"])
1012

1113
logger = logging.getLogger("api")
1214

@@ -16,7 +18,7 @@
1618
response_model=BlogResponseSchema,
1719
status_code=status.HTTP_201_CREATED,
1820
)
19-
def create_blog(blog: BlogCreateSchema, db: Session = Depends(get_db)):
21+
async def create_blog(blog: BlogCreateSchema, db: Session = Depends(get_db)):
2022
try:
2123
existing_blog = db.query(Blog).filter(Blog.title == blog.title).first()
2224
if existing_blog:
@@ -55,3 +57,59 @@ def create_blog(blog: BlogCreateSchema, db: Session = Depends(get_db)):
5557
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
5658
detail="Internal server error.",
5759
)
60+
61+
62+
@blog.get("", status_code=status.HTTP_200_OK)
63+
async def list_blog(
64+
page: int = Query(1, ge=1),
65+
page_size: int = Query(10, ge=1, le=100),
66+
db: Session = Depends(get_db),
67+
) -> dict[str, Any]:
68+
try:
69+
offset = (page - 1) * page_size
70+
query = (
71+
db.query(Blog)
72+
.filter(not_(Blog.is_deleted))
73+
.order_by(Blog.created_at.desc())
74+
)
75+
total_count = query.count()
76+
blogs = query.offset(offset).limit(page_size).all()
77+
78+
next_page = None
79+
if offset + page_size < total_count:
80+
next_page = f"/api/v1/blogs?page={page + 1}&page_size={page_size}"
81+
82+
prev_page = None
83+
if page > 1:
84+
prev_page = f"/api/v1/blogs?page={page - 1}&page_size={page_size}"
85+
86+
results = [
87+
{
88+
"id": blog.id,
89+
"title": blog.title,
90+
"excerpt": blog.excerpt,
91+
"image_url": blog.image_url,
92+
"created_at": blog.created_at,
93+
}
94+
for blog in blogs
95+
]
96+
97+
return {
98+
"count": total_count,
99+
"next": next_page,
100+
"previous": prev_page,
101+
"results": results,
102+
}
103+
104+
except SQLAlchemyError as e:
105+
logger.error(f"Database error occurred: {e}")
106+
raise HTTPException(
107+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
108+
detail="Database error occurred.",
109+
)
110+
except Exception as e:
111+
logger.error(f"Unexpected error occurred: {e}")
112+
raise HTTPException(
113+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
114+
detail="Internal server error.",
115+
)

tests/v1/blog/test_list_blog.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import pytest
2+
from fastapi.testclient import TestClient
3+
from unittest.mock import MagicMock
4+
from main import app
5+
from api.v1.models.blog import Blog
6+
from api.db.database import get_db
7+
8+
client = TestClient(app)
9+
10+
11+
@pytest.fixture
12+
def db_session_mock():
13+
return MagicMock()
14+
15+
16+
@pytest.fixture(autouse=True)
17+
def override_get_db(db_session_mock):
18+
app.dependency_overrides[get_db] = lambda: db_session_mock
19+
yield
20+
app.dependency_overrides[get_db] = None
21+
22+
23+
def test_successful_retrieval_of_paginated_blog_posts(db_session_mock):
24+
blog1 = Blog(
25+
id=1,
26+
title="My First Blog",
27+
excerpt="This is an excerpt from my first blog.",
28+
content="Content of the first blog",
29+
image_url="https://example.com/image1.jpg",
30+
)
31+
_ = Blog(
32+
id=2,
33+
title="My Second Blog",
34+
excerpt="This is an excerpt from my second blog.",
35+
content="Content of the second blog",
36+
image_url="https://example.com/image2.jpg",
37+
)
38+
39+
db_session_mock.query().filter().order_by().offset().limit().all.return_value = [
40+
blog1
41+
]
42+
db_session_mock.query().filter().order_by().count.return_value = 2
43+
44+
response = client.get("/api/v1/blogs?page=1&page_size=1")
45+
46+
assert response.status_code == 200
47+
data = response.json()
48+
assert data["count"] == 2
49+
assert data["next"] == "/api/v1/blogs?page=2&page_size=1"
50+
assert data["previous"] is None
51+
assert len(data["results"]) == 1
52+
assert data["results"][0]["title"] == "My First Blog"
53+
54+
55+
def test_no_blog_posts_present(db_session_mock):
56+
db_session_mock.query().filter().order_by().offset().limit().all.return_value = []
57+
db_session_mock.query().filter().order_by().count.return_value = 0
58+
59+
response = client.get("/api/v1/blogs?page=1&page_size=10")
60+
61+
assert response.status_code == 200
62+
data = response.json()
63+
assert data["count"] == 0
64+
assert data["next"] is None
65+
assert data["previous"] is None
66+
assert len(data["results"]) == 0
67+
68+
69+
def test_internal_server_error(mocker):
70+
mocker.patch("api.v1.routes.blog", side_effect=Exception("Test exception"))
71+
72+
response = client.get("/api/v1/blogs?page=1&page_size=10")
73+
74+
assert response.status_code == 500
75+
data = response.json()
76+
assert data["detail"] == "Internal server error."
77+
78+
79+
def test_invalid_page_or_page_size_parameters():
80+
# Test invalid page parameter
81+
response = client.get("/api/v1/blogs?page=-1&page_size=10")
82+
assert response.status_code == 422
83+
84+
# Test invalid page_size parameter
85+
response = client.get("/api/v1/blogs?page=1&page_size=-1")
86+
assert response.status_code == 422
87+
88+
# Test non-integer page parameter
89+
response = client.get("/api/v1/blogs?page=abc&page_size=10")
90+
assert response.status_code == 422
91+
92+
93+
def test_invalid_method():
94+
response = client.delete("/api/v1/blogs")
95+
96+
assert response.status_code == 405
97+
data = response.json()
98+
assert data["detail"] == "Method Not Allowed"
99+
100+
101+
def test_soft_deleted_blog_post_access_control(db_session_mock):
102+
_ = Blog(
103+
id=1,
104+
title="Soft Deleted Blog",
105+
excerpt="This is an excerpt from a soft deleted blog.",
106+
content="Content of the soft deleted blog",
107+
image_url="https://example.com/image1.jpg",
108+
is_deleted=True,
109+
)
110+
db_session_mock.query().filter().order_by().offset().limit().all.return_value = []
111+
db_session_mock.query().filter().order_by().count.return_value = 0
112+
113+
response = client.get("/api/v1/blogs?page=1&page_size=10")
114+
115+
assert response.status_code == 200
116+
data = response.json()
117+
assert data["count"] == 0
118+
assert data["next"] is None
119+
assert data["previous"] is None
120+
assert len(data["results"]) == 0

0 commit comments

Comments
 (0)