Skip to content

Commit e2ee945

Browse files
committed
tested the api using pytest, fixtures, mocking external services
1 parent 1b3c258 commit e2ee945

7 files changed

Lines changed: 636 additions & 0 deletions

File tree

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,9 @@ dependencies = [
1818
"pyjwt>=2.11.0",
1919
"sqlalchemy>=2.0.46",
2020
]
21+
22+
[dependency-groups]
23+
dev = [
24+
"moto[s3]>=5.1.22",
25+
"pytest>=9.0.3",
26+
]

tests/__init__.py

Whitespace-only changes.

tests/conftest.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import os
2+
from collections.abc import AsyncGenerator
3+
4+
5+
os.environ["DATABASE_URL"] = (
6+
"postgresql+psycopg://bloguser:blogpass@localhost/test_blog"
7+
)
8+
os.environ["S3_BUCKET_NAME"] = "test-bucket"
9+
os.environ["SECRET_KEY"] = "test-secret-key-for-testing-only"
10+
11+
os.environ["S3_ACCESS_KEY_ID"] = "testing"
12+
os.environ["S3_SECRET_ACCESS_KEY"] = "testing"
13+
os.environ["S3_REGION"] = "eu-north-1"
14+
15+
os.environ["AWS_ACCESS_KEY_ID"] = "testing"
16+
os.environ["AWS_SECRET_ACCESS_KEY"] = "testing"
17+
os.environ["AWS_DEFAULT_REGION"] = "eu-north-1"
18+
19+
import boto3
20+
import pytest
21+
from httpx import ASGITransport, AsyncClient
22+
from moto import mock_aws
23+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
24+
from sqlalchemy.pool import NullPool
25+
26+
from database import Base, get_db
27+
from main import app
28+
29+
pytest_plugins = ["anyio"]
30+
31+
@pytest.fixture(scope="session")
32+
def anyio_backend():
33+
return "asyncio"
34+
35+
@pytest.fixture(scope="session")
36+
def test_engine():
37+
engine = create_async_engine(
38+
os.environ["DATABASE_URL"],
39+
poolclass=NullPool,
40+
)
41+
return engine
42+
43+
44+
@pytest.fixture(scope="session")
45+
async def setup_database(test_engine):
46+
async with test_engine.begin() as conn:
47+
await conn.run_sync(Base.metadata.create_all)
48+
49+
yield
50+
51+
async with test_engine.begin() as conn:
52+
await conn.run_sync(Base.metadata.drop_all)
53+
54+
await test_engine.dispose()
55+
56+
@pytest.fixture
57+
async def db_session(
58+
test_engine,
59+
setup_database,
60+
) -> AsyncGenerator[AsyncSession]:
61+
conn = await test_engine.connect()
62+
trans = await conn.begin()
63+
64+
test_async_session = async_sessionmaker(
65+
bind=conn,
66+
class_=AsyncSession,
67+
expire_on_commit=False,
68+
join_transaction_mode="create_savepoint",
69+
)
70+
71+
async with test_async_session() as session:
72+
try:
73+
yield session
74+
finally:
75+
await session.close()
76+
await trans.rollback()
77+
await conn.close()
78+
79+
80+
@pytest.fixture
81+
def mocked_aws():
82+
with mock_aws():
83+
s3 = boto3.client("s3", region_name="eu-north-1")
84+
s3.create_bucket(
85+
Bucket=os.environ["S3_BUCKET_NAME"],
86+
CreateBucketConfiguration={"LocationConstraint": "eu-north-1"},
87+
)
88+
yield s3
89+
90+
91+
@pytest.fixture
92+
async def client(
93+
db_session: AsyncSession,
94+
mocked_aws,
95+
) -> AsyncGenerator[AsyncClient]:
96+
97+
async def override_get_db():
98+
yield db_session
99+
100+
app.dependency_overrides[get_db] = override_get_db
101+
102+
async with AsyncClient(
103+
transport=ASGITransport(app=app),
104+
base_url="http://test",
105+
) as ac:
106+
yield ac
107+
108+
app.dependency_overrides.clear()
109+
110+
111+
async def create_test_user(
112+
client: AsyncClient,
113+
username: str = "testuser",
114+
email: str = "test@example.com",
115+
password: str = "testpassword123",
116+
) -> dict:
117+
response = await client.post(
118+
"/api/users",
119+
json={
120+
"username": username,
121+
"email": email,
122+
"password": password,
123+
},
124+
)
125+
assert response.status_code == 201, f"Failed to create user: {response.text}"
126+
return response.json()
127+
128+
129+
async def login_user(
130+
client: AsyncClient,
131+
email: str = "test@example.com",
132+
password: str = "testpassword123",
133+
) -> str:
134+
response = await client.post(
135+
"/api/users/token",
136+
data={
137+
"username": email,
138+
"password": password,
139+
},
140+
)
141+
assert response.status_code == 200, f"Failed to login: {response.text}"
142+
return response.json()["access_token"]
143+
144+
145+
def auth_header(token: str) -> dict[str, str]:
146+
return {"Authorization": f"Bearer {token}"}

tests/test_image.jpg

51.2 KB
Loading

tests/test_posts.py

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import pytest
2+
from httpx import AsyncClient
3+
4+
from tests.conftest import auth_header, create_test_user, login_user
5+
6+
@pytest.mark.anyio
7+
async def test_get_posts_empty(client: AsyncClient):
8+
response = await client.get("/api/posts")
9+
10+
assert response.status_code == 200
11+
data = response.json()
12+
assert data["posts"] == []
13+
assert data["total"] == 0
14+
assert data["has_more"] is False
15+
16+
17+
@pytest.mark.anyio
18+
async def test_get_post_not_found(client: AsyncClient):
19+
response = await client.get("/api/posts/999")
20+
21+
assert response.status_code == 404
22+
assert response.json()["detail"] == "Post not found"
23+
24+
25+
@pytest.mark.anyio
26+
async def test_create_post_success(client: AsyncClient):
27+
user = await create_test_user(client)
28+
token = await login_user(client)
29+
headers = auth_header(token)
30+
31+
response = await client.post(
32+
"/api/posts",
33+
json={"title": "My First Post", "content": "This is the content"},
34+
headers=headers,
35+
)
36+
37+
assert response.status_code == 201
38+
data = response.json()
39+
assert data["title"] == "My First Post"
40+
assert data["content"] == "This is the content"
41+
assert data["user_id"] == user["id"]
42+
assert "id" in data
43+
assert "date_posted" in data
44+
assert data["author"]["username"] == "testuser"
45+
46+
47+
@pytest.mark.anyio
48+
async def test_create_post_unauthorized(client: AsyncClient):
49+
response = await client.post(
50+
"/api/posts",
51+
json={"title": "Test Post", "content": "Test content"},
52+
)
53+
54+
assert response.status_code == 401
55+
assert response.json()["detail"] == "Not authenticated"
56+
57+
58+
@pytest.mark.anyio
59+
async def test_update_post_success(client: AsyncClient):
60+
await create_test_user(client)
61+
token = await login_user(client)
62+
headers = auth_header(token)
63+
64+
response = await client.post(
65+
"/api/posts",
66+
json={"title": "Original Title", "content": "Original content"},
67+
headers=headers,
68+
)
69+
post_id = response.json()["id"]
70+
71+
response = await client.patch(
72+
f"/api/posts/{post_id}",
73+
json={"title": "Updated Title"},
74+
headers=headers,
75+
)
76+
77+
assert response.status_code == 200
78+
data = response.json()
79+
assert data["title"] == "Updated Title"
80+
assert data["content"] == "Original content"
81+
82+
83+
@pytest.mark.anyio
84+
async def test_update_post_wrong_user(client: AsyncClient):
85+
await create_test_user(client, username="user1", email="user1@example.com")
86+
token1 = await login_user(client, email="user1@example.com")
87+
88+
response = await client.post(
89+
"/api/posts",
90+
json={"title": "User 1's Post", "content": "Only user 1 can edit this"},
91+
headers=auth_header(token1),
92+
)
93+
post_id = response.json()["id"]
94+
95+
await create_test_user(client, username="user2", email="user2@example.com")
96+
token2 = await login_user(client, email="user2@example.com")
97+
98+
response = await client.patch(
99+
f"/api/posts/{post_id}",
100+
json={"title": "Hacked Title"},
101+
headers=auth_header(token2),
102+
)
103+
104+
assert response.status_code == 403
105+
assert response.json()["detail"] == "Not authorized to update this post"
106+
107+
108+
@pytest.mark.anyio
109+
async def test_get_posts_with_pagination(client: AsyncClient):
110+
await create_test_user(client)
111+
token = await login_user(client)
112+
headers = auth_header(token)
113+
114+
for i in range(5):
115+
response = await client.post(
116+
"/api/posts",
117+
json={"title": f"Post {i}", "content": f"Content for post {i}"},
118+
headers=headers,
119+
)
120+
assert response.status_code == 201
121+
122+
response = await client.get("/api/posts")
123+
assert response.status_code == 200
124+
data = response.json()
125+
assert data["total"] == 5
126+
assert len(data["posts"]) == 5
127+
assert data["has_more"] is False
128+
129+
response = await client.get("/api/posts?limit=2")
130+
assert response.status_code == 200
131+
data = response.json()
132+
assert data["total"] == 5
133+
assert len(data["posts"]) == 2
134+
assert data["has_more"] is True
135+
136+
response = await client.get("/api/posts?skip=2&limit=2")
137+
assert response.status_code == 200
138+
data = response.json()
139+
assert data["total"] == 5
140+
assert len(data["posts"]) == 2
141+
assert data["skip"] == 2
142+
assert data["limit"] == 2
143+
144+
145+
146+
147+

0 commit comments

Comments
 (0)