blob: d2c3e1a08eed2309fbd0b4b388a9e46e0ef6b28e [file] [log] [blame]
"""Tests for the heartbeats REST API."""
from datetime import timedelta, datetime
import unittest
import pytz
from django.db import connection
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient
from crashreports.tests.utils import (
Dummy,
RaceConditionsTestCase,
HiccupCrashreportsAPITestCase,
)
from crashreports.models import HeartBeat
class HeartbeatsTestCase(HiccupCrashreportsAPITestCase):
"""Test cases for heartbeats."""
# pylint: disable=too-many-public-methods,too-many-ancestors
LIST_CREATE_URL = "api_v1_heartbeats"
RETRIEVE_URL = "api_v1_heartbeat"
LIST_CREATE_BY_UUID_URL = "api_v1_heartbeats_by_uuid"
RETRIEVE_BY_UUID_URL = "api_v1_heartbeat_by_uuid"
@staticmethod
def _create_dummy_data(**kwargs):
return Dummy.heartbeat_data(**kwargs)
@staticmethod
def _create_alternative_dummy_data(**kwargs):
return Dummy.alternative_heartbeat_data(**kwargs)
def _post_multiple(self, client, data, count):
"""Send multiple POST requests to create reports.
Note that the date of the data will be adapted for each POST request
so that no duplicate reports are being created. However, the given
`data` parameter value will not be modified.
Args:
client: The client used for sending the requests
data: The data that is sent each request
count: The number of requests that should be made
Returns: A list of HTTP response objects
"""
results = []
data_to_send = data.copy()
for i in range(count):
data_to_send["date"] += timedelta(days=i)
results.append(
client.post(reverse(self.LIST_CREATE_URL), data_to_send)
)
return results
def _retrieve_single(self, user):
count = 5
response = self._post_multiple(self.fp_staff_client, self.data, count)
self.assertEqual(len(response), count)
self.assertEqual(response[0].status_code, status.HTTP_201_CREATED)
url = reverse(self.RETRIEVE_URL, args=[response[0].data["id"]])
request = user.get(url)
return request.status_code
def _retrieve_single_by_device(self, user):
count = 5
response = self._post_multiple(self.user, self.data, count)
self.assertEqual(len(response), count)
self.assertEqual(response[0].status_code, status.HTTP_201_CREATED)
url = reverse(
self.RETRIEVE_BY_UUID_URL,
args=[self.uuid, response[0].data["device_local_id"]],
)
request = user.get(url)
return request.status_code
def setUp(self):
"""Set up a device and some data."""
super().setUp()
self.uuid, self.user, self.token = self._register_device()
self.data = self._create_dummy_data(uuid=self.uuid)
def test_create_no_auth(self):
"""Test creation without authentication."""
noauth_client = APIClient()
response = noauth_client.post(reverse(self.LIST_CREATE_URL), self.data)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_create_as_fp_staff(self):
"""Test creation as Fairphone staff."""
response = self.fp_staff_client.post(
reverse(self.LIST_CREATE_URL), self.data
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertTrue(response.data["id"] > 0)
def test_create_as_fp_staff_not_existing_device(self):
"""Test creation of heartbeat on non-existing device."""
response = self.fp_staff_client.post(
reverse(self.LIST_CREATE_URL),
self._create_dummy_data(uuid=Dummy.UUIDs[0]),
)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_create_as_uuid_owner(self):
"""Test creation as owner."""
response = self.user.post(
reverse(self.LIST_CREATE_URL),
self._create_dummy_data(uuid=self.uuid),
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data["id"], -1)
def test_create_as_uuid_not_owner(self):
"""Test creation as non-owner."""
uuid, _, _ = self._register_device()
response = self.user.post(
reverse(self.LIST_CREATE_URL), self._create_dummy_data(uuid=uuid)
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_list(self):
"""Test listing of heartbeats."""
count = 5
self._post_multiple(self.user, self.data, count)
response = self.fp_staff_client.get(reverse(self.LIST_CREATE_URL))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data["results"]), count)
def test_retrieve_single_fp_staff(self):
"""Test retrieval as Fairphone staff."""
self.assertEqual(
self._retrieve_single(self.fp_staff_client), status.HTTP_200_OK
)
def test_retrieve_single_device_owner(self):
"""Test retrieval as device owner."""
self.assertEqual(
self._retrieve_single(self.user), status.HTTP_403_FORBIDDEN
)
def test_retrieve_single_noauth(self):
"""Test retrieval without authentication."""
noauth_client = APIClient()
self.assertEqual(
self._retrieve_single(noauth_client), status.HTTP_401_UNAUTHORIZED
)
def test_retrieve_single_by_device_fp_staff(self):
"""Test retrieval by device as Fairphone staff."""
self.assertEqual(
self._retrieve_single_by_device(self.fp_staff_client),
status.HTTP_200_OK,
)
def test_retrieve_single_by_device_device_owner(self):
"""Test retrieval by device as owner."""
self.assertEqual(
self._retrieve_single_by_device(self.user),
status.HTTP_403_FORBIDDEN,
)
def test_retrieve_single_by_device_noauth(self):
"""Test retrieval by device without authentication."""
noauth_client = APIClient()
self.assertEqual(
self._retrieve_single_by_device(noauth_client),
status.HTTP_401_UNAUTHORIZED,
)
def test_list_by_uuid(self):
"""Test listing of devices by UUID."""
count = 5
uuid, _, _ = self._register_device()
self._post_multiple(self.user, self.data, count)
self._post_multiple(
self.fp_staff_client, self._create_dummy_data(uuid=uuid), count
)
url = reverse(self.LIST_CREATE_BY_UUID_URL, args=[self.uuid])
response = self.fp_staff_client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data["results"]), count)
def test_list_noauth(self):
"""Test listing of devices without authentication."""
count = 5
noauth_client = APIClient()
self._post_multiple(self.user, self.data, count)
response = noauth_client.get(reverse(self.LIST_CREATE_URL))
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_list_device_owner(self):
"""Test listing as device owner."""
count = 5
self._post_multiple(self.user, self.data, count)
response = self.user.get(reverse(self.LIST_CREATE_URL))
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_no_radio_version(self):
"""Test creation and retrieval without radio version."""
data = self._create_dummy_data(uuid=self.uuid)
data.pop("radio_version")
response = self.user.post(reverse(self.LIST_CREATE_URL), data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
url = reverse(self.LIST_CREATE_BY_UUID_URL, args=[self.uuid])
response = self.fp_staff_client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data["results"]), 1)
self.assertIsNone(response.data["results"][0]["radio_version"])
def test_radio_version_field(self):
"""Test retrieval of radio version field."""
response = self.user.post(reverse(self.LIST_CREATE_URL), self.data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
url = reverse(self.LIST_CREATE_BY_UUID_URL, args=[self.uuid])
response = self.fp_staff_client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data["results"]), 1)
self.assertEqual(
response.data["results"][0]["radio_version"],
self.data["radio_version"],
)
def test_send_non_existent_time(self):
"""Test sending of heartbeat with non existent time.
Test the resolution of a naive date-time in which the
Europe/Amsterdam daylight saving time transition moved the time
"forward".
"""
data = self._create_dummy_data(uuid=self.uuid)
# In 2017, the Netherlands changed from CET to CEST on March,
# 26 at 02:00
data["date"] = "2017-03-26 02:34:56"
response = self.user.post(reverse(self.LIST_CREATE_URL), data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_send_ambiguous_time(self):
"""Test sending of heartbeat with ambiguous time.
Test the resolution of a naive date-time in which the
Europe/Amsterdam daylight saving time transition moved the time
"backward".
"""
data = self._create_dummy_data(uuid=self.uuid)
# In 2017, the Netherlands changed from CEST to CET on October,
# 29 at 03:00
data["date"] = "2017-10-29 02:34:56"
response = self.user.post(reverse(self.LIST_CREATE_URL), data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_create_duplicate(self):
"""Test creation of a duplicate Heartbeat."""
# Create a first Heartbeat
report_data = self._create_dummy_data(uuid=self.uuid)
response_first = self.user.post(
reverse(self.LIST_CREATE_URL), report_data
)
self.assertEqual(response_first.status_code, status.HTTP_201_CREATED)
# Create a second heartbeat for the same day
response_second = self.user.post(
reverse(self.LIST_CREATE_URL), report_data
)
self.assertEqual(response_second.status_code, status.HTTP_201_CREATED)
# Assert that only one heartbeat instance was created
url = reverse(self.LIST_CREATE_BY_UUID_URL, args=[self.uuid])
response = self.fp_staff_client.get(url)
self.assertEqual(len(response.data["results"]), 1)
def test_create_duplicate_different_values(self):
"""Test creation of a duplicate Heartbeat with different values."""
# Create a first Heartbeat
report_1_data = self._create_dummy_data(uuid=self.uuid)
response_first = self.user.post(
reverse(self.LIST_CREATE_URL), report_1_data
)
self.assertEqual(response_first.status_code, status.HTTP_201_CREATED)
# Create a second heartbeat for the same day with all different
# values except for the date and UUID
report_2_data = self._create_alternative_dummy_data(uuid=self.uuid)
response_second = self.user.post(
reverse(self.LIST_CREATE_URL), report_2_data
)
self.assertEqual(response_second.status_code, status.HTTP_201_CREATED)
# Assert that only one heartbeat instance was created
url = reverse(self.LIST_CREATE_BY_UUID_URL, args=[self.uuid])
response = self.fp_staff_client.get(url)
self.assertEqual(len(response.data["results"]), 1)
# Assert that the values are all the same as of the first heartbeat, as
# we are dropping all incoming duplicates (we need to ignore the `id`
# because its value is set to -1 in the response for creating reports)
self.assertTrue(
{k: v for k, v in response.data["results"][0].items() if k != "id"}
== {k: v for k, v in response_first.data.items() if k != "id"}
)
def test_create_with_datetime(self):
"""Test creation of heartbeats with datetime instead of date value.
Initially, the 'date' field of the HeartBeat model was a datetime
field but now has been changed to a date field. However, Hiccup clients
are still sending datetime values which also need to be accepted and
processed by the server.
"""
data = self._create_dummy_data(uuid=self.uuid)
data["date"] = datetime(2018, 3, 19, 12, 0, 0, tzinfo=pytz.utc)
response = self.user.post(reverse(self.LIST_CREATE_URL), data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data["date"], str(data["date"].date()))
@unittest.skip("Fails because of race condition when assigning local IDs")
class HeartBeatRaceConditionsTestCase(RaceConditionsTestCase):
"""Test cases for heartbeat race conditions."""
LIST_CREATE_URL = "api_v1_heartbeats"
def test_create_multiple_heartbeats(self):
"""Test that no race condition occurs when creating heartbeats."""
uuid, user, _ = self._register_device()
def upload_report(client, data):
response = client.post(reverse(self.LIST_CREATE_URL), data)
self.assertEqual(status.HTTP_201_CREATED, response.status_code)
connection.close()
data = Dummy.heartbeat_data(uuid=uuid)
argslist = [
[user, dict(data, date=data["date"] + timedelta(days=i))]
for i in range(10)
]
self._test_create_multiple(
HeartBeat, upload_report, argslist, "device_local_id"
)