Skip to content

Commit

Permalink
Merge pull request #996 from LibraryOfCongress/more-api-test-coverage
Browse files Browse the repository at this point in the history
Add more test coverage for API endpoints
  • Loading branch information
rstorey committed May 23, 2019
2 parents 4125e2a + dce8e28 commit 16f002f
Show file tree
Hide file tree
Showing 3 changed files with 244 additions and 19 deletions.
227 changes: 215 additions & 12 deletions concordia/tests/test_api_views.py
@@ -1,8 +1,17 @@
from urllib.parse import urlparse

from django.test import TestCase, override_settings
from django.urls import reverse
from django.utils.timezone import now

from concordia.models import Asset, Item, Transcription, TranscriptionStatus, User
from concordia.models import (
Asset,
Campaign,
Item,
Transcription,
TranscriptionStatus,
User,
)
from concordia.utils import get_anonymous_user

from .utils import JSONAssertMixin, create_asset, create_item, create_project
Expand All @@ -18,11 +27,14 @@ def setUpTestData(cls):
username="reviewer", email="tester@example.com"
)

project = create_project()
cls.test_project = create_project()

cls.items = [
create_item(
item_id=f"item_{i}", title=f"Item {i}", project=project, do_save=False
item_id=f"item_{i}",
title=f"Item {i}",
project=cls.test_project,
do_save=False,
)
for i in range(0, 3)
]
Expand Down Expand Up @@ -57,20 +69,73 @@ def setUpTestData(cls):
submitted_t.full_clean()
submitted_t.save()

def get_asset_list(self, url, page_size=23):
resp = self.client.get(url, {"per_page": page_size})
def get_api_response(self, url, **request_args):
"""
This issues a call to one of our API views and confirms that the
response follows our basic conventions of returning a valid JSON
response
"""

qs = {"format": "json"}
if request_args is not None:
qs.update(request_args)

resp = self.client.get(url, qs)
data = self.assertValidJSON(resp)
return resp, data

self.assertIn("objects", data)
object_count = len(data["objects"])
self.assertLessEqual(object_count, 23)
def get_api_list_response(self, url, page_size=23, **request_args):
"""
This issues a call to one of our API views and confirms that the
response follows our basic conventions of returning a top level object
with members“objects” (list) and “pagination” (object).
"""

qs = {"per_page": page_size}
if request_args is not None:
qs.update(request_args)

resp, data = self.get_api_response(url, **qs)

self.assertIn("objects", data)
self.assertIn("pagination", data)

self.assertAssetsHaveLatestTranscriptions(data["objects"])
object_count = len(data["objects"])
self.assertLessEqual(object_count, page_size)

self.assertAbsoluteURLs(data["objects"])
self.assertAbsoluteURLs(data["pagination"])

return resp, data

def assertAbsoluteUrl(self, url, allow_none=True):
"""Require a URL to either be None or an absolute URL"""

if url is None and allow_none:
return

parsed = urlparse(url)
self.assertIn(
parsed.scheme, ["http", "https"], msg=f"Expected {url} to have HTTP scheme"
)

self.assertTrue(parsed.netloc)

def assertAbsoluteURLs(self, data):
if isinstance(data, dict):
for k, v in data.items():
if k.endswith("url"):
self.assertAbsoluteUrl(v)
elif isinstance(v, (dict, list)):
self.assertAbsoluteURLs(v)
elif isinstance(data, list):
for i in data:
self.assertAbsoluteURLs(i)
else:
raise TypeError(
"assertAbsoluteURLs must be called with a dictionary or list"
)

def assertAssetStatuses(self, asset_list, expected_statuses):
asset_pks = [i["id"] for i in asset_list]

Expand Down Expand Up @@ -100,19 +165,157 @@ def assertAssetsHaveLatestTranscriptions(self, asset_list):
)

def test_asset_list(self):
resp, data = self.get_asset_list(reverse("assets-list-json"))
resp, data = self.get_api_list_response(reverse("assets-list-json"))

self.assertAssetsHaveLatestTranscriptions(data["objects"])

def test_transcribable_asset_list(self):
resp, data = self.get_asset_list(reverse("transcribe-assets-json"))
resp, data = self.get_api_list_response(reverse("transcribe-assets-json"))

self.assertAssetStatuses(
data["objects"],
[TranscriptionStatus.NOT_STARTED, TranscriptionStatus.IN_PROGRESS],
)

self.assertAssetsHaveLatestTranscriptions(data["objects"])

def test_reviewable_asset_list(self):
resp, data = self.get_asset_list(reverse("review-assets-json"))
resp, data = self.get_api_list_response(reverse("review-assets-json"))

self.assertAssetStatuses(data["objects"], [TranscriptionStatus.SUBMITTED])

self.assertGreater(len(data["objects"]), 0)

self.assertAssetsHaveLatestTranscriptions(data["objects"])

def test_campaign_list(self):
resp, data = self.get_api_list_response(reverse("transcriptions:campaign-list"))

self.assertGreater(len(data["objects"]), 0)

test_campaigns = {
i["id"]: i
for i in Campaign.objects.published().values(
"id", "title", "description", "short_description", "slug"
)
}

for obj in data["objects"]:
self.assertIn("id", obj)
self.assertIn("url", obj)
self.assertDictContainsSubset(test_campaigns[obj["id"]], obj)

def test_campaign_detail(self):
resp, data = self.get_api_response(
reverse(
"transcriptions:campaign-detail",
kwargs={"slug": self.test_project.campaign.slug},
)
)

self.assertIn("object", data)
self.assertNotIn("objects", data)

serialized_project = data["object"]

self.assertIn("id", serialized_project)
self.assertIn("url", serialized_project)
campaign = self.test_project.campaign
self.assertDictContainsSubset(
{
"id": campaign.id,
"title": campaign.title,
"description": campaign.description,
"slug": campaign.slug,
"metadata": campaign.metadata,
"thumbnail_image": campaign.thumbnail_image,
},
serialized_project,
)
self.assertURLEqual(
serialized_project["url"], f"http://testserver{campaign.get_absolute_url()}"
)

def test_project_detail(self):
project = self.test_project

resp, data = self.get_api_list_response(project.get_absolute_url())

# Until we clean up the project view code, projects have two key
# elements: objects lists the children (i.e. items) and the project
# itself is in a second top-level “project” object:
self.assertIn("objects", data)
self.assertIn("project", data)
self.assertNotIn("object", data)

serialized_project = data["project"]

self.assertIn("id", serialized_project)
self.assertIn("url", serialized_project)

self.assertURLEqual(
serialized_project["url"], f"http://testserver{project.get_absolute_url()}"
)
self.assertDictContainsSubset(
{
"description": project.description,
"id": project.id,
"metadata": project.metadata,
"slug": project.slug,
"thumbnail_image": project.thumbnail_image,
"title": project.title,
},
serialized_project,
)

for obj in data["objects"]:
self.assertIn("description", obj)
self.assertIn("item_id", obj)
self.assertIn("item_url", obj)
self.assertIn("metadata", obj)
self.assertIn("thumbnail_url", obj)
self.assertIn("title", obj)
self.assertIn("url", obj)

def test_item_detail(self):
item = self.test_project.item_set.first()
resp, data = self.get_api_list_response(item.get_absolute_url())

# Until we clean up the project view code, projects have two key
# elements: objects lists the children (i.e. items) and the project
# itself is in a second top-level “project” object:
self.assertIn("objects", data)
self.assertIn("item", data)
self.assertNotIn("object", data)

serialized_item = data["item"]

self.assertIn("id", serialized_item)
self.assertIn("url", serialized_item)
self.assertIn("thumbnail_url", serialized_item)

self.assertURLEqual(
serialized_item["url"], f"http://testserver{item.get_absolute_url()}"
)
self.assertDictContainsSubset(
{
"description": item.description,
"id": item.id,
"item_id": item.item_id,
"metadata": item.metadata,
"title": item.title,
},
serialized_item,
)

for obj in data["objects"]:
self.assertIn("description", obj)
self.assertIn("difficulty", obj)
self.assertIn("metadata", obj)
self.assertIn("image_url", obj)
self.assertIn("thumbnail_url", obj)
self.assertIn("resource_url", obj)
self.assertIn("title", obj)
self.assertIn("slug", obj)
self.assertIn("url", obj)
self.assertIn("year", obj)
19 changes: 19 additions & 0 deletions concordia/utils.py
Expand Up @@ -2,6 +2,8 @@

from django.contrib.auth.models import User

from .templatetags.concordia_media_tags import asset_media_url


def get_anonymous_user():
"""
Expand All @@ -26,3 +28,20 @@ def get_or_create_reservation_token(request):
if "reservation_token" not in request.session:
request.session["reservation_token"] = token_hex(25)
return request.session["reservation_token"]


def get_image_urls_from_asset(asset):
"""
Given an Asset, return a tuple containing the normalized full-size and
thumbnail-size image URLs
"""

image_url = asset_media_url(asset)
if asset.download_url and "iiif" in asset.download_url:
thumbnail_url = asset.download_url.replace(
"http://tile.loc.gov", "https://tile.loc.gov"
)
else:
thumbnail_url = image_url

return image_url, thumbnail_url
17 changes: 10 additions & 7 deletions concordia/views.py
Expand Up @@ -61,6 +61,7 @@
from concordia.templatetags.concordia_media_tags import asset_media_url
from concordia.utils import (
get_anonymous_user,
get_image_urls_from_asset,
get_or_create_reservation_token,
request_accepts_json,
)
Expand Down Expand Up @@ -651,6 +652,14 @@ def get_context_data(self, **kwargs):

def serialize_context(self, context):
data = super().serialize_context(context)

for i, asset in enumerate(context["object_list"]):
serialized_asset = data["objects"][i]
serialized_asset.pop("media_url")
image_url, thumbnail_url = get_image_urls_from_asset(asset)
serialized_asset["image_url"] = image_url
serialized_asset["thumbnail_url"] = thumbnail_url

data["item"] = self.serialize_object(context["item"])
return data

Expand Down Expand Up @@ -1474,13 +1483,7 @@ def serialize_object(self, obj):
project = item.project
campaign = project.campaign

image_url = asset_media_url(obj)
if obj.download_url and "iiif" in obj.download_url:
thumbnail_url = obj.download_url.replace(
"http://tile.loc.gov", "https://tile.loc.gov"
)
else:
thumbnail_url = image_url
image_url, thumbnail_url = get_image_urls_from_asset(obj)

metadata = {
"id": obj.pk,
Expand Down

0 comments on commit 16f002f

Please sign in to comment.