Initial commit

This commit is contained in:
2024-08-27 20:33:44 +02:00
commit 1f1832267d
14794 changed files with 1599592 additions and 0 deletions

View File

@@ -0,0 +1,615 @@
import json
from unittest import mock
from django.test import TestCase, TransactionTestCase
from django.test.utils import override_settings
from django.urls import reverse
from wagtail.api.v2 import signal_handlers
from wagtail.documents import get_document_model
class TestDocumentListing(TestCase):
fixtures = ["demosite.json"]
def get_response(self, **params):
return self.client.get(reverse("wagtailapi_v2:documents:listing"), params)
def get_document_id_list(self, content):
return [document["id"] for document in content["items"]]
# BASIC TESTS
def test_basic(self):
response = self.get_response()
self.assertEqual(response.status_code, 200)
self.assertEqual(response["Content-type"], "application/json")
# Will crash if the JSON is invalid
content = json.loads(response.content.decode("UTF-8"))
# Check that the meta section is there
self.assertIn("meta", content)
self.assertIsInstance(content["meta"], dict)
# Check that the total count is there and correct
self.assertIn("total_count", content["meta"])
self.assertIsInstance(content["meta"]["total_count"], int)
self.assertEqual(
content["meta"]["total_count"], get_document_model().objects.count()
)
# Check that the items section is there
self.assertIn("items", content)
self.assertIsInstance(content["items"], list)
# Check that each document has a meta section with type and detail_url attributes
for document in content["items"]:
self.assertIn("meta", document)
self.assertIsInstance(document["meta"], dict)
self.assertEqual(
set(document["meta"].keys()),
{"type", "detail_url", "download_url", "tags"},
)
# Type should always be wagtaildocs.Document
self.assertEqual(document["meta"]["type"], "wagtaildocs.Document")
# Check detail_url
self.assertEqual(
document["meta"]["detail_url"],
"http://localhost/api/main/documents/%d/" % document["id"],
)
# Check download_url
self.assertTrue(
document["meta"]["download_url"].startswith(
"http://localhost/documents/%d/" % document["id"]
)
)
# FIELDS
def test_fields_default(self):
response = self.get_response()
content = json.loads(response.content.decode("UTF-8"))
for document in content["items"]:
self.assertEqual(set(document.keys()), {"id", "meta", "title"})
self.assertEqual(
set(document["meta"].keys()),
{"type", "detail_url", "download_url", "tags"},
)
def test_fields(self):
response = self.get_response(fields="title")
content = json.loads(response.content.decode("UTF-8"))
for document in content["items"]:
self.assertEqual(set(document.keys()), {"id", "meta", "title"})
self.assertEqual(
set(document["meta"].keys()),
{"type", "detail_url", "download_url", "tags"},
)
def test_remove_fields(self):
response = self.get_response(fields="-title")
content = json.loads(response.content.decode("UTF-8"))
for document in content["items"]:
self.assertEqual(set(document.keys()), {"id", "meta"})
def test_remove_meta_fields(self):
response = self.get_response(fields="-download_url")
content = json.loads(response.content.decode("UTF-8"))
for document in content["items"]:
self.assertEqual(set(document.keys()), {"id", "meta", "title"})
self.assertEqual(
set(document["meta"].keys()), {"type", "detail_url", "tags"}
)
def test_remove_all_meta_fields(self):
response = self.get_response(fields="-type,-detail_url,-tags,-download_url")
content = json.loads(response.content.decode("UTF-8"))
for document in content["items"]:
self.assertEqual(set(document.keys()), {"id", "title"})
def test_remove_id_field(self):
response = self.get_response(fields="-id")
content = json.loads(response.content.decode("UTF-8"))
for document in content["items"]:
self.assertEqual(set(document.keys()), {"meta", "title"})
def test_all_fields(self):
response = self.get_response(fields="*")
content = json.loads(response.content.decode("UTF-8"))
for document in content["items"]:
self.assertEqual(set(document.keys()), {"id", "meta", "title"})
self.assertEqual(
set(document["meta"].keys()),
{"type", "detail_url", "tags", "download_url"},
)
def test_all_fields_then_remove_something(self):
response = self.get_response(fields="*,-title,-download_url")
content = json.loads(response.content.decode("UTF-8"))
for document in content["items"]:
self.assertEqual(set(document.keys()), {"id", "meta"})
self.assertEqual(
set(document["meta"].keys()), {"type", "detail_url", "tags"}
)
def test_fields_tags(self):
response = self.get_response(fields="tags")
content = json.loads(response.content.decode("UTF-8"))
for document in content["items"]:
self.assertIsInstance(document["meta"]["tags"], list)
def test_star_in_wrong_position_gives_error(self):
response = self.get_response(fields="title,*")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(
content, {"message": "fields error: '*' must be in the first position"}
)
def test_fields_which_are_not_in_api_fields_gives_error(self):
response = self.get_response(fields="uploaded_by_user")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {"message": "unknown fields: uploaded_by_user"})
def test_fields_unknown_field_gives_error(self):
response = self.get_response(fields="123,title,abc")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {"message": "unknown fields: 123, abc"})
def test_fields_remove_unknown_field_gives_error(self):
response = self.get_response(fields="-123,-title,-abc")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {"message": "unknown fields: 123, abc"})
# FILTERING
def test_filtering_exact_filter(self):
response = self.get_response(title="James Joyce")
content = json.loads(response.content.decode("UTF-8"))
document_id_list = self.get_document_id_list(content)
self.assertEqual(document_id_list, [2])
def test_filtering_on_id(self):
response = self.get_response(id=10)
content = json.loads(response.content.decode("UTF-8"))
document_id_list = self.get_document_id_list(content)
self.assertEqual(document_id_list, [10])
def test_filtering_tags(self):
get_document_model().objects.get(id=3).tags.add("test")
response = self.get_response(tags="test")
content = json.loads(response.content.decode("UTF-8"))
document_id_list = self.get_document_id_list(content)
self.assertEqual(document_id_list, [3])
def test_filtering_unknown_field_gives_error(self):
response = self.get_response(not_a_field="abc")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(
content,
{
"message": "query parameter is not an operation or a recognised field: not_a_field"
},
)
# ORDERING
def test_ordering_by_title(self):
response = self.get_response(order="title")
content = json.loads(response.content.decode("UTF-8"))
document_id_list = self.get_document_id_list(content)
self.assertEqual(document_id_list, [3, 12, 10, 2, 7, 8, 5, 4, 1, 11, 9, 6])
def test_ordering_by_title_backwards(self):
response = self.get_response(order="-title")
content = json.loads(response.content.decode("UTF-8"))
document_id_list = self.get_document_id_list(content)
self.assertEqual(document_id_list, [6, 9, 11, 1, 4, 5, 8, 7, 2, 10, 12, 3])
def test_ordering_by_random(self):
response_1 = self.get_response(order="random")
content_1 = json.loads(response_1.content.decode("UTF-8"))
document_id_list_1 = self.get_document_id_list(content_1)
response_2 = self.get_response(order="random")
content_2 = json.loads(response_2.content.decode("UTF-8"))
document_id_list_2 = self.get_document_id_list(content_2)
self.assertNotEqual(document_id_list_1, document_id_list_2)
def test_ordering_by_random_backwards_gives_error(self):
response = self.get_response(order="-random")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(
content, {"message": "cannot order by 'random' (unknown field)"}
)
def test_ordering_by_random_with_offset_gives_error(self):
response = self.get_response(order="random", offset=10)
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(
content, {"message": "random ordering with offset is not supported"}
)
def test_ordering_by_unknown_field_gives_error(self):
response = self.get_response(order="not_a_field")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(
content, {"message": "cannot order by 'not_a_field' (unknown field)"}
)
# LIMIT
def test_limit_only_two_items_returned(self):
response = self.get_response(limit=2)
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(len(content["items"]), 2)
def test_limit_total_count(self):
response = self.get_response(limit=2)
content = json.loads(response.content.decode("UTF-8"))
# The total count must not be affected by "limit"
self.assertEqual(
content["meta"]["total_count"], get_document_model().objects.count()
)
def test_limit_not_integer_gives_error(self):
response = self.get_response(limit="abc")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {"message": "limit must be a positive integer"})
def test_limit_too_high_gives_error(self):
response = self.get_response(limit=1000)
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {"message": "limit cannot be higher than 20"})
@override_settings(WAGTAILAPI_LIMIT_MAX=None)
def test_limit_max_none_gives_no_errors(self):
response = self.get_response(limit=1000000)
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(content["items"]), get_document_model().objects.count())
@override_settings(WAGTAILAPI_LIMIT_MAX=10)
def test_limit_maximum_can_be_changed(self):
response = self.get_response(limit=20)
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {"message": "limit cannot be higher than 10"})
@override_settings(WAGTAILAPI_LIMIT_MAX=2)
def test_limit_default_changes_with_max(self):
# The default limit is 20. If WAGTAILAPI_LIMIT_MAX is less than that,
# the default should change accordingly.
response = self.get_response()
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(len(content["items"]), 2)
# OFFSET
def test_offset_5_usually_appears_5th_in_list(self):
response = self.get_response()
content = json.loads(response.content.decode("UTF-8"))
document_id_list = self.get_document_id_list(content)
self.assertEqual(document_id_list.index(5), 4)
def test_offset_5_moves_after_offset(self):
response = self.get_response(offset=4)
content = json.loads(response.content.decode("UTF-8"))
document_id_list = self.get_document_id_list(content)
self.assertEqual(document_id_list.index(5), 0)
def test_offset_total_count(self):
response = self.get_response(offset=10)
content = json.loads(response.content.decode("UTF-8"))
# The total count must not be affected by "offset"
self.assertEqual(
content["meta"]["total_count"], get_document_model().objects.count()
)
def test_offset_not_integer_gives_error(self):
response = self.get_response(offset="abc")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {"message": "offset must be a positive integer"})
class TestDocumentListingSearch(TransactionTestCase):
fixtures = ["demosite.json"]
def get_response(self, **params):
return self.client.get(reverse("wagtailapi_v2:documents:listing"), params)
def get_document_id_list(self, content):
return [document["id"] for document in content["items"]]
def test_search_for_james_joyce(self):
response = self.get_response(search="james")
content = json.loads(response.content.decode("UTF-8"))
document_id_list = self.get_document_id_list(content)
self.assertEqual(set(document_id_list), {2})
def test_search_with_order(self):
response = self.get_response(search="james", order="title")
content = json.loads(response.content.decode("UTF-8"))
document_id_list = self.get_document_id_list(content)
self.assertEqual(document_id_list, [2])
@override_settings(WAGTAILAPI_SEARCH_ENABLED=False)
def test_search_when_disabled_gives_error(self):
response = self.get_response(search="james")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {"message": "search is disabled"})
def test_search_when_filtering_by_tag_gives_error(self):
response = self.get_response(search="james", tags="wagtail")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(
content,
{"message": "filtering by tag with a search query is not supported"},
)
class TestDocumentDetail(TestCase):
fixtures = ["demosite.json"]
def get_response(self, image_id, **params):
return self.client.get(
reverse("wagtailapi_v2:documents:detail", args=(image_id,)), params
)
def test_basic(self):
response = self.get_response(1)
self.assertEqual(response.status_code, 200)
self.assertEqual(response["Content-type"], "application/json")
# Will crash if the JSON is invalid
content = json.loads(response.content.decode("UTF-8"))
# Check the id field
self.assertIn("id", content)
self.assertEqual(content["id"], 1)
# Check that the meta section is there
self.assertIn("meta", content)
self.assertIsInstance(content["meta"], dict)
# Check the meta type
self.assertIn("type", content["meta"])
self.assertEqual(content["meta"]["type"], "wagtaildocs.Document")
# Check the meta detail_url
self.assertIn("detail_url", content["meta"])
self.assertEqual(
content["meta"]["detail_url"], "http://localhost/api/main/documents/1/"
)
# Check the meta download_url
self.assertIn("download_url", content["meta"])
self.assertEqual(
content["meta"]["download_url"],
"http://localhost/documents/1/wagtail_by_markyharky.jpg",
)
# Check the title field
self.assertIn("title", content)
self.assertEqual(content["title"], "Wagtail by mark Harkin")
# Check the tags field
self.assertIn("tags", content["meta"])
self.assertEqual(content["meta"]["tags"], [])
def test_tags(self):
get_document_model().objects.get(id=1).tags.add("hello")
get_document_model().objects.get(id=1).tags.add("world")
response = self.get_response(1)
content = json.loads(response.content.decode("UTF-8"))
self.assertIn("tags", content["meta"])
self.assertEqual(content["meta"]["tags"], ["hello", "world"])
@override_settings(WAGTAILAPI_BASE_URL="http://api.example.com/")
def test_download_url_with_custom_base_url(self):
response = self.get_response(1)
content = json.loads(response.content.decode("UTF-8"))
self.assertIn("download_url", content["meta"])
self.assertEqual(
content["meta"]["download_url"],
"http://api.example.com/documents/1/wagtail_by_markyharky.jpg",
)
# FIELDS
def test_remove_fields(self):
response = self.get_response(2, fields="-title")
content = json.loads(response.content.decode("UTF-8"))
self.assertIn("id", set(content.keys()))
self.assertNotIn("title", set(content.keys()))
def test_remove_meta_fields(self):
response = self.get_response(2, fields="-download_url")
content = json.loads(response.content.decode("UTF-8"))
self.assertIn("detail_url", set(content["meta"].keys()))
self.assertNotIn("download_url", set(content["meta"].keys()))
def test_remove_id_field(self):
response = self.get_response(2, fields="-id")
content = json.loads(response.content.decode("UTF-8"))
self.assertIn("title", set(content.keys()))
self.assertNotIn("id", set(content.keys()))
def test_remove_all_fields(self):
response = self.get_response(2, fields="_,id,type")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(set(content.keys()), {"id", "meta"})
self.assertEqual(set(content["meta"].keys()), {"type"})
def test_star_in_wrong_position_gives_error(self):
response = self.get_response(2, fields="title,*")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(
content, {"message": "fields error: '*' must be in the first position"}
)
def test_fields_which_are_not_in_api_fields_gives_error(self):
response = self.get_response(2, fields="path")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {"message": "unknown fields: path"})
def test_fields_unknown_field_gives_error(self):
response = self.get_response(2, fields="123,title,abc")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {"message": "unknown fields: 123, abc"})
def test_fields_remove_unknown_field_gives_error(self):
response = self.get_response(2, fields="-123,-title,-abc")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {"message": "unknown fields: 123, abc"})
def test_nested_fields_on_non_relational_field_gives_error(self):
response = self.get_response(2, fields="title(foo,bar)")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {"message": "'title' does not support nested fields"})
class TestDocumentFind(TestCase):
fixtures = ["demosite.json"]
def get_response(self, **params):
return self.client.get(reverse("wagtailapi_v2:documents:find"), params)
def test_without_parameters(self):
response = self.get_response()
self.assertEqual(response.status_code, 404)
self.assertEqual(response["Content-type"], "application/json")
# Will crash if the JSON is invalid
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(content, {"message": "not found"})
def test_find_by_id(self):
response = self.get_response(id=5)
self.assertRedirects(
response,
"http://localhost" + reverse("wagtailapi_v2:documents:detail", args=[5]),
fetch_redirect_response=False,
)
def test_find_by_id_nonexistent(self):
response = self.get_response(id=1234)
self.assertEqual(response.status_code, 404)
self.assertEqual(response["Content-type"], "application/json")
# Will crash if the JSON is invalid
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(content, {"message": "not found"})
@override_settings(
WAGTAILFRONTENDCACHE={
"varnish": {
"BACKEND": "wagtail.contrib.frontend_cache.backends.HTTPBackend",
"LOCATION": "http://localhost:8000",
},
},
WAGTAILAPI_BASE_URL="http://api.example.com",
)
@mock.patch("wagtail.contrib.frontend_cache.backends.http.HTTPBackend.purge")
class TestDocumentCacheInvalidation(TestCase):
fixtures = ["demosite.json"]
@classmethod
def setUpClass(cls):
super().setUpClass()
signal_handlers.register_signal_handlers()
@classmethod
def tearDownClass(cls):
super().tearDownClass()
signal_handlers.unregister_signal_handlers()
def test_resave_document_purges(self, purge):
get_document_model().objects.get(id=5).save()
purge.assert_any_call("http://api.example.com/api/main/documents/5/")
def test_delete_document_purges(self, purge):
get_document_model().objects.get(id=5).delete()
purge.assert_any_call("http://api.example.com/api/main/documents/5/")

View File

@@ -0,0 +1,607 @@
import json
from unittest import mock
from django.test import TestCase, TransactionTestCase
from django.test.utils import override_settings
from django.urls import reverse
from wagtail.api.v2 import signal_handlers
from wagtail.images import get_image_model
class TestImageListing(TestCase):
fixtures = ["demosite.json"]
def get_response(self, **params):
return self.client.get(reverse("wagtailapi_v2:images:listing"), params)
def get_image_id_list(self, content):
return [image["id"] for image in content["items"]]
# BASIC TESTS
def test_basic(self):
response = self.get_response()
self.assertEqual(response.status_code, 200)
self.assertEqual(response["Content-type"], "application/json")
# Will crash if the JSON is invalid
content = json.loads(response.content.decode("UTF-8"))
# Check that the meta section is there
self.assertIn("meta", content)
self.assertIsInstance(content["meta"], dict)
# Check that the total count is there and correct
self.assertIn("total_count", content["meta"])
self.assertIsInstance(content["meta"]["total_count"], int)
self.assertEqual(
content["meta"]["total_count"], get_image_model().objects.count()
)
# Check that the items section is there
self.assertIn("items", content)
self.assertIsInstance(content["items"], list)
# Check that each image has a meta section with type and detail_url attributes
for image in content["items"]:
self.assertIn("meta", image)
self.assertIsInstance(image["meta"], dict)
self.assertEqual(
set(image["meta"].keys()),
{"type", "detail_url", "tags", "download_url"},
)
# Type should always be wagtailimages.Image
self.assertEqual(image["meta"]["type"], "wagtailimages.Image")
# Check detail url
self.assertEqual(
image["meta"]["detail_url"],
"http://localhost/api/main/images/%d/" % image["id"],
)
# FIELDS
def test_fields_default(self):
response = self.get_response()
content = json.loads(response.content.decode("UTF-8"))
for image in content["items"]:
self.assertEqual(set(image.keys()), {"id", "meta", "title"})
self.assertEqual(
set(image["meta"].keys()),
{"type", "detail_url", "tags", "download_url"},
)
def test_fields(self):
response = self.get_response(fields="width,height")
content = json.loads(response.content.decode("UTF-8"))
for image in content["items"]:
self.assertEqual(
set(image.keys()), {"id", "meta", "title", "width", "height"}
)
self.assertEqual(
set(image["meta"].keys()),
{"type", "detail_url", "tags", "download_url"},
)
def test_remove_fields(self):
response = self.get_response(fields="-title")
content = json.loads(response.content.decode("UTF-8"))
for image in content["items"]:
self.assertEqual(set(image.keys()), {"id", "meta"})
def test_remove_meta_fields(self):
response = self.get_response(fields="-tags")
content = json.loads(response.content.decode("UTF-8"))
for image in content["items"]:
self.assertEqual(set(image.keys()), {"id", "meta", "title"})
self.assertEqual(
set(image["meta"].keys()), {"type", "detail_url", "download_url"}
)
def test_remove_all_meta_fields(self):
response = self.get_response(fields="-type,-detail_url,-tags")
content = json.loads(response.content.decode("UTF-8"))
for image in content["items"]:
self.assertEqual(set(image.keys()), {"id", "title", "meta"})
def test_remove_id_field(self):
response = self.get_response(fields="-id")
content = json.loads(response.content.decode("UTF-8"))
for image in content["items"]:
self.assertEqual(set(image.keys()), {"meta", "title"})
def test_all_fields(self):
response = self.get_response(fields="*")
content = json.loads(response.content.decode("UTF-8"))
for image in content["items"]:
self.assertEqual(
set(image.keys()), {"id", "meta", "title", "width", "height"}
)
self.assertEqual(
set(image["meta"].keys()),
{"type", "detail_url", "tags", "download_url"},
)
def test_all_fields_then_remove_something(self):
response = self.get_response(fields="*,-title,-tags")
content = json.loads(response.content.decode("UTF-8"))
for image in content["items"]:
self.assertEqual(set(image.keys()), {"id", "meta", "width", "height"})
self.assertEqual(
set(image["meta"].keys()), {"type", "detail_url", "download_url"}
)
def test_fields_tags(self):
response = self.get_response(fields="tags")
content = json.loads(response.content.decode("UTF-8"))
for image in content["items"]:
self.assertEqual(set(image.keys()), {"id", "meta", "title"})
self.assertEqual(set(image.keys()), {"id", "meta", "title"})
self.assertEqual(
set(image["meta"].keys()),
{"type", "detail_url", "tags", "download_url"},
)
self.assertIsInstance(image["meta"]["tags"], list)
def test_star_in_wrong_position_gives_error(self):
response = self.get_response(fields="title,*")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(
content, {"message": "fields error: '*' must be in the first position"}
)
def test_fields_which_are_not_in_api_fields_gives_error(self):
response = self.get_response(fields="uploaded_by_user")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {"message": "unknown fields: uploaded_by_user"})
def test_fields_unknown_field_gives_error(self):
response = self.get_response(fields="123,title,abc")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {"message": "unknown fields: 123, abc"})
def test_fields_remove_unknown_field_gives_error(self):
response = self.get_response(fields="-123,-title,-abc")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {"message": "unknown fields: 123, abc"})
# FILTERING
def test_filtering_exact_filter(self):
response = self.get_response(title="James Joyce")
content = json.loads(response.content.decode("UTF-8"))
image_id_list = self.get_image_id_list(content)
self.assertEqual(image_id_list, [5])
def test_filtering_on_id(self):
response = self.get_response(id=10)
content = json.loads(response.content.decode("UTF-8"))
image_id_list = self.get_image_id_list(content)
self.assertEqual(image_id_list, [10])
def test_filtering_tags(self):
get_image_model().objects.get(id=6).tags.add("test")
response = self.get_response(tags="test")
content = json.loads(response.content.decode("UTF-8"))
image_id_list = self.get_image_id_list(content)
self.assertEqual(image_id_list, [6])
def test_filtering_unknown_field_gives_error(self):
response = self.get_response(not_a_field="abc")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(
content,
{
"message": "query parameter is not an operation or a recognised field: not_a_field"
},
)
# ORDERING
def test_ordering_by_title(self):
response = self.get_response(order="title")
content = json.loads(response.content.decode("UTF-8"))
image_id_list = self.get_image_id_list(content)
self.assertEqual(image_id_list, [6, 15, 13, 5, 10, 11, 8, 7, 4, 14, 12, 9])
def test_ordering_by_title_backwards(self):
response = self.get_response(order="-title")
content = json.loads(response.content.decode("UTF-8"))
image_id_list = self.get_image_id_list(content)
self.assertEqual(image_id_list, [9, 12, 14, 4, 7, 8, 11, 10, 5, 13, 15, 6])
def test_ordering_by_random(self):
response_1 = self.get_response(order="random")
content_1 = json.loads(response_1.content.decode("UTF-8"))
image_id_list_1 = self.get_image_id_list(content_1)
response_2 = self.get_response(order="random")
content_2 = json.loads(response_2.content.decode("UTF-8"))
image_id_list_2 = self.get_image_id_list(content_2)
self.assertNotEqual(image_id_list_1, image_id_list_2)
def test_ordering_by_random_backwards_gives_error(self):
response = self.get_response(order="-random")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(
content, {"message": "cannot order by 'random' (unknown field)"}
)
def test_ordering_by_random_with_offset_gives_error(self):
response = self.get_response(order="random", offset=10)
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(
content, {"message": "random ordering with offset is not supported"}
)
def test_ordering_by_unknown_field_gives_error(self):
response = self.get_response(order="not_a_field")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(
content, {"message": "cannot order by 'not_a_field' (unknown field)"}
)
# LIMIT
def test_limit_only_two_items_returned(self):
response = self.get_response(limit=2)
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(len(content["items"]), 2)
def test_limit_total_count(self):
response = self.get_response(limit=2)
content = json.loads(response.content.decode("UTF-8"))
# The total count must not be affected by "limit"
self.assertEqual(
content["meta"]["total_count"], get_image_model().objects.count()
)
def test_limit_not_integer_gives_error(self):
response = self.get_response(limit="abc")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {"message": "limit must be a positive integer"})
def test_limit_too_high_gives_error(self):
response = self.get_response(limit=1000)
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {"message": "limit cannot be higher than 20"})
@override_settings(WAGTAILAPI_LIMIT_MAX=None)
def test_limit_max_none_gives_no_errors(self):
response = self.get_response(limit=1000000)
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(content["items"]), get_image_model().objects.count())
@override_settings(WAGTAILAPI_LIMIT_MAX=10)
def test_limit_maximum_can_be_changed(self):
response = self.get_response(limit=20)
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {"message": "limit cannot be higher than 10"})
@override_settings(WAGTAILAPI_LIMIT_MAX=2)
def test_limit_default_changes_with_max(self):
# The default limit is 20. If WAGTAILAPI_LIMIT_MAX is less than that,
# the default should change accordingly.
response = self.get_response()
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(len(content["items"]), 2)
# OFFSET
def test_offset_10_usually_appears_7th_in_list(self):
response = self.get_response()
content = json.loads(response.content.decode("UTF-8"))
image_id_list = self.get_image_id_list(content)
self.assertEqual(image_id_list.index(10), 6)
def test_offset_10_moves_after_offset(self):
response = self.get_response(offset=4)
content = json.loads(response.content.decode("UTF-8"))
image_id_list = self.get_image_id_list(content)
self.assertEqual(image_id_list.index(10), 2)
def test_offset_total_count(self):
response = self.get_response(offset=10)
content = json.loads(response.content.decode("UTF-8"))
# The total count must not be affected by "offset"
self.assertEqual(
content["meta"]["total_count"], get_image_model().objects.count()
)
def test_offset_not_integer_gives_error(self):
response = self.get_response(offset="abc")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {"message": "offset must be a positive integer"})
class TestImageListingSearch(TransactionTestCase):
fixtures = ["demosite.json"]
def get_response(self, **params):
return self.client.get(reverse("wagtailapi_v2:images:listing"), params)
def get_image_id_list(self, content):
return [image["id"] for image in content["items"]]
def test_search_for_james_joyce(self):
response = self.get_response(search="james")
content = json.loads(response.content.decode("UTF-8"))
image_id_list = self.get_image_id_list(content)
self.assertEqual(set(image_id_list), {5})
def test_search_with_order(self):
response = self.get_response(search="james", order="title")
content = json.loads(response.content.decode("UTF-8"))
image_id_list = self.get_image_id_list(content)
self.assertEqual(image_id_list, [5])
@override_settings(WAGTAILAPI_SEARCH_ENABLED=False)
def test_search_when_disabled_gives_error(self):
response = self.get_response(search="james")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {"message": "search is disabled"})
def test_search_when_filtering_by_tag_gives_error(self):
response = self.get_response(search="james", tags="wagtail")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(
content,
{"message": "filtering by tag with a search query is not supported"},
)
class TestImageDetail(TestCase):
fixtures = ["demosite.json"]
def get_response(self, image_id, **params):
return self.client.get(
reverse("wagtailapi_v2:images:detail", args=(image_id,)), params
)
def test_basic(self):
response = self.get_response(5)
self.assertEqual(response.status_code, 200)
self.assertEqual(response["Content-type"], "application/json")
# Will crash if the JSON is invalid
content = json.loads(response.content.decode("UTF-8"))
# Check the id field
self.assertIn("id", content)
self.assertEqual(content["id"], 5)
# Check that the meta section is there
self.assertIn("meta", content)
self.assertIsInstance(content["meta"], dict)
# Check the meta type
self.assertIn("type", content["meta"])
self.assertEqual(content["meta"]["type"], "wagtailimages.Image")
# Check the meta detail_url
self.assertIn("detail_url", content["meta"])
self.assertEqual(
content["meta"]["detail_url"], "http://localhost/api/main/images/5/"
)
# Check the title field
self.assertIn("title", content)
self.assertEqual(content["title"], "James Joyce")
# Check the width and height fields
self.assertIn("width", content)
self.assertIn("height", content)
self.assertEqual(content["width"], 500)
self.assertEqual(content["height"], 392)
# Check the tags field
self.assertIn("tags", content["meta"])
self.assertEqual(content["meta"]["tags"], [])
def test_tags(self):
image = get_image_model().objects.get(id=5)
image.tags.add("hello")
image.tags.add("world")
response = self.get_response(5)
content = json.loads(response.content.decode("UTF-8"))
self.assertIn("tags", content["meta"])
self.assertEqual(content["meta"]["tags"], ["hello", "world"])
# FIELDS
def test_remove_fields(self):
response = self.get_response(5, fields="-title")
content = json.loads(response.content.decode("UTF-8"))
self.assertIn("id", set(content.keys()))
self.assertNotIn("title", set(content.keys()))
def test_remove_meta_fields(self):
response = self.get_response(5, fields="-type")
content = json.loads(response.content.decode("UTF-8"))
self.assertIn("detail_url", set(content["meta"].keys()))
self.assertNotIn("type", set(content["meta"].keys()))
def test_remove_id_field(self):
response = self.get_response(5, fields="-id")
content = json.loads(response.content.decode("UTF-8"))
self.assertIn("title", set(content.keys()))
self.assertNotIn("id", set(content.keys()))
def test_remove_all_fields(self):
response = self.get_response(5, fields="_,id,type")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(set(content.keys()), {"id", "meta"})
self.assertEqual(set(content["meta"].keys()), {"type"})
def test_star_in_wrong_position_gives_error(self):
response = self.get_response(5, fields="title,*")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(
content, {"message": "fields error: '*' must be in the first position"}
)
def test_fields_which_are_not_in_api_fields_gives_error(self):
response = self.get_response(5, fields="path")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {"message": "unknown fields: path"})
def test_fields_unknown_field_gives_error(self):
response = self.get_response(5, fields="123,title,abc")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {"message": "unknown fields: 123, abc"})
def test_fields_remove_unknown_field_gives_error(self):
response = self.get_response(5, fields="-123,-title,-abc")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {"message": "unknown fields: 123, abc"})
def test_nested_fields_on_non_relational_field_gives_error(self):
response = self.get_response(5, fields="title(foo,bar)")
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(response.status_code, 400)
self.assertEqual(content, {"message": "'title' does not support nested fields"})
class TestImageFind(TestCase):
fixtures = ["demosite.json"]
def get_response(self, **params):
return self.client.get(reverse("wagtailapi_v2:images:find"), params)
def test_without_parameters(self):
response = self.get_response()
self.assertEqual(response.status_code, 404)
self.assertEqual(response["Content-type"], "application/json")
# Will crash if the JSON is invalid
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(content, {"message": "not found"})
def test_find_by_id(self):
response = self.get_response(id=5)
self.assertRedirects(
response,
"http://localhost" + reverse("wagtailapi_v2:images:detail", args=[5]),
fetch_redirect_response=False,
)
def test_find_by_id_nonexistent(self):
response = self.get_response(id=1234)
self.assertEqual(response.status_code, 404)
self.assertEqual(response["Content-type"], "application/json")
# Will crash if the JSON is invalid
content = json.loads(response.content.decode("UTF-8"))
self.assertEqual(content, {"message": "not found"})
@override_settings(
WAGTAILFRONTENDCACHE={
"varnish": {
"BACKEND": "wagtail.contrib.frontend_cache.backends.HTTPBackend",
"LOCATION": "http://localhost:8000",
},
},
WAGTAILAPI_BASE_URL="http://api.example.com",
)
@mock.patch("wagtail.contrib.frontend_cache.backends.http.HTTPBackend.purge")
class TestImageCacheInvalidation(TestCase):
fixtures = ["demosite.json"]
@classmethod
def setUpClass(cls):
super().setUpClass()
signal_handlers.register_signal_handlers()
@classmethod
def tearDownClass(cls):
super().tearDownClass()
signal_handlers.unregister_signal_handlers()
def test_resave_image_purges(self, purge):
get_image_model().objects.get(id=5).save()
purge.assert_any_call("http://api.example.com/api/main/images/5/")
def test_delete_image_purges(self, purge):
get_image_model().objects.get(id=5).delete()
purge.assert_any_call("http://api.example.com/api/main/images/5/")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,401 @@
from django.test import RequestFactory, TestCase, override_settings
from django.utils.encoding import force_bytes
from wagtail.models import Site
from ..utils import (
FieldsParameterParseError,
get_base_url,
parse_boolean,
parse_fields_parameter,
)
class DynamicBaseUrl:
def __str__(self):
return "https://www.example.com"
def __bytes__(self):
return force_bytes(self.__str__())
def decode(self, *args, **kwargs):
return self.__bytes__().decode(*args, **kwargs)
class TestGetBaseUrl(TestCase):
def setUp(self):
Site.objects.all().delete()
def prepare_site(self):
return Site.objects.get_or_create(
hostname="other.example.com",
port=8080,
root_page_id=1,
is_default_site=True,
)[0]
def clear_cached_site(self, request):
del request._wagtail_site
def test_get_base_url_unset(self):
self.assertIsNone(get_base_url())
def test_get_base_url_from_request(self):
# base url for siteless request should be None
request = RequestFactory().get("/")
self.assertIsNone(Site.find_for_request(request))
self.assertIsNone(get_base_url(request))
# base url for request with a site should be based on the site's details
site = self.prepare_site()
self.clear_cached_site(request)
self.assertEqual(site, Site.find_for_request(request))
self.assertEqual(get_base_url(request), "http://other.example.com:8080")
# port 443 should indicate https without a port
site.port = 443
site.save()
self.clear_cached_site(request)
self.assertEqual(get_base_url(request), "https://other.example.com")
# port 80 should indicate http without a port
site.port = 80
site.save()
self.clear_cached_site(request)
self.assertEqual(get_base_url(request), "http://other.example.com")
@override_settings(WAGTAILAPI_BASE_URL="https://bar.example.com")
def test_get_base_url_prefers_setting(self):
request = RequestFactory().get("/")
site = self.prepare_site()
self.assertEqual(site, Site.find_for_request(request))
self.assertEqual(get_base_url(request), "https://bar.example.com")
with override_settings(WAGTAILAPI_BASE_URL=None):
self.assertEqual(get_base_url(request), "http://other.example.com:8080")
@override_settings(WAGTAILAPI_BASE_URL="https://bar.example.com")
def test_get_base_url_from_setting_string(self):
self.assertEqual(get_base_url(), "https://bar.example.com")
@override_settings(WAGTAILAPI_BASE_URL=b"https://baz.example.com")
def test_get_base_url_from_setting_bytes(self):
self.assertEqual(get_base_url(), "https://baz.example.com")
@override_settings(WAGTAILAPI_BASE_URL=DynamicBaseUrl())
def test_get_base_url_from_setting_object(self):
self.assertEqual(get_base_url(), "https://www.example.com")
class TestParseFieldsParameter(TestCase):
# GOOD STUFF
def test_valid_single_field(self):
parsed = parse_fields_parameter("test")
self.assertEqual(
parsed,
[
("test", False, None),
],
)
def test_valid_multiple_fields(self):
parsed = parse_fields_parameter("test,another_test")
self.assertEqual(
parsed,
[
("test", False, None),
("another_test", False, None),
],
)
def test_valid_negated_field(self):
parsed = parse_fields_parameter("-test")
self.assertEqual(
parsed,
[
("test", True, None),
],
)
def test_valid_nested_fields(self):
parsed = parse_fields_parameter("test(foo,bar)")
self.assertEqual(
parsed,
[
(
"test",
False,
[
("foo", False, None),
("bar", False, None),
],
),
],
)
def test_valid_star_field(self):
parsed = parse_fields_parameter("*,-test")
self.assertEqual(
parsed,
[
("*", False, None),
("test", True, None),
],
)
def test_valid_star_with_additional_field(self):
# Note: '*,test' is not allowed but '*,test(foo)' is
parsed = parse_fields_parameter("*,test(foo)")
self.assertEqual(
parsed,
[
("*", False, None),
(
"test",
False,
[
("foo", False, None),
],
),
],
)
def test_valid_underscore_field(self):
parsed = parse_fields_parameter("_,test")
self.assertEqual(
parsed,
[
("_", False, None),
("test", False, None),
],
)
def test_valid_field_with_underscore_in_middle(self):
parsed = parse_fields_parameter("a_test")
self.assertEqual(
parsed,
[
("a_test", False, None),
],
)
def test_valid_negated_field_with_underscore_in_middle(self):
parsed = parse_fields_parameter("-a_test")
self.assertEqual(
parsed,
[
("a_test", True, None),
],
)
def test_valid_field_with_underscore_at_beginning(self):
parsed = parse_fields_parameter("_test")
self.assertEqual(
parsed,
[
("_test", False, None),
],
)
def test_valid_field_with_underscore_at_end(self):
parsed = parse_fields_parameter("test_")
self.assertEqual(
parsed,
[
("test_", False, None),
],
)
# BAD STUFF
def test_invalid_char(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter("test#")
self.assertEqual(str(e.exception), "unexpected char '#' at position 4")
def test_invalid_whitespace_before_identifier(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter(" test")
self.assertEqual(str(e.exception), "unexpected whitespace at position 0")
def test_invalid_whitespace_after_identifier(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter("test ")
self.assertEqual(str(e.exception), "unexpected whitespace at position 4")
def test_invalid_whitespace_after_comma(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter("test, test")
self.assertEqual(str(e.exception), "unexpected whitespace at position 5")
def test_invalid_whitespace_before_comma(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter("test ,test")
self.assertEqual(str(e.exception), "unexpected whitespace at position 4")
def test_invalid_unexpected_negation_operator(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter("test-")
self.assertEqual(str(e.exception), "unexpected char '-' at position 4")
def test_invalid_unexpected_open_bracket(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter("test,(foo)")
self.assertEqual(str(e.exception), "unexpected char '(' at position 5")
def test_invalid_unexpected_close_bracket(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter("test)")
self.assertEqual(str(e.exception), "unexpected char ')' at position 4")
def test_invalid_unexpected_comma_in_middle(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter("test,,foo")
self.assertEqual(str(e.exception), "unexpected char ',' at position 5")
def test_invalid_unexpected_comma_at_end(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter("test,foo,")
self.assertEqual(str(e.exception), "unexpected char ',' at position 9")
def test_invalid_unclosed_bracket(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter("test(foo")
self.assertEqual(
str(e.exception),
"unexpected end of input (did you miss out a close bracket?)",
)
def test_invalid_subfields_on_negated_field(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter("-test(foo)")
self.assertEqual(str(e.exception), "unexpected char '(' at position 5")
def test_invalid_star_field_in_wrong_position(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter("test,*")
self.assertEqual(str(e.exception), "'*' must be in the first position")
def test_invalid_negated_star(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter("-*")
self.assertEqual(str(e.exception), "'*' cannot be negated")
def test_invalid_star_with_nesting(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter("*(foo,bar)")
self.assertEqual(str(e.exception), "unexpected char '(' at position 1")
def test_invalid_star_with_chars_after(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter("*foo")
self.assertEqual(str(e.exception), "unexpected char 'f' at position 1")
def test_invalid_star_with_chars_before(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter("foo*")
self.assertEqual(str(e.exception), "unexpected char '*' at position 3")
def test_invalid_star_with_additional_field(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter("*,foo")
self.assertEqual(
str(e.exception), "additional fields with '*' doesn't make sense"
)
def test_invalid_underscore_in_wrong_position(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter("test,_")
self.assertEqual(str(e.exception), "'_' must be in the first position")
def test_invalid_negated_underscore(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter("-_")
self.assertEqual(str(e.exception), "'_' cannot be negated")
def test_invalid_underscore_with_nesting(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter("_(foo,bar)")
self.assertEqual(str(e.exception), "unexpected char '(' at position 1")
def test_invalid_underscore_with_negated_field(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter("_,-foo")
self.assertEqual(str(e.exception), "negated fields with '_' doesn't make sense")
def test_invalid_star_and_underscore(self):
with self.assertRaises(FieldsParameterParseError) as e:
parse_fields_parameter("*,_")
self.assertEqual(str(e.exception), "'_' must be in the first position")
class TestParseBoolean(TestCase):
# GOOD STUFF
def test_valid_true(self):
parsed = parse_boolean("true")
self.assertIs(parsed, True)
def test_valid_false(self):
parsed = parse_boolean("false")
self.assertIs(parsed, False)
def test_valid_1(self):
parsed = parse_boolean("1")
self.assertIs(parsed, True)
def test_valid_0(self):
parsed = parse_boolean("0")
self.assertIs(parsed, False)
# BAD STUFF
def test_invalid(self):
with self.assertRaises(ValueError) as e:
parse_boolean("foo")
self.assertEqual(str(e.exception), "expected 'true' or 'false', got 'foo'")
def test_invalid_integer(self):
with self.assertRaises(ValueError) as e:
parse_boolean("2")
self.assertEqual(str(e.exception), "expected 'true' or 'false', got '2'")