diff --git a/apps/archive/archive.py b/apps/archive/archive.py index 62b086d82..242fb8820 100644 --- a/apps/archive/archive.py +++ b/apps/archive/archive.py @@ -868,13 +868,18 @@ def update(self, id, updates, original): # send signal # TODO-ASYNC: Support async signals - signals.item_update.send(self, updates=updates, original=original) + superdesk_testing = get_app_config("SUPERDESK_TESTING", False) + if not superdesk_testing: + signals.item_update.send(self, updates=updates, original=original) super().update(id, updates, original) updated = copy(original) updated.update(updates) - signals.item_updated.send(self, item=updated, original=original) + + # TODO-ASYNC: Support async signals + if not superdesk_testing: + signals.item_updated.send(self, item=updated, original=original) if "marked_for_user" in updates: # TODO-ASYNC: Support async (see superdesk.tests.markers.requires_eve_resource_async_event) diff --git a/superdesk/tests/__init__.py b/superdesk/tests/__init__.py index 13d452283..eda175af2 100644 --- a/superdesk/tests/__init__.py +++ b/superdesk/tests/__init__.py @@ -8,7 +8,7 @@ # AUTHORS and LICENSE files distributed with this source code, or # at https://www.sourcefabric.org/superdesk/license -from typing import Dict, Any +from typing import Dict, Any, Tuple import os import functools import logging @@ -17,11 +17,11 @@ from dataclasses import dataclass from copy import deepcopy -from base64 import b64encode from unittest.mock import patch from unittest import IsolatedAsyncioTestCase from quart import Response from quart.testing import QuartClient +from werkzeug.datastructures import Authorization from superdesk.core import json from superdesk.flask import Config @@ -35,6 +35,7 @@ from superdesk.core.resources import ResourceModel from superdesk.storage.amazon_media_storage import AmazonMediaStorage from superdesk.storage.proxy import ProxyMediaStorage +from superdesk.types import User logger = logging.getLogger(__name__) @@ -362,7 +363,7 @@ def inner(*a, **kw): async def setup(context=None, config=None, app_factory=get_app, reset=False, auto_add_apps: bool = True): - if not hasattr(setup, "app") or setup.reset or config: # type: ignore[attr-defined] + if not hasattr(setup, "app") or hasattr(setup, "reset") or config: # type: ignore[attr-defined] if hasattr(setup, "app"): # Close all PyMongo Connections (new ones will be created with ``app_factory`` call) for key, val in setup.app.extensions["pymongo"].items(): @@ -393,12 +394,28 @@ async def setup_auth_user(context, user=None): await setup_db_user(context, user) -def add_to_context(context, token, user, auth_id=None): - context.headers.append(("Authorization", b"basic " + b64encode(token + b":"))) +def token_to_basic_auth_header(token: str) -> Tuple[str, str]: + """ + Use werkzeug's Authorization to create a valid basic auth token. This way we don't + need to know how quart/werkzeug handles it convertion to string internally + """ + basic_auth = Authorization("basic", data=dict(username=token, password="")) + return ("Authorization", basic_auth.to_header()) # type: ignore[attr-defined] + + +def add_user_info_to_context(context: Any, token: str, user: User, auth_id=None): + """ + Add current user's session information to context headers. + It converts the plain string token into a valid basic auth token that + will be converted back to string (internal) by quart/werkzeug Request. + """ + basic_token_header = token_to_basic_auth_header(token) + context.headers.append(basic_token_header) if getattr(context, "user", None): context.previous_user = context.user context.user = user + set_placeholder(context, "CONTEXT_USER_ID", str(user.get("_id"))) set_placeholder(context, "AUTH_ID", str(auth_id)) @@ -443,9 +460,9 @@ async def setup_db_user(context, user): ) auth_data = json.loads(await auth_response.get_data()) - token = auth_data.get("token").encode("ascii") + token = auth_data.get("token") auth_id = auth_data.get("_id") - add_to_context(context, token, user, auth_id) + add_user_info_to_context(context, token, user, auth_id) def setup_ad_user(context, user): @@ -492,7 +509,7 @@ def setup_ad_user(context, user): token = auth_response_as_json.get("token").encode("ascii") ad_user["_id"] = auth_response_as_json["user"] - add_to_context(context, token, ad_user) + add_user_info_to_context(context, token, ad_user) class NotificationMock: diff --git a/superdesk/tests/steps.py b/superdesk/tests/steps.py index cf26adfbe..ba4211bd7 100644 --- a/superdesk/tests/steps.py +++ b/superdesk/tests/steps.py @@ -23,7 +23,7 @@ from datetime import datetime, timedelta, date from os.path import basename from re import findall -from unittest.mock import patch +from inspect import isawaitable from urllib.parse import urlparse from pathlib import Path @@ -68,7 +68,7 @@ async def expect_status(response, code): - assert int(code) == response.status_code, "exptected {expected}, got {code}, reason={reason}".format( + assert int(code) == response.status_code, "expected {expected}, got {code}, reason={reason}".format( code=response.status_code, expected=code, reason=(await response.get_data()).decode("utf-8"), @@ -76,9 +76,12 @@ async def expect_status(response, code): async def expect_status_in(response, codes): + if isawaitable(response): + response = await response + assert response.status_code in [ int(code) for code in codes - ], "exptected on of {expected}, got {code}, reason={reason}".format( + ], "expected on of {expected}, got {code}, reason={reason}".format( code=response.status_code, expected=codes, reason=(await response.get_data()).decode("utf-8"), @@ -1445,6 +1448,11 @@ async def step_impl_then_get_nofield_in_path(context, path): @then("we get existing resource") @async_run_until_complete +async def step_impl_then_get_existing_resource(context): + # split into separate function so can be called/awaited independently + await step_impl_then_get_existing(context) + + async def step_impl_then_get_existing(context): await assert_200(context.response) print("got", get_response_readable(await context.response.get_data())) @@ -1952,7 +1960,8 @@ async def we_reset_password_for_user(context): @when("we switch user") -def when_we_switch_user(context): +@async_run_until_complete +async def when_we_switch_user(context): user = { "username": "test-user-2", "password": "pwd", @@ -1960,13 +1969,14 @@ def when_we_switch_user(context): "needs_activation": False, "sign_off": "foo", } - tests.setup_auth_user(context, user) + await tests.setup_auth_user(context, user) set_placeholder(context, "USERS_ID", str(context.user["_id"])) @when("we setup test user") -def when_we_setup_test_user(context): - tests.setup_auth_user(context, tests.test_user) +@async_run_until_complete +async def when_we_setup_test_user(context): + await tests.setup_auth_user(context, tests.test_user) @when('we get my "{url}"') @@ -2285,7 +2295,7 @@ async def then_we_get_activity(context): set_placeholder(context, "USERS_ID", item["user"]) -def login_as(context, username, password, user_type): +async def login_as(context, username, password, user_type): user = { "username": username, "password": password, @@ -2298,17 +2308,19 @@ def login_as(context, username, password, user_type): if context.text: user.update(json.loads(context.text)) - tests.setup_auth_user(context, user) + await tests.setup_auth_user(context, user) @given('we login as user "{username}" with password "{password}" and user type "{user_type}"') -def given_we_login_as_user(context, username, password, user_type): - login_as(context, username, password, user_type) +@async_run_until_complete +async def given_we_login_as_user(context, username, password, user_type): + await login_as(context, username, password, user_type) @when('we login as user "{username}" with password "{password}" and user type "{user_type}"') -def when_we_login_as_user(context, username, password, user_type): - login_as(context, username, password, user_type) +@async_run_until_complete +async def when_we_login_as_user(context, username, password, user_type): + await login_as(context, username, password, user_type) def is_user_resource(resource):