Skip to content

Commit efd8b20

Browse files
authored
Merge pull request #45 from PythonFloripa/feature/22-2
fix/#22
2 parents 98c3e4b + ad096ca commit efd8b20

File tree

13 files changed

+330
-150
lines changed

13 files changed

+330
-150
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
@@ -14,6 +14,44 @@
1414
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/authentication/token")
1515

1616

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

@@ -31,43 +69,6 @@ async def authenticate_community(
3169
return None
3270
return found_community
3371

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

7374
@router.post("/create_commumity")

app/routers/news/routes.py

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +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
3-
from services.database.orm.news import get_news_by_query_params
6+
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
411

512

613
class NewsPostResponse(BaseModel):
@@ -22,10 +29,22 @@ def setup():
2229
summary="News endpoint",
2330
description="Creates news and returns a confirmation message",
2431
)
25-
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+
):
2640
"""
2741
News endpoint that creates news and returns a confirmation message.
2842
"""
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+
)
2948
return NewsPostResponse()
3049

3150
@router.get(
@@ -35,16 +54,25 @@ async def post_news():
3554
summary="Get News",
3655
description="Retrieves news filtered by user and query params",
3756
)
38-
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+
):
3967
"""
4068
Get News endpoint that retrieves news filtered by user and query params.
4169
"""
4270
news_list = await get_news_by_query_params(
4371
session=request.app.db_session_factory,
44-
id=request.query_params.get("id"),
45-
user_email=request.headers.get("user-email"),
46-
category=request.query_params.get("category"),
47-
tags=request.query_params.get("tags"),
72+
id=id,
73+
email=user_email,
74+
category=category,
75+
tags=tags,
4876
)
4977
return NewsGetResponse(news_list=news_list)
5078

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/models/subscriptions.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from typing import List, Optional
22

3-
from schemas import SubscriptionTagEnum
43
from sqlalchemy import JSON, Column
54
from sqlmodel import Field, SQLModel
65

6+
from app.schemas import SubscriptionTagEnum
7+
78

89
class Subscription(SQLModel, table=True):
910
__tablename__ = "subscriptions" # type: ignore

app/services/database/orm/news.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,44 @@
11
from typing import Optional
22

3-
from services.database.models import News
43
from sqlmodel import select
54
from sqlmodel.ext.asyncio.session import AsyncSession
65

6+
from app.services.database.models import News
7+
8+
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+
724

825
async def get_news_by_query_params(
926
session: AsyncSession,
10-
user_email: Optional[str] = None,
27+
email: Optional[str] = None,
1128
category: Optional[str] = None,
1229
tags: Optional[str] = None,
1330
id: Optional[str] = None,
1431
) -> list[News]:
1532
filters = []
16-
if user_email is not None:
17-
filters.append(News.user_email == user_email)
33+
if email is not None:
34+
filters.append(News.user_email == email)
1835
if category is not None:
1936
filters.append(News.category == category)
2037
if tags is not None:
2138
filters.append(News.tags == tags)
2239
if id is not None:
2340
filters.append(News.id == id)
2441

25-
print("user_email:", user_email)
26-
print("Filters:", filters)
27-
2842
statement = select(News).where(*filters)
2943
results = await session.exec(statement)
3044
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 = "[email protected]"
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="[email protected]", 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):

0 commit comments

Comments
 (0)