blob: 2267a79c6196bceeb0b29a49dc4d105d86b7522c [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
4
Dirk Vogtc9e10ab2016-10-12 13:58:15 +02005from django.contrib.auth.models import User
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +01006from django.urls import reverse
Mitja Nikolaus35181602018-08-03 13:55:30 +02007
Dirk Vogt36635692016-10-17 12:19:10 +02008from rest_framework import status
Mitja Nikolaus35181602018-08-03 13:55:30 +02009from rest_framework.test import APIClient, APITestCase
Dirk Vogtf130c752016-08-23 14:45:01 +020010
Borjan Tchakalofffe5f25c2018-03-19 18:33:24 +040011from crashreports.models import Crashreport
12
13
14class InvalidCrashTypeError(BaseException):
Mitja Nikolaus35181602018-08-03 13:55:30 +020015 """Invalid crash type encountered.
Borjan Tchakalofffe5f25c2018-03-19 18:33:24 +040016
17 The valid crash type values (strings) are:
18 - 'crash';
19 - 'smpl';
20 - 'other'.
21
22 Args:
23 - crash_type: The invalid crash type.
24 """
25
26 def __init__(self, crash_type):
Mitja Nikolaus35181602018-08-03 13:55:30 +020027 """Initialise the exception using the crash type to build a message.
28
29 Args:
30 crash_type: The invalid crash type.
31 """
Borjan Tchakalofffe5f25c2018-03-19 18:33:24 +040032 super(InvalidCrashTypeError, self).__init__(
33 '{} is not a valid crash type'.format(crash_type))
34
Borjan Tchakaloffec814a72018-02-28 11:16:53 +040035
Mitja Nikolaus35181602018-08-03 13:55:30 +020036class Dummy():
37 """Dummy values for devices, heartbeats and crashreports."""
38
Borjan Tchakaloffec814a72018-02-28 11:16:53 +040039 DEFAULT_DUMMY_DEVICE_REGISTER_VALUES = {
40 'board_date': '2015-12-15T01:23:45Z',
41 'chipset': 'Qualcomm MSM8974PRO-AA',
42 }
43
44 DEFAULT_DUMMY_HEARTBEAT_VALUES = {
45 'uuid': None,
46 'app_version': 10100,
47 'uptime': (
48 'up time: 16 days, 21:49:56, idle time: 5 days, 20:55:04, '
49 'sleep time: 10 days, 20:46:27'),
50 'build_fingerprint': (
51 'Fairphone/FP2/FP2:6.0.1/FP2-gms-18.03.1/FP2-gms-18.03.1:user/'
52 'release-keys'),
53 'radio_version': '4437.1-FP2-0-08',
54 'date': '2018-03-19T09:58:30.386Z',
55 }
56
57 DEFAULT_DUMMY_CRASHREPORTS_VALUES = DEFAULT_DUMMY_HEARTBEAT_VALUES.copy()
58 DEFAULT_DUMMY_CRASHREPORTS_VALUES.update({
59 'is_fake_report': 0,
60 'boot_reason': 'why?',
61 'power_on_reason': 'it was powered on',
62 'power_off_reason': 'something happened and it went off',
63 })
64
Borjan Tchakalofffe5f25c2018-03-19 18:33:24 +040065 CRASH_TYPE_TO_BOOT_REASON_MAP = {
66 'crash': Crashreport.BOOT_REASON_KEYBOARD_POWER_ON,
67 'smpl': Crashreport.BOOT_REASON_RTC_ALARM,
68 'other': 'whatever',
69 }
70
Borjan Tchakaloffec814a72018-02-28 11:16:53 +040071 @staticmethod
72 def _update_copy(original, update):
Mitja Nikolaus35181602018-08-03 13:55:30 +020073 """Merge fields of update into a copy of original."""
Borjan Tchakaloffec814a72018-02-28 11:16:53 +040074 data = original.copy()
75 data.update(update)
76 return data
77
78 @staticmethod
79 def device_register_data(**kwargs):
Mitja Nikolaus35181602018-08-03 13:55:30 +020080 """Return the data required to register a device.
Borjan Tchakaloffec814a72018-02-28 11:16:53 +040081
Mitja Nikolaus35181602018-08-03 13:55:30 +020082 Use the values passed as keyword arguments or default to the ones
83 from `Dummy.DEFAULT_DUMMY_DEVICE_REGISTER_VALUES`.
Borjan Tchakaloffec814a72018-02-28 11:16:53 +040084 """
85 return Dummy._update_copy(
86 Dummy.DEFAULT_DUMMY_DEVICE_REGISTER_VALUES, kwargs)
87
88 @staticmethod
89 def heartbeat_data(**kwargs):
Mitja Nikolaus35181602018-08-03 13:55:30 +020090 """Return the data required to create a heartbeat.
Borjan Tchakaloffec814a72018-02-28 11:16:53 +040091
Mitja Nikolaus35181602018-08-03 13:55:30 +020092 Use the values passed as keyword arguments or default to the ones
93 from `Dummy.DEFAULT_DUMMY_HEARTBEAT_VALUES`.
Borjan Tchakaloffec814a72018-02-28 11:16:53 +040094 """
95 return Dummy._update_copy(Dummy.DEFAULT_DUMMY_HEARTBEAT_VALUES, kwargs)
96
97 @staticmethod
Borjan Tchakalofffe5f25c2018-03-19 18:33:24 +040098 def crashreport_data(report_type=None, **kwargs):
Mitja Nikolaus35181602018-08-03 13:55:30 +020099 """Return the data required to create a crashreport.
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400100
Mitja Nikolaus35181602018-08-03 13:55:30 +0200101 Use the values passed as keyword arguments or default to the ones
102 from `Dummy.DEFAULT_DUMMY_CRASHREPORTS_VALUES`.
Borjan Tchakalofffe5f25c2018-03-19 18:33:24 +0400103
Mitja Nikolaus35181602018-08-03 13:55:30 +0200104 Args:
105 report_type (str, optional): A valid value from
106 `Dummy.CRASH_TYPE_TO_BOOT_REASON_MAP.keys()` that will
107 define the boot reason if not explicitly defined in the
108 keyword arguments already.
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400109 """
Borjan Tchakalofffe5f25c2018-03-19 18:33:24 +0400110 data = Dummy._update_copy(
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400111 Dummy.DEFAULT_DUMMY_CRASHREPORTS_VALUES, kwargs)
Borjan Tchakalofffe5f25c2018-03-19 18:33:24 +0400112 if report_type and 'boot_reason' not in kwargs:
113 if report_type not in Dummy.CRASH_TYPE_TO_BOOT_REASON_MAP:
114 raise InvalidCrashTypeError(report_type)
115 data['boot_reason'] = Dummy.CRASH_TYPE_TO_BOOT_REASON_MAP.get(
116 report_type)
117 return data
Dirk Vogtf2a33422016-10-11 17:17:26 +0200118
119
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400120class DeviceRegisterAPITestCase(APITestCase):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200121 """Base class that offers a device registration method."""
Dirk Vogtf2a33422016-10-11 17:17:26 +0200122
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +0100123 REGISTER_DEVICE_URL = "api_v1_register_device"
124
Dirk Vogtf2a33422016-10-11 17:17:26 +0200125 def setUp(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200126 """Create an admin user for accessing the API.
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400127
Mitja Nikolaus35181602018-08-03 13:55:30 +0200128 The APIClient that can be used to make authenticated requests to the
129 server is stored in self.admin.
130 """
131 admin_user = User.objects.create_superuser(
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400132 'somebody', 'somebody@example.com', 'thepassword')
133 self.admin = APIClient()
Mitja Nikolaus35181602018-08-03 13:55:30 +0200134 self.admin.force_authenticate(admin_user)
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400135
136 def _register_device(self, **kwargs):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200137 """Register a new device.
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400138
139 Arguments:
140 **kwargs: The data to pass the dummy data creation
141 method `Dummy.device_register_data`.
142 Returns:
Mitja Nikolaus35181602018-08-03 13:55:30 +0200143 (UUID, APIClient, str): The uuid of the new device as well as an
144 authentication token and the associated user with credentials.
145
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400146 """
147 data = Dummy.device_register_data(**kwargs)
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +0100148 response = self.client.post(reverse(self.REGISTER_DEVICE_URL), data)
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400149 self.assertEqual(response.status_code, status.HTTP_200_OK)
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400150
Mitja Nikolaus35181602018-08-03 13:55:30 +0200151 uuid = response.data['uuid']
152 token = response.data['token']
153 user = APIClient()
154 user.credentials(HTTP_AUTHORIZATION='Token ' + token)
155
156 return uuid, user, token
157
158
159class DeviceTestCase(DeviceRegisterAPITestCase):
160 """Test cases for registering devices."""
161
162 def test_register(self):
163 """Test registration of devices."""
164 response = self.client.post(reverse(self.REGISTER_DEVICE_URL),
165 Dummy.device_register_data())
166 self.assertTrue("token" in response.data)
167 self.assertTrue("uuid" in response.data)
168 self.assertEqual(response.status_code, status.HTTP_200_OK)
169
170 def test_create_missing_fields(self):
171 """Test registration with missing fields."""
172 response = self.client.post(reverse(self.REGISTER_DEVICE_URL))
173 self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
174
175 def test_create_missing_board_date(self):
176 """Test registration with missing board date."""
177 data = Dummy.device_register_data()
178 data.pop('board_date')
179 response = self.client.post(reverse(self.REGISTER_DEVICE_URL), data)
180 self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
181
182 def test_create_missing_chipset(self):
183 """Test registration with missing chipset."""
184 data = Dummy.device_register_data()
185 data.pop('chipset')
186 response = self.client.post(reverse(self.REGISTER_DEVICE_URL), data)
187 self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
188
189 def test_create_invalid_board_date(self):
190 """Test registration with invalid board date."""
191 data = Dummy.device_register_data()
192 data['board_date'] = 'not_a_valid_date'
193 response = self.client.post(reverse(self.REGISTER_DEVICE_URL), data)
194 self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
195
196 def test_create_non_existent_time_board_date(self):
197 """Test registration with non existing time.
198
199 Test the resolution of a naive date-time in which the
200 Europe/Amsterdam daylight saving time transition moved the time
201 "forward". The server should not crash when receiving a naive
202 date-time which does not exist in the server timezone or locale.
203 """
204 data = Dummy.device_register_data()
205 # In 2017, the Netherlands changed from CET to CEST on March,
206 # 26 at 02:00
207 data['board_date'] = '2017-03-26 02:34:56'
208 response = self.client.post(reverse(self.REGISTER_DEVICE_URL), data)
209 self.assertEqual(response.status_code, status.HTTP_200_OK)
210
211 def test_create_ambiguous_time_board_date(self):
212 """Test registration with ambiguous time.
213
214 Test the resolution of a naive date-time in which the
215 Europe/Amsterdam daylight saving time transition moved the time
216 "backward". The server should not crash when receiving a naive
217 date-time that can belong to multiple timezones.
218 """
219 data = Dummy.device_register_data()
220 # In 2017, the Netherlands changed from CEST to CET on October,
221 # 29 at 03:00
222 data['board_date'] = '2017-10-29 02:34:56'
223 response = self.client.post(reverse(self.REGISTER_DEVICE_URL), data)
224 self.assertEqual(response.status_code, status.HTTP_200_OK)
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400225
226
227class ListDevicesTestCase(DeviceRegisterAPITestCase):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200228 """Test cases for listing and deleting devices."""
Dirk Vogtf2a33422016-10-11 17:17:26 +0200229
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +0100230 LIST_CREATE_URL = "api_v1_list_devices"
231 RETRIEVE_URL = "api_v1_retrieve_device"
232
Dirk Vogtf2a33422016-10-11 17:17:26 +0200233 def test_device_list(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200234 """Test registration of 2 devices."""
235 number_of_devices = 2
236 uuids = [
237 str(self._register_device()[0])
238 for _ in range(number_of_devices)
239 ]
240
241 response = self.admin.get(reverse(self.LIST_CREATE_URL), {})
242 self.assertEqual(response.status_code, status.HTTP_200_OK)
243 self.assertEqual(len(response.data['results']), number_of_devices)
244 for result in response.data['results']:
245 self.assertIn(result['uuid'], uuids)
Dirk Vogtf2a33422016-10-11 17:17:26 +0200246
247 def test_device_list_unauth(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200248 """Test listing devices without authentication."""
249 response = self.client.get(reverse(self.LIST_CREATE_URL), {})
250 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
Dirk Vogtf2a33422016-10-11 17:17:26 +0200251
252 def test_retrieve_device_auth(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200253 """Test retrieval of devices as admin user."""
254 uuid, _, token = self._register_device()
255 response = self.admin.get(reverse(self.RETRIEVE_URL, args=[uuid]), {})
256 self.assertEqual(response.status_code, status.HTTP_200_OK)
257 self.assertEqual(response.data['uuid'], str(uuid))
258 self.assertEqual(response.data['token'], token)
Dirk Vogtf2a33422016-10-11 17:17:26 +0200259
260 def test_retrieve_device_unauth(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200261 """Test retrieval of devices without authentication."""
262 uuid, _, _ = self._register_device()
263 response = self.client.get(reverse(self.RETRIEVE_URL, args=[uuid]), {})
264 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
Dirk Vogtf2a33422016-10-11 17:17:26 +0200265
266 def test_delete_device_auth(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200267 """Test deletion of devices as admin user."""
268 uuid, _, _ = self._register_device()
269 url = reverse(self.RETRIEVE_URL, args=[uuid])
270 response = self.admin.delete(url, {})
271 self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
272 response = self.admin.delete(url, {})
273 self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
Dirk Vogtf2a33422016-10-11 17:17:26 +0200274
Dirk Vogtf2a33422016-10-11 17:17:26 +0200275
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400276class HeartbeatListTestCase(DeviceRegisterAPITestCase):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200277 """Test cases for heartbeats."""
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400278
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +0100279 LIST_CREATE_URL = "api_v1_heartbeats"
280 RETRIEVE_URL = "api_v1_heartbeat"
281 LIST_CREATE_BY_UUID_URL = "api_v1_heartbeats_by_uuid"
282 RETRIEVE_BY_UUID_URL = "api_v1_heartbeat_by_uuid"
283
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400284 @staticmethod
285 def _create_dummy_data(**kwargs):
286 return Dummy.heartbeat_data(**kwargs)
287
288 def _post_multiple(self, client, data, count):
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +0100289 return [
290 client.post(reverse(self.LIST_CREATE_URL), data)
291 for _ in range(count)]
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400292
293 def _retrieve_single(self, user):
294 count = 5
Mitja Nikolaus35181602018-08-03 13:55:30 +0200295 response = self._post_multiple(self.admin, self.data, count)
296 self.assertEqual(len(response), count)
297 self.assertEqual(response[0].status_code, status.HTTP_201_CREATED)
298 url = reverse(self.RETRIEVE_URL, args=[response[0].data['id']])
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400299 request = user.get(url)
300 return request.status_code
301
302 def _retrieve_single_by_device(self, user):
303 count = 5
Mitja Nikolaus35181602018-08-03 13:55:30 +0200304 response = self._post_multiple(self.user, self.data, count)
305 self.assertEqual(len(response), count)
306 self.assertEqual(response[0].status_code, status.HTTP_201_CREATED)
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +0100307 url = reverse(self.RETRIEVE_BY_UUID_URL, args=[
Mitja Nikolaus35181602018-08-03 13:55:30 +0200308 self.uuid, response[0].data['device_local_id']])
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400309 request = user.get(url)
310 return request.status_code
Dirk Vogtf2a33422016-10-11 17:17:26 +0200311
312 def setUp(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200313 """Set up a device and some data."""
314 super().setUp()
315 self.uuid, self.user, self.token = self._register_device()
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400316 self.data = self._create_dummy_data(uuid=self.uuid)
Dirk Vogt67eb1482016-10-13 12:42:56 +0200317
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200318 def test_create_no_auth(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200319 """Test creation without authentication."""
320 noauth_client = APIClient()
321 response = noauth_client.post(
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +0100322 reverse(self.LIST_CREATE_URL), self.data)
Mitja Nikolaus35181602018-08-03 13:55:30 +0200323 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
Dirk Vogtf2a33422016-10-11 17:17:26 +0200324
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200325 def test_create_as_admin(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200326 """Test creation as admin."""
327 response = self.admin.post(reverse(self.LIST_CREATE_URL), self.data)
328 self.assertEqual(response.status_code, status.HTTP_201_CREATED)
329 self.assertTrue(response.data['id'] > 0)
Dirk Vogtf2a33422016-10-11 17:17:26 +0200330
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200331 def test_create_as_admin_not_existing_device(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200332 """Test creation of heartbeat on non-existing device."""
333 response = self.admin.post(
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +0100334 reverse(self.LIST_CREATE_URL), self._create_dummy_data())
Mitja Nikolaus35181602018-08-03 13:55:30 +0200335 self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200336
337 def test_create_as_uuid_owner(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200338 """Test creation as owner."""
339 response = self.user.post(
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +0100340 reverse(self.LIST_CREATE_URL),
341 self._create_dummy_data(uuid=self.uuid))
Mitja Nikolaus35181602018-08-03 13:55:30 +0200342 self.assertEqual(response.status_code, status.HTTP_201_CREATED)
343 self.assertEqual(response.data['id'], -1)
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200344
345 def test_create_as_uuid_not_owner(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200346 """Test creation as non-owner."""
347 uuid, _, _ = self._register_device()
348 response = self.user.post(
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +0100349 reverse(self.LIST_CREATE_URL),
Mitja Nikolaus35181602018-08-03 13:55:30 +0200350 self._create_dummy_data(uuid=uuid))
351 self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200352
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200353 def test_list(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200354 """Test listing of heartbeats."""
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200355 count = 5
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400356 self._post_multiple(self.user, self.data, count)
Mitja Nikolaus35181602018-08-03 13:55:30 +0200357 response = self.admin.get(reverse(self.LIST_CREATE_URL))
358 self.assertEqual(response.status_code, status.HTTP_200_OK)
359 self.assertEqual(len(response.data['results']), count)
Franz-Xaver Geigerd612fd22018-02-28 10:33:08 +0100360
361 def test_retrieve_single_admin(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200362 """Test retrieval as admin."""
Franz-Xaver Geigerd612fd22018-02-28 10:33:08 +0100363 self.assertEqual(
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400364 self._retrieve_single(self.admin), status.HTTP_200_OK)
Dirk Vogte1784882016-10-13 16:09:38 +0200365
366 def test_retrieve_single_device_owner(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200367 """Test retrieval as device owner."""
Franz-Xaver Geigerd612fd22018-02-28 10:33:08 +0100368 self.assertEqual(
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400369 self._retrieve_single(self.user), status.HTTP_403_FORBIDDEN)
370
371 def test_retrieve_single_noauth(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200372 """Test retrieval without authentication."""
373 noauth_client = APIClient()
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400374 self.assertEqual(
Mitja Nikolaus35181602018-08-03 13:55:30 +0200375 self._retrieve_single(noauth_client),
Franz-Xaver Geigerd612fd22018-02-28 10:33:08 +0100376 status.HTTP_401_UNAUTHORIZED)
Dirk Vogte1784882016-10-13 16:09:38 +0200377
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400378 def test_retrieve_single_by_device_admin(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200379 """Test retrieval by device as admin."""
Franz-Xaver Geigerd612fd22018-02-28 10:33:08 +0100380 self.assertEqual(
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400381 self._retrieve_single_by_device(self.admin), status.HTTP_200_OK)
Dirk Vogt0d9d5d22016-10-13 16:17:57 +0200382
383 def test_retrieve_single_by_device_device_owner(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200384 """Test retrieval by device as owner."""
Franz-Xaver Geigerd612fd22018-02-28 10:33:08 +0100385 self.assertEqual(
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400386 self._retrieve_single_by_device(self.user),
387 status.HTTP_403_FORBIDDEN)
388
389 def test_retrieve_single_by_device_noauth(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200390 """Test retrieval by device without authentication."""
391 noauth_client = APIClient()
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400392 self.assertEqual(
Mitja Nikolaus35181602018-08-03 13:55:30 +0200393 self._retrieve_single_by_device(noauth_client),
Franz-Xaver Geigerd612fd22018-02-28 10:33:08 +0100394 status.HTTP_401_UNAUTHORIZED)
Dirk Vogt0d9d5d22016-10-13 16:17:57 +0200395
Dirk Vogte1784882016-10-13 16:09:38 +0200396 def test_list_by_uuid(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200397 """Test listing of devices by UUID."""
Dirk Vogte1784882016-10-13 16:09:38 +0200398 count = 5
Mitja Nikolaus35181602018-08-03 13:55:30 +0200399 uuid, _, _ = self._register_device()
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400400 self._post_multiple(self.user, self.data, count)
401 self._post_multiple(
Mitja Nikolaus35181602018-08-03 13:55:30 +0200402 self.admin, self._create_dummy_data(uuid=uuid), count)
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +0100403 url = reverse(self.LIST_CREATE_BY_UUID_URL, args=[self.uuid])
Mitja Nikolaus35181602018-08-03 13:55:30 +0200404 response = self.admin.get(url)
405 self.assertEqual(response.status_code, status.HTTP_200_OK)
406 self.assertEqual(len(response.data['results']), count)
Dirk Vogte1784882016-10-13 16:09:38 +0200407
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200408 def test_list_noauth(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200409 """Test listing of devices without authentication."""
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200410 count = 5
Mitja Nikolaus35181602018-08-03 13:55:30 +0200411 noauth_client = APIClient()
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400412 self._post_multiple(self.user, self.data, count)
Mitja Nikolaus35181602018-08-03 13:55:30 +0200413 response = noauth_client.get(reverse(self.LIST_CREATE_URL))
414 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200415
416 def test_list_device_owner(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200417 """Test listing as device owner."""
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200418 count = 5
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400419 self._post_multiple(self.user, self.data, count)
Mitja Nikolaus35181602018-08-03 13:55:30 +0200420 response = self.user.get(reverse(self.LIST_CREATE_URL))
421 self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200422
Borjan Tchakaloff869cf922018-02-19 11:01:16 +0100423 def test_no_radio_version(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200424 """Test creation and retrieval without radio version."""
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400425 data = self._create_dummy_data(uuid=self.uuid)
Borjan Tchakaloff869cf922018-02-19 11:01:16 +0100426 data.pop('radio_version')
Mitja Nikolaus35181602018-08-03 13:55:30 +0200427 response = self.user.post(reverse(self.LIST_CREATE_URL), data)
428 self.assertEqual(response.status_code, status.HTTP_201_CREATED)
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +0100429 url = reverse(self.LIST_CREATE_BY_UUID_URL, args=[self.uuid])
Mitja Nikolaus35181602018-08-03 13:55:30 +0200430 response = self.admin.get(url)
431 self.assertEqual(response.status_code, status.HTTP_200_OK)
432 self.assertEqual(len(response.data['results']), 1)
433 self.assertIsNone(response.data['results'][0]['radio_version'])
Borjan Tchakaloff869cf922018-02-19 11:01:16 +0100434
435 def test_radio_version_field(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200436 """Test retrieval of radio version field."""
437 response = self.user.post(reverse(self.LIST_CREATE_URL), self.data)
438 self.assertEqual(response.status_code, status.HTTP_201_CREATED)
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +0100439 url = reverse(self.LIST_CREATE_BY_UUID_URL, args=[self.uuid])
Mitja Nikolaus35181602018-08-03 13:55:30 +0200440 response = self.admin.get(url)
441 self.assertEqual(response.status_code, status.HTTP_200_OK)
442 self.assertEqual(len(response.data['results']), 1)
443 self.assertEqual(response.data['results'][0]['radio_version'],
444 self.data['radio_version'])
Borjan Tchakaloff869cf922018-02-19 11:01:16 +0100445
Borjan Tchakaloff866f75e2018-03-01 12:04:00 +0400446 def test_send_non_existent_time(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200447 """Test sending of heartbeat with non existent time.
448
449 Test the resolution of a naive date-time in which the
450 Europe/Amsterdam daylight saving time transition moved the time
451 "forward".
Borjan Tchakaloff866f75e2018-03-01 12:04:00 +0400452 """
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400453 data = self._create_dummy_data(uuid=self.uuid)
Mitja Nikolaus35181602018-08-03 13:55:30 +0200454 # In 2017, the Netherlands changed from CET to CEST on March,
455 # 26 at 02:00
Borjan Tchakaloff866f75e2018-03-01 12:04:00 +0400456 data['date'] = '2017-03-26 02:34:56'
Mitja Nikolaus35181602018-08-03 13:55:30 +0200457 response = self.user.post(reverse(self.LIST_CREATE_URL), data)
458 self.assertEqual(response.status_code, status.HTTP_201_CREATED)
Borjan Tchakaloff866f75e2018-03-01 12:04:00 +0400459
460 def test_send_ambiguous_time(self):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200461 """Test sending of heartbeat with ambiguous time.
462
463 Test the resolution of a naive date-time in which the
464 Europe/Amsterdam daylight saving time transition moved the time
465 "backward".
Borjan Tchakaloff866f75e2018-03-01 12:04:00 +0400466 """
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400467 data = self._create_dummy_data(uuid=self.uuid)
Mitja Nikolaus35181602018-08-03 13:55:30 +0200468 # In 2017, the Netherlands changed from CEST to CET on October,
469 # 29 at 03:00
Borjan Tchakaloff866f75e2018-03-01 12:04:00 +0400470 data['date'] = '2017-10-29 02:34:56'
Mitja Nikolaus35181602018-08-03 13:55:30 +0200471 response = self.user.post(reverse(self.LIST_CREATE_URL), data)
472 self.assertEqual(response.status_code, status.HTTP_201_CREATED)
Borjan Tchakaloff866f75e2018-03-01 12:04:00 +0400473
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200474
Mitja Nikolaus35181602018-08-03 13:55:30 +0200475# pylint: disable=too-many-ancestors
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200476class CrashreportListTestCase(HeartbeatListTestCase):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200477 """Test cases for crash reports."""
Dirk Vogtc9e10ab2016-10-12 13:58:15 +0200478
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +0100479 LIST_CREATE_URL = "api_v1_crashreports"
480 RETRIEVE_URL = "api_v1_crashreport"
481 LIST_CREATE_BY_UUID_URL = "api_v1_crashreports_by_uuid"
482 RETRIEVE_BY_UUID_URL = "api_v1_crashreport_by_uuid"
Dirk Vogt67eb1482016-10-13 12:42:56 +0200483
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400484 @staticmethod
485 def _create_dummy_data(**kwargs):
486 return Dummy.crashreport_data(**kwargs)
Dirk Vogt67eb1482016-10-13 12:42:56 +0200487
488
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400489class LogfileUploadTest(DeviceRegisterAPITestCase):
Mitja Nikolaus35181602018-08-03 13:55:30 +0200490 """Test cases for upload of log files."""
Borjan Tchakaloffec814a72018-02-28 11:16:53 +0400491
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +0100492 LIST_CREATE_URL = "api_v1_crashreports"
493 PUT_LOGFILE_URL = "api_v1_putlogfile_for_device_id"
494
Mitja Nikolaus35181602018-08-03 13:55:30 +0200495 def _upload_crashreport(self, user, uuid):
496 """
497 Upload dummy crashreport data.
Dirk Vogt67eb1482016-10-13 12:42:56 +0200498
Mitja Nikolaus35181602018-08-03 13:55:30 +0200499 Args:
500 user: The user which should be used for uploading the report
501 uuid: The uuid of the device to which the report should be uploaded
Dirk Vogt36635692016-10-17 12:19:10 +0200502
Mitja Nikolaus35181602018-08-03 13:55:30 +0200503 Returns: The local id of the device for which the report was uploaded.
504
505 """
506 data = Dummy.crashreport_data(uuid=uuid)
507 response = user.post(reverse(self.LIST_CREATE_URL), data)
508 self.assertEqual(status.HTTP_201_CREATED, response.status_code)
509 self.assertTrue('device_local_id' in response.data)
510 device_local_id = response.data['device_local_id']
511
512 return device_local_id
513
514 def _test_logfile_upload(self, user, uuid):
515 # Upload crashreport
516 device_local_id = self._upload_crashreport(user, uuid)
517
518 # Upload a logfile for the crashreport
519 logfile = tempfile.NamedTemporaryFile('w+', suffix=".log", delete=True)
520 logfile.write(u"blihblahblub")
521 response = user.post(
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +0100522 reverse(self.PUT_LOGFILE_URL, args=[
Mitja Nikolaus35181602018-08-03 13:55:30 +0200523 uuid, device_local_id, os.path.basename(logfile.name)
Franz-Xaver Geiger67504d02018-03-19 15:04:48 +0100524 ]),
Mitja Nikolaus35181602018-08-03 13:55:30 +0200525 {'file': logfile}, format="multipart")
526 self.assertEqual(status.HTTP_201_CREATED, response.status_code)
527
528 def test_logfile_upload_as_user(self):
529 """Test upload of logfiles as device owner."""
530 uuid, user, _ = self._register_device()
531 self._test_logfile_upload(user, uuid)
532
533 def test_logfile_upload_as_admin(self):
534 """Test upload of logfiles as admin user."""
535 uuid, _, _ = self._register_device()
536 self._test_logfile_upload(self.admin, uuid)