Disallow duplicate heartbeats and crashreports

Add unique constraints and corresponding schema and data migration.
Adapt all test cases so that only unique heartbeats and crashreports
are sent. Delete test cases that are inappropriate as no duplicate
entries can exist in the database anymore.

Issue: HIC-180
Change-Id: I768d1610d4482c9d61b76cdbc588334198bfe415
diff --git a/crashreport_stats/management/commands/stats.py b/crashreport_stats/management/commands/stats.py
index d510a0e..c2cc56c 100644
--- a/crashreport_stats/management/commands/stats.py
+++ b/crashreport_stats/management/commands/stats.py
@@ -256,7 +256,7 @@
         return (
             query_objects.annotate(_report_day=TruncDate("date"))
             .values(self.version_field_name, "_report_day")
-            .annotate(count=Count("date", distinct=True))
+            .annotate(count=Count("date"))
         )
 
     def delete_stats(self) -> Dict[str, int]:
diff --git a/crashreport_stats/rest_endpoints.py b/crashreport_stats/rest_endpoints.py
index ba8ea9f..a15edf5 100644
--- a/crashreport_stats/rest_endpoints.py
+++ b/crashreport_stats/rest_endpoints.py
@@ -189,7 +189,7 @@
         device_heartbeats = list(device.heartbeats.all())
         device_crashreports = list(device.crashreports.all())
 
-        dates = {heartbeat.date.date() for heartbeat in device_heartbeats}
+        dates = {heartbeat.date for heartbeat in device_heartbeats}
 
         response = [
             get_stats_for_date(date, device_crashreports, device_heartbeats)
@@ -201,7 +201,7 @@
 
 def get_stats_for_date(date, crashreports, heartbeats):
     """Get the stats for a device for a specific date."""
-    heartbeats = filter_instances(heartbeats, lambda hb: hb.date.date() == date)
+    heartbeats = filter_instances(heartbeats, lambda hb: hb.date == date)
     crashreports = filter_instances(
         crashreports, lambda c: c.date.date() == date
     )
diff --git a/crashreport_stats/tests/test_rest_endpoints.py b/crashreport_stats/tests/test_rest_endpoints.py
index a3bd2d1..8708f3d 100644
--- a/crashreport_stats/tests/test_rest_endpoints.py
+++ b/crashreport_stats/tests/test_rest_endpoints.py
@@ -1,7 +1,6 @@
 """Tests for the rest_endpoints module."""
 import operator
 from datetime import datetime, timedelta
-import unittest
 
 import pytz
 from django.test import override_settings
@@ -585,15 +584,22 @@
         """Test getting device stats for a device."""
         # Create a device with a heartbeat and one report of each type
         device = Dummy.create_dummy_device(Dummy.create_dummy_user())
-        heartbeat = Dummy.create_dummy_report(HeartBeat, device)
+        crashreport_date = Dummy.DEFAULT_DUMMY_CRASHREPORT_VALUES["date"]
+        heartbeat = Dummy.create_dummy_report(
+            HeartBeat, device, date=crashreport_date.date()
+        )
         for boot_reason in (
             Crashreport.SMPL_BOOT_REASONS
             + Crashreport.CRASH_BOOT_REASONS
             + ["other boot reason"]
         ):
             Dummy.create_dummy_report(
-                Crashreport, device, boot_reason=boot_reason
+                Crashreport,
+                device,
+                boot_reason=boot_reason,
+                date=crashreport_date,
             )
+            crashreport_date += timedelta(milliseconds=1)
 
         # Get the device statistics
         response = self._get_with_params(
@@ -619,15 +625,15 @@
         device = Dummy.create_dummy_device(Dummy.create_dummy_user())
         num_days = 100
         for i in range(num_days):
-            report_day = datetime.now(tz=pytz.utc) + timedelta(days=i)
+            report_date = datetime.now(tz=pytz.utc) + timedelta(days=i)
             heartbeat = Dummy.create_dummy_report(
-                HeartBeat, device, date=report_day
+                HeartBeat, device, date=report_date.date()
             )
-            Dummy.create_dummy_report(Crashreport, device, date=report_day)
+            Dummy.create_dummy_report(Crashreport, device, date=report_date)
             Dummy.create_dummy_report(
                 Crashreport,
                 device,
-                date=report_day,
+                date=report_date + timedelta(minutes=1),
                 boot_reason=Crashreport.SMPL_BOOT_REASONS[0],
             )
 
@@ -656,13 +662,13 @@
         num_days = 100
         skip_day = round(num_days / 2)
         for i in range(num_days):
-            report_day = datetime.now(tz=pytz.utc) + timedelta(days=i)
+            report_date = datetime.now(tz=pytz.utc) + timedelta(days=i)
             # Skip creation of heartbeat at one day
             if i != skip_day:
                 heartbeat = Dummy.create_dummy_report(
-                    HeartBeat, device, date=report_day
+                    HeartBeat, device, date=report_date.date()
                 )
-            Dummy.create_dummy_report(Crashreport, device, date=report_day)
+            Dummy.create_dummy_report(Crashreport, device, date=report_date)
 
         # Get the device statistics
         response = self._get_with_params(
@@ -682,48 +688,6 @@
             last_active=heartbeat.date,
         )
 
-    @unittest.skip("Duplicate heartbeats are currently not dropped.")
-    def test_get_device_stats_multiple_days_duplicate_heartbeat(self):
-        """Test getting device stats for a device with duplicate heartbeat.
-
-        Duplicate heartbeats are dropped and thus should not influence the
-        statistics.
-        """
-        # Create a device with some heartbeats and reports over time
-        device = Dummy.create_dummy_device(Dummy.create_dummy_user())
-        num_days = 100
-        duplicate_day = round(num_days / 2)
-        first_report_day = Dummy.DEFAULT_DUMMY_HEARTBEAT_VALUES["date"]
-        for i in range(num_days):
-            report_day = first_report_day + timedelta(days=i)
-            heartbeat = Dummy.create_dummy_report(
-                HeartBeat, device, date=report_day
-            )
-            # Create a second at the duplicate day (with 1 hour delay)
-            if i == duplicate_day:
-                Dummy.create_dummy_report(
-                    HeartBeat, device, date=report_day + timedelta(hours=1)
-                )
-            Dummy.create_dummy_report(Crashreport, device, date=report_day)
-
-        # Get the device statistics
-        response = self._get_with_params(
-            self.device_overview_url, {"uuid": device.uuid}
-        )
-
-        # Assert that the statistics match
-        self._assert_device_stats_response_is(
-            response=response,
-            uuid=str(device.uuid),
-            board_date=device.board_date,
-            num_heartbeats=num_days,
-            num_crashreports=num_days,
-            num_smpls=0,
-            crashes_per_day=1,
-            smpl_per_day=0,
-            last_active=heartbeat.date,
-        )
-
     def test_get_device_report_history_no_reports(self):
         """Test getting report history stats for a device without reports."""
         # Create a device
@@ -741,15 +705,22 @@
         """Test getting report history stats for a device."""
         # Create a device with a heartbeat and one report of each type
         device = Dummy.create_dummy_device(Dummy.create_dummy_user())
-        heartbeat = Dummy.create_dummy_report(HeartBeat, device)
+        crashreport_date = Dummy.DEFAULT_DUMMY_CRASHREPORT_VALUES["date"]
+        heartbeat = Dummy.create_dummy_report(
+            HeartBeat, device, date=crashreport_date.date()
+        )
         for boot_reason in (
             Crashreport.SMPL_BOOT_REASONS
             + Crashreport.CRASH_BOOT_REASONS
             + ["other boot reason"]
         ):
             Dummy.create_dummy_report(
-                Crashreport, device, boot_reason=boot_reason
+                Crashreport,
+                device,
+                boot_reason=boot_reason,
+                date=crashreport_date,
             )
+            crashreport_date += timedelta(milliseconds=1)
 
         # Get the device report history statistics
         response = self._get_with_params(
@@ -759,7 +730,7 @@
         # Assert that the statistics match
         report_history = [
             {
-                "date": heartbeat.date.date(),
+                "date": heartbeat.date,
                 "heartbeats": 1,
                 "smpl": len(Crashreport.SMPL_BOOT_REASONS),
                 "prob_crashes": len(Crashreport.CRASH_BOOT_REASONS),
@@ -779,8 +750,10 @@
         for _ in range(10):
             report_date = report_date + timedelta(days=1)
 
-            Dummy.create_dummy_report(HeartBeat, device, date=report_date)
-            for boot_reason in (
+            Dummy.create_dummy_report(
+                HeartBeat, device, date=report_date.date()
+            )
+            for i, boot_reason in enumerate(
                 Crashreport.SMPL_BOOT_REASONS
                 + Crashreport.CRASH_BOOT_REASONS
                 + ["other boot reason"]
@@ -789,7 +762,7 @@
                     Crashreport,
                     device,
                     boot_reason=boot_reason,
-                    date=report_date,
+                    date=report_date + timedelta(milliseconds=i),
                 )
 
             # Create the expected report history object
@@ -831,14 +804,18 @@
         """Test getting update history stats for a device."""
         # Create a device with a heartbeat and one report of each type
         device = Dummy.create_dummy_device(Dummy.create_dummy_user())
-        heartbeat = Dummy.create_dummy_report(HeartBeat, device)
+        crashreport_date = Dummy.DEFAULT_DUMMY_CRASHREPORT_VALUES["date"]
+        heartbeat = Dummy.create_dummy_report(
+            HeartBeat, device, date=crashreport_date.date()
+        )
         for boot_reason in (
             Crashreport.SMPL_BOOT_REASONS
             + Crashreport.CRASH_BOOT_REASONS
             + ["other boot reason"]
         ):
-            params = {"boot_reason": boot_reason}
+            params = {"boot_reason": boot_reason, "date": crashreport_date}
             Dummy.create_dummy_report(Crashreport, device, **params)
+            crashreport_date += timedelta(milliseconds=1)
 
         # Get the device update history statistics
         response = self._get_with_params(
@@ -866,24 +843,24 @@
         device = Dummy.create_dummy_device(Dummy.create_dummy_user())
         expected_update_history = []
         for i, build_fingerprint in enumerate(Dummy.BUILD_FINGERPRINTS):
-            report_day = datetime.now(tz=pytz.utc) + timedelta(days=i)
+            report_date = datetime.now(tz=pytz.utc) + timedelta(days=i)
             Dummy.create_dummy_report(
                 HeartBeat,
                 device,
-                date=report_day,
+                date=report_date,
                 build_fingerprint=build_fingerprint,
             )
             Dummy.create_dummy_report(
                 Crashreport,
                 device,
-                date=report_day,
+                date=report_date,
                 build_fingerprint=build_fingerprint,
             )
 
             # Create the expected update history object
             expected_update_history.append(
                 {
-                    "update_date": report_day,
+                    "update_date": report_date.date(),
                     "build_fingerprint": build_fingerprint,
                     "max": device.id,
                     "prob_crashes": 1,
diff --git a/crashreport_stats/tests/test_stats_management_command.py b/crashreport_stats/tests/test_stats_management_command.py
index 136e2ef..6b2745e 100644
--- a/crashreport_stats/tests/test_stats_management_command.py
+++ b/crashreport_stats/tests/test_stats_management_command.py
@@ -1,5 +1,4 @@
 """Tests for the stats management command module."""
-
 from io import StringIO
 from datetime import datetime, timedelta
 import unittest
@@ -38,13 +37,14 @@
         self, report_type, unique_entry_name, device, number, **kwargs
     ):
         # Create reports with distinct timestamps
-        now = datetime.now(pytz.utc)
+        report_date = datetime.now(pytz.utc)
+        if report_type == HeartBeat:
+            report_date = report_date.date()
         for i in range(number):
-            report_date = now - timedelta(milliseconds=i)
             report_attributes = {
                 self.unique_entry_name: unique_entry_name,
                 "device": device,
-                "date": report_date,
+                "date": report_date - timedelta(days=i),
             }
             report_attributes.update(**kwargs)
             Dummy.create_dummy_report(report_type, **report_attributes)
@@ -86,12 +86,15 @@
         }
         version = self.version_class.objects.get(**get_params)
 
-        self.assertEqual(report.date.date(), version.first_seen_on)
+        report_date = (
+            report.date.date() if report_type == Crashreport else report.date
+        )
+        self.assertEqual(report_date, version.first_seen_on)
 
         # Create a new report from an earlier point in time
-        report_time_2 = report.date - timedelta(weeks=1)
+        report_date_2 = report.date - timedelta(weeks=1)
         Dummy.create_dummy_report(
-            report_type, device=device, date=report_time_2
+            report_type, device=device, date=report_date_2
         )
 
         # Run the command to update the database
@@ -101,7 +104,9 @@
         version = self.version_class.objects.get(**get_params)
 
         # Validate that the date matches the report recently sent
-        self.assertEqual(report_time_2.date(), version.first_seen_on)
+        if report_type == Crashreport:
+            report_date_2 = report_date_2.date()
+        self.assertEqual(report_date_2, version.first_seen_on)
 
     def test_older_heartbeat_updates_version_date(self):
         """Validate updating version date with older heartbeats."""
@@ -114,9 +119,9 @@
     def test_entries_are_unique(self):
         """Validate the entries' unicity and value."""
         # Create some reports
-        user = Dummy.create_dummy_user()
-        device = Dummy.create_dummy_device(user=user)
-        for unique_entry in self.unique_entries:
+        for unique_entry, username in zip(self.unique_entries, Dummy.USERNAMES):
+            user = Dummy.create_dummy_user(username=username)
+            device = Dummy.create_dummy_device(user=user)
             self._create_reports(HeartBeat, unique_entry, device, 10)
 
         # Run the command to update the database
@@ -142,9 +147,11 @@
                 "({} != {})".format(len(numbers), len(self.unique_entries))
             )
         # Create some reports
-        user = Dummy.create_dummy_user()
-        device = Dummy.create_dummy_device(user=user)
-        for unique_entry, num in zip(self.unique_entries, numbers):
+        for unique_entry, num, username in zip(
+            self.unique_entries, numbers, Dummy.USERNAMES
+        ):
+            user = Dummy.create_dummy_user(username=username)
+            device = Dummy.create_dummy_device(user=user)
             self._create_reports(
                 report_type, unique_entry, device, num, **kwargs
             )
@@ -334,20 +341,24 @@
     def _assert_updating_twice_gives_correct_counters(
         self, report_type, counter_attribute_name, **boot_reason_param
     ):
-        # Create a device and a corresponding reports for 2 different versions
-        device = Dummy.create_dummy_device(Dummy.create_dummy_user())
+        # Create a two devices and a corresponding reports for 2 different
+        # versions
+        device_1 = Dummy.create_dummy_device(Dummy.create_dummy_user())
         num_reports = 5
         self._create_reports(
             report_type,
             self.unique_entries[0],
-            device,
+            device_1,
             num_reports,
             **boot_reason_param
         )
+        device_2 = Dummy.create_dummy_device(
+            Dummy.create_dummy_user(username=Dummy.USERNAMES[1])
+        )
         self._create_reports(
             report_type,
             self.unique_entries[1],
-            device,
+            device_2,
             num_reports,
             **boot_reason_param
         )
@@ -372,7 +383,7 @@
         # Create another report for the first version
         report_new_attributes = {
             self.unique_entry_name: self.unique_entries[0],
-            "device": device,
+            "device": device_1,
             **boot_reason_param,
         }
         Dummy.create_dummy_report(report_type, **report_new_attributes)
@@ -520,74 +531,6 @@
             Crashreport, counter_attribute_name, **params
         )
 
-    def _assert_duplicates_are_ignored(
-        self, report_type, device, counter_attribute_name, **kwargs
-    ):
-        """Validate that reports with duplicate timestamps are ignored."""
-        # Create a report
-        report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
-
-        # Create a second report with the same timestamp
-        Dummy.create_dummy_report(
-            report_type, device=device, date=report.date, **kwargs
-        )
-
-        # Run the command to update the database
-        call_command("stats", "update")
-
-        # Get the corresponding version instance from the database
-        get_params = {
-            self.unique_entry_name: getattr(report, self.unique_entry_name)
-        }
-        version = self.version_class.objects.get(**get_params)
-
-        # Assert that the report with the duplicate timestamp is not
-        # counted, i.e. only 1 report is counted.
-        self.assertEqual(getattr(version, counter_attribute_name), 1)
-
-    def test_heartbeat_duplicates_are_ignored(self):
-        """Validate that heartbeat duplicates are ignored."""
-        counter_attribute_name = "heartbeats"
-        device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
-        self._assert_duplicates_are_ignored(
-            HeartBeat, device, counter_attribute_name
-        )
-
-    def test_crash_report_duplicates_are_ignored(self):
-        """Validate that crash report duplicates are ignored."""
-        counter_attribute_name = "prob_crashes"
-        device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
-        for i, boot_reason in enumerate(Crashreport.CRASH_BOOT_REASONS):
-            params = {
-                "boot_reason": boot_reason,
-                self.unique_entry_name: self.unique_entries[i],
-            }
-            self._assert_duplicates_are_ignored(
-                Crashreport, device, counter_attribute_name, **params
-            )
-
-    def test_smpl_report_duplicates_are_ignored(self):
-        """Validate that smpl report duplicates are ignored."""
-        counter_attribute_name = "smpl"
-        device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
-        for i, boot_reason in enumerate(Crashreport.SMPL_BOOT_REASONS):
-            params = {
-                "boot_reason": boot_reason,
-                self.unique_entry_name: self.unique_entries[i],
-            }
-            self._assert_duplicates_are_ignored(
-                Crashreport, device, counter_attribute_name, **params
-            )
-
-    def test_other_report_duplicates_are_ignored(self):
-        """Validate that other report duplicates are ignored."""
-        counter_attribute_name = "other"
-        params = {"boot_reason": "random boot reason"}
-        device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
-        self._assert_duplicates_are_ignored(
-            Crashreport, device, counter_attribute_name, **params
-        )
-
     def _assert_older_reports_update_released_on_date(
         self, report_type, **kwargs
     ):
@@ -609,7 +552,10 @@
         )
 
         # Assert that the released_on date matches the first report date
-        self.assertEqual(version.released_on, report.date.date())
+        report_date = (
+            report.date.date() if report_type == Crashreport else report.date
+        )
+        self.assertEqual(version.released_on, report_date)
 
         # Create a second report with the a timestamp earlier in time
         report_2_date = report.date - timedelta(days=1)
@@ -626,7 +572,9 @@
         )
 
         # Assert that the released_on date matches the older report date
-        self.assertEqual(version.released_on, report_2_date.date())
+        if report_type == Crashreport:
+            report_2_date = report_2_date.date()
+        self.assertEqual(version.released_on, report_2_date)
 
     def _assert_newer_reports_do_not_update_released_on_date(
         self, report_type, **kwargs
@@ -639,7 +587,9 @@
         # Create a report
         device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
         report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
-        report_1_date = report.date.date()
+        report_1_date = (
+            report.date.date() if report_type == Crashreport else report.date
+        )
 
         # Run the command to update the database
         call_command("stats", "update")
@@ -706,7 +656,10 @@
         )
 
         # Assert that the released_on date matches the first report date
-        self.assertEqual(version.released_on, report.date.date())
+        report_date = (
+            report.date.date() if report_type == Crashreport else report.date
+        )
+        self.assertEqual(version.released_on, report_date)
 
         # Create a second report with a timestamp earlier in time
         report_2_date = report.date - timedelta(days=1)
@@ -715,7 +668,7 @@
         )
 
         # Manually change the released_on date
-        version_release_date = report.date + timedelta(days=1)
+        version_release_date = report_date + timedelta(days=1)
         version.released_on = version_release_date
         version.save()
 
@@ -729,7 +682,7 @@
 
         # Assert that the released_on date still matches the date is was
         # manually changed to
-        self.assertEqual(version.released_on, version_release_date.date())
+        self.assertEqual(version.released_on, version_release_date)
 
     def test_manually_changed_released_on_date_is_not_updated_by_heartbeat(
         self