From fd67d2ed5494450e4017fdebd6a7a61583f2b235 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Tue, 28 Jul 2026 17:13:02 +0300 Subject: [PATCH] Done with repositories? --- app/db/sql.ts | 3 +- app/db/write-tracker.ts | 41 +++++++ .../admin/AdminRepository.server.test.ts | 15 +-- app/features/admin/routes/admin.test.ts | 35 +++--- app/features/api/ApiRepository.server.test.ts | 15 +-- app/features/api/core/perms.test.ts | 7 +- app/features/art/ArtRepository.server.test.ts | 44 +------- .../auth/LogInLinkRepository.server.test.ts | 15 +-- .../badges/BadgeRepository.server.test.ts | 7 +- .../builds/BuildRepository.server.test.ts | 11 +- .../friends/FriendRepository.server.test.ts | 44 +------- .../img-upload/ImageRepository.server.test.ts | 31 +---- .../LeaderboardRepository.server.test.ts | 7 +- .../MatchProfileRepository.server.test.ts | 8 +- .../notifications/core/notify.server.test.ts | 11 +- .../scrims/ScrimPostRepository.server.test.ts | 15 +-- app/features/scrims/routes/scrims.new.test.ts | 8 +- ...MatchContinueVoteRepository.server.test.ts | 12 +- .../SQMatchRepository.server.test.ts | 12 +- .../PrivateUserNoteRepository.server.test.ts | 8 +- .../sendouq/SQGroupRepository.server.test.ts | 12 +- .../sendouq/core/SendouQ.server.test.ts | 106 +++++------------- .../sendouq/core/default-maps.server.test.ts | 7 +- app/features/sendouq/routes/q.looking.test.ts | 8 +- .../actions/t.$customUrl.edit.server.test.ts | 7 +- .../team/actions/t.new.server.test.ts | 7 +- .../team/routes/t.$customUrl.edit.test.ts | 7 +- app/features/team/routes/t.$customUrl.test.ts | 10 +- .../XRankPlacementRepository.server.test.ts | 24 +--- .../TournamentLFGRepository.server.test.ts | 56 ++------- .../TournamentMatchRepository.server.test.ts | 7 +- .../routes/to.$id.matches.$mid.test.ts | 15 +-- ...amentOrganizationRepository.server.test.ts | 11 +- .../routes/org.$slug.stats.test.ts | 3 +- ...ournamentAuditLogRepository.server.test.ts | 8 +- .../TournamentRepository.finalize.test.ts | 6 +- .../UserCardRepository.server.test.ts | 16 +-- app/features/user-page/UserRepository.test.ts | 7 +- .../routes/u.$identifier.edit.test.ts | 7 +- .../vods/VodRepository.server.test.ts | 27 +---- .../closeExpiredContinueVotes.test.ts | 3 +- app/routines/notifyCheckInStart.test.ts | 2 - app/routines/syncLiveStreams.test.ts | 2 - app/routines/syncTournamentVods.test.ts | 12 +- app/test-setup.ts | 13 ++- app/utils/Test.ts | 18 +-- biome-plugins/no-raw-db-writes-in-tests.grit | 15 +++ biome.json | 8 +- docs/dev/repositories.md | 3 + 49 files changed, 204 insertions(+), 572 deletions(-) create mode 100644 app/db/write-tracker.ts create mode 100644 biome-plugins/no-raw-db-writes-in-tests.grit diff --git a/app/db/sql.ts b/app/db/sql.ts index ca65cba14..6c222dae0 100644 --- a/app/db/sql.ts +++ b/app/db/sql.ts @@ -9,6 +9,7 @@ import { logger } from "~/utils/logger"; import { roundToNDecimalPlaces } from "~/utils/number"; import { FastParseJSONResultsPlugin } from "./parse-json-results-plugin"; import type { DB } from "./tables"; +import { WriteTrackerPlugin } from "./write-tracker"; const migratedEmptyDb = new Database("db-test.sqlite3").serialize(); @@ -47,7 +48,7 @@ export const db = new Kysely({ database: sql, }), log, - plugins: [new FastParseJSONResultsPlugin()], + plugins: [new FastParseJSONResultsPlugin(), new WriteTrackerPlugin()], }); /** diff --git a/app/db/write-tracker.ts b/app/db/write-tracker.ts new file mode 100644 index 000000000..ff06b6ab1 --- /dev/null +++ b/app/db/write-tracker.ts @@ -0,0 +1,41 @@ +import type { + KyselyPlugin, + PluginTransformQueryArgs, + PluginTransformResultArgs, + QueryResult, + RootOperationNode, + UnknownRow, +} from "kysely"; + +let dirty = false; + +/** + * Records whether anything has been written to the database, so that a caller can + * react to writes it did not make itself: the vitest teardown wipes only when a + * test wrote something, and an e2e worker flushes the app server's caches only + * when its factories wrote something. + */ +export class WriteTrackerPlugin implements KyselyPlugin { + transformQuery(args: PluginTransformQueryArgs): RootOperationNode { + if (args.node.kind !== "SelectQueryNode") { + dirty = true; + } + + return args.node; + } + + async transformResult( + args: PluginTransformResultArgs, + ): Promise> { + return args.result; + } +} + +/** Whether the database has been written to since the last {@link markDatabaseClean}. */ +export function isDatabaseDirty() { + return dirty; +} + +export function markDatabaseClean() { + dirty = false; +} diff --git a/app/features/admin/AdminRepository.server.test.ts b/app/features/admin/AdminRepository.server.test.ts index 7a42c3728..1f37cce19 100644 --- a/app/features/admin/AdminRepository.server.test.ts +++ b/app/features/admin/AdminRepository.server.test.ts @@ -1,7 +1,6 @@ -import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import * as UserRepository from "~/features/user-page/UserRepository.server"; -import { dbReset } from "~/utils/Test"; import * as AdminRepository from "./AdminRepository.server"; const users = UserFactory.pool(); @@ -16,10 +15,6 @@ describe("findAllBannedUsers", () => { await createUsers(5); }); - afterEach(async () => { - await dbReset(); - }); - test("returns empty Map when no users are banned", async () => { const result = await AdminRepository.findAllBannedUsers(); @@ -109,10 +104,6 @@ describe("banUser", () => { await createUsers(3); }); - afterEach(async () => { - await dbReset(); - }); - test("permanently bans user (banned = 1)", async () => { await AdminRepository.banUser({ userId: users.id(1), @@ -224,10 +215,6 @@ describe("unbanUser", () => { await createUsers(3); }); - afterEach(async () => { - await dbReset(); - }); - test("unbans a previously banned user", async () => { await AdminRepository.banUser({ userId: users.id(1), diff --git a/app/features/admin/routes/admin.test.ts b/app/features/admin/routes/admin.test.ts index e90227d9f..fc76c655d 100644 --- a/app/features/admin/routes/admin.test.ts +++ b/app/features/admin/routes/admin.test.ts @@ -9,12 +9,7 @@ import * as BuildRepository from "~/features/builds/BuildRepository.server"; import { MATCHES_COUNT_NEEDED_FOR_LEADERBOARD } from "~/features/leaderboards/leaderboards-constants"; import * as TeamRepository from "~/features/team/TeamRepository.server"; import * as UserRepository from "~/features/user-page/UserRepository.server"; -import { - assertResponseErrored, - dbReset, - withUserId, - wrappedAction, -} from "~/utils/Test"; +import { assertResponseErrored, withUserId, wrappedAction } from "~/utils/Test"; import type { adminActionSchema } from "../actions/admin.server"; import { action } from "./admin"; @@ -43,6 +38,17 @@ const createLeaderboard = (userIds: number[]) => { matchesCount: MATCHES_COUNT_NEEDED_FOR_LEADERBOARD }, ); +/** Marks a user as skipping the plus server for the given season. */ +const skipPlusForSeason = (userId: number, seasonNth: number) => + // the app only ever reads the column; the one thing that sets it is + // `scripts/skip-plus.ts`, itself a raw update with no repository function behind it + // biome-ignore lint/plugin: no production write reaches the column + db + .updateTable("User") + .set({ plusSkippedForSeasonNth: seasonNth }) + .where("User.id", "=", userId) + .execute(); + describe("Plus voting", () => { beforeEach(() => { vi.useFakeTimers(); @@ -50,7 +56,6 @@ describe("Plus voting", () => { afterEach(async () => { vi.useRealTimers(); - await dbReset(); }); test("gives correct amount of plus tiers", async () => { @@ -141,11 +146,7 @@ describe("Plus voting", () => { await createUsers(11); await createLeaderboard(users.ids()); - await db - .updateTable("User") - .set({ plusSkippedForSeasonNth: 1 }) - .where("User.id", "=", users.id(1)) - .execute(); + await skipPlusForSeason(users.id(1), 1); await adminAction({ _action: "REFRESH" }, { user: "admin" }); @@ -159,11 +160,7 @@ describe("Plus voting", () => { await createUsers(11); await createLeaderboard(users.ids()); - await db - .updateTable("User") - .set({ plusSkippedForSeasonNth: 0 }) - .where("User.id", "=", users.id(1)) - .execute(); + await skipPlusForSeason(users.id(1), 0); await adminAction({ _action: "REFRESH" }, { user: "admin" }); @@ -274,10 +271,6 @@ describe("Account migration", () => { await createUsers(2); }); - afterEach(async () => { - await dbReset(); - }); - it("migrates a blank account", async () => { expect(await UserRepository.findProfileByIdentifier("0")).toBeDefined(); expect(await UserRepository.findProfileByIdentifier("1")).toBeDefined(); diff --git a/app/features/api/ApiRepository.server.test.ts b/app/features/api/ApiRepository.server.test.ts index d500bf401..d3e499f6e 100644 --- a/app/features/api/ApiRepository.server.test.ts +++ b/app/features/api/ApiRepository.server.test.ts @@ -1,6 +1,5 @@ -import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import * as UserFactory from "~/db/seed/factories/UserFactory"; -import { dbReset } from "~/utils/Test"; import * as ApiRepository from "./ApiRepository.server"; const users = UserFactory.pool(); @@ -10,10 +9,6 @@ describe("findTokenByUserId", () => { await users.create(3); }); - afterEach(async () => { - await dbReset(); - }); - test("returns undefined when user has no token", async () => { const result = await ApiRepository.findTokenByUserId(users.id(1), "read"); @@ -68,10 +63,6 @@ describe("generateToken", () => { await users.create(3); }); - afterEach(async () => { - await dbReset(); - }); - test("creates new token for user", async () => { const result = await ApiRepository.generateToken(users.id(1), "read"); @@ -145,10 +136,6 @@ describe("findAllApiTokens", () => { await users.create(1); }); - afterEach(async () => { - await dbReset(); - }); - test("returns empty array when no tokens exist", async () => { const result = await ApiRepository.findAllApiTokens(); diff --git a/app/features/api/core/perms.test.ts b/app/features/api/core/perms.test.ts index f3c9f2cb3..526bd3731 100644 --- a/app/features/api/core/perms.test.ts +++ b/app/features/api/core/perms.test.ts @@ -1,17 +1,12 @@ -import { afterEach, describe, expect, test } from "vitest"; +import { describe, expect, test } from "vitest"; import * as ApiTokenFactory from "~/db/seed/factories/ApiTokenFactory"; import * as TournamentOrganizationFactory from "~/db/seed/factories/TournamentOrganizationFactory"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import * as UserRepository from "~/features/user-page/UserRepository.server"; -import { dbReset } from "~/utils/Test"; import * as ApiRepository from "../ApiRepository.server"; import { checkUserHasApiAccess } from "./perms"; describe("Permission logic consistency between findAllApiTokens and checkUserHasApiAccess", () => { - afterEach(async () => { - await dbReset(); - }); - test("both functions grant access for isApiAccesser flag", async () => { const { id } = await UserFactory.create(null, { roles: ["API_ACCESSER"] }); diff --git a/app/features/art/ArtRepository.server.test.ts b/app/features/art/ArtRepository.server.test.ts index c74415b8f..e1f11ed48 100644 --- a/app/features/art/ArtRepository.server.test.ts +++ b/app/features/art/ArtRepository.server.test.ts @@ -1,8 +1,8 @@ -import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import * as ArtFactory from "~/db/seed/factories/ArtFactory"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import { databaseTimestampNow } from "~/utils/dates"; -import { dbReset, withUserId } from "~/utils/Test"; +import { withUserId } from "~/utils/Test"; import * as ArtRepository from "./ArtRepository.server"; const users = UserFactory.pool(); @@ -12,10 +12,6 @@ describe("findShowcaseArts", () => { await users.create(5); }); - afterEach(async () => { - await dbReset(); - }); - test("shows one art per artist", async () => { await ArtFactory.create({ authorId: users.id(1) }); await ArtFactory.create({ authorId: users.id(2) }); @@ -71,10 +67,6 @@ describe("findAllTags", () => { await users.create(1); }); - afterEach(async () => { - await dbReset(); - }); - test("returns all art tags", async () => { await ArtFactory.create({ authorId: users.id(1), @@ -103,10 +95,6 @@ describe("unlinkUserFromArt", () => { await users.create(2); }); - afterEach(async () => { - await dbReset(); - }); - test("removes user link from art", async () => { const art = await ArtFactory.create({ authorId: users.id(1), @@ -127,10 +115,6 @@ describe("findShowcaseArtsByTag", () => { await users.create(3); }); - afterEach(async () => { - await dbReset(); - }); - test("returns arts filtered by tag", async () => { const characterArt = await ArtFactory.create({ authorId: users.id(1), @@ -180,10 +164,6 @@ describe("findRecentlyUploadedArts", () => { await users.create(3); }); - afterEach(async () => { - await dbReset(); - }); - test("returns recently uploaded arts", async () => { const art = await ArtFactory.create({ authorId: users.id(1) }); @@ -199,10 +179,6 @@ describe("findArtsByUserId", () => { await users.create(3); }); - afterEach(async () => { - await dbReset(); - }); - test("returns authored art", async () => { const art = await ArtFactory.create({ authorId: users.id(1) }); @@ -230,10 +206,6 @@ describe("deleteById", () => { await users.create(1); }); - afterEach(async () => { - await dbReset(); - }); - test("deletes art by id", async () => { const art = await ArtFactory.create({ authorId: users.id(1) }); @@ -261,10 +233,6 @@ describe("deleteOrphanTags", () => { await users.create(1); }); - afterEach(async () => { - await dbReset(); - }); - test("deletes tags with no associated art", async () => { const art = await ArtFactory.create({ authorId: users.id(1), @@ -300,10 +268,6 @@ describe("insert", () => { await users.create(2); }); - afterEach(async () => { - await dbReset(); - }); - test("inserts art with all metadata", async () => { const art = await withUserId(users.id(1), () => ArtRepository.insert({ @@ -346,10 +310,6 @@ describe("update", () => { await users.create(3); }); - afterEach(async () => { - await dbReset(); - }); - test("updates art metadata", async () => { const art = await ArtFactory.create({ authorId: users.id(1), diff --git a/app/features/auth/LogInLinkRepository.server.test.ts b/app/features/auth/LogInLinkRepository.server.test.ts index 0ad0f767a..2e2e4d7fd 100644 --- a/app/features/auth/LogInLinkRepository.server.test.ts +++ b/app/features/auth/LogInLinkRepository.server.test.ts @@ -1,6 +1,5 @@ -import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import * as UserFactory from "~/db/seed/factories/UserFactory"; -import { dbReset } from "~/utils/Test"; import * as LogInLinkRepository from "./LogInLinkRepository.server"; describe("create", () => { @@ -10,10 +9,6 @@ describe("create", () => { userId = (await UserFactory.create()).id; }); - afterEach(async () => { - await dbReset(); - }); - test("creates a login link with correct userId", async () => { const link = await LogInLinkRepository.insert(userId); @@ -35,10 +30,6 @@ describe("del", () => { userId = (await UserFactory.create()).id; }); - afterEach(async () => { - await dbReset(); - }); - test("deletes a login link by code", async () => { const link = await LogInLinkRepository.insert(userId); @@ -56,10 +47,6 @@ describe("findValidByCode", () => { userId = (await UserFactory.create()).id; }); - afterEach(async () => { - await dbReset(); - }); - test("returns userId for valid code", async () => { const link = await LogInLinkRepository.insert(userId); diff --git a/app/features/badges/BadgeRepository.server.test.ts b/app/features/badges/BadgeRepository.server.test.ts index 90b3c3ea3..5f9e2ae37 100644 --- a/app/features/badges/BadgeRepository.server.test.ts +++ b/app/features/badges/BadgeRepository.server.test.ts @@ -1,9 +1,8 @@ -import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import * as BadgeFactory from "~/db/seed/factories/BadgeFactory"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import * as XRankPlacementFactory from "~/db/seed/factories/XRankPlacementFactory"; import * as XRankPlacementRepository from "~/features/top-search/XRankPlacementRepository.server"; -import { dbReset } from "~/utils/Test"; import * as BadgeRepository from "./BadgeRepository.server"; import { SPLATOON_3_XP_BADGE_VALUES } from "./badges-constants"; @@ -18,10 +17,6 @@ describe("syncXPBadges", () => { })); }); - afterEach(async () => { - await dbReset(); - }); - test("assigns badge to user with qualifying peakXp", async () => { await givePeakXp(user.id, 3000); diff --git a/app/features/builds/BuildRepository.server.test.ts b/app/features/builds/BuildRepository.server.test.ts index c66c937ea..319d57dc2 100644 --- a/app/features/builds/BuildRepository.server.test.ts +++ b/app/features/builds/BuildRepository.server.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import * as BuildFactory from "~/db/seed/factories/BuildFactory"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import * as XRankPlacementFactory from "~/db/seed/factories/XRankPlacementFactory"; @@ -7,7 +7,6 @@ import type { BuildAbilitiesTuple, MainWeaponId, } from "~/modules/in-game-lists/types"; -import { dbReset } from "~/utils/Test"; import * as BuildRepository from "./BuildRepository.server"; let owner: { id: number }; @@ -101,10 +100,6 @@ describe("BuildRepository.insert — computeBuildData", () => { [owner, otherOwner] = await UserFactory.createMany(2); }); - afterEach(async () => { - await dbReset(); - }); - describe("abilitiesSignature & ability sums", () => { test("writes the serialized abilitiesSignature sorted by AP desc", async () => { const { id } = await BuildRepository.insert(baseArgs()); @@ -307,10 +302,6 @@ describe("BuildRepository.findAllPopularAbilitiesByWeaponId", () => { [owner, otherOwner] = await UserFactory.createMany(2); }); - afterEach(async () => { - await dbReset(); - }); - test("counts each user at most once across signature buckets", async () => { // Each user has two Splattershot builds with different signatures. // Without per-user dedup, both users would inflate both buckets and diff --git a/app/features/friends/FriendRepository.server.test.ts b/app/features/friends/FriendRepository.server.test.ts index d1622ab4e..9f77597e7 100644 --- a/app/features/friends/FriendRepository.server.test.ts +++ b/app/features/friends/FriendRepository.server.test.ts @@ -1,8 +1,8 @@ -import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import * as FriendRequestFactory from "~/db/seed/factories/FriendRequestFactory"; import * as FriendshipFactory from "~/db/seed/factories/FriendshipFactory"; import * as UserFactory from "~/db/seed/factories/UserFactory"; -import { dbReset, withUserId } from "~/utils/Test"; +import { withUserId } from "~/utils/Test"; import * as FriendRepository from "./FriendRepository.server"; const users = UserFactory.pool(); @@ -17,10 +17,6 @@ describe("insertFriendRequest / findFriendRequestBetween", () => { await createUsers(3); }); - afterEach(async () => { - await dbReset(); - }); - test("finds request from sender to receiver", async () => { await FriendRepository.insertFriendRequest({ senderId: users.id(1), @@ -70,10 +66,6 @@ describe("findPendingSentRequests / findPendingReceivedRequests", () => { await createUsers(3); }); - afterEach(async () => { - await dbReset(); - }); - test("sent request appears in sender's sent requests", async () => { await FriendRequestFactory.create({ senderId: users.id(1), @@ -121,10 +113,6 @@ describe("countPendingSentRequests", () => { await createUsers(4); }); - afterEach(async () => { - await dbReset(); - }); - test("returns 0 with no requests", async () => { const count = await FriendRepository.countPendingSentRequests(users.id(1)); @@ -148,10 +136,6 @@ describe("deleteFriendRequest", () => { await createUsers(3); }); - afterEach(async () => { - await dbReset(); - }); - test("deletes request by sender", async () => { const request = await FriendRequestFactory.create({ senderId: users.id(1), @@ -194,10 +178,6 @@ describe("deleteFriendRequestByReceiver", () => { await createUsers(3); }); - afterEach(async () => { - await dbReset(); - }); - test("deletes request by receiver", async () => { const request = await FriendRequestFactory.create({ senderId: users.id(1), @@ -222,10 +202,6 @@ describe("insertFriendship / findFriendship / findFriendIds", () => { await createUsers(3); }); - afterEach(async () => { - await dbReset(); - }); - test("creates friendship and removes friend request", async () => { const request = await FriendRequestFactory.create({ senderId: users.id(2), @@ -306,10 +282,6 @@ describe("deleteFriendship", () => { await createUsers(3); }); - afterEach(async () => { - await dbReset(); - }); - test("removes friendship", async () => { const friendship = await FriendshipFactory.create({ userOneId: users.id(1), @@ -350,10 +322,6 @@ describe("findFriendRequestByIdAndReceiver", () => { await createUsers(3); }); - afterEach(async () => { - await dbReset(); - }); - test("returns sender ID when request exists for receiver", async () => { const request = await FriendRequestFactory.create({ senderId: users.id(1), @@ -389,10 +357,6 @@ describe("findMutualFriends", () => { await createUsers(4); }); - afterEach(async () => { - await dbReset(); - }); - test("returns mutual friend when two users share a common friend", async () => { await FriendshipFactory.create({ userOneId: users.id(1), @@ -436,10 +400,6 @@ describe("findByUserIdWithActivity", () => { await createUsers(3); }); - afterEach(async () => { - await dbReset(); - }); - test("returns friends with friendshipId and createdAt", async () => { await FriendshipFactory.create({ userOneId: users.id(1), diff --git a/app/features/img-upload/ImageRepository.server.test.ts b/app/features/img-upload/ImageRepository.server.test.ts index 4d3267bef..625c70dcf 100644 --- a/app/features/img-upload/ImageRepository.server.test.ts +++ b/app/features/img-upload/ImageRepository.server.test.ts @@ -1,9 +1,8 @@ -import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import * as ArtFactory from "~/db/seed/factories/ArtFactory"; import * as CalendarEventFactory from "~/db/seed/factories/CalendarEventFactory"; import * as ImageFactory from "~/db/seed/factories/ImageFactory"; import * as UserFactory from "~/db/seed/factories/UserFactory"; -import { dbReset } from "~/utils/Test"; import * as ArtRepository from "../art/ArtRepository.server"; import * as ImageRepository from "./ImageRepository.server"; @@ -19,10 +18,6 @@ describe("findById", () => { submitter = await UserFactory.create(); }); - afterEach(async () => { - await dbReset(); - }); - test("finds image by id", async () => { const img = await ImageFactory.create({ submitterUserId: submitter.id }); @@ -57,10 +52,6 @@ describe("deleteById", () => { submitter = await UserFactory.create(); }); - afterEach(async () => { - await dbReset(); - }); - test("deletes image by id", async () => { const img = await ImageFactory.create({ submitterUserId: submitter.id }); @@ -92,10 +83,6 @@ describe("countUnvalidatedArt", () => { submitter = await UserFactory.create(); }); - afterEach(async () => { - await dbReset(); - }); - test("counts unvalidated art by author", async () => { await createUnvalidatedArt(submitter.id); await createUnvalidatedArt(submitter.id); @@ -126,10 +113,6 @@ describe("countAllUnvalidated", () => { submitter = await UserFactory.create(); }); - afterEach(async () => { - await dbReset(); - }); - test("counts unvalidated images used in art", async () => { await createUnvalidatedArt(submitter.id); @@ -184,10 +167,6 @@ describe("countUnvalidatedBySubmitterUserId", () => { [submitter, otherSubmitter] = await UserFactory.createMany(2); }); - afterEach(async () => { - await dbReset(); - }); - test("counts unvalidated images connected to art by submitter", async () => { await createUnvalidatedArt(submitter.id); await createUnvalidatedArt(submitter.id); @@ -244,10 +223,6 @@ describe("validateById", () => { submitter = await UserFactory.create(); }); - afterEach(async () => { - await dbReset(); - }); - test("marks image as validated", async () => { const img = await ImageFactory.create({ submitterUserId: submitter.id }); @@ -278,10 +253,6 @@ describe("findAllUnvalidated", () => { ); }); - afterEach(async () => { - await dbReset(); - }); - test("fetches unvalidated images with submitter info", async () => { const filename = "unvalidated-art.png"; await ArtFactory.create({ diff --git a/app/features/leaderboards/LeaderboardRepository.server.test.ts b/app/features/leaderboards/LeaderboardRepository.server.test.ts index fc7289ed1..c45a863dd 100644 --- a/app/features/leaderboards/LeaderboardRepository.server.test.ts +++ b/app/features/leaderboards/LeaderboardRepository.server.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { beforeEach, describe, expect, test, vi } from "vitest"; vi.mock("~/features/chat/ChatSystemMessage.server", () => ({ send: vi.fn(), @@ -18,7 +18,6 @@ import * as Seasons from "~/features/mmr/core/Seasons"; import { FULL_GROUP_SIZE } from "~/features/sendouq/q-constants"; import type { MainWeaponId } from "~/modules/in-game-lists/types"; import { dateToDatabaseTimestamp } from "~/utils/dates"; -import { dbReset } from "~/utils/Test"; import * as LeaderboardRepository from "./LeaderboardRepository.server"; import { MATCHES_COUNT_NEEDED_FOR_LEADERBOARD } from "./leaderboards-constants"; @@ -127,10 +126,6 @@ describe("findSeasonPopularUsersWeapon", () => { [player, otherPlayer, ...groupFillers] = users; }); - afterEach(async () => { - await dbReset(); - }); - test("returns user's most reported SendouQ weapon", async () => { await reportSendouqWeapons({ userId: player.id, diff --git a/app/features/match-profile/MatchProfileRepository.server.test.ts b/app/features/match-profile/MatchProfileRepository.server.test.ts index 69236da7c..97e15e3d5 100644 --- a/app/features/match-profile/MatchProfileRepository.server.test.ts +++ b/app/features/match-profile/MatchProfileRepository.server.test.ts @@ -1,7 +1,7 @@ -import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import type { UserMapModePreferences } from "~/db/tables-json"; -import { dbReset, withUserId } from "~/utils/Test"; +import { withUserId } from "~/utils/Test"; import * as MatchProfileRepository from "./MatchProfileRepository.server"; let userId: number; @@ -40,10 +40,6 @@ describe("updateOwnMatchProfile", () => { userId = user.id; }); - afterEach(async () => { - await dbReset(); - }); - test("reports no change when nothing matchmaking-relevant changed", async () => { const result = await updateProfile({ vc: "YES", languages: ["en"] }); diff --git a/app/features/notifications/core/notify.server.test.ts b/app/features/notifications/core/notify.server.test.ts index 9c5ed2862..a6af2bce1 100644 --- a/app/features/notifications/core/notify.server.test.ts +++ b/app/features/notifications/core/notify.server.test.ts @@ -1,6 +1,5 @@ -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { beforeEach, describe, expect, test, vi } from "vitest"; import * as UserFactory from "~/db/seed/factories/UserFactory"; -import { dbReset } from "~/utils/Test"; import { APP_ICON_URL } from "~/utils/urls"; import * as NotificationRepository from "../NotificationRepository.server"; import { clearSentNotificationsForTesting, notify } from "./notify.server"; @@ -27,10 +26,6 @@ describe("notify()", () => { clearSentNotificationsForTesting(); }); - afterEach(async () => { - await dbReset(); - }); - test("different recipients receive same notification", async () => { await notify({ userIds: [users.id(1), users.id(2)], @@ -258,10 +253,6 @@ describe("notify() - web push notifications", () => { mockWebPushEnabled.value = false; }); - afterEach(async () => { - await dbReset(); - }); - test("sends web push notification when user has subscription", async () => { const mockSubscription = { endpoint: "https://fcm.googleapis.com/fcm/send/test", diff --git a/app/features/scrims/ScrimPostRepository.server.test.ts b/app/features/scrims/ScrimPostRepository.server.test.ts index a545e44ac..126a3d84e 100644 --- a/app/features/scrims/ScrimPostRepository.server.test.ts +++ b/app/features/scrims/ScrimPostRepository.server.test.ts @@ -1,11 +1,10 @@ import { add, sub } from "date-fns"; -import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import * as ScrimPostFactory from "~/db/seed/factories/ScrimPostFactory"; import * as TeamFactory from "~/db/seed/factories/TeamFactory"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import { dateToDatabaseTimestamp } from "~/utils/dates"; import { DuplicateEntryError } from "~/utils/errors"; -import { dbReset } from "~/utils/Test"; import * as ScrimPostRepository from "./ScrimPostRepository.server"; const users = UserFactory.pool(); @@ -24,10 +23,6 @@ describe("findPendingOverlapsForUsers", () => { await users.create(5); }); - afterEach(async () => { - await dbReset(); - }); - test("returns a specific-time pending post in window with its member ids", async () => { const postId = await ScrimPostFactory.create({ startsAt: dbTs(BOOKED_AT), @@ -201,10 +196,6 @@ describe("findUserScrims", () => { await users.create(5); }); - afterEach(async () => { - await dbReset(); - }); - test("passed-over requester does not see the scrim booked between the post and another team", async () => { await ScrimPostFactory.create( { @@ -265,10 +256,6 @@ describe("insertRequest", () => { await users.create(5); }); - afterEach(async () => { - await dbReset(); - }); - const insertTeamRequest = ({ scrimPostId, teamId, diff --git a/app/features/scrims/routes/scrims.new.test.ts b/app/features/scrims/routes/scrims.new.test.ts index e262680b0..18b4cb55e 100644 --- a/app/features/scrims/routes/scrims.new.test.ts +++ b/app/features/scrims/routes/scrims.new.test.ts @@ -1,8 +1,8 @@ import { add } from "date-fns"; -import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import type { SerializeFrom } from "~/utils/remix"; -import { dbReset, wrappedAction, wrappedLoader } from "~/utils/Test"; +import { wrappedAction, wrappedLoader } from "~/utils/Test"; import { action } from "../actions/scrims.new.server"; import { loader } from "../loaders/scrims.server"; import type { scrimsNewFormSchema } from "../scrims-schemas"; @@ -42,10 +42,6 @@ describe("New scrim post action", () => { pickupMembers = await UserFactory.createMany(3); }); - afterEach(async () => { - await dbReset(); - }); - test("scrim post made for now has isScheduledForFuture = false", async () => { const response = await newScrimAction( { diff --git a/app/features/sendouq-match/GroupMatchContinueVoteRepository.server.test.ts b/app/features/sendouq-match/GroupMatchContinueVoteRepository.server.test.ts index 3044a1d3f..3b97bb638 100644 --- a/app/features/sendouq-match/GroupMatchContinueVoteRepository.server.test.ts +++ b/app/features/sendouq-match/GroupMatchContinueVoteRepository.server.test.ts @@ -1,7 +1,7 @@ -import { afterEach, describe, expect, test } from "vitest"; +import { describe, expect, test } from "vitest"; import * as SQGroupFactory from "~/db/seed/factories/SQGroupFactory"; import * as UserFactory from "~/db/seed/factories/UserFactory"; -import { dbReset, withUserId } from "~/utils/Test"; +import { withUserId } from "~/utils/Test"; import * as GroupMatchContinueVoteRepository from "./GroupMatchContinueVoteRepository.server"; const createGroup = async () => { @@ -20,10 +20,6 @@ const castVote = (userId: number, groupId: number, isContinuing: boolean) => ); describe("findAllByGroupIds", () => { - afterEach(async () => { - await dbReset(); - }); - test("returns empty array without querying when no group ids given", async () => { const result = await GroupMatchContinueVoteRepository.findAllByGroupIds([]); expect(result).toEqual([]); @@ -53,10 +49,6 @@ describe("findAllByGroupIds", () => { }); describe("cast", () => { - afterEach(async () => { - await dbReset(); - }); - test("updates existing vote on conflict instead of inserting a duplicate", async () => { const voter = await UserFactory.create(); const groupId = await createGroup(); diff --git a/app/features/sendouq-match/SQMatchRepository.server.test.ts b/app/features/sendouq-match/SQMatchRepository.server.test.ts index bd2e56b96..0281def91 100644 --- a/app/features/sendouq-match/SQMatchRepository.server.test.ts +++ b/app/features/sendouq-match/SQMatchRepository.server.test.ts @@ -1,10 +1,10 @@ -import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import * as SQGroupFactory from "~/db/seed/factories/SQGroupFactory"; import * as SQMatchFactory from "~/db/seed/factories/SQMatchFactory"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import { db } from "~/db/sql"; import { FULL_GROUP_SIZE } from "~/features/sendouq/q-constants"; -import { dbReset, withUserId } from "~/utils/Test"; +import { withUserId } from "~/utils/Test"; import * as SQMatchRepository from "./SQMatchRepository.server"; const setupMatch = async () => { @@ -61,10 +61,6 @@ const fetchSkills = async (matchId: number) => { }; describe("lockMatchWithoutSkillChange", () => { - afterEach(async () => { - await dbReset(); - }); - test("inserts dummy skill to lock match", async () => { const { match } = await setupMatch(); @@ -87,10 +83,6 @@ describe("cancelMatch", () => { setup = await setupMatch(); }); - afterEach(async () => { - await dbReset(); - }); - test("first cancel report sets group inactive", async () => { const result = await withUserId(setup.alphaMembers[0].id, () => SQMatchRepository.cancelMatch({ diff --git a/app/features/sendouq/PrivateUserNoteRepository.server.test.ts b/app/features/sendouq/PrivateUserNoteRepository.server.test.ts index 225363296..62739345c 100644 --- a/app/features/sendouq/PrivateUserNoteRepository.server.test.ts +++ b/app/features/sendouq/PrivateUserNoteRepository.server.test.ts @@ -1,7 +1,7 @@ -import { afterEach, describe, expect, test } from "vitest"; +import { describe, expect, test } from "vitest"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import * as UserRepository from "~/features/user-page/UserRepository.server"; -import { dbReset, withUser } from "~/utils/Test"; +import { withUser } from "~/utils/Test"; import * as PrivateUserNoteRepository from "./PrivateUserNoteRepository.server"; const authorAndTarget = async () => { @@ -16,10 +16,6 @@ const authorAndTarget = async () => { }; describe("PrivateUserNoteRepository", () => { - afterEach(async () => { - await dbReset(); - }); - describe("upsertOwnNote", () => { test("stamps the acting user as the author", async () => { const { author, targetId } = await authorAndTarget(); diff --git a/app/features/sendouq/SQGroupRepository.server.test.ts b/app/features/sendouq/SQGroupRepository.server.test.ts index 5f93bcae9..fb437d436 100644 --- a/app/features/sendouq/SQGroupRepository.server.test.ts +++ b/app/features/sendouq/SQGroupRepository.server.test.ts @@ -1,9 +1,9 @@ -import { afterEach, describe, expect, test } from "vitest"; +import { describe, expect, test } from "vitest"; import * as SQGroupFactory from "~/db/seed/factories/SQGroupFactory"; import * as SQMatchFactory from "~/db/seed/factories/SQMatchFactory"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import * as GroupMatchContinueVoteRepository from "~/features/sendouq-match/GroupMatchContinueVoteRepository.server"; -import { dbReset, withUserId } from "~/utils/Test"; +import { withUserId } from "~/utils/Test"; import { FULL_GROUP_SIZE } from "./q-constants"; import * as SQGroupRepository from "./SQGroupRepository.server"; @@ -48,10 +48,6 @@ const castYesVote = (userId: number, groupId: number) => ); describe("insert", () => { - afterEach(async () => { - await dbReset(); - }); - test("records implicit no-vote on previous matchmade group when user creates a new group", async () => { const { alphaGroupId, alphaMembers, matchChatCode } = await setupConcludedMatch(); @@ -123,10 +119,6 @@ describe("insert", () => { }); describe("insertMember", () => { - afterEach(async () => { - await dbReset(); - }); - test("records implicit no-vote on previous matchmade group when user joins another group", async () => { const { alphaGroupId, alphaMembers, matchChatCode } = await setupConcludedMatch(); diff --git a/app/features/sendouq/core/SendouQ.server.test.ts b/app/features/sendouq/core/SendouQ.server.test.ts index 5b047265b..e070c9eb4 100644 --- a/app/features/sendouq/core/SendouQ.server.test.ts +++ b/app/features/sendouq/core/SendouQ.server.test.ts @@ -1,12 +1,11 @@ -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { subSeconds } from "date-fns"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { backdate } from "~/db/seed/core/backdate"; import * as SkillFactory from "~/db/seed/factories/SkillFactory"; import * as SQMatchFactory from "~/db/seed/factories/SQMatchFactory"; import * as UserFactory from "~/db/seed/factories/UserFactory"; -import { db } from "~/db/sql"; import { MATCHES_COUNT_NEEDED_FOR_LEADERBOARD } from "~/features/leaderboards/leaderboards-constants"; import { refreshUserSkills } from "~/features/mmr/tiered.server"; -import { databaseTimestampNow } from "~/utils/dates"; -import { dbReset } from "~/utils/Test"; import * as SQGroupRepository from "../SQGroupRepository.server"; import { refreshSendouQInstance, SendouQ } from "./SendouQ.server"; @@ -49,6 +48,19 @@ const createGroup = async ( return groupResult.id; }; +/** + * Gives every group the same `latestActionAt`, so the sort comparator's recency + * tie-breaker stays neutral and the assertion does not depend on whether the group + * inserts straddle a second boundary (which they can on slow CI). + */ +const alignLatestActionAt = async (groupIds: number[]) => { + const at = new Date(); + + for (const groupId of groupIds) { + await backdate("Group", groupId, { latestActionAt: at }); + } +}; + const inviteCodeOf = (position: number) => SendouQ.findOwnGroup(users.id(position))!.inviteCode; @@ -65,10 +77,6 @@ describe("SendouQ", () => { await users.create(8); }); - afterEach(async () => { - await dbReset(); - }); - test("returns 'default' when user not in any group", async () => { await refreshSendouQInstance(); @@ -117,10 +125,6 @@ describe("SendouQ", () => { await users.create(8); }); - afterEach(async () => { - await dbReset(); - }); - test("returns group when user is a member", async () => { await createGroup([1, 2, 3]); await refreshSendouQInstance(); @@ -181,10 +185,6 @@ describe("SendouQ", () => { await users.create(4); }); - afterEach(async () => { - await dbReset(); - }); - test("returns group when invite code is valid", async () => { await createGroup([1]); await refreshSendouQInstance(); @@ -222,10 +222,6 @@ describe("SendouQ", () => { await users.create(12); }); - afterEach(async () => { - await dbReset(); - }); - test("returns empty array when no groups exist", async () => { await refreshSendouQInstance(); @@ -324,15 +320,7 @@ describe("SendouQ", () => { const group1Id = await createGroup([2, 3, 4, 5]); const group2Id = await createGroup([6, 7, 8, 9]); - // Force identical latestActionAt so the sort comparator's - // recency tie-breaker stays neutral and the assertion does - // not depend on whether the group inserts straddle a - // millisecond boundary (which they can on slow CI). - await db - .updateTable("Group") - .set({ latestActionAt: databaseTimestampNow() }) - .where("id", "in", [group1Id, group2Id]) - .execute(); + await alignLatestActionAt([group1Id, group2Id]); await refreshSendouQInstance(); const groups = SendouQ.previewGroups(users.id(1)); @@ -351,15 +339,7 @@ describe("SendouQ", () => { const g4Id = await createGroup([4]); const g2Id = await createGroup([2]); const g3Id = await createGroup([3]); - // Force identical latestActionAt so the sort comparator's - // recency tie-breaker stays neutral and the assertion does - // not depend on whether the group inserts straddle a - // millisecond boundary (which they can on slow CI). - await db - .updateTable("Group") - .set({ latestActionAt: databaseTimestampNow() }) - .where("id", "in", [g4Id, g2Id, g3Id]) - .execute(); + await alignLatestActionAt([g4Id, g2Id, g3Id]); await refreshSendouQInstance(); const groups = SendouQ.previewGroups(users.id(1)); @@ -410,10 +390,6 @@ describe("SendouQ", () => { await users.create(20); }); - afterEach(async () => { - await dbReset(); - }); - test("returns empty array when user not in a group", async () => { await createGroup([1, 2, 3, 4]); await refreshSendouQInstance(); @@ -537,10 +513,6 @@ describe("SendouQ", () => { await users.create(12); }); - afterEach(async () => { - await dbReset(); - }); - test("marks group as replay when 3+ members overlap", async () => { await playOutMatchBetween([1, 2, 3, 4], [5, 6, 7, 8]); @@ -606,10 +578,6 @@ describe("SendouQ", () => { await users.create(12); }); - afterEach(async () => { - await dbReset(); - }); - test("full groups have members undefined", async () => { await createGroup([1, 2, 3, 4]); await createGroup([5, 6, 7, 8]); @@ -654,24 +622,18 @@ describe("SendouQ", () => { await users.create(10); }); - afterEach(async () => { - await dbReset(); - }); - test("groups with closer skill sorted first", async () => { await createSkill(1, 1000); await createSkill(2, 1050); await createSkill(3, 500); await createSkill(4, 2000); - await createGroup([1]); - await createGroup([2]); - await createGroup([3]); - await createGroup([4]); - await db - .updateTable("Group") - .set({ latestActionAt: databaseTimestampNow() }) - .execute(); + await alignLatestActionAt([ + await createGroup([1]), + await createGroup([2]), + await createGroup([3]), + await createGroup([4]), + ]); await refreshSendouQInstance(); @@ -710,21 +672,15 @@ describe("SendouQ", () => { await createSkill(3, 1000); const group1Id = await createGroup([2]); - await new Promise((resolve) => setTimeout(resolve, 10)); const group2Id = await createGroup([3]); - const currentTimeInSeconds = Math.floor(Date.now() / 1000); - await db - .updateTable("Group") - .set({ latestActionAt: currentTimeInSeconds - 100 }) - .where("id", "=", group1Id) - .execute(); - - await db - .updateTable("Group") - .set({ latestActionAt: currentTimeInSeconds - 50 }) - .where("id", "=", group2Id) - .execute(); + const now = new Date(); + await backdate("Group", group1Id, { + latestActionAt: subSeconds(now, 100), + }); + await backdate("Group", group2Id, { + latestActionAt: subSeconds(now, 50), + }); await createGroup([1]); await refreshSendouQInstance(); diff --git a/app/features/sendouq/core/default-maps.server.test.ts b/app/features/sendouq/core/default-maps.server.test.ts index 7aeda9f04..64f3fe3f8 100644 --- a/app/features/sendouq/core/default-maps.server.test.ts +++ b/app/features/sendouq/core/default-maps.server.test.ts @@ -4,7 +4,6 @@ import * as UserFactory from "~/db/seed/factories/UserFactory"; import { db } from "~/db/sql"; import type { UserMapModePreferences } from "~/db/tables-json"; import type { StageId } from "~/modules/in-game-lists/types"; -import { dbReset } from "~/utils/Test"; import { SENDOUQ_BEST_OF } from "../q-constants"; import { clearCacheForTesting, @@ -29,7 +28,6 @@ describe("getDefaultMapWeights()", () => { afterEach(async () => { vi.restoreAllMocks(); - await dbReset(); }); test("returns empty map when no season is found", async () => { @@ -377,8 +375,9 @@ async function createUsersWithPreferences({ async function createUserWithPoollessPreferences(seasonNth: number) { const user = await UserFactory.create(); - // written directly because every write of the column since the map pool was - // added stores one, so only rows predating it can be missing it + // every write of the column since the map pool was added stores one, so only rows + // predating it can be missing it + // biome-ignore lint/plugin: no production write leaves out the pool await db .updateTable("User") .set({ mapModePreferences: JSON.stringify({ modes: [] }) }) diff --git a/app/features/sendouq/routes/q.looking.test.ts b/app/features/sendouq/routes/q.looking.test.ts index d2fd50a25..ff2f07ab9 100644 --- a/app/features/sendouq/routes/q.looking.test.ts +++ b/app/features/sendouq/routes/q.looking.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { beforeEach, describe, expect, test, vi } from "vitest"; vi.mock("~/features/chat/ChatSystemMessage.server", () => ({ send: vi.fn(), @@ -14,7 +14,7 @@ import { BANNED_MAPS } from "~/features/match-profile/banned-maps"; import * as MatchProfileRepository from "~/features/match-profile/MatchProfileRepository.server"; import { stageIds } from "~/modules/in-game-lists/stage-ids"; import invariant from "~/utils/invariant"; -import { dbReset, withUserId, wrappedAction } from "~/utils/Test"; +import { withUserId, wrappedAction } from "~/utils/Test"; import { refreshSendouQInstance } from "../core/SendouQ.server"; import { FULL_GROUP_SIZE } from "../q-constants"; import type { lookingSchema } from "../q-schemas.server"; @@ -103,10 +103,6 @@ describe("SendouQ match creation", () => { await refreshSendouQInstance(); }); - afterEach(async () => { - await dbReset(); - }); - test("adds pools to memento", async () => { await createMatch(); diff --git a/app/features/team/actions/t.$customUrl.edit.server.test.ts b/app/features/team/actions/t.$customUrl.edit.server.test.ts index a9c00677c..76c918abc 100644 --- a/app/features/team/actions/t.$customUrl.edit.server.test.ts +++ b/app/features/team/actions/t.$customUrl.edit.server.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it } from "vitest"; import { REGULAR_USER_TEST_ID } from "~/db/seed/constants"; import * as ImageFactory from "~/db/seed/factories/ImageFactory"; import * as TeamFactory from "~/db/seed/factories/TeamFactory"; @@ -7,7 +7,7 @@ import * as ImageRepository from "~/features/img-upload/ImageRepository.server"; import * as TeamRepository from "~/features/team/TeamRepository.server"; import invariant from "~/utils/invariant"; import { clampThemeToGamut } from "~/utils/oklch-gamut"; -import { dbReset, wrappedAction } from "~/utils/Test"; +import { wrappedAction } from "~/utils/Test"; import type { editTeamActionSchema } from "../team-schemas.server"; import { action as _editTeamProfileAction } from "./t.$customUrl.edit.server"; @@ -53,9 +53,6 @@ describe("team page editing", () => { ownerUserId: REGULAR_USER_TEST_ID, }); }); - afterEach(async () => { - await dbReset(); - }); it("sets a custom theme via UPDATE_CUSTOM_THEME", async () => { const response = await editTeamProfileAction( diff --git a/app/features/team/actions/t.new.server.test.ts b/app/features/team/actions/t.new.server.test.ts index e5feb5c01..b5ecf5bc8 100644 --- a/app/features/team/actions/t.new.server.test.ts +++ b/app/features/team/actions/t.new.server.test.ts @@ -1,6 +1,6 @@ -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it } from "vitest"; import * as UserFactory from "~/db/seed/factories/UserFactory"; -import { dbReset, wrappedAction } from "~/utils/Test"; +import { wrappedAction } from "~/utils/Test"; import { action as teamIndexPageAction } from "../actions/t.new.server"; import type { createTeamSchema } from "../team-schemas"; @@ -13,9 +13,6 @@ describe("team creation", () => { beforeEach(async () => { await UserFactory.createRegular(); }); - afterEach(async () => { - await dbReset(); - }); it("prevents creating a team with a duplicate name", async () => { await action({ name: "Team 1" }, { user: "regular" }); diff --git a/app/features/team/routes/t.$customUrl.edit.test.ts b/app/features/team/routes/t.$customUrl.edit.test.ts index e22fcfa3d..e5f73a755 100644 --- a/app/features/team/routes/t.$customUrl.edit.test.ts +++ b/app/features/team/routes/t.$customUrl.edit.test.ts @@ -1,6 +1,6 @@ -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it } from "vitest"; import * as UserFactory from "~/db/seed/factories/UserFactory"; -import { dbReset, wrappedAction } from "~/utils/Test"; +import { wrappedAction } from "~/utils/Test"; import { action as teamIndexPageAction } from "../actions/t.new.server"; import { action as _editTeamAction } from "../routes/t.$customUrl.edit"; import type { createTeamSchema, editTeamFormSchema } from "../team-schemas"; @@ -27,9 +27,6 @@ describe("team creation", () => { beforeEach(async () => { await UserFactory.createRegular(); }); - afterEach(async () => { - await dbReset(); - }); it("can't take another team's name via editing", async () => { await createTeamAction({ name: "Team 1" }, { user: "regular" }); diff --git a/app/features/team/routes/t.$customUrl.test.ts b/app/features/team/routes/t.$customUrl.test.ts index 69085297e..5eb673afd 100644 --- a/app/features/team/routes/t.$customUrl.test.ts +++ b/app/features/team/routes/t.$customUrl.test.ts @@ -1,9 +1,9 @@ -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it } from "vitest"; import { REGULAR_USER_TEST_ID } from "~/db/seed/constants"; import * as TeamFactory from "~/db/seed/factories/TeamFactory"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import { ADMIN_ID } from "~/features/admin/admin-constants"; -import { assertResponseErrored, dbReset, wrappedAction } from "~/utils/Test"; +import { assertResponseErrored, wrappedAction } from "~/utils/Test"; import { action as _teamPageAction } from "../actions/t.$customUrl.index.server"; import { action as teamIndexPageAction } from "../actions/t.new.server"; import * as TeamRepository from "../TeamRepository.server"; @@ -44,9 +44,6 @@ describe("Secondary teams", () => { await UserFactory.createAdmin(); await UserFactory.createRegular(); }); - afterEach(async () => { - await dbReset(); - }); it("first team created becomes main team", async () => { await createTeamAction({ name: "Team 1" }, { user: "regular" }); @@ -169,9 +166,6 @@ describe("Secondary teams as patron", () => { beforeEach(async () => { await UserFactory.createRegular(null, { patronTier: 2 }); }); - afterEach(async () => { - await dbReset(); - }); it("creates more than 2 teams as patron", async () => { await createTeamAction({ name: "Team 1" }, { user: "regular" }); diff --git a/app/features/top-search/XRankPlacementRepository.server.test.ts b/app/features/top-search/XRankPlacementRepository.server.test.ts index 66ae10392..117dc0a8e 100644 --- a/app/features/top-search/XRankPlacementRepository.server.test.ts +++ b/app/features/top-search/XRankPlacementRepository.server.test.ts @@ -1,9 +1,8 @@ -import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import * as XRankPlacementFactory from "~/db/seed/factories/XRankPlacementFactory"; import { db } from "~/db/sql"; import type { MainWeaponId } from "~/modules/in-game-lists/types"; -import { dbReset } from "~/utils/Test"; import * as XRankPlacementRepository from "./XRankPlacementRepository.server"; const SPLATTERSHOT: MainWeaponId = 40; @@ -22,14 +21,6 @@ const findTenStarWeapons = () => db.selectFrom("TenStarWeapon").selectAll().execute(); describe("refreshAllPeakXp", () => { - beforeEach(async () => { - await dbReset(); - }); - - afterEach(async () => { - await dbReset(); - }); - test("sets peakXp to max power for each player", async () => { for (const power of [2500, 2700, 2600]) { await XRankPlacementFactory.create({ playerSplId: "player-1", power }); @@ -93,14 +84,9 @@ describe("refreshTenStarWeapons", () => { let user: { id: number }; beforeEach(async () => { - await dbReset(); user = await UserFactory.create(); }); - afterEach(async () => { - await dbReset(); - }); - test("JPN placement qualifies regardless of rank", async () => { await XRankPlacementFactory.create({ playerSplId: "player-1", @@ -192,14 +178,6 @@ describe("refreshTenStarWeapons", () => { }); describe("refreshTenStarWeapons with userId", () => { - beforeEach(async () => { - await dbReset(); - }); - - afterEach(async () => { - await dbReset(); - }); - test("only affects the target user", async () => { const [user1, user2] = await UserFactory.createMany(2); diff --git a/app/features/tournament-lfg/TournamentLFGRepository.server.test.ts b/app/features/tournament-lfg/TournamentLFGRepository.server.test.ts index c3edcdf75..19234b6ed 100644 --- a/app/features/tournament-lfg/TournamentLFGRepository.server.test.ts +++ b/app/features/tournament-lfg/TournamentLFGRepository.server.test.ts @@ -1,9 +1,9 @@ -import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import * as TournamentFactory from "~/db/seed/factories/TournamentFactory"; import * as TournamentTeamFactory from "~/db/seed/factories/TournamentTeamFactory"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import { db } from "~/db/sql"; -import { dbReset, withUserId } from "~/utils/Test"; +import { withUserId } from "~/utils/Test"; import * as TournamentLFGRepository from "./TournamentLFGRepository.server"; const users = UserFactory.pool(); @@ -29,10 +29,6 @@ describe("insertPlaceholderTeam", () => { await users.create(2); }); - afterEach(async () => { - await dbReset(); - }); - test("creates a placeholder team with owner member", async () => { const tournament = await createTournament(); const team = await createPlaceholder(tournament.id, users.id(1)); @@ -63,10 +59,6 @@ describe("findLookingTeamsByTournamentId", () => { await users.create(3); }); - afterEach(async () => { - await dbReset(); - }); - test("returns looking teams for given tournament", async () => { const tournament = await createTournament(); await createPlaceholder(tournament.id, users.id(1)); @@ -122,10 +114,6 @@ describe("insertLike / deleteLike", () => { await users.create(2); }); - afterEach(async () => { - await dbReset(); - }); - test("adds a like between two teams", async () => { const tournament = await createTournament(); const team1 = await createPlaceholder(tournament.id, users.id(1)); @@ -186,10 +174,6 @@ describe("findAllLikesByTeamId", () => { await users.create(3); }); - afterEach(async () => { - await dbReset(); - }); - test("separates likes into given and received", async () => { const tournament = await createTournament(); const team1 = await createPlaceholder(tournament.id, users.id(1)); @@ -229,10 +213,6 @@ describe("startLooking", () => { await users.create(3); }); - afterEach(async () => { - await dbReset(); - }); - test("generates chatCode for a 2+ member team", async () => { const tournament = await createTournament(); const team = await createRegisteredTeam(tournament.id, [ @@ -278,6 +258,9 @@ describe("startLooking", () => { users.id(1), users.id(2), ]); + // the only production write of the column is `startLooking` itself, which + // invents a random code + // biome-ignore lint/plugin: no production write sets a known chatCode await db .updateTable("TournamentTeam") .set({ chatCode: "existing-code" }) @@ -295,10 +278,6 @@ describe("mergeTeams", () => { await users.create(5); }); - afterEach(async () => { - await dbReset(); - }); - test("merges two teams, other team is deleted", async () => { const tournament = await createTournament(); const team1 = await createPlaceholder(tournament.id, users.id(1)); @@ -403,6 +382,7 @@ describe("mergeTeams", () => { const team1 = await createPlaceholder(tournament.id, users.id(1)); const team2 = await createPlaceholder(tournament.id, users.id(2)); + // biome-ignore lint/plugin: as above, a known chatCode has no production write await db .updateTable("TournamentTeam") .set({ chatCode: "other-code" }) @@ -450,7 +430,9 @@ describe("mergeTeams", () => { const team1 = await createPlaceholder(tournament.id, users.id(1)); const team2 = await createPlaceholder(tournament.id, users.id(2)); - // user 2 was looking before user 1 i.e. has an older createdAt + // user 2 was looking before user 1 i.e. has an older createdAt. The column + // defaults in SQL, and the table has no id of its own for `backdate` to key on + // biome-ignore lint/plugin: no production write sets the timestamp await db .updateTable("TournamentTeamMember") .set({ createdAt: 1000 }) @@ -480,10 +462,6 @@ describe("updateTeamNote", () => { await users.create(1); }); - afterEach(async () => { - await dbReset(); - }); - test("sets and clears a team note", async () => { const tournament = await createTournament(); const team = await createPlaceholder(tournament.id, users.id(1)); @@ -515,10 +493,6 @@ describe("updateMemberRole", () => { await users.create(2); }); - afterEach(async () => { - await dbReset(); - }); - test("changes role from REGULAR to MANAGER", async () => { const tournament = await createTournament(); const team = await TournamentTeamFactory.create({ @@ -559,10 +533,6 @@ describe("updateStayAsSub", () => { await users.create(1); }); - afterEach(async () => { - await dbReset(); - }); - test("toggles isStayAsSub on/off", async () => { const tournament = await createTournament(); const team = await createPlaceholder(tournament.id, users.id(1)); @@ -598,10 +568,6 @@ describe("leaveLfg", () => { await users.create(3); }); - afterEach(async () => { - await dbReset(); - }); - test("deletes placeholder team when last member leaves", async () => { const tournament = await createTournament(); await createPlaceholder(tournament.id, users.id(1)); @@ -650,10 +616,6 @@ describe("findAllSubsByTournamentId", () => { await users.create(2); }); - afterEach(async () => { - await dbReset(); - }); - test("returns userIds with isStayAsSub", async () => { const tournament = await createTournament(); await TournamentLFGRepository.insertPlaceholderTeam({ diff --git a/app/features/tournament-match/TournamentMatchRepository.server.test.ts b/app/features/tournament-match/TournamentMatchRepository.server.test.ts index bfe72965f..022c04b08 100644 --- a/app/features/tournament-match/TournamentMatchRepository.server.test.ts +++ b/app/features/tournament-match/TournamentMatchRepository.server.test.ts @@ -1,9 +1,8 @@ -import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import * as TournamentFactory from "~/db/seed/factories/TournamentFactory"; import * as TournamentTeamFactory from "~/db/seed/factories/TournamentTeamFactory"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import type { TournamentSettings } from "~/db/tables-json"; -import { dbReset } from "~/utils/Test"; import * as TournamentMatchRepository from "./TournamentMatchRepository.server"; const TEAMS_PER_POOL = 2; @@ -34,10 +33,6 @@ describe("findByTournamentTeamId", () => { await users.create(TEAM_COUNT); }); - afterEach(async () => { - await dbReset(); - }); - test("preserves stage order: matches from an earlier stage come first even when later stage has lower group numbers", async () => { // the pools stage numbers its groups 1..2 while the final is group 1 of its // own stage, so the team page has to order by stage before group diff --git a/app/features/tournament-match/routes/to.$id.matches.$mid.test.ts b/app/features/tournament-match/routes/to.$id.matches.$mid.test.ts index 438ea5115..75abe72e1 100644 --- a/app/features/tournament-match/routes/to.$id.matches.$mid.test.ts +++ b/app/features/tournament-match/routes/to.$id.matches.$mid.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("~/features/chat/ChatSystemMessage.server", () => ({ send: vi.fn(), @@ -16,7 +16,6 @@ import type { matchSchema } from "~/features/tournament-bracket/tournament-brack import type { SerializeFrom } from "~/utils/remix"; import { assertResponseErrored, - dbReset, wrappedAction, wrappedLoader, } from "~/utils/Test"; @@ -104,10 +103,6 @@ describe("Tournament match page", () => { await TournamentFactory.startBracket(tournament.id); }); - afterEach(async () => { - await dbReset(); - }); - describe("results", () => { it("is empty array for new match", async () => { const data = await loadMatchData(); @@ -244,9 +239,9 @@ describe("Tournament match page", () => { describe("locked match", () => { it("should return error when reporting score for a match waiting on previous matches", async () => { await setActiveRosterAction(); - // written directly rather than seeded: the state under test is one an - // earlier match of a larger bracket puts this row in, not one the match - // was created in + // the state under test is one an earlier match of a larger bracket puts this + // row in, not one the match was created in + // biome-ignore lint/plugin: written rather than seeded, see above await db .updateTable("TournamentMatch") .set({ opponentOne: JSON.stringify({ id: null }) }) @@ -263,6 +258,7 @@ describe("Tournament match page", () => { // as above: a BYE and a TBD opponent are states the surrounding bracket // produces, so they are written here rather than seeded it("should 404 when accessing a BYE match", async () => { + // biome-ignore lint/plugin: as above await db .updateTable("TournamentMatch") .set({ opponentTwo: null }) @@ -273,6 +269,7 @@ describe("Tournament match page", () => { }); it("should not 404 when an opponent is a TBD placeholder waiting for an earlier match", async () => { + // biome-ignore lint/plugin: as above await db .updateTable("TournamentMatch") .set({ opponentTwo: JSON.stringify({ id: null }) }) diff --git a/app/features/tournament-organization/TournamentOrganizationRepository.server.test.ts b/app/features/tournament-organization/TournamentOrganizationRepository.server.test.ts index e09b11e3c..a74360a60 100644 --- a/app/features/tournament-organization/TournamentOrganizationRepository.server.test.ts +++ b/app/features/tournament-organization/TournamentOrganizationRepository.server.test.ts @@ -1,7 +1,6 @@ -import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import * as TournamentOrganizationFactory from "~/db/seed/factories/TournamentOrganizationFactory"; import * as UserFactory from "~/db/seed/factories/UserFactory"; -import { dbReset } from "~/utils/Test"; import * as TournamentOrganizationRepository from "./TournamentOrganizationRepository.server"; import { seedOrgEventWithParticipants } from "./test-utils"; @@ -12,10 +11,6 @@ describe("findByUserId", () => { await users.create(3); }); - afterEach(async () => { - await dbReset(); - }); - test("returns organizations where user is a member", async () => { const [org1, org2] = await TournamentOrganizationFactory.createMany(2, { ownerId: users.id(1), @@ -80,10 +75,6 @@ describe("countActiveParticipants", () => { await users.create(5); }); - afterEach(async () => { - await dbReset(); - }); - test("counts distinct participants across the organization's events in the window", async () => { const org = await TournamentOrganizationFactory.create({ ownerId: users.id(1), diff --git a/app/features/tournament-organization/routes/org.$slug.stats.test.ts b/app/features/tournament-organization/routes/org.$slug.stats.test.ts index 13d7f0259..5c6e68d13 100644 --- a/app/features/tournament-organization/routes/org.$slug.stats.test.ts +++ b/app/features/tournament-organization/routes/org.$slug.stats.test.ts @@ -3,7 +3,7 @@ import * as TournamentOrganizationFactory from "~/db/seed/factories/TournamentOr import * as UserFactory from "~/db/seed/factories/UserFactory"; import { dateToDatabaseTimestamp } from "~/utils/dates"; import type { SerializeFrom } from "~/utils/remix"; -import { dbReset, wrappedLoader } from "~/utils/Test"; +import { wrappedLoader } from "~/utils/Test"; import { loader } from "../loaders/org.$slug.stats.server"; import { seedOrgEventWithParticipants } from "../test-utils"; import { ESTABLISHED_ORG } from "../tournament-organization-constants"; @@ -24,7 +24,6 @@ describe("org stats loader", () => { afterEach(async () => { vi.useRealTimers(); - await dbReset(); }); test("throws when the user is not an org admin", async () => { diff --git a/app/features/tournament/TournamentAuditLogRepository.server.test.ts b/app/features/tournament/TournamentAuditLogRepository.server.test.ts index a82722570..755e1b4c1 100644 --- a/app/features/tournament/TournamentAuditLogRepository.server.test.ts +++ b/app/features/tournament/TournamentAuditLogRepository.server.test.ts @@ -1,11 +1,11 @@ -import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import * as TournamentFactory from "~/db/seed/factories/TournamentFactory"; import * as TournamentTeamFactory from "~/db/seed/factories/TournamentTeamFactory"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import { db } from "~/db/sql"; import type { Tables } from "~/db/tables"; import type { TournamentAuditLogMetadata } from "~/db/tables-json"; -import { dbReset, withUserId } from "~/utils/Test"; +import { withUserId } from "~/utils/Test"; import * as TournamentAuditLogRepository from "./TournamentAuditLogRepository.server"; import * as TournamentTeamRepository from "./TournamentTeamRepository.server"; @@ -55,10 +55,6 @@ describe("TournamentAuditLogRepository", () => { [actor, subject] = await UserFactory.createMany(3); }); - afterEach(async () => { - await dbReset(); - }); - test("insert creates a stable history row from the live team", async () => { const tournament = await createTournament(); const team = await createTeam(tournament.id, "Team Olive"); diff --git a/app/features/tournament/TournamentRepository.finalize.test.ts b/app/features/tournament/TournamentRepository.finalize.test.ts index 33a2db4a1..5e5450eae 100644 --- a/app/features/tournament/TournamentRepository.finalize.test.ts +++ b/app/features/tournament/TournamentRepository.finalize.test.ts @@ -1,9 +1,8 @@ -import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import * as TournamentFactory from "~/db/seed/factories/TournamentFactory"; import * as TournamentTeamFactory from "~/db/seed/factories/TournamentTeamFactory"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import { db } from "~/db/sql"; -import { dbReset } from "~/utils/Test"; import type { TournamentSummary } from "../tournament-bracket/core/summarizer.server"; import * as TournamentRepository from "./TournamentRepository.server"; @@ -42,9 +41,6 @@ describe("TournamentRepository.finalize", () => { // four users so that the "1-2-3-4" team identifier the tests use names real ones [player] = await UserFactory.createMany(4); }); - afterEach(async () => { - await dbReset(); - }); test("matchesCount on a new season's Skill row does not include prior seasons", async () => { await finalizePriorSeason({ diff --git a/app/features/user-card/UserCardRepository.server.test.ts b/app/features/user-card/UserCardRepository.server.test.ts index d49c70199..4bc874f5a 100644 --- a/app/features/user-card/UserCardRepository.server.test.ts +++ b/app/features/user-card/UserCardRepository.server.test.ts @@ -1,9 +1,9 @@ -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it } from "vitest"; import * as ImageFactory from "~/db/seed/factories/ImageFactory"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import * as XRankPlacementFactory from "~/db/seed/factories/XRankPlacementFactory"; -import { db } from "~/db/sql"; -import { dbReset, withNoUser, withUserId } from "~/utils/Test"; +import * as UserRepository from "~/features/user-page/UserRepository.server"; +import { withNoUser, withUserId } from "~/utils/Test"; import * as UserCardRepository from "./UserCardRepository.server"; import type { UserCardData } from "./user-card-types"; @@ -30,10 +30,6 @@ describe("UserCardRepository.findAllByUserIds", () => { [owner, other] = await UserFactory.createMany(2); }); - afterEach(async () => { - await dbReset(); - }); - it("returns an empty map when given no user ids", async () => { const { userCards } = await withNoUser(() => UserCardRepository.findAllByUserIds({ @@ -46,11 +42,7 @@ describe("UserCardRepository.findAllByUserIds", () => { it("keys cards by user id and builds the stats array from db fields", async () => { const plusMember = await UserFactory.create(null, { plusTier: 2 }); - await db - .updateTable("User") - .set({ div: "1" }) - .where("id", "=", plusMember.id) - .execute(); + await UserRepository.updateManyDivs([{ userId: plusMember.id, div: "1" }]); await insertVerifiedXp(plusMember.id, 2500); const { userCards } = await withNoUser(() => diff --git a/app/features/user-page/UserRepository.test.ts b/app/features/user-page/UserRepository.test.ts index c338c38cf..0f8ba330e 100644 --- a/app/features/user-page/UserRepository.test.ts +++ b/app/features/user-page/UserRepository.test.ts @@ -1,13 +1,8 @@ -import { afterEach, describe, expect, test } from "vitest"; +import { describe, expect, test } from "vitest"; import * as UserFactory from "~/db/seed/factories/UserFactory"; -import { dbReset } from "~/utils/Test"; import * as UserRepository from "./UserRepository.server"; describe("UserRepository", () => { - afterEach(async () => { - await dbReset(); - }); - test("created user has createdAt field", async () => { await UserRepository.upsert({ discordId: "1", diff --git a/app/features/user-page/routes/u.$identifier.edit.test.ts b/app/features/user-page/routes/u.$identifier.edit.test.ts index cafd3360c..da454057c 100644 --- a/app/features/user-page/routes/u.$identifier.edit.test.ts +++ b/app/features/user-page/routes/u.$identifier.edit.test.ts @@ -1,7 +1,7 @@ -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it } from "vitest"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import type { MainWeaponId } from "~/modules/in-game-lists/types"; -import { dbReset, wrappedAction } from "~/utils/Test"; +import { wrappedAction } from "~/utils/Test"; import type { userEditProfileBaseSchema } from "../user-page-schemas"; import { action as editUserProfileAction } from "./u.$identifier.edit"; @@ -34,9 +34,6 @@ describe("user page editing", () => { beforeEach(async () => { userId = (await UserFactory.createRegular()).id; }); - afterEach(async () => { - await dbReset(); - }); it("saves profile with default fields", async () => { const response = await action( diff --git a/app/features/vods/VodRepository.server.test.ts b/app/features/vods/VodRepository.server.test.ts index 9f823ac62..dca014697 100644 --- a/app/features/vods/VodRepository.server.test.ts +++ b/app/features/vods/VodRepository.server.test.ts @@ -1,7 +1,6 @@ -import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import * as VodFactory from "~/db/seed/factories/VodFactory"; -import { dbReset } from "~/utils/Test"; import * as VodRepository from "./VodRepository.server"; const users = UserFactory.pool(); @@ -11,10 +10,6 @@ describe("findByUserId", () => { await users.create(5); }); - afterEach(async () => { - await dbReset(); - }); - test("returns vods for a specific user", async () => { await VodFactory.create({ submitterUserId: users.id(1), @@ -62,10 +57,6 @@ describe("findVods", () => { await users.create(5); }); - afterEach(async () => { - await dbReset(); - }); - test("filters by weapon", async () => { const vod = await VodFactory.create({ submitterUserId: users.id(1), @@ -140,10 +131,6 @@ describe("findVodById", () => { await users.create(5); }); - afterEach(async () => { - await dbReset(); - }); - test("returns null when vod doesn't exist", async () => { const result = await VodRepository.findVodById(999); @@ -181,10 +168,6 @@ describe("insert", () => { await users.create(5); }); - afterEach(async () => { - await dbReset(); - }); - test("inserts vod with all metadata", async () => { const result = await VodRepository.insert({ title: "Complete VOD", @@ -301,10 +284,6 @@ describe("update", () => { await users.create(5); }); - afterEach(async () => { - await dbReset(); - }); - test("updates vod metadata", async () => { const vod = await VodFactory.create({ submitterUserId: users.id(1), @@ -385,10 +364,6 @@ describe("deleteById", () => { await users.create(5); }); - afterEach(async () => { - await dbReset(); - }); - test("deletes vod by id", async () => { const vod = await VodFactory.create({ submitterUserId: users.id(1) }); diff --git a/app/routines/closeExpiredContinueVotes.test.ts b/app/routines/closeExpiredContinueVotes.test.ts index 1f11ab213..4f8d7484d 100644 --- a/app/routines/closeExpiredContinueVotes.test.ts +++ b/app/routines/closeExpiredContinueVotes.test.ts @@ -14,7 +14,7 @@ import * as UserFactory from "~/db/seed/factories/UserFactory"; import { db } from "~/db/sql"; import { FULL_GROUP_SIZE } from "~/features/sendouq/q-constants"; import * as GroupMatchContinueVoteRepository from "~/features/sendouq-match/GroupMatchContinueVoteRepository.server"; -import { dbReset, withUserId } from "~/utils/Test"; +import { withUserId } from "~/utils/Test"; import { CloseExpiredContinueVotesRoutine } from "./closeExpiredContinueVotes"; let alphaUserIds: number[]; @@ -64,7 +64,6 @@ describe("CloseExpiredContinueVotesRoutine", () => { beforeEach(async () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-01-15T12:00:00Z")); - await dbReset(); const users = await UserFactory.createMany(FULL_GROUP_SIZE * 2); alphaUserIds = users.slice(0, FULL_GROUP_SIZE).map((user) => user.id); diff --git a/app/routines/notifyCheckInStart.test.ts b/app/routines/notifyCheckInStart.test.ts index 778d20a49..45ad6165d 100644 --- a/app/routines/notifyCheckInStart.test.ts +++ b/app/routines/notifyCheckInStart.test.ts @@ -4,7 +4,6 @@ import * as TournamentFactory from "~/db/seed/factories/TournamentFactory"; import * as UserFactory from "~/db/seed/factories/UserFactory"; import { clearAllTournamentDataCache } from "~/features/tournament-bracket/core/Tournament.server"; import { dateToDatabaseTimestamp } from "~/utils/dates"; -import { dbReset } from "~/utils/Test"; import { NotifyCheckInStartRoutine } from "./notifyCheckInStart"; let author: { id: number }; @@ -22,7 +21,6 @@ describe("NotifyCheckInStartRoutine", () => { beforeEach(async () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2025-01-15T12:00:00Z")); - await dbReset(); clearAllTournamentDataCache(); [author, otherAuthor] = await UserFactory.createMany(2); mockNotify.mockClear(); diff --git a/app/routines/syncLiveStreams.test.ts b/app/routines/syncLiveStreams.test.ts index 96e15439a..e585da8ad 100644 --- a/app/routines/syncLiveStreams.test.ts +++ b/app/routines/syncLiveStreams.test.ts @@ -6,7 +6,6 @@ import { testTournament, tournamentCtxTeam, } from "~/features/tournament-bracket/core/tests/test-utils"; -import { dbReset } from "~/utils/Test"; import { SyncLiveStreamsRoutine } from "./syncLiveStreams"; const { mockGetStreams } = vi.hoisted(() => ({ @@ -50,7 +49,6 @@ describe("syncLiveStreams tournament streamers", () => { add(new Date("2025-01-15T12:00:00Z"), { minutes: timeOffset }), ); timeOffset += 31; - await dbReset(); RunningTournaments.clear(); mockGetStreams.mockReset(); }); diff --git a/app/routines/syncTournamentVods.test.ts b/app/routines/syncTournamentVods.test.ts index 59f9f21a5..845945c88 100644 --- a/app/routines/syncTournamentVods.test.ts +++ b/app/routines/syncTournamentVods.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { beforeEach, describe, expect, test, vi } from "vitest"; import { backdate } from "~/db/seed/core/backdate"; import * as TournamentFactory from "~/db/seed/factories/TournamentFactory"; import * as TournamentStreamerFactory from "~/db/seed/factories/TournamentStreamerFactory"; @@ -7,7 +7,6 @@ import * as UserFactory from "~/db/seed/factories/UserFactory"; import { db } from "~/db/sql"; import type { TournamentSettings } from "~/db/tables-json"; import * as TournamentRepository from "~/features/tournament/TournamentRepository.server"; -import { dbReset } from "~/utils/Test"; const { mockGetUsersByLogin, mockGetArchiveVideos } = vi.hoisted(() => ({ mockGetUsersByLogin: vi.fn(), @@ -50,16 +49,11 @@ let teams: Array<{ id: number; ownerUserId: number }>; describe("syncTournamentVods", () => { beforeEach(async () => { - await dbReset(); mockGetUsersByLogin.mockReset(); mockGetArchiveVideos.mockReset(); await users.create(TEAM_COUNT); }); - afterEach(async () => { - await dbReset(); - }); - test("player streamer gets VODs only for matches they participated in", async () => { await seedTournamentWithMatches(); const player = playerOf(firstMatch.winnerTeamId); @@ -224,7 +218,9 @@ describe("syncTournamentVods", () => { await seedTournamentWithMatches(); await seedStreamer("player_stream", playerOf(firstMatch.winnerTeamId)); - // clear startedAt on all matches + // clear startedAt on all matches: a played match that was never started is a + // state no production write leaves behind + // biome-ignore lint/plugin: written rather than seeded, see above await db.updateTable("TournamentMatch").set({ startedAt: null }).execute(); await runProcessOneTournament(); diff --git a/app/test-setup.ts b/app/test-setup.ts index 74b96233c..7a934d32c 100644 --- a/app/test-setup.ts +++ b/app/test-setup.ts @@ -1,4 +1,15 @@ -import { vi } from "vitest"; +import { afterEach, vi } from "vitest"; +import { isDatabaseDirty } from "~/db/write-tracker"; + +// Wipes the database after any test that wrote to it, so no test has to remember to. +// The flag keeps this free for the tests that never touch the database — importing +// `~/utils/Test` would open the connection for them too, hence the dynamic import. +afterEach(async () => { + if (!isDatabaseDirty()) return; + + const { dbReset } = await import("~/utils/Test"); + await dbReset(); +}); // after updating some packages got // Error: Cannot find module '/Users/kalle/Documents/personal/repos/sendou.ink/node_modules/@aws-sdk/core/dist-es/submodules/client/index' imported from diff --git a/app/utils/Test.ts b/app/utils/Test.ts index 6a750c157..a15a37ba3 100644 --- a/app/utils/Test.ts +++ b/app/utils/Test.ts @@ -10,6 +10,7 @@ import { REGULAR_USER_TEST_ID } from "~/db/seed/constants"; import { actAs } from "~/db/seed/core/actAs"; import { resetFactories } from "~/db/seed/core/defineFactory"; import { db } from "~/db/sql"; +import { markDatabaseClean } from "~/db/write-tracker"; import { ADMIN_ID } from "~/features/admin/admin-constants"; import { SESSION_KEY } from "~/features/auth/core/authenticator.server"; import { authSessionStorage } from "~/features/auth/core/session.server"; @@ -209,20 +210,9 @@ async function authHeader( * Resets all data in the database by deleting all rows from every table, * except for SQLite system tables and the 'migrations' table. * - * @example - * describe("My integration test", () => { - * beforeEach(async () => { - * await UserFactory.createMany(2); - * }); - * - * afterEach(async () => { - * await dbReset(); - * }); - * - * // tests go here - * }); + * Tests do not call this — `app/test-setup.ts` runs it after every test that wrote + * anything. Call it by hand only to wipe *within* a test. */ -// xxx: make it automatic export const dbReset = async () => { // virtual tables and their shadow tables (e.g. UserSearch_data) can not be // deleted from directly; the fts index stays in sync via the User triggers @@ -246,4 +236,6 @@ export const dbReset = async () => { await sql`PRAGMA foreign_keys = ON`.execute(db); resetFactories(); + // last, because the deletes above are themselves writes + markDatabaseClean(); }; diff --git a/biome-plugins/no-raw-db-writes-in-tests.grit b/biome-plugins/no-raw-db-writes-in-tests.grit new file mode 100644 index 000000000..c3cfa3d45 --- /dev/null +++ b/biome-plugins/no-raw-db-writes-in-tests.grit @@ -0,0 +1,15 @@ +language js + +// Tests arrange their state through `app/db/seed/factories`, so that seeding +// exercises the same write paths the app does. A raw write is only allowed when +// no production write reaches the state at all, and then it has to say so: +// `// biome-ignore lint/plugin: ` +`db.$method($_)` as $write where { + $method <: or { + `insertInto`, + `updateTable`, + `deleteFrom`, + `replaceInto` + }, + register_diagnostic(span=$write, message="Tests must not write to the database directly. Use a factory from `app/db/seed/factories`, or a repository write function if the test is about that write. If no production write reaches this state, keep the raw write and suppress with `// biome-ignore lint/plugin: `.", severity="error") +} diff --git a/biome.json b/biome.json index 145afaaba..78baea4d2 100644 --- a/biome.json +++ b/biome.json @@ -71,5 +71,11 @@ "parser": { "cssModules": true } - } + }, + "overrides": [ + { + "includes": ["**/*.test.ts", "**/*.test.tsx"], + "plugins": ["./biome-plugins/no-raw-db-writes-in-tests.grit"] + } + ] } diff --git a/docs/dev/repositories.md b/docs/dev/repositories.md index db2476c8c..ff5023b75 100644 --- a/docs/dev/repositories.md +++ b/docs/dev/repositories.md @@ -63,6 +63,7 @@ export async function addInitialSkill(args: AddInitialSkillArgs, trx?: Transacti - A missing row is `undefined` — what Kysely already gives us. Don't map it to `null`. - Return plain data. Never return a Kysely query builder or a raw `sql` fragment from a repository; shared SQL fragments live in `~/utils/kysely.server`. - Convert at the boundary so callers deal in domain values: take a `boolean` and write it with `toDBBoolean`, take a `Date` and write it with `dateToDatabaseTimestamp`. +- **A write returns the inserted row's id when the row has an id** — that is, when other rows can reference it. `insert`, `upsert`, `addInitialSkill`, `insertFriendship` and friends all `.returning("id")` (or `.returningAll()` where the caller wants the row), and a bulk insert returns an array in insertion order. Writes to join and detail tables, keyed by their foreign keys rather than an id of their own (`GroupMember`, `MapPoolMap`, `PlusVote`, `AllTeamMember`), return nothing — there is no value a caller could use. ## Types @@ -143,3 +144,5 @@ Not every function needs a test. Write one for: - Permission, visibility or ownership scoping Plain `findById`-style queries don't need one. Tests run against `db-test.sqlite3`. + +A test's setup goes through the factories in `app/db/seed/factories`, never a raw `db.insertInto` / `db.updateTable` — a lint rule enforces this, and [seeds.md](./seeds.md) covers the factories and the rare exceptions. Only the write a test is actually asserting about is called through its repository directly. There is no cleanup to write: the database is wiped after every test that wrote to it.