Repository: WordPress/openverse-api

Issue:
Results may be `None` during tallying
## Sentry link

<!-- The public (aka "share") Sentry issue link. -->

https://openverse.sentry.io/share/issue/eaeee780feef4a96944d4e5ef4e6d39b/

## Description

<!-- Example: We are trying to access property foo of ImportantClass but the instance is null. -->
<!-- Include any additional information you may have, including potential remedies if any come to mind, and the general context of the code (what causes it to run in the app).  -->

<!-- Mention whether this is a known regression, i.e., the feature used to work and now does not.  -->

It appears as though the value of `results` can be `None` during the tallying step:

```python
TypeError: 'NoneType' object is not subscriptable
  File "django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
  File "django/core/handlers/base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "django/views/decorators/csrf.py", line 54, in wrapped_view
    return view_func(*args, **kwargs)
  File "rest_framework/viewsets.py", line 125, in view
    return self.dispatch(request, *args, **kwargs)
  File "rest_framework/views.py", line 509, in dispatch
    response = self.handle_exception(exc)
  File "rest_framework/views.py", line 469, in handle_exception
    self.raise_uncaught_exception(exc)
  File "rest_framework/views.py", line 480, in raise_uncaught_exception
    raise exc
  File "rest_framework/views.py", line 506, in dispatch
    response = handler(request, *args, **kwargs)
  File "django/utils/decorators.py", line 46, in _wrapper
    return bound_method(*args, **kwargs)
  File "catalog/api/views/media_views.py", line 88, in list
    results, num_pages, num_results = search_controller.search(
  File "catalog/api/controllers/search_controller.py", line 429, in search
    results_to_tally = results[: 80 - (max_result_depth - page_size)]
```

This is right after a log line, `no image urls to validate`

## Reproduction

<!-- Share the steps to reproduce the issue, if you were able to, OR a note sharing that you tried to reproduce but weren’t able to. -->

I _am_ able to reproduce this with the URL: https://api.openverse.engineering/v1/images/?license=by,by-nd,by-sa,cc0,pdm&license_type=commercial&filter_dead=true&mature=false&size=medium,large&page_size=100&page=1&q=kobinet

**Note**: You must use an authenticated app to request `page_size=100`; I tried pages 1-10 of the base `page_size` and I was unable to reproduce the error.



Discussion:
Potentially introduced in #1113 
@AetherUnbound Ahhh, interesting edge case. The falsey check on `results` needs to wrap the entire block of page size checks, right?

Resolving patch:
diff --git a/api/catalog/api/controllers/search_controller.py b/api/catalog/api/controllers/search_controller.py
--- a/api/catalog/api/controllers/search_controller.py
+++ b/api/catalog/api/controllers/search_controller.py
@@ -413,11 +413,11 @@ def search(
         search_response, results, page_size, page
     )
 
+    results_to_tally = results or []
     max_result_depth = page * page_size
     if max_result_depth <= 80:
         # Applies when `page_size * page` could land "evenly" on 80
         should_tally = True
-        results_to_tally = results
     elif max_result_depth - page_size < 80:
         # Applies when `page_size * page` could land beyond 80, but still
         # encompass some results on _this page_ that are at or below the 80th
@@ -426,7 +426,7 @@ def search(
         # the first eight results in `results` that are below or at the 80th
         # position for the query.
         should_tally = True
-        results_to_tally = results[: 80 - (max_result_depth - page_size)]
+        results_to_tally = results_to_tally[: 80 - (max_result_depth - page_size)]
     else:
         should_tally = False
 


Test patch:
diff --git a/api/test/unit/controllers/test_search_controller.py b/api/test/unit/controllers/test_search_controller.py
--- a/api/test/unit/controllers/test_search_controller.py
+++ b/api/test/unit/controllers/test_search_controller.py
@@ -463,3 +463,43 @@ def test_search_tallies_pages_less_than_5(
         assert len(passed_results) == number_of_results_passed
     else:
         count_provider_occurrences_mock.assert_not_called()
+
+
+@pytest.mark.parametrize(
+    "index",
+    (
+        "image",
+        "audio",
+    ),
+)
+@mock.patch.object(
+    tallies, "count_provider_occurrences", wraps=tallies.count_provider_occurrences
+)
+@mock.patch(
+    "catalog.api.controllers.search_controller._post_process_results",
+)
+@pytest.mark.django_db
+def test_search_tallies_handles_empty_page(
+    mock_post_process_results,
+    count_provider_occurrences_mock: mock.MagicMock,
+    index,
+    request_factory,
+):
+    mock_post_process_results.return_value = None
+
+    serializer = MediaSearchRequestSerializer(data={"q": "dogs"})
+    serializer.is_valid()
+
+    search_controller.search(
+        search_params=serializer,
+        ip=0,
+        index=index,
+        # Force calculated result depth length to include results within 80th position and above
+        # to force edge case where retrieved results are only partially tallied.
+        page=1,
+        page_size=100,
+        request=request_factory.get("/"),
+        filter_dead=True,
+    )
+
+    count_provider_occurrences_mock.assert_not_called()