blob: f69ff0cb20aa03910968967aad02cb45a62c0980 [file] [log] [blame]
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +02001"""Test crashreport_stats models and the 'stats' command."""
Mitja Nikolaus52e44b82018-09-04 14:23:19 +02002
3# pylint: disable=too-many-lines,too-many-public-methods
4
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04005from io import StringIO
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +04006from datetime import datetime, date, timedelta
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02007import operator
8import os
Mitja Nikolaus3a09c6e2018-09-04 12:17:45 +02009import unittest
Mitja Nikolaus98e3a142018-09-18 16:39:46 +020010from urllib.parse import urlencode
Mitja Nikolaus78e3a052018-09-05 12:18:35 +020011import zipfile
Mitja Nikolaus3a09c6e2018-09-04 12:17:45 +020012
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020013import pytz
Dirk Vogt62ff7f22017-05-04 16:07:21 +020014
Mitja Nikolaus98e3a142018-09-18 16:39:46 +020015from django.conf import settings
Mitja Nikolaus78e3a052018-09-05 12:18:35 +020016from django.contrib.auth.models import Group
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +040017from django.core.management import call_command
18from django.test import TestCase
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020019from django.urls import reverse
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020020
21from rest_framework import status
Mitja Nikolaus98e3a142018-09-18 16:39:46 +020022from rest_framework.authtoken.models import Token
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020023from rest_framework.test import APITestCase, APIClient
24
25from crashreport_stats.models import (
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020026 Version,
27 VersionDaily,
28 RadioVersion,
29 RadioVersionDaily,
30 StatsMetadata,
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020031)
32
Mitja Nikolaus78e3a052018-09-05 12:18:35 +020033from crashreports.models import Crashreport, Device, HeartBeat, LogFile, User
34from hiccup.allauth_adapters import FP_STAFF_GROUP_NAME
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020035
36
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020037class Dummy:
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020038 """Class for creating dummy instances for testing."""
39
40 # Valid unique entries
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020041 BUILD_FINGERPRINTS = [
42 (
Mitja Nikolaus19cf9a92018-08-23 18:15:01 +020043 "Fairphone/FP2/FP2:5.1/FP2/r4275.1_FP2_gms76_1.13.0"
44 ":user/release-keys"
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020045 ),
46 (
47 "Fairphone/FP2/FP2:5.1.1/FP2-gms75.1.13.0/FP2-gms75.1.13.0"
48 ":user/release-keys"
49 ),
50 (
51 "Fairphone/FP2/FP2:6.0.1/FP2-gms-18.04.1/FP2-gms-18.04.1"
52 ":user/release-keys"
53 ),
Mitja Nikolaus19cf9a92018-08-23 18:15:01 +020054 ("Fairphone/FP2/FP2:7.1.2/18.07.2/gms-7480c31d:user/release-keys"),
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020055 ]
56 RADIO_VERSIONS = [
57 "4437.1-FP2-0-07",
58 "4437.1-FP2-0-08",
59 "4437.1-FP2-0-09",
60 "4437.1-FP2-0-10",
61 ]
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020062
Mitja Nikolaus3a09c6e2018-09-04 12:17:45 +020063 USERNAMES = ["testuser1", "testuser2"]
64
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020065 DATES = [date(2018, 3, 19), date(2018, 3, 26), date(2018, 5, 1)]
66
67 DEFAULT_DUMMY_VERSION_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020068 "build_fingerprint": BUILD_FINGERPRINTS[0],
69 "first_seen_on": DATES[1],
70 "released_on": DATES[0],
Mitja Nikolausded30ae2018-09-14 15:40:08 +020071 "is_beta_release": False,
72 "is_official_release": True,
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020073 }
74
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020075 DEFAULT_DUMMY_VERSION_DAILY_VALUES = {"date": DATES[1]}
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020076
77 DEFAULT_DUMMY_RADIO_VERSION_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020078 "radio_version": RADIO_VERSIONS[0],
79 "first_seen_on": DATES[1],
80 "released_on": DATES[0],
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020081 }
82
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020083 DEFAULT_DUMMY_RADIO_VERSION_DAILY_VALUES = {"date": DATES[1]}
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020084
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +020085 DEFAULT_DUMMY_STATSMETADATA_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020086 "updated_at": datetime(2018, 6, 15, 2, 12, 24, tzinfo=pytz.utc)
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +020087 }
88
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020089 DEFAULT_DUMMY_DEVICE_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020090 "board_date": datetime(2015, 12, 15, 1, 23, 45, tzinfo=pytz.utc),
91 "chipset": "Qualcomm MSM8974PRO-AA",
92 "token": "64111c62d521fb4724454ca6dea27e18f93ef56e",
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020093 }
94
Mitja Nikolaus3a09c6e2018-09-04 12:17:45 +020095 DEFAULT_DUMMY_USER_VALUES = {"username": USERNAMES[0]}
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020096
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +040097 DEFAULT_DUMMY_HEARTBEAT_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020098 "app_version": 10100,
99 "uptime": (
100 "up time: 16 days, 21:49:56, idle time: 5 days, 20:55:04, "
101 "sleep time: 10 days, 20:46:27"
102 ),
103 "build_fingerprint": BUILD_FINGERPRINTS[0],
104 "radio_version": RADIO_VERSIONS[0],
Mitja Nikolaus78e3a052018-09-05 12:18:35 +0200105 "date": datetime(2018, 3, 19, 12, 0, 0, tzinfo=pytz.utc),
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400106 }
107
108 DEFAULT_DUMMY_CRASHREPORT_VALUES = DEFAULT_DUMMY_HEARTBEAT_VALUES.copy()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200109 DEFAULT_DUMMY_CRASHREPORT_VALUES.update(
110 {
111 "is_fake_report": 0,
112 "boot_reason": Crashreport.BOOT_REASON_UNKOWN,
113 "power_on_reason": "it was powered on",
114 "power_off_reason": "something happened and it went off",
115 }
116 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400117
Mitja Nikolaus78e3a052018-09-05 12:18:35 +0200118 DEFAULT_DUMMY_LOG_FILE_VALUES = {
119 "logfile_type": "last_kmsg",
120 "logfile": os.path.join("resources", "test", "test_logfile.zip"),
121 }
122
123 DEFAULT_DUMMY_LOG_FILE_NAME = "dmesg.log"
124
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200125 @staticmethod
126 def update_copy(original, update):
127 """Merge fields of update into a copy of original."""
128 data = original.copy()
129 data.update(update)
130 return data
131
132 @staticmethod
133 def create_dummy_user(**kwargs):
134 """Create a dummy user instance.
135
136 The dummy instance is created and saved to the database.
137 Args:
138 **kwargs:
139 Optional arguments to extend/overwrite the default values.
140
141 Returns: The created user instance.
142
143 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200144 entity = User(
145 **Dummy.update_copy(Dummy.DEFAULT_DUMMY_USER_VALUES, kwargs)
146 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200147 entity.save()
148 return entity
149
150 @staticmethod
151 def create_dummy_device(user, **kwargs):
152 """Create a dummy device instance.
153
154 The dummy instance is created and saved to the database.
155 Args:
156 user: The user instance that the device should relate to
157 **kwargs:
158 Optional arguments to extend/overwrite the default values.
159
160 Returns: The created device instance.
161
162 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200163 entity = Device(
164 user=user,
165 **Dummy.update_copy(Dummy.DEFAULT_DUMMY_DEVICE_VALUES, kwargs)
166 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200167 entity.save()
168 return entity
169
170 @staticmethod
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400171 def create_dummy_report(report_type, device, **kwargs):
172 """Create a dummy report instance of the given report class type.
173
174 The dummy instance is created and saved to the database.
175 Args:
176 report_type: The class of the report type to be created.
177 user: The device instance that the heartbeat should relate to
178 **kwargs:
179 Optional arguments to extend/overwrite the default values.
180
181 Returns: The created report instance.
182
183 """
184 if report_type == HeartBeat:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200185 entity = HeartBeat(
186 device=device,
187 **Dummy.update_copy(
188 Dummy.DEFAULT_DUMMY_HEARTBEAT_VALUES, kwargs
189 )
190 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400191 elif report_type == Crashreport:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200192 entity = Crashreport(
193 device=device,
194 **Dummy.update_copy(
195 Dummy.DEFAULT_DUMMY_CRASHREPORT_VALUES, kwargs
196 )
197 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400198 else:
199 raise RuntimeError(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200200 "No dummy report instance can be created for {}".format(
201 report_type.__name__
202 )
203 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400204 entity.save()
205 return entity
206
207 @staticmethod
Mitja Nikolaus78e3a052018-09-05 12:18:35 +0200208 def create_dummy_log_file(crashreport, **kwargs):
209 """Create a dummy log file instance.
210
211 The dummy instance is created and saved to the database.
212
213 Args:
214 crashreport: The crashreport that the log file belongs to.
215 **kwargs: Optional arguments to extend/overwrite the default values.
216
217 Returns: The created log file instance.
218
219 """
220 entity = LogFile(
221 crashreport=crashreport,
222 **Dummy.update_copy(Dummy.DEFAULT_DUMMY_LOG_FILE_VALUES, kwargs)
223 )
224
225 entity.save()
226 return entity
227
228 @staticmethod
229 def read_logfile_contents(path_to_zipfile, logfile_name):
230 """Read bytes of a zipped logfile."""
231 archive = zipfile.ZipFile(path_to_zipfile, "r")
232 return archive.read(logfile_name)
233
234 @staticmethod
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200235 def create_dummy_version(**kwargs):
236 """Create a dummy version instance.
237
238 The dummy instance is created and saved to the database.
239 Args:
240 **kwargs:
241 Optional arguments to extend/overwrite the default values.
242
243 Returns: The created version instance.
244
245 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200246 entity = Version(
247 **Dummy.update_copy(Dummy.DEFAULT_DUMMY_VERSION_VALUES, kwargs)
248 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200249 entity.save()
250 return entity
251
252 @staticmethod
253 def create_dummy_radio_version(**kwargs):
254 """Create a dummy radio version instance.
255
256 The dummy instance is created and saved to the database.
257 Args:
258 **kwargs:
259 Optional arguments to extend/overwrite the default values.
260
261 Returns: The created radio version instance.
262
263 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200264 entity = RadioVersion(
265 **Dummy.update_copy(
266 Dummy.DEFAULT_DUMMY_RADIO_VERSION_VALUES, kwargs
267 )
268 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200269 entity.save()
270 return entity
271
272 @staticmethod
273 def create_dummy_daily_version(version, **kwargs):
274 """Create a dummy daily version instance.
275
276 The dummy instance is created and saved to the database.
277 Args:
278 **kwargs:
279 Optional arguments to extend/overwrite the default values.
280
281 Returns: The created daily version instance.
282
283 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200284 entity = VersionDaily(
285 version=version,
286 **Dummy.update_copy(
287 Dummy.DEFAULT_DUMMY_VERSION_DAILY_VALUES, kwargs
288 )
289 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200290 entity.save()
291 return entity
292
293 @staticmethod
294 def create_dummy_daily_radio_version(version, **kwargs):
295 """Create a dummy daily radio version instance.
296
297 The dummy instance is created and saved to the database.
298 Args:
299 **kwargs:
300 Optional arguments to extend/overwrite the default values.
301
302 Returns: The created daily radio version instance.
303
304 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200305 entity = RadioVersionDaily(
306 version=version,
307 **Dummy.update_copy(
308 Dummy.DEFAULT_DUMMY_RADIO_VERSION_DAILY_VALUES, kwargs
309 )
310 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200311 entity.save()
312 return entity
313
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +0200314 @staticmethod
315 def create_dummy_stats_metadata(**kwargs):
316 """Create a dummy stats metadata instance.
317
318 The dummy instance is created and saved to the database.
319 Args:
320 **kwargs:
321 Optional arguments to extend/overwrite the default values.
322
323 Returns: The created stats metadata instance.
324
325 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200326 entity = StatsMetadata(
327 **Dummy.update_copy(
328 Dummy.DEFAULT_DUMMY_STATSMETADATA_VALUES, kwargs
329 )
330 )
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +0200331 entity.save()
332 return entity
333
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200334
Mitja Nikolaus98e3a142018-09-18 16:39:46 +0200335class _HiccupStatsAPITestCase(APITestCase):
336 """Abstract class for Hiccup stats REST API test cases to inherit from."""
Mitja Nikolaus78e3a052018-09-05 12:18:35 +0200337
338 @classmethod
339 def setUpTestData(cls): # noqa: N802
Mitja Nikolaus98e3a142018-09-18 16:39:46 +0200340 """Create an admin and two client users for accessing the API.
Mitja Nikolaus78e3a052018-09-05 12:18:35 +0200341
342 The APIClient that can be used to make authenticated requests as
Mitja Nikolaus98e3a142018-09-18 16:39:46 +0200343 admin user is stored in self.admin. A client which is related to a
344 user that is part of the Fairphone staff group is stored in
345 self.fp_staff_client. A client which is related to a device owner
346 user is stored in self.device_owner_client.
Mitja Nikolaus78e3a052018-09-05 12:18:35 +0200347 """
348 admin_user = User.objects.create_superuser(
349 "somebody", "somebody@example.com", "thepassword"
350 )
351 cls.admin = APIClient()
352 cls.admin.force_authenticate(admin_user)
353
Mitja Nikolaus98e3a142018-09-18 16:39:46 +0200354 fp_staff_group = Group(name=FP_STAFF_GROUP_NAME)
355 fp_staff_group.save()
356 fp_staff_user = User.objects.create_user(
Mitja Nikolaus78e3a052018-09-05 12:18:35 +0200357 "fp_staff", "somebody@fairphone.com", "thepassword"
358 )
Mitja Nikolaus98e3a142018-09-18 16:39:46 +0200359 fp_staff_user.groups.add(fp_staff_group)
Mitja Nikolaus78e3a052018-09-05 12:18:35 +0200360 cls.fp_staff_client = APIClient()
Mitja Nikolaus98e3a142018-09-18 16:39:46 +0200361 cls.fp_staff_client.force_login(fp_staff_user)
362
363 cls.device_owner_user = User.objects.create_user(
364 "device_owner", "somebody@somemail.com", "thepassword"
365 )
366 Token.objects.create(user=cls.device_owner_user)
367 cls.device_owner_device = Dummy.create_dummy_device(
368 user=cls.device_owner_user
369 )
370 cls.device_owner_client = APIClient()
371 cls.device_owner_client.credentials(
372 HTTP_AUTHORIZATION="Token " + cls.device_owner_user.auth_token.key
373 )
374
375 def _assert_get_as_admin_user_succeeds(
376 self, url, expected_status=status.HTTP_200_OK
377 ):
378 response = self.admin.get(url)
379 self.assertEqual(response.status_code, expected_status)
380
381 def _assert_get_as_fp_staff_succeeds(
382 self, url, expected_status=status.HTTP_200_OK
383 ):
384 response = self.fp_staff_client.get(url)
385 self.assertEqual(response.status_code, expected_status)
386
387 def _assert_get_without_authentication_fails(
388 self, url, expected_status=status.HTTP_401_UNAUTHORIZED
389 ):
390 response = self.client.get(url)
391 self.assertEqual(response.status_code, expected_status)
392
393 def _assert_get_as_device_owner_fails(
394 self, url, expected_status=status.HTTP_403_FORBIDDEN
395 ):
396 response = self.device_owner_client.get(url)
397 self.assertEqual(response.status_code, expected_status)
Mitja Nikolaus78e3a052018-09-05 12:18:35 +0200398
399
Mitja Nikolaus98e3a142018-09-18 16:39:46 +0200400class StatusTestCase(_HiccupStatsAPITestCase):
Mitja Nikolaus4d19e182018-09-05 13:45:36 +0200401 """Test the status endpoint."""
402
403 status_url = reverse("hiccup_stats_api_v1_status")
404
405 def _assert_status_response_is(
406 self, response, num_devices, num_crashreports, num_heartbeats
407 ):
408 self.assertEqual(response.status_code, status.HTTP_200_OK)
409 self.assertIn("devices", response.data)
410 self.assertIn("crashreports", response.data)
411 self.assertIn("heartbeats", response.data)
412 self.assertEqual(response.data["devices"], num_devices)
413 self.assertEqual(response.data["crashreports"], num_crashreports)
414 self.assertEqual(response.data["heartbeats"], num_heartbeats)
415
Mitja Nikolaus98e3a142018-09-18 16:39:46 +0200416 def test_status_url_as_admin(self):
417 """Test that admin users can access the status URL."""
418 self._assert_get_as_admin_user_succeeds(self.status_url)
419
420 def test_status_url_as_fp_staff(self):
421 """Test that Fairphone staff users can access the status URL."""
422 self._assert_get_as_fp_staff_succeeds(self.status_url)
423
424 def test_status_url_as_device_owner(self):
425 """Test that device owner users can not access the status URL."""
426 self._assert_get_as_device_owner_fails(self.status_url)
427
428 def test_status_url_no_auth(self):
429 """Test that non-authenticated users can not access the status URL."""
430 self._assert_get_without_authentication_fails(self.status_url)
431
Mitja Nikolaus4d19e182018-09-05 13:45:36 +0200432 def test_get_status_empty_database(self):
433 """Get the status when the database is empty."""
434 response = self.fp_staff_client.get(self.status_url)
Mitja Nikolaus98e3a142018-09-18 16:39:46 +0200435
436 # Assert that only the device that was created by the setUpTestData()
437 # method is found.
438 self._assert_status_response_is(response, 1, 0, 0)
Mitja Nikolaus4d19e182018-09-05 13:45:36 +0200439
440 def test_get_status(self):
441 """Get the status after some reports have been created."""
442 # Create a device with a heartbeat and a crash report
443 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
444 Dummy.create_dummy_report(HeartBeat, device)
445 Dummy.create_dummy_report(Crashreport, device)
446
447 # Create a second device without any reports
448 Dummy.create_dummy_device(
449 Dummy.create_dummy_user(username=Dummy.USERNAMES[1])
450 )
451
Mitja Nikolaus98e3a142018-09-18 16:39:46 +0200452 # Assert that the status includes the appropriate numbers (a third
453 # device was created by the setUpTestData() method)
Mitja Nikolaus4d19e182018-09-05 13:45:36 +0200454 response = self.fp_staff_client.get(self.status_url)
455 self._assert_status_response_is(
Mitja Nikolaus98e3a142018-09-18 16:39:46 +0200456 response, num_devices=3, num_crashreports=1, num_heartbeats=1
Mitja Nikolaus4d19e182018-09-05 13:45:36 +0200457 )
458
459
Mitja Nikolaus98e3a142018-09-18 16:39:46 +0200460class _VersionTestCase(_HiccupStatsAPITestCase):
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200461 """Abstract class for version-related test cases to inherit from."""
462
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200463 @staticmethod
464 def _create_dummy_version(**kwargs):
465 return Dummy.create_dummy_version(**kwargs)
466
467 def _get_with_params(self, url, params):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200468 return self.admin.get("{}?{}".format(url, urlencode(params)))
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200469
470 def _assert_result_length_is(self, response, count):
471 self.assertEqual(response.status_code, status.HTTP_200_OK)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200472 self.assertIn("results", response.data)
473 self.assertIn("count", response.data)
474 self.assertEqual(response.data["count"], count)
475 self.assertEqual(len(response.data["results"]), count)
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200476
Mitja Nikolaus98e3a142018-09-18 16:39:46 +0200477 def _assert_filter_result_matches(
478 self, endpoint_url, unique_entry_name, filter_params, expected_result
479 ):
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200480 # List entities with filter
Mitja Nikolaus98e3a142018-09-18 16:39:46 +0200481 response = self._get_with_params(endpoint_url, filter_params)
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200482
483 # Expect only the single matching result to be returned
484 self._assert_result_length_is(response, 1)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200485 self.assertEqual(
Mitja Nikolaus98e3a142018-09-18 16:39:46 +0200486 response.data["results"][0][unique_entry_name],
487 getattr(expected_result, unique_entry_name),
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200488 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200489
490
491class VersionTestCase(_VersionTestCase):
492 """Test the Version and REST endpoint."""
493
Mitja Nikolaus78e3a052018-09-05 12:18:35 +0200494 # pylint: disable=too-many-ancestors
495
Mitja Nikolaus98e3a142018-09-18 16:39:46 +0200496 # The attribute name characterising the unicity of a stats entry (the
497 # named identifier)
498 unique_entry_name = "build_fingerprint"
499 # The collection of unique entries to post
500 unique_entries = Dummy.BUILD_FINGERPRINTS
501 # The URL to retrieve the stats entries from
502 endpoint_url = reverse("hiccup_stats_api_v1_versions")
503
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200504 def _create_version_entities(self):
505 versions = [
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200506 self._create_dummy_version(**{self.unique_entry_name: unique_entry})
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200507 for unique_entry in self.unique_entries
508 ]
509 return versions
510
Mitja Nikolaus98e3a142018-09-18 16:39:46 +0200511 def test_endpoint_url_as_admin(self):
512 """Test that admin users can access the endpoint URL."""
513 self._assert_get_as_admin_user_succeeds(self.endpoint_url)
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200514
Mitja Nikolaus98e3a142018-09-18 16:39:46 +0200515 def test_endpoint_url_as_fp_staff(self):
516 """Test that Fairphone staff users can access the endpoint URL."""
517 self._assert_get_as_fp_staff_succeeds(self.endpoint_url)
518
519 def test_endpoint_url_as_device_owner(self):
520 """Test that device owner users can not access the endpoint URL."""
521 self._assert_get_as_device_owner_fails(self.endpoint_url)
522
523 def test_endpoint_url_no_auth(self):
524 """Test that non-authenticated users can not access the endpoint URL."""
525 self._assert_get_without_authentication_fails(self.endpoint_url)
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200526
527 def test_list_versions_empty_database(self):
528 """Test listing of versions on an empty database."""
529 response = self.admin.get(self.endpoint_url)
530 self._assert_result_length_is(response, 0)
531
532 def test_list_versions(self):
533 """Test listing versions."""
534 versions = self._create_version_entities()
535 response = self.admin.get(self.endpoint_url)
536 self._assert_result_length_is(response, len(versions))
537
538 def test_filter_versions_by_unique_entry_name(self):
539 """Test filtering versions by their unique entry name."""
540 versions = self._create_version_entities()
541 response = self.admin.get(self.endpoint_url)
542
543 # Listing all entities should return the correct result length
544 self._assert_result_length_is(response, len(versions))
545
546 # List entities with filter
547 filter_params = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200548 self.unique_entry_name: getattr(versions[0], self.unique_entry_name)
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200549 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200550 self._assert_filter_result_matches(
Mitja Nikolaus98e3a142018-09-18 16:39:46 +0200551 self.endpoint_url,
552 self.unique_entry_name,
553 filter_params,
554 expected_result=versions[0],
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200555 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200556
557 def test_filter_versions_by_release_type(self):
558 """Test filtering versions by release type."""
559 # Create versions for all combinations of release types
560 versions = []
561 i = 0
562 for is_official_release in True, False:
563 for is_beta_release in True, False:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200564 versions.append(
565 self._create_dummy_version(
566 **{
567 "is_official_release": is_official_release,
568 "is_beta_release": is_beta_release,
569 self.unique_entry_name: self.unique_entries[i],
570 }
571 )
572 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200573 i += 1
574
575 # # Listing all entities should return the correct result length
576 response = self.admin.get(self.endpoint_url)
577 self._assert_result_length_is(response, len(versions))
578
579 # List each of the entities with the matching filter params
580 for version in versions:
581 filter_params = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200582 "is_official_release": version.is_official_release,
583 "is_beta_release": version.is_beta_release,
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200584 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200585 self._assert_filter_result_matches(
Mitja Nikolaus98e3a142018-09-18 16:39:46 +0200586 self.endpoint_url,
587 self.unique_entry_name,
588 filter_params,
589 expected_result=version,
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200590 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200591
592 def test_filter_versions_by_first_seen_date(self):
593 """Test filtering versions by first seen date."""
594 versions = self._create_version_entities()
595
596 # Set the first seen date of an entity
597 versions[0].first_seen_on = Dummy.DATES[2]
598 versions[0].save()
599
600 # Listing all entities should return the correct result length
601 response = self.admin.get(self.endpoint_url)
602 self._assert_result_length_is(response, len(versions))
603
604 # Expect the single matching result to be returned
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200605 filter_params = {"first_seen_after": Dummy.DATES[2]}
606 self._assert_filter_result_matches(
Mitja Nikolaus98e3a142018-09-18 16:39:46 +0200607 self.endpoint_url,
608 self.unique_entry_name,
609 filter_params,
610 expected_result=versions[0],
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200611 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200612
613
614# pylint: disable=too-many-ancestors
615class RadioVersionTestCase(VersionTestCase):
616 """Test the RadioVersion REST endpoint."""
617
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200618 unique_entry_name = "radio_version"
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200619 unique_entries = Dummy.RADIO_VERSIONS
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200620 endpoint_url = reverse("hiccup_stats_api_v1_radio_versions")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200621
622 @staticmethod
623 def _create_dummy_version(**kwargs):
624 return Dummy.create_dummy_radio_version(**kwargs)
625
626
627class VersionDailyTestCase(_VersionTestCase):
628 """Test the VersionDaily REST endpoint."""
629
Mitja Nikolaus98e3a142018-09-18 16:39:46 +0200630 unique_entry_name = "build_fingerprint"
631 unique_entries = Dummy.BUILD_FINGERPRINTS
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200632 endpoint_url = reverse("hiccup_stats_api_v1_version_daily")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200633
634 @staticmethod
635 def _create_dummy_daily_version(version, **kwargs):
636 return Dummy.create_dummy_daily_version(version, **kwargs)
637
638 def _create_version_entities(self):
639 versions = [
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200640 self._create_dummy_version(**{self.unique_entry_name: unique_entry})
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200641 for unique_entry in self.unique_entries
642 ]
643 versions_daily = [
644 self._create_dummy_daily_version(version=version)
645 for version in versions
646 ]
647 return versions_daily
648
Mitja Nikolaus98e3a142018-09-18 16:39:46 +0200649 def test_endpoint_url_as_admin(self):
650 """Test that admin users can access the endpoint URL."""
651 self._assert_get_as_admin_user_succeeds(self.endpoint_url)
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200652
Mitja Nikolaus98e3a142018-09-18 16:39:46 +0200653 def test_endpoint_url_as_fp_staff(self):
654 """Test that Fairphone staff users can access the endpoint URL."""
655 self._assert_get_as_fp_staff_succeeds(self.endpoint_url)
656
657 def test_endpoint_url_as_device_owner(self):
658 """Test that device owner users can not access the endpoint URL."""
659 self._assert_get_as_device_owner_fails(self.endpoint_url)
660
661 def test_endpoint_url_no_auth(self):
662 """Test that non-authenticated users can not access the endpoint URL."""
663 self._assert_get_without_authentication_fails(self.endpoint_url)
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200664
665 def test_list_daily_versions_empty_database(self):
666 """Test listing of daily versions on an empty database."""
667 response = self.admin.get(self.endpoint_url)
668 self._assert_result_length_is(response, 0)
669
670 def test_list_daily_versions(self):
671 """Test listing daily versions."""
672 versions_daily = self._create_version_entities()
673 response = self.admin.get(self.endpoint_url)
674 self._assert_result_length_is(response, len(versions_daily))
675
676 def test_filter_daily_versions_by_version(self):
677 """Test filtering versions by the version they relate to."""
678 # Create VersionDaily entities
679 versions = self._create_version_entities()
680
681 # Listing all entities should return the correct result length
682 response = self.admin.get(self.endpoint_url)
683 self._assert_result_length_is(response, len(versions))
684
685 # List entities with filter
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200686 param_name = "version__" + self.unique_entry_name
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200687 filter_params = {
688 param_name: getattr(versions[0].version, self.unique_entry_name)
689 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200690 self._assert_filter_result_matches(
Mitja Nikolaus98e3a142018-09-18 16:39:46 +0200691 self.endpoint_url,
692 self.unique_entry_name,
693 filter_params,
694 expected_result=versions[0].version,
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200695 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200696
697 def test_filter_daily_versions_by_date(self):
698 """Test filtering daily versions by date."""
699 # Create Version and VersionDaily entities
700 versions = self._create_version_entities()
701
702 # Update the date
703 versions[0].date = Dummy.DATES[2]
704 versions[0].save()
705
706 # Listing all entities should return the correct result length
707 response = self.admin.get(self.endpoint_url)
708 self._assert_result_length_is(response, len(versions))
709
710 # Expect the single matching result to be returned
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200711 filter_params = {"date": versions[0].date}
712 self._assert_filter_result_matches(
Mitja Nikolaus98e3a142018-09-18 16:39:46 +0200713 self.endpoint_url,
714 self.unique_entry_name,
715 filter_params,
716 expected_result=versions[0].version,
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200717 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200718
719
720class RadioVersionDailyTestCase(VersionDailyTestCase):
721 """Test the RadioVersionDaily REST endpoint."""
722
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200723 unique_entry_name = "radio_version"
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200724 unique_entries = Dummy.RADIO_VERSIONS
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200725 endpoint_url = reverse("hiccup_stats_api_v1_radio_version_daily")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200726
727 @staticmethod
728 def _create_dummy_version(**kwargs):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200729 entity = RadioVersion(
730 **Dummy.update_copy(
731 Dummy.DEFAULT_DUMMY_RADIO_VERSION_VALUES, kwargs
732 )
733 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200734 entity.save()
735 return entity
736
737 @staticmethod
738 def _create_dummy_daily_version(version, **kwargs):
739 return Dummy.create_dummy_daily_radio_version(version, **kwargs)
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400740
741
742class StatsCommandVersionsTestCase(TestCase):
743 """Test the generation of Version stats with the stats command."""
744
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400745 # The class of the version type to be tested
746 version_class = Version
747 # The attribute name characterising the unicity of a stats entry (the
748 # named identifier)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200749 unique_entry_name = "build_fingerprint"
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400750 # The collection of unique entries to post
751 unique_entries = Dummy.BUILD_FINGERPRINTS
752
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200753 def _create_reports(
754 self, report_type, unique_entry_name, device, number, **kwargs
755 ):
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400756 # Create reports with distinct timestamps
757 now = datetime.now(pytz.utc)
758 for i in range(number):
759 report_date = now - timedelta(milliseconds=i)
760 report_attributes = {
761 self.unique_entry_name: unique_entry_name,
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200762 "device": device,
763 "date": report_date,
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400764 }
765 report_attributes.update(**kwargs)
766 Dummy.create_dummy_report(report_type, **report_attributes)
767
768 def test_stats_calculation(self):
769 """Test generation of a Version instance."""
770 user = Dummy.create_dummy_user()
771 device = Dummy.create_dummy_device(user=user)
772 heartbeat = Dummy.create_dummy_report(HeartBeat, device=device)
773
774 # Expect that we do not have the Version before updating the stats
775 get_params = {
776 self.unique_entry_name: getattr(heartbeat, self.unique_entry_name)
777 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200778 self.assertRaises(
779 self.version_class.DoesNotExist,
780 self.version_class.objects.get,
781 **get_params
782 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400783
784 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200785 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400786
787 # Assume that a corresponding Version instance has been created
788 version = self.version_class.objects.get(**get_params)
789 self.assertIsNotNone(version)
790
791 def _assert_older_report_updates_version_date(self, report_type):
792 """Validate that older reports sent later affect the version date."""
793 user = Dummy.create_dummy_user()
794 device = Dummy.create_dummy_device(user=user)
795 report = Dummy.create_dummy_report(report_type, device=device)
796
797 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200798 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400799
800 get_params = {
801 self.unique_entry_name: getattr(report, self.unique_entry_name)
802 }
803 version = self.version_class.objects.get(**get_params)
804
805 self.assertEqual(report.date.date(), version.first_seen_on)
806
807 # Create a new report from an earlier point in time
808 report_time_2 = report.date - timedelta(weeks=1)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200809 Dummy.create_dummy_report(
810 report_type, device=device, date=report_time_2
811 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400812
813 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200814 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400815
816 # Get the same version object from before
817 version = self.version_class.objects.get(**get_params)
818
819 # Validate that the date matches the report recently sent
820 self.assertEqual(report_time_2.date(), version.first_seen_on)
821
822 def test_older_heartbeat_updates_version_date(self):
823 """Validate updating version date with older heartbeats."""
824 self._assert_older_report_updates_version_date(HeartBeat)
825
826 def test_older_crash_report_updates_version_date(self):
827 """Validate updating version date with older crash reports."""
828 self._assert_older_report_updates_version_date(Crashreport)
829
830 def test_entries_are_unique(self):
831 """Validate the entries' unicity and value."""
832 # Create some reports
833 user = Dummy.create_dummy_user()
834 device = Dummy.create_dummy_device(user=user)
835 for unique_entry in self.unique_entries:
836 self._create_reports(HeartBeat, unique_entry, device, 10)
837
838 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200839 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400840
841 # Check whether the correct amount of distinct versions have been
842 # created
843 versions = self.version_class.objects.all()
844 for version in versions:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200845 self.assertIn(
846 getattr(version, self.unique_entry_name), self.unique_entries
847 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400848 self.assertEqual(len(versions), len(self.unique_entries))
849
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200850 def _assert_counter_distribution_is_correct(
851 self, report_type, numbers, counter_attribute_name, **kwargs
852 ):
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400853 """Validate a counter distribution in the database."""
854 if len(numbers) != len(self.unique_entries):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200855 raise ValueError(
856 "The length of the numbers list must match the "
857 "length of self.unique_entries in the test class"
858 "({} != {})".format(len(numbers), len(self.unique_entries))
859 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400860 # Create some reports
861 user = Dummy.create_dummy_user()
862 device = Dummy.create_dummy_device(user=user)
863 for unique_entry, num in zip(self.unique_entries, numbers):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200864 self._create_reports(
865 report_type, unique_entry, device, num, **kwargs
866 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400867
868 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200869 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400870
871 # Check whether the numbers of reports match
872 for version in self.version_class.objects.all():
873 unique_entry_name = getattr(version, self.unique_entry_name)
874 num = numbers[self.unique_entries.index(unique_entry_name)]
875 self.assertEqual(num, getattr(version, counter_attribute_name))
876
877 def test_heartbeats_counter(self):
878 """Test the calculation of the heartbeats counter."""
879 numbers = [10, 7, 8, 5]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200880 counter_attribute_name = "heartbeats"
881 self._assert_counter_distribution_is_correct(
882 HeartBeat, numbers, counter_attribute_name
883 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400884
885 def test_crash_reports_counter(self):
886 """Test the calculation of the crashreports counter."""
887 numbers = [2, 5, 0, 3]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200888 counter_attribute_name = "prob_crashes"
889 boot_reason_param = {"boot_reason": Crashreport.BOOT_REASON_UNKOWN}
890 self._assert_counter_distribution_is_correct(
891 Crashreport, numbers, counter_attribute_name, **boot_reason_param
892 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400893
894 def test_smpl_reports_counter(self):
895 """Test the calculation of the smpl reports counter."""
896 numbers = [1, 3, 4, 0]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200897 counter_attribute_name = "smpl"
898 boot_reason_param = {"boot_reason": Crashreport.BOOT_REASON_RTC_ALARM}
899 self._assert_counter_distribution_is_correct(
900 Crashreport, numbers, counter_attribute_name, **boot_reason_param
901 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400902
903 def test_other_reports_counter(self):
904 """Test the calculation of the other reports counter."""
905 numbers = [0, 2, 1, 2]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200906 counter_attribute_name = "other"
907 boot_reason_param = {"boot_reason": "random boot reason"}
908 self._assert_counter_distribution_is_correct(
909 Crashreport, numbers, counter_attribute_name, **boot_reason_param
910 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400911
Mitja Nikolaus3a09c6e2018-09-04 12:17:45 +0200912 def _assert_reports_with_same_timestamp_are_counted(
913 self, report_type, counter_attribute_name, **kwargs
914 ):
915 """Validate that reports with the same timestamp are counted.
916
917 Reports from different devices but the same timestamp should be
918 counted as independent reports.
919 """
920 # Create a report
921 device1 = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
922 report1 = Dummy.create_dummy_report(
923 report_type, device=device1, **kwargs
924 )
925
926 # Create a second report with the same timestamp but from another device
927 device2 = Dummy.create_dummy_device(
928 user=Dummy.create_dummy_user(username=Dummy.USERNAMES[1])
929 )
930 Dummy.create_dummy_report(
931 report_type, device=device2, date=report1.date, **kwargs
932 )
933
934 # Run the command to update the database
935 call_command("stats", "update")
936
937 # Get the corresponding version instance from the database
938 get_params = {
939 self.unique_entry_name: getattr(report1, self.unique_entry_name)
940 }
941 version = self.version_class.objects.get(**get_params)
942
943 # Assert that both reports are counted
944 self.assertEqual(getattr(version, counter_attribute_name), 2)
945
946 @unittest.skip(
947 "Duplicates are dropped based on their timestamp at the moment. This is"
948 "to be adapted so that they are dropped taking into account the device"
949 "UUID as well."
950 )
951 def test_heartbeats_with_same_timestamp_are_counted(self):
952 """Validate that heartbeats with same timestamp are counted."""
953 counter_attribute_name = "heartbeats"
954 self._assert_reports_with_same_timestamp_are_counted(
955 HeartBeat, counter_attribute_name
956 )
957
958 @unittest.skip(
959 "Duplicates are dropped based on their timestamp at the moment. This is"
960 "to be adapted so that they are dropped taking into account the device"
961 "UUID as well."
962 )
963 def test_crash_reports_with_same_timestamp_are_counted(self):
964 """Validate that crash report duplicates are ignored."""
965 counter_attribute_name = "prob_crashes"
966 for unique_entry, boot_reason in zip(
967 self.unique_entries, Crashreport.CRASH_BOOT_REASONS
968 ):
969 params = {
970 "boot_reason": boot_reason,
971 self.unique_entry_name: unique_entry,
972 }
973 self._assert_reports_with_same_timestamp_are_counted(
974 Crashreport, counter_attribute_name, **params
975 )
976
977 @unittest.skip(
978 "Duplicates are dropped based on their timestamp at the moment. This is"
979 "to be adapted so that they are dropped taking into account the device"
980 "UUID as well."
981 )
982 def test_smpl_reports_with_same_timestamp_are_counted(self):
983 """Validate that smpl report duplicates are ignored."""
984 counter_attribute_name = "smpl"
985 for unique_entry, boot_reason in zip(
986 self.unique_entries, Crashreport.SMPL_BOOT_REASONS
987 ):
988 params = {
989 "boot_reason": boot_reason,
990 self.unique_entry_name: unique_entry,
991 }
992 self._assert_reports_with_same_timestamp_are_counted(
993 Crashreport, counter_attribute_name, **params
994 )
995
996 @unittest.skip(
997 "Duplicates are dropped based on their timestamp at the moment. This is"
998 "to be adapted so that they are dropped taking into account the device"
999 "UUID as well."
1000 )
1001 def test_other_reports_with_same_timestamp_are_counted(self):
1002 """Validate that other report duplicates are ignored."""
1003 counter_attribute_name = "other"
1004 params = {"boot_reason": "random boot reason"}
1005 self._assert_reports_with_same_timestamp_are_counted(
1006 Crashreport, counter_attribute_name, **params
1007 )
1008
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001009 def _assert_duplicates_are_ignored(
1010 self, report_type, device, counter_attribute_name, **kwargs
1011 ):
Borjan Tchakaloffd803b632018-03-20 17:32:37 +04001012 """Validate that reports with duplicate timestamps are ignored."""
1013 # Create a report
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001014 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
Borjan Tchakaloffd803b632018-03-20 17:32:37 +04001015
1016 # Create a second report with the same timestamp
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001017 Dummy.create_dummy_report(
1018 report_type, device=device, date=report.date, **kwargs
1019 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +04001020
1021 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001022 call_command("stats", "update")
Borjan Tchakaloffd803b632018-03-20 17:32:37 +04001023
1024 # Get the corresponding version instance from the database
1025 get_params = {
1026 self.unique_entry_name: getattr(report, self.unique_entry_name)
1027 }
1028 version = self.version_class.objects.get(**get_params)
1029
1030 # Assert that the report with the duplicate timestamp is not
1031 # counted, i.e. only 1 report is counted.
1032 self.assertEqual(getattr(version, counter_attribute_name), 1)
1033
1034 def test_heartbeat_duplicates_are_ignored(self):
1035 """Validate that heartbeat duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001036 counter_attribute_name = "heartbeats"
Borjan Tchakaloffd803b632018-03-20 17:32:37 +04001037 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001038 self._assert_duplicates_are_ignored(
1039 HeartBeat, device, counter_attribute_name
1040 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +04001041
1042 def test_crash_report_duplicates_are_ignored(self):
1043 """Validate that crash report duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001044 counter_attribute_name = "prob_crashes"
Borjan Tchakaloffd803b632018-03-20 17:32:37 +04001045 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
1046 for i, boot_reason in enumerate(Crashreport.CRASH_BOOT_REASONS):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001047 params = {
1048 "boot_reason": boot_reason,
1049 self.unique_entry_name: self.unique_entries[i],
1050 }
1051 self._assert_duplicates_are_ignored(
1052 Crashreport, device, counter_attribute_name, **params
1053 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +04001054
1055 def test_smpl_report_duplicates_are_ignored(self):
1056 """Validate that smpl report duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001057 counter_attribute_name = "smpl"
Borjan Tchakaloffd803b632018-03-20 17:32:37 +04001058 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
1059 for i, boot_reason in enumerate(Crashreport.SMPL_BOOT_REASONS):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001060 params = {
1061 "boot_reason": boot_reason,
1062 self.unique_entry_name: self.unique_entries[i],
1063 }
1064 self._assert_duplicates_are_ignored(
1065 Crashreport, device, counter_attribute_name, **params
1066 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +04001067
1068 def test_other_report_duplicates_are_ignored(self):
1069 """Validate that other report duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001070 counter_attribute_name = "other"
1071 params = {"boot_reason": "random boot reason"}
Borjan Tchakaloffd803b632018-03-20 17:32:37 +04001072 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001073 self._assert_duplicates_are_ignored(
1074 Crashreport, device, counter_attribute_name, **params
1075 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +04001076
Mitja Nikolaus52e44b82018-09-04 14:23:19 +02001077 def _assert_older_reports_update_released_on_date(
1078 self, report_type, **kwargs
1079 ):
1080 """Test updating of the released_on date.
1081
1082 Validate that the released_on date is updated once an older report is
1083 sent.
1084 """
1085 # Create a report
1086 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
1087 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
1088
1089 # Run the command to update the database
1090 call_command("stats", "update")
1091
1092 # Get the corresponding version instance from the database
1093 version = self.version_class.objects.get(
1094 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
1095 )
1096
1097 # Assert that the released_on date matches the first report date
1098 self.assertEqual(version.released_on, report.date.date())
1099
1100 # Create a second report with the a timestamp earlier in time
1101 report_2_date = report.date - timedelta(days=1)
1102 Dummy.create_dummy_report(
1103 report_type, device=device, date=report_2_date, **kwargs
1104 )
1105
1106 # Run the command to update the database
1107 call_command("stats", "update")
1108
1109 # Get the corresponding version instance from the database
1110 version = self.version_class.objects.get(
1111 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
1112 )
1113
1114 # Assert that the released_on date matches the older report date
1115 self.assertEqual(version.released_on, report_2_date.date())
1116
1117 def _assert_newer_reports_do_not_update_released_on_date(
1118 self, report_type, **kwargs
1119 ):
1120 """Test updating of the released_on date.
1121
1122 Validate that the released_on date is not updated once a newer report is
1123 sent.
1124 """
1125 # Create a report
1126 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
1127 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
1128 report_1_date = report.date.date()
1129
1130 # Run the command to update the database
1131 call_command("stats", "update")
1132
1133 # Get the corresponding version instance from the database
1134 version = self.version_class.objects.get(
1135 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
1136 )
1137
1138 # Assert that the released_on date matches the first report date
1139 self.assertEqual(version.released_on, report_1_date)
1140
1141 # Create a second report with the a timestamp later in time
1142 report_2_date = report.date + timedelta(days=1)
1143 Dummy.create_dummy_report(
1144 report_type, device=device, date=report_2_date, **kwargs
1145 )
1146
1147 # Run the command to update the database
1148 call_command("stats", "update")
1149
1150 # Get the corresponding version instance from the database
1151 version = self.version_class.objects.get(
1152 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
1153 )
1154
1155 # Assert that the released_on date matches the older report date
1156 self.assertEqual(version.released_on, report_1_date)
1157
1158 def test_older_heartbeat_updates_released_on_date(self):
1159 """Validate that older heartbeats update the release date."""
1160 self._assert_older_reports_update_released_on_date(HeartBeat)
1161
1162 def test_older_crash_report_updates_released_on_date(self):
1163 """Validate that older crash reports update the release date."""
1164 self._assert_older_reports_update_released_on_date(Crashreport)
1165
1166 def test_newer_heartbeat_does_not_update_released_on_date(self):
1167 """Validate that newer heartbeats don't update the release date."""
1168 self._assert_newer_reports_do_not_update_released_on_date(HeartBeat)
1169
1170 def test_newer_crash_report_does_not_update_released_on_date(self):
1171 """Validate that newer crash reports don't update the release date."""
1172 self._assert_newer_reports_do_not_update_released_on_date(Crashreport)
1173
1174 def _assert_manually_changed_released_on_date_is_not_updated(
1175 self, report_type, **kwargs
1176 ):
1177 """Test updating of manually changed released_on dates.
1178
1179 Validate that a manually changed released_on date is not updated when
1180 new reports are sent.
1181 """
1182 # Create a report
1183 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
1184 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
1185
1186 # Run the command to update the database
1187 call_command("stats", "update")
1188
1189 # Get the corresponding version instance from the database
1190 version = self.version_class.objects.get(
1191 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
1192 )
1193
1194 # Assert that the released_on date matches the first report date
1195 self.assertEqual(version.released_on, report.date.date())
1196
1197 # Create a second report with a timestamp earlier in time
1198 report_2_date = report.date - timedelta(days=1)
1199 Dummy.create_dummy_report(
1200 report_type, device=device, date=report_2_date, **kwargs
1201 )
1202
1203 # Manually change the released_on date
1204 version_release_date = report.date + timedelta(days=1)
1205 version.released_on = version_release_date
1206 version.save()
1207
1208 # Run the command to update the database
1209 call_command("stats", "update")
1210
1211 # Get the corresponding version instance from the database
1212 version = self.version_class.objects.get(
1213 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
1214 )
1215
1216 # Assert that the released_on date still matches the date is was
1217 # manually changed to
1218 self.assertEqual(version.released_on, version_release_date.date())
1219
1220 def test_manually_changed_released_on_date_is_not_updated_by_heartbeat(
1221 self
1222 ):
1223 """Test update of manually changed released_on date with heartbeat."""
1224 self._assert_manually_changed_released_on_date_is_not_updated(HeartBeat)
1225
1226 def test_manually_changed_released_on_date_is_not_updated_by_crash_report(
1227 self
1228 ):
1229 """Test update of manually changed released_on date with crashreport."""
1230 self._assert_manually_changed_released_on_date_is_not_updated(
1231 Crashreport
1232 )
1233
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +04001234
1235# pylint: disable=too-many-ancestors
1236class StatsCommandRadioVersionsTestCase(StatsCommandVersionsTestCase):
1237 """Test the generation of RadioVersion stats with the stats command."""
1238
1239 version_class = RadioVersion
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001240 unique_entry_name = "radio_version"
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +04001241 unique_entries = Dummy.RADIO_VERSIONS
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001242
1243
1244class CommandDebugOutputTestCase(TestCase):
1245 """Test the reset and update commands debug output."""
1246
1247 # Additional positional arguments to pass to the commands
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001248 _CMD_ARGS = ["--no-color", "-v 2"]
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001249
1250 # The stats models
1251 _STATS_MODELS = [Version, VersionDaily, RadioVersion, RadioVersionDaily]
1252 # The models that will generate an output
1253 _ALL_MODELS = _STATS_MODELS + [StatsMetadata]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001254 _COUNTER_NAMES = ["heartbeats", "crashes", "smpl", "other"]
1255 _COUNTER_ACTIONS = ["created", "updated"]
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001256
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +02001257 def _assert_command_output_matches(self, command, number, facts, models):
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001258 """Validate the debug output of a command.
1259
1260 The debug output is matched against the facts and models given in
1261 the parameters.
1262 """
1263 buffer = StringIO()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001264 call_command("stats", command, *self._CMD_ARGS, stdout=buffer)
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001265 output = buffer.getvalue().splitlines()
1266
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001267 expected_output = "{number} {model} {fact}"
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001268 for model in models:
1269 for fact in facts:
1270 self.assertIn(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001271 expected_output.format(
1272 number=number, model=model.__name__, fact=fact
1273 ),
1274 output,
1275 )
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001276
1277 def test_reset_command_on_empty_db(self):
1278 """Test the reset command on an empty database.
1279
1280 The reset command should yield nothing on an empty database.
1281 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001282 self._assert_command_output_matches(
1283 "reset", 0, ["deleted"], self._ALL_MODELS
1284 )
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001285
1286 def test_update_command_on_empty_db(self):
1287 """Test the update command on an empty database.
1288
1289 The update command should yield nothing on an empty database.
1290 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001291 pattern = "{action} for counter {counter}"
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001292 facts = [
1293 pattern.format(action=counter_action, counter=counter_name)
1294 for counter_action in self._COUNTER_ACTIONS
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001295 for counter_name in self._COUNTER_NAMES
1296 ]
1297 self._assert_command_output_matches(
1298 "update", 0, facts, self._STATS_MODELS
1299 )
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +02001300
1301 def test_reset_command_deletion_of_instances(self):
1302 """Test the deletion of stats model instances with the reset command.
1303
1304 This test validates that model instances get deleted when the
1305 reset command is called on a database that only contains a single
1306 model instance for each class.
1307 """
1308 # Create dummy version instances
1309 version = Dummy.create_dummy_version()
1310 radio_version = Dummy.create_dummy_radio_version()
1311 Dummy.create_dummy_daily_version(version)
1312 Dummy.create_dummy_daily_radio_version(radio_version)
1313 Dummy.create_dummy_stats_metadata()
1314
1315 # We expect that the model instances get deleted
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001316 self._assert_command_output_matches(
1317 "reset", 1, ["deleted"], self._ALL_MODELS
1318 )
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02001319
1320
Mitja Nikolaus98e3a142018-09-18 16:39:46 +02001321class DeviceStatsTestCase(_HiccupStatsAPITestCase):
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02001322 """Test the single device stats REST endpoints."""
1323
Mitja Nikolaus98e3a142018-09-18 16:39:46 +02001324 device_overview_url = "hiccup_stats_api_v1_device_overview"
1325 device_report_history_url = "hiccup_stats_api_v1_device_report_history"
1326 device_update_history_url = "hiccup_stats_api_v1_device_update_history"
1327 device_logfile_download_url = "hiccup_stats_api_v1_logfile_download"
1328
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02001329 def _get_with_params(self, url, params):
1330 url = reverse(url, kwargs=params)
1331 return self.fp_staff_client.get(url)
1332
1333 def _assert_device_stats_response_is(
1334 self,
1335 response,
1336 uuid,
1337 board_date,
1338 num_heartbeats,
1339 num_crashreports,
1340 num_smpls,
1341 crashes_per_day,
1342 smpl_per_day,
1343 last_active,
1344 ):
1345 # pylint: disable=too-many-arguments
1346 self.assertEqual(response.status_code, status.HTTP_200_OK)
1347
1348 self.assertIn("uuid", response.data)
1349 self.assertIn("board_date", response.data)
1350 self.assertIn("heartbeats", response.data)
1351 self.assertIn("crashreports", response.data)
1352 self.assertIn("smpls", response.data)
1353 self.assertIn("crashes_per_day", response.data)
1354 self.assertIn("smpl_per_day", response.data)
1355 self.assertIn("last_active", response.data)
1356
1357 self.assertEqual(response.data["uuid"], uuid)
1358 self.assertEqual(response.data["board_date"], board_date)
1359 self.assertEqual(response.data["heartbeats"], num_heartbeats)
1360 self.assertEqual(response.data["crashreports"], num_crashreports)
1361 self.assertEqual(response.data["smpls"], num_smpls)
1362 self.assertEqual(response.data["crashes_per_day"], crashes_per_day)
1363 self.assertEqual(response.data["smpl_per_day"], smpl_per_day)
1364 self.assertEqual(response.data["last_active"], last_active)
1365
1366 @unittest.skip(
1367 "Fails because there is no fallback for the last_active "
1368 "date for devices without heartbeats."
1369 )
Mitja Nikolaus98e3a142018-09-18 16:39:46 +02001370 def test_device_overview_url_as_admin(self):
1371 """Test that admin users can access the URL."""
1372 self._assert_get_as_admin_user_succeeds(
1373 reverse(
1374 self.device_overview_url,
1375 kwargs={"uuid": self.device_owner_device.uuid},
1376 )
1377 )
1378
1379 @unittest.skip(
1380 "Fails because there is no fallback for the last_active "
1381 "date for devices without heartbeats."
1382 )
1383 def test_device_overview_url_as_fp_staff(self):
1384 """Test that Fairphone staff users can access the URL."""
1385 self._assert_get_as_fp_staff_succeeds(
1386 reverse(
1387 self.device_overview_url,
1388 kwargs={"uuid": self.device_owner_device.uuid},
1389 )
1390 )
1391
1392 def test_device_overview_url_as_device_owner(self):
1393 """Test that device owner users can not access the URL."""
1394 self._assert_get_as_device_owner_fails(
1395 reverse(
1396 self.device_overview_url,
1397 kwargs={"uuid": self.device_owner_device.uuid},
1398 )
1399 )
1400
1401 def test_device_overview_url_no_auth(self):
1402 """Test that non-authenticated users can not access the URL."""
1403 self._assert_get_without_authentication_fails(
1404 reverse(
1405 self.device_overview_url,
1406 kwargs={"uuid": self.device_owner_device.uuid},
1407 )
1408 )
1409
1410 def test_device_report_history_url_as_admin(self):
1411 """Test that admin users can access device report history URL."""
1412 self._assert_get_as_admin_user_succeeds(
1413 reverse(
1414 self.device_report_history_url,
1415 kwargs={"uuid": self.device_owner_device.uuid},
1416 )
1417 )
1418
1419 def test_device_report_history_url_as_fp_staff(self):
1420 """Test that FP staff can access device report history URL."""
1421 self._assert_get_as_fp_staff_succeeds(
1422 reverse(
1423 self.device_report_history_url,
1424 kwargs={"uuid": self.device_owner_device.uuid},
1425 )
1426 )
1427
1428 def test_device_report_history_url_as_device_owner(self):
1429 """Test that device owners can't access device report history URL."""
1430 self._assert_get_as_device_owner_fails(
1431 reverse(
1432 self.device_report_history_url,
1433 kwargs={"uuid": self.device_owner_device.uuid},
1434 )
1435 )
1436
1437 def test_device_report_history_url_no_auth(self):
1438 """Test that device report history is not accessible without auth."""
1439 self._assert_get_without_authentication_fails(
1440 reverse(
1441 self.device_report_history_url,
1442 kwargs={"uuid": self.device_owner_device.uuid},
1443 )
1444 )
1445
1446 def test_device_update_history_url_as_admin(self):
1447 """Test that admin users can access device update history URL."""
1448 self._assert_get_as_admin_user_succeeds(
1449 reverse(
1450 self.device_update_history_url,
1451 kwargs={"uuid": self.device_owner_device.uuid},
1452 )
1453 )
1454
1455 def test_device_update_history_url_as_fp_staff(self):
1456 """Test that FP staff can access device update history URL."""
1457 self._assert_get_as_fp_staff_succeeds(
1458 reverse(
1459 self.device_update_history_url,
1460 kwargs={"uuid": self.device_owner_device.uuid},
1461 )
1462 )
1463
1464 def test_device_update_history_url_as_device_owner(self):
1465 """Test that device owners can't access device update history URL."""
1466 self._assert_get_as_device_owner_fails(
1467 reverse(
1468 self.device_update_history_url,
1469 kwargs={"uuid": self.device_owner_device.uuid},
1470 )
1471 )
1472
1473 def test_device_update_history_url_no_auth(self):
1474 """Test that device update history is not accessible without auth."""
1475 self._assert_get_without_authentication_fails(
1476 reverse(
1477 self.device_update_history_url,
1478 kwargs={"uuid": self.device_owner_device.uuid},
1479 )
1480 )
1481
1482 def test_logfile_download_url_as_admin(self):
1483 """Test that admin users can access the logfile download URL."""
1484 non_existent_logfile_id = 0
1485 self.assertFalse(
1486 LogFile.objects.filter(id=non_existent_logfile_id).exists()
1487 )
1488 self._assert_get_as_admin_user_succeeds(
1489 reverse(
1490 self.device_logfile_download_url,
1491 kwargs={"id_logfile": non_existent_logfile_id},
1492 ),
1493 expected_status=status.HTTP_404_NOT_FOUND,
1494 )
1495
1496 def tes_logfile_download_url_as_fp_staff(self):
1497 """Test that FP staff can access the logfile download URL."""
1498 non_existent_logfile_id = 0
1499 self.assertFalse(
1500 LogFile.objects.filter(id=non_existent_logfile_id).exists()
1501 )
1502 self._assert_get_as_fp_staff_succeeds(
1503 reverse(
1504 self.device_logfile_download_url,
1505 kwargs={"id_logfile": non_existent_logfile_id},
1506 ),
1507 expected_status=status.HTTP_404_NOT_FOUND,
1508 )
1509
1510 def test_logfile_download_url_as_device_owner(self):
1511 """Test that device owners can't access the logfile download URL."""
1512 non_existent_logfile_id = 0
1513 self.assertFalse(
1514 LogFile.objects.filter(id=non_existent_logfile_id).exists()
1515 )
1516 self._assert_get_as_device_owner_fails(
1517 reverse(
1518 self.device_logfile_download_url,
1519 kwargs={"id_logfile": non_existent_logfile_id},
1520 )
1521 )
1522
1523 def test_logfile_download_url_no_auth(self):
1524 """Test that the logfile download URL is not accessible without auth."""
1525 non_existent_logfile_id = 0
1526 self.assertFalse(
1527 LogFile.objects.filter(id=non_existent_logfile_id).exists()
1528 )
1529 self._assert_get_without_authentication_fails(
1530 reverse(
1531 self.device_logfile_download_url,
1532 kwargs={"id_logfile": non_existent_logfile_id},
1533 )
1534 )
1535
1536 @unittest.skip(
1537 "Fails because there is no fallback for the last_active "
1538 "date for devices without heartbeats."
1539 )
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02001540 def test_get_device_stats_no_reports(self):
1541 """Test getting device stats for a device without reports."""
1542 # Create a device
1543 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1544
1545 # Get the device statistics
1546 response = self._get_with_params(
Mitja Nikolaus98e3a142018-09-18 16:39:46 +02001547 self.device_overview_url, {"uuid": device.uuid}
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02001548 )
1549
1550 # Assert that the statistics match
1551 self._assert_device_stats_response_is(
1552 response=response,
1553 uuid=str(device.uuid),
1554 board_date=device.board_date,
1555 num_heartbeats=0,
1556 num_crashreports=0,
1557 num_smpls=0,
1558 crashes_per_day=0.0,
1559 smpl_per_day=0.0,
1560 last_active=device.board_date,
1561 )
1562
1563 def test_get_device_stats_no_crash_reports(self):
1564 """Test getting device stats for a device without crashreports."""
1565 # Create a device and a heartbeat
1566 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1567 heartbeat = Dummy.create_dummy_report(HeartBeat, device)
1568
1569 # Get the device statistics
1570 response = self._get_with_params(
Mitja Nikolaus98e3a142018-09-18 16:39:46 +02001571 self.device_overview_url, {"uuid": device.uuid}
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02001572 )
1573
1574 # Assert that the statistics match
1575 self._assert_device_stats_response_is(
1576 response=response,
1577 uuid=str(device.uuid),
1578 board_date=device.board_date,
1579 num_heartbeats=1,
1580 num_crashreports=0,
1581 num_smpls=0,
1582 crashes_per_day=0.0,
1583 smpl_per_day=0.0,
1584 last_active=heartbeat.date,
1585 )
1586
1587 @unittest.skip(
1588 "Fails because there is no fallback for the last_active "
1589 "date for devices without heartbeats."
1590 )
1591 def test_get_device_stats_no_heartbeats(self):
1592 """Test getting device stats for a device without heartbeats."""
1593 # Create a device and crashreport
1594 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1595 Dummy.create_dummy_report(Crashreport, device)
1596
1597 # Get the device statistics
1598 response = self._get_with_params(
Mitja Nikolaus98e3a142018-09-18 16:39:46 +02001599 self.device_overview_url, {"uuid": device.uuid}
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02001600 )
1601
1602 # Assert that the statistics match
1603 self._assert_device_stats_response_is(
1604 response=response,
1605 uuid=str(device.uuid),
1606 board_date=device.board_date,
1607 num_heartbeats=0,
1608 num_crashreports=1,
1609 num_smpls=0,
1610 crashes_per_day=0.0,
1611 smpl_per_day=0.0,
1612 last_active=device.board_date,
1613 )
1614
1615 def test_get_device_stats(self):
1616 """Test getting device stats for a device."""
1617 # Create a device with a heartbeat and one report of each type
1618 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1619 heartbeat = Dummy.create_dummy_report(HeartBeat, device)
1620 for boot_reason in (
1621 Crashreport.SMPL_BOOT_REASONS
1622 + Crashreport.CRASH_BOOT_REASONS
1623 + ["other boot reason"]
1624 ):
1625 Dummy.create_dummy_report(
1626 Crashreport, device, boot_reason=boot_reason
1627 )
1628
1629 # Get the device statistics
1630 response = self._get_with_params(
Mitja Nikolaus98e3a142018-09-18 16:39:46 +02001631 self.device_overview_url, {"uuid": device.uuid}
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02001632 )
1633
1634 # Assert that the statistics match
1635 self._assert_device_stats_response_is(
1636 response=response,
1637 uuid=str(device.uuid),
1638 board_date=device.board_date,
1639 num_heartbeats=1,
1640 num_crashreports=len(Crashreport.CRASH_BOOT_REASONS),
1641 num_smpls=len(Crashreport.SMPL_BOOT_REASONS),
1642 crashes_per_day=len(Crashreport.CRASH_BOOT_REASONS),
1643 smpl_per_day=len(Crashreport.SMPL_BOOT_REASONS),
1644 last_active=heartbeat.date,
1645 )
1646
1647 def test_get_device_stats_multiple_days(self):
1648 """Test getting device stats for a device that sent more reports."""
1649 # Create a device with some heartbeats and reports over time
1650 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1651 num_days = 100
1652 for i in range(num_days):
1653 report_day = datetime.now(tz=pytz.utc) + timedelta(days=i)
1654 heartbeat = Dummy.create_dummy_report(
1655 HeartBeat, device, date=report_day
1656 )
1657 Dummy.create_dummy_report(Crashreport, device, date=report_day)
1658 Dummy.create_dummy_report(
1659 Crashreport,
1660 device,
1661 date=report_day,
1662 boot_reason=Crashreport.SMPL_BOOT_REASONS[0],
1663 )
1664
1665 # Get the device statistics
1666 response = self._get_with_params(
Mitja Nikolaus98e3a142018-09-18 16:39:46 +02001667 self.device_overview_url, {"uuid": device.uuid}
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02001668 )
1669
1670 # Assert that the statistics match
1671 self._assert_device_stats_response_is(
1672 response=response,
1673 uuid=str(device.uuid),
1674 board_date=device.board_date,
1675 num_heartbeats=num_days,
1676 num_crashreports=num_days,
1677 num_smpls=num_days,
1678 crashes_per_day=1,
1679 smpl_per_day=1,
1680 last_active=heartbeat.date,
1681 )
1682
1683 def test_get_device_stats_multiple_days_missing_heartbeat(self):
1684 """Test getting device stats for a device with missing heartbeat."""
1685 # Create a device with some heartbeats and reports over time
1686 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1687 num_days = 100
1688 skip_day = round(num_days / 2)
1689 for i in range(num_days):
1690 report_day = datetime.now(tz=pytz.utc) + timedelta(days=i)
1691 # Skip creation of heartbeat at one day
1692 if i != skip_day:
1693 heartbeat = Dummy.create_dummy_report(
1694 HeartBeat, device, date=report_day
1695 )
1696 Dummy.create_dummy_report(Crashreport, device, date=report_day)
1697
1698 # Get the device statistics
1699 response = self._get_with_params(
Mitja Nikolaus98e3a142018-09-18 16:39:46 +02001700 self.device_overview_url, {"uuid": device.uuid}
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02001701 )
1702
1703 # Assert that the statistics match
1704 self._assert_device_stats_response_is(
1705 response=response,
1706 uuid=str(device.uuid),
1707 board_date=device.board_date,
1708 num_heartbeats=num_days - 1,
1709 num_crashreports=num_days,
1710 num_smpls=0,
1711 crashes_per_day=num_days / (num_days - 1),
1712 smpl_per_day=0,
1713 last_active=heartbeat.date,
1714 )
1715
1716 @unittest.skip("Duplicate heartbeats are currently not dropped.")
1717 def test_get_device_stats_multiple_days_duplicate_heartbeat(self):
1718 """Test getting device stats for a device with duplicate heartbeat.
1719
1720 Duplicate heartbeats are dropped and thus should not influence the
1721 statistics.
1722 """
1723 # Create a device with some heartbeats and reports over time
1724 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1725 num_days = 100
1726 duplicate_day = round(num_days / 2)
1727 first_report_day = Dummy.DEFAULT_DUMMY_HEARTBEAT_VALUES["date"]
1728 for i in range(num_days):
1729 report_day = first_report_day + timedelta(days=i)
1730 heartbeat = Dummy.create_dummy_report(
1731 HeartBeat, device, date=report_day
1732 )
1733 # Create a second at the duplicate day (with 1 hour delay)
1734 if i == duplicate_day:
1735 Dummy.create_dummy_report(
1736 HeartBeat, device, date=report_day + timedelta(hours=1)
1737 )
1738 Dummy.create_dummy_report(Crashreport, device, date=report_day)
1739
1740 # Get the device statistics
1741 response = self._get_with_params(
Mitja Nikolaus98e3a142018-09-18 16:39:46 +02001742 self.device_overview_url, {"uuid": device.uuid}
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02001743 )
1744
1745 # Assert that the statistics match
1746 self._assert_device_stats_response_is(
1747 response=response,
1748 uuid=str(device.uuid),
1749 board_date=device.board_date,
1750 num_heartbeats=num_days,
1751 num_crashreports=num_days,
1752 num_smpls=0,
1753 crashes_per_day=1,
1754 smpl_per_day=0,
1755 last_active=heartbeat.date,
1756 )
1757
1758 def test_get_device_report_history_no_reports(self):
1759 """Test getting report history stats for a device without reports."""
1760 # Create a device
1761 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1762
1763 # Get the device report history statistics
1764 response = self._get_with_params(
Mitja Nikolaus98e3a142018-09-18 16:39:46 +02001765 self.device_report_history_url, {"uuid": device.uuid}
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02001766 )
1767
1768 # Assert that the report history is empty
1769 self.assertEqual([], response.data)
1770
1771 @unittest.skip("Broken raw query. Heartbeats are not counted correctly.")
1772 def test_get_device_report_history(self):
1773 """Test getting report history stats for a device."""
1774 # Create a device with a heartbeat and one report of each type
1775 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1776 heartbeat = Dummy.create_dummy_report(HeartBeat, device)
1777 for boot_reason in (
1778 Crashreport.SMPL_BOOT_REASONS
1779 + Crashreport.CRASH_BOOT_REASONS
1780 + ["other boot reason"]
1781 ):
1782 Dummy.create_dummy_report(
1783 Crashreport, device, boot_reason=boot_reason
1784 )
1785
1786 # Get the device report history statistics
1787 response = self._get_with_params(
Mitja Nikolaus98e3a142018-09-18 16:39:46 +02001788 self.device_report_history_url, {"uuid": device.uuid}
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02001789 )
1790
1791 # Assert that the statistics match
1792 report_history = [
1793 {
1794 "date": heartbeat.date.date(),
1795 "heartbeats": 1,
1796 "smpl": len(Crashreport.SMPL_BOOT_REASONS),
1797 "prob_crashes": len(Crashreport.CRASH_BOOT_REASONS),
1798 "other": 1,
1799 }
1800 ]
1801 self.assertEqual(report_history, response.data)
1802
1803 def test_get_device_update_history_no_reports(self):
1804 """Test getting update history stats for a device without reports."""
1805 # Create a device
1806 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1807
1808 # Get the device report history statistics
1809 response = self._get_with_params(
Mitja Nikolaus98e3a142018-09-18 16:39:46 +02001810 self.device_update_history_url, {"uuid": device.uuid}
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02001811 )
1812
1813 # Assert that the update history is empty
1814 self.assertEqual([], response.data)
1815
1816 def test_get_device_update_history(self):
1817 """Test getting update history stats for a device."""
1818 # Create a device with a heartbeat and one report of each type
1819 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1820 heartbeat = Dummy.create_dummy_report(HeartBeat, device)
1821 for boot_reason in (
1822 Crashreport.SMPL_BOOT_REASONS
1823 + Crashreport.CRASH_BOOT_REASONS
1824 + ["other boot reason"]
1825 ):
1826 params = {"boot_reason": boot_reason}
1827 Dummy.create_dummy_report(Crashreport, device, **params)
1828
1829 # Get the device update history statistics
1830 response = self._get_with_params(
Mitja Nikolaus98e3a142018-09-18 16:39:46 +02001831 self.device_update_history_url, {"uuid": device.uuid}
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02001832 )
1833
1834 # Assert that the statistics match
1835 update_history = [
1836 {
1837 "build_fingerprint": heartbeat.build_fingerprint,
1838 "heartbeats": 1,
1839 "max": device.id,
1840 "other": 1,
1841 "prob_crashes": len(Crashreport.CRASH_BOOT_REASONS),
1842 "smpl": len(Crashreport.SMPL_BOOT_REASONS),
1843 "update_date": heartbeat.date,
1844 }
1845 ]
1846 self.assertEqual(update_history, response.data)
1847
1848 def test_get_device_update_history_multiple_updates(self):
1849 """Test getting update history stats with multiple updates."""
1850 # Create a device with a heartbeats and crashreport for each build
1851 # fingerprint in the dummy values
1852 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1853 expected_update_history = []
1854 for i, build_fingerprint in enumerate(Dummy.BUILD_FINGERPRINTS):
1855 report_day = datetime.now(tz=pytz.utc) + timedelta(days=i)
1856 Dummy.create_dummy_report(
1857 HeartBeat,
1858 device,
1859 date=report_day,
1860 build_fingerprint=build_fingerprint,
1861 )
1862 Dummy.create_dummy_report(
1863 Crashreport,
1864 device,
1865 date=report_day,
1866 build_fingerprint=build_fingerprint,
1867 )
1868
1869 # Create the expected update history object
1870 expected_update_history.append(
1871 {
1872 "update_date": report_day,
1873 "build_fingerprint": build_fingerprint,
1874 "max": device.id,
1875 "prob_crashes": 1,
1876 "smpl": 0,
1877 "other": 0,
1878 "heartbeats": 1,
1879 }
1880 )
1881 # Sort the expected values by build fingerprint
1882 expected_update_history.sort(
1883 key=operator.itemgetter("build_fingerprint")
1884 )
1885
1886 # Get the device update history statistics and sort it
1887 response = self._get_with_params(
Mitja Nikolaus98e3a142018-09-18 16:39:46 +02001888 self.device_update_history_url, {"uuid": device.uuid}
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02001889 )
1890 response.data.sort(key=operator.itemgetter("build_fingerprint"))
1891
1892 # Assert that the statistics match
1893 self.assertEqual(expected_update_history, response.data)
1894
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02001895 def test_download_non_existing_logfile(self):
1896 """Test download of a non existing log file."""
1897 # Try to get a log file
1898 response = self._get_with_params(
Mitja Nikolaus98e3a142018-09-18 16:39:46 +02001899 self.device_logfile_download_url, {"id_logfile": 0}
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02001900 )
1901
1902 # Assert that the log file was not found
1903 self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
1904
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02001905 def test_download_logfile(self):
1906 """Test download of log files."""
1907 # Create a device with a crash report along with log file
1908 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1909 crashreport = Dummy.create_dummy_report(Crashreport, device)
1910 logfile = Dummy.create_dummy_log_file(crashreport)
1911
1912 # Get the log file
1913 response = self._get_with_params(
Mitja Nikolaus98e3a142018-09-18 16:39:46 +02001914 self.device_logfile_download_url, {"id_logfile": logfile.id}
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02001915 )
1916
1917 # Assert that the log file contents are in the response data
1918 self.assertEqual(response.status_code, status.HTTP_200_OK)
1919 self.assertIn(Dummy.DEFAULT_DUMMY_LOG_FILE_NAME, response.data)
1920 expected_logfile_content = Dummy.read_logfile_contents(
1921 logfile.logfile.path, Dummy.DEFAULT_DUMMY_LOG_FILE_NAME
1922 )
1923 self.assertEqual(
1924 response.data[Dummy.DEFAULT_DUMMY_LOG_FILE_NAME],
1925 expected_logfile_content,
1926 )
Mitja Nikolausded30ae2018-09-14 15:40:08 +02001927
1928
Mitja Nikolaus98e3a142018-09-18 16:39:46 +02001929class ViewsTestCase(_HiccupStatsAPITestCase):
Mitja Nikolausded30ae2018-09-14 15:40:08 +02001930 """Test cases for the statistics views."""
1931
1932 home_url = reverse("device")
1933 device_url = reverse("hiccup_stats_device")
1934 versions_url = reverse("hiccup_stats_versions")
1935 versions_all_url = reverse("hiccup_stats_versions_all")
1936
1937 @staticmethod
1938 def _url_with_params(url, params):
Mitja Nikolaus98e3a142018-09-18 16:39:46 +02001939 # Encode params, but keep slashes because we want to accept URLs as
1940 # parameter values.
1941 encoded_params = urlencode(params, safe="/")
1942 return "{}?{}".format(url, encoded_params)
Mitja Nikolausded30ae2018-09-14 15:40:08 +02001943
1944 def _get_with_params(self, url, params):
1945 return self.fp_staff_client.get(self._url_with_params(url, params))
1946
Mitja Nikolaus98e3a142018-09-18 16:39:46 +02001947 @unittest.skip(
1948 "Fails because the view is currently not accessible for admin users."
1949 )
1950 def test_home_view_as_admin(self):
1951 """Test that admin users can access the home view."""
1952 self._assert_get_as_admin_user_succeeds(self.home_url)
1953
1954 def test_home_view_as_fp_staff(self):
1955 """Test that Fairphone staff users can access the home view."""
1956 self._assert_get_as_fp_staff_succeeds(self.home_url)
1957
1958 def test_home_view_as_device_owner(self):
1959 """Test that device owner users can not access the home view."""
1960 # Assert that the response is a redirect (to the login page)
1961 self._assert_get_as_device_owner_fails(
1962 self.home_url, expected_status=status.HTTP_302_FOUND
1963 )
1964
1965 def test_home_view_no_auth(self):
1966 """Test that one can not access the home view without auth."""
1967 # Assert that the response is a redirect (to the login page)
1968 self._assert_get_without_authentication_fails(
1969 self.home_url, expected_status=status.HTTP_302_FOUND
1970 )
1971
1972 @unittest.skip(
1973 "Fails because the view is currently not accessible for admin users."
1974 )
1975 def test_device_view_as_admin(self):
1976 """Test that admin users can access the device view."""
1977 self._assert_get_as_admin_user_succeeds(
1978 self._url_with_params(
1979 self.device_url, {"uuid": self.device_owner_device.uuid}
1980 )
1981 )
1982
1983 def test_device_view_as_fp_staff(self):
1984 """Test that Fairphone staff users can access the device view."""
1985 self._assert_get_as_fp_staff_succeeds(
1986 self._url_with_params(
1987 self.device_url, {"uuid": self.device_owner_device.uuid}
1988 )
1989 )
1990
1991 def test_device_view_as_device_owner(self):
1992 """Test that device owner users can not access the device view."""
1993 # Assert that the response is a redirect (to the login page)
1994 self._assert_get_as_device_owner_fails(
1995 self._url_with_params(
1996 self.device_url, {"uuid": self.device_owner_device.uuid}
1997 ),
1998 expected_status=status.HTTP_302_FOUND,
1999 )
2000
2001 def test_device_view_no_auth(self):
2002 """Test that non-authenticated users can not access the device view."""
2003 # Assert that the response is a redirect (to the login page)
2004 self._assert_get_without_authentication_fails(
2005 self._url_with_params(
2006 self.device_url, {"uuid": self.device_owner_device.uuid}
2007 ),
2008 expected_status=status.HTTP_302_FOUND,
2009 )
2010
2011 @unittest.skip(
2012 "Fails because the view is currently not accessible for admin users."
2013 )
2014 def test_versions_view_as_admin(self):
2015 """Test that admin users can access the versions view."""
2016 self._assert_get_as_admin_user_succeeds(self.versions_url)
2017
2018 def test_versions_view_as_fp_staff(self):
2019 """Test that Fairphone staff users can access the versions view."""
2020 self._assert_get_as_fp_staff_succeeds(self.versions_url)
2021
2022 def test_versions_view_as_device_owner(self):
2023 """Test that device owner users can not access the versions view."""
2024 # Assert that the response is a redirect (to the login page)
2025 self._assert_get_as_device_owner_fails(
2026 self.versions_url, expected_status=status.HTTP_302_FOUND
2027 )
2028
2029 def test_versions_view_no_auth(self):
2030 """Test one can not access the versions view without auth."""
2031 # Assert that the response is a redirect (to the login page)
2032 self._assert_get_without_authentication_fails(
2033 self.versions_url, expected_status=status.HTTP_302_FOUND
2034 )
2035
2036 @unittest.skip(
2037 "Fails because the view is currently not accessible for admin users."
2038 )
2039 def test_versions_all_view_as_admin(self):
2040 """Test that admin users can access the versions all view."""
2041 self._assert_get_as_admin_user_succeeds(self.versions_all_url)
2042
2043 def test_versions_all_view_as_fp_staff(self):
2044 """Test that Fairphone staff users can access the versions all view."""
2045 self._assert_get_as_fp_staff_succeeds(self.versions_all_url)
2046
2047 def test_versions_all_view_as_device_owner(self):
2048 """Test that device owner users can not access the versions all view."""
2049 # Assert that the response is a redirect (to the login page)
2050 self._assert_get_as_device_owner_fails(
2051 self.versions_all_url, expected_status=status.HTTP_302_FOUND
2052 )
2053
2054 def test_versions_all_view_no_auth(self):
2055 """Test that one can not access the versions all view without auth."""
2056 # Assert that the response is a redirect (to the login page)
2057 self._assert_get_without_authentication_fails(
2058 self.versions_all_url, expected_status=status.HTTP_302_FOUND
2059 )
2060
2061 @unittest.skip(
2062 "Fails because the view is currently not accessible for admin users."
2063 )
2064 def test_home_view_post_as_admin_user(self):
2065 """Test HTTP POST method to home view as admin user."""
2066 response = self.admin.post(
2067 self.home_url, data={"uuid": str(self.device_owner_device.uuid)}
2068 )
2069
2070 # Assert that the response is a redirect to the device page
2071 self.assertRedirects(
2072 response,
2073 self._url_with_params(
2074 self.device_url, {"uuid": self.device_owner_device.uuid}
2075 ),
2076 )
2077
2078 def test_home_view_post_as_fp_staff(self):
2079 """Test HTTP POST method to home view as Fairphone staff user."""
2080 response = self.fp_staff_client.post(
2081 self.home_url, data={"uuid": str(self.device_owner_device.uuid)}
2082 )
2083
2084 # Assert that the response is a redirect to the device page
2085 self.assertRedirects(
2086 response,
2087 self._url_with_params(
2088 self.device_url, {"uuid": self.device_owner_device.uuid}
2089 ),
2090 )
2091
2092 def test_home_view_post_no_auth(self):
2093 """Test HTTP POST method to home view without authentication."""
2094 response = self.client.post(
2095 self.home_url, data={"uuid": str(self.device_owner_device.uuid)}
2096 )
2097
2098 # Assert that the response is a redirect to the login page
2099 self.assertRedirects(
2100 response,
2101 self._url_with_params(
2102 settings.ACCOUNT_LOGOUT_REDIRECT_URL,
2103 {"next": settings.LOGIN_REDIRECT_URL},
2104 ),
2105 )
2106
2107 def test_home_view_post_as_device_owner(self):
2108 """Test HTTP POST method to home view as device owner."""
2109 response = self.device_owner_client.post(
2110 self.home_url, data={"uuid": str(self.device_owner_device.uuid)}
2111 )
2112
2113 # Assert that the response is a redirect to the login page
2114
2115 self.assertRedirects(
2116 response,
2117 self._url_with_params(
2118 settings.ACCOUNT_LOGOUT_REDIRECT_URL,
2119 {"next": settings.LOGIN_REDIRECT_URL},
2120 ),
2121 )
2122
Mitja Nikolausded30ae2018-09-14 15:40:08 +02002123 def test_get_home_view(self):
2124 """Test getting the home view with device search form."""
2125 response = self.fp_staff_client.get(self.home_url)
2126 self.assertEqual(response.status_code, status.HTTP_200_OK)
2127 self.assertTemplateUsed(
2128 response, "crashreport_stats/home.html", count=1
2129 )
2130 self.assertEqual(response.context["devices"], None)
2131
2132 def test_home_view_filter_devices_by_uuid(self):
2133 """Test filtering devices by UUID."""
2134 # Create a device
2135 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
2136
2137 # Filter devices by UUID of the created device
2138 response = self.fp_staff_client.post(
2139 self.home_url, data={"uuid": str(device.uuid)}
2140 )
2141
2142 # Assert that the the client is redirected to the device page
2143 self.assertRedirects(
2144 response,
2145 self._url_with_params(self.device_url, {"uuid": device.uuid}),
2146 )
2147
2148 def test_home_view_filter_devices_by_uuid_part(self):
2149 """Test filtering devices by start of UUID."""
2150 # Create a device
2151 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
2152
2153 # Filter devices with start of the created device's UUID
2154 response = self.fp_staff_client.post(
2155 self.home_url, data={"uuid": str(device.uuid)[:4]}
2156 )
2157
2158 # Assert that the the client is redirected to the device page
2159 self.assertRedirects(
2160 response,
2161 self._url_with_params(self.device_url, {"uuid": device.uuid}),
2162 )
2163
2164 def test_home_view_filter_devices_by_uuid_part_ambiguous_result(self):
2165 """Test filtering devices with common start of UUIDs."""
2166 # Create two devices
2167 device1 = Dummy.create_dummy_device(Dummy.create_dummy_user())
2168 device2 = Dummy.create_dummy_device(
2169 Dummy.create_dummy_user(username=Dummy.USERNAMES[1])
2170 )
2171
2172 # Adapt the devices' UUID so that they start with the same characters
2173 device1.uuid = "4060fd90-6de1-4b03-a380-4277c703e913"
2174 device1.save()
2175 device2.uuid = "4061c59b-823d-4ec6-a463-8ac0c1cea67d"
2176 device2.save()
2177
2178 # Filter devices with first three (common) characters of the UUID
2179 response = self.fp_staff_client.post(
2180 self.home_url, data={"uuid": str(device1.uuid)[:3]}
2181 )
2182
2183 # Assert that both devices are part of the result
2184 self.assertEqual(response.status_code, status.HTTP_200_OK)
2185 self.assertTemplateUsed(
2186 response, "crashreport_stats/home.html", count=1
2187 )
2188 self.assertEqual(set(response.context["devices"]), {device1, device2})
2189
2190 def test_home_view_filter_devices_empty_database(self):
2191 """Test filtering devices on an empty database."""
2192 response = self.fp_staff_client.post(
2193 self.home_url, data={"uuid": "TestUUID"}
2194 )
2195 self.assertEqual(response.status_code, status.HTTP_200_OK)
2196 self.assertIsNotNone(response.content)
2197
2198 def test_home_view_filter_devices_no_uuid(self):
2199 """Test filtering devices without specifying UUID."""
2200 response = self.fp_staff_client.post(self.home_url)
2201 self.assertEqual(response.status_code, status.HTTP_200_OK)
2202 self.assertTemplateUsed(
2203 response, "crashreport_stats/home.html", count=1
2204 )
2205 self.assertEqual(response.context["devices"], None)
2206
2207 def test_get_device_view_empty_database(self):
2208 """Test getting device view on an empty database."""
2209 response = self.fp_staff_client.get(self.device_url)
2210 self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
2211
2212 def test_get_device_view(self):
2213 """Test getting device view."""
2214 # Create a device
2215 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
2216
2217 # Get the corresponding device view
2218 response = self._get_with_params(self.device_url, {"uuid": device.uuid})
2219
2220 # Assert that the view is constructed from the correct templates and
2221 # the response context contains the device UUID
2222 self.assertEqual(response.status_code, status.HTTP_200_OK)
2223 self.assertTemplateUsed(
2224 response, "crashreport_stats/device.html", count=1
2225 )
2226 self.assertTemplateUsed(
2227 response, "crashreport_stats/tags/device_overview.html", count=1
2228 )
2229 self.assertTemplateUsed(
2230 response,
2231 "crashreport_stats/tags/device_update_history.html",
2232 count=1,
2233 )
2234 self.assertTemplateUsed(
2235 response,
2236 "crashreport_stats/tags/device_report_history.html",
2237 count=1,
2238 )
2239 self.assertTemplateUsed(
2240 response,
2241 "crashreport_stats/tags/device_crashreport_table.html",
2242 count=1,
2243 )
2244 self.assertEqual(response.context["uuid"], str(device.uuid))
2245
2246 def _assert_versions_view_templates_are_used(self, response):
2247 self.assertTemplateUsed(
2248 response, "crashreport_stats/versions.html", count=1
2249 )
2250 self.assertTemplateUsed(
2251 response, "crashreport_stats/tags/versions_table.html", count=1
2252 )
2253 self.assertTemplateUsed(
2254 response, "crashreport_stats/tags/versions_pie_chart.html", count=1
2255 )
2256 self.assertTemplateUsed(
2257 response, "crashreport_stats/tags/versions_bar_chart.html", count=1
2258 )
2259 self.assertTemplateUsed(
2260 response, "crashreport_stats/tags/versions_area_chart.html", count=1
2261 )
2262
2263 @unittest.skip("Fails because of wrong boolean usage in views.py")
2264 def test_get_versions_view_empty_database(self):
2265 """Test getting versions view on an empty database."""
2266 response = self.fp_staff_client.get(self.versions_url)
2267
2268 # Assert that the correct templates are used and the response context
2269 # contains the correct value for is_official_release
2270 self._assert_versions_view_templates_are_used(response)
2271 self.assertEqual(response.context["is_official_release"], True)
2272
2273 @unittest.skip("Fails because of wrong boolean usage in views.py")
2274 def test_get_versions_view(self):
2275 """Test getting versions view."""
2276 # Create a version
2277 Dummy.create_dummy_version()
2278
2279 # Get the versions view
2280 response = self.fp_staff_client.get(self.versions_url)
2281
2282 # Assert that the correct templates are used and the response context
2283 # contains the correct value for is_official_release
2284 self._assert_versions_view_templates_are_used(response)
2285 self.assertEqual(response.context["is_official_release"], True)
2286
2287 @unittest.skip("Fails because of wrong boolean usage in views.py")
2288 def test_get_versions_all_view_no_versions(self):
2289 """Test getting versions all view on an empty database."""
2290 response = self.fp_staff_client.get(self.versions_all_url)
2291
2292 # Assert that the correct templates are used and the response context
2293 # contains an empty value for is_official_release
2294 self._assert_versions_view_templates_are_used(response)
2295 self.assertEqual(response.context.get("is_official_release", ""), "")
2296
2297 @unittest.skip("Fails because of wrong boolean usage in views.py")
2298 def test_get_versions_all_view(self):
2299 """Test getting versions view."""
2300 # Create a version
2301 Dummy.create_dummy_version()
2302
2303 # Get the versions view
2304 response = self.fp_staff_client.get(self.versions_all_url)
2305
2306 # Assert that the correct templates are used and the response context
2307 # contains the an empty value for is_official_release
2308 self._assert_versions_view_templates_are_used(response)
2309 self.assertEqual(response.context.get("is_official_release", ""), "")