|
1 | 1 | # FORK: tests for the tracking parity endpoints — episode watch/drop, |
2 | 2 | # tag management, and the history timeline. |
3 | 3 | import datetime |
| 4 | +from decimal import Decimal |
4 | 5 | from http import HTTPStatus as HTTP # noqa: N814 |
5 | 6 | from unittest.mock import patch |
6 | 7 |
|
@@ -103,6 +104,55 @@ def test_watch_invalid_date_rejected(self): |
103 | 104 | response = self._watch(1, payload={"end_date": "not-a-date"}) |
104 | 105 | self.assertEqual(response.status_code, HTTP.BAD_REQUEST) |
105 | 106 |
|
| 107 | + @patch( |
| 108 | + "app.models.providers.services.get_media_metadata", |
| 109 | + side_effect=_season_metadata_side_effect, |
| 110 | + ) |
| 111 | + def test_watch_with_score_sets_episode_score(self, _mock): |
| 112 | + """POST watch with a score sets it on all plays of the episode.""" |
| 113 | + response = self._watch(1, payload={"score": "8.3"}) |
| 114 | + self.assertEqual(response.status_code, HTTP.CREATED) |
| 115 | + self.assertEqual(response.json()["score"], 8.3) |
| 116 | + scores = set( |
| 117 | + Episode.objects.filter( |
| 118 | + related_season=self.season_medias[0], |
| 119 | + item__episode_number=1, |
| 120 | + ).values_list("score", flat=True), |
| 121 | + ) |
| 122 | + self.assertEqual(scores, {Decimal("8.3")}) |
| 123 | + |
| 124 | + @patch( |
| 125 | + "app.models.providers.services.get_media_metadata", |
| 126 | + side_effect=_season_metadata_side_effect, |
| 127 | + ) |
| 128 | + def test_watch_without_score_leaves_score_unset(self, _mock): |
| 129 | + """POST watch without a score field doesn't touch the score.""" |
| 130 | + response = self._watch(1) |
| 131 | + self.assertEqual(response.status_code, HTTP.CREATED) |
| 132 | + self.assertIsNone(response.json()["score"]) |
| 133 | + |
| 134 | + @patch( |
| 135 | + "app.models.providers.services.get_media_metadata", |
| 136 | + side_effect=_season_metadata_side_effect, |
| 137 | + ) |
| 138 | + def test_watch_invalid_score_rejected(self, _mock): |
| 139 | + """POST watch with an out-of-range score 400s and creates no play.""" |
| 140 | + play_count_before = Episode.objects.filter( |
| 141 | + related_season=self.season_medias[0], |
| 142 | + item__episode_number=1, |
| 143 | + ).count() |
| 144 | + |
| 145 | + response = self._watch(1, payload={"score": "11"}) |
| 146 | + |
| 147 | + self.assertEqual(response.status_code, HTTP.BAD_REQUEST) |
| 148 | + self.assertEqual( |
| 149 | + Episode.objects.filter( |
| 150 | + related_season=self.season_medias[0], |
| 151 | + item__episode_number=1, |
| 152 | + ).count(), |
| 153 | + play_count_before, |
| 154 | + ) |
| 155 | + |
106 | 156 | @patch( |
107 | 157 | "app.models.providers.services.get_media_metadata", |
108 | 158 | side_effect=_season_metadata_side_effect, |
@@ -562,6 +612,45 @@ def test_history_returns_days(self): |
562 | 612 | any(entry["media_type"] == "movie" for entry in all_entries), |
563 | 613 | ) |
564 | 614 |
|
| 615 | + def test_history_day_entries_are_capped(self): |
| 616 | + """A day with many plays doesn't blow up the response body (#1004). |
| 617 | +
|
| 618 | + `limit`/`offset` on this endpoint paginate over DAYS, not entries, so |
| 619 | + a single busy day (imports, binge sessions, frequent podcast |
| 620 | + scrobbles) must still be bounded — mirrors the web history page's |
| 621 | + existing per-day cap (HISTORY_ENTRIES_PER_DAY_PAGE). |
| 622 | + """ |
| 623 | + entry_count = history_cache.HISTORY_ENTRIES_PER_DAY_PAGE + 5 |
| 624 | + same_day = datetime.datetime(2024, 6, 1, tzinfo=datetime.UTC) |
| 625 | + for index in range(entry_count): |
| 626 | + item = Item.objects.create( |
| 627 | + media_id=f"history-cap-movie-{index}", |
| 628 | + source=Sources.TMDB.value, |
| 629 | + media_type=MediaTypes.MOVIE.value, |
| 630 | + title=f"History Cap Movie {index}", |
| 631 | + ) |
| 632 | + Movie.objects.create( |
| 633 | + item=item, |
| 634 | + user=self.user1, |
| 635 | + end_date=same_day, |
| 636 | + ) |
| 637 | + cache.clear() |
| 638 | + |
| 639 | + response = self.call_api( |
| 640 | + "get", |
| 641 | + "api_history", |
| 642 | + params={"media_type": "movie", "limit": 1}, |
| 643 | + headers=self.auth_headers, |
| 644 | + ) |
| 645 | + |
| 646 | + self.assertEqual(response.status_code, HTTP.OK) |
| 647 | + days = response.json()["results"] |
| 648 | + self.assertEqual(len(days), 1) |
| 649 | + day = days[0] |
| 650 | + self.assertLessEqual(len(day["entries"]), history_cache.HISTORY_ENTRIES_PER_DAY_PAGE) |
| 651 | + self.assertEqual(day["entry_count"], entry_count) |
| 652 | + self.assertTrue(day["entries_truncated"]) |
| 653 | + |
565 | 654 | def test_history_flat_returns_paginated_entry_list(self): |
566 | 655 | """?flat=1 returns a flat, card-oriented entry list, not day buckets.""" |
567 | 656 | day_response = self.call_api( |
|
0 commit comments