def test_list_count_included(self, page_size_param, api_client): Movie.objects.create(name="foo123") Movie.objects.create(name="test") response = api_client.get("/v1/movies", data={"_count": "true"}) data = read_response_json(response) assert response.status_code == 200 assert response["X-Total-Count"] == "2" assert data["page"]["totalElements"] == 2 assert response["X-Pagination-Count"] == "1" assert data["page"]["totalPages"] == 1 Movie.objects.create(name="bla") response = api_client.get("/v1/movies", data={ "_count": "true", page_size_param: 2 }) assert response.status_code == 200 data = read_response_json(response) assert response["X-Total-Count"] == "3" assert data["page"]["totalElements"] == 3 assert response["X-Pagination-Count"] == "2" assert data["page"]["totalPages"] == 2
def test_list_filter_wildcard(api_client): """Prove that ?name=foo doesn't works with wildcards""" Movie.objects.create(name="foo123") Movie.objects.create(name="test") response = api_client.get("/v1/movies", data={"name": "foo1?3"}) read_response_json(response) assert response.status_code == 200, response assert response["Content-Type"] == "application/hal+json"
def test_remote_schema_validation( api_client, fetch_auth_token, router, brp_dataset, urllib3_mocker, filled_router ): """Prove that the schema is validated.""" urllib3_mocker.add( "GET", "/unittest/brp/ingeschrevenpersonen/999990901", body=orjson.dumps({"secret": "I should not appear in the error response or the log"}), content_type="application/json", ) # Prove that URLs can now be resolved. url = reverse("dynamic_api:brp-ingeschrevenpersonen-detail", kwargs={"pk": "999990901"}) token = fetch_auth_token(["BRP/R"]) response = api_client.get(url, HTTP_AUTHORIZATION=f"Bearer {token}") data = read_response_json(response) assert response["content-type"] == "application/problem+json" # check before reading assert response.status_code == 502, data assert response["content-type"] == "application/problem+json" # and after assert data == { "type": "urn:apiexception:validation_errors", "code": "validation_errors", "title": "Invalid remote data", "status": 502, "instance": "http://testserver/v1/remote/brp/ingeschrevenpersonen/999990901/", "detail": "Some fields in the remote's response did not match the schema", } # Same, but now for a list view. urllib3_mocker.add( "GET", "/unittest/brp/ingeschrevenpersonen", body=orjson.dumps([{"secret": "I should not appear in the error response or the log"}]), content_type="application/json", ) # Prove that URLs can now be resolved. url = reverse("dynamic_api:brp-ingeschrevenpersonen-list") response = api_client.get(url, HTTP_AUTHORIZATION=f"Bearer {token}") data = read_response_json(response) assert response["content-type"] == "application/problem+json" # check before reading assert response.status_code == 502, data assert response["content-type"] == "application/problem+json" # and after assert data == { "type": "urn:apiexception:validation_errors", "code": "validation_errors", "title": "Invalid remote data", "status": 502, "instance": "http://testserver/v1/remote/brp/ingeschrevenpersonen/", "detail": "Some fields in the remote's response did not match the schema", }
def test_geofilter_contains(parkeervakken_parkeervak_model, filled_router): """ Prove that geofilter contains filters work as expected. """ parkeervakken_parkeervak_model.objects.create( id="121138489006", type="File", soort="MULDER", aantal=1.0, e_type="E6b", buurtcode="A05d", straatnaam="Zoutkeetsgracht", geometry=GEOSGeometry( "POLYGON((121140.66 489048.21, 121140.72 489047.1, 121140.8 489046.9, 121140.94 " "489046.74,121141.11 489046.62, 121141.31 489046.55, 121141.52 489046.53, " "121134.67 489045.85, 121134.47 489047.87, 121140.66 489048.21))", 28992, ), ) response = APIClient().get( "/v1/parkeervakken/parkeervakken/", data={"geometry[contains]": "52.388231,4.8897865"}, HTTP_ACCEPT_CRS=4326, ) data = read_response_json(response) assert len(data["_embedded"]["parkeervakken"]) == 1 response = APIClient().get( "/v1/parkeervakken/parkeervakken/", data={"geometry[contains]": "52.3883019,4.8900356"}, HTTP_ACCEPT_CRS=4326, ) data = read_response_json(response) assert len(data["_embedded"]["parkeervakken"]) == 0 response = APIClient().get( "/v1/parkeervakken/parkeervakken/", data={"geometry[contains]": "121137.7,489046.9"}, HTTP_ACCEPT_CRS=28992, ) data = read_response_json(response) assert len(data["_embedded"]["parkeervakken"]) == 1 response = APIClient().get( "/v1/parkeervakken/parkeervakken/", data={"geometry[contains]": "52.388231,48897865"}, HTTP_ACCEPT_CRS=4326, ) assert response.status_code == 400
def test_limit_expanded_field_exclude(self, api_client, movie): """Prove that excluding a subfield still gives the whole main object.""" response = api_client.get( "/v1/movies", data={ "_fields": "-category.last_updated_by.name", "_expandScope": "category.last_updated_by", }, ) data = read_response_json(response) assert response.status_code == 200, data assert data["_embedded"] == { "movie": [ # Still complete movie: { "category_id": 1, "date_added": None, "name": "foo123" }, ], "category": [{ "name": "bar", "_embedded": { "last_updated_by": {}, # removed name (the only field) }, }], }
def test_detail_expand_scope(self, api_client, movie, params): """Prove that ?_expandScope works fine for a single level. Nesting also doesn't go deeper here. """ response = api_client.get(f"/v1/movies/{movie.pk}", data=params) data = read_response_json(response) assert data == { "name": "foo123", "category_id": movie.category_id, "date_added": None, "_embedded": { "actors": [ { "name": "John Doe" }, { "name": "Jane Doe" }, ], "category": { "name": "bar" }, }, } assert response["Content-Type"] == "application/hal+json"
def test_remote_timeout(api_client, fetch_auth_token, router, brp_dataset, urllib3_mocker): """Prove that the remote router can proxy the other service.""" def _raise_timeout(request): raise urllib3.exceptions.TimeoutError() router.reload() urllib3_mocker.add_callback( "GET", "/unittest/brp/ingeschrevenpersonen/999990901", callback=_raise_timeout, content_type="application/json", ) # Prove that URLs can now be resolved. url = reverse("dynamic_api:brp-ingeschrevenpersonen-detail", kwargs={"pk": "999990901"}) token = fetch_auth_token(["BRP/R"]) response = api_client.get(url, HTTP_AUTHORIZATION=f"Bearer {token}") data = read_response_json(response) assert response.status_code == 504, data assert data == { "type": "urn:apiexception:gateway_timeout", "title": "Connection failed (server timeout)", "detail": "Connection failed (server timeout)", "status": 504, }
def test_haalcentraalbrk_client( api_client, fetch_auth_token, router, hcbrk_dataset, urllib3_mocker, case ): """Test Haal Centraal BRK remote client.""" def respond(request): assert "Authorization" not in request.headers assert "X-Api-Key" in request.headers assert request.body is None return (200, {"Content-Crs": "epsg:28992"}, case["from_kadaster"]) table, ident = case["table"], case["ident"] router.reload() urllib3_mocker.add_callback( "GET", f"/esd/bevragen/v1/{table}/{ident}", callback=respond, content_type="application/json", ) url = reverse(f"dynamic_api:haalcentraalbrk-{table}-detail", kwargs={"pk": str(ident)}) token = fetch_auth_token(case["scopes"]) response = api_client.get(url, HTTP_AUTHORIZATION=f"Bearer {token}") data = read_response_json(response) assert response.status_code == 200, data assert data == case["output"]
def test_limit_expanded_field(self, api_client, movie): """Prove that ?_fields=name results in result with only names""" response = api_client.get( "/v1/movies", data={ "_fields": "name,category.name,category.last_updated_by.name", "_expandScope": "category.last_updated_by", }, ) data = read_response_json(response) assert response.status_code == 200, data assert data["_embedded"] == { "movie": [ { "name": "foo123" }, ], "category": [ { "name": "bar", "_embedded": { "last_updated_by": { "name": "bar_man" }, }, }, ], }
def test_haalcentraalbrk_geojson( api_client, fetch_auth_token, router, hcbrk_dataset, urllib3_mocker ): """Test whether remote API responses are properly converted.""" def respond(request): assert "Authorization" not in request.headers assert "X-Api-Key" in request.headers assert request.body is None return (200, {"Content-Crs": "epsg:28992"}, HCBRK_ONROERENDE_ZAAK) router.reload() urllib3_mocker.add_callback( "GET", "/esd/bevragen/v1/kadastraalonroerendezaken/76870487970000", callback=respond, content_type="application/json", ) url = reverse( "dynamic_api:haalcentraalbrk-kadastraalonroerendezaken-detail", kwargs={"pk": "76870487970000"}, ) token = fetch_auth_token(["BRK/RS", "BRK/RO"]) response = api_client.get(url, {"_format": "geojson"}, HTTP_AUTHORIZATION=f"Bearer {token}") data = read_response_json(response) assert response.status_code == 200, data rounder = lambda p: [round(c, 6) for c in p] # Prove that coordinates are properly transformed from RD/NEW to WGS84 plaatscoordinaten = data["properties"]["plaatscoordinaten"] assert plaatscoordinaten["type"] == "Point" assert rounder(plaatscoordinaten["coordinates"]) == [5.966022, 52.164126]
def test_incorrect_field_in_fields_results_in_error(self, api_client): """Prove that adding invalid name to ?_fields will result in error""" api_client.raise_request_exception = False response = api_client.get("/v1/movies", data={"_fields": "name,date"}) data = read_response_json(response) assert response.status_code == 400, data assert data == { "type": "urn:apiexception:invalid", "title": "Invalid input.", "status": 400, "instance": "http://testserver/v1/movies?_fields=name%2Cdate", "invalid-params": [{ "type": "urn:apiexception:invalid:fields", "name": "fields", "reason": "The following field name is invalid: 'date'.", }], "x-validation-errors": ["The following field name is invalid: 'date'."], }
def test_coordinate_conversion_fail(api_client, api_rf): """Prove that some errors will be properly displayed as human readable. The test run causes an error in the first record, which is examined before the streaming starts. This triggers the standard exception handler. """ with connection.cursor() as c: # Create an "POINT(Infinity Infinity)" coordinate without triggering # any validations at Python-level. c.execute( f"INSERT INTO {Location._meta.db_table} (id, geometry) VALUES (1, %s)", ("010100002040710000000000000000F07F000000000000F07F", ), ) response = api_client.get("/v1/locations", HTTP_ACCEPT_CRS="EPSG:4258") data = read_response_json(response) assert response[ "content-type"] == "application/problem+json", data # check before reading assert data == { "detail": ("Fout tijdens coördinaatconversie voor location #1. Neem a.u.b. " "contact op met de bronhouder van deze data om dit probleem op te lossen." ), "instance": "http://testserver/v1/locations", "status": 500, "title": "Coordinate conversion failed", "type": "urn:apiexception:error", }
def test_list_filter_datetime_invalid(api_client): """Prove that invalid input is captured, and returns a proper error response.""" response = api_client.get("/v1/movies", data={"date_added": "2020-01-fubar"}) assert response.status_code == 400, response assert response[ "Content-Type"] == "application/problem+json", response # check first data = read_response_json(response) assert response[ "Content-Type"] == "application/problem+json", response # and after assert data == { "type": "urn:apiexception:invalid", "title": "Invalid input.", "status": 400, "instance": "http://testserver/v1/movies?date_added=2020-01-fubar", "invalid-params": [{ "type": "urn:apiexception:invalid:invalid", "name": "date_added", "reason": "Enter a valid ISO date-time, or single date.", }], "x-validation-errors": { "date_added": ["Enter a valid ISO date-time, or single date."] }, }
def test_filter_isempty(parkeervakken_parkeervak_model, filled_router): parkeervakken_parkeervak_model.objects.create( id="121138489006", type="File", soort="MULDER", aantal=1.0, e_type="E6b", buurtcode="A05d", straatnaam="Zoutkeetsgracht", ) parkeervakken_parkeervak_model.objects.create( id="121138489007", type="File", soort="", aantal=1.0, e_type="E6b", buurtcode="A05d", straatnaam="Zoutkeetsgracht", ) parkeervakken_parkeervak_model.objects.create( id="121138489008", type="File", soort=None, aantal=1.0, e_type="E6b", buurtcode="A05d", straatnaam="Zoutkeetsgracht", ) response = APIClient().get( "/v1/parkeervakken/parkeervakken/", data={"soort[isempty]": "true"}, ) data = read_response_json(response) assert len(data["_embedded"]["parkeervakken"]) == 2 assert ( data["_embedded"]["parkeervakken"][0]["id"] == "121138489007" or data["_embedded"]["parkeervakken"][0]["id"] == "121138489008" ) response = APIClient().get( "/v1/parkeervakken/parkeervakken/", data={"soort[isempty]": "false"}, ) data = read_response_json(response) assert len(data["_embedded"]["parkeervakken"]) == 1 assert data["_embedded"]["parkeervakken"][0]["id"] == "121138489006"
def test_detail_expand_false(self, api_client, movie, params): """Prove that ?_expand=false doesn't trigger expansion.""" response = api_client.get(f"/v1/movies/{movie.pk}", data=params) data = read_response_json(response) assert data == { "name": "foo123", "category_id": movie.category_id, "date_added": None, } assert response["Content-Type"] == "application/hal+json"
def test_remote_400_problem_json( api_client, fetch_auth_token, router, brp_dataset, urllib3_mocker ): """Prove that the schema is validated.""" router.reload() urllib3_mocker.add( "GET", "/unittest/brp/ingeschrevenpersonen/999990901342", status=400, body=orjson.dumps( { "type": ( "https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html" "#sec10.4.1 400 Bad Request" ), "title": "Een of meerdere parameters zijn niet correct.", "status": 400, "detail": "De foutieve parameter(s) zijn: burgerservicenummer.", "instance": "http://testserver/v1/remote/brp/ingeschrevenpersonen/999990901342/", "code": "paramsValidation", "invalid-params": [ { "code": "maxLength", "reason": "Waarde is langer dan maximale lengte 9.", "name": "burgerservicenummer", } ], } ), content_type="application/problem+json", ) # Prove that URLs can now be resolved. url = reverse("dynamic_api:brp-ingeschrevenpersonen-detail", kwargs={"pk": "999990901342"}) token = fetch_auth_token(["BRP/R"]) response = api_client.get(url, HTTP_AUTHORIZATION=f"Bearer {token}") data = read_response_json(response) assert response["content-type"] == "application/problem+json" # check before reading assert response.status_code == 400, data assert data == { "type": "urn:apiexception:parse_error", # changed for consistency! "title": "Malformed request.", "status": 400, "detail": "De foutieve parameter(s) zijn: burgerservicenummer.", "instance": "http://testserver/v1/remote/brp/ingeschrevenpersonen/999990901342/", "code": "parse_error", # changed for consistency! "invalid-params": [ { "code": "maxLength", "reason": "Waarde is langer dan maximale lengte 9.", "name": "burgerservicenummer", } ], }
def test_details_can_be_requested_with_version(self, api_client, stadsdelen): """Prove that object can be requested by identification and version.""" identificatie = stadsdelen[0].identificatie url = reverse("dynamic_api:gebieden-stadsdelen-detail", args=(identificatie, )) response = api_client.get(url, {"volgnummer": "1"}) data = read_response_json(response) assert response.status_code == 200, data assert data["_links"]["self"]["volgnummer"] == 1, response.data
def test_list_default_active_objects(self, api_client, stadsdelen): """Prove that default API response contains only active versions.""" url = reverse("dynamic_api:gebieden-stadsdelen-list") response = api_client.get(url) data = read_response_json(response) assert response.status_code == 200, data stadsdelen = data["_embedded"]["stadsdelen"] assert len(stadsdelen) == 1, stadsdelen assert stadsdelen[0]["_links"]["self"]["volgnummer"] == 2, stadsdelen[ 0]
def test_list_ordering_name_old_param(api_client): """Prove that ?_sort=... works on the list view.""" Movie.objects.create(name="test") Movie.objects.create(name="foo123") # Sort descending by name response = api_client.get("/v1/movies", data={"sorteer": "-name"}) data = read_response_json(response) assert response.status_code == 200, data names = [movie["name"] for movie in data["_embedded"]["movie"]] assert names == ["test", "foo123"]
def test_details_record_can_be_requested_by_pk(self, api_client, stadsdelen): """Prove that request with PK (combined field) is allowed. It still needs ?geldigOp=* or it will not find the "deleted" record. """ url = reverse("dynamic_api:gebieden-stadsdelen-detail", args=(stadsdelen[0].id, )) response = api_client.get(url, {"geldigOp": "*"}) data = read_response_json(response) assert response.status_code == 200, data assert data["_links"]["self"]["volgnummer"] == stadsdelen[ 0].volgnummer, data
def test_details_default_returns_latest_record(self, api_client, stadsdelen): """Prove that object can be requested by identification and response will contain only latest object.""" identificatie = stadsdelen[0].identificatie url = reverse("dynamic_api:gebieden-stadsdelen-detail", args=(identificatie, )) response = api_client.get(url) data = read_response_json(response) assert response.status_code == 200, data assert data["_links"]["self"]["volgnummer"] == 2, data assert data["id"] == stadsdelen[1].id, data
def test_list_all_objects(self, api_client, stadsdelen): """Prove that API response can return ALL versions if requested.""" url = reverse("dynamic_api:gebieden-stadsdelen-list") response = api_client.get(url, {"geldigOp": "*"}) data = read_response_json(response) assert response.status_code == 200, data stadsdelen = data["_embedded"]["stadsdelen"] assert len(stadsdelen) == 2, stadsdelen assert stadsdelen[0]["_links"]["self"]["volgnummer"] == 1, stadsdelen[ 0] assert stadsdelen[1]["_links"]["self"]["volgnummer"] == 2, stadsdelen[ 1]
def test_list_ordering_date(api_client): """Prove that ?_sort=... works on the list view.""" Movie.objects.create(name="foo123", date_added=datetime(2020, 1, 1, 0, 45)) Movie.objects.create(name="test", date_added=datetime(2020, 2, 2, 13, 15)) # Sort descending by name response = api_client.get("/v1/movies", data={"_sort": "-date_added"}) data = read_response_json(response) assert response.status_code == 200, data names = [movie["name"] for movie in data["_embedded"]["movie"]] assert names == ["test", "foo123"]
def test_filtered_list_contains_only_correct_objects( self, api_client, stadsdelen, buurt): """Prove that date filter displays only active-on-that-date objects.""" url = reverse("dynamic_api:gebieden-stadsdelen-list") response = api_client.get(f"{url}?geldigOp=2015-01-02") data = read_response_json(response) assert response.status_code == 200, data assert len(data["_embedded"] ["stadsdelen"]) == 1, data["_embedded"]["stadsdelen"] stadsdelen = data["_embedded"]["stadsdelen"] assert stadsdelen[0]["_links"]["self"]["volgnummer"] == 2, stadsdelen[ 0]
def test_pagination_many(drf_request, movie): """Prove that the serializer can embed data (for the detail page)""" queryset = Movie.objects.all() serializer = MovieSerializer( many=True, instance=queryset, fields_to_expand=["actors", "category"], context={"request": drf_request}, ) paginator = DSOPageNumberPagination() paginator.paginate_queryset(queryset, drf_request) response = paginator.get_paginated_response(serializer.data) # Since response didn't to through APIView.finalize_response(), fix that: response.accepted_renderer = drf_request.accepted_renderer response.accepted_media_type = drf_request.accepted_renderer.media_type response.renderer_context = {"request": drf_request} data = read_response_json(response.render()) assert data == { "_links": { "self": { "href": "http://testserver/v1/dummy/" }, }, "_embedded": { "movie": [{ "name": "foo123", "category_id": movie.category_id, "date_added": None }], "actors": [ { "name": "John Doe" }, { "name": "Jane Doe" }, ], "category": [{ "name": "bar" }], }, "page": { "number": 1, "size": 20 }, } assert response["X-Pagination-Page"] == "1" assert response["X-Pagination-Limit"] == "20"
def test_list_filter_datetime(api_client): """Prove that datetime fields can be queried using a single data value""" Movie.objects.create(name="foo123", date_added=datetime(2020, 1, 1, 0, 45)) Movie.objects.create(name="test", date_added=datetime(2020, 2, 2, 13, 15)) response = api_client.get("/v1/movies", data={"date_added": "2020-01-01"}) data = read_response_json(response) assert response.status_code == 200, response names = [movie["name"] for movie in data["_embedded"]["movie"]] assert names == ["foo123"] assert response["Content-Type"] == "application/hal+json"
def test_list_expand_true(self, api_client, movie): """Prove that ?_expand both work for the list view.""" response = api_client.get("/v1/movies", data={"_expand": "true"}) data = read_response_json(response) assert data == { "_links": { "self": { "href": "http://testserver/v1/movies?_expand=true" }, }, "_embedded": { "movie": [{ "name": "foo123", "category_id": movie.category_id, "date_added": None }], "actors": [ { "name": "John Doe", "_embedded": { "last_updated_by": None }, }, { "name": "Jane Doe", "_embedded": { "last_updated_by": { "name": "jane_updater" } }, }, ], "category": [{ "name": "bar", "_embedded": { "last_updated_by": { "name": "bar_man" } }, }], }, "page": { "number": 1, "size": 20 }, } assert response["Content-Type"] == "application/hal+json"
def test_invalid_format(api_client, api_rf): """Prove that failed content negotiation still renders an error""" api_client.raise_request_exception = False response = api_client.get("/v1/movies", data={"_format": "jsonfdsfds"}) assert response.status_code == 404 assert response[ "content-type"] == "application/problem+json" # check before reading data = read_response_json(response) # This 404 originates from DefaultContentNegotiation.filter_renderers() # Raising HTTP 406 Not Acceptable would only apply to HTTP Accept headers. assert data == { "detail": "Not found.", "status": 404, "title": "", "type": "urn:apiexception:not_found", }
def test_limit_one_field(self, api_client): """Prove that ?_fields=name results in result with only names""" Movie.objects.create(name="test") Movie.objects.create(name="foo123") response = api_client.get("/v1/movies", data={"_fields": "name"}) data = read_response_json(response) assert response.status_code == 200, data assert data["_embedded"] == { "movie": [ { "name": "foo123" }, { "name": "test" }, ] }
def test_list_count_excluded(self, api_client, django_assert_num_queries, data): Movie.objects.create(name="foo123") Movie.objects.create(name="test") with django_assert_num_queries(2) as captured: response = api_client.get("/v1/movies", data=data) # Make sure we are not inadvertently executing a COUNT assert all( ["COUNT" not in q["sql"] for q in captured.captured_queries]) data = read_response_json(response) assert response.status_code == 200 assert "X-Total-Count" not in response assert "X-Pagination-Count" not in response assert "totalElements" not in data["page"] assert "totalPages" not in data["page"]