Files
timetracker/tests/test_sorting.py
T

139 lines
5.0 KiB
Python

"""Tests for the list-view sorting system (games/sorting.py)."""
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
import pytest
from django.conf import settings
from django.test import RequestFactory
from games.filters import FindFilter
from games.models import Game, Platform, Purchase, Session
from games.sorting import (
GAME_DEFAULT_SORT,
GAME_SORTS,
PURCHASE_DEFAULT_SORT,
PURCHASE_SORTS,
SESSION_DEFAULT_SORT,
SESSION_SORTS,
SortSpec,
SortTerm,
apply_sort,
parse_find_filter,
parse_sort_terms,
)
ZONEINFO = ZoneInfo(settings.TIME_ZONE)
# A minimal map; parse_sort_terms only checks key membership, not spec internals.
SAMPLE_MAP = {"name": SortSpec("name"), "date": SortSpec("created_at")}
class TestParseSortTerms:
def test_bare_key_is_ascending(self):
parsed = parse_sort_terms("name", SAMPLE_MAP)
assert parsed.terms == [SortTerm("name", False)]
assert parsed.unknown == []
def test_dash_prefix_is_descending(self):
parsed = parse_sort_terms("-date", SAMPLE_MAP)
assert parsed.terms == [SortTerm("date", True)]
def test_multi_column_preserves_order(self):
parsed = parse_sort_terms("date,-name", SAMPLE_MAP)
assert parsed.terms == [SortTerm("date", False), SortTerm("name", True)]
def test_unknown_key_is_reported_not_raised(self):
parsed = parse_sort_terms("bogus", SAMPLE_MAP)
assert parsed.terms == []
assert parsed.unknown == ["bogus"]
def test_mixed_valid_and_unknown(self):
parsed = parse_sort_terms("-name,bogus", SAMPLE_MAP)
assert parsed.terms == [SortTerm("name", True)]
assert parsed.unknown == ["bogus"]
def test_whitespace_and_empty_tokens_ignored(self):
parsed = parse_sort_terms(" name , , -date ", SAMPLE_MAP)
assert parsed.terms == [SortTerm("name", False), SortTerm("date", True)]
def test_empty_string_yields_nothing(self):
parsed = parse_sort_terms("", SAMPLE_MAP)
assert parsed.terms == []
assert parsed.unknown == []
def _find(sort=None):
return FindFilter(sort=sort)
@pytest.fixture
def two_games(db):
platform = Platform.objects.create(name="P", icon="p")
alpha = Game.objects.create(name="Alpha", sort_name="Alpha", platform=platform)
beta = Game.objects.create(name="Beta", sort_name="Beta", platform=platform)
return alpha, beta
class TestApplySortGames:
def test_name_ascending(self, two_games):
alpha, beta = two_games
result = apply_sort(Game.objects.all(), _find("name"), GAME_SORTS, GAME_DEFAULT_SORT)
assert list(result.queryset) == [alpha, beta]
assert result.terms[0].key == "name"
assert result.unknown == []
def test_name_descending(self, two_games):
alpha, beta = two_games
result = apply_sort(Game.objects.all(), _find("-name"), GAME_SORTS, GAME_DEFAULT_SORT)
assert list(result.queryset) == [beta, alpha]
def test_default_sort_when_absent_is_created_desc(self, two_games):
alpha, beta = two_games # beta created after alpha
result = apply_sort(Game.objects.all(), _find(None), GAME_SORTS, GAME_DEFAULT_SORT)
assert list(result.queryset) == [beta, alpha]
def test_unknown_key_reported_and_falls_back(self, two_games):
result = apply_sort(Game.objects.all(), _find("bogus"), GAME_SORTS, GAME_DEFAULT_SORT)
assert result.unknown == ["bogus"]
assert result.queryset.count() == 2 # still returns rows (default order)
def test_playtime_annotation_no_duplicate_rows(self, two_games):
alpha, _ = two_games
Session.objects.create(
game=alpha,
timestamp_start=datetime(2022, 1, 1, 10, tzinfo=ZONEINFO),
timestamp_end=datetime(2022, 1, 1, 12, tzinfo=ZONEINFO),
)
Session.objects.create(
game=alpha,
timestamp_start=datetime(2022, 1, 2, 10, tzinfo=ZONEINFO),
timestamp_end=datetime(2022, 1, 2, 11, tzinfo=ZONEINFO),
)
result = apply_sort(Game.objects.all(), _find("-playtime"), GAME_SORTS, GAME_DEFAULT_SORT)
# two sessions on alpha must not duplicate the alpha row
assert result.queryset.count() == 2
assert list(result.queryset)[0] == alpha # most playtime first
class TestParseFindFilter:
def test_reads_sort_param(self):
request = RequestFactory().get("/x", {"sort": "-playtime,name"})
assert parse_find_filter(request).sort == "-playtime,name"
def test_absent_sort_is_none(self):
request = RequestFactory().get("/x")
assert parse_find_filter(request).sort is None
class TestSortMapShapes:
def test_default_sort_keys_exist_in_maps(self):
# every key referenced by a default sort string must be defined in its map
for default, sort_map in [
(GAME_DEFAULT_SORT, GAME_SORTS),
(SESSION_DEFAULT_SORT, SESSION_SORTS),
(PURCHASE_DEFAULT_SORT, PURCHASE_SORTS),
]:
for token in default.split(","):
assert token.lstrip("-") in sort_map