blob: fcaa2a3f0dbb8fa1438bfde38180742ab8f9eff6 [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
76 USERNAMES = ["testuser1", "testuser2", "testuser3"]
77
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],
100 "date": datetime(2018, 3, 19, 12, 0, 0, tzinfo=pytz.utc),
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200101 }
102
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100103 DEFAULT_DUMMY_CRASHREPORT_VALUES = DEFAULT_DUMMY_HEARTBEAT_VALUES.copy()
104 DEFAULT_DUMMY_CRASHREPORT_VALUES.update(
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200105 {
106 "is_fake_report": 0,
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100107 "boot_reason": Crashreport.BOOT_REASON_UNKOWN,
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200108 "power_on_reason": "it was powered on",
109 "power_off_reason": "something happened and it went off",
110 }
111 )
112
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100113 DEFAULT_DUMMY_LOG_FILE_NAME = "dmesg.log"
114
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200115 CRASH_TYPE_TO_BOOT_REASON_MAP = {
116 "crash": Crashreport.BOOT_REASON_KEYBOARD_POWER_ON,
117 "smpl": Crashreport.BOOT_REASON_RTC_ALARM,
118 "other": "whatever",
119 }
120
Mitja Nikolauscc90d572018-11-22 16:40:15 +0100121 DEFAULT_DUMMY_LOG_FILE_FILENAME = "test_logfile.zip"
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100122 DEFAULT_DUMMY_LOG_FILE_DIRECTORY = os.path.join("resources", "test")
123
Mitja Nikolaus6e118472018-10-04 11:15:29 +0200124 DEFAULT_DUMMY_LOG_FILE_PATH = os.path.join(
Mitja Nikolauscc90d572018-11-22 16:40:15 +0100125 DEFAULT_DUMMY_LOG_FILE_DIRECTORY, DEFAULT_DUMMY_LOG_FILE_FILENAME
Mitja Nikolaus6e118472018-10-04 11:15:29 +0200126 )
127
Mitja Nikolauscc90d572018-11-22 16:40:15 +0100128 DEFAULT_DUMMY_LOG_FILE_VALUES = {
129 "logfile_type": "last_kmsg",
130 "logfile": DEFAULT_DUMMY_LOG_FILE_FILENAME,
131 }
132
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200133 @staticmethod
134 def _update_copy(original, update):
135 """Merge fields of update into a copy of original."""
136 data = original.copy()
137 data.update(update)
138 return data
139
140 @staticmethod
141 def device_register_data(**kwargs):
142 """Return the data required to register a device.
143
144 Use the values passed as keyword arguments or default to the ones
145 from `Dummy.DEFAULT_DUMMY_DEVICE_REGISTER_VALUES`.
146 """
147 return Dummy._update_copy(
148 Dummy.DEFAULT_DUMMY_DEVICE_REGISTER_VALUES, kwargs
149 )
150
151 @staticmethod
152 def heartbeat_data(**kwargs):
153 """Return the data required to create a heartbeat.
154
155 Use the values passed as keyword arguments or default to the ones
156 from `Dummy.DEFAULT_DUMMY_HEARTBEAT_VALUES`.
157 """
158 return Dummy._update_copy(Dummy.DEFAULT_DUMMY_HEARTBEAT_VALUES, kwargs)
159
160 @staticmethod
161 def crashreport_data(report_type: Optional[str] = None, **kwargs):
162 """Return the data required to create a crashreport.
163
164 Use the values passed as keyword arguments or default to the ones
165 from `Dummy.DEFAULT_DUMMY_CRASHREPORTS_VALUES`.
166
167 Args:
168 report_type: A valid value from
169 `Dummy.CRASH_TYPE_TO_BOOT_REASON_MAP.keys()` that will
170 define the boot reason if not explicitly defined in the
171 keyword arguments already.
172 """
173 data = Dummy._update_copy(
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100174 Dummy.DEFAULT_DUMMY_CRASHREPORT_VALUES, kwargs
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200175 )
176 if report_type and "boot_reason" not in kwargs:
177 if report_type not in Dummy.CRASH_TYPE_TO_BOOT_REASON_MAP:
178 raise InvalidCrashTypeError(report_type)
179 data["boot_reason"] = Dummy.CRASH_TYPE_TO_BOOT_REASON_MAP.get(
180 report_type
181 )
182 return data
183
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100184 @staticmethod
185 def create_dummy_user(**kwargs):
186 """Create a dummy user instance.
187
188 The dummy instance is created and saved to the database.
189 Args:
190 **kwargs:
191 Optional arguments to extend/overwrite the default values.
192
193 Returns: The created user instance.
194
195 """
196 entity = User(
197 **Dummy._update_copy(Dummy.DEFAULT_DUMMY_USER_VALUES, kwargs)
198 )
199 entity.save()
200 return entity
201
202 @staticmethod
203 def create_dummy_device(user, **kwargs):
204 """Create a dummy device instance.
205
206 The dummy instance is created and saved to the database.
207 Args:
208 user: The user instance that the device should relate to
209 **kwargs:
210 Optional arguments to extend/overwrite the default values.
211
212 Returns: The created device instance.
213
214 """
215 entity = Device(
216 user=user,
217 **Dummy._update_copy(Dummy.DEFAULT_DUMMY_DEVICE_VALUES, kwargs)
218 )
219 entity.save()
220 return entity
221
222 @staticmethod
223 def create_dummy_report(report_type, device, **kwargs):
224 """Create a dummy report instance of the given report class type.
225
226 The dummy instance is created and saved to the database.
227 Args:
228 report_type: The class of the report type to be created.
229 user: The device instance that the heartbeat should relate to
230 **kwargs:
231 Optional arguments to extend/overwrite the default values.
232
233 Returns: The created report instance.
234
235 Raises:
236 RuntimeError: If report_type is not a report class type.
237
238 """
239 if report_type == HeartBeat:
240 entity = HeartBeat(
241 device=device,
242 **Dummy._update_copy(
243 Dummy.DEFAULT_DUMMY_HEARTBEAT_VALUES, kwargs
244 )
245 )
246 elif report_type == Crashreport:
247 entity = Crashreport(
248 device=device,
249 **Dummy._update_copy(
250 Dummy.DEFAULT_DUMMY_CRASHREPORT_VALUES, kwargs
251 )
252 )
253 else:
254 raise RuntimeError(
255 "No dummy report instance can be created for {}".format(
256 report_type.__name__
257 )
258 )
259 entity.save()
260 return entity
261
262 @staticmethod
263 def create_dummy_log_file(crashreport, **kwargs):
264 """Create a dummy log file instance.
265
266 The dummy instance is created and saved to the database.
267
268 Args:
269 crashreport: The crashreport that the log file belongs to.
270 **kwargs: Optional arguments to extend/overwrite the default values.
271
272 Returns: The created log file instance.
273
274 """
275 entity = LogFile(
276 crashreport=crashreport,
277 **Dummy._update_copy(Dummy.DEFAULT_DUMMY_LOG_FILE_VALUES, kwargs)
278 )
279
280 entity.save()
281 return entity
282
283 @staticmethod
Mitja Nikolauscc90d572018-11-22 16:40:15 +0100284 def create_dummy_log_file_with_actual_file(crashreport, **kwargs):
285 """Create a dummy log file instance along with a file.
286
287 The dummy instance is created and saved to the database. The log file
288 is copied to the respective location in the media directory.
289
290 Args:
291 crashreport: The crashreport that the log file belongs to.
292 **kwargs: Optional arguments to extend/overwrite the default values.
293
294 Returns: The created log file instance and the path to the copied file.
295
296 """
297 logfile = Dummy.create_dummy_log_file(crashreport, **kwargs)
298 logfile_filename = os.path.basename(logfile.logfile.path)
299 test_logfile_path = os.path.join(
300 settings.MEDIA_ROOT,
301 crashreport_file_name(logfile, logfile_filename),
302 )
303 logfile.logfile = test_logfile_path
304 logfile.save()
305
306 os.makedirs(os.path.dirname(test_logfile_path))
307 shutil.copy(
308 os.path.join(
309 Dummy.DEFAULT_DUMMY_LOG_FILE_DIRECTORY, logfile_filename
310 ),
311 test_logfile_path,
312 )
313 return logfile, test_logfile_path
314
315 @staticmethod
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100316 def read_logfile_contents(path_to_zipfile, logfile_name):
317 """Read bytes of a zipped logfile."""
318 archive = zipfile.ZipFile(path_to_zipfile, "r")
319 return archive.read(logfile_name)
320
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200321
322class HiccupCrashreportsAPITestCase(APITestCase):
323 """Base class that offers a device registration method."""
324
325 REGISTER_DEVICE_URL = "api_v1_register_device"
326
327 def setUp(self):
Mitja Nikolause0e83772018-11-05 10:00:53 +0100328 """Create a Fairphone staff user for accessing the API.
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200329
330 The APIClient that can be used to make authenticated requests to the
Mitja Nikolause0e83772018-11-05 10:00:53 +0100331 server is stored in self.fp_staff_client.
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200332 """
Mitja Nikolause0e83772018-11-05 10:00:53 +0100333 fp_staff_group = Group.objects.get(name=FP_STAFF_GROUP_NAME)
334 fp_staff_user = User.objects.create_user(
335 "fp_staff", "somebody@fairphone.com", "thepassword"
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200336 )
Mitja Nikolause0e83772018-11-05 10:00:53 +0100337 fp_staff_user.groups.add(fp_staff_group)
338 self.fp_staff_client = APIClient()
339 self.fp_staff_client.force_login(fp_staff_user)
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200340
341 def _register_device(self, **kwargs):
342 """Register a new device.
343
344 Arguments:
345 **kwargs: The data to pass the dummy data creation
346 method `Dummy.device_register_data`.
347 Returns:
348 (UUID, APIClient, str): The uuid of the new device as well as an
349 authentication token and the associated user with credentials.
350
351 """
352 data = Dummy.device_register_data(**kwargs)
353 response = self.client.post(reverse(self.REGISTER_DEVICE_URL), data)
354 self.assertEqual(response.status_code, status.HTTP_200_OK)
355
356 uuid = response.data["uuid"]
357 token = response.data["token"]
358 user = APIClient()
359 user.credentials(HTTP_AUTHORIZATION="Token " + token)
360
361 return uuid, user, token