From dc18342d58c9d5353b437ed1b6481728ec8c5d87 Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Fri, 18 Sep 2026 08:04:46 -0400 Subject: [PATCH] bug: let utility practice invitees connect before joining --- src/utility/utility-practice.service.ts | 15 ++++++++++++-- test/utility-practice.spec.ts | 27 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/utility/utility-practice.service.ts b/src/utility/utility-practice.service.ts index 448fe353..e6859f04 100644 --- a/src/utility/utility-practice.service.ts +++ b/src/utility/utility-practice.service.ts @@ -552,6 +552,10 @@ export class UtilityPracticeService { [session.id, steamIds, user.steam_id], ); + if (invited.length > 0 && session.match_id) { + await this.matchAssistant.sendUtilityPracticeRefresh(session.match_id); + } + await this.notifyInvited( session, user.steam_id, @@ -884,13 +888,20 @@ export class UtilityPracticeService { return null; } + // Invitees are on the door list before they join: a player who takes the + // invite by joining the host through Steam never touches joinUtilityPractice, + // and would otherwise be turned away with no idea why. const players = await this.postgres.query>( `SELECT mlp.steam_id::text AS steam_id FROM public.match_lineup_players mlp INNER JOIN public.match_lineups ml ON ml.id = mlp.match_lineup_id WHERE ml.match_id = $1::uuid - AND mlp.steam_id IS NOT NULL`, - [row.match_id], + AND mlp.steam_id IS NOT NULL + UNION + SELECT i.steam_id::text AS steam_id + FROM public.utility_practice_invites i + WHERE i.utility_practice_session_id = $2::uuid`, + [row.match_id, row.session_id], ); return { diff --git a/test/utility-practice.spec.ts b/test/utility-practice.spec.ts index 6c143f19..65ec2914 100644 --- a/test/utility-practice.spec.ts +++ b/test/utility-practice.spec.ts @@ -1517,6 +1517,33 @@ describe("utility practice sessions (SQL-driven)", () => { expect(session?.steam_ids.sort()).toEqual([host, mate].sort()); }); + it("lets an invitee in before they have joined from the site", async () => { + const host = await fx.player(); + const invited = await fx.player(); + const { matchId } = await createPracticeMatch(host); + const sessionId = await insertSession(host, { + match_id: matchId, + status: "Ready", + }); + const serverId = await reservedServer("practice-a", matchId, 27964); + + const sendUtilityPracticeRefresh = jest.fn( + async (): Promise => undefined, + ); + const service = makeService({ + matchAssistant: { sendUtilityPracticeRefresh }, + }); + + await service.invite({ steam_id: host } as never, { + session_id: sessionId, + steam_ids: [invited], + }); + + expect(sendUtilityPracticeRefresh).toHaveBeenCalledWith(matchId); + const session = await service.sessionForServer(serverId); + expect(session?.steam_ids.sort()).toEqual([host, invited].sort()); + }); + // A compromised game server that could name a session id could read another // session's match password, which is the credential for walking onto that // server. The lookup starts from the authenticated server for that reason.