mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-24 20:16:41 -05:00
Block joining SendouQ if season's initial powers were never seeded
Closes #2395
This commit is contained in:
@@ -2,6 +2,7 @@ import { refreshApiTokensCache } from "~/features/api-public/api-public-utils.se
|
||||
import { refreshBannedCache } from "~/features/ban/core/banned.server";
|
||||
import { clearParticipationInfoMap } from "~/features/front-page/core/ShowcaseTournaments.server";
|
||||
import { refreshSendouQInstance } from "~/features/sendouq/core/SendouQ.server";
|
||||
import { clearSeasonSkillsCache } from "~/features/sendouq/q-utils.server";
|
||||
import {
|
||||
clearAllTournamentDataCache,
|
||||
refreshRunningTournaments,
|
||||
@@ -17,6 +18,7 @@ import { cache } from "~/utils/cache.server";
|
||||
export async function refreshCaches() {
|
||||
clearAllTournamentDataCache();
|
||||
clearParticipationInfoMap();
|
||||
clearSeasonSkillsCache();
|
||||
cache.clear();
|
||||
await refreshBannedCache();
|
||||
await refreshSendouQInstance();
|
||||
|
||||
@@ -98,6 +98,20 @@ export async function findOrderedUserOrdinalsBySeason(season: number) {
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the season has any Skill rows.
|
||||
*/
|
||||
export async function existsBySeason(season: number) {
|
||||
const row = await db
|
||||
.selectFrom("Skill")
|
||||
.select("Skill.id")
|
||||
.where("Skill.season", "=", season)
|
||||
.limit(1)
|
||||
.executeTakeFirst();
|
||||
|
||||
return Boolean(row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeding skills of the given users for one seeding type, keyed by user id. Users without
|
||||
* a seeding skill of that type are absent from the map.
|
||||
|
||||
@@ -23,6 +23,7 @@ import { qSearchParams } from "../q-search-params";
|
||||
import { userCanJoinQueueAt } from "../q-utils";
|
||||
import {
|
||||
SendouQError,
|
||||
seasonInitialSkillsExist,
|
||||
setGroupChatMetadata,
|
||||
sqRedirectIfNeeded,
|
||||
} from "../q-utils.server";
|
||||
@@ -196,6 +197,11 @@ async function validateCanJoinQ(user: { id: number; discordId: string }) {
|
||||
errorToastIfFalsy(friendCode, "No friend code");
|
||||
const canJoinQueue = userCanJoinQueueAt(user, friendCode) === "NOW";
|
||||
|
||||
errorToastIfFalsy(Seasons.current(), "Season is not active");
|
||||
const season = Seasons.current();
|
||||
errorToastIfFalsy(season, "Season is not active");
|
||||
errorToastIfFalsy(canJoinQueue, "Can't join queue right now");
|
||||
errorToastIfFalsy(
|
||||
await seasonInitialSkillsExist(season.nth),
|
||||
"Season's starting powers are not set yet. Please contact staff on the Discord helpdesk",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { redirect } from "react-router";
|
||||
import * as ChatSystemMessage from "~/features/chat/ChatSystemMessage.server";
|
||||
import { TIERS } from "~/features/mmr/mmr-constants";
|
||||
import * as SkillRepository from "~/features/mmr/SkillRepository.server";
|
||||
import type { TieredSkill } from "~/features/mmr/tiered.server";
|
||||
import {
|
||||
navIconUrl,
|
||||
@@ -19,6 +20,27 @@ export class SendouQError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
const seasonsKnownToHaveSkills = new Set<number>();
|
||||
|
||||
/** Whether the season's initial skills were seeded (or there was no previous season's skills to seed them from). */
|
||||
export async function seasonInitialSkillsExist(season: number) {
|
||||
if (seasonsKnownToHaveSkills.has(season)) return true;
|
||||
|
||||
if (await SkillRepository.existsBySeason(season)) {
|
||||
seasonsKnownToHaveSkills.add(season);
|
||||
return true;
|
||||
}
|
||||
|
||||
// if the previous season has no skills either there was nothing to seed the
|
||||
// new season's initial skills from (e.g. a fresh development database)
|
||||
return !(await SkillRepository.existsBySeason(season - 1));
|
||||
}
|
||||
|
||||
/** Clears the in-process cache backing `seasonInitialSkillsExist`. */
|
||||
export function clearSeasonSkillsCache() {
|
||||
seasonsKnownToHaveSkills.clear();
|
||||
}
|
||||
|
||||
function groupRedirectLocation(group?: SQOwnGroup) {
|
||||
if (group?.status === "PREPARING") return SENDOUQ_PREPARING_PAGE;
|
||||
if (group?.matchId) return sendouQMatchPage(group.matchId);
|
||||
|
||||
@@ -53,6 +53,7 @@ export async function loadFactories(parallelIndex: number) {
|
||||
"~/db/seed/factories/SavedCalendarEventFactory"
|
||||
),
|
||||
ScrimPostFactory: await import("~/db/seed/factories/ScrimPostFactory"),
|
||||
SkillFactory: await import("~/db/seed/factories/SkillFactory"),
|
||||
SQGroupFactory: await import("~/db/seed/factories/SQGroupFactory"),
|
||||
SQMatchFactory: await import("~/db/seed/factories/SQMatchFactory"),
|
||||
TeamFactory: await import("~/db/seed/factories/TeamFactory"),
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { FULL_GROUP_SIZE } from "~/features/sendouq/q-constants";
|
||||
import { SENDOUQ_LOOKING_PAGE, SENDOUQ_PREPARING_PAGE } from "~/utils/urls";
|
||||
import {
|
||||
SENDOUQ_LOOKING_PAGE,
|
||||
SENDOUQ_PAGE,
|
||||
SENDOUQ_PREPARING_PAGE,
|
||||
} from "~/utils/urls";
|
||||
import { expect, impersonate, test } from "./helpers/playwright";
|
||||
import { SendouQLookingPage } from "./pages/sendouq/sendouq-looking-page";
|
||||
import { SendouQPage } from "./pages/sendouq/sendouq-page";
|
||||
@@ -112,4 +116,25 @@ test.describe("SendouQ", () => {
|
||||
await looking.goto();
|
||||
await expect(looking.locators.undoButtons).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("Joining the queue is blocked when the season's initial powers were never seeded", async ({
|
||||
page,
|
||||
factories,
|
||||
}) => {
|
||||
const [user] = await factories.UserFactory.createMany(1);
|
||||
// the previous season concluded with skills but the current one has none,
|
||||
// meaning season-initial-powers was forgotten
|
||||
await factories.SkillFactory.create({ userId: user.id, season: 0 });
|
||||
|
||||
await impersonate(page, user.id);
|
||||
|
||||
const q = new SendouQPage(page);
|
||||
await q.goto();
|
||||
await q.joinSolo();
|
||||
|
||||
await expect(
|
||||
page.getByText("Season's starting powers are not set yet"),
|
||||
).toBeAttached();
|
||||
await expect(page).toHaveURL(SENDOUQ_PAGE);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -360,6 +360,9 @@ export function buildCases(fx: Fixtures): {
|
||||
(skillBatch) =>
|
||||
SkillRepository.findOrderedUserOrdinalsBySeason(skillBatch.season),
|
||||
);
|
||||
add("SkillRepository.existsBySeason", fx.skillBatch, (skillBatch) =>
|
||||
SkillRepository.existsBySeason(skillBatch.season),
|
||||
);
|
||||
add("SkillRepository.findSeedingSkills", fx.skillBatch, (skillBatch) =>
|
||||
SkillRepository.findSeedingSkills({
|
||||
type: "RANKED",
|
||||
|
||||
@@ -14,19 +14,12 @@ invariant(rawNth, "nth of new season needed (argument 1)");
|
||||
const nth = Number(rawNth);
|
||||
invariant(!Number.isNaN(nth), "nth of new season must be a number");
|
||||
|
||||
const seasonHasSkills = async (season: number) =>
|
||||
Boolean(
|
||||
await db
|
||||
.selectFrom("Skill")
|
||||
.select("id")
|
||||
.where("season", "=", season)
|
||||
.limit(1)
|
||||
.executeTakeFirst(),
|
||||
);
|
||||
|
||||
invariant(await seasonHasSkills(nth - 1), `No skills for season ${nth - 1}`);
|
||||
invariant(
|
||||
!(await seasonHasSkills(nth)),
|
||||
await SkillRepository.existsBySeason(nth - 1),
|
||||
`No skills for season ${nth - 1}`,
|
||||
);
|
||||
invariant(
|
||||
!(await SkillRepository.existsBySeason(nth)),
|
||||
`Skills for season ${nth} already exist`,
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user