From c9287b0ed6e1a0f54bf70ffdefdd28b61f7c516d Mon Sep 17 00:00:00 2001 From: Shir Bar Lev <51282497+shirblc@users.noreply.github.com> Date: Sun, 28 Apr 2024 16:57:42 +0300 Subject: [PATCH] change: Converted the SQLAlchemy-related code to use async/await. (#618) * Updated Flask and SQLAlchemy requirements to the async version. * Added the AsyncAttrs mixin to the models. * Added the async driver for Postgres. * Set the posts' user relationship to 'selectin' for the join. * Switched the roles' and permissions' relationship joins to 'selectin'. * Added async counterparts to all methods in the DB class. * Added missing deprecation warning. * Updated all endpoints to use the new async methods. * Installed Quart and Quart-cors to replace Flask (and Flask-cors). * Switched Auth to use async/await. * Replaced Flask and Flask-cors with Quart and Quart-cors. * Deleted most of the non-async methods. * Installed pytest-asyncio for tests. * Updated the tests to use the async versions of all functions. * Deleted the rest of the non-async functions in db. * Cleaned up some of the test fixtures. * Merged the two app fixtures. Now that we're not using the app context anymore, we don't really need separate fixtures for the app and the test_client. * Split the db setup fixtures. * Added missing role to test setup, * Replaced daatabase_url with async_database_url in the DB class setup. * Tweaked the fixtures a little more. * Changed asyncio_mode to auto in tests. * Changed the asyncio scope to session. * Updated the URL of the database in CI to include the driver. * Added a subscriptions task in tests. * Updated the way the database is reset in tests. * Updated the docstrings. * Deleted sh from the README as we no longer use it. * Disabled sending push notifications in tests. * Moved the mock to the function-scoped fixture. * Added some logging to see what the problem is. * Testing to see if it's the wrong location to patch. * Deleted the unneeded print. * Added a changelog entry. * Changed all db methods to return formatted objects. * Added missing parsing of dates to all dates coming from the front-end. * Cast all the IDs explicitly to integers. Seems that asyncpg isn't as good at handling str -> int as psycopg2... * Fixed the bug I caused in the GET users endpoint. * Added refreshes to the 'add' methods in the db. * Updated the db tests to the formatted return value. * Updated the date format in the dummy data to match the front-end. * Cast some more string IDs as integers... * Updated the error message in test_db to match the asyncpg syntax. * Updated the fixture in test_auth to the db fixture. * Fixed some issues with the fixtures setting up the database. * Added proepr logging to the db class. * Changed the name of the app. * Added a note to the README to include the driver in the URL. * Added missing engine.dispose to the suite setup. * Deleted the old changelog file. * Replaced gunicorn with hypercorn and removed psycopg as it's not needed anymore. * Changed the names of the db's attributes and methods back to their original names. They were originally named this way to avoid breaking any other methods that use the originals during the migration, but now that everything has been migrated and the old methods have been removed, the changed names are no longer needed. * Added a changelog entry. * Updated the date format in the API docs. * Fixed a 403 for user update. * Fixed the tests I broke... --- .circleci/config.yml | 4 +- API_DOCS.md | 10 +- README.md | 2 +- auth.py | 8 +- changelog/pr618.json | 37 +++++ create_app.py | 284 +++++++++++++++++++----------------- models/db.py | 201 +++++++++++++++---------- models/models.py | 11 +- requirements.txt | 7 +- setup.cfg | 2 +- tests/conftest.py | 86 ++++++----- tests/data_models.py | 154 ++++++++++--------- tests/test_app.py | 8 +- tests/test_auth.py | 14 +- tests/test_db.py | 158 ++++++++++++-------- tests/test_filters.py | 22 +-- tests/test_messages.py | 98 ++++++------- tests/test_notifications.py | 40 ++--- tests/test_posts.py | 60 ++++---- tests/test_reports.py | 44 +++--- tests/test_users.py | 91 ++++++------ 21 files changed, 750 insertions(+), 591 deletions(-) create mode 100644 changelog/pr618.json diff --git a/.circleci/config.yml b/.circleci/config.yml index 1dcaa2ce..43ab05f3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,7 +12,7 @@ jobs: - image: cimg/python:3.11 environment: FLASK_CONFIG: testing - TEST_DATABASE_URL: postgresql://postgres:password@localhost:5432/test_sah?sslmode=disable + TEST_DATABASE_URL: postgresql+asyncpg://postgres:password@localhost:5432/test_sah?sslmode=disable steps: - checkout - restore_cache: @@ -30,7 +30,7 @@ jobs: - image: cimg/python:3.11 environment: FLASK_CONFIG: testing - TEST_DATABASE_URL: postgresql://postgres:password@localhost:5432/test_sah?sslmode=disable + TEST_DATABASE_URL: postgresql+asyncpg://postgres:password@localhost:5432/test_sah?sslmode=disable - image: cimg/postgres:14.10 auth: username: mydockerhub-user diff --git a/API_DOCS.md b/API_DOCS.md index 9d013c5c..f8a76e4a 100644 --- a/API_DOCS.md +++ b/API_DOCS.md @@ -251,7 +251,7 @@ For full instructions check the [`backend README`](./backend/README.md) **Expected Errors**: - 500 (Internal Server Error) - In case there's an error adding the new post to the database. -**CURL Request Sample**: `curl -X POST http://127.0.0.1:5000/posts -H "Content-Type: application/json" -H 'Authorization: Bearer ' -d '{"user_id":4, "user":"user14", "text":"test curl", "date":"Wed Jun 10 2020 10:30:05 GMT+0300", "givenHugs":0}'` +**CURL Request Sample**: `curl -X POST http://127.0.0.1:5000/posts -H "Content-Type: application/json" -H 'Authorization: Bearer ' -d '{"user_id":4, "user":"user14", "text":"test curl", "date":"2020-06-07T15:57:45.901Z", "givenHugs":0}'` **Response Example:** ``` @@ -290,7 +290,7 @@ For full instructions check the [`backend README`](./backend/README.md) - 404 (Not Found) - In case there's no post with that ID. - 500 (Internal Server Error) - In case there's an error updating the post's data in the database. -**CURL Request Sample**: `curl -X PATCH http://127.0.0.1:5000/posts/15 -H "Content-Type: application/json" -H 'Authorization: Bearer ' -d '{"user_id":4, "user":"user14", "text":"test curl", "date":"Wed Jun 10 2020 10:30:05 GMT+0300", "givenHugs":0}'` +**CURL Request Sample**: `curl -X PATCH http://127.0.0.1:5000/posts/15 -H "Content-Type: application/json" -H 'Authorization: Bearer ' -d '{"user_id":4, "user":"user14", "text":"test curl", "date":"2020-06-07T15:57:45.901Z", "givenHugs":0}'` **Response Example:** ``` @@ -750,7 +750,7 @@ For full instructions check the [`backend README`](./backend/README.md) - 403 (Forbidden) - In case the user is trying to post the message from another user. - 500 (Internal Server Error) - In case there's an error adding the new message (or the new thread if one is needed) to the database. -**CURL Request Sample**: `curl -X POST http://127.0.0.1:5000/messages -H "Content-Type: application/json" -H 'Authorization: Bearer ' -d '{"from":"user14", "fromId":4, "forId":1, "messageText":"hang in there", "date":"Mon, 08 Jun 2020 14:43:15 GMT"}'` +**CURL Request Sample**: `curl -X POST http://127.0.0.1:5000/messages -H "Content-Type: application/json" -H 'Authorization: Bearer ' -d '{"from":"user14", "fromId":4, "forId":1, "messageText":"hang in there", "date":"2020-06-07T15:57:45.901Z"}'` **Response Example:** ``` @@ -894,7 +894,7 @@ For full instructions check the [`backend README`](./backend/README.md) - 404 (Not Found) - In case the item being reported doesn't exist. - 500 (Internal Server Error) - In case an error occurred while adding the new report to the database. -**CURL Request Sample**: `curl -X POST http://127.0.0.1:5000/reports -H "Content-Type: application/json" -H 'Authorization: Bearer ' -d '{"type":"Post", "userID":"1", "postID":4, "reporter":5,"reportReason":"this post is inappropriate", "date":"Tue Jun 23 2020 14:59:31 GMT+0300"}'` +**CURL Request Sample**: `curl -X POST http://127.0.0.1:5000/reports -H "Content-Type: application/json" -H 'Authorization: Bearer ' -d '{"type":"Post", "userID":"1", "postID":4, "reporter":5,"reportReason":"this post is inappropriate", "date":"2020-06-07T15:57:45.901Z"}'` **Response Example:** ``` @@ -942,7 +942,7 @@ For full instructions check the [`backend README`](./backend/README.md) - 404 (Not Found) - In case a report with that ID doesn't exist. - 500 (Internal Server Error) - In case an error occurred while updating the report in the database. -**CURL Request Sample**: `curl -X PATCH http://127.0.0.1:5000/reports/36 -H "Content-Type: application/json" -H 'Authorization: Bearer ' -d '{"type":"Post", "userID":"1", "postID":4, "reporter":5,"reportReason":"this post is inappropriate", "date":"Tue Jun 23 2020 14:59:31 GMT+0300", "dismissed": true, "closed": true}'` +**CURL Request Sample**: `curl -X PATCH http://127.0.0.1:5000/reports/36 -H "Content-Type: application/json" -H 'Authorization: Bearer ' -d '{"type":"Post", "userID":"1", "postID":4, "reporter":5,"reportReason":"this post is inappropriate", "date":"2020-06-07T15:57:45.901Z", "dismissed": true, "closed": true}'` **Response Example:** ``` diff --git a/README.md b/README.md index c8d3262e..684dbb85 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ The project is open source, so feel free to use parts of the code. However, the 5. Run ```pre-commit install``` to install and initialise pre-commit. 6. Create a database for the app. 7. Update the database URI to match your system. - - The database URI comes from an environment variable named **DATABASE_URL**. + - The database URI comes from an environment variable named **DATABASE_URL**. Make sure you include the driver in the URL (e.g., `postgresql+asyncpg` instead of `postgresql`), as otherwise SQLAlchemy assumes it should use the default driver, which (at least for postgres) doesn't support async/await. 8. Set Auth0 configuration variables: - AUTH0_DOMAIN - environment variable containing your Auth0 domain. - API_AUDIENCE - environment variable containing your Auth0 API audience. diff --git a/auth.py b/auth.py index a82b0a67..dc2545bc 100644 --- a/auth.py +++ b/auth.py @@ -250,13 +250,15 @@ def check_permissions_legacy(permission: list[str], payload: dict[str, Any]) -> return True -def get_current_user(payload: dict[str, Any], db: SendADatabase) -> dict[str, Any]: +async def get_current_user( + payload: dict[str, Any], db: SendADatabase +) -> dict[str, Any]: """ Fetches the details of the currently logged in user from the database. param payload: The payload from the decoded, verified JWT. """ - current_user: User | None = db.session.scalar( + current_user: User | None = await db.session.scalar( select(User).filter(User.auth0_id == payload["sub"]) ) @@ -323,7 +325,7 @@ async def wrapper(*args, **kwargs): returned_payload = payload check_permissions_legacy(permission, payload) else: - current_user = get_current_user(payload, db) + current_user = await get_current_user(payload, db) returned_payload = { "id": current_user["id"], "auth0Id": current_user["auth0Id"], diff --git a/changelog/pr618.json b/changelog/pr618.json new file mode 100644 index 00000000..589b6d83 --- /dev/null +++ b/changelog/pr618.json @@ -0,0 +1,37 @@ +{ + "pr_number": 618, + "changes": [ + { + "change": "Changes", + "description": "Updated all database interactions to use SQLAlchemy's asyncio extension. The engine, session maker and session have been replaced by their async variants, and the helper methods for interacting with the database have all been updated to use async/await. This means that we can now write asynchronous code for read, write, update and delete, which should allow the server itself to handle more requests at once." + }, + { + "change": "Changes", + "description": "The helper methods for creating and updating objects now return the formatted object instead of the raw SQLAlchemy object." + }, + { + "change": "Features", + "description": "Added a logger to the Database Handler class and added logging for all errors raised by the database." + }, + { + "change": "Chores", + "description": "Replaced psycopg2 with asyncpg as the driver used for database interactions, as psycopg2 doesn't support async/await." + }, + { + "change": "Changes", + "description": "Changed several relationships (user relationship in posts; role relationship in users; and permission relationship in roles) to use eager loading using 'selectin' instead of the default lazy loading. The SQLAlchemy asyncio extension doesn't support accessing attributes of lazily-loaded objects, and all three attributes are required to return the formatted versions of the models they're defined in (post, user and role respectively)." + }, + { + "change": "Fixes", + "description": "Dates are now explicitly parsed from strings passed into the API (using `strptime`) and strings that contain integers (e.g., IDs) are explicitly cast from strings to integers. This fixes an issue where inserts and queries errored due to the wrong type being used for dates/IDs." + }, + { + "change": "Changes", + "description": "Changed the name of the Quart app from the current file's name to `SendAHug` to better reflect the server's purpose." + }, + { + "change": "Fixes", + "description": "Fixed a bug where an attempt to update user details (such as login count) returned a 403 error saying 'you don't have permission to block users'. This happened due to a recent change in the front-end, which now sends the whole user object (of the logged in user) back to the back-end on update (instead of just the updated fields). As the back-end was checking whether the 'blocked' field existed, instead of whether it changed, this caused that error to be raised. Now, the back-end checks whether the value of 'blocked' changed instead." + } + ] +} diff --git a/create_app.py b/create_app.py index 6a59ad3a..da4452eb 100644 --- a/create_app.py +++ b/create_app.py @@ -60,9 +60,12 @@ from config import SAHConfig +DATETIME_PATTERN = "%Y-%m-%dT%H:%M:%S.%fZ" + + def create_app(config: SAHConfig) -> Quart: # create and configure the app - app = Quart(__name__) + app = Quart("SendAHug") config.db.init_app(app=app) config.db.set_default_per_page(per_page=5) # Utilities @@ -116,13 +119,14 @@ def after_request(response: Response): return response # Send push notification - def send_push_notification(user_id: int, data: RawPushData): + async def send_push_notification(user_id: int, data: RawPushData): vapid_key = os.environ.get("PRIVATE_KEY") notification_data = generate_push_data(data) vapid_claims = generate_vapid_claims() - subscriptions: Sequence[NotificationSub] = config.db.session.scalars( - select(NotificationSub).filter(NotificationSub.user == user_id) - ).all() + subscriptions_scalars = await config.db.session.scalars( + select(NotificationSub).filter(NotificationSub.user == int(user_id)) + ) + subscriptions: Sequence[NotificationSub] = subscriptions_scalars.all() # Try to send the push notification try: @@ -138,10 +142,10 @@ def send_push_notification(user_id: int, data: RawPushData): except WebPushException as e: app.logger.error(e) - def get_current_filters() -> list[str]: + async def get_current_filters() -> list[str]: """Fetches the current filters from the database.""" - filters: Sequence[Filter] = config.db.session.scalars(select(Filter)).all() - return [filter.filter for filter in filters] + filters = await config.db.session.scalars(select(Filter)) + return [filter.filter for filter in filters.all()] # Routes # ----------------------------------------------------------------- @@ -166,9 +170,8 @@ async def index(): else: posts_query = posts_query.order_by(Post.given_hugs, Post.date) - post_instances: Sequence[Post] = config.db.session.scalars( - posts_query.limit(10) - ).all() + posts_scalars = await config.db.session.scalars(posts_query.limit(10)) + post_instances: Sequence[Post] = posts_scalars.all() # formats each post in the list posts[target] = [post.format() for post in post_instances] @@ -196,11 +199,12 @@ async def search(): validator.check_type(search_query, "Search query") # Get the users with the search query in their display name - users: Sequence[User] = config.db.session.scalars( + users_scalars = await config.db.session.scalars( select(User).filter(User.display_name.ilike(f"%{search_query}%")) - ).all() + ) + users: Sequence[User] = users_scalars.all() - posts = config.db.paginate( + posts = await config.db.paginate( select(Post) .order_by(desc(Post.date)) .filter(Post.text.ilike(f"%{search_query}%")) @@ -234,20 +238,20 @@ async def add_post(token_payload: UserData): validator.validate_post_or_message( text=new_post_data["text"], type="post", - filtered_words=get_current_filters(), + filtered_words=await get_current_filters(), ) # Create a new post object new_post = Post( - user_id=new_post_data["userId"], + user_id=int(new_post_data["userId"]), text=new_post_data["text"], - date=new_post_data["date"], + date=datetime.strptime(new_post_data["date"], DATETIME_PATTERN), given_hugs=new_post_data["givenHugs"], sent_hugs=[], ) # Try to add the post to the database - added_post = config.db.add_object(new_post).format() + added_post = await config.db.add_object(new_post) return jsonify({"success": True, "posts": added_post}) @@ -263,8 +267,8 @@ async def edit_post(token_payload: UserData, post_id: int): validator.check_type(post_id, "Post ID") updated_post = json.loads(await request.data) - original_post: Post = config.db.one_or_404( - item_id=post_id, + original_post: Post = await config.db.one_or_404( + item_id=int(post_id), item_type=Post, ) @@ -291,14 +295,14 @@ async def edit_post(token_payload: UserData, post_id: int): validator.validate_post_or_message( text=updated_post["text"], type="post", - filtered_words=get_current_filters(), + filtered_words=await get_current_filters(), ) original_post.text = updated_post["text"] # Try to update the database - updated = config.db.update_object(obj=original_post) + updated = await config.db.update_object(obj=original_post) - return jsonify({"success": True, "updated": updated.format()}) + return jsonify({"success": True, "updated": updated}) # Endpoint: POST /posts//hugs # Description: Sends a hug to a specific user. @@ -310,19 +314,19 @@ async def send_hug_for_post(token_payload: UserData, post_id: int): # Check if the post ID isn't an integer; if it isn't, abort validator.check_type(post_id, "Post ID") - original_post: Post = config.db.one_or_404( + original_post: Post = await config.db.one_or_404( item_id=int(post_id), item_type=Post, ) # Gets the current user so we can update their 'sent hugs' value - current_user: User = config.db.one_or_404( + current_user: User = await config.db.one_or_404( item_id=token_payload["id"], item_type=User, ) hugs = original_post.sent_hugs or [] - post_author: User | None = config.db.session.scalar( + post_author: User | None = await config.db.session.scalar( select(User).filter(User.id == original_post.user_id) ) notification: Notification | None = None @@ -363,12 +367,12 @@ async def send_hug_for_post(token_payload: UserData, post_id: int): ] if notification: - config.db.add_object(notification) + await config.db.add_object(notification) - config.db.update_multiple_objects(objects=to_update) + await config.db.update_multiple_objects(objects=to_update) if post_author and push_notification: - send_push_notification(user_id=post_author.id, data=push_notification) + await send_push_notification(user_id=post_author.id, data=push_notification) return jsonify( { @@ -388,8 +392,8 @@ async def delete_post(token_payload: UserData, post_id: int): validator.check_type(post_id, "Post ID") # Gets the post to delete - post_data: Post = config.db.one_or_404( - item_id=post_id, + post_data: Post = await config.db.one_or_404( + item_id=int(post_id), item_type=Post, ) @@ -410,7 +414,7 @@ async def delete_post(token_payload: UserData, post_id: int): # Otherwise, it's either their post or they're allowed to delete any # post. # Try to delete the post - config.db.delete_object(post_data) + await config.db.delete_object(post_data) return jsonify({"success": True, "deleted": int(post_id)}) @@ -429,7 +433,7 @@ async def get_new_posts(type: Literal["new", "suggested"]): else: full_posts_query = full_posts_query.order_by(Post.given_hugs, Post.date) - paginated_posts = config.db.paginate(full_posts_query, current_page=page) + paginated_posts = await config.db.paginate(full_posts_query, current_page=page) return jsonify( { @@ -452,9 +456,10 @@ async def get_users_by_type(token_payload: UserData, type: str): if type.lower() == "blocked": # Check which users need to be unblocked current_date = datetime.now() - users_to_unblock: Sequence[User] = config.db.session.scalars( + user_scalars = await config.db.session.scalars( select(User).filter(User.release_date < current_date) - ).all() + ) + users_to_unblock: Sequence[User] = user_scalars.all() to_unblock = [] for user in users_to_unblock: @@ -464,10 +469,10 @@ async def get_users_by_type(token_payload: UserData, type: str): to_unblock.append(user) # Try to update the database - config.db.update_multiple_objects(objects=to_unblock) + await config.db.update_multiple_objects(objects=to_unblock) # Get all blocked users - paginated_users = config.db.paginate( + paginated_users = await config.db.paginate( select(User).filter(User.blocked == true()).order_by(User.release_date), current_page=page, ) @@ -494,12 +499,12 @@ async def get_user_data(token_payload: UserData, user_id: int | str): # regular ID, so try to find the user with that ID try: int(user_id) - user_data = config.db.session.scalar( + user_data = await config.db.session.scalar( select(User).filter(User.id == int(user_id)) ) # Otherwise, it's an Auth0 ID except ValueError: - user_data = config.db.session.scalar( + user_data = await config.db.session.scalar( select(User).filter(User.auth0_id == user_id) ) @@ -507,6 +512,8 @@ async def get_user_data(token_payload: UserData, user_id: int | str): if user_data is None: abort(404) + formatted_user = user_data.format() + # If the user is currently blocked, compare their release date to # the current date and time. if user_data.release_date: @@ -518,13 +525,9 @@ async def get_user_data(token_payload: UserData, user_id: int | str): user_data.role_id = 3 # regular user # Try to update the database - user_data = config.db.update_object(user_data) - - formatted_user_data = user_data.format() + formatted_user = await config.db.update_object(user_data) - print(jsonify({"success": True, "user": formatted_user_data})) - - return jsonify({"success": True, "user": formatted_user_data}) + return jsonify({"success": True, "user": formatted_user}) # Endpoint: POST /users # Description: Adds a new user to the users table. @@ -543,7 +546,7 @@ async def add_user(token_payload): # Checks whether a user with that Auth0 ID already exists # If it is, aborts - database_user: User | None = config.db.session.scalar( + database_user: User | None = await config.db.session.scalar( select(User).filter(User.auth0_id == user_data["id"]) ) @@ -567,7 +570,7 @@ async def add_user(token_payload): ) # Try to add the user to the database - added_user = config.db.add_object(new_user).format() + added_user = await config.db.add_object(new_user) return jsonify({"success": True, "user": added_user}) @@ -582,8 +585,8 @@ async def edit_user(token_payload: UserData, user_id: int): validator.check_type(user_id, "User ID") updated_user = json.loads(await request.data) - user_to_update: User = config.db.one_or_404( - item_id=user_id, + user_to_update: User = await config.db.one_or_404( + item_id=int(user_id), item_type=User, ) @@ -619,8 +622,11 @@ async def edit_user(token_payload: UserData, user_id: int): user_to_update.display_name = updated_user["displayName"] - # If the request was in done in order to block or unlock a user - if "blocked" in updated_user: + # If the request was in done in order to block or unblock a user + if ( + "blocked" in updated_user + and updated_user["blocked"] != user_to_update.blocked + ): # If the user doesn't have permission to block/unblock a user if "block:user" not in token_payload["role"]["permissions"]: raise AuthError( @@ -678,9 +684,9 @@ async def edit_user(token_payload: UserData, user_id: int): user_to_update.icon_colours = json.dumps(updated_user["iconColours"]) # Try to update it in the database - updated = config.db.update_object(obj=user_to_update) + updated = await config.db.update_object(obj=user_to_update) - return jsonify({"success": True, "updated": updated.format()}) + return jsonify({"success": True, "updated": updated}) # Endpoint: GET /users/all//posts # Description: Gets a specific user's posts. @@ -688,7 +694,7 @@ async def edit_user(token_payload: UserData, user_id: int): # Authorization: read:user. @app.route("/users/all//posts") @requires_auth(config.db, ["read:user"]) - async def get_user_posts(token_payload: UserData, user_id): + async def get_user_posts(token_payload: UserData, user_id: int): page = request.args.get("page", 1, type=int) # if there's no user ID provided, abort with 'Bad Request' @@ -698,8 +704,8 @@ async def get_user_posts(token_payload: UserData, user_id): validator.check_type(user_id, "User ID") # Gets all posts written by the given user - user_posts = config.db.paginate( - select(Post).filter(Post.user_id == user_id).order_by(Post.date), + user_posts = await config.db.paginate( + select(Post).filter(Post.user_id == int(user_id)).order_by(Post.date), current_page=page, ) @@ -740,8 +746,8 @@ async def delete_user_posts(token_payload: UserData, user_id: int): # Otherwise, the user is either trying to delete their own posts or # they're allowed to delete others' posts, so let them continue - post_count: int | None = config.db.session.scalar( - select(func.count(Post.id)).filter(Post.user_id == user_id) + post_count: int | None = await config.db.session.scalar( + select(func.count(Post.id)).filter(Post.user_id == int(user_id)) ) # If the user has no posts, abort @@ -749,8 +755,8 @@ async def delete_user_posts(token_payload: UserData, user_id: int): abort(404) # Try to delete - config.db.delete_multiple_objects( - delete_stmt=delete(Post).where(Post.user_id == user_id) + await config.db.delete_multiple_objects( + delete_stmt=delete(Post).where(Post.user_id == int(user_id)) ) return jsonify({"success": True, "userID": int(user_id), "deleted": post_count}) @@ -763,12 +769,12 @@ async def delete_user_posts(token_payload: UserData, user_id: int): @requires_auth(config.db, ["read:user"]) async def send_hug_to_user(token_payload: UserData, user_id: int): validator.check_type(user_id, "User ID") - user_to_hug: User = config.db.one_or_404( - item_id=user_id, + user_to_hug: User = await config.db.one_or_404( + item_id=int(user_id), item_type=User, ) # Fetch the current user to update their 'given hugs' value - current_user: User = config.db.one_or_404( + current_user: User = await config.db.one_or_404( item_id=token_payload["id"], item_type=User ) @@ -790,9 +796,9 @@ async def send_hug_to_user(token_payload: UserData, user_id: int): # Try to update it in the database to_update = [user_to_hug, current_user] - config.db.add_object(obj=notification) - config.db.update_multiple_objects(objects=to_update) - send_push_notification(user_id=user_to_hug.id, data=push_notification) + await config.db.add_object(obj=notification) + await config.db.update_multiple_objects(objects=to_update) + await send_push_notification(user_id=user_to_hug.id, data=push_notification) return jsonify( { @@ -845,7 +851,7 @@ async def get_user_messages(token_payload: UserData): ).filter(Message.from_id == user_id) # Gets a specific thread's messages else: - message = config.db.session.scalar( + message = await config.db.session.scalar( select(Thread).filter(Thread.id == thread_id) ) # Check if there's a thread with that ID at all @@ -871,7 +877,7 @@ async def get_user_messages(token_payload: UserData): | ((Message.from_id == user_id) & (Message.from_deleted == false())) ).filter(Message.thread == thread_id) - messages = config.db.paginate( + messages = await config.db.paginate( messages_query.order_by(desc(Message.date)), current_page=page, ) @@ -883,7 +889,7 @@ async def get_user_messages(token_payload: UserData): # For threads, gets all threads' data else: # Get the thread ID, and users' names and IDs - threads_messages = config.db.paginate( + threads_messages = await config.db.paginate( select(Thread) .filter( or_( @@ -938,21 +944,21 @@ async def add_message(token_payload: UserData): validator.validate_post_or_message( text=message_data["messageText"], type="message", - filtered_words=get_current_filters(), + filtered_words=await get_current_filters(), ) # Checks if there's an existing thread between the users (with user 1 # being the sender and user 2 being the recipient) - thread: Thread | None = config.db.session.scalar( + thread: Thread | None = await config.db.session.scalar( select(Thread).filter( or_( and_( - Thread.user_1_id == message_data["fromId"], - Thread.user_2_id == message_data["forId"], + Thread.user_1_id == int(message_data["fromId"]), + Thread.user_2_id == int(message_data["forId"]), ), and_( - Thread.user_1_id == message_data["forId"], - Thread.user_2_id == message_data["fromId"], + Thread.user_1_id == int(message_data["forId"]), + Thread.user_2_id == int(message_data["fromId"]), ), ) ) @@ -961,11 +967,12 @@ async def add_message(token_payload: UserData): # If there's no thread between the users if thread is None: new_thread = Thread( - user_1_id=message_data["fromId"], user_2_id=message_data["forId"] + user_1_id=int(message_data["fromId"]), + user_2_id=int(message_data["forId"]), ) # Try to create the new thread - added_thread = config.db.add_object(new_thread) - thread_id = added_thread.id + added_thread = await config.db.add_object(new_thread) + thread_id = added_thread["id"] # If there's a thread between the users else: thread_id = thread.id @@ -975,24 +982,24 @@ async def add_message(token_payload: UserData): thread.user_1_deleted = False thread.user_2_deleted = False # Update the thread in the database - config.db.update_object(obj=thread) + await config.db.update_object(obj=thread) # Create a new message new_message = Message( - from_id=message_data["fromId"], - for_id=message_data["forId"], + from_id=int(message_data["fromId"]), + for_id=int(message_data["forId"]), text=message_data["messageText"], - date=message_data["date"], + date=datetime.strptime(message_data["date"], DATETIME_PATTERN), thread=thread_id, ) # Create a notification for the user getting the message notification = Notification( - for_id=message_data["forId"], - from_id=message_data["fromId"], + for_id=int(message_data["forId"]), + from_id=int(message_data["fromId"]), type="message", text="You have a new message", - date=message_data["date"], + date=datetime.strptime(message_data["date"], DATETIME_PATTERN), ) push_notification: RawPushData = { "type": "message", @@ -1001,9 +1008,11 @@ async def add_message(token_payload: UserData): notification_for = message_data["forId"] # Try to add the message to the database - added = config.db.add_multiple_objects(objects=[new_message, notification]) + added = await config.db.add_multiple_objects( + objects=[new_message, notification] + ) sent_message = [item for item in added if "threadID" in item.keys()] - send_push_notification(user_id=notification_for, data=push_notification) + await send_push_notification(user_id=notification_for, data=push_notification) return jsonify({"success": True, "message": sent_message[0]}) @@ -1029,14 +1038,14 @@ async def delete_thread( # TODO: This should be renamed to delete_message # If the mailbox type is inbox or outbox, search for a message # with that ID if mailbox_type in ["inbox", "outbox", "thread"]: - delete_item = config.db.one_or_404( - item_id=item_id, + delete_item = await config.db.one_or_404( + item_id=int(item_id), item_type=Message, ) # If the mailbox type is threads, search for a thread with that ID elif mailbox_type == "threads": - delete_item = config.db.one_or_404( - item_id=item_id, + delete_item = await config.db.one_or_404( + item_id=int(item_id), item_type=Thread, ) else: @@ -1109,7 +1118,7 @@ async def delete_thread( # TODO: This should be renamed to delete_message # If both users deleted the message/thread, delete it from # the database entirely if delete_message: - config.db.delete_object(delete_item) + await config.db.delete_object(delete_item) # Otherwise, just update the appropriate deleted property else: if type(delete_item) == Thread: @@ -1156,14 +1165,14 @@ async def delete_thread( # TODO: This should be renamed to delete_message ) ) - config.db.update_object(obj=delete_item) - config.db.update_multiple_objects_with_dml( + await config.db.update_object(obj=delete_item) + await config.db.update_multiple_objects_with_dml( update_stmts=[from_stmt, for_stmt] ) - config.db.delete_multiple_objects(delete_stmt=delete_stmt) + await config.db.delete_multiple_objects(delete_stmt=delete_stmt) else: - config.db.update_object(delete_item) + await config.db.update_object(delete_item) return jsonify({"success": True, "deleted": int(item_id)}) @@ -1198,7 +1207,7 @@ async def clear_mailbox( # If the user is trying to clear their inbox if mailbox_type == "inbox": - num_messages = config.db.session.scalar( + num_messages = await config.db.session.scalar( select(func.count(Message.id)).filter(Message.for_id == user_id) ) # If there are no messages, abort @@ -1223,11 +1232,11 @@ async def clear_mailbox( ) # config.db.delete_multiple_objects(delete_stmt=delete_stmt) - config.db.update_multiple_objects_with_dml(update_stmts=update_stmt) + await config.db.update_multiple_objects_with_dml(update_stmts=update_stmt) # If the user is trying to clear their outbox if mailbox_type == "outbox": - num_messages = config.db.session.scalar( + num_messages = await config.db.session.scalar( select(func.count(Message.id)).filter(Message.from_id == user_id) ) # If there are no messages, abort @@ -1251,12 +1260,12 @@ async def clear_mailbox( .values(from_deleted=true()) ) - config.db.delete_multiple_objects(delete_stmt=delete_stmt) - config.db.update_multiple_objects_with_dml(update_stmts=update_stmt) + await config.db.delete_multiple_objects(delete_stmt=delete_stmt) + await config.db.update_multiple_objects_with_dml(update_stmts=update_stmt) # If the user is trying to clear their threads mailbox if mailbox_type == "threads": - num_messages = config.db.session.scalar( + num_messages = await config.db.session.scalar( select(func.count(Thread.id)).filter( or_( and_( @@ -1343,9 +1352,9 @@ async def clear_mailbox( ) ) - config.db.delete_multiple_objects(delete_stmt=delete_messages_stmt) - config.db.delete_multiple_objects(delete_stmt=delete_threads_stmt) - config.db.update_multiple_objects_with_dml(update_stmts=update_stmts) + await config.db.delete_multiple_objects(delete_stmt=delete_messages_stmt) + await config.db.delete_multiple_objects(delete_stmt=delete_threads_stmt) + await config.db.update_multiple_objects_with_dml(update_stmts=update_stmts) return jsonify( {"success": True, "userID": int(user_id), "deleted": num_messages} @@ -1371,7 +1380,7 @@ async def get_open_reports(token_payload: UserData): for report_type in reports.keys(): reports_page = request.args.get(f"{report_type.lower()}Page", 1, type=int) - paginated_reports = config.db.paginate( + paginated_reports = await config.db.paginate( select(Report) .filter(Report.closed == false()) .filter(Report.type == report_type) @@ -1412,17 +1421,17 @@ async def create_new_report(token_payload: UserData): abort(422) # Get the post. If this post doesn't exist, abort - reported_item: Post | User = config.db.one_or_404( + reported_item: Post | User = await config.db.one_or_404( item_id=report_data["postID"], item_type=Post, ) report = Report( type=report_data["type"], - date=report_data["date"], - user_id=report_data["userID"], - post_id=report_data["postID"], - reporter=report_data["reporter"], + date=datetime.strptime(report_data["date"], DATETIME_PATTERN), + user_id=int(report_data["userID"]), + post_id=int(report_data["postID"]), + reporter=int(report_data["reporter"]), report_reason=report_data["reportReason"], dismissed=False, closed=False, @@ -1436,16 +1445,16 @@ async def create_new_report(token_payload: UserData): abort(422) # Get the user. If this user doesn't exist, abort - reported_item = config.db.one_or_404( + reported_item = await config.db.one_or_404( item_id=report_data["userID"], item_type=User, ) report = Report( type=report_data["type"], - date=report_data["date"], - user_id=report_data["userID"], - reporter=report_data["reporter"], + date=datetime.strptime(report_data["date"], DATETIME_PATTERN), + user_id=int(report_data["userID"]), + reporter=int(report_data["reporter"]), report_reason=report_data["reportReason"], dismissed=False, closed=False, @@ -1454,10 +1463,10 @@ async def create_new_report(token_payload: UserData): reported_item.open_report = True # Try to add the report to the database - added_report = config.db.add_object(obj=report) - config.db.update_object(obj=reported_item) + added_report = await config.db.add_object(obj=report) + await config.db.update_object(obj=reported_item) - return jsonify({"success": True, "report": added_report.format()}) + return jsonify({"success": True, "report": added_report}) # Endpoint: PATCH /reports/ # Description: Update the status of the report with the given ID. @@ -1467,8 +1476,8 @@ async def create_new_report(token_payload: UserData): @requires_auth(config.db, ["read:admin-board"]) async def update_report_status(token_payload: UserData, report_id: int): updated_report = json.loads(await request.data) - report: Report | None = config.db.session.scalar( - select(Report).filter(Report.id == report_id) + report: Report | None = await config.db.session.scalar( + select(Report).filter(Report.id == int(report_id)) ) # If there's no report with that ID, abort @@ -1482,7 +1491,7 @@ async def update_report_status(token_payload: UserData, report_id: int): if not updated_report.get("userID", None): abort(422) - reported_item = config.db.session.scalar( + reported_item = await config.db.session.scalar( select(User).filter(User.id == updated_report["userID"]) ) # If the item reported is a post @@ -1490,7 +1499,7 @@ async def update_report_status(token_payload: UserData, report_id: int): if not updated_report.get("postID", None): abort(422) - reported_item = config.db.session.scalar( + reported_item = await config.db.session.scalar( select(Post).filter(Post.id == updated_report["postID"]) ) @@ -1506,7 +1515,7 @@ async def update_report_status(token_payload: UserData, report_id: int): to_update.append(reported_item) # Try to update the report in the database - updated = config.db.update_multiple_objects(objects=to_update) + updated = await config.db.update_multiple_objects(objects=to_update) return_report = [item for item in updated if "reporter" in item.keys()] return jsonify({"success": True, "updated": return_report[0]}) @@ -1520,7 +1529,7 @@ async def update_report_status(token_payload: UserData, report_id: int): async def get_filters(token_payload: UserData): page = request.args.get("page", 1, type=int) words_per_page = 10 - filtered_words = config.db.paginate( + filtered_words = await config.db.paginate( select(Filter).order_by(Filter.id), current_page=page, per_page=words_per_page, @@ -1547,7 +1556,7 @@ async def add_filter(token_payload: UserData): validator.check_length(new_filter, "Phrase to filter") # If the word already exists in the filters list, abort - existing_filter: Filter | None = config.db.session.scalar( + existing_filter: Filter | None = await config.db.session.scalar( select(Filter).filter(Filter.filter == new_filter.lower()) ) @@ -1556,7 +1565,7 @@ async def add_filter(token_payload: UserData): # Try to add the word to the filters list filter = Filter(filter=new_filter.lower()) - added = config.db.add_object(filter).format() + added = await config.db.add_object(filter) return jsonify({"success": True, "added": added}) @@ -1570,14 +1579,14 @@ async def delete_filter(token_payload: UserData, filter_id: int): validator.check_type(filter_id, "Filter ID") # If there's no word in that index - to_delete: Filter = config.db.one_or_404( - item_id=filter_id, + to_delete: Filter = await config.db.one_or_404( + item_id=int(filter_id), item_type=Filter, ) # Otherwise, try to delete it removed = to_delete.format() - config.db.delete_object(to_delete) + await config.db.delete_object(to_delete) return jsonify({"success": True, "deleted": removed}) @@ -1589,7 +1598,7 @@ async def delete_filter(token_payload: UserData, filter_id: int): @requires_auth(config.db, ["read:messages"]) async def get_latest_notifications(token_payload: UserData): silent_refresh = request.args.get("silentRefresh", True) - user: User = config.db.one_or_404( + user: User = await config.db.one_or_404( item_id=token_payload["id"], item_type=User, ) @@ -1603,12 +1612,13 @@ async def get_latest_notifications(token_payload: UserData): last_read = datetime(2020, 7, 1, 12, 00) # Gets all new notifications - notifications: Sequence[Notification] = config.db.session.scalars( + notifications_scalars = await config.db.session.scalars( select(Notification) .filter(Notification.for_id == user_id) .filter(Notification.date > last_read) .order_by(Notification.date) - ).all() + ) + notifications: Sequence[Notification] = notifications_scalars.all() formatted_notifications = [ notification.format() for notification in notifications @@ -1620,7 +1630,7 @@ async def get_latest_notifications(token_payload: UserData): if silent_refresh == "false": # Update the user's last-read date user.last_notifications_read = datetime.now() - config.db.update_object(obj=user) + await config.db.update_object(obj=user) return jsonify({"success": True, "notifications": formatted_notifications}) @@ -1652,7 +1662,7 @@ async def add_notification_subscription(token_payload: UserData): # Try to add it to the database subscribed = token_payload["displayName"] - sub = config.db.add_object(subscription).format() + sub = await config.db.add_object(subscription) return { "success": True, @@ -1678,8 +1688,8 @@ async def update_notification_subscription(token_payload: UserData, sub_id: int) subscription_json = request_data.decode("utf8").replace("'", '"') subscription_data = json.loads(subscription_json) - old_sub: NotificationSub = config.db.one_or_404( - item_id=sub_id, item_type=NotificationSub + old_sub: NotificationSub = await config.db.one_or_404( + item_id=int(sub_id), item_type=NotificationSub ) old_sub.endpoint = subscription_data["endpoint"] @@ -1688,7 +1698,7 @@ async def update_notification_subscription(token_payload: UserData, sub_id: int) # Try to add it to the database subscribed = token_payload["displayName"] subId = old_sub.id - config.db.update_object(obj=old_sub) + await config.db.update_object(obj=old_sub) return {"success": True, "subscribed": subscribed, "subId": subId} diff --git a/models/db.py b/models/db.py index ba25010a..dfb2c16c 100644 --- a/models/db.py +++ b/models/db.py @@ -25,20 +25,30 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +from asyncio import current_task from dataclasses import dataclass import math from typing import Protocol, Sequence, Type, TypeVar, cast, overload +import logging from quart import Quart, abort -from sqlalchemy import Delete, Engine, Update, create_engine, Select, func, select -from sqlalchemy.orm import sessionmaker, Session, scoped_session, Mapped +from sqlalchemy import Delete, Update, Select, func, select +from sqlalchemy.orm import Mapped from sqlalchemy.exc import DataError, IntegrityError +from sqlalchemy.ext.asyncio import ( + async_sessionmaker, + create_async_engine, + AsyncEngine, + async_scoped_session, + AsyncSession, +) from werkzeug.exceptions import HTTPException from .models import BaseModel, HugModelType, DumpedModel T = TypeVar("T", bound=BaseModel) +LOGGER = logging.getLogger("SendAHug") class CoreSAHModel(Protocol[HugModelType]): @@ -67,8 +77,8 @@ class SendADatabase: """ database_url: str - engine: Engine - session_factory: sessionmaker[Session] + engine: AsyncEngine + session_factory: async_sessionmaker[AsyncSession] def __init__( self, @@ -81,11 +91,12 @@ def __init__( param default_per_page: A default per_page value for the pagination method. param db_url: The URL of the database. """ - self.default_per_page = default_per_page + # Temporary second variable self.database_url = database_url - self.engine = create_engine(self.database_url) + self.default_per_page = default_per_page + self.engine = create_async_engine(self.database_url) self._create_session_factory() - self.session = self.create_scoped_session() + self.session = self.create_session() def init_app(self, app: Quart) -> None: """ @@ -97,31 +108,29 @@ def init_app(self, app: Quart) -> None: app.config["SQLALCHEMY_DATABASE_URI"] = self.database_url app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False self.app = app - self.app.teardown_appcontext(self._remove_session) def _create_session_factory(self): """ - Creates the session factory to be used to generate scoped sessions. + Creates the async session factory to be used to generate scoped sessions. """ - session_factory = sessionmaker( - bind=self.engine, - class_=Session, - ) + session_factory = async_sessionmaker(self.engine, expire_on_commit=False) self.session_factory = session_factory - def create_scoped_session(self): + # TODO: Do we want to continue with this pattern? According to + # https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#using-asyncio-scoped-session + # it's not really recommended anymore. + def create_session(self): """ - Creates a new scoped session. + Creates a new async scoped session. """ - return scoped_session(session_factory=self.session_factory) + return async_scoped_session( + session_factory=self.session_factory, scopefunc=current_task + ) - def _remove_session(self, exception: BaseException | None): - """ - Removes the sesion once the app context is torn down. - Copied from Flask-SQLAlchemy. - """ - self.session.remove() + async def _remove_session(self, exception: BaseException | None): + """ """ + await self.session.remove() def set_default_per_page(self, per_page: int): """ @@ -131,7 +140,7 @@ def set_default_per_page(self, per_page: int): # READ # ----------------------------------------------------------------- - def paginate( + async def paginate( self, query: Select, # TODO: This select needs to be Select[tuple[CoreSAHModel]] current_page: int, @@ -145,38 +154,43 @@ def paginate( param current_page: The current page to fetch the items for. param per_page: The amount of items to include in each page. """ - session = self.create_scoped_session() + LOGGER.debug(f"Fetching items for page {current_page}") if per_page is None: per_page = self.default_per_page - with session() as sess: - try: - items = sess.scalars( - query.limit(per_page).offset((current_page - 1) * per_page) - ).all() - total_items = ( - sess.scalar(select(func.count()).select_from(query.cte())) or 0 + try: + items_scalars = await self.session.scalars( + query.limit(per_page).offset((current_page - 1) * per_page) + ) + items = items_scalars.all() + total_items = ( + await ( + self.session.scalar(select(func.count()).select_from(query.cte())) ) + or 0 + ) - return PaginationResult( - resource=[item.format() for item in list(items)], - current_page=current_page, - per_page=per_page, - total_items=total_items, - total_pages=math.ceil(total_items / per_page), - ) + return PaginationResult( + resource=[item.format() for item in list(items)], + current_page=current_page, + per_page=per_page, + total_items=total_items, + total_pages=math.ceil(total_items / per_page), + ) - except Exception as err: - abort(500, str(err)) + except Exception as err: + LOGGER.error(str(err)) + abort(500, str(err)) - def one_or_404(self, item_id: int, item_type: Type[T]) -> T: + async def one_or_404(self, item_id: int, item_type: Type[T]) -> T: """ Fetch a single item or return 404 if it doesn't exist. Inspired by Flask-SQLAlchemy 3's `get_or_404` method. """ + LOGGER.debug(f"Fetching item {item_id}") try: - item = self.session.get(item_type, item_id) + item = await self.session.get(item_type, item_id) if item is None: abort(404) @@ -184,14 +198,16 @@ def one_or_404(self, item_id: int, item_type: Type[T]) -> T: return item except HTTPException as exc: + LOGGER.error(str(exc)) raise exc except Exception as err: + LOGGER.error(str(err)) abort(422, str(err)) # CREATE # ----------------------------------------------------------------- - def add_object(self, obj: CoreSAHModel) -> CoreSAHModel: + async def add_object(self, obj: CoreSAHModel) -> DumpedModel: """ Inserts a new record into the database. @@ -200,43 +216,56 @@ def add_object(self, obj: CoreSAHModel) -> CoreSAHModel: # Try to add the object to the database try: self.session.add(obj) - self.session.commit() + await self.session.commit() + await self.session.refresh(obj) - return obj + return obj.format() # If there's a database error except (DataError, IntegrityError) as err: - self.session.rollback() + await self.session.rollback() + LOGGER.error(str(err)) abort(422, str(err.orig)) # If there's an error, rollback except Exception as err: - self.session.rollback() + await self.session.rollback() + LOGGER.error(str(err)) abort(500, str(err)) # Bulk add - def add_multiple_objects(self, objects: list[CoreSAHModel]) -> list[DumpedModel]: + async def add_multiple_objects( + self, objects: list[CoreSAHModel] + ) -> list[DumpedModel]: """ Inserts multiple records into the database. param objects: The list of objects to add to the database. """ + formatted_objects: list[DumpedModel] = [] + # Try to add the objects to the database try: self.session.add_all(objects) - self.session.commit() + await self.session.commit() + + for object in objects: + await self.session.refresh(object) + formatted_objects.append(object.format()) - return [item.format() for item in objects] + return formatted_objects # If there's a database error except (DataError, IntegrityError) as err: - self.session.rollback() + await self.session.rollback() + LOGGER.error(str(err)) abort(422, str(err.orig)) # If there's an error, rollback except Exception as err: - self.session.rollback() + await self.session.rollback() + LOGGER.error(str(err)) abort(500, str(err)) # UPDATE # ----------------------------------------------------------------- - def update_object(self, obj: CoreSAHModel) -> CoreSAHModel: + async def update_object(self, obj: CoreSAHModel) -> DumpedModel: """ Updates an existing record. @@ -244,21 +273,23 @@ def update_object(self, obj: CoreSAHModel) -> CoreSAHModel: """ # Try to update the object in the database try: - self.session.commit() - self.session.refresh(obj) + await self.session.commit() + await self.session.refresh(obj) - return obj + return obj.format() # If there's a database error except (DataError, IntegrityError) as err: - self.session.rollback() + await self.session.rollback() + LOGGER.error(str(err)) abort(422, str(err.orig)) # If there's an error, rollback except Exception as err: - self.session.rollback() + await self.session.rollback() + LOGGER.error(str(err)) abort(500, str(err)) # Bulk Update - def update_multiple_objects( + async def update_multiple_objects( self, objects: Sequence[CoreSAHModel] ) -> list[DumpedModel]: """ @@ -270,31 +301,33 @@ def update_multiple_objects( # Try to update the objects in the database try: - self.session.commit() + await self.session.commit() for obj in objects: - self.session.refresh(obj) + await self.session.refresh(obj) updated_objects.append(obj.format()) return updated_objects # If there's a database error except (DataError, IntegrityError) as err: - self.session.rollback() + await self.session.rollback() + LOGGER.error(str(err)) abort(422, str(err.orig)) # If there's an error, rollback except Exception as err: - self.session.rollback() + await self.session.rollback() + LOGGER.error(str(err)) abort(500, str(err)) @overload - def update_multiple_objects_with_dml(self, update_stmts: list[Update]): + async def update_multiple_objects_with_dml(self, update_stmts: Update): ... @overload - def update_multiple_objects_with_dml(self, update_stmts: Update): + async def update_multiple_objects_with_dml(self, update_stmts: list[Update]): ... - def update_multiple_objects_with_dml(self, update_stmts): + async def update_multiple_objects_with_dml(self, update_stmts): """ Updates multiple objects with a single UPDATE statement. @@ -303,23 +336,25 @@ def update_multiple_objects_with_dml(self, update_stmts): try: if isinstance(update_stmts, list): for stmt in cast(list[Update], update_stmts): - self.session.execute(stmt) + await self.session.execute(stmt) else: - self.session.execute(update_stmts) + await self.session.execute(update_stmts) - self.session.commit() + await self.session.commit() # If there's a database error except (DataError, IntegrityError) as err: - self.session.rollback() + await self.session.rollback() + LOGGER.error(str(err)) abort(422, str(err.orig)) # If there's an error, rollback except Exception as err: - self.session.rollback() + await self.session.rollback() + LOGGER.error(str(err)) abort(500, str(err)) # DELETE # ----------------------------------------------------------------- - def delete_object(self, object: CoreSAHModel) -> int: + async def delete_object(self, object: CoreSAHModel) -> int: """ Deletes an existing record. @@ -327,21 +362,23 @@ def delete_object(self, object: CoreSAHModel) -> int: """ # Try to delete the record from the database try: - self.session.delete(object) - self.session.commit() + await self.session.delete(object) + await self.session.commit() return object.id # If there's a database error except (DataError, IntegrityError) as err: - self.session.rollback() + await self.session.rollback() + LOGGER.error(str(err)) abort(422, str(err.orig)) # If there's an error, rollback except Exception as err: - self.session.rollback() + await self.session.rollback() + LOGGER.error(str(err)) abort(500, str(err)) # Bulk delete # TODO: Return the number of deleted items - def delete_multiple_objects(self, delete_stmt: Delete): + async def delete_multiple_objects(self, delete_stmt: Delete): """ Executes a delete statement to delete multiple objects. @@ -349,13 +386,15 @@ def delete_multiple_objects(self, delete_stmt: Delete): """ # Try to delete the objects from the database try: - self.session.execute(delete_stmt) - self.session.commit() + await self.session.execute(delete_stmt) + await self.session.commit() # If there's a database error except (DataError, IntegrityError) as err: - self.session.rollback() + await self.session.rollback() + LOGGER.error(str(err)) abort(422, str(err.orig)) # If there's an error, rollback except Exception as err: - self.session.rollback() + await self.session.rollback() + LOGGER.error(str(err)) abort(500, str(err)) diff --git a/models/models.py b/models/models.py index 7f9ca37a..919ca8ae 100644 --- a/models/models.py +++ b/models/models.py @@ -30,6 +30,7 @@ from typing import Any, List, Optional, TypeAlias, TypeVar from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.ext.asyncio import AsyncAttrs from sqlalchemy.orm import ( Mapped, column_property, @@ -52,7 +53,7 @@ from sqlalchemy.dialects.postgresql import ARRAY -class BaseModel(DeclarativeBase): +class BaseModel(AsyncAttrs, DeclarativeBase): pass @@ -81,7 +82,7 @@ class Post(BaseModel): ForeignKey("users.id", onupdate="CASCADE", ondelete="SET NULL"), nullable=False, ) - user: Mapped["User"] = relationship("User", back_populates="posts") + user: Mapped["User"] = relationship("User", back_populates="posts", lazy="selectin") text: Mapped[str] = mapped_column(String(480), nullable=False) date: Mapped[Optional[datetime]] = mapped_column(DateTime) given_hugs: Mapped[int] = mapped_column(Integer, default=0) @@ -121,7 +122,9 @@ class User(BaseModel): ForeignKey("roles.id", onupdate="CASCADE", ondelete="SET NULL"), default=4, ) - role: Mapped[Optional["Role"]] = relationship("Role", foreign_keys="User.role_id") + role: Mapped[Optional["Role"]] = relationship( + "Role", foreign_keys="User.role_id", lazy="selectin" + ) blocked: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) release_date: Mapped[Optional[datetime]] = mapped_column(DateTime) open_report: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) @@ -495,7 +498,7 @@ class Role(BaseModel): id: Mapped[int] = mapped_column(Integer, primary_key=True) name: Mapped[str] = mapped_column(String(), nullable=False) permissions: Mapped[List[Permission]] = relationship( - "Permission", secondary=roles_permissions_map + "Permission", secondary=roles_permissions_map, lazy="selectin" ) # Format method diff --git a/requirements.txt b/requirements.txt index 76042d6a..0094213d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,11 @@ alembic==1.13.1 quart==0.19.5 quart-cors==0.7.0 -gunicorn==22.0.0 -psycopg2-binary==2.9.9 +hypercorn==0.16.0 +asyncpg==0.29.0 python-jose==3.3.0 pywebpush==2.0.0 -SQLAlchemy==2.0.29 +SQLAlchemy[asyncio]==2.0.29 setuptools>=65.5.1 # not directly required, pinned by Snyk to avoid a vulnerability aiohttp>= 3.9.2 # not directly required, pinned to avoid a vulnerability -hypercorn>=0.16.0 # not directly required, pinned by Snyk to avoid a vulnerability werkzeug>=2.3.8 # not directly required, pinned by Snyk to avoid a vulnerability diff --git a/setup.cfg b/setup.cfg index 889f07e8..7e3b40dd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,4 +7,4 @@ per-file-ignores = __init__.py:F401 addopts = --cov-config=.coveragerc --cov=. -asyncio_mode=auto +asyncio_mode = auto diff --git a/tests/conftest.py b/tests/conftest.py index 78fb6bd0..fb605962 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,14 +2,16 @@ import urllib.request import json from datetime import datetime +from asyncio import current_task import pytest from pytest_mock import MockerFixture -from sqlalchemy.orm import scoped_session, sessionmaker +from sqlalchemy.ext.asyncio import async_sessionmaker, async_scoped_session from create_app import create_app from config import SAHConfig from models.models import BaseModel +from models import SendADatabase from tests.data_models import create_data, DATETIME_PATTERN, update_sequences AUTH0_DOMAIN = os.environ.get("AUTH0_DOMAIN", "") @@ -66,11 +68,11 @@ def user_headers(): @pytest.fixture(scope="session") def test_config(): """Set up the config""" - test_db_path = "postgresql://postgres:password@localhost:5432/test_sah" - return SAHConfig(database_url=test_db_path) + test_db_path = "postgresql+asyncpg://postgres:password@localhost:5432/test_sah" + yield SAHConfig(database_url=test_db_path) -@pytest.fixture(scope="session") +@pytest.fixture(scope="function") def app_client(test_config: SAHConfig): """Get the test client for the test app""" app = create_app(config=test_config) @@ -78,18 +80,26 @@ def app_client(test_config: SAHConfig): @pytest.fixture(scope="session") -def db(test_config: SAHConfig): +async def db(test_config: SAHConfig): """Creates the database and inserts the test data.""" - BaseModel.metadata.drop_all(test_config.db.engine) - # create all tables - BaseModel.metadata.create_all(test_config.db.engine) - create_data(test_config.db) + try: + async with test_config.db.engine.begin() as conn: + await conn.run_sync(BaseModel.metadata.drop_all) + await conn.run_sync(BaseModel.metadata.create_all) + + await create_data(test_config.db) + + await test_config.db.engine.dispose() - yield test_config.db + yield test_config.db + + finally: + async with test_config.db.engine.begin() as conn: + await conn.run_sync(BaseModel.metadata.drop_all) @pytest.fixture(scope="function") -def test_db(db, mocker: MockerFixture): +async def test_db(db: SendADatabase, mocker: MockerFixture): """ Generates the session to use in tests. Once tests are done, rolls back the transaction and closes the session. Also updates the values @@ -97,30 +107,36 @@ def test_db(db, mocker: MockerFixture): Credit to gmassman; the code is mostly copied from https://github.com/gmassman/fsa-rollback-per-test-example. """ - connection = db.engine.connect() - transaction = connection.begin_nested() - - db.session_factory = sessionmaker( - bind=connection, - join_transaction_mode="create_savepoint", - ) + try: + connection = await db.engine.connect() + transaction = await connection.begin_nested() + + db.session_factory = async_sessionmaker( + bind=connection, + expire_on_commit=False, + join_transaction_mode="create_savepoint", + ) - db.session = scoped_session(session_factory=db.session_factory) + db.session = async_scoped_session( + session_factory=db.session_factory, scopefunc=current_task + ) - update_sequences(db) + await update_sequences(db) - db.session.begin_nested() - mocker.patch("pywebpush.webpush") - mocker.patch("create_app.webpush") + await db.session.begin_nested() + mocker.patch("pywebpush.webpush") + mocker.patch("create_app.webpush") - yield db + yield db - # Delete the session - db.session.remove() + finally: + # Delete the session + await db.session.remove() - # Rollback the transaction and return the connection to the pool - transaction.rollback() - connection.close() + # Rollback the transaction and return the connection to the pool + await transaction.rollback() + await connection.close() + await db.engine.dispose() @pytest.fixture @@ -157,19 +173,19 @@ def dummy_request_data(): "new_post": { "userId": 0, "text": "test post", - "date": "Sun Jun 07 2020 15:57:45", + "date": "2020-06-07T15:57:45.901Z", "givenHugs": 0, }, "updated_post": { "userId": 0, "text": "test post", - "date": "Sun Jun 07 2020 15:57:45", + "date": "2020-06-07T15:57:45.901Z", "givenHugs": 0, }, "report_post": { "user_id": 0, "text": "test post", - "date": "Sun Jun 07 2020 15:57:45", + "date": "2020-06-07T15:57:45.901Z", "givenHugs": 0, "closeReport": 1, }, @@ -207,7 +223,7 @@ def dummy_request_data(): "fromId": 0, "forId": 0, "messageText": "meow", - "date": "Sun Jun 07 2020 15:57:45", + "date": "2020-06-07T15:57:45.901Z", }, "new_report": { "type": "Post", @@ -215,14 +231,14 @@ def dummy_request_data(): "postID": 0, "reporter": 0, "reportReason": "It is inappropriate", - "date": "Sun Jun 07 2020 15:57:45", + "date": "2020-06-07T15:57:45.901Z", }, "new_user_report": { "type": "User", "userID": 0, "reporter": 0, "reportReason": "The user is posting Spam", - "date": "Sun Jun 07 2022 15:57:45", + "date": "2022-06-07T15:57:45.901Z", }, "new_subscription": { "endpoint": "https://fcm.googleapis.com/fcm/send/epyhl2GD", diff --git a/tests/data_models.py b/tests/data_models.py index 788e2807..fd4b7739 100644 --- a/tests/data_models.py +++ b/tests/data_models.py @@ -1,4 +1,5 @@ from datetime import datetime +from typing import Sequence import json from sqlalchemy import select, text @@ -21,20 +22,20 @@ DATETIME_PATTERN = "%Y-%m-%d %H:%M:%S.%f" -def create_filters(db: SendADatabase): +async def create_filters(db: SendADatabase): """Creates the filters in the test database.""" filter_1 = Filter(id=1, filter="filtered_word_1") filter_2 = Filter(id=2, filter="filtered_word_2") try: db.session.add_all([filter_1, filter_2]) - db.session.execute(text("ALTER SEQUENCE filters_id_seq RESTART WITH 3;")) - db.session.commit() + await db.session.execute(text("ALTER SEQUENCE filters_id_seq RESTART WITH 3;")) + await db.session.commit() finally: - db.session.close() + await db.session.remove() -def create_permissions(db): +async def create_permissions(db: SendADatabase): permission_1 = Permission( id=1, permission="block:user", description="Block or unblock a user" ) @@ -101,13 +102,15 @@ def create_permissions(db): permission_15, ] ) - db.session.execute(text("ALTER SEQUENCE permissions_id_seq RESTART WITH 16;")) - db.session.commit() + await db.session.execute( + text("ALTER SEQUENCE permissions_id_seq RESTART WITH 16;") + ) + await db.session.commit() finally: - db.session.close() + await db.session.remove() -def create_roles(db): +async def create_roles(db: SendADatabase): role_1 = Role(id=1, name="admin") role_2 = Role(id=2, name="moderator") role_3 = Role(id=3, name="user") @@ -115,11 +118,12 @@ def create_roles(db): role_5 = Role(id=5, name="blocked user") try: - permissions = db.session.scalars( + permissions_scalars = await db.session.scalars( select(Permission).order_by(Permission.id) - ).all() + ) + permissions: Sequence[Permission] = permissions_scalars.all() - role_1.permissions = permissions[0:11] + role_1.permissions = [*permissions[0:11]] role_2.permissions = [*permissions[2:4], *permissions[5:8], *permissions[9:13]] role_3.permissions = [permissions[2], *permissions[5:8], *permissions[9:14]] role_4.permissions = [permissions[2], *permissions[5:7], *permissions[9:]] @@ -131,13 +135,13 @@ def create_roles(db): ] db.session.add_all([role_1, role_2, role_3, role_4, role_5]) - db.session.execute(text("ALTER SEQUENCE roles_id_seq RESTART WITH 6;")) - db.session.commit() + await db.session.execute(text("ALTER SEQUENCE roles_id_seq RESTART WITH 6;")) + await db.session.commit() finally: - db.session.close() + await db.session.remove() -def create_users(db: SendADatabase): +async def create_users(db: SendADatabase): """Creates the users in the test database.""" user_1 = User( id=1, @@ -241,13 +245,13 @@ def create_users(db: SendADatabase): try: db.session.add_all([user_1, user_2, user_3, user_4, user_5]) - db.session.execute(text("ALTER SEQUENCE users_id_seq RESTART WITH 21;")) - db.session.commit() + await db.session.execute(text("ALTER SEQUENCE users_id_seq RESTART WITH 21;")) + await db.session.commit() finally: - db.session.close() + await db.session.remove() -def create_posts(db: SendADatabase): +async def create_posts(db: SendADatabase): """Creates the posts in the test database.""" post_1 = Post( id=1, @@ -495,13 +499,13 @@ def create_posts(db: SendADatabase): post_24, ] ) - db.session.execute(text("ALTER SEQUENCE posts_id_seq RESTART WITH 46;")) - db.session.commit() + await db.session.execute(text("ALTER SEQUENCE posts_id_seq RESTART WITH 46;")) + await db.session.commit() finally: - db.session.close() + await db.session.remove() -def create_threads(db: SendADatabase): +async def create_threads(db: SendADatabase): """Creates the threads in the test database.""" thread_1 = Thread( id=1, user_1_id=1, user_2_id=1, user_1_deleted=False, user_2_deleted=False @@ -529,13 +533,13 @@ def create_threads(db: SendADatabase): db.session.add_all( [thread_1, thread_2, thread_3, thread_4, thread_5, thread_6, thread_7] ) - db.session.execute(text("ALTER SEQUENCE threads_id_seq RESTART WITH 9;")) - db.session.commit() + await db.session.execute(text("ALTER SEQUENCE threads_id_seq RESTART WITH 9;")) + await db.session.commit() finally: - db.session.close() + await db.session.remove() -def create_messages(db: SendADatabase): +async def create_messages(db: SendADatabase): """Creates the messages in the test database.""" message_1 = Message( id=5, @@ -697,13 +701,15 @@ def create_messages(db: SendADatabase): message_14, ] ) - db.session.execute(text("ALTER SEQUENCE messages_id_seq RESTART WITH 27;")) - db.session.commit() + await db.session.execute( + text("ALTER SEQUENCE messages_id_seq RESTART WITH 27;") + ) + await db.session.commit() finally: - db.session.close() + await db.session.remove() -def create_reports(db: SendADatabase): +async def create_reports(db: SendADatabase): """Creates the reports in the test database.""" report_1 = Report( id=1, @@ -1239,13 +1245,13 @@ def create_reports(db: SendADatabase): report_44, ] ) - db.session.execute(text("ALTER SEQUENCE reports_id_seq RESTART WITH 45;")) - db.session.commit() + await db.session.execute(text("ALTER SEQUENCE reports_id_seq RESTART WITH 45;")) + await db.session.commit() finally: - db.session.close() + await db.session.remove() -def create_notifications(db: SendADatabase): +async def create_notifications(db: SendADatabase): """Creates the notifications in the test database.""" notification_1 = Notification( id=1, @@ -1640,13 +1646,15 @@ def create_notifications(db: SendADatabase): notification_95, ] ) - db.session.execute(text("ALTER SEQUENCE notifications_id_seq RESTART WITH 96;")) - db.session.commit() + await db.session.execute( + text("ALTER SEQUENCE notifications_id_seq RESTART WITH 96;") + ) + await db.session.commit() finally: - db.session.close() + await db.session.remove() -def create_subscriptions(db: SendADatabase): +async def create_subscriptions(db: SendADatabase): """Creates the push subscriptions in the database""" sub_1 = NotificationSub( id=1, @@ -1687,41 +1695,51 @@ def create_subscriptions(db: SendADatabase): try: db.session.add_all([sub_1, sub_2, sub_3]) - db.session.execute(text("ALTER SEQUENCE subscriptions_id_seq RESTART WITH 4;")) - db.session.commit() + await db.session.execute( + text("ALTER SEQUENCE subscriptions_id_seq RESTART WITH 4;") + ) + await db.session.commit() finally: - db.session.close() + await db.session.close() -def update_sequences(db: SendADatabase): +async def update_sequences(db: SendADatabase): """Updates the values of all sequences.""" try: - db.session.execute(text("ALTER SEQUENCE filters_id_seq RESTART WITH 3;")) - db.session.execute(text("ALTER SEQUENCE permissions_id_seq RESTART WITH 16;")) - db.session.execute(text("ALTER SEQUENCE roles_id_seq RESTART WITH 6;")) - db.session.execute(text("ALTER SEQUENCE users_id_seq RESTART WITH 21;")) - db.session.execute(text("ALTER SEQUENCE posts_id_seq RESTART WITH 46;")) - db.session.execute(text("ALTER SEQUENCE messages_id_seq RESTART WITH 27;")) - db.session.execute(text("ALTER SEQUENCE threads_id_seq RESTART WITH 9;")) - db.session.execute(text("ALTER SEQUENCE reports_id_seq RESTART WITH 45;")) - db.session.execute(text("ALTER SEQUENCE notifications_id_seq RESTART WITH 96;")) - db.session.execute(text("ALTER SEQUENCE subscriptions_id_seq RESTART WITH 4;")) - db.session.commit() + await db.session.execute( + text("ALTER SEQUENCE permissions_id_seq RESTART WITH 16;") + ) + await db.session.execute(text("ALTER SEQUENCE filters_id_seq RESTART WITH 3;")) + await db.session.execute(text("ALTER SEQUENCE roles_id_seq RESTART WITH 6;")) + await db.session.execute(text("ALTER SEQUENCE users_id_seq RESTART WITH 21;")) + await db.session.execute(text("ALTER SEQUENCE posts_id_seq RESTART WITH 46;")) + await db.session.execute( + text("ALTER SEQUENCE messages_id_seq RESTART WITH 27;") + ) + await db.session.execute(text("ALTER SEQUENCE threads_id_seq RESTART WITH 9;")) + await db.session.execute(text("ALTER SEQUENCE reports_id_seq RESTART WITH 45;")) + await db.session.execute( + text("ALTER SEQUENCE notifications_id_seq RESTART WITH 96;") + ) + await db.session.execute( + text("ALTER SEQUENCE subscriptions_id_seq RESTART WITH 4;") + ) + await db.session.commit() finally: - db.session.close() + await db.session.close() -def create_data(db: SendADatabase): +async def create_data(db: SendADatabase): """Creates the data in the test database.""" - create_filters(db) - create_permissions(db) - create_roles(db) - create_users(db) - create_posts(db) - create_threads(db) - create_messages(db) - create_reports(db) - create_notifications(db) - create_subscriptions(db) + await create_filters(db) + await create_permissions(db) + await create_roles(db) + await create_users(db) + await create_posts(db) + await create_threads(db) + await create_messages(db) + await create_reports(db) + await create_notifications(db) + await create_subscriptions(db) - db.session.close() + await db.session.remove() diff --git a/tests/test_app.py b/tests/test_app.py index fd23d0f1..068ffbe2 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -33,7 +33,7 @@ # App testing # Index Route Tests ('/', GET) # ------------------------------------------------------- -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_home_page(app_client, test_db, user_headers): response = await app_client.get("/") response_data = await response.get_json() @@ -47,7 +47,7 @@ async def test_get_home_page(app_client, test_db, user_headers): # Search Route Tests ('/', POST) # ------------------------------------------------------- # Run a search -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_search(app_client, test_db, user_headers): response = await app_client.post("/", data=json.dumps({"search": "user"})) response_data = await response.get_json() @@ -59,7 +59,7 @@ async def test_search(app_client, test_db, user_headers): # Run a search which returns multiple pages of results -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_search_multiple_pages(app_client, test_db, user_headers): response = await app_client.post("/", data=json.dumps({"search": "test"})) response_data = await response.get_json() @@ -74,7 +74,7 @@ async def test_search_multiple_pages(app_client, test_db, user_headers): # Run a search which returns multiple pages of results - get page 2 -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_search_multiple_pages_page_2(app_client, test_db, user_headers): response = await app_client.post("/?page=2", data=json.dumps({"search": "test"})) response_data = await response.get_json() diff --git a/tests/test_auth.py b/tests/test_auth.py index 939a22a7..9ad4cda1 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -36,7 +36,7 @@ get_current_user, check_user_permissions, ) -from config import SAHConfig +from models import SendADatabase # Auth testing @@ -101,9 +101,10 @@ def test_verify_jwt_error(mocker, error, error_message): assert error_message in str(exc.value) -def test_get_current_user_error(test_config: SAHConfig, test_db): +@pytest.mark.asyncio(scope="session") +async def test_get_current_user_error(test_db: SendADatabase): with pytest.raises(AuthError) as exc: - get_current_user( + await get_current_user( { "sub": "auth0|12345", "aud": [ @@ -111,14 +112,15 @@ def test_get_current_user_error(test_config: SAHConfig, test_db): ], "permissions": ["read:user"], }, - test_config.db, + test_db, ) assert "Unauthorised. User not found." in str(exc.value) -def test_get_current_user(dummy_users_data, test_config: SAHConfig, test_db): - user = get_current_user({"sub": dummy_users_data["user"]["auth0"]}, test_config.db) +@pytest.mark.asyncio(scope="session") +async def test_get_current_user(dummy_users_data, test_db: SendADatabase): + user = await get_current_user({"sub": dummy_users_data["user"]["auth0"]}, test_db) assert user["id"] == int(dummy_users_data["user"]["internal"]) diff --git a/tests/test_db.py b/tests/test_db.py index 9acef1d7..9d1e02db 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -80,52 +80,59 @@ def update_post_stmts(): ] -def test_paginate_no_error(test_db: SendADatabase): +@pytest.mark.asyncio(scope="session") +async def test_paginate_no_error(test_db: SendADatabase): query = select(Post).where(Post.given_hugs > 0) - results = test_db.paginate(query, 1, 10) + results = await test_db.paginate(query, 1, 10) assert results.total_pages == 3 assert results.total_items == 21 assert len(results.resource) == 10 -def test_paginate_with_error(mocker: MockerFixture, test_db: SendADatabase): +@pytest.mark.asyncio(scope="session") +async def test_paginate_with_error(mocker: MockerFixture, test_db: SendADatabase): mocker.patch( "models.models.Post.format", side_effect=Exception("There was an error!") ) query = select(Post).where(Post.given_hugs > 0) with pytest.raises(HTTPException) as exc: - test_db.paginate(query, 1, 10) + await test_db.paginate(query, 1, 10) assert "There was an error" in str(exc.value) assert "500" in str(exc.value) -def test_one_or_404_success(test_db: SendADatabase): - result = test_db.one_or_404(1, Post) +@pytest.mark.asyncio(scope="session") +async def test_one_or_404_success(test_db: SendADatabase): + result = await test_db.one_or_404(1, Post) assert result.id == 1 assert result.text == "test" -def test_one_or_404_not_existing(test_db: SendADatabase): +@pytest.mark.asyncio(scope="session") +async def test_one_or_404_not_existing(test_db: SendADatabase): with pytest.raises(HTTPException) as exc: - test_db.one_or_404(100, Post) + await test_db.one_or_404(100, Post) assert "404" in str(exc.value) -def test_one_or_404_error(test_db: SendADatabase): +@pytest.mark.asyncio(scope="session") +async def test_one_or_404_error(test_db: SendADatabase): with pytest.raises(HTTPException) as exc: - test_db.one_or_404("hi", Post) # type: ignore + await test_db.one_or_404("hi", Post) # type: ignore assert "422" in str(exc.value) - assert "invalid input syntax for type integer" in str(exc.value) + assert "invalid input for query argument" in str(exc.value) + assert "'str' object cannot be interpreted as an integer" in str(exc.value) -def test_add_no_errors(test_db: SendADatabase, posts_to_add: list[Post]): +@pytest.mark.asyncio(scope="session") +async def test_add_no_errors(test_db: SendADatabase, posts_to_add: list[Post]): post_to_add = posts_to_add[0] expected_return = { "id": 46, @@ -137,21 +144,23 @@ def test_add_no_errors(test_db: SendADatabase, posts_to_add: list[Post]): "sentHugs": [], } - actual_return = test_db.add_object(obj=post_to_add).format() + actual_return = await test_db.add_object(obj=post_to_add) assert expected_return == actual_return -def test_add_integrity_error(test_db: SendADatabase, invalid_post_to_add: Post): +@pytest.mark.asyncio(scope="session") +async def test_add_integrity_error(test_db: SendADatabase, invalid_post_to_add: Post): with pytest.raises(HTTPException) as exc: - test_db.add_object(obj=invalid_post_to_add) + await test_db.add_object(obj=invalid_post_to_add) assert "Unprocessable Entity" in str(exc.value) assert "422" in str(exc.value) assert "violates foreign key constraint" in str(exc.value) -def test_add_other_error( +@pytest.mark.asyncio(scope="session") +async def test_add_other_error( test_db: SendADatabase, mocker: MockerFixture, posts_to_add: list[Post] ): post_to_add = posts_to_add[0] @@ -164,13 +173,14 @@ def test_add_other_error( ) with pytest.raises(HTTPException) as exc: - test_db.add_object(obj=post_to_add) + await test_db.add_object(obj=post_to_add) assert "500 Internal Server Error" in str(exc.value) assert "test error" in str(exc.value) -def test_add_multiple_no_errors(test_db: SendADatabase, posts_to_add: list[Post]): +@pytest.mark.asyncio(scope="session") +async def test_add_multiple_no_errors(test_db: SendADatabase, posts_to_add: list[Post]): expected_return = [ { "id": 46, @@ -192,23 +202,25 @@ def test_add_multiple_no_errors(test_db: SendADatabase, posts_to_add: list[Post] }, ] - actual_return = test_db.add_multiple_objects(objects=[*posts_to_add]) + actual_return = await test_db.add_multiple_objects(objects=[*posts_to_add]) assert expected_return == actual_return -def test_add_multiple_integrity_error( +@pytest.mark.asyncio(scope="session") +async def test_add_multiple_integrity_error( test_db: SendADatabase, invalid_post_to_add: Post, posts_to_add: list[Post] ): with pytest.raises(HTTPException) as exc: - test_db.add_multiple_objects(objects=[invalid_post_to_add, *posts_to_add]) + await test_db.add_multiple_objects(objects=[invalid_post_to_add, *posts_to_add]) assert "Unprocessable Entity" in str(exc.value) assert "422" in str(exc.value) assert "violates foreign key constraint" in str(exc.value) -def test_add_multiple_other_error( +@pytest.mark.asyncio(scope="session") +async def test_add_multiple_other_error( test_db: SendADatabase, mocker: MockerFixture, posts_to_add: list[Post] ): mocker.patch.object( @@ -220,30 +232,32 @@ def test_add_multiple_other_error( ) with pytest.raises(HTTPException) as exc: - test_db.add_multiple_objects(objects=[*posts_to_add]) + await test_db.add_multiple_objects(objects=[*posts_to_add]) assert "500 Internal Server Error" in str(exc.value) assert "test error" in str(exc.value) -def test_update_no_errors(test_db: SendADatabase, db_helpers_dummy_data): +@pytest.mark.asyncio(scope="session") +async def test_update_no_errors(test_db: SendADatabase, db_helpers_dummy_data): expected_return = db_helpers_dummy_data["updated_post"] - post = test_db.session.get(Post, 1) + post = await test_db.session.get(Post, 1) if not post: pytest.fail("The post doesn't exist! Check the test database") original_text = post.text post.text = "new test" - actual_return = test_db.update_object(obj=post) + actual_return = await test_db.update_object(obj=post) - assert expected_return == actual_return.format() - assert original_text != actual_return.format()["text"] + assert expected_return == actual_return + assert original_text != actual_return["text"] -def test_update_integrity_error(test_db: SendADatabase): - post = test_db.session.get(Post, 1) +@pytest.mark.asyncio(scope="session") +async def test_update_integrity_error(test_db: SendADatabase): + post = await test_db.session.get(Post, 1) if not post: pytest.fail("The post doesn't exist! Check the test database") @@ -251,13 +265,14 @@ def test_update_integrity_error(test_db: SendADatabase): post.text = None # type: ignore with pytest.raises(HTTPException) as exc: - test_db.update_object(obj=post) + await test_db.update_object(obj=post) assert "Unprocessable Entity" in str(exc.value) assert "violates not-null constraint" in str(exc.value) -def test_update_other_error( +@pytest.mark.asyncio(scope="session") +async def test_update_other_error( test_db: SendADatabase, mocker: MockerFixture, posts_to_add: list[Post] ): mocker.patch.object( @@ -269,13 +284,14 @@ def test_update_other_error( ) with pytest.raises(HTTPException) as exc: - test_db.update_object(obj=posts_to_add[0]) + await test_db.update_object(obj=posts_to_add[0]) assert "500 Internal Server Error" in str(exc.value) assert "test error" in str(exc.value) -def test_update_multiple_no_errors(test_db: SendADatabase, db_helpers_dummy_data): +@pytest.mark.asyncio(scope="session") +async def test_update_multiple_no_errors(test_db: SendADatabase, db_helpers_dummy_data): expected_return = [ db_helpers_dummy_data["updated_post"], { @@ -291,14 +307,15 @@ def test_update_multiple_no_errors(test_db: SendADatabase, db_helpers_dummy_data }, ] - posts = test_db.session.scalars( + posts_instances = await test_db.session.scalars( select(Post).filter(Post.id < 3).order_by(Post.id) - ).all() + ) + posts = posts_instances.all() original_post_1_text = posts[0].text posts[0].text = "new test" original_post_2_hugs = posts[1].given_hugs posts[1].given_hugs = 3 - actual_return = test_db.update_multiple_objects(objects=posts) + actual_return = await test_db.update_multiple_objects(objects=posts) updated_posts = sorted(actual_return, key=lambda p: p["id"]) assert expected_return == updated_posts @@ -306,21 +323,23 @@ def test_update_multiple_no_errors(test_db: SendADatabase, db_helpers_dummy_data assert updated_posts[1]["givenHugs"] != original_post_2_hugs -def test_update_multiple_error(test_db: SendADatabase): - posts = test_db.session.scalars( +@pytest.mark.asyncio(scope="session") +async def test_update_multiple_error(test_db: SendADatabase): + posts_instances = await test_db.session.scalars( select(Post).filter(Post.id < 3).order_by(Post.id) - ).all() + ) + posts = posts_instances.all() posts[0].text = "hello" posts[1].user_id = 1000 with pytest.raises(HTTPException) as exc: - test_db.update_multiple_objects(objects=posts) + await test_db.update_multiple_objects(objects=posts) assert "Unprocessable Entity" in str(exc.value) assert "violates foreign key constraint" in str(exc.value) # Make sure the post that's right wasn't updated either - post = test_db.session.get(Post, 1) + post = await test_db.session.get(Post, 1) if not post: pytest.fail("The post doesn't exist! Check the test database") @@ -328,7 +347,8 @@ def test_update_multiple_error(test_db: SendADatabase): assert post.text != "hello" -def test_update_multiple_other_error( +@pytest.mark.asyncio(scope="session") +async def test_update_multiple_other_error( test_db: SendADatabase, mocker: MockerFixture, posts_to_add: list[Post] ): mocker.patch.object( @@ -340,45 +360,52 @@ def test_update_multiple_other_error( ) with pytest.raises(HTTPException) as exc: - test_db.update_multiple_objects(objects=[*posts_to_add]) + await test_db.update_multiple_objects(objects=[*posts_to_add]) assert "500 Internal Server Error" in str(exc.value) assert "test error" in str(exc.value) -def test_update_multiple_dml_one_stmt( +@pytest.mark.asyncio(scope="session") +async def test_update_multiple_dml_one_stmt( test_db: SendADatabase, update_post_stmts: list[Update] ): - test_db.update_multiple_objects_with_dml(update_stmts=update_post_stmts[0]) + await test_db.update_multiple_objects_with_dml(update_stmts=update_post_stmts[0]) - closed_report_posts = test_db.session.scalars( + closed_report_posts_scalars = await test_db.session.scalars( select(Post).where(and_(Post.user_id == 1, Post.open_report == false())) - ).all() - given_hugs_posts = test_db.session.scalars( + ) + closed_report_posts = closed_report_posts_scalars.all() + given_hugs_posts_scalars = await test_db.session.scalars( select(Post).where(and_(Post.user_id == 4, Post.given_hugs != 10)) - ).all() + ) + given_hugs_posts = given_hugs_posts_scalars.all() assert len(closed_report_posts) == 0 assert len(given_hugs_posts) != 0 -def test_update_multiple_dml_multiple_stmts( +@pytest.mark.asyncio(scope="session") +async def test_update_multiple_dml_multiple_stmts( test_db: SendADatabase, update_post_stmts: list[Update] ): - test_db.update_multiple_objects_with_dml(update_stmts=update_post_stmts) + await test_db.update_multiple_objects_with_dml(update_stmts=update_post_stmts) - closed_report_posts = test_db.session.scalars( + closed_report_posts_scalars = await test_db.session.scalars( select(Post).where(and_(Post.user_id == 1, Post.open_report == false())) - ).all() - given_hugs_posts = test_db.session.scalars( + ) + closed_report_posts = closed_report_posts_scalars.all() + given_hugs_posts_scalars = await test_db.session.scalars( select(Post).where(and_(Post.user_id == 4, Post.given_hugs != 10)) - ).all() + ) + given_hugs_posts = given_hugs_posts_scalars.all() assert len(closed_report_posts) == 0 assert len(given_hugs_posts) == 0 -def test_update_multiple_dml_integrity_error( +@pytest.mark.asyncio(scope="session") +async def test_update_multiple_dml_integrity_error( test_db: SendADatabase, mocker: MockerFixture, update_post_stmts: list[Update] ): mocker.patch.object( @@ -390,13 +417,14 @@ def test_update_multiple_dml_integrity_error( ) with pytest.raises(HTTPException) as exc: - test_db.update_multiple_objects_with_dml(update_stmts=update_post_stmts) + await test_db.update_multiple_objects_with_dml(update_stmts=update_post_stmts) assert "Unprocessable Entity" in str(exc.value) assert "422" in str(exc.value) -def test_update_multiple_dml_other_error( +@pytest.mark.asyncio(scope="session") +async def test_update_multiple_dml_other_error( test_db: SendADatabase, mocker: MockerFixture, update_post_stmts: list[Update] ): mocker.patch.object( @@ -408,7 +436,7 @@ def test_update_multiple_dml_other_error( ) with pytest.raises(HTTPException) as exc: - test_db.update_multiple_objects_with_dml(update_stmts=update_post_stmts) + await test_db.update_multiple_objects_with_dml(update_stmts=update_post_stmts) assert "500 Internal Server Error" in str(exc.value) assert "test error" in str(exc.value) @@ -422,7 +450,8 @@ def test_update_multiple_dml_other_error( (OperationalError, "500 Internal Server Error", "test error"), ], ) -def test_delete_error( +@pytest.mark.asyncio(scope="session") +async def test_delete_error( test_db: SendADatabase, mocker: MockerFixture, posts_to_add: list[Post], @@ -439,7 +468,7 @@ def test_delete_error( ) with pytest.raises(HTTPException) as exc: - test_db.delete_object(object=posts_to_add[0]) + await test_db.delete_object(object=posts_to_add[0]) assert exception_code in str(exc.value) assert exception_error in str(exc.value) @@ -453,7 +482,8 @@ def test_delete_error( (OperationalError, "500 Internal Server Error", "test error"), ], ) -def test_delete_dml_error( +@pytest.mark.asyncio(scope="session") +async def test_delete_dml_error( test_db: SendADatabase, mocker: MockerFixture, error, @@ -471,7 +501,7 @@ def test_delete_dml_error( delete_stmt = delete(Post).where(Post.user_id == 1) with pytest.raises(HTTPException) as exc: - test_db.delete_multiple_objects(delete_stmt=delete_stmt) + await test_db.delete_multiple_objects(delete_stmt=delete_stmt) assert exception_code in str(exc.value) assert exception_error in str(exc.value) diff --git a/tests/test_filters.py b/tests/test_filters.py index 5b5c57bc..05b20e20 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -33,7 +33,7 @@ # Get Filters Tests ('/filters', GET) # ------------------------------------------------------- # Attempt to get filters without auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_filters_no_auth(app_client, test_db): response = await app_client.get("/filters") response_data = await response.get_json() @@ -54,7 +54,7 @@ async def test_get_filters_no_auth(app_client, test_db): ("moderator", 403), ], ) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_filters_auth_error( app_client, test_db, user_headers, user, status_code ): @@ -66,7 +66,7 @@ async def test_get_filters_auth_error( # Attempt to get filters with an admin's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_filters_as_admin(app_client, test_db, user_headers): response = await app_client.get("/filters", headers=user_headers["admin"]) response_data = await response.get_json() @@ -80,7 +80,7 @@ async def test_get_filters_as_admin(app_client, test_db, user_headers): # Create Filters Tests ('/filters', POST) # ------------------------------------------------------- # Attempt to create a filter without auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_create_filters_no_auth(app_client, test_db, user_headers): response = await app_client.post("/filters", data=json.dumps({"word": "sample"})) response_data = await response.get_json() @@ -101,7 +101,7 @@ async def test_create_filters_no_auth(app_client, test_db, user_headers): ("moderator", 403), ], ) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_create_filters_auth_error( app_client, test_db, user_headers, user, status_code ): @@ -117,7 +117,7 @@ async def test_create_filters_auth_error( # Attempt to create a filter with an admin's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_create_filters_as_admin(app_client, test_db, user_headers): response = await app_client.post( "/filters", headers=user_headers["admin"], data=json.dumps({"word": "sample"}) @@ -131,7 +131,7 @@ async def test_create_filters_as_admin(app_client, test_db, user_headers): # Attempt to create a filter with an admin's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_create_duplicate_filters_as_admin(app_client, test_db, user_headers): filter = {"word": "sample"} await app_client.post( @@ -149,7 +149,7 @@ async def test_create_duplicate_filters_as_admin(app_client, test_db, user_heade # Delete Filters Tests ('/filters/', DELETE) # ------------------------------------------------------- # Attempt to delete a filter without auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_filters_no_auth(app_client, test_db, user_headers): response = await app_client.delete("/filters/1") response_data = await response.get_json() @@ -170,7 +170,7 @@ async def test_delete_filters_no_auth(app_client, test_db, user_headers): ("moderator", 403), ], ) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_filters_auth_error( app_client, test_db, user_headers, user, status_code ): @@ -182,7 +182,7 @@ async def test_delete_filters_auth_error( # Attempt to delete a filter with an admin's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_filters_as_admin(app_client, test_db, user_headers): # Delete the filter response = await app_client.delete("/filters/2", headers=user_headers["admin"]) @@ -195,7 +195,7 @@ async def test_delete_filters_as_admin(app_client, test_db, user_headers): # Attempt to delete a filter that doesn't exist with an admin's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_nonexistent_filters_as_admin(app_client, test_db, user_headers): response = await app_client.delete("/filters/100", headers=user_headers["admin"]) response_data = await response.get_json() diff --git a/tests/test_messages.py b/tests/test_messages.py index e8b0a359..8ae4064e 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -33,7 +33,7 @@ # Get User's Messages Tests ('/messages', GET) # ------------------------------------------------------- # Attempt to get a user's messages without auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_user_messages_no_auth(app_client, test_db, user_headers): response = await app_client.get("/messages?userID=1") response_data = await response.get_json() @@ -43,7 +43,7 @@ async def test_get_user_messages_no_auth(app_client, test_db, user_headers): # Attempt to get a user's messages with malformed auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_user_messages_malformed_auth(app_client, test_db, user_headers): response = await app_client.get( "/messages?userID=1", headers=user_headers["malformed"] @@ -55,7 +55,7 @@ async def test_get_user_messages_malformed_auth(app_client, test_db, user_header # Attempt to get a user's inbox with a user's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_user_inbox_as_user( app_client, test_db, user_headers, dummy_users_data ): @@ -73,7 +73,7 @@ async def test_get_user_inbox_as_user( # Attempt to get a user's outbox with a user's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_user_outbox_as_user( app_client, test_db, user_headers, dummy_users_data ): @@ -91,7 +91,7 @@ async def test_get_user_outbox_as_user( # Attempt to get a user's threads mailbox with a user's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_user_threads_as_user( app_client, test_db, user_headers, dummy_users_data ): @@ -109,7 +109,7 @@ async def test_get_user_threads_as_user( # Attempt to get another user's messages with a user's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_another_users_messages_as_user( app_client, test_db, user_headers, dummy_users_data ): @@ -124,7 +124,7 @@ async def test_get_another_users_messages_as_user( # Attempt to get a user's inbox with a moderator's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_user_inbox_as_mod( app_client, test_db, user_headers, dummy_users_data ): @@ -142,7 +142,7 @@ async def test_get_user_inbox_as_mod( # Attempt to get a user's outbox with a moderator's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_user_outbox_as_mod( app_client, test_db, user_headers, dummy_users_data ): @@ -160,7 +160,7 @@ async def test_get_user_outbox_as_mod( # Attempt to get a user's threads mailbox with a moderator's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_user_threads_as_mod( app_client, test_db, user_headers, dummy_users_data ): @@ -178,7 +178,7 @@ async def test_get_user_threads_as_mod( #  Attempt to get another user's messages with a moderator's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_another_users_messages_as_mod( app_client, test_db, user_headers, dummy_users_data ): @@ -193,7 +193,7 @@ async def test_get_another_users_messages_as_mod( # Attempt to get a user's inbox with an admin's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_user_inbox_as_admin( app_client, test_db, user_headers, dummy_users_data ): @@ -211,7 +211,7 @@ async def test_get_user_inbox_as_admin( # Attempt to get a user's outbox with an admin's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_user_outbox_as_admin( app_client, test_db, user_headers, dummy_users_data ): @@ -229,7 +229,7 @@ async def test_get_user_outbox_as_admin( # Attempt to get a user's threads mailbox with an admin's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_user_threads_as_admin( app_client, test_db, user_headers, dummy_users_data ): @@ -247,7 +247,7 @@ async def test_get_user_threads_as_admin( # Attempt to get another user's messages with an admin's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_another_users_messages_as_admin( app_client, test_db, user_headers, dummy_users_data ): @@ -262,7 +262,7 @@ async def test_get_another_users_messages_as_admin( # Attempt to get a user's messages with no ID (with admin's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_no_id_user_messages_as_admin(app_client, test_db, user_headers): response = await app_client.get("/messages", headers=user_headers["admin"]) response_data = await response.get_json() @@ -272,7 +272,7 @@ async def test_get_no_id_user_messages_as_admin(app_client, test_db, user_header # Attempt to get other users' messaging thread (with admin's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def get_other_users_thread_as_admin(app_client, test_db, user_headers): response = await app_client.get( "/messages?userID=4&type=thread&threadID=2", @@ -285,7 +285,7 @@ async def get_other_users_thread_as_admin(app_client, test_db, user_headers): # Attempt to get other users' messaging thread (with admin's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def get_nonexistent_thread_as_admin( app_client, test_db, user_headers, dummy_users_data ): @@ -303,7 +303,7 @@ async def get_nonexistent_thread_as_admin( # Create Message Route Tests ('/message', POST) # ------------------------------------------------------- # Attempt to create a message with no authorisation header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_send_message_no_auth( app_client, test_db, user_headers, dummy_request_data ): @@ -317,7 +317,7 @@ async def test_send_message_no_auth( # Attempt to create a message with a malformed auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_send_message_malformed_auth( app_client, test_db, user_headers, dummy_request_data ): @@ -333,7 +333,7 @@ async def test_send_message_malformed_auth( # Attempt to create a message with a user's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_send_message_as_user( app_client, test_db, user_headers, dummy_users_data, dummy_request_data ): @@ -352,7 +352,7 @@ async def test_send_message_as_user( # Attempt to create a message from another user (with a user's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_send_message_from_another_user_as_user( app_client, test_db, user_headers, dummy_users_data, dummy_request_data ): @@ -369,7 +369,7 @@ async def test_send_message_from_another_user_as_user( # Attempt to create a message with a moderator's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_send_message_as_mod( app_client, test_db, user_headers, dummy_users_data, dummy_request_data ): @@ -388,7 +388,7 @@ async def test_send_message_as_mod( # Attempt to create a message from another user (with a moderator's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_send_message_from_another_user_as_mod( app_client, test_db, user_headers, dummy_users_data, dummy_request_data ): @@ -405,7 +405,7 @@ async def test_send_message_from_another_user_as_mod( # Attempt to create a message with an admin's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_send_message_as_admin( app_client, test_db, user_headers, dummy_users_data, dummy_request_data ): @@ -424,7 +424,7 @@ async def test_send_message_as_admin( # Attempt to create a message from another user (with an admin's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_send_message_from_another_user_as_admin( app_client, test_db, user_headers, dummy_users_data, dummy_request_data ): @@ -441,7 +441,7 @@ async def test_send_message_from_another_user_as_admin( # Attempt to send a message from a user (when there's no thread) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_send_message_existing_thread_as_user( app_client, test_db, user_headers, dummy_users_data, dummy_request_data ): @@ -465,7 +465,7 @@ async def test_send_message_existing_thread_as_user( assert len(new_thread_data["messages"]) == 2 -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_send_message_create_thread( app_client, test_db, user_headers, dummy_users_data, dummy_request_data ): @@ -494,7 +494,7 @@ async def test_send_message_create_thread( # Delete Message Route Tests ('/message/', DELETE) # ------------------------------------------------------- # Attempt to delete a message with no authorisation header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_message_no_auth(app_client, test_db, user_headers): response = await app_client.delete("/messages/inbox/1") response_data = await response.get_json() @@ -504,7 +504,7 @@ async def test_delete_message_no_auth(app_client, test_db, user_headers): # Attempt to delete a message with a malformed auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_message_malformed_auth(app_client, test_db, user_headers): response = await app_client.delete( "/messages/inbox/1", headers=user_headers["malformed"] @@ -516,7 +516,7 @@ async def test_delete_message_malformed_auth(app_client, test_db, user_headers): # Attempt to delete a message with a user's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_message_as_user(app_client, test_db, user_headers): response = await app_client.delete( "/messages/inbox/3", headers=user_headers["user"] @@ -529,7 +529,7 @@ async def test_delete_message_as_user(app_client, test_db, user_headers): # Attempt to delete another user's message (with a user's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_message_from_another_user_as_user( app_client, test_db, user_headers ): @@ -543,7 +543,7 @@ async def test_delete_message_from_another_user_as_user( # Attempt to delete a thread with a user's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_thread_as_user(app_client, test_db, user_headers): response = await app_client.delete( "/messages/threads/2", headers=user_headers["user"] @@ -562,7 +562,7 @@ async def test_delete_thread_as_user(app_client, test_db, user_headers): # Attempt to delete a message with a moderator's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_message_as_mod(app_client, test_db, user_headers): response = await app_client.delete( "/messages/inbox/5", headers=user_headers["moderator"] @@ -575,7 +575,7 @@ async def test_delete_message_as_mod(app_client, test_db, user_headers): # Attempt to delete another user's message (with a moderator's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_message_from_another_user_as_mod( app_client, test_db, user_headers ): @@ -589,7 +589,7 @@ async def test_delete_message_from_another_user_as_mod( # Attempt to delete a message with an admin's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_message_as_admin(app_client, test_db, user_headers): response = await app_client.delete( "/messages/outbox/10", headers=user_headers["admin"] @@ -602,7 +602,7 @@ async def test_delete_message_as_admin(app_client, test_db, user_headers): # Attempt to delete another user's message (with an admin's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_message_from_another_user_as_admin( app_client, test_db, user_headers ): @@ -616,7 +616,7 @@ async def test_delete_message_from_another_user_as_admin( # Attempt to delete a user's message with no mailbox (with admin's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_no_id_user_message_as_admin(app_client, test_db, user_headers): response = await app_client.delete("/messages/", headers=user_headers["admin"]) response_data = await response.get_json() @@ -626,7 +626,7 @@ async def test_delete_no_id_user_message_as_admin(app_client, test_db, user_head # Attempt to delete a nonexistent user's message (with admin's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_nonexistent_user_message_as_admin( app_client, test_db, user_headers ): @@ -640,7 +640,7 @@ async def test_delete_nonexistent_user_message_as_admin( # Attempt to delete a message without ID -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_message_without_id_admin(app_client, test_db, user_headers): response = await app_client.delete( "/messages/inbox/", headers=user_headers["admin"] @@ -654,7 +654,7 @@ async def test_delete_message_without_id_admin(app_client, test_db, user_headers # Empty Mailbox Tests ('/messages/', DELETE) # ------------------------------------------------------- # Attempt to empty mailbox without auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_empty_mailbox_no_auth(app_client, test_db, user_headers): response = await app_client.delete("/messages/inbox?userID=4") response_data = await response.get_json() @@ -664,7 +664,7 @@ async def test_empty_mailbox_no_auth(app_client, test_db, user_headers): # Attempt to empty mailbox with malformed auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_empty_mailbox_malformed_auth(app_client, test_db, user_headers): response = await app_client.delete( "/messages/inbox?userID=4", headers=user_headers["malformed"] @@ -676,7 +676,7 @@ async def test_empty_mailbox_malformed_auth(app_client, test_db, user_headers): # Attempt to empty user's inbox (user JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_empty_mailbox_as_user( app_client, test_db, user_headers, dummy_users_data ): @@ -701,7 +701,7 @@ async def test_empty_mailbox_as_user( # Attempt to empty another user's inbox (user JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_empty_other_users_mailbox_as_user(app_client, test_db, user_headers): response = await app_client.delete( "/messages/inbox?userID=4", headers=user_headers["user"] @@ -713,7 +713,7 @@ async def test_empty_other_users_mailbox_as_user(app_client, test_db, user_heade # Attempt to empty user's outbox (moderator's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_empty_mailbox_as_mod( app_client, test_db, user_headers, dummy_users_data ): @@ -730,7 +730,7 @@ async def test_empty_mailbox_as_mod( # Attempt to empty another user's outbox (moderator's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_empty_other_users_mailbox_as_mod(app_client, test_db, user_headers): response = await app_client.delete( "/messages/outbox?userID=1", headers=user_headers["moderator"] @@ -742,7 +742,7 @@ async def test_empty_other_users_mailbox_as_mod(app_client, test_db, user_header # Attempt to empty user's threads mailbox (admin's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_empty_mailbox_as_admin( app_client, test_db, user_headers, dummy_users_data ): @@ -759,7 +759,7 @@ async def test_empty_mailbox_as_admin( # Attempt to empty another user's threads mailbox (admin's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_empty_other_users_mailbox_as_admin(app_client, test_db, user_headers): response = await app_client.delete( "/messages/threads?userID=5", headers=user_headers["admin"] @@ -771,7 +771,7 @@ async def test_empty_other_users_mailbox_as_admin(app_client, test_db, user_head # Attempt to empty user mailbox without user type -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_empty_mailbox_type_as_admin(app_client, test_db, user_headers): response = await app_client.delete("/messages/", headers=user_headers["admin"]) response_data = await response.get_json() @@ -781,7 +781,7 @@ async def test_empty_mailbox_type_as_admin(app_client, test_db, user_headers): # Attempt to empty user mailbox without user ID -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_empty_mailbox_id_as_admin(app_client, test_db, user_headers): response = await app_client.delete( "/messages/threads?userID=", headers=user_headers["admin"] diff --git a/tests/test_notifications.py b/tests/test_notifications.py index dcf9595d..68ad7d09 100644 --- a/tests/test_notifications.py +++ b/tests/test_notifications.py @@ -34,7 +34,7 @@ # Get New Notifications Route Tests ('/notifications', GET) # ------------------------------------------------------- # Attempt to get user notifications without auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_notifications_no_auth(app_client, test_db, user_headers): response = await app_client.get("/notifications") response_data = await response.get_json() @@ -44,7 +44,7 @@ async def test_get_notifications_no_auth(app_client, test_db, user_headers): # Attempt to get user notifications with malformed auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_notifications_malformed_auth(app_client, test_db, user_headers): response = await app_client.get("/notifications", headers=user_headers["malformed"]) response_data = await response.get_json() @@ -54,7 +54,7 @@ async def test_get_notifications_malformed_auth(app_client, test_db, user_header # Attempt to get user notifications with a user's JWT (silent refresh) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_silent_notifications_as_user( app_client, test_db, user_headers, dummy_users_data ): @@ -81,7 +81,7 @@ async def test_get_silent_notifications_as_user( # Attempt to get user notifications with a user's JWT (non-silent refresh) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_non_silent_notifications_as_user( app_client, test_db, user_headers, dummy_users_data ): @@ -108,7 +108,7 @@ async def test_get_non_silent_notifications_as_user( # Attempt to get user notifications with a mod's JWT (silent refresh) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_silent_notifications_as_mod( app_client, test_db, user_headers, dummy_users_data ): @@ -137,7 +137,7 @@ async def test_get_silent_notifications_as_mod( # Attempt to get user notifications with a mod's JWT (non-silent refresh) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_non_silent_notifications_as_mod( app_client, test_db, user_headers, dummy_users_data ): @@ -166,7 +166,7 @@ async def test_get_non_silent_notifications_as_mod( # Attempt to get user notifications with an admin's JWT (silently) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_silent_notifications_as_admin( app_client, test_db, user_headers, dummy_users_data ): @@ -195,7 +195,7 @@ async def test_get_silent_notifications_as_admin( # Attempt to get user notifications with an admin's JWT (non-silently) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_non_silent_notifications_as_admin( app_client, test_db, user_headers, dummy_users_data ): @@ -226,7 +226,7 @@ async def test_get_non_silent_notifications_as_admin( # Add New Push Subscription Route Tests ('/notifications', POST) # ------------------------------------------------------- # Attempt to create push subscription without auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_post_subscription_no_auth( app_client, test_db, user_headers, dummy_request_data ): @@ -240,7 +240,7 @@ async def test_post_subscription_no_auth( # Attempt to create push subscription with malformed auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_post_subscription_malformed_auth( app_client, test_db, user_headers, dummy_request_data ): @@ -256,7 +256,7 @@ async def test_post_subscription_malformed_auth( # Attempt to create push subscription with a user's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_post_subscription_as_user( app_client, test_db, user_headers, dummy_request_data ): @@ -273,7 +273,7 @@ async def test_post_subscription_as_user( # Attempt to create push subscription with a moderator's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_post_subscription_as_mod( app_client, test_db, user_headers, dummy_request_data ): @@ -290,7 +290,7 @@ async def test_post_subscription_as_mod( # Attempt to create push subscription with an admin's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_post_subscription_as_admin( app_client, test_db, user_headers, dummy_request_data ): @@ -307,7 +307,7 @@ async def test_post_subscription_as_admin( # Attempt to create push subscription with an admin's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_post_subscription_empty_data_as_admin(app_client, test_db, user_headers): response = await app_client.post( "/notifications", @@ -323,7 +323,7 @@ async def test_post_subscription_empty_data_as_admin(app_client, test_db, user_h # Update Push Subscription Route Tests ('/notifications/', PATCH) # ------------------------------------------------------- # Attempt to update push subscription without auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_subscription_no_auth( app_client, test_db, user_headers, dummy_request_data ): @@ -346,7 +346,7 @@ async def test_update_subscription_no_auth( # Attempt to update push subscription with malformed auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_subscription_malformed_auth( app_client, test_db, user_headers, dummy_request_data ): @@ -371,7 +371,7 @@ async def test_update_subscription_malformed_auth( # Attempt to update push subscription with a user's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_subscription_as_user( app_client, test_db, user_headers, dummy_request_data ): @@ -391,7 +391,7 @@ async def test_update_subscription_as_user( # Attempt to create push subscription with a moderator's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_subscription_as_mod( app_client, test_db, user_headers, dummy_request_data ): @@ -411,7 +411,7 @@ async def test_update_subscription_as_mod( # Attempt to create push subscription with an admin's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_subscription_as_admin( app_client, test_db, user_headers, dummy_request_data ): @@ -431,7 +431,7 @@ async def test_update_subscription_as_admin( # Attempt to create push subscription with an admin's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_subscription_empty_data_as_admin( app_client, test_db, user_headers ): diff --git a/tests/test_posts.py b/tests/test_posts.py index cb595025..36a14f4f 100644 --- a/tests/test_posts.py +++ b/tests/test_posts.py @@ -33,7 +33,7 @@ # Create Post Route Tests ('/posts', POST) # ------------------------------------------------------- # Attempt to create a post without auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_send_post_no_auth(app_client, test_db, user_headers, dummy_request_data): response = await app_client.post( "/posts", data=json.dumps(dummy_request_data["new_post"]) @@ -45,7 +45,7 @@ async def test_send_post_no_auth(app_client, test_db, user_headers, dummy_reques # Attempt to create a post with a malformed auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_send_post_malformed_auth( app_client, test_db, user_headers, dummy_request_data ): @@ -61,7 +61,7 @@ async def test_send_post_malformed_auth( # Attempt to create a post with a user's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_send_post_as_user( app_client, test_db, user_headers, dummy_request_data, dummy_users_data ): @@ -79,7 +79,7 @@ async def test_send_post_as_user( # Attempt to create a post with a moderator's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_send_post_as_mod( app_client, test_db, user_headers, dummy_request_data, dummy_users_data ): @@ -97,7 +97,7 @@ async def test_send_post_as_mod( # Attempt to create a post with an admin's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_send_post_as_admin( app_client, test_db, user_headers, dummy_request_data, dummy_users_data ): @@ -115,7 +115,7 @@ async def test_send_post_as_admin( # Attempt to create a post with a blocked user's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_send_post_as_blocked( app_client, test_db, user_headers, dummy_request_data, dummy_users_data ): @@ -133,7 +133,7 @@ async def test_send_post_as_blocked( # Update Post Route Tests ('/posts/', PATCH) # ------------------------------------------------------- # Attempt to update a post with no authorisation header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_post_no_auth( app_client, test_db, user_headers, dummy_request_data ): @@ -147,7 +147,7 @@ async def test_update_post_no_auth( # Attempt to update a post with a malformed auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_post_malformed_auth( app_client, test_db, user_headers, dummy_request_data ): @@ -163,7 +163,7 @@ async def test_update_post_malformed_auth( # Attempt to update the user's post (with same user's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_own_post_as_user( app_client, test_db, user_headers, dummy_request_data, dummy_users_data ): @@ -182,7 +182,7 @@ async def test_update_own_post_as_user( # Attempt to update another user's post (with user's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_other_users_post_as_user( app_client, test_db, user_headers, dummy_request_data, dummy_users_data ): @@ -199,7 +199,7 @@ async def test_update_other_users_post_as_user( # Attempt to update the moderator's post (with same moderator's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_own_post_as_mod( app_client, test_db, user_headers, dummy_request_data, dummy_users_data ): @@ -218,7 +218,7 @@ async def test_update_own_post_as_mod( # Attempt to update another user's post (with moderator's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_other_users_post_as_mod( app_client, test_db, user_headers, dummy_request_data, dummy_users_data ): @@ -237,7 +237,7 @@ async def test_update_other_users_post_as_mod( # Attempt to update the admin's post (with same admin's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_own_post_as_admin( app_client, test_db, user_headers, dummy_request_data, dummy_users_data ): @@ -256,7 +256,7 @@ async def test_update_own_post_as_admin( # Attempt to update another user's post (with admin's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_other_users_post_as_admin( app_client, test_db, user_headers, dummy_request_data, dummy_users_data ): @@ -275,7 +275,7 @@ async def test_update_other_users_post_as_admin( # Attempt to close the report on another user's post (with admin's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_other_users_post_report_as_admin( app_client, test_db, user_headers, dummy_request_data, dummy_users_data ): @@ -294,7 +294,7 @@ async def test_update_other_users_post_report_as_admin( # Attempt to update a post that doesn't exist (with admin's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_nonexistent_post_as_admin( app_client, test_db, user_headers, dummy_request_data, dummy_users_data ): @@ -310,7 +310,7 @@ async def test_update_nonexistent_post_as_admin( # Attempt to update a post without post ID (with admin's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_post_no_id_as_admin( app_client, test_db, user_headers, dummy_request_data, dummy_users_data ): @@ -328,7 +328,7 @@ async def test_update_post_no_id_as_admin( # Send a Hug for post Tests ('/posts//hugs', POST) # ------------------------------------------------------- # Attempt to send hugs for post you already sent hugs for -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_post_hugs_given_duplicate_hugs(app_client, test_db, user_headers): response = await app_client.post("/posts/1/hugs", headers=user_headers["admin"]) response_data = await response.get_json() @@ -338,7 +338,7 @@ async def test_post_hugs_given_duplicate_hugs(app_client, test_db, user_headers) # Attempt to send hugs for a post that doesn't exist -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_post_hugs_post_no_existing(app_client, test_db, user_headers): response = await app_client.post("/posts/1000/hugs", headers=user_headers["admin"]) response_data = await response.get_json() @@ -348,7 +348,7 @@ async def test_post_hugs_post_no_existing(app_client, test_db, user_headers): # Attempt to send hugs -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_post_hugs(app_client, test_db, user_headers): response = await app_client.post("/posts/1/hugs", headers=user_headers["moderator"]) response_data = await response.get_json() @@ -361,7 +361,7 @@ async def test_post_hugs(app_client, test_db, user_headers): # Delete Post Route Tests ('/posts/', DELETE) # ------------------------------------------------------- # Attempt to delete a post with no authorisation header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_post_no_auth(app_client, test_db, user_headers): response = await app_client.delete("/posts/3") response_data = await response.get_json() @@ -371,7 +371,7 @@ async def test_delete_post_no_auth(app_client, test_db, user_headers): # Attempt to delete a post with a malformed auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_post_malformed_auth(app_client, test_db, user_headers): response = await app_client.delete("/posts/3", headers=user_headers["malformed"]) response_data = await response.get_json() @@ -392,7 +392,7 @@ async def test_delete_post_malformed_auth(app_client, test_db, user_headers): (23, "admin"), ], ) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_own_post(app_client, test_db, user_headers, post_id, user): response = await app_client.delete(f"/posts/{post_id}", headers=user_headers[user]) response_data = await response.get_json() @@ -403,7 +403,7 @@ async def test_delete_own_post(app_client, test_db, user_headers, post_id, user) # Attempt to delete another user's post (with user's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_other_users_post_as_user(app_client, test_db, user_headers): response = await app_client.delete("/posts/12", headers=user_headers["user"]) response_data = await response.get_json() @@ -413,7 +413,7 @@ async def test_delete_other_users_post_as_user(app_client, test_db, user_headers # Attempt to delete another user's post (with moderator's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_other_users_post_as_mod(app_client, test_db, user_headers): response = await app_client.delete("/posts/25", headers=user_headers["moderator"]) response_data = await response.get_json() @@ -423,7 +423,7 @@ async def test_delete_other_users_post_as_mod(app_client, test_db, user_headers) # Attempt to delete another user's post (with admin's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_other_users_post_as_admin(app_client, test_db, user_headers): response = await app_client.delete("/posts/1", headers=user_headers["admin"]) response_data = await response.get_json() @@ -434,7 +434,7 @@ async def test_delete_other_users_post_as_admin(app_client, test_db, user_header # Attempt to delete a post that doesn't exist (with admin's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_nonexistent_post_as_admin(app_client, test_db, user_headers): response = await app_client.delete("/posts/100", headers=user_headers["admin"]) response_data = await response.get_json() @@ -444,7 +444,7 @@ async def test_delete_nonexistent_post_as_admin(app_client, test_db, user_header # Attempt to delete a post without post ID (with admin's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_post_no_id_as_admin(app_client, test_db, user_headers): response = await app_client.delete("/posts/", headers=user_headers["admin"]) response_data = await response.get_json() @@ -463,7 +463,7 @@ async def test_delete_post_no_id_as_admin(app_client, test_db, user_headers): ("suggested"), ], ) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_full_posts_page_1(app_client, test_db, post_type): response = await app_client.get(f"/posts/{post_type}") response_data = await response.get_json() @@ -482,7 +482,7 @@ async def test_get_full_posts_page_1(app_client, test_db, post_type): ("suggested"), ], ) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_full_posts_page_2(app_client, test_db, post_type): response = await app_client.get(f"/posts/{post_type}?page=2") response_data = await response.get_json() diff --git a/tests/test_reports.py b/tests/test_reports.py index bbc8450e..3429ea57 100644 --- a/tests/test_reports.py +++ b/tests/test_reports.py @@ -33,7 +33,7 @@ # Get Open Reports Tests ('/reports', GET) # ------------------------------------------------------- # Attempt to get open reports without auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_open_reports_no_auth(app_client, test_db, user_headers): response = await app_client.get("/reports") response_data = await response.get_json() @@ -43,7 +43,7 @@ async def test_get_open_reports_no_auth(app_client, test_db, user_headers): # Attempt to get open reports with malformed auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_open_reports_malformed_auth(app_client, test_db, user_headers): response = await app_client.get("/reports", headers=user_headers["malformed"]) response_data = await response.get_json() @@ -53,7 +53,7 @@ async def test_get_open_reports_malformed_auth(app_client, test_db, user_headers # Attempt to get open reports with a user's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_open_reports_as_user(app_client, test_db, user_headers): response = await app_client.get("/reports", headers=user_headers["user"]) response_data = await response.get_json() @@ -63,7 +63,7 @@ async def test_get_open_reports_as_user(app_client, test_db, user_headers): #  Attempt to get open reports with a moderator's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_open_reports_as_mod(app_client, test_db, user_headers): response = await app_client.get("/reports", headers=user_headers["moderator"]) response_data = await response.get_json() @@ -73,7 +73,7 @@ async def test_get_open_reports_as_mod(app_client, test_db, user_headers): # Attempt to get open reports with an admin's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_open_reports_as_admin(app_client, test_db, user_headers): response = await app_client.get("/reports", headers=user_headers["admin"]) response_data = await response.get_json() @@ -89,7 +89,7 @@ async def test_get_open_reports_as_admin(app_client, test_db, user_headers): # Create Report Route Tests ('/reports', POST) # ------------------------------------------------------- # Attempt to create a report with no authorisation header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_send_report_no_auth( app_client, test_db, user_headers, dummy_request_data ): @@ -103,7 +103,7 @@ async def test_send_report_no_auth( # Attempt to create a report with a malformed auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_send_report_malformed_auth( app_client, test_db, user_headers, dummy_request_data ): @@ -119,7 +119,7 @@ async def test_send_report_malformed_auth( # Attempt to create a report with a user's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_send_report_as_user( app_client, test_db, user_headers, dummy_request_data, dummy_users_data ): @@ -140,7 +140,7 @@ async def test_send_report_as_user( # Attempt to create a report with a moderator's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_send_report_as_mod( app_client, test_db, user_headers, dummy_request_data, dummy_users_data ): @@ -161,7 +161,7 @@ async def test_send_report_as_mod( # Attempt to create a report with an admin's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_send_report_as_admin( app_client, test_db, user_headers, dummy_request_data, dummy_users_data ): @@ -182,7 +182,7 @@ async def test_send_report_as_admin( # Attempt to create a post report without post ID with an admin's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_send_malformed_report_as_admin( app_client, test_db, user_headers, dummy_request_data, dummy_users_data ): @@ -200,7 +200,7 @@ async def test_send_malformed_report_as_admin( # Attempt to create a post report for post that doesn't exist -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_send_report_nonexistent_post_as_admin( app_client, test_db, user_headers, dummy_request_data, dummy_users_data ): @@ -218,7 +218,7 @@ async def test_send_report_nonexistent_post_as_admin( # Attempt to create a report with an admin's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_send_user_report_as_admin( app_client, test_db, user_headers, dummy_request_data, dummy_users_data ): @@ -238,7 +238,7 @@ async def test_send_user_report_as_admin( # Attempt to create a report for user that doesn't exist -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_send_user_report_nonexistent_user_as_admin( app_client, test_db, user_headers, dummy_request_data, dummy_users_data ): @@ -257,7 +257,7 @@ async def test_send_user_report_nonexistent_user_as_admin( # Update Report Route Tests ('/reports/', PATCH) # ------------------------------------------------------- # Attempt to update a report with no authorisation header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_report_no_auth( app_client, test_db, user_headers, dummy_request_data ): @@ -271,7 +271,7 @@ async def test_update_report_no_auth( # Attempt to update a report with a malformed auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_report_malformed_auth( app_client, test_db, user_headers, dummy_request_data ): @@ -287,7 +287,7 @@ async def test_update_report_malformed_auth( # Attempt to update a report (with user's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_report_as_user( app_client, test_db, user_headers, dummy_request_data, dummy_users_data ): @@ -305,7 +305,7 @@ async def test_update_report_as_user( # Attempt to update a report (with moderator's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_report_as_mod( app_client, test_db, user_headers, dummy_request_data, dummy_users_data ): @@ -323,7 +323,7 @@ async def test_update_report_as_mod( # Attempt to update a report (with admin's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_report_as_admin( app_client, test_db, user_headers, dummy_request_data, dummy_users_data ): @@ -346,7 +346,7 @@ async def test_update_report_as_admin( # Attempt to update a report (with admin's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_user_report_as_admin( app_client, test_db, user_headers, dummy_request_data, dummy_users_data ): @@ -369,7 +369,7 @@ async def test_update_user_report_as_admin( # Attempt to update a report with no ID (with admin's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_no_id_report_as_admin( app_client, test_db, user_headers, dummy_request_data, dummy_users_data ): @@ -390,7 +390,7 @@ async def test_update_no_id_report_as_admin( # Attempt to update a report that doesn't exist (with admin's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_nonexistent_report_as_admin( app_client, test_db, user_headers, dummy_request_data, dummy_users_data ): diff --git a/tests/test_users.py b/tests/test_users.py index 09bdc617..a36331a9 100644 --- a/tests/test_users.py +++ b/tests/test_users.py @@ -33,7 +33,7 @@ # Get Users by Type Tests ('/users/', GET) # ------------------------------------------------------- # Attempt to get list of users without auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_user_list_no_auth(app_client, test_db, user_headers): response = await app_client.get("/users/blocked") response_data = await response.get_json() @@ -54,7 +54,7 @@ async def test_get_user_list_no_auth(app_client, test_db, user_headers): ("moderator", 403), ], ) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_user_list_auth_error( app_client, test_db, user_headers, user, error_code ): @@ -66,7 +66,7 @@ async def test_get_user_list_auth_error( # Attempt to get list of users with admin's auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_user_list_as_admin(app_client, test_db, user_headers): response = await app_client.get("/users/blocked", headers=user_headers["admin"]) response_data = await response.get_json() @@ -77,7 +77,7 @@ async def test_get_user_list_as_admin(app_client, test_db, user_headers): # Attempt to get list of users with admin's auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_user_list_unsupported_type(app_client, test_db, user_headers): response = await app_client.get("/users/meow", headers=user_headers["admin"]) response_data = await response.get_json() @@ -89,7 +89,7 @@ async def test_get_user_list_unsupported_type(app_client, test_db, user_headers) # Get User Data Tests ('/users/all/', GET) # ------------------------------------------------------- # Attempt to get a user's data without auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_user_data_no_auth(app_client, test_db, user_headers): response = await app_client.get("/users/all/1") response_data = await response.get_json() @@ -99,7 +99,7 @@ async def test_get_user_data_no_auth(app_client, test_db, user_headers): # Attempt to get a user's data with malformed auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_user_data_malformed_auth(app_client, test_db, user_headers): response = await app_client.get("/users/all/1", headers=user_headers["malformed"]) response_data = await response.get_json() @@ -109,7 +109,7 @@ async def test_get_user_data_malformed_auth(app_client, test_db, user_headers): # Attempt to get a user's data with a user's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_user_data_as_user( app_client, test_db, user_headers, dummy_users_data ): @@ -125,7 +125,7 @@ async def test_get_user_data_as_user( # Attempt to get a user's data with a moderator's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_user_data_as_mod( app_client, test_db, user_headers, dummy_users_data ): @@ -142,7 +142,7 @@ async def test_get_user_data_as_mod( # Attempt to get a user's data with an admin's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_user_data_as_admin( app_client, test_db, user_headers, dummy_users_data ): @@ -159,7 +159,7 @@ async def test_get_user_data_as_admin( # Attempt to get a nonexistent user's data (with admin's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_nonexistent_user_as_admin(app_client, test_db, user_headers): response = await app_client.get("/users/all/100", headers=user_headers["admin"]) response_data = await response.get_json() @@ -169,7 +169,7 @@ async def test_get_nonexistent_user_as_admin(app_client, test_db, user_headers): # Attempt to get a user's data with no ID (with admin's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_user_no_id_as_admin(app_client, test_db, user_headers): response = await app_client.get("/users/all/", headers=user_headers["admin"]) response_data = await response.get_json() @@ -181,7 +181,7 @@ async def test_get_user_no_id_as_admin(app_client, test_db, user_headers): # Create User Tests ('/users', POST) # ------------------------------------------------------- # Attempt to create a user without auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_create_user_no_auth( app_client, test_db, user_headers, dummy_request_data ): @@ -195,7 +195,7 @@ async def test_create_user_no_auth( # Attempt to create a user with malformed auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_create_user_malformed_auth( app_client, test_db, user_headers, dummy_request_data ): @@ -211,7 +211,7 @@ async def test_create_user_malformed_auth( # Attempt to create a user with user's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_create_user_as_user( app_client, test_db, user_headers, dummy_request_data ): @@ -227,7 +227,7 @@ async def test_create_user_as_user( # Attempt to create a user with moderator's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_create_user_as_moderator( app_client, test_db, user_headers, dummy_request_data ): @@ -243,7 +243,7 @@ async def test_create_user_as_moderator( # Attempt to create a user with admin's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_create_user_as_damin( app_client, test_db, user_headers, dummy_request_data ): @@ -263,7 +263,7 @@ async def test_create_user_as_damin( # is done automatically, it's no longer needed, but in case of an error # adjusting a user's roles, it's important to make sure they still # can't create other users -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_create_different_user_as_new_user( app_client, test_db, user_headers, dummy_request_data ): @@ -281,7 +281,7 @@ async def test_create_different_user_as_new_user( # Edit User Data Tests ('/users/all/', PATCH) # ------------------------------------------------------- # Attempt to update a user's data without auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_user_no_auth( app_client, test_db, user_headers, dummy_request_data ): @@ -295,7 +295,7 @@ async def test_update_user_no_auth( # Attempt to update a user's data with malformed auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_user_malformed_auth( app_client, test_db, user_headers, dummy_request_data ): @@ -311,7 +311,7 @@ async def test_update_user_malformed_auth( # Attempt to update a user's data with a user's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_user_as_user( app_client, test_db, user_headers, dummy_users_data, dummy_request_data ): @@ -332,7 +332,7 @@ async def test_update_user_as_user( # Attempt to update another user's display name with a user's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_other_users_display_name_as_user( app_client, test_db, user_headers, dummy_users_data, dummy_request_data ): @@ -350,12 +350,13 @@ async def test_update_other_users_display_name_as_user( # Attempt to update a user's blocked state with a user's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_block_user_as_user( app_client, test_db, user_headers, dummy_users_data, dummy_request_data ): user = dummy_request_data["updated_unblock_user"] user["id"] = dummy_users_data["user"]["internal"] + user["blocked"] = True response = await app_client.patch( f"/users/all/{dummy_users_data['user']['internal']}", headers=user_headers["user"], @@ -368,7 +369,7 @@ async def test_update_block_user_as_user( # Attempt to update a user's data with a moderator's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_user_as_mod( app_client, test_db, user_headers, dummy_users_data, dummy_request_data ): @@ -389,7 +390,7 @@ async def test_update_user_as_mod( # Attempt to update another user's display name with a moderator's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_other_users_display_name_as_mod( app_client, test_db, user_headers, dummy_users_data, dummy_request_data ): @@ -407,12 +408,13 @@ async def test_update_other_users_display_name_as_mod( # Attempt to update a user's blocked state with a moderator's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_block_user_as_mod( app_client, test_db, user_headers, dummy_users_data, dummy_request_data ): user = dummy_request_data["updated_unblock_user"] user["id"] = dummy_users_data["moderator"]["internal"] + user["blocked"] = True response = await app_client.patch( f"/users/all/{dummy_users_data['moderator']['internal']}", headers=user_headers["moderator"], @@ -425,7 +427,7 @@ async def test_update_block_user_as_mod( # Attempt to update a user's data with an admin's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_user_as_admin( app_client, test_db, user_headers, dummy_users_data, dummy_request_data ): @@ -446,7 +448,7 @@ async def test_update_user_as_admin( # Attempt to update another user's display name with an admin's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_other_user_as_admin( app_client, test_db, user_headers, dummy_users_data, dummy_request_data ): @@ -467,7 +469,7 @@ async def test_update_other_user_as_admin( # Attempt to update a user's blocked state with an admin's JWT -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_block_user_as_admin( app_client, test_db, user_headers, dummy_users_data, dummy_request_data ): @@ -487,7 +489,7 @@ async def test_update_block_user_as_admin( # Attempt to update another user's settings (admin's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_user_settings_as_admin( app_client, test_db, user_headers, dummy_users_data, dummy_request_data ): @@ -507,7 +509,7 @@ async def test_update_user_settings_as_admin( # Attempt to update a user's data with no ID (with admin's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_no_id_user_as_admin( app_client, test_db, user_headers, dummy_request_data ): @@ -523,7 +525,7 @@ async def test_update_no_id_user_as_admin( # Attempt to update another user's settings (admin's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_admin_settings_as_admin( app_client, test_db, user_headers, dummy_users_data, dummy_request_data ): @@ -548,7 +550,7 @@ async def test_update_admin_settings_as_admin( # Attempt to update another user's settings (admin's JWT) - 0 refresh rate -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_update_admin_settings_as_admin_invalid_settings( app_client, test_db, user_headers, dummy_users_data, dummy_request_data ): @@ -578,7 +580,7 @@ async def test_update_admin_settings_as_admin_invalid_settings( # Get User's Posts Tests ('/users/all//posts', GET) # ------------------------------------------------------- # Attempt to get a user's posts without auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_user_posts_no_auth(app_client, test_db, user_headers): response = await app_client.get("/users/all/1/posts") response_data = await response.get_json() @@ -588,7 +590,7 @@ async def test_get_user_posts_no_auth(app_client, test_db, user_headers): # Attempt to get a user's posts with malformed auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_user_posts_malformed_auth(app_client, test_db, user_headers): response = await app_client.get( "/users/all/1/posts", headers=user_headers["malformed"] @@ -600,6 +602,7 @@ async def test_get_user_posts_malformed_auth(app_client, test_db, user_headers): # Attempt to get a user's posts with +@pytest.mark.asyncio(scope="session") @pytest.mark.parametrize( "user_id, user, total_pages, posts_num", [ @@ -611,7 +614,7 @@ async def test_get_user_posts_malformed_auth(app_client, test_db, user_headers): (5, "admin", 1, 2), ], ) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_get_user_posts( app_client, test_db, user_headers, user_id, user, total_pages, posts_num ): @@ -630,7 +633,7 @@ async def test_get_user_posts( # Delete User's Posts Route Tests ('/users/all//posts', DELETE) # ------------------------------------------------------- # Attempt to delete user's posts with no authorisation header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_posts_no_auth(app_client, test_db, user_headers): response = await app_client.delete("/users/all/1/posts") response_data = await response.get_json() @@ -640,7 +643,7 @@ async def test_delete_posts_no_auth(app_client, test_db, user_headers): # Attempt to delete user's with a malformed auth header -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_posts_malformed_auth(app_client, test_db, user_headers): response = await app_client.delete( "/users/all/1/posts", headers=user_headers["malformed"] @@ -663,7 +666,7 @@ async def test_delete_posts_malformed_auth(app_client, test_db, user_headers): (4, "admin", 14), ], ) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_own_posts( app_client, test_db, user_headers, user_id, user, deleted_post ): @@ -693,7 +696,7 @@ async def test_delete_own_posts( (1, "moderator"), ], ) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_other_users_posts_no_permission( app_client, test_db, user_headers, user_id, user ): @@ -707,7 +710,7 @@ async def test_delete_other_users_posts_no_permission( # Attempt to delete another user's posts (with admin's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_other_users_posts_as_admin(app_client, test_db, user_headers): response = await app_client.delete( "/users/all/5/posts", headers=user_headers["admin"] @@ -720,7 +723,7 @@ async def test_delete_other_users_posts_as_admin(app_client, test_db, user_heade # Attempt to delete the posts of a user that doesn't exist (admin's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_nonexistent_users_posts_as_admin( app_client, test_db, user_headers ): @@ -734,7 +737,7 @@ async def test_delete_nonexistent_users_posts_as_admin( # Attempt to delete the posts of a user that has no posts (admin's JWT) -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_delete_nonexistent_posts_as_admin(app_client, test_db, user_headers): response = await app_client.delete( "/users/all/9/posts", headers=user_headers["admin"] @@ -748,7 +751,7 @@ async def test_delete_nonexistent_posts_as_admin(app_client, test_db, user_heade # Send a Hug for user Tests ('/users/all//hugs', POST) # ------------------------------------------------------- # Attempt to send hugs for a post that doesn't exist -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_user_hugs_post_no_existing(app_client, test_db, user_headers): response = await app_client.post( "/users/all/1000/hugs", headers=user_headers["admin"] @@ -760,7 +763,7 @@ async def test_user_hugs_post_no_existing(app_client, test_db, user_headers): # Attempt to send hugs -@pytest.mark.asyncio +@pytest.mark.asyncio(scope="session") async def test_user_hugs(app_client, test_db, user_headers): response = await app_client.post( "/users/all/1/hugs", headers=user_headers["moderator"]