blob: 73a479b990671c7af8493a346a4fb727e473992a [file] [log] [blame]
Mitja Nikolaus03e412b2018-09-18 17:50:15 +02001"""Utility functions shared by all crashreports tests."""
2
Mitja Nikolaus6e118472018-10-04 11:15:29 +02003import os
Mitja Nikolauscc90d572018-11-22 16:40:15 +01004import shutil
Mitja Nikolaus7dc86722018-11-27 14:57:39 +01005import zipfile
6from datetime import date, datetime
Mitja Nikolaus03e412b2018-09-18 17:50:15 +02007from typing import Optional
8
Mitja Nikolaus7dc86722018-11-27 14:57:39 +01009import pytz
Mitja Nikolauscc90d572018-11-22 16:40:15 +010010from django.conf import settings
Mitja Nikolause0e83772018-11-05 10:00:53 +010011from django.contrib.auth.models import User, Group
Mitja Nikolaus03e412b2018-09-18 17:50:15 +020012from django.urls import reverse
13from rest_framework import status
14from rest_framework.test import APITestCase, APIClient
15
Mitja Nikolauscc90d572018-11-22 16:40:15 +010016from crashreports.models import (
17 Crashreport,
18 Device,
19 HeartBeat,
20 LogFile,
21 crashreport_file_name,
22)
Mitja Nikolause0e83772018-11-05 10:00:53 +010023from hiccup.allauth_adapters import FP_STAFF_GROUP_NAME
Mitja Nikolaus03e412b2018-09-18 17:50:15 +020024
25
26class InvalidCrashTypeError(BaseException):
27 """Invalid crash type encountered.
28
29 The valid crash type values (strings) are:
30 - 'crash';
31 - 'smpl';
32 - 'other'.
33
34 Args:
35 - crash_type: The invalid crash type.
36 """
37
38 def __init__(self, crash_type):
39 """Initialise the exception using the crash type to build a message.
40
41 Args:
42 crash_type: The invalid crash type.
43 """
44 super(InvalidCrashTypeError, self).__init__(
45 "{} is not a valid crash type".format(crash_type)
46 )
47
48
49class Dummy:
50 """Dummy values for devices, heartbeats and crashreports."""
51
Mitja Nikolaus7dc86722018-11-27 14:57:39 +010052 # Valid unique entries
53 BUILD_FINGERPRINTS = [
54 (
55 "Fairphone/FP2/FP2:5.1/FP2/r4275.1_FP2_gms76_1.13.0"
56 ":user/release-keys"
57 ),
58 (
59 "Fairphone/FP2/FP2:5.1.1/FP2-gms75.1.13.0/FP2-gms75.1.13.0"
60 ":user/release-keys"
61 ),
62 (
63 "Fairphone/FP2/FP2:6.0.1/FP2-gms-18.04.1/FP2-gms-18.04.1"
64 ":user/release-keys"
65 ),
66 ("Fairphone/FP2/FP2:7.1.2/18.07.2/gms-7480c31d:user/release-keys"),
67 ]
68 RADIO_VERSIONS = [
69 "4437.1-FP2-0-07",
70 "4437.1-FP2-0-08",
71 "4437.1-FP2-0-09",
72 "4437.1-FP2-0-10",
73 ]
74 UUIDs = ["e1c0cc95-ab8d-461a-a768-cb8d9d7fdb04"]
75
Mitja Nikolausfd452f82018-11-07 11:53:59 +010076 USERNAMES = ["testuser1", "testuser2", "testuser3", "testuser4"]
Mitja Nikolaus7dc86722018-11-27 14:57:39 +010077
78 DATES = [date(2018, 3, 19), date(2018, 3, 26), date(2018, 5, 1)]
79
80 DEFAULT_DUMMY_USER_VALUES = {"username": USERNAMES[0]}
81
Mitja Nikolaus03e412b2018-09-18 17:50:15 +020082 DEFAULT_DUMMY_DEVICE_REGISTER_VALUES = {
Mitja Nikolaus7dc86722018-11-27 14:57:39 +010083 "board_date": datetime(2015, 12, 15, 1, 23, 45, tzinfo=pytz.utc),
Mitja Nikolaus03e412b2018-09-18 17:50:15 +020084 "chipset": "Qualcomm MSM8974PRO-AA",
85 }
86
Mitja Nikolaus7dc86722018-11-27 14:57:39 +010087 DEFAULT_DUMMY_DEVICE_VALUES = DEFAULT_DUMMY_DEVICE_REGISTER_VALUES.copy()
88 DEFAULT_DUMMY_DEVICE_VALUES.update(
89 {"token": "64111c62d521fb4724454ca6dea27e18f93ef56e"}
90 )
91
Mitja Nikolaus03e412b2018-09-18 17:50:15 +020092 DEFAULT_DUMMY_HEARTBEAT_VALUES = {
Mitja Nikolaus03e412b2018-09-18 17:50:15 +020093 "app_version": 10100,
94 "uptime": (
95 "up time: 16 days, 21:49:56, idle time: 5 days, 20:55:04, "
96 "sleep time: 10 days, 20:46:27"
97 ),
Mitja Nikolaus7dc86722018-11-27 14:57:39 +010098 "build_fingerprint": BUILD_FINGERPRINTS[0],
99 "radio_version": RADIO_VERSIONS[0],
Mitja Nikolausfd452f82018-11-07 11:53:59 +0100100 "date": date(2018, 3, 19),
101 }
102
103 ALTERNATIVE_HEARTBEAT_VALUES = {
104 "app_version": 10101,
105 "uptime": (
106 "up time: 2 days, 12:39:13, idle time: 2 days, 11:35:01, "
107 "sleep time: 2 days, 11:56:12"
108 ),
109 "build_fingerprint": BUILD_FINGERPRINTS[1],
110 "radio_version": RADIO_VERSIONS[1],
111 "date": date(2018, 3, 19),
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200112 }
113
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100114 DEFAULT_DUMMY_CRASHREPORT_VALUES = DEFAULT_DUMMY_HEARTBEAT_VALUES.copy()
115 DEFAULT_DUMMY_CRASHREPORT_VALUES.update(
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200116 {
Mitja Nikolausfd452f82018-11-07 11:53:59 +0100117 "is_fake_report": False,
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100118 "boot_reason": Crashreport.BOOT_REASON_UNKOWN,
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200119 "power_on_reason": "it was powered on",
120 "power_off_reason": "something happened and it went off",
Mitja Nikolausfd452f82018-11-07 11:53:59 +0100121 "date": datetime(2018, 3, 19, 12, 0, 0, tzinfo=pytz.utc),
122 }
123 )
124
125 ALTERNATIVE_CRASHREPORT_VALUES = ALTERNATIVE_HEARTBEAT_VALUES.copy()
126 ALTERNATIVE_CRASHREPORT_VALUES.update(
127 {
128 "is_fake_report": True,
129 "boot_reason": Crashreport.BOOT_REASON_KEYBOARD_POWER_ON,
130 "power_on_reason": "alternative power on reason",
131 "power_off_reason": "alternative power off reason",
132 "date": datetime(2018, 3, 19, 12, 0, 0, tzinfo=pytz.utc),
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200133 }
134 )
135
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100136 DEFAULT_DUMMY_LOG_FILE_NAME = "dmesg.log"
137
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200138 CRASH_TYPE_TO_BOOT_REASON_MAP = {
139 "crash": Crashreport.BOOT_REASON_KEYBOARD_POWER_ON,
140 "smpl": Crashreport.BOOT_REASON_RTC_ALARM,
141 "other": "whatever",
142 }
143
Mitja Nikolausfd452f82018-11-07 11:53:59 +0100144 DEFAULT_DUMMY_LOG_FILE_FILENAMES = [
145 "test_logfile_1.zip",
146 "test_logfile_2.zip",
147 ]
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100148 DEFAULT_DUMMY_LOG_FILE_DIRECTORY = os.path.join("resources", "test")
149
Mitja Nikolauscc90d572018-11-22 16:40:15 +0100150 DEFAULT_DUMMY_LOG_FILE_VALUES = {
151 "logfile_type": "last_kmsg",
Mitja Nikolausfd452f82018-11-07 11:53:59 +0100152 "logfile": DEFAULT_DUMMY_LOG_FILE_FILENAMES[0],
Mitja Nikolauscc90d572018-11-22 16:40:15 +0100153 }
154
Mitja Nikolausfd452f82018-11-07 11:53:59 +0100155 DEFAULT_DUMMY_LOG_FILE_PATHS = [
156 os.path.join(
157 DEFAULT_DUMMY_LOG_FILE_DIRECTORY,
158 DEFAULT_DUMMY_LOG_FILE_FILENAMES[0],
159 ),
160 os.path.join(
161 DEFAULT_DUMMY_LOG_FILE_DIRECTORY,
162 DEFAULT_DUMMY_LOG_FILE_FILENAMES[1],
163 ),
164 ]
165
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200166 @staticmethod
167 def _update_copy(original, update):
168 """Merge fields of update into a copy of original."""
169 data = original.copy()
170 data.update(update)
171 return data
172
173 @staticmethod
174 def device_register_data(**kwargs):
175 """Return the data required to register a device.
176
177 Use the values passed as keyword arguments or default to the ones
178 from `Dummy.DEFAULT_DUMMY_DEVICE_REGISTER_VALUES`.
179 """
180 return Dummy._update_copy(
181 Dummy.DEFAULT_DUMMY_DEVICE_REGISTER_VALUES, kwargs
182 )
183
184 @staticmethod
185 def heartbeat_data(**kwargs):
186 """Return the data required to create a heartbeat.
187
188 Use the values passed as keyword arguments or default to the ones
189 from `Dummy.DEFAULT_DUMMY_HEARTBEAT_VALUES`.
190 """
191 return Dummy._update_copy(Dummy.DEFAULT_DUMMY_HEARTBEAT_VALUES, kwargs)
192
193 @staticmethod
Mitja Nikolausfd452f82018-11-07 11:53:59 +0100194 def alternative_heartbeat_data(**kwargs):
195 """Return the alternative data required to create a heartbeat.
196
197 Use the values passed as keyword arguments or default to the ones
198 from `Dummy.ALTERNATIVE_HEARTBEAT_VALUES`.
199 """
200 return Dummy._update_copy(Dummy.ALTERNATIVE_HEARTBEAT_VALUES, kwargs)
201
202 @staticmethod
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200203 def crashreport_data(report_type: Optional[str] = None, **kwargs):
204 """Return the data required to create a crashreport.
205
206 Use the values passed as keyword arguments or default to the ones
207 from `Dummy.DEFAULT_DUMMY_CRASHREPORTS_VALUES`.
208
209 Args:
210 report_type: A valid value from
211 `Dummy.CRASH_TYPE_TO_BOOT_REASON_MAP.keys()` that will
212 define the boot reason if not explicitly defined in the
213 keyword arguments already.
214 """
215 data = Dummy._update_copy(
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100216 Dummy.DEFAULT_DUMMY_CRASHREPORT_VALUES, kwargs
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200217 )
218 if report_type and "boot_reason" not in kwargs:
219 if report_type not in Dummy.CRASH_TYPE_TO_BOOT_REASON_MAP:
220 raise InvalidCrashTypeError(report_type)
221 data["boot_reason"] = Dummy.CRASH_TYPE_TO_BOOT_REASON_MAP.get(
222 report_type
223 )
224 return data
225
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100226 @staticmethod
Mitja Nikolausfd452f82018-11-07 11:53:59 +0100227 def alternative_crashreport_data(**kwargs):
228 """Return the alternative data required to create a crashreport.
229
230 Use the values passed as keyword arguments or default to the ones
231 from `Dummy.ALTERNATIVE_CRASHREPORT_VALUES`.
232 """
233 return Dummy._update_copy(Dummy.ALTERNATIVE_CRASHREPORT_VALUES, kwargs)
234
235 @staticmethod
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100236 def create_dummy_user(**kwargs):
237 """Create a dummy user instance.
238
239 The dummy instance is created and saved to the database.
240 Args:
241 **kwargs:
242 Optional arguments to extend/overwrite the default values.
243
244 Returns: The created user instance.
245
246 """
247 entity = User(
248 **Dummy._update_copy(Dummy.DEFAULT_DUMMY_USER_VALUES, kwargs)
249 )
250 entity.save()
251 return entity
252
253 @staticmethod
254 def create_dummy_device(user, **kwargs):
255 """Create a dummy device instance.
256
257 The dummy instance is created and saved to the database.
258 Args:
259 user: The user instance that the device should relate to
260 **kwargs:
261 Optional arguments to extend/overwrite the default values.
262
263 Returns: The created device instance.
264
265 """
266 entity = Device(
267 user=user,
268 **Dummy._update_copy(Dummy.DEFAULT_DUMMY_DEVICE_VALUES, kwargs)
269 )
270 entity.save()
271 return entity
272
273 @staticmethod
274 def create_dummy_report(report_type, device, **kwargs):
275 """Create a dummy report instance of the given report class type.
276
277 The dummy instance is created and saved to the database.
278 Args:
279 report_type: The class of the report type to be created.
280 user: The device instance that the heartbeat should relate to
281 **kwargs:
282 Optional arguments to extend/overwrite the default values.
283
284 Returns: The created report instance.
285
286 Raises:
287 RuntimeError: If report_type is not a report class type.
288
289 """
290 if report_type == HeartBeat:
291 entity = HeartBeat(
292 device=device,
293 **Dummy._update_copy(
294 Dummy.DEFAULT_DUMMY_HEARTBEAT_VALUES, kwargs
295 )
296 )
297 elif report_type == Crashreport:
298 entity = Crashreport(
299 device=device,
300 **Dummy._update_copy(
301 Dummy.DEFAULT_DUMMY_CRASHREPORT_VALUES, kwargs
302 )
303 )
304 else:
305 raise RuntimeError(
306 "No dummy report instance can be created for {}".format(
307 report_type.__name__
308 )
309 )
310 entity.save()
311 return entity
312
313 @staticmethod
314 def create_dummy_log_file(crashreport, **kwargs):
315 """Create a dummy log file instance.
316
317 The dummy instance is created and saved to the database.
318
319 Args:
320 crashreport: The crashreport that the log file belongs to.
321 **kwargs: Optional arguments to extend/overwrite the default values.
322
323 Returns: The created log file instance.
324
325 """
326 entity = LogFile(
327 crashreport=crashreport,
328 **Dummy._update_copy(Dummy.DEFAULT_DUMMY_LOG_FILE_VALUES, kwargs)
329 )
330
331 entity.save()
332 return entity
333
334 @staticmethod
Mitja Nikolauscc90d572018-11-22 16:40:15 +0100335 def create_dummy_log_file_with_actual_file(crashreport, **kwargs):
336 """Create a dummy log file instance along with a file.
337
338 The dummy instance is created and saved to the database. The log file
339 is copied to the respective location in the media directory.
340
341 Args:
342 crashreport: The crashreport that the log file belongs to.
343 **kwargs: Optional arguments to extend/overwrite the default values.
344
345 Returns: The created log file instance and the path to the copied file.
346
347 """
348 logfile = Dummy.create_dummy_log_file(crashreport, **kwargs)
349 logfile_filename = os.path.basename(logfile.logfile.path)
350 test_logfile_path = os.path.join(
351 settings.MEDIA_ROOT,
352 crashreport_file_name(logfile, logfile_filename),
353 )
354 logfile.logfile = test_logfile_path
355 logfile.save()
356
357 os.makedirs(os.path.dirname(test_logfile_path))
358 shutil.copy(
359 os.path.join(
360 Dummy.DEFAULT_DUMMY_LOG_FILE_DIRECTORY, logfile_filename
361 ),
362 test_logfile_path,
363 )
364 return logfile, test_logfile_path
365
366 @staticmethod
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100367 def read_logfile_contents(path_to_zipfile, logfile_name):
368 """Read bytes of a zipped logfile."""
369 archive = zipfile.ZipFile(path_to_zipfile, "r")
370 return archive.read(logfile_name)
371
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200372
373class HiccupCrashreportsAPITestCase(APITestCase):
374 """Base class that offers a device registration method."""
375
376 REGISTER_DEVICE_URL = "api_v1_register_device"
377
378 def setUp(self):
Mitja Nikolause0e83772018-11-05 10:00:53 +0100379 """Create a Fairphone staff user for accessing the API.
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200380
381 The APIClient that can be used to make authenticated requests to the
Mitja Nikolause0e83772018-11-05 10:00:53 +0100382 server is stored in self.fp_staff_client.
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200383 """
Mitja Nikolause0e83772018-11-05 10:00:53 +0100384 fp_staff_group = Group.objects.get(name=FP_STAFF_GROUP_NAME)
385 fp_staff_user = User.objects.create_user(
386 "fp_staff", "somebody@fairphone.com", "thepassword"
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200387 )
Mitja Nikolause0e83772018-11-05 10:00:53 +0100388 fp_staff_user.groups.add(fp_staff_group)
389 self.fp_staff_client = APIClient()
390 self.fp_staff_client.force_login(fp_staff_user)
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200391
392 def _register_device(self, **kwargs):
393 """Register a new device.
394
395 Arguments:
396 **kwargs: The data to pass the dummy data creation
397 method `Dummy.device_register_data`.
398 Returns:
399 (UUID, APIClient, str): The uuid of the new device as well as an
400 authentication token and the associated user with credentials.
401
402 """
403 data = Dummy.device_register_data(**kwargs)
404 response = self.client.post(reverse(self.REGISTER_DEVICE_URL), data)
405 self.assertEqual(response.status_code, status.HTTP_200_OK)
406
407 uuid = response.data["uuid"]
408 token = response.data["token"]
409 user = APIClient()
410 user.credentials(HTTP_AUTHORIZATION="Token " + token)
411
412 return uuid, user, token