Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add coverage #7

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,16 @@ jobs:
run: pip install -r requirements.txt
-
name: Run tests
run: cd src && pytest test.py
run: cd src && pytest test.py --cov main --cov-report xml:../coverage.xml

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage.xml

- name: Run linters
uses: wearerequired/lint-action@v2
with:
auto_fix: true
black: true
black_auto_fix: true
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# FIUBER-Metrics

[![codecov](https://codecov.io/gh/TallerDeProgramacion2-2022-2c-Grupo7/FIUBER-Metrics/branch/main/graph/badge.svg?token=KJPOL2HW69)](https://codecov.io/gh/TallerDeProgramacion2-2022-2c-Grupo7/FIUBER-Metrics)


## Local installation & usage

1. Copy the Firebase credentials JSON (`firebase_credentials.json`) into the `src` directory of the repository.
Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ firebase-admin==5.4.0
psycopg2-binary==2.9.5
SQLAlchemy==1.4.42
pytest==7.2.0
pytest-cov==3.0.0
black==22.8.0
datadog==0.44.0
3 changes: 3 additions & 0 deletions src/common/utils.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from .firebase_admin import auth


def get_user_count():
count = 0
for _ in auth.list_users().iterate_all():
count += 1
return count


def get_admin_count():
count = 0
for user in auth.list_users().iterate_all():
Expand All @@ -16,6 +18,7 @@ def get_admin_count():
pass
return count


def get_blocked_user_count():
count = 0
for user in auth.list_users().iterate_all():
Expand Down
4 changes: 3 additions & 1 deletion src/db/conn.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

DATABASE_PASSWORD = os.environ.get("DATABASE_PASSWORD", "admin")
DATABASE_HOST = os.environ.get("DATABASE_HOST", "localhost")
DATABASE_URL = f"postgresql://postgres:{DATABASE_PASSWORD}@{DATABASE_HOST}:5432/postgres"
DATABASE_URL = (
f"postgresql://postgres:{DATABASE_PASSWORD}@{DATABASE_HOST}:5432/postgres"
)

engine = create_engine(DATABASE_URL)
Session = sessionmaker(engine)
Expand Down
30 changes: 14 additions & 16 deletions src/db/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .utils import db_method
from enum import Enum


class EventType(str, Enum):
signup = "signup"
login = "login"
Expand All @@ -16,6 +17,7 @@ class EventType(str, Enum):
block = "block"
unblock = "unblock"


class Events(Base):
__tablename__ = "events"
__table_args__ = {"extend_existing": True}
Expand All @@ -31,10 +33,7 @@ def __iter__(self):
yield key, value

@db_method
def find_all(
id_from: Union[int, None] = None,
max_results: int = 10
) -> list:
def find_all(id_from: Union[int, None] = None, max_results: int = 10) -> list:
with Session.begin() as session:
query = session.query(Events)
if id_from is not None:
Expand All @@ -43,20 +42,19 @@ def find_all(
return res.all()

@db_method
def get_count_by_day(
event_type: str,
date_from: date,
date_to: date
) -> list:
def get_count_by_day(event_type: str, date_from: date, date_to: date) -> list:
with Session.begin() as session:
date = cast(Events.datetime, Date)
res = session.query(
date.label("date"),
func.count(Events.event_id).label("count")
).where(
(date.between(date_from, date_to))
& (Events.event_type == event_type)
).group_by(date)
res = (
session.query(
date.label("date"), func.count(Events.event_id).label("count")
)
.where(
(date.between(date_from, date_to))
& (Events.event_type == event_type)
)
.group_by(date)
)
return res.all()

@db_method
Expand Down
3 changes: 3 additions & 0 deletions src/db/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@

_all_tables_created = False


def db_method(f):
"""
Decorator for DB related methods.
Creates the tables if they don't exist
before executing the method itself.
"""

def wrapper(*args, **kwargs):
global _all_tables_created
if not _all_tables_created or os.environ.get("ENV") == "test":
Expand All @@ -18,4 +20,5 @@ def wrapper(*args, **kwargs):
if not res:
return res
return [dict(row) for row in res]

return wrapper
26 changes: 14 additions & 12 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,18 @@
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
allow_credentials=True
allow_credentials=True,
)

app.add_middleware(IdTokenMiddleware)
app.add_middleware(DatadogEventMiddleware)


@app.get("/")
async def get_events(
max_results: Union[int, None] = 10,
page_token: Union[int, None] = None,
):
max_results: Union[int, None] = 10,
page_token: Union[int, None] = None,
):
"""
List up to max_results recent events.
"""
Expand All @@ -37,15 +38,16 @@ async def get_events(
except dbexc.OperationalError:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal server error"
detail="Internal server error",
)
n_results = len(events)
more_results = n_results >= max_results
return {
"result": events,
"page_token": (events[-1]["event_id"]) if more_results else None
"page_token": (events[-1]["event_id"]) if more_results else None,
}


@app.post("/{event_type}")
async def create_event(uid: str, event_type: EventType):
"""
Expand All @@ -57,9 +59,10 @@ async def create_event(uid: str, event_type: EventType):
except dbexc.OperationalError:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal server error"
detail="Internal server error",
)


@app.get("/stats")
async def get_stats():
"""
Expand All @@ -71,19 +74,18 @@ async def get_stats():
try:
stats = {
event_type.value: Events.get_count_by_day(
event_type.value,
date_from,
date_to
event_type.value, date_from, date_to
)
for event_type in EventType
}
except dbexc.OperationalError:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal server error"
detail="Internal server error",
)
return {"result": stats}


@app.get("/usersSummary")
async def get_users_summary():
"""
Expand All @@ -93,6 +95,6 @@ async def get_users_summary():
"result": {
"total_users": utils.get_user_count(),
"total_admins": utils.get_admin_count(),
"total_blocked_users": utils.get_blocked_user_count()
"total_blocked_users": utils.get_blocked_user_count(),
}
}
8 changes: 6 additions & 2 deletions src/middlewares/datadog_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,26 @@
from starlette.middleware.base import BaseHTTPMiddleware
from datadog import statsd


def send_event(title: str, message: str, alert_type: str):
try:
statsd.event(title, message, alert_type, tags=["app_name:fiuber-metrics"])
except:
pass


class DatadogEventMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
try:
response = await call_next(request)
send_event(f"{request.method} {request.url} {response.status_code}", "", "info")
send_event(
f"{request.method} {request.url} {response.status_code}", "", "info"
)
return response
except Exception as e:
send_event(
f"{request.method} {request.url} {status.HTTP_500_INTERNAL_SERVER_ERROR}",
str(e),
"error"
"error",
)
raise e
17 changes: 13 additions & 4 deletions src/middlewares/id_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,36 @@
from firebase_admin import _auth_utils as auth_utils
from starlette.middleware.base import BaseHTTPMiddleware


class IdTokenMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
if os.environ.get("ENV") == "test":
return await call_next(request)
if request.method == "OPTIONS" or request.url.path in ("/docs", "/openapi.json"):
if request.method == "OPTIONS" or request.url.path in (
"/docs",
"/openapi.json",
):
return await call_next(request)
try:
authorization = request.headers["Authorization"]
user = auth.verify_id_token(authorization[7:])
except (KeyError, TypeError, UnicodeDecodeError, auth_utils.InvalidIdTokenError) as e:
except (
KeyError,
TypeError,
UnicodeDecodeError,
auth_utils.InvalidIdTokenError,
) as e:
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={"detail": "You must be logged in to make this request"},
headers={"Access-Control-Allow-Origin": "*"}
headers={"Access-Control-Allow-Origin": "*"},
)
if request.method == "GET" and not user.get("admin"):
# Cualquier usuario puede crear eventos
# pero sólo los admins pueden obtener métricas.
return JSONResponse(
status_code=status.HTTP_403_FORBIDDEN,
content={"detail": "You must be an admin to make this request"},
headers={"Access-Control-Allow-Origin": "*"}
headers={"Access-Control-Allow-Origin": "*"},
)
return await call_next(request)
16 changes: 12 additions & 4 deletions src/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@
os.environ["ENV"] = "test"
client = TestClient(app)


@pytest.fixture()
def test_db():
yield
Session.close_all()
Base.metadata.drop_all(engine)


def test_get_events_with_no_events_returns_empty_list(test_db):
response = client.get("/")
assert response.status_code == 200
Expand All @@ -24,6 +26,7 @@ def test_get_events_with_no_events_returns_empty_list(test_db):
assert len(data["result"]) == 0
assert data["page_token"] == None


def test_add_event_ok(test_db):
uid = f"test_add_event_ok_uid_{datetime.now()}"
response = client.post("/login", params={"uid": uid})
Expand All @@ -40,15 +43,18 @@ def test_add_event_ok(test_db):
assert event["event_type"] == "login"
assert event["user_id"] == uid


def test_add_event_error_invalid_event_type(test_db):
uid = f"test_add_event_ok_uid_{datetime.now()}"
response = client.post("/invalid", params={"uid": uid})
assert response.status_code == 422


def test_add_event_error_missing_uid(test_db):
response = client.post("/login")
assert response.status_code == 422


def test_get_events_multiple_results_ok(test_db):
uid_1 = f"test_get_events_multiple_results_ok_{datetime.now()}"
response = client.post("/login", params={"uid": uid_1})
Expand All @@ -74,6 +80,7 @@ def test_get_events_multiple_results_ok(test_db):
assert event["event_type"] == "login"
assert event["user_id"] == uid_1


def test_get_stats(test_db):
n_logins = randint(1, 10)
n_signups = randint(1, 10)
Expand All @@ -83,24 +90,24 @@ def test_get_stats(test_db):
uid_1 = f"test_get_stats_{datetime.now()}"
response = client.post("/login", params={"uid": uid_1})
assert response.status_code == 200

for _ in range(n_signups):
uid_1 = f"test_get_stats_{datetime.now()}"
response = client.post("/signup", params={"uid": uid_1})
assert response.status_code == 200

for _ in range(n_passwordresets):
uid_1 = f"test_get_stats_{datetime.now()}"
response = client.post("/passwordReset", params={"uid": uid_1})
assert response.status_code == 200

response = client.get("/stats")
assert response.status_code == 200
data = response.json()

result = data["result"]
assert isinstance(result, dict)

logins = result["login"]
assert isinstance(logins, list)
assert len(logins) == 1
Expand All @@ -119,6 +126,7 @@ def test_get_stats(test_db):
assert passwordresets[0]["count"] == n_passwordresets
assert passwordresets[0]["date"] == datetime.today().strftime("%Y-%m-%d")


def test_get_users_summary(test_db):
response = client.get("/usersSummary")
assert response.status_code == 200
Expand Down