from unittest import mock import httpx from django.core.cache import cache from django.test import TestCase, override_settings from accounts.models import CustomUser from events.models import Event from .models import EventGame, Game from .services.steam_store import refresh_steam_games from .tasks import ( STEAM_REFRESH_GUARD_KEY, refresh_steam_games_task, schedule_steam_refresh, ) LOCMEM_CACHE = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}} def steam_response(app_id, data, success=True): return httpx.Response( 200, json={str(app_id): {"success": success, "data": data}}, request=httpx.Request("GET", "https://store.steampowered.com"), ) class RefreshSteamGamesTests(TestCase): def setUp(self): self.game = Game.objects.create(name="Noita", path="noita", steam_app_id=881100) @mock.patch("games.services.steam_store.httpx.get") def test_stores_price_and_discount(self, get): get.return_value = steam_response( 881100, { "is_free": False, "short_description": "Every pixel is simulated.", "header_image": "https://example.com/header.jpg", "price_overview": { "currency": "CHF", "initial": 1950, "final": 780, "discount_percent": 60, }, }, ) refresh_steam_games() self.game.refresh_from_db() self.assertEqual(self.game.steam_price_initial, 1950) self.assertEqual(self.game.steam_price_final, 780) self.assertEqual(self.game.steam_discount_percent, 60) self.assertEqual(self.game.steam_currency, "CHF") self.assertEqual(self.game.steam_short_description, "Every pixel is simulated.") self.assertIsNotNone(self.game.steam_info_updated_at) @mock.patch("games.services.steam_store.httpx.get") def test_free_game_has_no_price(self, get): get.return_value = steam_response(881100, {"is_free": True}) refresh_steam_games() self.game.refresh_from_db() self.assertTrue(self.game.steam_is_free) self.assertIsNone(self.game.steam_price_final) @mock.patch("games.services.steam_store.httpx.get") def test_failure_keeps_previous_info(self, get): Game.objects.filter(pk=self.game.pk).update(steam_price_final=999) get.side_effect = httpx.ConnectError("down") refresh_steam_games() self.game.refresh_from_db() self.assertEqual(self.game.steam_price_final, 999) @override_settings(CACHES=LOCMEM_CACHE) class ScheduleSteamRefreshTests(TestCase): def setUp(self): cache.clear() @mock.patch("games.tasks.refresh_steam_games_task") def test_only_one_loop_is_scheduled(self, task): self.assertTrue(schedule_steam_refresh()) self.assertFalse(schedule_steam_refresh()) task.enqueue.assert_called_once_with(loop_id=cache.get(STEAM_REFRESH_GUARD_KEY)) @mock.patch("games.tasks.refresh_steam_games") def test_task_reschedules_itself_even_on_failure(self, refresh): cache.set(STEAM_REFRESH_GUARD_KEY, "loop") refresh.side_effect = RuntimeError("boom") with mock.patch("games.tasks.refresh_steam_games_task") as patched: with self.assertRaises(RuntimeError): refresh_steam_games_task.call(loop_id="loop") patched.using.return_value.enqueue.assert_called_once_with(loop_id="loop") @mock.patch("games.tasks.refresh_steam_games") def test_superseded_loop_stops(self, refresh): cache.set(STEAM_REFRESH_GUARD_KEY, "new-loop") with mock.patch("games.tasks.refresh_steam_games_task") as patched: refresh_steam_games_task.call(loop_id="old-loop") refresh.assert_not_called() patched.using.assert_not_called() self.assertEqual(cache.get(STEAM_REFRESH_GUARD_KEY), "new-loop") @mock.patch("games.tasks.refresh_steam_games") def test_one_off_run_does_not_reschedule(self, refresh): with mock.patch("games.tasks.refresh_steam_games_task") as patched: refresh_steam_games_task.call(game_ids=[1]) refresh.assert_called_once_with([1]) patched.using.assert_not_called() @override_settings(CACHES=LOCMEM_CACHE) class EventSteamGamesApiTests(TestCase): def setUp(self): self.event = Event.objects.create(name="Event", path="event") noita = Game.objects.create( name="Noita", path="noita", steam_app_id=881100, steam_price_final=780 ) clock = Game.objects.create(name="Clock", path="clock", custom_path=True) EventGame.objects.create(event=self.event, game=noita) EventGame.objects.create(event=self.event, game=clock) user = CustomUser.objects.create_user(username="user", password="x") self.client.force_login(user) @mock.patch("games.api.schedule_steam_refresh") def test_lists_only_steam_games_with_store_info(self, schedule): response = self.client.get("/api/events/event/games/steam") self.assertEqual(response.status_code, 200) [game] = response.json() self.assertEqual(game["name"], "Noita") self.assertEqual(game["price_final"], 780) self.assertEqual(game["store_url"], "https://store.steampowered.com/app/881100") schedule.assert_called_once() class EventGamesDescriptionTests(TestCase): def setUp(self): self.event = Event.objects.create(name="Event", path="event") self.user = CustomUser.objects.create_user(username="user", password="x") self.client.force_login(self.user) def test_steam_description_is_fallback_for_admin_one(self): steam = Game.objects.create( name="Noita", path="noita", steam_app_id=881100, steam_short_description="From Steam", ) custom = Game.objects.create( name="Opus", path="opus", steam_app_id=558990, description="From admin", steam_short_description="From Steam", ) EventGame.objects.create(event=self.event, game=steam) EventGame.objects.create(event=self.event, game=custom) response = self.client.get("/api/events/event/games") descriptions = {g["name"]: g["description"] for g in response.json()} self.assertEqual(descriptions, {"Noita": "From Steam", "Opus": "From admin"})