Skip to content

Commit 42852dd

Browse files
committed
Merge remote-tracking branch 'origin/main' into feature/#34
2 parents 88a7ac3 + efd8b20 commit 42852dd

File tree

12 files changed

+325
-148
lines changed

12 files changed

+325
-148
lines changed

Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
API_CONTAINER_NAME=pynews-server
12
.PHONY: help build up down logs test lint format clean dev prod restart health
23

34
# Colors for terminal output
@@ -73,3 +74,7 @@ shell: ## Entra no shell do container
7374
setup: install build up ## Setup completo do projeto
7475
@echo "$(GREEN)Setup completo realizado!$(NC)"
7576
@echo "$(GREEN)Acesse: http://localhost:8000/docs$(NC)"
77+
78+
79+
docker/test:
80+
docker exec -e PYTHONPATH=/app $(API_CONTAINER_NAME) pytest -s --cov-report=term-missing --cov-report html --cov-report=xml --cov=app tests/

app/routers/authentication.py

Lines changed: 38 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,44 @@
1515
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/authentication/token")
1616

1717

18+
async def get_current_community(
19+
request: Request,
20+
token: Annotated[str, Depends(oauth2_scheme)],
21+
) -> DBCommunity:
22+
credentials_exception = HTTPException(
23+
status_code=status.HTTP_401_UNAUTHORIZED,
24+
detail="Could not validate credentials",
25+
headers={"WWW-Authenticate": "Bearer"},
26+
)
27+
28+
try:
29+
payload = jwt.decode(
30+
token, auth.SECRET_KEY, algorithms=[auth.ALGORITHM]
31+
)
32+
username = payload.get("sub")
33+
if username is None:
34+
raise credentials_exception
35+
token_data = TokenPayload(username=username)
36+
except InvalidTokenError:
37+
raise credentials_exception
38+
session: AsyncSession = request.app.db_session_factory
39+
community = await get_community_by_username(
40+
session=session, username=token_data.username
41+
)
42+
if community is None:
43+
raise credentials_exception
44+
45+
return community
46+
47+
48+
async def get_current_active_community(
49+
current_user: Annotated[DBCommunity, Depends(get_current_community)],
50+
) -> DBCommunity:
51+
# A função simplesmente retorna o usuário.
52+
# Pode ser estendido futuramente para verificar um status "ativo".
53+
return current_user
54+
55+
1856
def setup():
1957
router = APIRouter(prefix="/authentication", tags=["authentication"])
2058

@@ -32,43 +70,6 @@ async def authenticate_community(
3270
return None
3371
return found_community
3472

35-
# Teste
36-
async def get_current_community(
37-
request: Request,
38-
token: Annotated[str, Depends(oauth2_scheme)],
39-
) -> DBCommunity:
40-
credentials_exception = HTTPException(
41-
status_code=status.HTTP_401_UNAUTHORIZED,
42-
detail="Could not validate credentials",
43-
headers={"WWW-Authenticate": "Bearer"},
44-
)
45-
46-
try:
47-
payload = jwt.decode(
48-
token, auth.SECRET_KEY, algorithms=[auth.ALGORITHM]
49-
)
50-
username = payload.get("sub")
51-
if username is None:
52-
raise credentials_exception
53-
token_data = TokenPayload(username=username)
54-
except InvalidTokenError:
55-
raise credentials_exception
56-
session: AsyncSession = request.app.db_session_factory
57-
community = await get_community_by_username(
58-
session=session, username=token_data.username
59-
)
60-
if community is None:
61-
raise credentials_exception
62-
63-
return community
64-
65-
async def get_current_active_community(
66-
current_user: Annotated[DBCommunity, Depends(get_current_community)],
67-
) -> DBCommunity:
68-
# A função simplesmente retorna o usuário.
69-
# Pode ser estendido futuramente para verificar um status "ativo".
70-
return current_user
71-
7273
# Teste
7374

7475
@router.post("/create_commumity")

app/routers/news/routes.py

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1-
from fastapi import APIRouter, Request, status
1+
from typing import Annotated
2+
3+
from fastapi import APIRouter, Depends, Request, status
4+
from fastapi.params import Header
25
from pydantic import BaseModel
36

4-
from app.services.database.orm.news import get_news_by_query_params
7+
from app.routers.authentication import get_current_active_community
8+
from app.schemas import News
9+
from app.services.database.models import Community as DBCommunity
10+
from app.services.database.orm.news import create_news, get_news_by_query_params
511

612

713
class NewsPostResponse(BaseModel):
@@ -23,10 +29,22 @@ def setup():
2329
summary="News endpoint",
2430
description="Creates news and returns a confirmation message",
2531
)
26-
async def post_news():
32+
async def post_news(
33+
request: Request,
34+
current_community: Annotated[
35+
DBCommunity, Depends(get_current_active_community)
36+
],
37+
news: News,
38+
user_email: str = Header(..., alias="user-email"),
39+
):
2740
"""
2841
News endpoint that creates news and returns a confirmation message.
2942
"""
43+
news_dict = news.__dict__
44+
news_dict["user_email"] = user_email
45+
await create_news(
46+
session=request.app.db_session_factory, news=news_dict
47+
)
3048
return NewsPostResponse()
3149

3250
@router.get(
@@ -36,16 +54,25 @@ async def post_news():
3654
summary="Get News",
3755
description="Retrieves news filtered by user and query params",
3856
)
39-
async def get_news(request: Request):
57+
async def get_news(
58+
request: Request,
59+
current_community: Annotated[
60+
DBCommunity, Depends(get_current_active_community)
61+
],
62+
id: str | None = None,
63+
user_email: str = Header(..., alias="user-email"),
64+
category: str | None = None,
65+
tags: str | None = None,
66+
):
4067
"""
4168
Get News endpoint that retrieves news filtered by user and query params.
4269
"""
4370
news_list = await get_news_by_query_params(
4471
session=request.app.db_session_factory,
45-
id=request.query_params.get("id"),
46-
user_email=request.headers.get("user-email"),
47-
category=request.query_params.get("category"),
48-
tags=request.query_params.get("tags"),
72+
id=id,
73+
email=user_email,
74+
category=category,
75+
tags=tags,
4976
)
5077
return NewsGetResponse(news_list=news_list)
5178

app/schemas.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ class CommunityInDB(Community):
2121
password: str
2222

2323

24+
class News(BaseModel):
25+
title: str
26+
content: str
27+
category: str
28+
tags: str | None = None
29+
source_url: str
30+
social_media_url: str | None = None
31+
likes: int = 0
32+
33+
2434
class Token(BaseModel):
2535
access_token: str
2636
token_type: str

app/services/database/orm/news.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,39 @@
66
from app.services.database.models import News
77

88

9+
async def create_news(session: AsyncSession, news: dict) -> None:
10+
_news = News(
11+
title=news["title"],
12+
content=news["content"],
13+
category=news["category"],
14+
user_email=news["user_email"],
15+
source_url=news["source_url"],
16+
tags=news["tags"] or "",
17+
social_media_url=news["social_media_url"] or "",
18+
likes=news["likes"],
19+
)
20+
session.add(_news)
21+
await session.commit()
22+
await session.refresh(_news)
23+
24+
925
async def get_news_by_query_params(
1026
session: AsyncSession,
11-
user_email: Optional[str] = None,
27+
email: Optional[str] = None,
1228
category: Optional[str] = None,
1329
tags: Optional[str] = None,
1430
id: Optional[str] = None,
1531
) -> list[News]:
1632
filters = []
17-
if user_email is not None:
18-
filters.append(News.user_email == user_email)
33+
if email is not None:
34+
filters.append(News.user_email == email)
1935
if category is not None:
2036
filters.append(News.category == category)
2137
if tags is not None:
2238
filters.append(News.tags == tags)
2339
if id is not None:
2440
filters.append(News.id == id)
2541

26-
print("user_email:", user_email)
27-
print("Filters:", filters)
28-
2942
statement = select(News).where(*filters)
3043
results = await session.exec(statement)
3144
return results.all()

tests/conftest.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
import pytest
44
import pytest_asyncio
5-
from fastapi import FastAPI
5+
from fastapi import FastAPI, status
66
from httpx import ASGITransport, AsyncClient
77
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
88
from sqlmodel import SQLModel
99
from sqlmodel.ext.asyncio.session import AsyncSession
1010

1111
from app.main import app
12+
from app.services.auth import hash_password
13+
from app.services.database.models.communities import Community
1214

1315
# from app.main import get_db_session
1416

@@ -73,8 +75,46 @@ async def async_client(test_app: FastAPI) -> AsyncGenerator[AsyncClient, None]:
7375
yield client
7476

7577

78+
class CommunityCredentials:
79+
username: str = "community_username"
80+
email: str = "community_name@test.com"
81+
password: str = "community_password"
82+
hashed_password: str = hash_password(password)
83+
84+
85+
@pytest_asyncio.fixture
86+
async def community(session: AsyncSession):
87+
community = Community(
88+
username=CommunityCredentials.username,
89+
email=CommunityCredentials.email,
90+
password=CommunityCredentials.hashed_password,
91+
)
92+
session.add(community)
93+
await session.commit()
94+
await session.refresh(community)
95+
return community
96+
97+
98+
@pytest_asyncio.fixture()
99+
async def token(async_client: AsyncGenerator[AsyncClient, None]) -> str:
100+
form_data = {
101+
"grant_type": "password",
102+
"username": CommunityCredentials.username,
103+
"password": CommunityCredentials.password,
104+
}
105+
token_response = await async_client.post(
106+
"/api/authentication/token",
107+
data=form_data,
108+
headers={"Content-Type": "application/x-www-form-urlencoded"},
109+
)
110+
assert token_response.status_code == status.HTTP_200_OK
111+
return token_response.json()["access_token"]
112+
113+
76114
@pytest.fixture
77-
def mock_headers():
115+
def valid_auth_headers(community: Community, token: str) -> dict[str, str]:
78116
return {
79-
"header1": "value1",
117+
"Authorization": f"Bearer {token}",
118+
"Content-Type": "application/json",
119+
"user-email": CommunityCredentials.email,
80120
}

tests/test_auth.py

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,9 @@
11
import pytest
2-
import pytest_asyncio
32
from fastapi import status
43
from httpx import AsyncClient
5-
from services.database.models import Community
6-
from sqlmodel.ext.asyncio.session import AsyncSession
74

8-
from app.services.auth import hash_password
9-
10-
password = "123Asd!@#"
11-
12-
13-
# gerar usuario para autenticação
14-
@pytest_asyncio.fixture
15-
async def community(session: AsyncSession):
16-
hashed_password = hash_password(password)
17-
community = Community(
18-
username="username", email="username@test.com", password=hashed_password
19-
)
20-
session.add(community)
21-
await session.commit()
22-
await session.refresh(community)
23-
return community
5+
from app.services.database.models import Community
6+
from tests.conftest import CommunityCredentials
247

258

269
@pytest.mark.asyncio
@@ -33,7 +16,10 @@ async def test_authentication_token_endpoint(
3316
"""
3417
# 1. Teste de login com credenciais válidas
3518
# O OAuth2PasswordRequestForm espera 'username' e 'password'
36-
form_data = {"username": community.username, "password": password}
19+
form_data = {
20+
"username": community.username,
21+
"password": CommunityCredentials.password,
22+
}
3723

3824
response = await async_client.post(
3925
"/api/authentication/token",
@@ -66,16 +52,18 @@ async def test_authentication_token_endpoint(
6652

6753
@pytest.mark.asyncio
6854
async def test_community_me_with_valid_token(
69-
async_client: AsyncClient, community: Community
55+
async_client: AsyncClient,
56+
community: Community,
7057
):
7158
"""
72-
Testa se o endpoint protegido /authenticate/me/ retorna os dados do usuário com um token válido.
59+
Testa se o endpoint protegido /authenticate/me/ retorna os dados do usuário
60+
com um token válido.
7361
"""
7462
# 1. Obter um token de acesso primeiro
7563
form_data = {
7664
"grant_type": "password",
7765
"username": community.username,
78-
"password": password,
66+
"password": CommunityCredentials.password,
7967
}
8068
token_response = await async_client.post(
8169
"/api/authentication/token",

tests/test_communities.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import pytest
2-
from services.database.models import Community
32
from sqlmodel import select
43
from sqlmodel.ext.asyncio.session import AsyncSession
54

5+
from app.services.database.models import Community
6+
67

78
@pytest.mark.asyncio
89
async def test_insert_communities(session: AsyncSession):

tests/test_healthcheck.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@
77

88
@pytest.mark.asyncio
99
async def test_healthcheck_endpoint(
10-
async_client: AsyncClient, mock_headers: Mapping[str, str]
10+
async_client: AsyncClient, valid_auth_headers: Mapping[str, str]
1111
):
1212
"""Test the healthcheck endpoint returns correct status and version."""
13-
# response = await async_client.get('/v2/healthcheck', headers=mock_headers)
14-
response = await async_client.get("/api/healthcheck", headers=mock_headers)
13+
# response = await async_client.get(
14+
# '/v2/healthcheck', headers=valid_auth_headers
15+
# )
16+
response = await async_client.get(
17+
"/api/healthcheck", headers=valid_auth_headers
18+
)
1519

1620
assert response.status_code == status.HTTP_200_OK
1721
assert response.json() == {"status": "healthy", "version": "2.0.0"}

0 commit comments

Comments
 (0)