Skip to content

Commit 738f9ce

Browse files
authored
Merge pull request #2169 from lexicalunit/deleted-games-bug-fix
Fix lfg bug with deleted games
2 parents 114a685 + 2c04ddd commit 738f9ce

File tree

7 files changed

+142
-6
lines changed

7 files changed

+142
-6
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- Fixed some edge case bugs with deleted games and user queues.
13+
1014
## [v17.10.0](https://github.com/lexicalunit/spellbot/releases/tag/v17.10.0) - 2026-01-22
1115

1216
### Added

src/spellbot/models/user.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ def game(self, channel_xid: int) -> Game | None:
8484
.filter(
8585
Queue.user_xid == self.xid,
8686
Post.channel_xid == channel_xid,
87+
Game.deleted_at.is_(None),
8788
)
8889
.order_by(Game.updated_at.desc())
8990
.first()
@@ -104,10 +105,18 @@ def waiting(self, channel_xid: int) -> bool:
104105
def pending_games(self) -> int:
105106
from spellbot.database import DatabaseSession # allow_inline
106107

107-
from . import Queue # allow_inline
108+
from . import Game, Queue # allow_inline
108109

109110
session = DatabaseSession.object_session(self)
110-
return session.query(Queue).filter(Queue.user_xid == self.xid).count()
111+
return (
112+
session.query(Queue)
113+
.join(Game)
114+
.filter(
115+
Queue.user_xid == self.xid,
116+
Game.deleted_at.is_(None),
117+
)
118+
.count()
119+
)
111120

112121
def to_dict(self) -> UserDict:
113122
return {

src/spellbot/services/games.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -436,10 +436,15 @@ def filter_blocked_list(self, author_xid: int, other_xids: list[int]) -> list[in
436436
@sync_to_async()
437437
@tracer.wrap()
438438
def filter_pending_games(self, user_xids: list[int]) -> list[int]:
439-
rows = DatabaseSession.query(
440-
Queue.user_xid,
441-
func.count(Queue.user_xid).label("pending"),
442-
).group_by(Queue.user_xid)
439+
rows = (
440+
DatabaseSession.query(
441+
Queue.user_xid,
442+
func.count(Queue.user_xid).label("pending"),
443+
)
444+
.join(Game)
445+
.filter(Game.deleted_at.is_(None))
446+
.group_by(Queue.user_xid)
447+
)
443448
counts = {row[0]: row[1] for row in rows if row[0]}
444449

445450
return [

src/spellbot/services/users.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ def current_game_id(self, channel_xid: int) -> int | None:
8181
and_(
8282
Queue.user_xid == self.user.xid,
8383
Post.channel_xid == channel_xid,
84+
Game.deleted_at.is_(None),
8485
),
8586
)
8687
.first()
@@ -98,6 +99,7 @@ def leave_game(self, channel_xid: int) -> None:
9899
and_(
99100
Queue.user_xid == self.user.xid,
100101
Post.channel_xid == channel_xid,
102+
Game.deleted_at.is_(None),
101103
),
102104
)
103105
)

tests/models/test_user.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,44 @@ def test_pending_games(self, factories: Factories) -> None:
5454
user = factories.user.create(game=game)
5555
assert user.pending_games() == 1
5656

57+
def test_pending_games_deleted_game(self, factories: Factories) -> None:
58+
guild = factories.guild.create()
59+
channel = factories.channel.create(guild=guild)
60+
game = factories.game.create(
61+
guild=guild,
62+
channel=channel,
63+
deleted_at=datetime(2021, 11, 1, tzinfo=UTC),
64+
)
65+
user = factories.user.create(game=game)
66+
# Orphaned queue entry for deleted game should not count
67+
assert user.pending_games() == 0
68+
69+
70+
class TestModelUserGame:
71+
def test_happy_path(self, factories: Factories) -> None:
72+
guild = factories.guild.create()
73+
channel = factories.channel.create(guild=guild)
74+
game = factories.game.create(guild=guild, channel=channel)
75+
user = factories.user.create(game=game)
76+
assert user.game(channel.xid) == game
77+
78+
def test_no_game(self, factories: Factories) -> None:
79+
guild = factories.guild.create()
80+
channel = factories.channel.create(guild=guild)
81+
user = factories.user.create()
82+
assert user.game(channel.xid) is None
83+
84+
def test_deleted_game(self, factories: Factories) -> None:
85+
guild = factories.guild.create()
86+
channel = factories.channel.create(guild=guild)
87+
game = factories.game.create(
88+
guild=guild,
89+
channel=channel,
90+
deleted_at=datetime(2021, 11, 1, tzinfo=UTC),
91+
)
92+
user = factories.user.create(game=game)
93+
assert user.game(channel.xid) is None
94+
5795

5896
class TestModelUserWaiting:
5997
def test_happy_path(self, factories: Factories) -> None:

tests/services/test_games.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
from datetime import UTC, datetime
4+
from typing import TYPE_CHECKING
35
from unittest.mock import patch
46

57
import pytest
@@ -21,6 +23,9 @@
2123
WatchFactory,
2224
)
2325

26+
if TYPE_CHECKING:
27+
from tests.fixtures import Factories
28+
2429
pytestmark = pytest.mark.use_db
2530

2631

@@ -210,6 +215,24 @@ async def test_happy_path(self) -> None:
210215
games = GamesService()
211216
assert await games.filter_pending_games([user1.xid, user2.xid]) == [user2.xid]
212217

218+
async def test_deleted_game(self, factories: Factories) -> None:
219+
guild = factories.guild.create()
220+
channel = factories.channel.create(guild=guild)
221+
user = factories.user.create()
222+
deleted_game = factories.game.create(
223+
status=GameStatus.PENDING.value,
224+
guild=guild,
225+
channel=channel,
226+
deleted_at=datetime(2021, 11, 1, tzinfo=UTC),
227+
)
228+
# Orphaned queue entry for deleted game
229+
QueueFactory.create(game_id=deleted_game.id, user_xid=user.xid)
230+
231+
with patch("spellbot.services.games.settings.MAX_PENDING_GAMES", 2):
232+
games = GamesService()
233+
# User should be allowed since deleted game doesn't count
234+
assert await games.filter_pending_games([user.xid]) == [user.xid]
235+
213236

214237
@pytest.mark.asyncio
215238
class TestServiceGamesBlocked:

tests/services/test_users.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
from datetime import UTC, datetime
4+
from typing import TYPE_CHECKING
35
from unittest.mock import ANY, MagicMock
46

57
import pytest
@@ -9,6 +11,9 @@
911
from spellbot.services import UsersService
1012
from tests.factories import UserFactory
1113

14+
if TYPE_CHECKING:
15+
from tests.fixtures import Factories
16+
1217
pytestmark = pytest.mark.use_db
1318

1419

@@ -196,3 +201,53 @@ async def test_users_leave_game(self, game: Game) -> None:
196201
await users.leave_game(game.channel_xid)
197202

198203
assert DatabaseSession.query(Queue).count() == 0
204+
205+
async def test_users_current_game_id_deleted_game(self, factories: Factories) -> None:
206+
guild = factories.guild.create()
207+
channel = factories.channel.create(guild=guild)
208+
game = factories.game.create(
209+
guild=guild,
210+
channel=channel,
211+
deleted_at=datetime(2021, 11, 1, tzinfo=UTC),
212+
)
213+
user = factories.user.create(game=game)
214+
215+
users = UsersService()
216+
await users.select(user.xid)
217+
assert await users.current_game_id(channel.xid) is None
218+
219+
async def test_users_is_waiting_deleted_game(self, factories: Factories) -> None:
220+
guild = factories.guild.create()
221+
channel = factories.channel.create(guild=guild)
222+
game = factories.game.create(
223+
guild=guild,
224+
channel=channel,
225+
deleted_at=datetime(2021, 11, 1, tzinfo=UTC),
226+
)
227+
user = factories.user.create(game=game)
228+
229+
users = UsersService()
230+
await users.select(user.xid)
231+
assert not await users.is_waiting(channel.xid)
232+
233+
async def test_users_leave_game_deleted_game(self, factories: Factories) -> None:
234+
guild = factories.guild.create()
235+
channel = factories.channel.create(guild=guild)
236+
game = factories.game.create(
237+
guild=guild,
238+
channel=channel,
239+
deleted_at=datetime(2021, 11, 1, tzinfo=UTC),
240+
)
241+
user = factories.user.create(game=game)
242+
243+
users = UsersService()
244+
await users.select(user.xid)
245+
246+
# Queue entry still exists (game was soft-deleted but queue wasn't cleaned up)
247+
assert DatabaseSession.query(Queue).count() == 1
248+
249+
# leave_game should not find the deleted game, so queue should remain
250+
await users.leave_game(channel.xid)
251+
252+
# Queue entry should still exist since the game was deleted
253+
assert DatabaseSession.query(Queue).count() == 1

0 commit comments

Comments
 (0)