blob: 354b33081d40ea8a83329682201be019e9b84121 [file] [log] [blame]
"""Test the API for crashreports, devices, heartbeats and logfiles."""
import os
import tempfile
from django.contrib.auth.models import User
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient, APITestCase
from crashreports.models import Crashreport
class InvalidCrashTypeError(BaseException):
"""Invalid crash type encountered.
The valid crash type values (strings) are:
- 'crash';
- 'smpl';
- 'other'.
Args:
- crash_type: The invalid crash type.
"""
def __init__(self, crash_type):
"""Initialise the exception using the crash type to build a message.
Args:
crash_type: The invalid crash type.
"""
super(InvalidCrashTypeError, self).__init__(
"{} is not a valid crash type".format(crash_type)
)
class Dummy:
"""Dummy values for devices, heartbeats and crashreports."""
DEFAULT_DUMMY_DEVICE_REGISTER_VALUES = {
"board_date": "2015-12-15T01:23:45Z",
"chipset": "Qualcomm MSM8974PRO-AA",
}
DEFAULT_DUMMY_HEARTBEAT_VALUES = {
"uuid": None,
"app_version": 10100,
"uptime": (
"up time: 16 days, 21:49:56, idle time: 5 days, 20:55:04, "
"sleep time: 10 days, 20:46:27"
),
"build_fingerprint": (
"Fairphone/FP2/FP2:6.0.1/FP2-gms-18.03.1/FP2-gms-18.03.1:user/"
"release-keys"
),
"radio_version": "4437.1-FP2-0-08",
"date": "2018-03-19T09:58:30.386Z",
}
DEFAULT_DUMMY_CRASHREPORTS_VALUES = DEFAULT_DUMMY_HEARTBEAT_VALUES.copy()
DEFAULT_DUMMY_CRASHREPORTS_VALUES.update(
{
"is_fake_report": 0,
"boot_reason": "why?",
"power_on_reason": "it was powered on",
"power_off_reason": "something happened and it went off",
}
)
CRASH_TYPE_TO_BOOT_REASON_MAP = {
"crash": Crashreport.BOOT_REASON_KEYBOARD_POWER_ON,
"smpl": Crashreport.BOOT_REASON_RTC_ALARM,
"other": "whatever",
}
@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 device_register_data(**kwargs):
"""Return the data required to register a device.
Use the values passed as keyword arguments or default to the ones
from `Dummy.DEFAULT_DUMMY_DEVICE_REGISTER_VALUES`.
"""
return Dummy._update_copy(
Dummy.DEFAULT_DUMMY_DEVICE_REGISTER_VALUES, kwargs
)
@staticmethod
def heartbeat_data(**kwargs):
"""Return the data required to create a heartbeat.
Use the values passed as keyword arguments or default to the ones
from `Dummy.DEFAULT_DUMMY_HEARTBEAT_VALUES`.
"""
return Dummy._update_copy(Dummy.DEFAULT_DUMMY_HEARTBEAT_VALUES, kwargs)
@staticmethod
def crashreport_data(report_type=None, **kwargs):
"""Return the data required to create a crashreport.
Use the values passed as keyword arguments or default to the ones
from `Dummy.DEFAULT_DUMMY_CRASHREPORTS_VALUES`.
Args:
report_type (str, optional): A valid value from
`Dummy.CRASH_TYPE_TO_BOOT_REASON_MAP.keys()` that will
define the boot reason if not explicitly defined in the
keyword arguments already.
"""
data = Dummy._update_copy(
Dummy.DEFAULT_DUMMY_CRASHREPORTS_VALUES, kwargs
)
if report_type and "boot_reason" not in kwargs:
if report_type not in Dummy.CRASH_TYPE_TO_BOOT_REASON_MAP:
raise InvalidCrashTypeError(report_type)
data["boot_reason"] = Dummy.CRASH_TYPE_TO_BOOT_REASON_MAP.get(
report_type
)
return data
class DeviceRegisterAPITestCase(APITestCase):
"""Base class that offers a device registration method."""
REGISTER_DEVICE_URL = "api_v1_register_device"
def setUp(self):
"""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"
)
self.admin = APIClient()
self.admin.force_authenticate(admin_user)
def _register_device(self, **kwargs):
"""Register a new device.
Arguments:
**kwargs: The data to pass the dummy data creation
method `Dummy.device_register_data`.
Returns:
(UUID, APIClient, str): The uuid of the new device as well as an
authentication token and the associated user with credentials.
"""
data = Dummy.device_register_data(**kwargs)
response = self.client.post(reverse(self.REGISTER_DEVICE_URL), data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
uuid = response.data["uuid"]
token = response.data["token"]
user = APIClient()
user.credentials(HTTP_AUTHORIZATION="Token " + token)
return uuid, user, token
class DeviceTestCase(DeviceRegisterAPITestCase):
"""Test cases for registering devices."""
def test_register(self):
"""Test registration of devices."""
response = self.client.post(
reverse(self.REGISTER_DEVICE_URL), Dummy.device_register_data()
)
self.assertTrue("token" in response.data)
self.assertTrue("uuid" in response.data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_create_missing_fields(self):
"""Test registration with missing fields."""
response = self.client.post(reverse(self.REGISTER_DEVICE_URL))
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_create_missing_board_date(self):
"""Test registration with missing board date."""
data = Dummy.device_register_data()
data.pop("board_date")
response = self.client.post(reverse(self.REGISTER_DEVICE_URL), data)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_create_missing_chipset(self):
"""Test registration with missing chipset."""
data = Dummy.device_register_data()
data.pop("chipset")
response = self.client.post(reverse(self.REGISTER_DEVICE_URL), data)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_create_invalid_board_date(self):
"""Test registration with invalid board date."""
data = Dummy.device_register_data()
data["board_date"] = "not_a_valid_date"
response = self.client.post(reverse(self.REGISTER_DEVICE_URL), data)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_create_non_existent_time_board_date(self):
"""Test registration with non existing time.
Test the resolution of a naive date-time in which the
Europe/Amsterdam daylight saving time transition moved the time
"forward". The server should not crash when receiving a naive
date-time which does not exist in the server timezone or locale.
"""
data = Dummy.device_register_data()
# In 2017, the Netherlands changed from CET to CEST on March,
# 26 at 02:00
data["board_date"] = "2017-03-26 02:34:56"
response = self.client.post(reverse(self.REGISTER_DEVICE_URL), data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_create_ambiguous_time_board_date(self):
"""Test registration with ambiguous time.
Test the resolution of a naive date-time in which the
Europe/Amsterdam daylight saving time transition moved the time
"backward". The server should not crash when receiving a naive
date-time that can belong to multiple timezones.
"""
data = Dummy.device_register_data()
# In 2017, the Netherlands changed from CEST to CET on October,
# 29 at 03:00
data["board_date"] = "2017-10-29 02:34:56"
response = self.client.post(reverse(self.REGISTER_DEVICE_URL), data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
class ListDevicesTestCase(DeviceRegisterAPITestCase):
"""Test cases for listing and deleting devices."""
LIST_CREATE_URL = "api_v1_list_devices"
RETRIEVE_URL = "api_v1_retrieve_device"
def test_device_list(self):
"""Test registration of 2 devices."""
number_of_devices = 2
uuids = [
str(self._register_device()[0]) for _ in range(number_of_devices)
]
response = self.admin.get(reverse(self.LIST_CREATE_URL), {})
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data["results"]), number_of_devices)
for result in response.data["results"]:
self.assertIn(result["uuid"], uuids)
def test_device_list_unauth(self):
"""Test listing devices without authentication."""
response = self.client.get(reverse(self.LIST_CREATE_URL), {})
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_retrieve_device_auth(self):
"""Test retrieval of devices as admin user."""
uuid, _, token = self._register_device()
response = self.admin.get(reverse(self.RETRIEVE_URL, args=[uuid]), {})
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["uuid"], str(uuid))
self.assertEqual(response.data["token"], token)
def test_retrieve_device_unauth(self):
"""Test retrieval of devices without authentication."""
uuid, _, _ = self._register_device()
response = self.client.get(reverse(self.RETRIEVE_URL, args=[uuid]), {})
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_delete_device_auth(self):
"""Test deletion of devices as admin user."""
uuid, _, _ = self._register_device()
url = reverse(self.RETRIEVE_URL, args=[uuid])
response = self.admin.delete(url, {})
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
response = self.admin.delete(url, {})
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
class HeartbeatListTestCase(DeviceRegisterAPITestCase):
"""Test cases for heartbeats."""
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)
def _post_multiple(self, client, data, count):
return [
client.post(reverse(self.LIST_CREATE_URL), data)
for _ in range(count)
]
def _retrieve_single(self, user):
count = 5
response = self._post_multiple(self.admin, 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_admin(self):
"""Test creation as admin."""
response = self.admin.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_admin_not_existing_device(self):
"""Test creation of heartbeat on non-existing device."""
response = self.admin.post(
reverse(self.LIST_CREATE_URL), self._create_dummy_data()
)
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.admin.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_admin(self):
"""Test retrieval as admin."""
self.assertEqual(self._retrieve_single(self.admin), 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_admin(self):
"""Test retrieval by device as admin."""
self.assertEqual(
self._retrieve_single_by_device(self.admin), 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.admin, self._create_dummy_data(uuid=uuid), count
)
url = reverse(self.LIST_CREATE_BY_UUID_URL, args=[self.uuid])
response = self.admin.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.admin.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.admin.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)
# pylint: disable=too-many-ancestors
class CrashreportListTestCase(HeartbeatListTestCase):
"""Test cases for crash reports."""
LIST_CREATE_URL = "api_v1_crashreports"
RETRIEVE_URL = "api_v1_crashreport"
LIST_CREATE_BY_UUID_URL = "api_v1_crashreports_by_uuid"
RETRIEVE_BY_UUID_URL = "api_v1_crashreport_by_uuid"
@staticmethod
def _create_dummy_data(**kwargs):
return Dummy.crashreport_data(**kwargs)
class LogfileUploadTest(DeviceRegisterAPITestCase):
"""Test cases for upload of log files."""
LIST_CREATE_URL = "api_v1_crashreports"
PUT_LOGFILE_URL = "api_v1_putlogfile_for_device_id"
def _upload_crashreport(self, user, uuid):
"""
Upload dummy crashreport data.
Args:
user: The user which should be used for uploading the report
uuid: The uuid of the device to which the report should be uploaded
Returns: The local id of the device for which the report was uploaded.
"""
data = Dummy.crashreport_data(uuid=uuid)
response = user.post(reverse(self.LIST_CREATE_URL), data)
self.assertEqual(status.HTTP_201_CREATED, response.status_code)
self.assertTrue("device_local_id" in response.data)
device_local_id = response.data["device_local_id"]
return device_local_id
def _test_logfile_upload(self, user, uuid):
# Upload crashreport
device_local_id = self._upload_crashreport(user, uuid)
# Upload a logfile for the crashreport
logfile = tempfile.NamedTemporaryFile("w+", suffix=".log", delete=True)
logfile.write(u"blihblahblub")
response = user.post(
reverse(
self.PUT_LOGFILE_URL,
args=[uuid, device_local_id, os.path.basename(logfile.name)],
),
{"file": logfile},
format="multipart",
)
self.assertEqual(status.HTTP_201_CREATED, response.status_code)
def test_logfile_upload_as_user(self):
"""Test upload of logfiles as device owner."""
uuid, user, _ = self._register_device()
self._test_logfile_upload(user, uuid)
def test_logfile_upload_as_admin(self):
"""Test upload of logfiles as admin user."""
uuid, _, _ = self._register_device()
self._test_logfile_upload(self.admin, uuid)