Add tests for statistical database REST API
Issue: HIC-112
Change-Id: I5d6da6f28f1be7e74fd39af08f79d5cb92d0e6c5
diff --git a/crashreport_stats/tests.py b/crashreport_stats/tests.py
index 7ce503c..a204d42 100644
--- a/crashreport_stats/tests.py
+++ b/crashreport_stats/tests.py
@@ -1,3 +1,440 @@
-from django.test import TestCase
+"""Test crashreport_stats models and the 'stats' command."""
+from datetime import datetime, date
+import pytz
-# Create your tests here.
+from django.urls import reverse
+from django.utils.http import urlencode
+
+from rest_framework import status
+from rest_framework.test import APITestCase, APIClient
+
+from crashreport_stats.models import (
+ Version, VersionDaily, RadioVersion, RadioVersionDaily
+)
+
+from crashreports.models import User, Device
+
+
+class Dummy():
+ """Class for creating dummy instances for testing."""
+
+ # Valid unique entries
+ BUILD_FINGERPRINTS = [(
+ 'Fairphone/FP2/FP2:5.1/FP2/r4275.1_FP2_gms76_1.13.0:user/release-keys'
+ ), (
+ 'Fairphone/FP2/FP2:5.1.1/FP2-gms75.1.13.0/FP2-gms75.1.13.0'
+ ':user/release-keys'
+ ), (
+ 'Fairphone/FP2/FP2:6.0.1/FP2-gms-18.04.1/FP2-gms-18.04.1'
+ ':user/release-keys'
+ ), (
+ 'Fairphone/FP2/FP2:7.1.2/18.07.2/gms-7480c31d'
+ ':user/release-keys'
+ )]
+ RADIO_VERSIONS = ['4437.1-FP2-0-07', '4437.1-FP2-0-08',
+ '4437.1-FP2-0-09', '4437.1-FP2-0-10']
+
+ DATES = [date(2018, 3, 19), date(2018, 3, 26), date(2018, 5, 1)]
+
+ DEFAULT_DUMMY_VERSION_VALUES = {
+ 'build_fingerprint': BUILD_FINGERPRINTS[0],
+ 'first_seen_on': DATES[1],
+ 'released_on': DATES[0]
+ }
+
+ DEFAULT_DUMMY_VERSION_DAILY_VALUES = {
+ 'date': DATES[1]
+ }
+
+ DEFAULT_DUMMY_RADIO_VERSION_VALUES = {
+ 'radio_version': RADIO_VERSIONS[0],
+ 'first_seen_on': DATES[1],
+ 'released_on': DATES[0]
+ }
+
+ DEFAULT_DUMMY_RADIO_VERSION_DAILY_VALUES = {
+ 'date': DATES[1]
+ }
+
+ DEFAULT_DUMMY_DEVICE_VALUES = {
+ 'board_date': datetime(2015, 12, 15, 1, 23, 45, tzinfo=pytz.utc),
+ 'chipset': 'Qualcomm MSM8974PRO-AA',
+ 'token': '64111c62d521fb4724454ca6dea27e18f93ef56e'
+ }
+
+ DEFAULT_DUMMY_USER_VALUES = {
+ 'username': 'testuser'
+ }
+
+ @staticmethod
+ def update_copy(original, update):
+ """Merge fields of update into a copy of original."""
+ data = original.copy()
+ data.update(update)
+ return data
+
+ @staticmethod
+ def create_dummy_user(**kwargs):
+ """Create a dummy user instance.
+
+ The dummy instance is created and saved to the database.
+ Args:
+ **kwargs:
+ Optional arguments to extend/overwrite the default values.
+
+ Returns: The created user instance.
+
+ """
+ entity = User(**Dummy.update_copy(
+ Dummy.DEFAULT_DUMMY_USER_VALUES, kwargs))
+ entity.save()
+ return entity
+
+ @staticmethod
+ def create_dummy_device(user, **kwargs):
+ """Create a dummy device instance.
+
+ The dummy instance is created and saved to the database.
+ Args:
+ user: The user instance that the device should relate to
+ **kwargs:
+ Optional arguments to extend/overwrite the default values.
+
+ Returns: The created device instance.
+
+ """
+ entity = Device(user=user, **Dummy.update_copy(
+ Dummy.DEFAULT_DUMMY_DEVICE_VALUES, kwargs))
+ entity.save()
+ return entity
+
+ @staticmethod
+ def create_dummy_version(**kwargs):
+ """Create a dummy version instance.
+
+ The dummy instance is created and saved to the database.
+ Args:
+ **kwargs:
+ Optional arguments to extend/overwrite the default values.
+
+ Returns: The created version instance.
+
+ """
+ entity = Version(**Dummy.update_copy(
+ Dummy.DEFAULT_DUMMY_VERSION_VALUES, kwargs))
+ entity.save()
+ return entity
+
+ @staticmethod
+ def create_dummy_radio_version(**kwargs):
+ """Create a dummy radio version instance.
+
+ The dummy instance is created and saved to the database.
+ Args:
+ **kwargs:
+ Optional arguments to extend/overwrite the default values.
+
+ Returns: The created radio version instance.
+
+ """
+ entity = RadioVersion(**Dummy.update_copy(
+ Dummy.DEFAULT_DUMMY_RADIO_VERSION_VALUES, kwargs))
+ entity.save()
+ return entity
+
+ @staticmethod
+ def create_dummy_daily_version(version, **kwargs):
+ """Create a dummy daily version instance.
+
+ The dummy instance is created and saved to the database.
+ Args:
+ **kwargs:
+ Optional arguments to extend/overwrite the default values.
+
+ Returns: The created daily version instance.
+
+ """
+ entity = VersionDaily(version=version, **Dummy.update_copy(
+ Dummy.DEFAULT_DUMMY_VERSION_DAILY_VALUES, kwargs))
+ entity.save()
+ return entity
+
+ @staticmethod
+ def create_dummy_daily_radio_version(version, **kwargs):
+ """Create a dummy daily radio version instance.
+
+ The dummy instance is created and saved to the database.
+ Args:
+ **kwargs:
+ Optional arguments to extend/overwrite the default values.
+
+ Returns: The created daily radio version instance.
+
+ """
+ entity = RadioVersionDaily(version=version, **Dummy.update_copy(
+ Dummy.DEFAULT_DUMMY_RADIO_VERSION_DAILY_VALUES, kwargs))
+ entity.save()
+ return entity
+
+
+class _VersionTestCase(APITestCase):
+ """Abstract class for version-related test cases to inherit from."""
+
+ # The attribute name characterising the unicity of a stats entry (the
+ # named identifier)
+ unique_entry_name = 'build_fingerprint'
+ # The collection of unique entries to post
+ unique_entries = Dummy.BUILD_FINGERPRINTS
+ # The URL to retrieve the stats entries from
+ endpoint_url = reverse('hiccup_stats_api_v1_versions')
+
+ @classmethod
+ def setUpTestData(cls): # noqa: N802
+ """Create an admin user for accessing the API.
+
+ The APIClient that can be used to make authenticated requests to the
+ server is stored in self.admin.
+ """
+ admin_user = User.objects.create_superuser(
+ 'somebody', 'somebody@example.com', 'thepassword')
+ cls.admin = APIClient()
+ cls.admin.force_authenticate(admin_user)
+
+ @staticmethod
+ def _create_dummy_version(**kwargs):
+ return Dummy.create_dummy_version(**kwargs)
+
+ def _get_with_params(self, url, params):
+ return self.admin.get('{}?{}'.format(url, urlencode(params)))
+
+ def _assert_result_length_is(self, response, count):
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertIn('results', response.data)
+ self.assertIn('count', response.data)
+ self.assertEqual(response.data['count'], count)
+ self.assertEqual(len(response.data['results']), count)
+
+ def _assert_device_owner_has_no_get_access(self, entries_url):
+ # Create a user and device
+ user = Dummy.create_dummy_user()
+ device = Dummy.create_dummy_device(user=user)
+
+ # Create authenticated client
+ user = APIClient()
+ user.credentials(HTTP_AUTHORIZATION='Token ' + device.token)
+
+ # Try getting entries using the client
+ response = user.get(entries_url)
+ self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+
+ def _assert_filter_result_matches(self, filter_params, expected_result):
+ # List entities with filter
+ response = self._get_with_params(self.endpoint_url, filter_params)
+
+ # Expect only the single matching result to be returned
+ self._assert_result_length_is(response, 1)
+ self.assertEqual(response.data['results'][0][self.unique_entry_name],
+ getattr(expected_result, self.unique_entry_name))
+
+
+class VersionTestCase(_VersionTestCase):
+ """Test the Version and REST endpoint."""
+
+ def _create_version_entities(self):
+ versions = [
+ self._create_dummy_version(
+ **{self.unique_entry_name: unique_entry}
+ )
+ for unique_entry in self.unique_entries
+ ]
+ return versions
+
+ def test_list_versions_without_authentication(self):
+ """Test listing of versions without authentication."""
+ response = self.client.get(self.endpoint_url)
+ self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+
+ def test_list_versions_as_device_owner(self):
+ """Test listing of versions as device owner."""
+ self._assert_device_owner_has_no_get_access(self.endpoint_url)
+
+ def test_list_versions_empty_database(self):
+ """Test listing of versions on an empty database."""
+ response = self.admin.get(self.endpoint_url)
+ self._assert_result_length_is(response, 0)
+
+ def test_list_versions(self):
+ """Test listing versions."""
+ versions = self._create_version_entities()
+ response = self.admin.get(self.endpoint_url)
+ self._assert_result_length_is(response, len(versions))
+
+ def test_filter_versions_by_unique_entry_name(self):
+ """Test filtering versions by their unique entry name."""
+ versions = self._create_version_entities()
+ response = self.admin.get(self.endpoint_url)
+
+ # Listing all entities should return the correct result length
+ self._assert_result_length_is(response, len(versions))
+
+ # List entities with filter
+ filter_params = {
+ self.unique_entry_name: getattr(versions[0],
+ self.unique_entry_name)
+ }
+ self._assert_filter_result_matches(filter_params,
+ expected_result=versions[0])
+
+ def test_filter_versions_by_release_type(self):
+ """Test filtering versions by release type."""
+ # Create versions for all combinations of release types
+ versions = []
+ i = 0
+ for is_official_release in True, False:
+ for is_beta_release in True, False:
+ versions.append(self._create_dummy_version(**{
+ 'is_official_release': is_official_release,
+ 'is_beta_release': is_beta_release,
+ self.unique_entry_name: self.unique_entries[i]
+ }))
+ i += 1
+
+ # # Listing all entities should return the correct result length
+ response = self.admin.get(self.endpoint_url)
+ self._assert_result_length_is(response, len(versions))
+
+ # List each of the entities with the matching filter params
+ for version in versions:
+ filter_params = {
+ 'is_official_release': version.is_official_release,
+ 'is_beta_release': version.is_beta_release
+ }
+ self._assert_filter_result_matches(filter_params,
+ expected_result=version)
+
+ def test_filter_versions_by_first_seen_date(self):
+ """Test filtering versions by first seen date."""
+ versions = self._create_version_entities()
+
+ # Set the first seen date of an entity
+ versions[0].first_seen_on = Dummy.DATES[2]
+ versions[0].save()
+
+ # Listing all entities should return the correct result length
+ response = self.admin.get(self.endpoint_url)
+ self._assert_result_length_is(response, len(versions))
+
+ # Expect the single matching result to be returned
+ filter_params = {'first_seen_after': Dummy.DATES[2]}
+ self._assert_filter_result_matches(filter_params,
+ expected_result=versions[0])
+
+
+# pylint: disable=too-many-ancestors
+class RadioVersionTestCase(VersionTestCase):
+ """Test the RadioVersion REST endpoint."""
+
+ unique_entry_name = 'radio_version'
+ unique_entries = Dummy.RADIO_VERSIONS
+ endpoint_url = reverse('hiccup_stats_api_v1_radio_versions')
+
+ @staticmethod
+ def _create_dummy_version(**kwargs):
+ return Dummy.create_dummy_radio_version(**kwargs)
+
+
+class VersionDailyTestCase(_VersionTestCase):
+ """Test the VersionDaily REST endpoint."""
+
+ endpoint_url = reverse('hiccup_stats_api_v1_version_daily')
+
+ @staticmethod
+ def _create_dummy_daily_version(version, **kwargs):
+ return Dummy.create_dummy_daily_version(version, **kwargs)
+
+ def _create_version_entities(self):
+ versions = [
+ self._create_dummy_version(
+ **{self.unique_entry_name: unique_entry}
+ )
+ for unique_entry in self.unique_entries
+ ]
+ versions_daily = [
+ self._create_dummy_daily_version(version=version)
+ for version in versions
+ ]
+ return versions_daily
+
+ def test_list_daily_versions_without_authentication(self):
+ """Test listing of daily versions without authentication."""
+ response = self.client.get(self.endpoint_url)
+ self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+
+ def test_list_daily_versions_as_device_owner(self):
+ """Test listing of daily versions as device owner."""
+ self._assert_device_owner_has_no_get_access(self.endpoint_url)
+
+ def test_list_daily_versions_empty_database(self):
+ """Test listing of daily versions on an empty database."""
+ response = self.admin.get(self.endpoint_url)
+ self._assert_result_length_is(response, 0)
+
+ def test_list_daily_versions(self):
+ """Test listing daily versions."""
+ versions_daily = self._create_version_entities()
+ response = self.admin.get(self.endpoint_url)
+ self._assert_result_length_is(response, len(versions_daily))
+
+ def test_filter_daily_versions_by_version(self):
+ """Test filtering versions by the version they relate to."""
+ # Create VersionDaily entities
+ versions = self._create_version_entities()
+
+ # Listing all entities should return the correct result length
+ response = self.admin.get(self.endpoint_url)
+ self._assert_result_length_is(response, len(versions))
+
+ # List entities with filter
+ param_name = 'version__' + self.unique_entry_name
+ filter_params = {
+ param_name: getattr(versions[0].version, self.unique_entry_name)
+ }
+ self._assert_filter_result_matches(filter_params,
+ expected_result=versions[0].version)
+
+ def test_filter_daily_versions_by_date(self):
+ """Test filtering daily versions by date."""
+ # Create Version and VersionDaily entities
+ versions = self._create_version_entities()
+
+ # Update the date
+ versions[0].date = Dummy.DATES[2]
+ versions[0].save()
+
+ # Listing all entities should return the correct result length
+ response = self.admin.get(self.endpoint_url)
+ self._assert_result_length_is(response, len(versions))
+
+ # Expect the single matching result to be returned
+ filter_params = {'date': versions[0].date}
+ self._assert_filter_result_matches(filter_params,
+ expected_result=versions[0].version)
+
+
+class RadioVersionDailyTestCase(VersionDailyTestCase):
+ """Test the RadioVersionDaily REST endpoint."""
+
+ unique_entry_name = 'radio_version'
+ unique_entries = Dummy.RADIO_VERSIONS
+ endpoint_url = reverse('hiccup_stats_api_v1_radio_version_daily')
+
+ @staticmethod
+ def _create_dummy_version(**kwargs):
+ entity = RadioVersion(**Dummy.update_copy(
+ Dummy.DEFAULT_DUMMY_RADIO_VERSION_VALUES, kwargs))
+ entity.save()
+ return entity
+
+ @staticmethod
+ def _create_dummy_daily_version(version, **kwargs):
+ return Dummy.create_dummy_daily_radio_version(version, **kwargs)