blob: b70f400f962865a77e379ffe00d391eccf48ed3b [file] [log] [blame]
Mitja Nikolaus35181602018-08-03 13:55:30 +02001"""Test the API for crashreports, devices, heartbeats and logfiles."""
Franz-Xaver Geiger4d022d52018-03-20 13:12:49 +01002import os
3import tempfile
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +02004from typing import Optional
Franz-Xaver Geiger4d022d52018-03-20 13:12:49 +01005
Dirk Vogtc9e10ab2016-10-12 13:58:15 +02006from django.contrib.auth.models import User
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +01007from django.urls import reverse
Mitja Nikolaus35181602018-08-03 13:55:30 +02008
Dirk Vogt36635692016-10-17 12:19:10 +02009from rest_framework import status
Mitja Nikolaus35181602018-08-03 13:55:30 +020010from rest_framework.test import APIClient, APITestCase
Dirk Vogtf130c752016-08-23 14:45:01 +020011
Borjan Tchakalofffe5f25c2018-03-19 18:33:24 +040012from crashreports.models import Crashreport
13
14
15class InvalidCrashTypeError(BaseException):
Mitja Nikolaus35181602018-08-03 13:55:30 +020016 """Invalid crash type encountered.
Borjan Tchakalofffe5f25c2018-03-19 18:33:24 +040017
18 The valid crash type values (strings) are:
19 - 'crash';
20 - 'smpl';
21 - 'other'.
22
23 Args:
24 - crash_type: The invalid crash type.
25 """
26
27 def __init__(self, crash_type):
Mitja Nikolaus35181602018-08-03 13:55:30 +020028 """Initialise the exception using the crash type to build a message.
29
30 Args:
31 crash_type: The invalid crash type.
32 """
Borjan Tchakalofffe5f25c2018-03-19 18:33:24 +040033 super(InvalidCrashTypeError, self).__init__(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020034 "{} is not a valid crash type".format(crash_type)
35 )
Borjan Tchakalofffe5f25c2018-03-19 18:33:24 +040036
Borjan Tchakaloffec814a72018-02-28 11:16:53 +040037
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020038class Dummy:
Mitja Nikolaus35181602018-08-03 13:55:30 +020039 """Dummy values for devices, heartbeats and crashreports."""
40
Borjan Tchakaloffec814a72018-02-28 11:16:53 +040041 DEFAULT_DUMMY_DEVICE_REGISTER_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020042 "board_date": "2015-12-15T01:23:45Z",
43 "chipset": "Qualcomm MSM8974PRO-AA",
Borjan Tchakaloffec814a72018-02-28 11:16:53 +040044 }
45
46 DEFAULT_DUMMY_HEARTBEAT_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020047 "uuid": None,
48 "app_version": 10100,
49 "uptime": (
50 "up time: 16 days, 21:49:56, idle time: 5 days, 20:55:04, "
51 "sleep time: 10 days, 20:46:27"
52 ),
53 "build_fingerprint": (
54 "Fairphone/FP2/FP2:6.0.1/FP2-gms-18.03.1/FP2-gms-18.03.1:user/"
55 "release-keys"
56 ),
57 "radio_version": "4437.1-FP2-0-08",
58 "date": "2018-03-19T09:58:30.386Z",
Borjan Tchakaloffec814a72018-02-28 11:16:53 +040059 }
60
61 DEFAULT_DUMMY_CRASHREPORTS_VALUES = DEFAULT_DUMMY_HEARTBEAT_VALUES.copy()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020062 DEFAULT_DUMMY_CRASHREPORTS_VALUES.update(
63 {
64 "is_fake_report": 0,
65 "boot_reason": "why?",
66 "power_on_reason": "it was powered on",
67 "power_off_reason": "something happened and it went off",
68 }
69 )
Borjan Tchakaloffec814a72018-02-28 11:16:53 +040070
Borjan Tchakalofffe5f25c2018-03-19 18:33:24 +040071 CRASH_TYPE_TO_BOOT_REASON_MAP = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020072 "crash": Crashreport.BOOT_REASON_KEYBOARD_POWER_ON,
73 "smpl": Crashreport.BOOT_REASON_RTC_ALARM,
74 "other": "whatever",
Borjan Tchakalofffe5f25c2018-03-19 18:33:24 +040075 }
76
Borjan Tchakaloffec814a72018-02-28 11:16:53 +040077 @staticmethod
78 def _update_copy(original, update):
Mitja Nikolaus35181602018-08-03 13:55:30 +020079 """Merge fields of update into a copy of original."""
Borjan Tchakaloffec814a72018-02-28 11:16:53 +040080 data = original.copy()
81 data.update(update)
82 return data
83
84 @staticmethod
85 def device_register_data(**kwargs):
Mitja Nikolaus35181602018-08-03 13:55:30 +020086 """Return the data required to register a device.
Borjan Tchakaloffec814a72018-02-28 11:16:53 +040087
Mitja Nikolaus35181602018-08-03 13:55:30 +020088 Use the values passed as keyword arguments or default to the ones
89 from `Dummy.DEFAULT_DUMMY_DEVICE_REGISTER_VALUES`.
Borjan Tchakaloffec814a72018-02-28 11:16:53 +040090 """
91 return Dummy._update_copy(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020092 Dummy.DEFAULT_DUMMY_DEVICE_REGISTER_VALUES, kwargs
93 )
Borjan Tchakaloffec814a72018-02-28 11:16:53 +040094
95 @staticmethod
96 def heartbeat_data(**kwargs):
Mitja Nikolaus35181602018-08-03 13:55:30 +020097 """Return the data required to create a heartbeat.
Borjan Tchakaloffec814a72018-02-28 11:16:53 +040098
Mitja Nikolaus35181602018-08-03 13:55:30 +020099 Use the values passed as keyword arguments or default to the ones
100 from `Dummy.DEFAULT_DUMMY_HEARTBEAT_VALUES`.
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400101 """
102 return Dummy._update_copy(Dummy.DEFAULT_DUMMY_HEARTBEAT_VALUES, kwargs)
103
104 @staticmethod
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +0200105 def crashreport_data(report_type: Optional[str] = None, **kwargs):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200106 """Return the data required to create a crashreport.
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400107
Mitja Nikolaus35181602018-08-03 13:55:30 +0200108 Use the values passed as keyword arguments or default to the ones
109 from `Dummy.DEFAULT_DUMMY_CRASHREPORTS_VALUES`.
Borjan Tchakalofffe5f25c2018-03-19 18:33:24 +0400110
Mitja Nikolaus35181602018-08-03 13:55:30 +0200111 Args:
Mitja Nikolaus9c3b29e2018-08-22 11:17:50 +0200112 report_type: A valid value from
Mitja Nikolaus35181602018-08-03 13:55:30 +0200113 `Dummy.CRASH_TYPE_TO_BOOT_REASON_MAP.keys()` that will
114 define the boot reason if not explicitly defined in the
115 keyword arguments already.
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400116 """
Borjan Tchakalofffe5f25c2018-03-19 18:33:24 +0400117 data = Dummy._update_copy(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200118 Dummy.DEFAULT_DUMMY_CRASHREPORTS_VALUES, kwargs
119 )
120 if report_type and "boot_reason" not in kwargs:
Borjan Tchakalofffe5f25c2018-03-19 18:33:24 +0400121 if report_type not in Dummy.CRASH_TYPE_TO_BOOT_REASON_MAP:
122 raise InvalidCrashTypeError(report_type)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200123 data["boot_reason"] = Dummy.CRASH_TYPE_TO_BOOT_REASON_MAP.get(
124 report_type
125 )
Borjan Tchakalofffe5f25c2018-03-19 18:33:24 +0400126 return data
Dirk Vogtf2a33422016-10-11 17:17:26 +0200127
128
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400129class DeviceRegisterAPITestCase(APITestCase):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200130 """Base class that offers a device registration method."""
Dirk Vogtf2a33422016-10-11 17:17:26 +0200131
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +0100132 REGISTER_DEVICE_URL = "api_v1_register_device"
133
Dirk Vogtf2a33422016-10-11 17:17:26 +0200134 def setUp(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200135 """Create an admin user for accessing the API.
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400136
Mitja Nikolaus35181602018-08-03 13:55:30 +0200137 The APIClient that can be used to make authenticated requests to the
138 server is stored in self.admin.
139 """
140 admin_user = User.objects.create_superuser(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200141 "somebody", "somebody@example.com", "thepassword"
142 )
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400143 self.admin = APIClient()
Mitja Nikolaus35181602018-08-03 13:55:30 +0200144 self.admin.force_authenticate(admin_user)
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400145
146 def _register_device(self, **kwargs):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200147 """Register a new device.
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400148
149 Arguments:
150 **kwargs: The data to pass the dummy data creation
151 method `Dummy.device_register_data`.
152 Returns:
Mitja Nikolaus35181602018-08-03 13:55:30 +0200153 (UUID, APIClient, str): The uuid of the new device as well as an
154 authentication token and the associated user with credentials.
155
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400156 """
157 data = Dummy.device_register_data(**kwargs)
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +0100158 response = self.client.post(reverse(self.REGISTER_DEVICE_URL), data)
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400159 self.assertEqual(response.status_code, status.HTTP_200_OK)
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400160
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200161 uuid = response.data["uuid"]
162 token = response.data["token"]
Mitja Nikolaus35181602018-08-03 13:55:30 +0200163 user = APIClient()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200164 user.credentials(HTTP_AUTHORIZATION="Token " + token)
Mitja Nikolaus35181602018-08-03 13:55:30 +0200165
166 return uuid, user, token
167
168
169class DeviceTestCase(DeviceRegisterAPITestCase):
170 """Test cases for registering devices."""
171
172 def test_register(self):
173 """Test registration of devices."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200174 response = self.client.post(
175 reverse(self.REGISTER_DEVICE_URL), Dummy.device_register_data()
176 )
Mitja Nikolaus35181602018-08-03 13:55:30 +0200177 self.assertTrue("token" in response.data)
178 self.assertTrue("uuid" in response.data)
179 self.assertEqual(response.status_code, status.HTTP_200_OK)
180
181 def test_create_missing_fields(self):
182 """Test registration with missing fields."""
183 response = self.client.post(reverse(self.REGISTER_DEVICE_URL))
184 self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
185
186 def test_create_missing_board_date(self):
187 """Test registration with missing board date."""
188 data = Dummy.device_register_data()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200189 data.pop("board_date")
Mitja Nikolaus35181602018-08-03 13:55:30 +0200190 response = self.client.post(reverse(self.REGISTER_DEVICE_URL), data)
191 self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
192
193 def test_create_missing_chipset(self):
194 """Test registration with missing chipset."""
195 data = Dummy.device_register_data()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200196 data.pop("chipset")
Mitja Nikolaus35181602018-08-03 13:55:30 +0200197 response = self.client.post(reverse(self.REGISTER_DEVICE_URL), data)
198 self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
199
200 def test_create_invalid_board_date(self):
201 """Test registration with invalid board date."""
202 data = Dummy.device_register_data()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200203 data["board_date"] = "not_a_valid_date"
Mitja Nikolaus35181602018-08-03 13:55:30 +0200204 response = self.client.post(reverse(self.REGISTER_DEVICE_URL), data)
205 self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
206
207 def test_create_non_existent_time_board_date(self):
208 """Test registration with non existing time.
209
210 Test the resolution of a naive date-time in which the
211 Europe/Amsterdam daylight saving time transition moved the time
212 "forward". The server should not crash when receiving a naive
213 date-time which does not exist in the server timezone or locale.
214 """
215 data = Dummy.device_register_data()
216 # In 2017, the Netherlands changed from CET to CEST on March,
217 # 26 at 02:00
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200218 data["board_date"] = "2017-03-26 02:34:56"
Mitja Nikolaus35181602018-08-03 13:55:30 +0200219 response = self.client.post(reverse(self.REGISTER_DEVICE_URL), data)
220 self.assertEqual(response.status_code, status.HTTP_200_OK)
221
222 def test_create_ambiguous_time_board_date(self):
223 """Test registration with ambiguous time.
224
225 Test the resolution of a naive date-time in which the
226 Europe/Amsterdam daylight saving time transition moved the time
227 "backward". The server should not crash when receiving a naive
228 date-time that can belong to multiple timezones.
229 """
230 data = Dummy.device_register_data()
231 # In 2017, the Netherlands changed from CEST to CET on October,
232 # 29 at 03:00
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200233 data["board_date"] = "2017-10-29 02:34:56"
Mitja Nikolaus35181602018-08-03 13:55:30 +0200234 response = self.client.post(reverse(self.REGISTER_DEVICE_URL), data)
235 self.assertEqual(response.status_code, status.HTTP_200_OK)
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400236
237
238class ListDevicesTestCase(DeviceRegisterAPITestCase):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200239 """Test cases for listing and deleting devices."""
Dirk Vogtf2a33422016-10-11 17:17:26 +0200240
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +0100241 LIST_CREATE_URL = "api_v1_list_devices"
242 RETRIEVE_URL = "api_v1_retrieve_device"
243
Dirk Vogtf2a33422016-10-11 17:17:26 +0200244 def test_device_list(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200245 """Test registration of 2 devices."""
246 number_of_devices = 2
247 uuids = [
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200248 str(self._register_device()[0]) for _ in range(number_of_devices)
Mitja Nikolaus35181602018-08-03 13:55:30 +0200249 ]
250
251 response = self.admin.get(reverse(self.LIST_CREATE_URL), {})
252 self.assertEqual(response.status_code, status.HTTP_200_OK)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200253 self.assertEqual(len(response.data["results"]), number_of_devices)
254 for result in response.data["results"]:
255 self.assertIn(result["uuid"], uuids)
Dirk Vogtf2a33422016-10-11 17:17:26 +0200256
257 def test_device_list_unauth(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200258 """Test listing devices without authentication."""
259 response = self.client.get(reverse(self.LIST_CREATE_URL), {})
260 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
Dirk Vogtf2a33422016-10-11 17:17:26 +0200261
262 def test_retrieve_device_auth(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200263 """Test retrieval of devices as admin user."""
264 uuid, _, token = self._register_device()
265 response = self.admin.get(reverse(self.RETRIEVE_URL, args=[uuid]), {})
266 self.assertEqual(response.status_code, status.HTTP_200_OK)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200267 self.assertEqual(response.data["uuid"], str(uuid))
268 self.assertEqual(response.data["token"], token)
Dirk Vogtf2a33422016-10-11 17:17:26 +0200269
270 def test_retrieve_device_unauth(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200271 """Test retrieval of devices without authentication."""
272 uuid, _, _ = self._register_device()
273 response = self.client.get(reverse(self.RETRIEVE_URL, args=[uuid]), {})
274 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
Dirk Vogtf2a33422016-10-11 17:17:26 +0200275
276 def test_delete_device_auth(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200277 """Test deletion of devices as admin user."""
278 uuid, _, _ = self._register_device()
279 url = reverse(self.RETRIEVE_URL, args=[uuid])
280 response = self.admin.delete(url, {})
281 self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
282 response = self.admin.delete(url, {})
283 self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
Dirk Vogtf2a33422016-10-11 17:17:26 +0200284
Dirk Vogtf2a33422016-10-11 17:17:26 +0200285
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400286class HeartbeatListTestCase(DeviceRegisterAPITestCase):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200287 """Test cases for heartbeats."""
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400288
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +0100289 LIST_CREATE_URL = "api_v1_heartbeats"
290 RETRIEVE_URL = "api_v1_heartbeat"
291 LIST_CREATE_BY_UUID_URL = "api_v1_heartbeats_by_uuid"
292 RETRIEVE_BY_UUID_URL = "api_v1_heartbeat_by_uuid"
293
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400294 @staticmethod
295 def _create_dummy_data(**kwargs):
296 return Dummy.heartbeat_data(**kwargs)
297
298 def _post_multiple(self, client, data, count):
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +0100299 return [
300 client.post(reverse(self.LIST_CREATE_URL), data)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200301 for _ in range(count)
302 ]
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400303
304 def _retrieve_single(self, user):
305 count = 5
Mitja Nikolaus35181602018-08-03 13:55:30 +0200306 response = self._post_multiple(self.admin, self.data, count)
307 self.assertEqual(len(response), count)
308 self.assertEqual(response[0].status_code, status.HTTP_201_CREATED)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200309 url = reverse(self.RETRIEVE_URL, args=[response[0].data["id"]])
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400310 request = user.get(url)
311 return request.status_code
312
313 def _retrieve_single_by_device(self, user):
314 count = 5
Mitja Nikolaus35181602018-08-03 13:55:30 +0200315 response = self._post_multiple(self.user, self.data, count)
316 self.assertEqual(len(response), count)
317 self.assertEqual(response[0].status_code, status.HTTP_201_CREATED)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200318 url = reverse(
319 self.RETRIEVE_BY_UUID_URL,
320 args=[self.uuid, response[0].data["device_local_id"]],
321 )
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400322 request = user.get(url)
323 return request.status_code
Dirk Vogtf2a33422016-10-11 17:17:26 +0200324
325 def setUp(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200326 """Set up a device and some data."""
327 super().setUp()
328 self.uuid, self.user, self.token = self._register_device()
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400329 self.data = self._create_dummy_data(uuid=self.uuid)
Dirk Vogt67eb1482016-10-13 12:42:56 +0200330
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200331 def test_create_no_auth(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200332 """Test creation without authentication."""
333 noauth_client = APIClient()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200334 response = noauth_client.post(reverse(self.LIST_CREATE_URL), self.data)
Mitja Nikolaus35181602018-08-03 13:55:30 +0200335 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
Dirk Vogtf2a33422016-10-11 17:17:26 +0200336
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200337 def test_create_as_admin(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200338 """Test creation as admin."""
339 response = self.admin.post(reverse(self.LIST_CREATE_URL), self.data)
340 self.assertEqual(response.status_code, status.HTTP_201_CREATED)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200341 self.assertTrue(response.data["id"] > 0)
Dirk Vogtf2a33422016-10-11 17:17:26 +0200342
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200343 def test_create_as_admin_not_existing_device(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200344 """Test creation of heartbeat on non-existing device."""
345 response = self.admin.post(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200346 reverse(self.LIST_CREATE_URL), self._create_dummy_data()
347 )
Mitja Nikolaus35181602018-08-03 13:55:30 +0200348 self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200349
350 def test_create_as_uuid_owner(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200351 """Test creation as owner."""
352 response = self.user.post(
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +0100353 reverse(self.LIST_CREATE_URL),
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200354 self._create_dummy_data(uuid=self.uuid),
355 )
Mitja Nikolaus35181602018-08-03 13:55:30 +0200356 self.assertEqual(response.status_code, status.HTTP_201_CREATED)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200357 self.assertEqual(response.data["id"], -1)
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200358
359 def test_create_as_uuid_not_owner(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200360 """Test creation as non-owner."""
361 uuid, _, _ = self._register_device()
362 response = self.user.post(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200363 reverse(self.LIST_CREATE_URL), self._create_dummy_data(uuid=uuid)
364 )
Mitja Nikolaus35181602018-08-03 13:55:30 +0200365 self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200366
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200367 def test_list(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200368 """Test listing of heartbeats."""
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200369 count = 5
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400370 self._post_multiple(self.user, self.data, count)
Mitja Nikolaus35181602018-08-03 13:55:30 +0200371 response = self.admin.get(reverse(self.LIST_CREATE_URL))
372 self.assertEqual(response.status_code, status.HTTP_200_OK)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200373 self.assertEqual(len(response.data["results"]), count)
Franz-Xaver Geigerd612fd22018-02-28 10:33:08 +0100374
375 def test_retrieve_single_admin(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200376 """Test retrieval as admin."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200377 self.assertEqual(self._retrieve_single(self.admin), status.HTTP_200_OK)
Dirk Vogte1784882016-10-13 16:09:38 +0200378
379 def test_retrieve_single_device_owner(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200380 """Test retrieval as device owner."""
Franz-Xaver Geigerd612fd22018-02-28 10:33:08 +0100381 self.assertEqual(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200382 self._retrieve_single(self.user), status.HTTP_403_FORBIDDEN
383 )
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400384
385 def test_retrieve_single_noauth(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200386 """Test retrieval without authentication."""
387 noauth_client = APIClient()
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400388 self.assertEqual(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200389 self._retrieve_single(noauth_client), status.HTTP_401_UNAUTHORIZED
390 )
Dirk Vogte1784882016-10-13 16:09:38 +0200391
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400392 def test_retrieve_single_by_device_admin(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200393 """Test retrieval by device as admin."""
Franz-Xaver Geigerd612fd22018-02-28 10:33:08 +0100394 self.assertEqual(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200395 self._retrieve_single_by_device(self.admin), status.HTTP_200_OK
396 )
Dirk Vogt0d9d5d22016-10-13 16:17:57 +0200397
398 def test_retrieve_single_by_device_device_owner(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200399 """Test retrieval by device as owner."""
Franz-Xaver Geigerd612fd22018-02-28 10:33:08 +0100400 self.assertEqual(
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400401 self._retrieve_single_by_device(self.user),
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200402 status.HTTP_403_FORBIDDEN,
403 )
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400404
405 def test_retrieve_single_by_device_noauth(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200406 """Test retrieval by device without authentication."""
407 noauth_client = APIClient()
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400408 self.assertEqual(
Mitja Nikolaus35181602018-08-03 13:55:30 +0200409 self._retrieve_single_by_device(noauth_client),
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200410 status.HTTP_401_UNAUTHORIZED,
411 )
Dirk Vogt0d9d5d22016-10-13 16:17:57 +0200412
Dirk Vogte1784882016-10-13 16:09:38 +0200413 def test_list_by_uuid(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200414 """Test listing of devices by UUID."""
Dirk Vogte1784882016-10-13 16:09:38 +0200415 count = 5
Mitja Nikolaus35181602018-08-03 13:55:30 +0200416 uuid, _, _ = self._register_device()
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400417 self._post_multiple(self.user, self.data, count)
418 self._post_multiple(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200419 self.admin, self._create_dummy_data(uuid=uuid), count
420 )
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +0100421 url = reverse(self.LIST_CREATE_BY_UUID_URL, args=[self.uuid])
Mitja Nikolaus35181602018-08-03 13:55:30 +0200422 response = self.admin.get(url)
423 self.assertEqual(response.status_code, status.HTTP_200_OK)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200424 self.assertEqual(len(response.data["results"]), count)
Dirk Vogte1784882016-10-13 16:09:38 +0200425
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200426 def test_list_noauth(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200427 """Test listing of devices without authentication."""
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200428 count = 5
Mitja Nikolaus35181602018-08-03 13:55:30 +0200429 noauth_client = APIClient()
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400430 self._post_multiple(self.user, self.data, count)
Mitja Nikolaus35181602018-08-03 13:55:30 +0200431 response = noauth_client.get(reverse(self.LIST_CREATE_URL))
432 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200433
434 def test_list_device_owner(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200435 """Test listing as device owner."""
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200436 count = 5
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400437 self._post_multiple(self.user, self.data, count)
Mitja Nikolaus35181602018-08-03 13:55:30 +0200438 response = self.user.get(reverse(self.LIST_CREATE_URL))
439 self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200440
Borjan Tchakaloff869cf922018-02-19 11:01:16 +0100441 def test_no_radio_version(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200442 """Test creation and retrieval without radio version."""
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400443 data = self._create_dummy_data(uuid=self.uuid)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200444 data.pop("radio_version")
Mitja Nikolaus35181602018-08-03 13:55:30 +0200445 response = self.user.post(reverse(self.LIST_CREATE_URL), data)
446 self.assertEqual(response.status_code, status.HTTP_201_CREATED)
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +0100447 url = reverse(self.LIST_CREATE_BY_UUID_URL, args=[self.uuid])
Mitja Nikolaus35181602018-08-03 13:55:30 +0200448 response = self.admin.get(url)
449 self.assertEqual(response.status_code, status.HTTP_200_OK)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200450 self.assertEqual(len(response.data["results"]), 1)
451 self.assertIsNone(response.data["results"][0]["radio_version"])
Borjan Tchakaloff869cf922018-02-19 11:01:16 +0100452
453 def test_radio_version_field(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200454 """Test retrieval of radio version field."""
455 response = self.user.post(reverse(self.LIST_CREATE_URL), self.data)
456 self.assertEqual(response.status_code, status.HTTP_201_CREATED)
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +0100457 url = reverse(self.LIST_CREATE_BY_UUID_URL, args=[self.uuid])
Mitja Nikolaus35181602018-08-03 13:55:30 +0200458 response = self.admin.get(url)
459 self.assertEqual(response.status_code, status.HTTP_200_OK)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200460 self.assertEqual(len(response.data["results"]), 1)
461 self.assertEqual(
462 response.data["results"][0]["radio_version"],
463 self.data["radio_version"],
464 )
Borjan Tchakaloff869cf922018-02-19 11:01:16 +0100465
Borjan Tchakaloff866f75e2018-03-01 12:04:00 +0400466 def test_send_non_existent_time(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200467 """Test sending of heartbeat with non existent time.
468
469 Test the resolution of a naive date-time in which the
470 Europe/Amsterdam daylight saving time transition moved the time
471 "forward".
Borjan Tchakaloff866f75e2018-03-01 12:04:00 +0400472 """
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400473 data = self._create_dummy_data(uuid=self.uuid)
Mitja Nikolaus35181602018-08-03 13:55:30 +0200474 # In 2017, the Netherlands changed from CET to CEST on March,
475 # 26 at 02:00
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200476 data["date"] = "2017-03-26 02:34:56"
Mitja Nikolaus35181602018-08-03 13:55:30 +0200477 response = self.user.post(reverse(self.LIST_CREATE_URL), data)
478 self.assertEqual(response.status_code, status.HTTP_201_CREATED)
Borjan Tchakaloff866f75e2018-03-01 12:04:00 +0400479
480 def test_send_ambiguous_time(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200481 """Test sending of heartbeat with ambiguous time.
482
483 Test the resolution of a naive date-time in which the
484 Europe/Amsterdam daylight saving time transition moved the time
485 "backward".
Borjan Tchakaloff866f75e2018-03-01 12:04:00 +0400486 """
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400487 data = self._create_dummy_data(uuid=self.uuid)
Mitja Nikolaus35181602018-08-03 13:55:30 +0200488 # In 2017, the Netherlands changed from CEST to CET on October,
489 # 29 at 03:00
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200490 data["date"] = "2017-10-29 02:34:56"
Mitja Nikolaus35181602018-08-03 13:55:30 +0200491 response = self.user.post(reverse(self.LIST_CREATE_URL), data)
492 self.assertEqual(response.status_code, status.HTTP_201_CREATED)
Borjan Tchakaloff866f75e2018-03-01 12:04:00 +0400493
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200494
Mitja Nikolaus35181602018-08-03 13:55:30 +0200495# pylint: disable=too-many-ancestors
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200496class CrashreportListTestCase(HeartbeatListTestCase):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200497 """Test cases for crash reports."""
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200498
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +0100499 LIST_CREATE_URL = "api_v1_crashreports"
500 RETRIEVE_URL = "api_v1_crashreport"
501 LIST_CREATE_BY_UUID_URL = "api_v1_crashreports_by_uuid"
502 RETRIEVE_BY_UUID_URL = "api_v1_crashreport_by_uuid"
Dirk Vogt67eb1482016-10-13 12:42:56 +0200503
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400504 @staticmethod
505 def _create_dummy_data(**kwargs):
506 return Dummy.crashreport_data(**kwargs)
Dirk Vogt67eb1482016-10-13 12:42:56 +0200507
508
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400509class LogfileUploadTest(DeviceRegisterAPITestCase):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200510 """Test cases for upload of log files."""
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400511
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +0100512 LIST_CREATE_URL = "api_v1_crashreports"
513 PUT_LOGFILE_URL = "api_v1_putlogfile_for_device_id"
514
Mitja Nikolaus35181602018-08-03 13:55:30 +0200515 def _upload_crashreport(self, user, uuid):
516 """
517 Upload dummy crashreport data.
Dirk Vogt67eb1482016-10-13 12:42:56 +0200518
Mitja Nikolaus35181602018-08-03 13:55:30 +0200519 Args:
520 user: The user which should be used for uploading the report
521 uuid: The uuid of the device to which the report should be uploaded
Dirk Vogt36635692016-10-17 12:19:10 +0200522
Mitja Nikolaus35181602018-08-03 13:55:30 +0200523 Returns: The local id of the device for which the report was uploaded.
524
525 """
526 data = Dummy.crashreport_data(uuid=uuid)
527 response = user.post(reverse(self.LIST_CREATE_URL), data)
528 self.assertEqual(status.HTTP_201_CREATED, response.status_code)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200529 self.assertTrue("device_local_id" in response.data)
530 device_local_id = response.data["device_local_id"]
Mitja Nikolaus35181602018-08-03 13:55:30 +0200531
532 return device_local_id
533
534 def _test_logfile_upload(self, user, uuid):
535 # Upload crashreport
536 device_local_id = self._upload_crashreport(user, uuid)
537
538 # Upload a logfile for the crashreport
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200539 logfile = tempfile.NamedTemporaryFile("w+", suffix=".log", delete=True)
Mitja Nikolaus35181602018-08-03 13:55:30 +0200540 logfile.write(u"blihblahblub")
541 response = user.post(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200542 reverse(
543 self.PUT_LOGFILE_URL,
544 args=[uuid, device_local_id, os.path.basename(logfile.name)],
545 ),
546 {"file": logfile},
547 format="multipart",
548 )
Mitja Nikolaus35181602018-08-03 13:55:30 +0200549 self.assertEqual(status.HTTP_201_CREATED, response.status_code)
550
551 def test_logfile_upload_as_user(self):
552 """Test upload of logfiles as device owner."""
553 uuid, user, _ = self._register_device()
554 self._test_logfile_upload(user, uuid)
555
556 def test_logfile_upload_as_admin(self):
557 """Test upload of logfiles as admin user."""
558 uuid, _, _ = self._register_device()
559 self._test_logfile_upload(self.admin, uuid)