Skip to content

Commit 7817fb2

Browse files
committed
tests: login, articles, like
1 parent a582810 commit 7817fb2

File tree

7 files changed

+202
-2
lines changed

7 files changed

+202
-2
lines changed

app/routes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ async def single_article_view(
110110
summary="Like an article",
111111
description="""
112112
****
113-
This endpoint allows authenticated users to like an article
113+
This endpoint allows authenticated users to like or unlike an article
114114
Set token generated from the login endpoint in the authorize dialog field.
115115
""",
116116
status_code=200,

app/schemas.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from datetime import datetime
12
from typing import List
23
from pydantic import BaseModel, EmailStr, Field
34

@@ -28,6 +29,8 @@ class ArticleSchema(BaseModel):
2829
slug: str
2930
desc: str
3031
likes_count: int
32+
created_at: datetime
33+
updated_at: datetime
3134

3235

3336
class ArticlesResponseSchema(ResponseSchema):

app/tests/__init__.py

Whitespace-only changes.

app/tests/conftest.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
2+
3+
from app.main import app
4+
from app.database import get_db, Base
5+
from pytest_postgresql import factories
6+
from pytest_postgresql.janitor import DatabaseJanitor
7+
from httpx import AsyncClient
8+
import pytest, asyncio
9+
10+
from app.models import Article, User
11+
from app.utils import get_password_hash
12+
13+
test_db = factories.postgresql_proc(port=None, dbname="test_db")
14+
15+
16+
@pytest.fixture(scope="session")
17+
def event_loop():
18+
"""Overrides pytest default function scoped event loop"""
19+
loop = asyncio.get_event_loop_policy().new_event_loop()
20+
yield loop
21+
loop.close()
22+
23+
24+
@pytest.fixture(scope="session")
25+
async def engine(test_db):
26+
pg_host = test_db.host
27+
pg_port = test_db.port
28+
pg_user = test_db.user
29+
pg_db = test_db.dbname
30+
pg_password = test_db.password
31+
32+
with DatabaseJanitor(
33+
user=pg_user,
34+
host=pg_host,
35+
port=pg_port,
36+
dbname=pg_db,
37+
version=test_db.version,
38+
password=pg_password,
39+
):
40+
connection_str = f"postgresql+psycopg://{pg_user}:@{pg_host}:{pg_port}/{pg_db}"
41+
engine = create_async_engine(connection_str)
42+
yield engine
43+
engine.dispose()
44+
45+
46+
@pytest.fixture
47+
async def database(engine):
48+
async with engine.begin() as conn:
49+
await conn.run_sync(Base.metadata.drop_all)
50+
await conn.run_sync(Base.metadata.create_all)
51+
52+
TestSessionLocal = async_sessionmaker(
53+
bind=engine,
54+
expire_on_commit=False,
55+
)
56+
async with TestSessionLocal() as db:
57+
yield db
58+
59+
60+
@pytest.fixture
61+
async def client(database):
62+
async def overide_get_db():
63+
try:
64+
yield database
65+
finally:
66+
await database.close()
67+
68+
app.dependency_overrides[get_db] = overide_get_db
69+
async with AsyncClient(app=app, base_url="http://test/api/v1") as client:
70+
yield client
71+
72+
73+
@pytest.fixture
74+
async def test_user(database):
75+
user = User(
76+
name="Test User",
77+
email="testuser@example.com",
78+
password=get_password_hash("testpassword"),
79+
)
80+
database.add(user)
81+
await database.commit()
82+
return user
83+
84+
85+
@pytest.fixture
86+
async def test_article(database):
87+
article = Article(
88+
title="Test Article", slug="test-article", desc="This is my test article"
89+
)
90+
database.add(article)
91+
await database.commit()
92+
return article

app/tests/test_routes.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from app.utils import create_auth_token
2+
3+
4+
async def test_login(mocker, client, test_user):
5+
# Test for error response with invalid credentials
6+
response = await client.post(
7+
"/auth/login",
8+
json={"email": "invalid@email.com", "password": "invalidpassword"},
9+
)
10+
assert response.status_code == 401
11+
assert response.json() == {
12+
"status": "failure",
13+
"message": "Invalid credentials",
14+
}
15+
16+
# Test for success response valid credentials
17+
response = await client.post(
18+
"/auth/login",
19+
json={"email": test_user.email, "password": "testpassword"},
20+
)
21+
assert response.status_code == 201
22+
assert response.json() == {
23+
"status": "success",
24+
"message": "Login successful",
25+
"data": {"token": mocker.ANY},
26+
}
27+
28+
29+
async def test_retrieve_all_articles(client, test_article):
30+
# Verify that all articles are retrieved successfully
31+
response = await client.get("/articles")
32+
assert response.status_code == 200
33+
result = response.json()
34+
assert result["status"] == "success"
35+
assert result["message"] == "Articles fetched successfully"
36+
data = result["data"]
37+
assert len(data) > 0
38+
assert any(isinstance(obj["title"], str) for obj in data)
39+
40+
41+
async def test_retrieve_article_detail(client, test_article):
42+
# Verify that a single article detail was received successfully
43+
response = await client.get(f"/articles/{test_article.slug}")
44+
assert response.status_code == 200
45+
assert response.json() == {
46+
"status": "success",
47+
"message": "Article details fetched successfully",
48+
"data": {
49+
"title": test_article.title,
50+
"slug": test_article.slug,
51+
"desc": test_article.desc,
52+
"likes_count": test_article.likes_count,
53+
"created_at": test_article.created_at.isoformat(),
54+
"updated_at": test_article.updated_at.isoformat(),
55+
},
56+
}
57+
58+
# Verify that an error returns for invalid slug
59+
response = await client.get("/articles/invalid_slug")
60+
assert response.status_code == 404
61+
assert response.json() == {
62+
"status": "failure",
63+
"message": "Article does not exist!",
64+
}
65+
66+
67+
async def test_like_article(client, test_article, test_user):
68+
# Verify that an error returns for unauthorized user
69+
response = await client.get(f"/articles/{test_article.slug}/like")
70+
assert response.status_code == 401
71+
assert response.json() == {
72+
"status": "failure",
73+
"message": "Unauthorized User!",
74+
}
75+
76+
# Set authorization for client
77+
token = create_auth_token(test_user.id)
78+
client.headers = {**client.headers, "Authorization": f"Bearer {token}"}
79+
80+
# Verify that an error returns for invalid slug
81+
response = await client.get("/articles/invalid_slug/like")
82+
assert response.status_code == 404
83+
assert response.json() == {
84+
"status": "failure",
85+
"message": "Article does not exist!",
86+
}
87+
88+
# Verify that the article was liked or unliked successfully
89+
response = await client.get(f"/articles/{test_article.slug}/like")
90+
assert response.status_code == 200
91+
assert response.json() == {
92+
"status": "success",
93+
"message": "Like added successfully",
94+
}

app/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from app.conf import settings
99
from app.models import User
1010

11-
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
11+
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
1212

1313
ALGORITHM = "HS256"
1414

requirements.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,34 @@ httpcore==1.0.6
1414
httptools==0.6.4
1515
httpx==0.27.2
1616
idna==3.10
17+
iniconfig==2.0.0
1718
Jinja2==3.1.4
1819
Mako==1.3.6
1920
markdown-it-py==3.0.0
2021
MarkupSafe==3.0.2
2122
mdurl==0.1.2
23+
mirakuru==2.5.3
24+
packaging==24.1
2225
passlib==1.7.4
26+
pluggy==1.5.0
27+
port-for==0.7.4
28+
psutil==6.1.0
2329
psycopg==3.2.3
2430
psycopg-binary==3.2.3
2531
pydantic==2.9.2
2632
pydantic-settings==2.6.1
2733
pydantic_core==2.23.4
2834
Pygments==2.18.0
2935
PyJWT==2.9.0
36+
pytest==8.3.3
37+
pytest-asyncio==0.24.0
38+
pytest-mock==3.14.0
39+
pytest-postgresql==6.1.1
3040
python-dotenv==1.0.1
3141
python-multipart==0.0.16
3242
PyYAML==6.0.2
3343
rich==13.9.3
44+
setuptools==75.3.0
3445
shellingham==1.5.4
3546
sniffio==1.3.1
3647
SQLAlchemy==2.0.36

0 commit comments

Comments
 (0)