Skip to content

Commit ccccc3a

Browse files
authored
[SDESK-7497] Small adjustments for superdesk-planning behave tests (#2853)
* Small adjustments for superdesk-planning behave tests SDESK-7497 * Allow empty async resources SDESK-7497 * Add missing context block * Rollback workaround as it won't be needed Some newer implementation of the publishing engine will most likely solve the problem * Fix mypy error and remove comment
1 parent 2681c75 commit ccccc3a

File tree

5 files changed

+83
-14
lines changed

5 files changed

+83
-14
lines changed

apps/publish/content/common.py

+2
Original file line numberDiff line numberDiff line change
@@ -242,9 +242,11 @@ def update(self, id, updates, original):
242242
# race condition.
243243
published_service = get_resource_service(PUBLISHED)
244244
assert published_service is not None
245+
245246
published_service.patch(id, {QUEUE_STATE: PUBLISH_STATE.PUSHED})
246247
from apps.publish.enqueue import push_publish
247248

249+
# TODO-ASYNC: update this service so task below completes eagearly and tests pass
248250
push_publish.apply_async(str(id))
249251

250252
push_notification(

superdesk/core/resources/resource_rest_endpoints.py

+25-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
# at https://www.sourcefabric.org/superdesk/license
1010

1111
import math
12+
import logging
13+
import traceback
14+
1215
from typing import Optional, cast, Any, Literal
1316
from copy import deepcopy
1417

@@ -44,6 +47,9 @@
4447
from .utils import combine_projection_args
4548

4649

50+
logger = logging.getLogger("superdesk")
51+
52+
4753
@dataclass
4854
class RestParentLink:
4955
#: Name of the resource this parent link belongs to
@@ -368,7 +374,7 @@ async def create_item(self, request: Request) -> Response:
368374
issues = []
369375
return_code = 201
370376
for value in deepcopy(payload):
371-
# Validate the provided item,
377+
# Validate the provided item
372378
try:
373379
for parent_link in self.endpoint_config.parent_links or []:
374380
parent_item = parent_items.get(parent_link.resource_name)
@@ -385,6 +391,8 @@ async def create_item(self, request: Request) -> Response:
385391
ISSUES: get_field_errors_from_pydantic_validation_error(validation_error),
386392
}
387393
)
394+
self._log_traceback(validation_error, "Validation error while creating item")
395+
388396
except SuperdeskError as superdesk_error:
389397
return_code = superdesk_error.status_code
390398
# Let the Superdesk exception populate the issue dictionary
@@ -402,6 +410,7 @@ async def create_item(self, request: Request) -> Response:
402410
ISSUES: {"exception": str(exception)},
403411
}
404412
)
413+
self._log_traceback(exception, "Unexpected exception while creating item")
405414

406415
if self.endpoint_config.exclude_fields_in_response:
407416
# If projection is enabled, we fetch all newly created items with projection applied
@@ -413,7 +422,14 @@ async def create_item(self, request: Request) -> Response:
413422
results: list[dict]
414423
results = [
415424
{
416-
**self._populate_item_hateoas(request, model_instance.to_dict()),
425+
**self._populate_item_hateoas(
426+
request,
427+
# make sure to include unset and default values
428+
model_instance.to_dict(
429+
exclude_unset=False,
430+
exclude_defaults=False,
431+
),
432+
),
417433
STATUS: STATUS_OK,
418434
}
419435
for model_instance in model_instances
@@ -499,6 +515,8 @@ async def update_item(
499515
except ValidationError as validation_error:
500516
return_code = 400
501517
issues = get_field_errors_from_pydantic_validation_error(validation_error)
518+
self._log_traceback(validation_error, "Validation error while updating item")
519+
502520
except SuperdeskError as superdesk_error:
503521
return_code = superdesk_error.status_code
504522
# Let the Superdesk exception populate the issue dictionary
@@ -730,3 +748,8 @@ async def on_fetched_item(self, request: Request, doc: dict) -> None:
730748

731749
async def on_fetched(self, request: Request, doc: RestGetResponse) -> None:
732750
pass
751+
752+
def _log_traceback(self, exception: Exception, message: str = "Exception occurred") -> None:
753+
"""Logs the exception message and the full traceback."""
754+
logger.warn(f"{message}: {exception}")
755+
logger.warn(traceback.format_exc())

superdesk/tests/__init__.py

+15
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,9 @@ async def setup(context=None, config=None, app_factory=get_app, reset=False, aut
403403

404404
if context:
405405
context.app = app
406+
app.test_client_class = TestClient
406407
context.client = app.test_client()
408+
407409
if not hasattr(context, "BEHAVE") and not hasattr(context, "test_context"):
408410
context.test_context = app.test_request_context("/")
409411
await context.test_context.push()
@@ -434,6 +436,10 @@ def add_user_info_to_context(context: Any, token: str, user: User, auth_id=None)
434436
will be converted back to string (internal) by quart/werkzeug Request.
435437
"""
436438
basic_token_header = token_to_basic_auth_header(token)
439+
440+
# remove any existing authorization header. Updates the list (in-place)
441+
# to preserve any references to context.headers elsewhere
442+
context.headers[:] = [h for h in context.headers if h[0] != "Authorization"]
437443
context.headers.append(basic_token_header)
438444

439445
if getattr(context, "user", None):
@@ -607,6 +613,15 @@ def assertDictContains(self, source: dict, contains: dict):
607613

608614

609615
class TestClient(QuartClient):
616+
async def open(self, *args, **kwargs) -> Response:
617+
"""
618+
Appends the request path to the response object for later debugging
619+
"""
620+
621+
response = await super().open(*args, **kwargs)
622+
response.request_path = kwargs.get("path", args[0] if args else "/") # type: ignore[attr-defined]
623+
return response
624+
610625
def model_instance_to_json(self, model_instance: ResourceModel):
611626
return model_instance.to_dict(mode="json")
612627

superdesk/tests/publish_steps.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@
2828

2929

3030
@when("we enqueue published")
31-
def step_impl_when_auth(context):
32-
enqueue_published.apply_async()
31+
@async_run_until_complete
32+
async def step_impl_when_auth(context):
33+
await enqueue_published.apply_async()
3334

3435

3536
@then("we get formatted item")

superdesk/tests/steps.py

+38-10
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010

1111

1212
import os
13+
import io
1314
import time
15+
from typing import Any
1416
import arrow
1517
import celery
1618
import shutil
@@ -20,8 +22,9 @@
2022
from unittest import mock
2123
from copy import deepcopy
2224
from base64 import b64encode
23-
from datetime import datetime, timedelta, date
2425
from os.path import basename
26+
from datetime import datetime, timedelta, date
27+
from quart.datastructures import FileStorage
2528
from re import findall
2629
from inspect import isawaitable
2730
from urllib.parse import urlparse
@@ -82,10 +85,11 @@ async def expect_status_in(response, codes):
8285

8386
assert response.status_code in [
8487
int(code) for code in codes
85-
], "expected on of {expected}, got {code}, reason={reason}".format(
88+
], "expected one of {expected}, got {code}, reason={reason}, url='{requested_path}'".format(
8689
code=response.status_code,
8790
expected=codes,
8891
reason=(await response.get_data()).decode("utf-8"),
92+
requested_path=response.request_path,
8993
)
9094

9195

@@ -491,12 +495,25 @@ def format_items(items):
491495
return ",\n".join(output)
492496

493497

498+
async def delete_entries_for(context, resource: str) -> None:
499+
"""
500+
Attempts to remove all items from the resources MongoDB and/or Elastic.
501+
First tries with async, otherwise it falls back to sync resources.
502+
"""
503+
504+
async with context.app.test_request_context(context.app.config["URL_PREFIX"]):
505+
try:
506+
async_app = context.app.async_app
507+
await async_app.resources.get_resource_service(resource).delete_many({})
508+
except KeyError:
509+
get_resource_service(resource).delete_action()
510+
511+
494512
@given('empty "{resource}"')
495513
@async_run_until_complete
496514
async def step_impl_given_empty(context, resource):
497515
if not is_user_resource(resource):
498-
async with context.app.test_request_context(context.app.config["URL_PREFIX"]):
499-
get_resource_service(resource).delete_action()
516+
await delete_entries_for(context, resource)
500517

501518

502519
@given('"{resource}"')
@@ -1230,15 +1247,28 @@ async def when_upload_patch_dictionary(context):
12301247
async def upload_file(context, dest, filename, file_field, extra_data=None, method="post", user_headers=None):
12311248
if user_headers is None:
12321249
user_headers = []
1250+
12331251
with open(get_fixture_path(context, filename), "rb") as f:
1234-
data = {file_field: f}
1235-
if extra_data:
1236-
data.update(extra_data)
1252+
file_content = f.read()
1253+
files = {
1254+
file_field: FileStorage(
1255+
io.BytesIO(file_content),
1256+
filename=os.path.basename(filename),
1257+
name=file_field,
1258+
)
1259+
}
1260+
12371261
headers = [("Content-Type", "multipart/form-data")]
12381262
headers.extend(user_headers)
12391263
headers = unique_headers(headers, context.headers)
12401264
url = get_prefixed_url(context.app, dest)
1241-
context.response = await getattr(context.client, method)(url, data=data, headers=headers)
1265+
1266+
context.response = await getattr(context.client, method)(
1267+
url,
1268+
files=files,
1269+
form=extra_data,
1270+
headers=headers,
1271+
)
12421272
await assert_ok(context.response)
12431273
await store_placeholder(context, url)
12441274

@@ -1298,7 +1328,6 @@ async def _step_impl_then_get_error(context, code):
12981328
async def step_impl_then_get_error(context, code):
12991329
await expect_status(context.response, int(code))
13001330
if context.text:
1301-
print("got", (await context.response.get_data()).decode("utf-8"))
13021331
await test_json(context)
13031332

13041333

@@ -1469,7 +1498,6 @@ async def step_impl_then_get_existing_resource(context):
14691498

14701499
async def step_impl_then_get_existing(context):
14711500
await assert_200(context.response)
1472-
print("got", get_response_readable(await context.response.get_data()))
14731501
await test_json(context)
14741502

14751503

0 commit comments

Comments
 (0)