diff --git a/AUTHORS.md b/AUTHORS.md index add6ca4b..f97e0934 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -33,6 +33,7 @@ * PureDreamer - Developer * ShiZinDle - Developer * YairEn - Developer + * IdanPelled - Developer # Special thanks to diff --git a/app/database/models.py b/app/database/models.py index 63d350ae..bdaa090d 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -1,6 +1,6 @@ from __future__ import annotations - from datetime import datetime +import enum from typing import Any, Dict from sqlalchemy import ( @@ -8,6 +8,7 @@ Column, DateTime, DDL, + Enum, event, Float, ForeignKey, @@ -203,20 +204,80 @@ class PSQLEnvironmentError(Exception): ) +class InvitationStatusEnum(enum.Enum): + UNREAD = 0 + ACCEPTED = 1 + DECLINED = 2 + + +class MessageStatusEnum(enum.Enum): + UNREAD = 0 + READ = 1 + + class Invitation(Base): __tablename__ = "invitations" id = Column(Integer, primary_key=True, index=True) - status = Column(String, nullable=False, default="unread") + creation = Column(DateTime, default=datetime.now, nullable=False) + status = Column( + Enum(InvitationStatusEnum), + default=InvitationStatusEnum.UNREAD, + nullable=False, + ) + recipient_id = Column(Integer, ForeignKey("users.id")) event_id = Column(Integer, ForeignKey("events.id")) - creation = Column(DateTime, default=datetime.now) - recipient = relationship("User") event = relationship("Event") + def decline(self, session: Session) -> None: + """declines the invitation.""" + self.status = InvitationStatusEnum.DECLINED + session.merge(self) + session.commit() + + def accept(self, session: Session) -> None: + """Accepts the invitation by creating an + UserEvent association that represents + participantship at the event.""" + + association = UserEvent( + user_id=self.recipient.id, + event_id=self.event.id, + ) + self.status = InvitationStatusEnum.ACCEPTED + session.merge(self) + session.add(association) + session.commit() + + def __repr__(self): + return f"" + + +class Message(Base): + __tablename__ = "messages" + + id = Column(Integer, primary_key=True, index=True) + body = Column(String, nullable=False) + link = Column(String) + creation = Column(DateTime, default=datetime.now, nullable=False) + status = Column( + Enum(MessageStatusEnum), + default=MessageStatusEnum.UNREAD, + nullable=False, + ) + + recipient_id = Column(Integer, ForeignKey("users.id")) + recipient = relationship("User") + + def mark_as_read(self, session): + self.status = MessageStatusEnum.READ + session.merge(self) + session.commit() + def __repr__(self): - return f"" + return f"" class UserSettings(Base): diff --git a/app/database/schemas.py b/app/database/schemas.py index 61d31a33..29748b6f 100644 --- a/app/database/schemas.py +++ b/app/database/schemas.py @@ -2,7 +2,7 @@ from pydantic import BaseModel, validator, EmailStr, EmailError -EMPTY_FIELD_STRING = 'field is required' +EMPTY_FIELD_STRING = "field is required" MIN_FIELD_LENGTH = 3 MAX_FIELD_LENGTH = 20 @@ -19,10 +19,14 @@ class UserBase(BaseModel): Validating fields types Returns a User object without sensitive information """ + username: str email: str full_name: str + + language_id: Optional[int] = 1 description: Optional[str] = None + target_weight: Optional[Union[int, float]] = None class Config: orm_mode = True @@ -30,6 +34,7 @@ class Config: class UserCreate(UserBase): """Validating fields types""" + password: str confirm_password: str @@ -37,41 +42,49 @@ class UserCreate(UserBase): Calling to field_not_empty validaion function, for each required field. """ - _fields_not_empty_username = validator( - 'username', allow_reuse=True)(fields_not_empty) - _fields_not_empty_full_name = validator( - 'full_name', allow_reuse=True)(fields_not_empty) - _fields_not_empty_password = validator( - 'password', allow_reuse=True)(fields_not_empty) + _fields_not_empty_username = validator("username", allow_reuse=True)( + fields_not_empty, + ) + _fields_not_empty_full_name = validator("full_name", allow_reuse=True)( + fields_not_empty, + ) + _fields_not_empty_password = validator("password", allow_reuse=True)( + fields_not_empty, + ) _fields_not_empty_confirm_password = validator( - 'confirm_password', allow_reuse=True)(fields_not_empty) - _fields_not_empty_email = validator( - 'email', allow_reuse=True)(fields_not_empty) - - @validator('confirm_password') + "confirm_password", + allow_reuse=True, + )(fields_not_empty) + _fields_not_empty_email = validator("email", allow_reuse=True)( + fields_not_empty, + ) + + @validator("confirm_password") def passwords_match( - cls, confirm_password: str, - values: UserBase) -> Union[ValueError, str]: + cls, + confirm_password: str, + values: UserBase, + ) -> Union[ValueError, str]: """Validating passwords fields identical.""" - if 'password' in values and confirm_password != values['password']: + if "password" in values and confirm_password != values["password"]: raise ValueError("doesn't match to password") return confirm_password - @validator('username') + @validator("username") def username_length(cls, username: str) -> Union[ValueError, str]: """Validating username length is legal""" if not (MIN_FIELD_LENGTH < len(username) < MAX_FIELD_LENGTH): raise ValueError("must contain between 3 to 20 charactars") return username - @validator('password') + @validator("password") def password_length(cls, password: str) -> Union[ValueError, str]: """Validating username length is legal""" if not (MIN_FIELD_LENGTH < len(password) < MAX_FIELD_LENGTH): raise ValueError("must contain between 3 to 20 charactars") return password - @validator('email') + @validator("email") def confirm_mail(cls, email: str) -> Union[ValueError, str]: """Validating email is valid mail address.""" try: @@ -86,5 +99,6 @@ class User(UserBase): Validating fields types Returns a User object without sensitive information """ + id: int is_active: bool diff --git a/app/internal/notification.py b/app/internal/notification.py new file mode 100644 index 00000000..af86c9bb --- /dev/null +++ b/app/internal/notification.py @@ -0,0 +1,176 @@ +from operator import attrgetter +from typing import Iterator, List, Union, Callable + +from fastapi import HTTPException +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import Session +from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_406_NOT_ACCEPTABLE + +from app.database.models import ( + Invitation, + Message, + InvitationStatusEnum, + MessageStatusEnum, +) +from app.internal.utils import create_model + + +WRONG_NOTIFICATION_ID = ( + "The notification id you have entered is wrong\n." + "If you did not enter the notification id manually, report this exception." +) + +NOTIFICATION_TYPE = Union[Invitation, Message] + +UNREAD_STATUS = { + InvitationStatusEnum.UNREAD, + MessageStatusEnum.UNREAD, +} + +ARCHIVED = { + InvitationStatusEnum.DECLINED, + MessageStatusEnum.READ, +} + + +async def get_message_by_id( + message_id: int, + session: Session, +) -> Union[Message, None]: + """Returns an invitation by an id. + if id does not exist, returns None. + """ + return session.query(Message).filter_by(id=message_id).first() + + +def _is_unread(notification: NOTIFICATION_TYPE) -> bool: + """Returns True if notification is unread, False otherwise.""" + return notification.status in UNREAD_STATUS + + +def _is_archived(notification: NOTIFICATION_TYPE) -> bool: + """Returns True if notification should be + in archived page, False otherwise. + """ + return notification.status in ARCHIVED + + +def is_owner(user, notification: NOTIFICATION_TYPE) -> bool: + """Checks if user is owner of the notification. + + Args: + notification: a NOTIFICATION_TYPE object. + user: user schema object. + + Returns: + True or raises HTTPException. + """ + if notification.recipient_id == user.user_id: + return True + + msg = "The notification you are trying to access is not yours." + raise HTTPException( + status_code=HTTP_401_UNAUTHORIZED, + detail=msg, + ) + + +def raise_wrong_id_error() -> None: + """Raises HTTPException. + + Returns: + None + """ + raise HTTPException( + status_code=HTTP_406_NOT_ACCEPTABLE, + detail=WRONG_NOTIFICATION_ID, + ) + + +def filter_notifications( + session: Session, + user_id: int, + func: Callable[[NOTIFICATION_TYPE], bool], +) -> Iterator[NOTIFICATION_TYPE]: + """Filters notifications by "func".""" + yield from filter(func, get_all_notifications(session, user_id)) + + +def get_unread_notifications( + session: Session, + user_id: int, +) -> Iterator[NOTIFICATION_TYPE]: + """Returns all unread notifications.""" + yield from filter_notifications(session, user_id, _is_unread) + + +def get_archived_notifications( + session: Session, + user_id: int, +) -> List[NOTIFICATION_TYPE]: + """Returns all archived notifications.""" + yield from filter_notifications(session, user_id, _is_archived) + + +def get_all_notifications( + session: Session, + user_id: int, +) -> List[NOTIFICATION_TYPE]: + """Returns all notifications.""" + invitations: List[Invitation] = get_all_invitations( + session, + recipient_id=user_id, + ) + messages: List[Message] = get_all_messages(session, user_id) + + notifications = invitations + messages + return sort_notifications(notifications) + + +def sort_notifications( + notification: List[NOTIFICATION_TYPE], +) -> List[NOTIFICATION_TYPE]: + """Sorts the notifications by the creation date.""" + return sorted(notification, key=attrgetter("creation"), reverse=True) + + +def create_message( + session: Session, + msg: str, + recipient_id: int, + link=None, +) -> Message: + """Creates a new message.""" + return create_model( + session, + Message, + body=msg, + recipient_id=recipient_id, + link=link, + ) + + +def get_all_messages(session: Session, recipient_id: int) -> List[Message]: + """Returns all messages.""" + condition = Message.recipient_id == recipient_id + return session.query(Message).filter(condition).all() + + +def get_all_invitations(session: Session, **param) -> List[Invitation]: + """Returns all invitations filter by param.""" + try: + invitations = session.query(Invitation).filter_by(**param).all() + except SQLAlchemyError: + return [] + else: + return invitations + + +def get_invitation_by_id( + invitation_id: int, + session: Session, +) -> Union[Invitation, None]: + """Returns an invitation by an id. + if id does not exist, returns None. + """ + return session.query(Invitation).filter_by(id=invitation_id).first() diff --git a/app/internal/utils.py b/app/internal/utils.py index 2f669212..a7e208f5 100644 --- a/app/internal/utils.py +++ b/app/internal/utils.py @@ -2,6 +2,8 @@ from typing import Any, List, Optional, Union from sqlalchemy.orm import Session +from starlette.responses import RedirectResponse +from starlette.status import HTTP_302_FOUND from app.database.models import Base, User @@ -19,6 +21,7 @@ def save(session: Session, instance: Base) -> bool: def create_model(session: Session, model_class: Base, **kwargs: Any) -> Base: """Creates and saves a db model.""" instance = model_class(**kwargs) + save(session, instance) return instance @@ -69,7 +72,7 @@ def get_time_from_string(string: str) -> Optional[Union[date, time]]: datetime.time | datetime.date | None: Date or Time object if valid, None otherwise. """ - formats = {'%Y-%m-%d': 'date', '%H:%M': 'time', '%H:%M:%S': 'time'} + formats = {"%Y-%m-%d": "date", "%H:%M": "time", "%H:%M:%S": "time"} for time_format, method in formats.items(): try: time_obj = getattr(datetime.strptime(string, time_format), method) @@ -89,10 +92,31 @@ def get_placeholder_user() -> User: A User object. """ return User( - username='new_user', - email='my@email.po', - password='1a2s3d4f5g6', - full_name='My Name', + username="new_user", + email="my@email.po", + password="1a2s3d4f5g6", + full_name="My Name", language_id=1, - telegram_id='', + telegram_id="", ) + + +def safe_redirect_response( + url: str, + default: str = "/", + status_code: int = HTTP_302_FOUND, +): + """Returns a safe redirect response. + + Args: + url: the url to redirect to. + default: where to redirect if url isn't safe. + status_code: the response status code. + + Returns: + The Notifications HTML page. + """ + if not url.startswith("/"): + url = default + + return RedirectResponse(url=url, status_code=status_code) diff --git a/app/main.py b/app/main.py index 3560d142..3eb750a0 100644 --- a/app/main.py +++ b/app/main.py @@ -63,11 +63,11 @@ def create_tables(engine, psql_environment): four_o_four, friendview, google_connect, - invitation, joke, login, logout, meds, + notification, profile, register, search, @@ -113,11 +113,11 @@ async def swagger_ui_redirect(): four_o_four.router, friendview.router, google_connect.router, - invitation.router, joke.router, login.router, logout.router, meds.router, + notification.router, profile.router, register.router, salary.router, diff --git a/app/routers/event.py b/app/routers/event.py index a5673307..7858ebba 100644 --- a/app/routers/event.py +++ b/app/routers/event.py @@ -33,6 +33,7 @@ EVENT_DATA = Tuple[Event, List[Dict[str, str]], str] TIME_FORMAT = "%Y-%m-%d %H:%M" START_FORMAT = "%A, %d/%m/%Y %H:%M" + UPDATE_EVENTS_FIELDS = { "title": str, "start": dt, diff --git a/app/routers/export.py b/app/routers/export.py index a5fd4229..0fa5b279 100644 --- a/app/routers/export.py +++ b/app/routers/export.py @@ -9,7 +9,8 @@ from app.dependencies import get_db from app.internal.agenda_events import get_events_in_time_frame from app.internal.export import get_icalendar_with_multiple_events -from app.internal.utils import get_current_user +from app.internal.security.schema import CurrentUser +from tests.security_testing_routes import current_user router = APIRouter( prefix="/export", @@ -20,9 +21,10 @@ @router.get("/") def export( - start_date: Union[date, str], - end_date: Union[date, str], - db: Session = Depends(get_db), + start_date: Union[date, str], + end_date: Union[date, str], + db: Session = Depends(get_db), + user: CurrentUser = Depends(current_user), ) -> StreamingResponse: """Returns the Export page route. @@ -30,19 +32,18 @@ def export( start_date: A date or an empty string. end_date: A date or an empty string. db: Optional; The database connection. + user: user schema object. Returns: - # TODO add description + A StreamingResponse that contains an .ics file. """ - # TODO: connect to real user - user = get_current_user(db) - events = get_events_in_time_frame(start_date, end_date, user.id, db) + events = get_events_in_time_frame(start_date, end_date, user.user_id, db) file = BytesIO(get_icalendar_with_multiple_events(db, list(events))) return StreamingResponse( content=file, media_type="text/calendar", headers={ - # Change filename to "pylandar.ics". - "Content-Disposition": "attachment;filename=pylandar.ics", + # Change filename to "PyLendar.ics". + "Content-Disposition": "attachment;filename=PyLendar.ics", }, ) diff --git a/app/routers/invitation.py b/app/routers/invitation.py deleted file mode 100644 index da2ba209..00000000 --- a/app/routers/invitation.py +++ /dev/null @@ -1,110 +0,0 @@ -from typing import Any, List, Optional - -from fastapi import APIRouter, Depends, Request, status -from fastapi.responses import RedirectResponse, Response -from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.orm import Session - -from app.database.models import Invitation -from app.dependencies import get_db, templates -from app.routers.share import accept - -router = APIRouter( - prefix="/invitations", - tags=["invitation"], - dependencies=[Depends(get_db)], -) - - -@router.get("/", include_in_schema=False) -def view_invitations( - request: Request, db: Session = Depends(get_db) -) -> Response: - """Returns the Invitations page route. - - Args: - request: The HTTP request. - db: Optional; The database connection. - - Returns: - The Invitations HTML page. - """ - return templates.TemplateResponse("invitations.html", { - "request": request, - # TODO: Connect to current user. - # recipient_id should be the current user - # but because we don't have one yet, - # "get_all_invitations" returns all invitations - "invitations": get_all_invitations(db), - }) - - -@router.post("/", include_in_schema=False) -async def accept_invitations( - request: Request, db: Session = Depends(get_db) -) -> RedirectResponse: - """Creates a new connection between the User and the Event in the database. - - See Also: - share.accept for more information. - - Args: - request: The HTTP request. - db: Optional; The database connection. - - Returns: - An updated Invitations HTML page. - """ - data = await request.form() - invite_id = list(data.values())[0] - - invitation = get_invitation_by_id(invite_id, db) - if invitation: - accept(invitation, db) - - url = router.url_path_for("view_invitations") - return RedirectResponse(url=url, status_code=status.HTTP_302_FOUND) - - -# TODO: should be a get request with the path of: -# @router.get("/all") -@router.get("/get_all_invitations") -def get_all_invitations( - db: Session = Depends(get_db), **param: Any -) -> List[Invitation]: - """Returns all Invitations filtered by the requested parameters. - - Args: - db: Optional; The database connection. - **param: A list of parameters to filter by. - - Returns: - A list of all Invitations. - """ - try: - invitations = list(db.query(Invitation).filter_by(**param)) - except SQLAlchemyError: - return [] - else: - return invitations - - -# TODO: should be a get request with the path of: -# @router.get("/{id}") -@router.post("/get_invitation_by_id") -def get_invitation_by_id( - invitation_id: int, db: Session = Depends(get_db) -) -> Optional[Invitation]: - """Returns an Invitation by an ID. - - Args: - invitation_id: The Invitation ID. - db: Optional; The database connection. - - Returns: - An Invitation object if found, otherwise returns None. - """ - return (db.query(Invitation) - .filter_by(id=invitation_id) - .first() - ) diff --git a/app/routers/login.py b/app/routers/login.py index 99fd5b5c..59645520 100644 --- a/app/routers/login.py +++ b/app/routers/login.py @@ -3,13 +3,11 @@ from fastapi import APIRouter, Depends, Request from sqlalchemy.orm import Session from starlette.responses import RedirectResponse -from starlette.status import HTTP_302_FOUND from app.dependencies import get_db, templates -from app.internal.security.ouath2 import ( - authenticate_user, create_jwt_token) +from app.internal.security.ouath2 import authenticate_user, create_jwt_token from app.internal.security import schema - +from app.internal.utils import safe_redirect_response router = APIRouter( prefix="", @@ -20,21 +18,23 @@ @router.get("/login") async def login_user_form( - request: Request, message: Optional[str] = "") -> templates: + request: Request, + message: Optional[str] = "", +) -> templates: """rendering login route get method""" - return templates.TemplateResponse("login.html", { - "request": request, - "message": message, - 'current_user': "logged in" - }) + return templates.TemplateResponse( + "login.html", + {"request": request, "message": message, "current_user": "logged in"}, + ) -@router.post('/login') +@router.post("/login") async def login( - request: Request, - next: Optional[str] = "/", - db: Session = Depends(get_db), - existing_jwt: Union[str, bool] = False) -> RedirectResponse: + request: Request, + next: Optional[str] = "/", + db: Session = Depends(get_db), + existing_jwt: Union[str, bool] = False, +) -> RedirectResponse: """rendering login route post method.""" form = await request.form() form_dict = dict(form) @@ -49,19 +49,17 @@ async def login( if user: user = await authenticate_user(db, user) if not user: - return templates.TemplateResponse("login.html", { - "request": request, - "message": 'Please check your credentials' - }) + return templates.TemplateResponse( + "login.html", + {"request": request, "message": "Please check your credentials"}, + ) # creating HTTPONLY cookie with jwt-token out of user unique data # for testing if not existing_jwt: jwt_token = create_jwt_token(user) else: jwt_token = existing_jwt - if not next.startswith("/"): - next = "/" - response = RedirectResponse(next, status_code=HTTP_302_FOUND) + response = safe_redirect_response(next) response.set_cookie( "Authorization", value=jwt_token, diff --git a/app/routers/notification.py b/app/routers/notification.py new file mode 100644 index 00000000..74b51102 --- /dev/null +++ b/app/routers/notification.py @@ -0,0 +1,192 @@ +from fastapi import APIRouter, Depends, Form, Request +from sqlalchemy.orm import Session + +from app.database.models import MessageStatusEnum +from app.dependencies import get_db, templates +from app.internal.notification import ( + get_all_messages, + get_archived_notifications, + get_invitation_by_id, + get_message_by_id, + get_unread_notifications, + is_owner, + raise_wrong_id_error, +) +from app.internal.security.dependencies import current_user, is_logged_in + +from app.internal.security.schema import CurrentUser +from app.internal.utils import safe_redirect_response + +router = APIRouter( + prefix="/notification", + tags=["notification"], + dependencies=[ + Depends(get_db), + Depends(is_logged_in), + ], +) + + +@router.get("/", include_in_schema=False) +async def view_notifications( + request: Request, + user: CurrentUser = Depends(current_user), + db: Session = Depends(get_db), +): + """Returns the Notifications page. + + Args: + request: The HTTP request. + db: Optional; The database connection. + user: user schema object. + + Returns: + The Notifications HTML page. + """ + return templates.TemplateResponse( + "notifications.html", + { + "request": request, + "new_messages": bool(get_all_messages), + "notifications": list( + get_unread_notifications( + session=db, + user_id=user.user_id, + ), + ), + }, + ) + + +@router.get("/archive", include_in_schema=False) +async def view_archive( + request: Request, + user: CurrentUser = Depends(current_user), + db: Session = Depends(get_db), +): + """Returns the Archived Notifications page. + + Args: + request: The HTTP request. + db: Optional; The database connection. + user: user schema object. + + Returns: + The Archived Notifications HTML page. + """ + return templates.TemplateResponse( + "archive.html", + { + "request": request, + "notifications": list( + get_archived_notifications( + session=db, + user_id=user.user_id, + ), + ), + }, + ) + + +@router.post("/invitation/accept") +async def accept_invitations( + invite_id: int = Form(...), + next_url: str = Form(...), + db: Session = Depends(get_db), + user: CurrentUser = Depends(current_user), +): + """Creates a new connection between the User and the Event in the database. + + See Also: + models.Invitation.accept for more information. + + Args: + invite_id: the id of the invitation. + next_url: url to redirect to. + db: Optional; The database connection. + user: user schema object. + + Returns: + A redirect to where the user called the route from. + """ + invitation = get_invitation_by_id(invite_id, session=db) + if invitation and is_owner(user, invitation): + invitation.accept(db) + return safe_redirect_response(next_url) + + raise_wrong_id_error() + + +@router.post("/invitation/decline") +async def decline_invitations( + invite_id: int = Form(...), + next_url: str = Form(...), + db: Session = Depends(get_db), + user: CurrentUser = Depends(current_user), +): + """Declines an invitations. + + Args: + invite_id: the id of the invitation. + db: Optional; The database connection. + next_url: url to redirect to. + user: user schema object. + + Returns: + A redirect to where the user called the route from. + """ + invitation = get_invitation_by_id(invite_id, session=db) + if invitation and is_owner(user, invitation): + invitation.decline(db) + return safe_redirect_response(next_url) + + raise_wrong_id_error() + + +@router.post("/message/read") +async def mark_message_as_read( + message_id: int = Form(...), + next_url: str = Form(...), + db: Session = Depends(get_db), + user: CurrentUser = Depends(current_user), +): + """Marks a message as read. + + Args: + message_id: the id of the message. + db: Optional; The database connection. + next_url: url to redirect to. + user: user schema object. + + Returns: + A redirect to where the user called the route from. + """ + message = await get_message_by_id(message_id, session=db) + if message and is_owner(user, message): + message.mark_as_read(db) + return safe_redirect_response(next_url) + + raise_wrong_id_error() + + +@router.post("/message/read/all") +async def mark_all_as_read( + next_url: str = Form(...), + db: Session = Depends(get_db), + user: CurrentUser = Depends(current_user), +): + """Marks all messages as read. + + Args: + next_url: url to redirect to. + user: user schema object. + db: Optional; The database connection. + + Returns: + A redirect to where the user called the route from. + """ + for message in get_all_messages(db, user.user_id): + if message.status == MessageStatusEnum.UNREAD: + message.mark_as_read(db) + + return safe_redirect_response(next_url) diff --git a/app/routers/register.py b/app/routers/register.py index 57f77165..2bd8c4bf 100644 --- a/app/routers/register.py +++ b/app/routers/register.py @@ -11,7 +11,7 @@ from app.database import schemas from app.database import models from app.dependencies import get_db, templates - +from app.internal.utils import save router = APIRouter( prefix="", @@ -20,6 +20,13 @@ ) +def _create_user(session, **kw) -> models.User: + """Creates and saves a new user.""" + user = models.User(**kw) + save(session, user) + return user + + async def create_user(db: Session, user: schemas.UserCreate) -> models.User: """ creating a new User object in the database, with hashed password @@ -32,12 +39,10 @@ async def create_user(db: Session, user: schemas.UserCreate) -> models.User: "email": user.email, "password": hashed_password, "description": user.description, + "language_id": user.language_id, + "target_weight": user.target_weight, } - db_user = models.User(**user_details) - db.add(db_user) - db.commit() - db.refresh(db_user) - return db_user + return _create_user(**user_details, session=db) async def check_unique_fields( diff --git a/app/routers/share.py b/app/routers/share.py index a33f44fd..fe5d449e 100644 --- a/app/routers/share.py +++ b/app/routers/share.py @@ -2,8 +2,7 @@ from sqlalchemy.orm import Session -from app.database.models import Event, Invitation, UserEvent -from app.internal.utils import save +from app.database.models import Event, Invitation from app.internal.export import get_icalendar from app.routers.user import does_user_exist, get_users @@ -32,11 +31,11 @@ def send_email_invitation( event: Event, ) -> bool: """Sends an email with an invitation.""" - - ical_invitation = get_icalendar(event, participants) # noqa: F841 - for _ in participants: - # TODO: send email - pass + if participants: + ical_invitation = get_icalendar(event, participants) # noqa: F841 + for _ in participants: + # TODO: send email + pass return True @@ -50,7 +49,6 @@ def send_in_app_invitation( for participant in participants: # email is unique recipient = get_users(email=participant, session=session)[0] - if recipient.id != event.owner.id: session.add(Invitation(recipient=recipient, event=event)) @@ -62,20 +60,6 @@ def send_in_app_invitation( return True -def accept(invitation: Invitation, session: Session) -> None: - """Accepts an invitation by creating an - UserEvent association that represents - participantship at the event.""" - - association = UserEvent( - user_id=invitation.recipient.id, - event_id=invitation.event.id - ) - invitation.status = 'accepted' - save(session, invitation) - save(session, association) - - def share(event: Event, participants: List[str], session: Session) -> bool: """Sends invitations to all event participants.""" diff --git a/app/routers/user.py b/app/routers/user.py index 05206c8f..8b8a0403 100644 --- a/app/routers/user.py +++ b/app/routers/user.py @@ -10,8 +10,7 @@ from app.database.models import Event, User, UserEvent from app.dependencies import get_db from app.internal.user.availability import disable, enable -from app.internal.utils import get_current_user, save - +from app.internal.utils import get_current_user router = APIRouter( prefix="/user", @@ -23,7 +22,7 @@ class UserModel(BaseModel): username: str password: str - email: str = Field(regex='^\\S+@\\S+\\.\\S+$') + email: str = Field(regex="^\\S+@\\S+\\.\\S+$") language: str language_id: int @@ -38,32 +37,8 @@ async def get_user(id: int, session=Depends(get_db)): return session.query(User).filter_by(id=id).first() -@router.post("/") -def manually_create_user(user: UserModel, session=Depends(get_db)): - create_user(**user.dict(), session=session) - return f'User {user.username} successfully created' - - -def create_user(username: str, - password: str, - email: str, - language_id: int, - session: Session) -> User: - """Creates and saves a new user.""" - - user = User( - username=username, - password=password, - email=email, - language_id=language_id - ) - save(session, user) - return user - - def get_users(session: Session, **param): """Returns all users filtered by param.""" - try: users = list(session.query(User).filter_by(**param)) except SQLAlchemyError: @@ -73,13 +48,10 @@ def get_users(session: Session, **param): def does_user_exist( - session: Session, - *, user_id=None, - username=None, email=None + session: Session, *, user_id=None, username=None, email=None ): """Returns True if user exists, False otherwise. - function can receive one of the there parameters""" - + function can receive one of the there parameters""" if user_id: return len(get_users(session=session, id=user_id)) == 1 if username: @@ -91,16 +63,16 @@ def does_user_exist( def get_all_user_events(session: Session, user_id: int) -> List[Event]: """Returns all events that the user participants in.""" - return ( - session.query(Event).join(UserEvent) - .filter(UserEvent.user_id == user_id).all() + session.query(Event) + .join(UserEvent) + .filter(UserEvent.user_id == user_id) + .all() ) @router.post("/disable") -def disable_logged_user( - request: Request, session: Session = Depends(get_db)): +def disable_logged_user(request: Request, session: Session = Depends(get_db)): """route that sends request to disable the user. after successful disable it will be directed to main page. if the disable fails user will stay at settings page @@ -113,8 +85,7 @@ def disable_logged_user( @router.post("/enable") -def enable_logged_user( - request: Request, session: Session = Depends(get_db)): +def enable_logged_user(request: Request, session: Session = Depends(get_db)): """router that sends a request to enable the user. if enable successful it will be directed to main page. if it fails user will stay at settings page diff --git a/app/static/notification.css b/app/static/notification.css new file mode 100644 index 00000000..7dbe0a7d --- /dev/null +++ b/app/static/notification.css @@ -0,0 +1,70 @@ +/* general */ +#main { + width: 90%; + margin: 0 25% 0 5%; +} + +#link { + font-size: 1.5rem; +} + + +/* notifications */ +#notifications-box { + margin-top: 1rem; + +} + +.notification { + padding: 0.5rem 1rem; + display: flex; + justify-content: space-between; +} + +.notification:hover { + background-color: var(--surface-variant); + border-radius: 0.2rem; +} + +.action, .description { + display: inline-block; +} + +.action { + width: 4rem; +} + + +/* buttons */ +.notification-btn { + background-color: transparent; + border: none; +} + +.notification-btn:focus { + outline: 0; +} + +.btn-accept { + color: green; +} + +.btn-decline { + color: red; +} + +#mark-all-as-read { + margin: 1rem; +} + + +/* form */ +.notification-form { + display: inline-block; +} + + +/* icons */ +.icon { + font-size: 1.5rem; +} diff --git a/app/templates/archive.html b/app/templates/archive.html new file mode 100644 index 00000000..a74da791 --- /dev/null +++ b/app/templates/archive.html @@ -0,0 +1,26 @@ +{% extends "./partials/notification/base.html" %} +{% block page_name %}Archive{% endblock page_name %} + +{% block description %} +
+
Archived Notifications
+

+ In this page you can view all of your archived notifications.
+ Any notification you have marked as read or declined, you will see here.
+ You can use the + button to accept an invitation that you already declined. +

+
+{% endblock description %} + +{% block link %} + +{% endblock link %} + +{% block notifications %} + {% include './partials/notification/generate_archive.html' %} +{% endblock notifications %} + +{% block no_notifications_msg %} + You don't have any archived notifications. +{% endblock no_notifications_msg %} diff --git a/app/templates/base.html b/app/templates/base.html index 5d211ad5..d6039fb4 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -39,40 +39,42 @@ - - - - - - - - Create Categories - - - - + + + + + + + + + Create Categories + + + + + @@ -80,6 +82,7 @@ {% block content %}{% endblock %} + @@ -95,5 +98,4 @@ - diff --git a/app/templates/calendar/layout.html b/app/templates/calendar/layout.html new file mode 100644 index 00000000..5f98e980 --- /dev/null +++ b/app/templates/calendar/layout.html @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + Calendar + + + +
+ +
+
FEATURE NAME
+
+
+
+
+
{{day.display()}}
+
Location 0oc 00:00
+
+ +
+
+ {% block main %} {% endblock %} +
+
+
+ + + + + + + + diff --git a/app/templates/invitations.html b/app/templates/invitations.html deleted file mode 100644 index 83d29418..00000000 --- a/app/templates/invitations.html +++ /dev/null @@ -1,24 +0,0 @@ -{% extends "partials/index/index_base.html" %} - - -{% block content %} - -
-

{{ message }}

-
- - {% if invitations %} -
- {% for i in invitations %} -
- {{ i.event.owner.username }} - {{ i.event.title }} ({{ i.event.start }}) ({{ i.status }}) - - -
- {% endfor %} -
- {% else %} - You don't have any invitations. - {% endif %} - -{% endblock %} \ No newline at end of file diff --git a/app/templates/notifications.html b/app/templates/notifications.html new file mode 100644 index 00000000..7b33a17f --- /dev/null +++ b/app/templates/notifications.html @@ -0,0 +1,38 @@ +{% extends "./partials/notification/base.html" %} +{% block page_name %}Notifications{% endblock page_name %} + +{% block description %} +
+
New Notifications
+

+ In this page you can view all of your new notifications.
+ use the + button to mark as read + and the + and + to accept and decline. +

+
+{% endblock description %} + +{% block link %} + +{% endblock link %} + +{% block optional %} +
+ + +
+{% endblock optional %} + +{% block notifications %} + {% include './partials/notification/generate_notifications.html' %} +{% endblock notifications %} + +{% block no_notifications_msg %} + You don't have any new notifications. +{% endblock no_notifications_msg %} diff --git a/app/templates/partials/calendar/navigation.html b/app/templates/partials/calendar/navigation.html index ab65b8eb..ed73747d 100644 --- a/app/templates/partials/calendar/navigation.html +++ b/app/templates/partials/calendar/navigation.html @@ -7,6 +7,11 @@ +
+ + + +
diff --git a/app/templates/partials/index/navigation.html b/app/templates/partials/index/navigation.html index b1fa3b3f..60baf35d 100644 --- a/app/templates/partials/index/navigation.html +++ b/app/templates/partials/index/navigation.html @@ -20,17 +20,14 @@ - + diff --git a/app/templates/partials/notification/base.html b/app/templates/partials/notification/base.html new file mode 100644 index 00000000..0df8fcde --- /dev/null +++ b/app/templates/partials/notification/base.html @@ -0,0 +1,56 @@ +{% extends "partials/base.html" %} +{% block head %} + {{super()}} + + + + + +{% endblock head %} +{% block body %} +
+ {% include 'partials/calendar/navigation.html' %} +
+ {% include 'partials/calendar/feature_settings/example.html' %} +
+
+ {% block content %} +
+ {% block description %} + {% endblock description %} + +
+
+
+ {% block link %} + {% endblock link %} +
+ {% if notifications %} +
+ + {% block optional %} + {% endblock optional %} + +
+ {% block notifications %} + {% endblock notifications %} +
+ + {% else %} + {% block no_notifications_msg %} + {% endblock no_notifications_msg %} +
+ {% endif %} +
+ {% endblock content %} +
+
+ + + + +{% endblock body %} diff --git a/app/templates/partials/notification/generate_archive.html b/app/templates/partials/notification/generate_archive.html new file mode 100644 index 00000000..2d0e0ed3 --- /dev/null +++ b/app/templates/partials/notification/generate_archive.html @@ -0,0 +1,29 @@ +{% for n in notifications %} + {% set type = n.__table__.name %} + +
+ {% if type == "invitations" %} +
+ Invitation from {{ n.event.owner.username }} - + {{ n.event.title }} + (declined) +
+
+
+ + +
+
+ + {% elif type == "messages" %} +
+ {% if n.link %}{{ n.body }} + {% else %}{{ n.body }}{% endif %} +
+ {% endif %} + +
+{% endfor %} diff --git a/app/templates/partials/notification/generate_notifications.html b/app/templates/partials/notification/generate_notifications.html new file mode 100644 index 00000000..b779db22 --- /dev/null +++ b/app/templates/partials/notification/generate_notifications.html @@ -0,0 +1,48 @@ +{% for n in notifications %} + {% set type = n.__table__.name %} + +
+ {% if type == "invitations" %} +
+ Invitation from {{ n.event.owner.username }} - + {{ n.event.title }} + ({{ n.event.start.strftime('%H:%M %m/%d/%Y') }}) +
+
+
+ + +
+
+ + +
+
+ + {% elif type == "messages" %} +
+ {% if n.link %}{{ n.body }} + {% else %}{{ n.body }}{% endif %} +
+
+
+ + +
+
+ {% endif %} + +
+{% endfor %} diff --git a/tests/conftest.py b/tests/conftest.py index 4c2d7f12..ab9d02c6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,20 +9,24 @@ pytest_plugins = [ - 'tests.user_fixture', - 'tests.event_fixture', - 'tests.dayview_fixture', - 'tests.invitation_fixture', - 'tests.association_fixture', - 'tests.client_fixture', - 'tests.asyncio_fixture', - 'tests.logger_fixture', - 'tests.category_fixture', - 'smtpdfix', - 'tests.quotes_fixture', - 'tests.zodiac_fixture', - 'tests.jokes_fixture', - 'tests.comment_fixture', + "tests.fixtures.user_fixture", + "tests.fixtures.event_fixture", + "tests.fixtures.invitation_fixture", + "tests.fixtures.message_fixture", + "tests.fixtures.association_fixture", + "tests.fixtures.client_fixture", + "tests.fixtures.asyncio_fixture", + "tests.fixtures.logger_fixture", + "tests.fixtures.category_fixture", + "tests.fixtures.quotes_fixture", + "tests.fixtures.zodiac_fixture", + "tests.fixtures.dayview_fixture", + "tests.fixtures.comment_fixture", + "tests.fixtures.quotes_fixture", + "tests.fixtures.zodiac_fixture", + "tests.fixtures.jokes_fixture", + "tests.fixtures.comment_fixture", + "smtpdfix", ] # When testing in a PostgreSQL environment please make sure that: @@ -31,21 +35,22 @@ if PSQL_ENVIRONMENT: SQLALCHEMY_TEST_DATABASE_URL = ( - "postgresql://postgres:1234" - "@localhost/postgres" - ) - test_engine = create_engine( - SQLALCHEMY_TEST_DATABASE_URL + "postgresql://postgres:1234" "@localhost/postgres" ) + test_engine = create_engine(SQLALCHEMY_TEST_DATABASE_URL) else: SQLALCHEMY_TEST_DATABASE_URL = "sqlite:///./test.db" test_engine = create_engine( - SQLALCHEMY_TEST_DATABASE_URL, connect_args={"check_same_thread": False} + SQLALCHEMY_TEST_DATABASE_URL, + connect_args={"check_same_thread": False}, ) TestingSessionLocal = sessionmaker( - autocommit=False, autoflush=False, bind=test_engine) + autocommit=False, + autoflush=False, + bind=test_engine, +) def get_test_db(): @@ -66,11 +71,15 @@ def session(): def sqlite_engine(): SQLALCHEMY_TEST_DATABASE_URL = "sqlite:///./test.db" sqlite_test_engine = create_engine( - SQLALCHEMY_TEST_DATABASE_URL, connect_args={"check_same_thread": False} + SQLALCHEMY_TEST_DATABASE_URL, + connect_args={"check_same_thread": False}, ) TestingSession = sessionmaker( - autocommit=False, autoflush=False, bind=sqlite_test_engine) + autocommit=False, + autoflush=False, + bind=sqlite_test_engine, + ) yield sqlite_test_engine session = TestingSession() diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/association_fixture.py b/tests/fixtures/association_fixture.py similarity index 100% rename from tests/association_fixture.py rename to tests/fixtures/association_fixture.py diff --git a/tests/asyncio_fixture.py b/tests/fixtures/asyncio_fixture.py similarity index 83% rename from tests/asyncio_fixture.py rename to tests/fixtures/asyncio_fixture.py index 2506ab53..7f567b3b 100644 --- a/tests/asyncio_fixture.py +++ b/tests/fixtures/asyncio_fixture.py @@ -7,7 +7,7 @@ from app.main import app from app.routers import telegram from app.routers.event import create_event -from tests.client_fixture import get_test_placeholder_user +from tests.fixtures.client_fixture import get_test_placeholder_user from tests.conftest import get_test_db, test_engine @@ -32,24 +32,24 @@ def fake_user_events(session): session.commit() create_event( db=session, - title='Cool today event', - color='red', + title="Cool today event", + color="red", start=today_date, end=today_date + timedelta(days=2), all_day=False, - content='test event', + content="test event", owner_id=user.id, location="Here", is_google_event=False, ) create_event( db=session, - title='Cool (somewhen in two days) event', - color='blue', + title="Cool (somewhen in two days) event", + color="blue", start=today_date + timedelta(days=1), end=today_date + timedelta(days=3), all_day=False, - content='this week test event', + content="this week test event", owner_id=user.id, location="Here", is_google_event=False, diff --git a/tests/category_fixture.py b/tests/fixtures/category_fixture.py similarity index 100% rename from tests/category_fixture.py rename to tests/fixtures/category_fixture.py diff --git a/tests/client_fixture.py b/tests/fixtures/client_fixture.py similarity index 91% rename from tests/client_fixture.py rename to tests/fixtures/client_fixture.py index 5f5f8971..7a5d1e3c 100644 --- a/tests/client_fixture.py +++ b/tests/fixtures/client_fixture.py @@ -1,11 +1,12 @@ -from typing import Generator, Iterator +from typing import Generator, Iterator, Dict -from fastapi.testclient import TestClient import pytest +from fastapi.testclient import TestClient from sqlalchemy.orm import Session from app import main from app.database.models import Base, User + from app.routers import ( agenda, audio, @@ -13,8 +14,8 @@ event, friendview, google_connect, - invitation, meds, + notification, profile, weight, ) @@ -22,9 +23,15 @@ from tests import security_testing_routes from tests.conftest import get_test_db, test_engine +LOGIN_DATA_TYPE = Dict[str, str] + main.app.include_router(security_testing_routes.router) +def login_client(client: TestClient, data: LOGIN_DATA_TYPE) -> None: + client.post(client.app.url_path_for("login"), data=data) + + def get_test_placeholder_user() -> User: return User( username="fake_user", @@ -57,6 +64,11 @@ def agenda_test_client() -> Generator[TestClient, None, None]: yield from create_test_client(agenda.get_db) +@pytest.fixture(scope="session") +def notification_test_client(): + yield from create_test_client(notification.get_db) + + @pytest.fixture(scope="session") def friendview_test_client() -> Generator[TestClient, None, None]: yield from create_test_client(friendview.get_db) @@ -77,11 +89,6 @@ def home_test_client() -> Generator[TestClient, None, None]: yield from create_test_client(main.get_db) -@pytest.fixture(scope="session") -def invitation_test_client() -> Generator[TestClient, None, None]: - yield from create_test_client(invitation.get_db) - - @pytest.fixture(scope="session") def categories_test_client() -> Generator[TestClient, None, None]: yield from create_test_client(categories.get_db) diff --git a/tests/comment_fixture.py b/tests/fixtures/comment_fixture.py similarity index 100% rename from tests/comment_fixture.py rename to tests/fixtures/comment_fixture.py diff --git a/tests/dayview_fixture.py b/tests/fixtures/dayview_fixture.py similarity index 100% rename from tests/dayview_fixture.py rename to tests/fixtures/dayview_fixture.py diff --git a/tests/event_fixture.py b/tests/fixtures/event_fixture.py similarity index 83% rename from tests/event_fixture.py rename to tests/fixtures/event_fixture.py index 7c3d8a56..17213e6f 100644 --- a/tests/event_fixture.py +++ b/tests/fixtures/event_fixture.py @@ -13,10 +13,10 @@ def event(sender: User, category: Category, session: Session) -> Event: return create_event( db=session, - title='event', + title="event", start=today_date, end=today_date, - content='test event', + content="test event", owner_id=sender.id, location="Some random location", vc_link=None, @@ -28,11 +28,11 @@ def event(sender: User, category: Category, session: Session) -> Event: def today_event(sender: User, session: Session) -> Event: return create_event( db=session, - title='event 1', + title="event 1", start=today_date + timedelta(hours=7), end=today_date + timedelta(hours=9), all_day=False, - content='test event', + content="test event", owner_id=sender.id, ) @@ -41,12 +41,12 @@ def today_event(sender: User, session: Session) -> Event: def today_event_2(sender: User, session: Session) -> Event: return create_event( db=session, - title='event 2', - color='blue', + title="event 2", + color="blue", start=today_date + timedelta(hours=3), end=today_date + timedelta(days=2, hours=3), all_day=False, - content='test event', + content="test event", owner_id=sender.id, ) @@ -55,12 +55,12 @@ def today_event_2(sender: User, session: Session) -> Event: def yesterday_event(sender: User, session: Session) -> Event: return create_event( db=session, - title='event 3', - color='green', + title="event 3", + color="green", start=today_date - timedelta(hours=8), end=today_date, all_day=False, - content='test event', + content="test event", owner_id=sender.id, ) @@ -69,12 +69,12 @@ def yesterday_event(sender: User, session: Session) -> Event: def next_week_event(sender: User, session: Session) -> Event: return create_event( db=session, - title='event 4', - color='blue', + title="event 4", + color="blue", start=today_date + timedelta(days=7, hours=2), end=today_date + timedelta(days=7, hours=4), all_day=False, - content='test event', + content="test event", owner_id=sender.id, ) @@ -83,12 +83,12 @@ def next_week_event(sender: User, session: Session) -> Event: def next_month_event(sender: User, session: Session) -> Event: return create_event( db=session, - title='event 5', + title="event 5", color="green", start=today_date + timedelta(days=20, hours=4), end=today_date + timedelta(days=20, hours=6), all_day=False, - content='test event', + content="test event", owner_id=sender.id, ) @@ -97,12 +97,12 @@ def next_month_event(sender: User, session: Session) -> Event: def old_event(sender: User, session: Session) -> Event: return create_event( db=session, - title='event 6', + title="event 6", color="red", start=today_date - timedelta(days=5), end=today_date - timedelta(days=1), all_day=False, - content='test event', + content="test event", owner_id=sender.id, ) @@ -111,11 +111,11 @@ def old_event(sender: User, session: Session) -> Event: def all_day_event(sender: User, category: Category, session: Session) -> Event: return create_event( db=session, - title='event', + title="event", start=today_date, end=today_date, all_day=True, - content='test event', + content="test event", owner_id=sender.id, location="Some random location", category_id=category.id, diff --git a/tests/invitation_fixture.py b/tests/fixtures/invitation_fixture.py similarity index 100% rename from tests/invitation_fixture.py rename to tests/fixtures/invitation_fixture.py diff --git a/tests/jokes_fixture.py b/tests/fixtures/jokes_fixture.py similarity index 89% rename from tests/jokes_fixture.py rename to tests/fixtures/jokes_fixture.py index d7e3258c..062d5d45 100644 --- a/tests/jokes_fixture.py +++ b/tests/fixtures/jokes_fixture.py @@ -16,5 +16,5 @@ def joke(session: Session) -> Joke: yield from add_joke( session=session, id_joke=1, - text='Chuck Norris can slam a revolving door.', + text="Chuck Norris can slam a revolving door.", ) diff --git a/tests/logger_fixture.py b/tests/fixtures/logger_fixture.py similarity index 100% rename from tests/logger_fixture.py rename to tests/fixtures/logger_fixture.py diff --git a/tests/fixtures/message_fixture.py b/tests/fixtures/message_fixture.py new file mode 100644 index 00000000..839051ba --- /dev/null +++ b/tests/fixtures/message_fixture.py @@ -0,0 +1,29 @@ +import pytest +from sqlalchemy.orm import Session + +from app.database.models import Message, User +from app.internal.utils import create_model, delete_instance + + +@pytest.fixture +def message(user: User, session: Session) -> Message: + invitation = create_model( + session, Message, + body='A test message', + link='#', + recipient_id=user.id, + ) + yield invitation + delete_instance(session, invitation) + + +@pytest.fixture +def sec_message(user: User, session: Session) -> Message: + invitation = create_model( + session, Message, + body='A test message', + link='#', + recipient_id=user.id, + ) + yield invitation + delete_instance(session, invitation) diff --git a/tests/quotes_fixture.py b/tests/fixtures/quotes_fixture.py similarity index 100% rename from tests/quotes_fixture.py rename to tests/fixtures/quotes_fixture.py diff --git a/tests/user_fixture.py b/tests/fixtures/user_fixture.py similarity index 68% rename from tests/user_fixture.py rename to tests/fixtures/user_fixture.py index b50fb900..e2a7ad26 100644 --- a/tests/user_fixture.py +++ b/tests/fixtures/user_fixture.py @@ -4,20 +4,24 @@ from sqlalchemy.orm import Session from app.database.models import User +from app.database.schemas import UserCreate from app.internal.utils import create_model, delete_instance +from app.routers.register import create_user @pytest.fixture -def user(session: Session) -> Generator[User, None, None]: - mock_user = create_model( - session, - User, +async def user(session: Session) -> Generator[User, None, None]: + schema = UserCreate( username="test_username", password="test_password", + confirm_password="test_password", email="test.email@gmail.com", + full_name="test_full_name", + description="test_description", language_id=1, target_weight=60, ) + mock_user = await create_user(session, schema) yield mock_user delete_instance(session, mock_user) diff --git a/tests/zodiac_fixture.py b/tests/fixtures/zodiac_fixture.py similarity index 100% rename from tests/zodiac_fixture.py rename to tests/fixtures/zodiac_fixture.py diff --git a/tests/salary/test_routes.py b/tests/salary/test_routes.py index 13e22e05..830569ff 100644 --- a/tests/salary/test_routes.py +++ b/tests/salary/test_routes.py @@ -199,7 +199,6 @@ def test_invalid_category_redirect( response = salary_test_client.get(path) assert any(temp.status_code == status.HTTP_307_TEMPORARY_REDIRECT for temp in response.history) - print(response.text) assert message in response.text diff --git a/tests/test_a_telegram_asyncio.py b/tests/test_a_telegram_asyncio.py index faf99d98..ff1a7ddf 100644 --- a/tests/test_a_telegram_asyncio.py +++ b/tests/test_a_telegram_asyncio.py @@ -6,141 +6,145 @@ from app.telegram.handlers import MessageHandler, reply_unknown_user from app.telegram.keyboards import DATE_FORMAT from app.telegram.models import Bot, Chat -from tests.asyncio_fixture import today_date -from tests.client_fixture import get_test_placeholder_user +from tests.fixtures.asyncio_fixture import today_date +from tests.fixtures.client_fixture import get_test_placeholder_user def gen_message(text): return { - 'update_id': 10000000, - 'message': { - 'message_id': 2434, - 'from': { - 'id': 666666, - 'is_bot': False, - 'first_name': 'Moshe', - 'username': 'banana', - 'language_code': 'en' + "update_id": 10000000, + "message": { + "message_id": 2434, + "from": { + "id": 666666, + "is_bot": False, + "first_name": "Moshe", + "username": "banana", + "language_code": "en", }, - 'chat': { - 'id': 666666, - 'first_name': 'Moshe', - 'username': 'banana', - 'type': 'private' + "chat": { + "id": 666666, + "first_name": "Moshe", + "username": "banana", + "type": "private", }, - 'date': 1611240725, - 'text': f'{text}' - } + "date": 1611240725, + "text": f"{text}", + }, } def gen_callback(text): return { - 'update_id': 568265, - 'callback_query': { - 'id': '546565356486', - 'from': { - 'id': 666666, - 'is_bot': False, - 'first_name': 'Moshe', - 'username': 'banana', - 'language_code': 'en' - }, 'message': { - 'message_id': 838, - 'from': { - 'id': 2566252, - 'is_bot': True, - 'first_name': 'PyLandar', - 'username': 'pylander_bot' - }, 'chat': { - 'id': 666666, - 'first_name': 'Moshe', - 'username': 'banana', - 'type': 'private' + "update_id": 568265, + "callback_query": { + "id": "546565356486", + "from": { + "id": 666666, + "is_bot": False, + "first_name": "Moshe", + "username": "banana", + "language_code": "en", + }, + "message": { + "message_id": 838, + "from": { + "id": 2566252, + "is_bot": True, + "first_name": "PyLandar", + "username": "pylander_bot", + }, + "chat": { + "id": 666666, + "first_name": "Moshe", + "username": "banana", + "type": "private", }, - 'date': 161156, - 'text': 'Choose events day.', - 'reply_markup': { - 'inline_keyboard': [ + "date": 161156, + "text": "Choose events day.", + "reply_markup": { + "inline_keyboard": [ [ + {"text": "Today", "callback_data": "Today"}, { - 'text': 'Today', - 'callback_data': 'Today' + "text": "This week", + "callback_data": "This week", }, - { - 'text': 'This week', - 'callback_data': 'This week' - } - ] - ] - } + ], + ], + }, }, - 'chat_instance': '-154494', - 'data': f'{text}'}} + "chat_instance": "-154494", + "data": f"{text}", + }, + } class TestChatModel: - @staticmethod def test_private_message(): - chat = Chat(gen_message('Cool message')) - assert chat.message == 'Cool message' + chat = Chat(gen_message("Cool message")) + assert chat.message == "Cool message" assert chat.user_id == 666666 - assert chat.first_name == 'Moshe' + assert chat.first_name == "Moshe" @staticmethod def test_callback_message(): - chat = Chat(gen_callback('Callback Message')) - assert chat.message == 'Callback Message' + chat = Chat(gen_callback("Callback Message")) + assert chat.message == "Callback Message" assert chat.user_id == 666666 - assert chat.first_name == 'Moshe' + assert chat.first_name == "Moshe" @pytest.mark.asyncio async def test_bot_model(): bot = Bot("fake bot id", "https://google.com") - assert bot.base == 'https://api.telegram.org/botfake bot id/' - assert bot.webhook_setter_url == 'https://api.telegram.org/botfake \ -bot id/setWebhook?url=https://google.com/telegram/' + assert bot.base == "https://api.telegram.org/botfake bot id/" + assert ( + bot.webhook_setter_url + == "https://api.telegram.org/botfake \ +bot id/setWebhook?url=https://google.com/telegram/" + ) assert bot.base == bot._set_base_url("fake bot id") assert bot.webhook_setter_url == bot._set_webhook_setter_url( - "https://google.com") + "https://google.com", + ) set_request = await bot.set_webhook() assert set_request.json() == { - 'ok': False, - 'error_code': 404, - 'description': 'Not Found' + "ok": False, + "error_code": 404, + "description": "Not Found", } drop_request = await bot.drop_webhook() assert drop_request.json() == { - 'ok': False, - 'error_code': 404, - 'description': 'Not Found' + "ok": False, + "error_code": 404, + "description": "Not Found", } send_request = await bot.send_message("654654645", "hello") assert send_request.status_code == status.HTTP_404_NOT_FOUND assert send_request.json() == { - 'ok': False, - 'error_code': 404, - 'description': 'Not Found' + "ok": False, + "error_code": 404, + "description": "Not Found", } class TestBotClient: - @staticmethod @pytest.mark.asyncio async def test_user_not_registered(telegram_client): response = await telegram_client.post( - '/telegram/', json=gen_message('/start')) + "/telegram/", + json=gen_message("/start"), + ) assert response.status_code == status.HTTP_200_OK - assert b'Hello, Moshe!' in response.content - assert b'To use PyLendar Bot you have to register' \ - in response.content + assert b"Hello, Moshe!" in response.content + assert b"To use PyLendar Bot you have to register" in response.content @staticmethod @pytest.mark.asyncio @@ -148,9 +152,11 @@ async def test_user_registered(telegram_client, session): session.add(get_test_placeholder_user()) session.commit() response = await telegram_client.post( - '/telegram/', json=gen_message('/start')) + "/telegram/", + json=gen_message("/start"), + ) assert response.status_code == status.HTTP_200_OK - assert b'Welcome to PyLendar telegram client!' in response.content + assert b"Welcome to PyLendar telegram client!" in response.content class TestHandlers: @@ -158,21 +164,27 @@ class TestHandlers: @pytest.mark.asyncio async def test_start_handlers(self): - chat = Chat(gen_message('/start')) + chat = Chat(gen_message("/start")) message = MessageHandler(chat, self.TEST_USER) - assert '/start' in message.handlers - assert await message.process_callback() == '''Hello, Moshe! -Welcome to PyLendar telegram client!''' + assert "/start" in message.handlers + assert ( + await message.process_callback() + == """Hello, Moshe! +Welcome to PyLendar telegram client!""" + ) @pytest.mark.asyncio async def test_default_handlers(self): wrong_start = MessageHandler( - Chat(gen_message('start')), self.TEST_USER) + Chat(gen_message("start")), + self.TEST_USER, + ) wrong_show_events = MessageHandler( - Chat(gen_message('show_events')), self.TEST_USER) - message = MessageHandler( - Chat(gen_message('hello')), self.TEST_USER) + Chat(gen_message("show_events")), + self.TEST_USER, + ) + message = MessageHandler(Chat(gen_message("hello")), self.TEST_USER) assert await wrong_start.process_callback() == "Unknown command." assert await wrong_show_events.process_callback() == "Unknown command." @@ -180,34 +192,34 @@ async def test_default_handlers(self): @pytest.mark.asyncio async def test_show_events_handler(self): - chat = Chat(gen_message('/show_events')) + chat = Chat(gen_message("/show_events")) message = MessageHandler(chat, self.TEST_USER) - assert await message.process_callback() == 'Choose events day.' + assert await message.process_callback() == "Choose events day." @pytest.mark.asyncio async def test_no_today_events_handler(self): - chat = Chat(gen_callback('Today')) + chat = Chat(gen_callback("Today")) message = MessageHandler(chat, self.TEST_USER) assert await message.process_callback() == "There're no events today." @pytest.mark.asyncio async def test_today_handler(self, fake_user_events): - chat = Chat(gen_callback('Today')) + chat = Chat(gen_callback("Today")) message = MessageHandler(chat, fake_user_events) answer = f"{today_date.strftime('%A, %B %d')}:\n" assert await message.process_callback() == answer @pytest.mark.asyncio async def test_this_week_handler(self): - chat = Chat(gen_callback('This week')) + chat = Chat(gen_callback("This week")) message = MessageHandler(chat, self.TEST_USER) - assert await message.process_callback() == 'Choose a day.' + assert await message.process_callback() == "Choose a day." @pytest.mark.asyncio async def test_no_chosen_day_handler(self): - chat = Chat(gen_callback('10 Feb 2021')) + chat = Chat(gen_callback("10 Feb 2021")) message = MessageHandler(chat, self.TEST_USER) - message.handlers['10 Feb 2021'] = message.chosen_day_handler + message.handlers["10 Feb 2021"] = message.chosen_day_handler answer = "There're no events on February 10." assert await message.process_callback() == answer @@ -223,99 +235,101 @@ async def test_chosen_day_handler(self, fake_user_events): @pytest.mark.asyncio async def test_new_event_handler(self): - chat = Chat(gen_message('/new_event')) + chat = Chat(gen_message("/new_event")) message = MessageHandler(chat, self.TEST_USER) - answer = 'Please, give your event a title.' + answer = "Please, give your event a title." assert await message.process_callback() == answer @pytest.mark.asyncio async def test_process_new_event(self): - chat = Chat(gen_message('New Title')) + chat = Chat(gen_message("New Title")) message = MessageHandler(chat, self.TEST_USER) - answer = 'Title:\nNew Title\n\n' - answer += 'Add a description of the event.' + answer = "Title:\nNew Title\n\n" + answer += "Add a description of the event." assert await message.process_callback() == answer - chat = Chat(gen_message('New Content')) + chat = Chat(gen_message("New Content")) message = MessageHandler(chat, self.TEST_USER) - answer = 'Content:\nNew Content\n\n' - answer += 'Where the event will be held?' + answer = "Content:\nNew Content\n\n" + answer += "Where the event will be held?" assert await message.process_callback() == answer - chat = Chat(gen_message('Universe')) + chat = Chat(gen_message("Universe")) message = MessageHandler(chat, self.TEST_USER) - answer = 'Location:\nUniverse\n\n' - answer += 'When does it start?' + answer = "Location:\nUniverse\n\n" + answer += "When does it start?" assert await message.process_callback() == answer - chat = Chat(gen_message('Not valid start datetime input')) + chat = Chat(gen_message("Not valid start datetime input")) message = MessageHandler(chat, self.TEST_USER) - answer = '❗️ Please, enter a valid date/time.' + answer = "❗️ Please, enter a valid date/time." assert await message.process_callback() == answer - chat = Chat(gen_message('today')) + chat = Chat(gen_message("today")) message = MessageHandler(chat, self.TEST_USER) today = datetime.today() answer = f'Starts on:\n{today.strftime("%d %b %Y %H:%M")}\n\n' - answer += 'And when does it end?' + answer += "And when does it end?" assert await message.process_callback() == answer - chat = Chat(gen_message('Not valid end datetime input')) + chat = Chat(gen_message("Not valid end datetime input")) message = MessageHandler(chat, self.TEST_USER) - answer = '❗️ Please, enter a valid date/time.' + answer = "❗️ Please, enter a valid date/time." assert await message.process_callback() == answer - chat = Chat(gen_message('tomorrow')) + chat = Chat(gen_message("tomorrow")) message = MessageHandler(chat, self.TEST_USER) tomorrow = today + timedelta(days=1) - answer = 'Title:\nNew Title\n\n' - answer += 'Content:\nNew Content\n\n' - answer += 'Location:\nUniverse\n\n' + answer = "Title:\nNew Title\n\n" + answer += "Content:\nNew Content\n\n" + answer += "Location:\nUniverse\n\n" answer += f'Starts on:\n{today.strftime("%d %b %Y %H:%M")}\n\n' answer += f'Ends on:\n{tomorrow.strftime("%d %b %Y %H:%M")}' assert await message.process_callback() == answer - chat = Chat(gen_message('create')) + chat = Chat(gen_message("create")) message = MessageHandler(chat, self.TEST_USER) - answer = 'New event was successfully created 🎉' + answer = "New event was successfully created 🎉" assert await message.process_callback() == answer @pytest.mark.asyncio async def test_process_new_event_cancel(self): - chat = Chat(gen_message('/new_event')) + chat = Chat(gen_message("/new_event")) message = MessageHandler(chat, self.TEST_USER) - answer = 'Please, give your event a title.' + answer = "Please, give your event a title." assert await message.process_callback() == answer - chat = Chat(gen_message('cancel')) + chat = Chat(gen_message("cancel")) message = MessageHandler(chat, self.TEST_USER) - answer = '🚫 The process was canceled.' + answer = "🚫 The process was canceled." assert await message.process_callback() == answer @pytest.mark.asyncio async def test_process_new_event_restart(self): - chat = Chat(gen_message('/new_event')) + chat = Chat(gen_message("/new_event")) message = MessageHandler(chat, self.TEST_USER) - answer = 'Please, give your event a title.' + answer = "Please, give your event a title." assert await message.process_callback() == answer - chat = Chat(gen_message('New Title')) + chat = Chat(gen_message("New Title")) message = MessageHandler(chat, self.TEST_USER) - answer = 'Title:\nNew Title\n\n' - answer += 'Add a description of the event.' + answer = "Title:\nNew Title\n\n" + answer += "Add a description of the event." assert await message.process_callback() == answer - chat = Chat(gen_message('restart')) + chat = Chat(gen_message("restart")) message = MessageHandler(chat, self.TEST_USER) - answer = 'Please, give your event a title.' + answer = "Please, give your event a title." assert await message.process_callback() == answer @pytest.mark.asyncio async def test_reply_unknown_user(): - chat = Chat(gen_message('/show_events')) + chat = Chat(gen_message("/show_events")) answer = await reply_unknown_user(chat) - assert answer == ''' + assert ( + answer + == """ Hello, Moshe! To use PyLendar Bot you have to register @@ -325,4 +339,5 @@ async def test_reply_unknown_user(): Keep it secret! https://calendar.pythonic.guru/profile/ -''' +""" + ) diff --git a/tests/test_calendar_privacy.py b/tests/test_calendar_privacy.py index d94dfd56..0fde3d0a 100644 --- a/tests/test_calendar_privacy.py +++ b/tests/test_calendar_privacy.py @@ -1,7 +1,8 @@ from app.internal.calendar_privacy import can_show_calendar + # TODO after user system is merged: -# from app.internal.security.dependencies import CurrentUser -from app.routers.user import create_user +# from app.internal.security.dependancies import CurrentUser +from app.routers.register import _create_user def test_can_show_calendar_public(session, user): @@ -10,32 +11,37 @@ def test_can_show_calendar_public(session, user): # current_user = CurrentUser(**user.__dict__) current_user = user result = can_show_calendar( - requested_user_username='test_username', - db=session, current_user=current_user + requested_user_username="test_username", + db=session, + current_user=current_user, ) assert result is True session.commit() def test_can_show_calendar_private(session, user): - another_user = create_user( + another_user = _create_user( session=session, - username='new_test_username2', - email='new_test.email2@gmail.com', - password='passpar_2', - language_id=1 + username="new_test_username2", + email="new_test.email2@gmail.com", + password="passpar_2", + language_id=1, + full_name="test_full_name", + description="test_description", ) current_user = user # TODO to be replaced after user system is merged: # current_user = CurrentUser(**user.__dict__) result_a = can_show_calendar( - requested_user_username='new_test_username2', - db=session, current_user=current_user + requested_user_username="new_test_username2", + db=session, + current_user=current_user, ) result_b = can_show_calendar( - requested_user_username='test_username', - db=session, current_user=current_user + requested_user_username="test_username", + db=session, + current_user=current_user, ) assert result_a is False assert result_b is True diff --git a/tests/test_google_connect.py b/tests/test_google_connect.py index 02511266..58ac8aa3 100644 --- a/tests/test_google_connect.py +++ b/tests/test_google_connect.py @@ -5,12 +5,13 @@ import app.internal.google_connect as google_connect from app.routers.event import create_event from app.database.models import OAuthCredentials -from app.routers.user import create_user from google.oauth2.credentials import Credentials from googleapiclient.discovery import build from googleapiclient.http import HttpMock +from app.routers.register import _create_user + @pytest.fixture def google_events_mock(): @@ -24,25 +25,13 @@ def google_events_mock(): "created": "2021-01-13T09:10:02.000Z", "updated": "2021-01-13T09:10:02.388Z", "summary": "some title", - "creator": { - "email": "someemail", - "self": True - }, - "organizer": { - "email": "someemail", - "self": True - }, - "start": { - "dateTime": "2021-02-25T13:00:00+02:00" - }, - "end": { - "dateTime": "2021-02-25T14:00:00+02:00" - }, + "creator": {"email": "someemail", "self": True}, + "organizer": {"email": "someemail", "self": True}, + "start": {"dateTime": "2021-02-25T13:00:00+02:00"}, + "end": {"dateTime": "2021-02-25T14:00:00+02:00"}, "iCalUID": "somecode", "sequence": 0, - "reminders": { - "useDefault": True - } + "reminders": {"useDefault": True}, }, { "kind": "calendar#event", @@ -53,27 +42,15 @@ def google_events_mock(): "created": "2021-01-13T09:10:02.000Z", "updated": "2021-01-13T09:10:02.388Z", "summary": "some title to all day event", - "creator": { - "email": "someemail", - "self": True - }, - "organizer": { - "email": "someemail", - "self": True - }, - "start": { - "date": "2021-02-25" - }, - "end": { - "date": "2021-02-25" - }, + "creator": {"email": "someemail", "self": True}, + "organizer": {"email": "someemail", "self": True}, + "start": {"date": "2021-02-25"}, + "end": {"date": "2021-02-25"}, "iCalUID": "somecode", "sequence": 0, - "location": 'somelocation', - "reminders": { - "useDefault": True - } - } + "location": "somelocation", + "reminders": {"useDefault": True}, + }, ] @@ -85,7 +62,7 @@ def credentials(): token_uri="some_uri", client_id="somecode", client_secret="some_secret", - expiry=datetime(2021, 1, 28) + expiry=datetime(2021, 1, 28), ) return cred @@ -100,30 +77,30 @@ def test_push_events_to_db(google_events_mock, user, session): def test_db_cleanup(google_events_mock, user, session): for event in google_events_mock: location = None - title = event['summary'] + title = event["summary"] # support for all day events - if 'dateTime' in event['start'].keys(): + if "dateTime" in event["start"].keys(): # part time event - start = datetime.fromisoformat(event['start']['dateTime']) - end = datetime.fromisoformat(event['end']['dateTime']) + start = datetime.fromisoformat(event["start"]["dateTime"]) + end = datetime.fromisoformat(event["end"]["dateTime"]) else: # all day event - start = event['start']['date'].split('-') + start = event["start"]["date"].split("-") start = datetime( year=int(start[0]), month=int(start[1]), - day=int(start[2]) + day=int(start[2]), ) - end = event['end']['date'].split('-') + end = event["end"]["date"].split("-") end = datetime( year=int(end[0]), month=int(end[1]), - day=int(end[2]) + day=int(end[2]), ) - if 'location' in event.keys(): - location = event['location'] + if "location" in event.keys(): + location = event["location"] create_event( db=session, @@ -132,20 +109,26 @@ def test_db_cleanup(google_events_mock, user, session): end=end, owner_id=user.id, location=location, - is_google_event=True + is_google_event=True, ) assert google_connect.cleanup_user_google_calendar_events( - user, session) + user, + session, + ) @pytest.mark.usefixtures("session") def test_get_credentials_from_db(session): - user = create_user(session=session, - username='new_test_username', - password='new_test_password', - email='new_test.email@gmail.com', - language_id=1) + user = _create_user( + session=session, + username="new_test_username", + password="new_test_password", + email="new_test.email@gmail.com", + language_id=1, + full_name="test_full_name", + description="test_description", + ) credentials = OAuthCredentials( owner=user, @@ -154,7 +137,7 @@ def test_get_credentials_from_db(session): token_uri="some_uri", client_id="somecode", client_secret="some_secret", - expiry=datetime(2021, 2, 22) + expiry=datetime(2021, 2, 22), ) session.add(credentials) session.commit() @@ -166,17 +149,16 @@ def test_get_credentials_from_db(session): @pytest.mark.usefixtures("session", "user", "credentials") def test_refresh_token(mocker, session, user, credentials): - mocker.patch( - 'google.oauth2.credentials.Credentials.refresh', - return_value=logger.debug('refreshed') + "google.oauth2.credentials.Credentials.refresh", + return_value=logger.debug("refreshed"), ) assert google_connect.refresh_token(credentials, user, session) mocker.patch( - 'google.oauth2.credentials.Credentials.expired', - return_value=False + "google.oauth2.credentials.Credentials.expired", + return_value=False, ) assert google_connect.refresh_token(credentials, user, session) @@ -189,76 +171,75 @@ def __init__(self, service): self.service = service def list(self, *args): - request = self.service.events().list(calendarId='primary', - timeMin=datetime( - 2021, 1, 1).isoformat(), - timeMax=datetime( - 2022, 1, 1).isoformat(), - singleEvents=True, - orderBy='startTime' - ) - http = HttpMock( - 'calendar-linux.json', - {'status': '200'} + request = self.service.events().list( + calendarId="primary", + timeMin=datetime(2021, 1, 1).isoformat(), + timeMax=datetime(2022, 1, 1).isoformat(), + singleEvents=True, + orderBy="startTime", ) + http = HttpMock("calendar-linux.json", {"status": "200"}) response = request.execute(http=http) return response - http = HttpMock( - './tests/calendar-discovery.json', - {'status': '200'} - ) + http = HttpMock("./tests/calendar-discovery.json", {"status": "200"}) - service = build('calendar', 'v3', http=http) + service = build("calendar", "v3", http=http) mocker.patch( - 'googleapiclient.discovery.build', + "googleapiclient.discovery.build", return_value=service, - events=service + events=service, ) mocker.patch( - 'googleapiclient.discovery.Resource', - events=mock_events(service) + "googleapiclient.discovery.Resource", + events=mock_events(service), ) assert google_connect.get_current_year_events(credentials, user, session) -@pytest.mark.usefixtures("user", "session", - "google_connect_test_client", "credentials") +@pytest.mark.usefixtures( + "user", + "session", + "google_connect_test_client", + "credentials", +) def test_google_sync(mocker, google_connect_test_client, session, credentials): - create_user(session=session, - username='new_test_username', - password='new_test_password', - email='new_test.email@gmail.com', - language_id=1) + _create_user( + session=session, + username="new_test_username", + password="new_test_password", + email="new_test.email@gmail.com", + language_id=1, + full_name="test_full_name", + description="test_description", + ) mocker.patch( - 'app.routers.google_connect.get_credentials', - return_value=credentials + "app.routers.google_connect.get_credentials", + return_value=credentials, ) mocker.patch( - 'app.routers.google_connect.fetch_save_events', - return_value=None + "app.routers.google_connect.fetch_save_events", + return_value=None, ) connect = google_connect_test_client.get( - 'google/sync', - headers={ - "referer": 'http://testserver/' - }) + "google/sync", + headers={"referer": "http://testserver/"}, + ) assert connect.ok # second case mocker.patch( - 'app.routers.google_connect.get_credentials', - return_value=None + "app.routers.google_connect.get_credentials", + return_value=None, ) connect = google_connect_test_client.get( - 'google/sync', - headers={ - "referer": 'http://testserver/' - }) + "google/sync", + headers={"referer": "http://testserver/"}, + ) assert connect.ok @@ -270,97 +251,125 @@ def test_is_client_secret_none(): @pytest.mark.usefixtures("session") def test_clean_up_old_credentials_from_db(session): google_connect.clean_up_old_credentials_from_db(session) - assert len(session.query(OAuthCredentials) - .filter_by(user_id=None).all()) == 0 + assert ( + len(session.query(OAuthCredentials).filter_by(user_id=None).all()) == 0 + ) -@pytest.mark.usefixtures("session", 'user', 'credentials') -def test_get_credentials_from_consent_screen(mocker, session, - user, credentials): +@pytest.mark.usefixtures("session", "user", "credentials") +def test_get_credentials_from_consent_screen( + mocker, + session, + user, + credentials, +): mocker.patch( - 'google_auth_oauthlib.flow.InstalledAppFlow.from_client_secrets_file', - return_value=mocker.Mock(name='flow', **{ - "credentials": credentials, - "run_local_server": mocker.Mock(name='run_local_server', - return_value=logger.debug( - 'running server')) - }) + "google_auth_oauthlib.flow.InstalledAppFlow.from_client_secrets_file", + return_value=mocker.Mock( + name="flow", + **{ + "credentials": credentials, + "run_local_server": mocker.Mock( + name="run_local_server", + return_value=logger.debug("running server"), + ), + } + ), ) mocker.patch( - 'app.internal.google_connect.is_client_secret_none', - return_value=False + "app.internal.google_connect.is_client_secret_none", + return_value=False, ) - assert google_connect.get_credentials_from_consent_screen( - user, session) == credentials + assert ( + google_connect.get_credentials_from_consent_screen(user, session) + == credentials + ) @pytest.mark.usefixtures("session") def test_create_google_event(session): - user = create_user(session=session, - username='new_test_username', - password='new_test_password', - email='new_test.email@gmail.com', - language_id=1) + user = _create_user( + session=session, + username="new_test_username", + password="new_test_password", + email="new_test.email@gmail.com", + language_id=1, + full_name="test_full_name", + description="test_description", + ) event = google_connect.create_google_event( - 'title', - datetime(2021, 1, 1, 15, 15), - datetime(2021, 1, 1, 15, 30), - user, - 'location', - session - ) + "title", + datetime(2021, 1, 1, 15, 15), + datetime(2021, 1, 1, 15, 30), + user, + "location", + session, + ) - assert event.title == 'title' + assert event.title == "title" -@pytest.mark.usefixtures("session", "user", 'credentials') +@pytest.mark.usefixtures("session", "user", "credentials") def test_get_credentials(mocker, session, user, credentials): - user = create_user( + user = _create_user( session=session, - username='new_test_username', - password='new_test_password', - email='new_test.email@gmail.com', - language_id=1 + username="new_test_username", + password="new_test_password", + email="new_test.email@gmail.com", + language_id=1, + full_name="test_full_name", + description="test_description", ) mocker.patch( - 'app.internal.google_connect.get_credentials_from_consent_screen', - return_value=credentials + "app.internal.google_connect.get_credentials_from_consent_screen", + return_value=credentials, ) - assert google_connect.get_credentials(user=user, - session=session) == credentials + assert ( + google_connect.get_credentials(user=user, session=session) + == credentials + ) mocker.patch( - 'app.internal.google_connect.get_credentials', - return_value=credentials + "app.internal.google_connect.get_credentials", + return_value=credentials, ) mocker.patch( - 'app.internal.google_connect.refresh_token', - return_value=credentials + "app.internal.google_connect.refresh_token", + return_value=credentials, ) - assert google_connect.get_credentials(user=user, - session=session) == credentials - + assert ( + google_connect.get_credentials(user=user, session=session) + == credentials + ) -@pytest.mark.usefixtures("session", "user", - 'credentials', 'google_events_mock') -def test_fetch_save_events(mocker, session, user, credentials, - google_events_mock): +@pytest.mark.usefixtures( + "session", + "user", + "credentials", + "google_events_mock", +) +def test_fetch_save_events( + mocker, + session, + user, + credentials, + google_events_mock, +): mocker.patch( - 'app.internal.google_connect.get_current_year_events', - return_value=google_events_mock + "app.internal.google_connect.get_current_year_events", + return_value=google_events_mock, ) - assert google_connect.fetch_save_events(credentials, - user, session) is None + assert google_connect.fetch_save_events(credentials, user, session) is None -@pytest.mark.usefixtures("session", "user", 'credentials') +@pytest.mark.usefixtures("session", "user", "credentials") def test_push_credentials_to_db(session, user, credentials): assert google_connect.push_credentials_to_db(credentials, user, session) diff --git a/tests/test_invitation.py b/tests/test_invitation.py deleted file mode 100644 index c609a973..00000000 --- a/tests/test_invitation.py +++ /dev/null @@ -1,50 +0,0 @@ -from fastapi import status - -from app.routers.invitation import get_all_invitations, get_invitation_by_id - - -class TestInvitations: - NO_INVITATIONS = b"You don't have any invitations." - URL = "/invitations/" - - @staticmethod - def test_view_no_invitations(invitation_test_client): - response = invitation_test_client.get(TestInvitations.URL) - assert response.ok - assert TestInvitations.NO_INVITATIONS in response.content - - @staticmethod - def test_accept_invitations(user, invitation, invitation_test_client): - invitation = {"invite_id ": invitation.id} - resp = invitation_test_client.post( - TestInvitations.URL, data=invitation) - assert resp.status_code == status.HTTP_302_FOUND - - @staticmethod - def test_get_all_invitations_success(invitation, event, user, session): - invitations = get_all_invitations(event=event, db=session) - assert invitations == [invitation] - invitations = get_all_invitations(recipient=user, db=session) - assert invitations == [invitation] - - @staticmethod - def test_get_all_invitations_failure(user, session): - invitations = get_all_invitations(unknown_parameter=user, db=session) - assert invitations == [] - - invitations = get_all_invitations(recipient=None, db=session) - assert invitations == [] - - @staticmethod - def test_get_invitation_by_id(invitation, session): - get_invitation = get_invitation_by_id(invitation.id, db=session) - assert get_invitation == invitation - - @staticmethod - def test_repr(invitation): - invitation_repr = ( - f'' - ) - assert invitation.__repr__() == invitation_repr diff --git a/tests/test_notification.py b/tests/test_notification.py new file mode 100644 index 00000000..23eacfd2 --- /dev/null +++ b/tests/test_notification.py @@ -0,0 +1,177 @@ +from starlette.status import HTTP_406_NOT_ACCEPTABLE + +from app.database.models import InvitationStatusEnum, MessageStatusEnum +from app.internal.notification import get_all_invitations, get_invitation_by_id +from app.routers.notification import router +from tests.fixtures.client_fixture import login_client + + +class TestNotificationRoutes: + NO_NOTIFICATIONS = b"You don't have any new notifications." + NO_NOTIFICATION_IN_ARCHIVE = b"You don't have any archived notifications." + NEW_NOTIFICATIONS_URL = router.url_path_for("view_notifications") + LOGIN_DATA = {"username": "test_username", "password": "test_password"} + + def test_view_no_notifications( + self, + user, + notification_test_client, + ): + login_client(notification_test_client, self.LOGIN_DATA) + resp = notification_test_client.get(self.NEW_NOTIFICATIONS_URL) + assert resp.ok + assert self.NO_NOTIFICATIONS in resp.content + + def test_accept_invitations( + self, + user, + invitation, + notification_test_client, + ): + login_client(notification_test_client, self.LOGIN_DATA) + assert invitation.status == InvitationStatusEnum.UNREAD + data = { + "invite_id": invitation.id, + "next_url": self.NEW_NOTIFICATIONS_URL, + } + url = router.url_path_for("accept_invitations") + resp = notification_test_client.post(url, data=data) + assert resp.ok + assert InvitationStatusEnum.ACCEPTED + + def test_decline_invitations( + self, + user, + invitation, + notification_test_client, + session, + ): + login_client(notification_test_client, self.LOGIN_DATA) + assert invitation.status == InvitationStatusEnum.UNREAD + data = { + "invite_id": invitation.id, + "next_url": self.NEW_NOTIFICATIONS_URL, + } + url = router.url_path_for("decline_invitations") + resp = notification_test_client.post(url, data=data) + assert resp.ok + session.refresh(invitation) + assert invitation.status == InvitationStatusEnum.DECLINED + + def test_mark_message_as_read( + self, + user, + message, + notification_test_client, + session, + ): + login_client(notification_test_client, self.LOGIN_DATA) + assert message.status == MessageStatusEnum.UNREAD + data = { + "message_id": message.id, + "next_url": self.NEW_NOTIFICATIONS_URL, + } + url = router.url_path_for("mark_message_as_read") + resp = notification_test_client.post(url, data=data) + assert resp.ok + session.refresh(message) + assert message.status == MessageStatusEnum.READ + + def test_mark_all_as_read( + self, + user, + message, + sec_message, + notification_test_client, + session, + ): + login_client(notification_test_client, self.LOGIN_DATA) + url = router.url_path_for("mark_all_as_read") + assert message.status == MessageStatusEnum.UNREAD + assert sec_message.status == MessageStatusEnum.UNREAD + data = {"next_url": self.NEW_NOTIFICATIONS_URL} + resp = notification_test_client.post(url, data=data) + assert resp.ok + session.refresh(message) + session.refresh(sec_message) + assert message.status == MessageStatusEnum.READ + assert sec_message.status == MessageStatusEnum.READ + + def test_archive( + self, + user, + message, + notification_test_client, + session, + ): + login_client(notification_test_client, self.LOGIN_DATA) + archive_url = router.url_path_for("view_archive") + resp = notification_test_client.get(archive_url) + assert resp.ok + assert self.NO_NOTIFICATION_IN_ARCHIVE in resp.content + + # read message + data = { + "message_id": message.id, + "next_url": self.NEW_NOTIFICATIONS_URL, + } + url = router.url_path_for("mark_message_as_read") + notification_test_client.post(url, data=data) + + resp = notification_test_client.get(archive_url) + assert resp.ok + assert self.NO_NOTIFICATION_IN_ARCHIVE not in resp.content + + def test_wrong_id( + self, + user, + notification_test_client, + session, + ): + login_client(notification_test_client, self.LOGIN_DATA) + data = { + "message_id": 1, + "next_url": "/", + } + url = router.url_path_for("mark_message_as_read") + resp = notification_test_client.post(url, data=data) + assert resp.status_code == HTTP_406_NOT_ACCEPTABLE + + +class TestNotification: + def test_get_all_invitations_success( + self, + invitation, + event, + user, + session, + ): + invitations = get_all_invitations(event=event, session=session) + assert invitations == [invitation] + invitations = get_all_invitations(recipient=user, session=session) + assert invitations == [invitation] + + def test_get_all_invitations_failure(self, user, session): + invitations = get_all_invitations( + unknown_parameter=user, + session=session, + ) + assert invitations == [] + + invitations = get_all_invitations(recipient=None, session=session) + assert invitations == [] + + def test_get_invitation_by_id(self, invitation, session): + get_invitation = get_invitation_by_id(invitation.id, session=session) + assert get_invitation == invitation + + def test_invitation_repr(self, invitation): + invitation_repr = ( + f"" + ) + assert invitation.__repr__() == invitation_repr + + def test_message_repr(self, message): + message_repr = f"" + assert message.__repr__() == message_repr diff --git a/tests/test_share_event.py b/tests/test_share_event.py index 67679b2c..e202597a 100644 --- a/tests/test_share_event.py +++ b/tests/test_share_event.py @@ -1,49 +1,68 @@ -from app.routers.invitation import get_all_invitations -from app.routers.share import (accept, send_email_invitation, - send_in_app_invitation, share, sort_emails) +from app.database.models import InvitationStatusEnum +from app.internal.notification import get_all_invitations +from app.routers.share import ( + send_email_invitation, + send_in_app_invitation, + share, + sort_emails, +) class TestShareEvent: - def test_share_success(self, user, event, session): - participants = [user.email] - share(event, participants, session) - invitations = get_all_invitations(db=session, recipient_id=user.id) + share(event, [user.email], session) + invitations = get_all_invitations( + session=session, + recipient_id=user.id, + ) assert invitations != [] def test_share_failure(self, event, session): participants = [event.owner.email] share(event, participants, session) invitations = get_all_invitations( - db=session, recipient_id=event.owner.id) + session=session, + recipient_id=event.owner.id, + ) assert invitations == [] def test_sort_emails(self, user, session): # the user is being imported # so he will be created data = [ - 'test.email@gmail.com', # registered user - 'not_logged_in@gmail.com', # unregistered user + "test.email@gmail.com", # registered user + "not_logged_in@gmail.com", # unregistered user ] sorted_data = sort_emails(data, session=session) assert sorted_data == { - 'registered': ['test.email@gmail.com'], - 'unregistered': ['not_logged_in@gmail.com'] + "registered": ["test.email@gmail.com"], + "unregistered": ["not_logged_in@gmail.com"], } def test_send_in_app_invitation_success( - self, user, sender, event, session + self, + user, + sender, + event, + session, ): assert send_in_app_invitation([user.email], event, session=session) - invitation = get_all_invitations(db=session, recipient=user)[0] + invitation = get_all_invitations(session=session, recipient=user)[0] assert invitation.event.owner == sender assert invitation.recipient == user session.delete(invitation) def test_send_in_app_invitation_failure( - self, user, sender, event, session): - assert (send_in_app_invitation( - [sender.email], event, session=session) is False) + self, + user, + sender, + event, + session, + ): + assert ( + send_in_app_invitation([sender.email], event, session=session) + is False + ) def test_send_email_invitation(self, user, event): send_email_invitation([user.email], event) @@ -51,5 +70,9 @@ def test_send_email_invitation(self, user, event): assert True def test_accept(self, invitation, session): - accept(invitation, session=session) - assert invitation.status == 'accepted' + invitation.accept(session=session) + assert invitation.status == InvitationStatusEnum.ACCEPTED + + def test_decline(self, invitation, session): + invitation.decline(session=session) + assert invitation.status == InvitationStatusEnum.DECLINED diff --git a/tests/test_statistics.py b/tests/test_statistics.py index 7ef52afb..10cc8457 100644 --- a/tests/test_statistics.py +++ b/tests/test_statistics.py @@ -1,40 +1,63 @@ import datetime +from app.internal.notification import get_all_invitations from app.internal.statistics import get_statistics from app.internal.statistics import INVALID_DATE_RANGE, INVALID_USER from app.internal.statistics import SUCCESS_STATUS from app.routers.event import create_event -from app.routers.user import create_user -from app.routers.share import send_in_app_invitation, accept -from app.routers.invitation import get_all_invitations +from app.routers.register import _create_user +from app.routers.share import send_in_app_invitation def create_events_and_user_events(session, start, end, owner, invitations): for _ in range(1, 3): event = create_event( - db=session, title="title" + str(_), start=start, end=end, - owner_id=owner, location="location" + str(_)) + db=session, + title="title" + str(_), + start=start, + end=end, + owner_id=owner, + location="location" + str(_), + ) send_in_app_invitation(invitations, event, session) def create_data(session): - _ = [create_user("user" + str(_), "password" + str(_), - "email" + str(_) + '@' + 'gmail.com', "Hebrew", - session) for _ in range(1, 4)] + _ = [ + _create_user( + username="user" + str(_), + password="password" + str(_), + email="email" + str(_) + "@" + "gmail.com", + language_id="Hebrew", + session=session, + description="", + full_name="", + ) + for _ in range(1, 4) + ] start = datetime.datetime.now() + datetime.timedelta(hours=-1) end = datetime.datetime.now() + datetime.timedelta(hours=1) - create_events_and_user_events(session, start, end, 1, - ["email2@gmail.com", "email3@gmail.com"]) + create_events_and_user_events( + session, + start, + end, + 1, + ["email2@gmail.com", "email3@gmail.com"], + ) start = datetime.datetime.now() + datetime.timedelta(days=-1) end = datetime.datetime.now() + datetime.timedelta(days=-1, hours=2) - create_events_and_user_events(session, start, end, 1, - ["email2@gmail.com", "email3@gmail.com"]) + create_events_and_user_events( + session, + start, + end, + 1, + ["email2@gmail.com", "email3@gmail.com"], + ) start = datetime.datetime.now() + datetime.timedelta(hours=1) end = datetime.datetime.now() + datetime.timedelta(hours=1.5) - create_events_and_user_events(session, start, end, 2, - ["email3@gmail.com"]) + create_events_and_user_events(session, start, end, 2, ["email3@gmail.com"]) for invitation in get_all_invitations(session): - accept(invitation, session) + invitation.accept(session) def test_statistics_invalid_date_range(session): diff --git a/tests/test_user.py b/tests/test_user.py index 213e7589..1a1ed0a7 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -1,9 +1,8 @@ from datetime import datetime import pytest -from app.routers.user import ( - create_user, does_user_exist, get_users -) +from app.routers.register import _create_user +from app.routers.user import does_user_exist, get_users from app.internal.user.availability import disable, enable from app.internal.utils import save from app.database.models import UserEvent, Event @@ -13,12 +12,14 @@ @pytest.fixture def user1(session): # a user made for testing who doesn't own any event. - user = create_user( + user = _create_user( session=session, - username='new_test_username', - password='new_test_password', - email='new2_test.email@gmail.com', - language_id='english' + username="new_test_username", + full_name="test_user", + password="new_test_password", + email="new2_test.email@gmail.com", + language_id="english", + description="", ) return user @@ -27,21 +28,23 @@ def user1(session): @pytest.fixture def user2(session): # a user made for testing who already owns an event. - user = create_user( + user = _create_user( session=session, - username='new_test_username2', - password='new_test_password2', - email='new_test_love231.email@gmail.com', - language_id='english' + username="new_test_username2", + full_name="test_user", + password="new_test_password2", + email="new_test_love231.email@gmail.com", + language_id="english", + description="", ) data = { - 'title': 'user2 event', - 'start': datetime.strptime('2021-05-05 14:59', '%Y-%m-%d %H:%M'), - 'end': datetime.strptime('2021-05-05 15:01', '%Y-%m-%d %H:%M'), - 'location': 'https://us02web.zoom.us/j/875384596', - 'content': 'content', - 'owner_id': user.id, + "title": "user2 event", + "start": datetime.strptime("2021-05-05 14:59", "%Y-%m-%d %H:%M"), + "end": datetime.strptime("2021-05-05 15:01", "%Y-%m-%d %H:%M"), + "location": "https://us02web.zoom.us/j/875384596", + "content": "content", + "owner_id": user.id, } create_event(session, **data) @@ -52,12 +55,12 @@ def user2(session): @pytest.fixture def event1(session, user2): data = { - 'title': 'test event title', - 'start': datetime.strptime('2021-05-05 14:59', '%Y-%m-%d %H:%M'), - 'end': datetime.strptime('2021-05-05 15:01', '%Y-%m-%d %H:%M'), - 'location': 'https://us02web.zoom.us/j/87538459r6', - 'content': 'content', - 'owner_id': user2.id, + "title": "test event title", + "start": datetime.strptime("2021-05-05 14:59", "%Y-%m-%d %H:%M"), + "end": datetime.strptime("2021-05-05 15:01", "%Y-%m-%d %H:%M"), + "location": "https://us02web.zoom.us/j/87538459r6", + "content": "content", + "owner_id": user2.id, } event = create_event(session, **data) @@ -68,41 +71,41 @@ def test_disabling_no_event_user(session, user1): # users without any future event can disable themselves disable(session, user1.id) assert user1.disabled - future_events = list(session.query(Event.id) - .join(UserEvent) - .filter( - UserEvent.user_id == user1.id, - Event.start > datetime - .now())) + future_events = list( + session.query(Event.id) + .join(UserEvent) + .filter(UserEvent.user_id == user1.id, Event.start > datetime.now()), + ) assert not future_events # making sure that after disabling the user he can be easily enabled. enable(session, user1.id) assert not user1.disabled -def test_disabling_user_participating_event( - session, user1, event1): +def test_disabling_user_participating_event(session, user1, event1): """making sure only users who only participate in events can disable and enable themselves.""" - association = UserEvent( - user_id=user1.id, - event_id=event1.id - ) + association = UserEvent(user_id=user1.id, event_id=event1.id) save(session, association) disable(session, user1.id) assert user1.disabled - future_events = list(session.query(Event.id) - .join(UserEvent) - .filter( - UserEvent.user_id == user1.id, - Event.start > datetime.now(), - Event.owner_id == user1.id)) + future_events = list( + session.query(Event.id) + .join(UserEvent) + .filter( + UserEvent.user_id == user1.id, + Event.start > datetime.now(), + Event.owner_id == user1.id, + ), + ) assert not future_events enable(session, user1.id) assert not user1.disabled - deleted_user_event_connection = session.query(UserEvent).filter( - UserEvent.user_id == user1.id, - UserEvent.event_id == event1.id).first() + deleted_user_event_connection = ( + session.query(UserEvent) + .filter(UserEvent.user_id == user1.id, UserEvent.event_id == event1.id) + .first() + ) session.delete(deleted_user_event_connection) @@ -113,18 +116,19 @@ def test_disabling_event_owning_user(session, user2): class TestUser: - def test_create_user(self, session): - user = create_user( + user = _create_user( session=session, - username='new_test_username', - password='new_test_password', - email='new_test.email@gmail.com', - language_id=1 + username="new_test_username", + password="new_test_password", + email="new_test.email@gmail.com", + language_id=1, + description="", + full_name="test_user", ) - assert user.username == 'new_test_username' - assert user.password == 'new_test_password' - assert user.email == 'new_test.email@gmail.com' + assert user.username == "new_test_username" + assert user.password == "new_test_password" + assert user.email == "new_test.email@gmail.com" assert user.language_id == 1 session.delete(user) session.commit() @@ -135,7 +139,7 @@ def test_get_users_success(self, user, session): assert get_users(email=user.email, session=session) == [user] def test_get_users_failure(self, session, user): - assert get_users(username='wrong username', session=session) == [] + assert get_users(username="wrong username", session=session) == [] assert get_users(wrong_param=user.username, session=session) == [] def test_does_user_exist_success(self, user, session): @@ -144,8 +148,8 @@ def test_does_user_exist_success(self, user, session): assert does_user_exist(email=user.email, session=session) def test_does_user_exist_failure(self, session): - assert not does_user_exist(username='wrong username', session=session) + assert not does_user_exist(username="wrong username", session=session) assert not does_user_exist(session=session) def test_repr(self, user): - assert user.__repr__() == f'' + assert user.__repr__() == f"" diff --git a/tests/utils.py b/tests/utils.py deleted file mode 100644 index 58ffdbd0..00000000 --- a/tests/utils.py +++ /dev/null @@ -1,13 +0,0 @@ -from sqlalchemy.orm import Session - - -def create_model(session: Session, model_class, **kw): - instance = model_class(**kw) - session.add(instance) - session.commit() - return instance - - -def delete_instance(session: Session, instance): - session.delete(instance) - session.commit()