From 715906eefbd39e144c807707e6a1f42ca4c0bb95 Mon Sep 17 00:00:00 2001 From: zkldi <20380519+zkldi@users.noreply.github.com> Date: Fri, 22 Apr 2022 02:12:32 +0100 Subject: [PATCH] test: add tests for migrations --- server/src/lib/migration/migrations.test.ts | 125 ++++++++++++++++++ server/src/lib/migration/migrations.ts | 45 +++++-- server/src/test-utils/mock-db/migrations.json | 1 + server/src/test-utils/test-data.ts | 7 + server/src/utils/misc.ts | 3 +- 5 files changed, 166 insertions(+), 15 deletions(-) create mode 100644 server/src/lib/migration/migrations.test.ts create mode 100644 server/src/test-utils/mock-db/migrations.json diff --git a/server/src/lib/migration/migrations.test.ts b/server/src/lib/migration/migrations.test.ts new file mode 100644 index 000000000..3d5509d02 --- /dev/null +++ b/server/src/lib/migration/migrations.test.ts @@ -0,0 +1,125 @@ +import db from "external/mongo/db"; +import t from "tap"; +import ResetDBState from "test-utils/resets"; +import { FAKE_MIGRATION } from "test-utils/test-data"; +import { ApplyMigration, FindUnappliedMigrations } from "./migrations"; + +t.test("#FindUnappliedMigrations", (t) => { + t.beforeEach(ResetDBState); + + t.test("Should find all unapplied migrations", async (t) => { + const res = await FindUnappliedMigrations(); + + t.equal( + res.includes("fake-migration"), + true, + "Should contain fake-migration since it isn't applied." + ); + + t.end(); + }); + + t.test("Shouldn't return pending migrations", async (t) => { + await db.migrations.insert({ + migrationID: "fake-migration", + status: "pending", + }); + + const res = await FindUnappliedMigrations(); + + t.equal(res.includes("fake-migration"), false); + + t.end(); + }); + + t.test("Shouldn't return applied migrations", async (t) => { + await db.migrations.insert({ + migrationID: "fake-migration", + status: "applied", + appliedOn: 1000, + }); + + const res = await FindUnappliedMigrations(); + + t.equal(res.includes("fake-migration"), false); + + t.end(); + }); + + t.end(); +}); + +t.test("#ApplyMigration", (t) => { + t.beforeEach(ResetDBState); + + t.test("Should apply a migration.", async (t) => { + await ApplyMigration(FAKE_MIGRATION); + + const dbRes = await db.users.findOne({ + id: 1, + }); + + t.equal( + // @ts-expect-error Our migration is a fake one that adds __wasMigrated + // to all user documents. There's nothing special about this property name. + dbRes?.__wasMigrated, + true, + "Migration should be applied on the users." + ); + + t.end(); + }); + + t.test("Should panic and reset if something goes wrong.", async (t) => { + try { + await ApplyMigration({ + id: "bad-migration", + up: () => { + throw new Error("Failed to migrate."); + }, + down: () => + db.users.update({}, { $set: { __wasUnmigrated: true } }, { multi: true }), + }); + } catch (e) { + const err = e as Error; + t.equal(err?.message, "Was going to exit with statusCode 1, but we're in testing."); + } + + const dbRes = await db.users.findOne({ + id: 1, + }); + + t.equal( + // @ts-expect-error Our un-migration adds this property to check if it worked. + dbRes?.__wasUnmigrated, + true, + "Migration should be applied on the users." + ); + + t.end(); + }); + + t.test( + "Should panic and panic again if un-migration failed, with a scarier log message.", + async (t) => { + try { + await ApplyMigration({ + id: "bad-migration", + up: () => { + throw new Error("Failed to migrate."); + }, + down: () => { + throw new Error("Failed to undo migration."); + }, + }); + } catch (e) { + const err = e as Error; + t.equal(err?.message, "Was going to exit with statusCode 1, but we're in testing."); + } + + t.end(); + } + ); + + t.end(); +}); diff --git a/server/src/lib/migration/migrations.ts b/server/src/lib/migration/migrations.ts index 0202cc091..9da5a01b9 100644 --- a/server/src/lib/migration/migrations.ts +++ b/server/src/lib/migration/migrations.ts @@ -1,5 +1,7 @@ import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; +import { Environment } from "lib/setup/config"; +import { FAKE_MIGRATION } from "test-utils/test-data"; import { Migration } from "utils/types"; import UGPTRivalsMigration from "./migrations/add-rivals-to-ugpt"; @@ -8,7 +10,10 @@ const logger = CreateLogCtx(__filename); // Migrations are stored in an array because they have some concept of order // That is, migrations should ideally be applied in a fixed order just to avoid // any potential unsavoury interactions. -const REGISTERED_MIGRATIONS: Migration[] = [UGPTRivalsMigration]; +const REGISTERED_MIGRATIONS: Migration[] = + // If we're testing, we should pull fake migrations instead to ensure the tests + // stay consistent + Environment.nodeEnv === "test" ? [FAKE_MIGRATION] : [UGPTRivalsMigration]; function CreateMigrationLookupMap(migrations: Migration[]) { const map = new Map(); @@ -18,6 +23,8 @@ function CreateMigrationLookupMap(migrations: Migration[]) { logger.crit( `Multiple migrations are registered for ${mig.id}. Cannot safely apply migrations.` ); + + // note, we want to exit in testing here, this is fine. process.exit(1); } @@ -77,15 +84,32 @@ export async function ApplyUnappliedMigrations() { // Note that if any migration fails, this will exit at CRIT level // and not continue. // eslint-disable-next-line no-await-in-loop - await ApplyMigration(migrationID); + await ApplyMigrationByID(migrationID); } } +export function ApplyMigrationByID(migrationID: string) { + const migration = MIGRATION_LOOKUP.get(migrationID); + + if (!migration) { + logger.error( + `Attempted to apply migration ${migrationID}, but that migration doesn't exist?` + ); + throw new Error( + `Attempted to apply migration ${migrationID}, but that migration doesn't exist?` + ); + } + + return ApplyMigration(migration); +} + /** * Applies a migration. Will **EXIT TACHI** if a migration fails to apply, as this implies * database level inconsistencies which could cause severe problems. */ -export async function ApplyMigration(migrationID: string) { +export async function ApplyMigration(migration: Migration) { + const migrationID = migration.id; + logger.info(`Recieved request to apply migration '${migrationID}'.`); // Lock the migration here: @@ -112,17 +136,6 @@ export async function ApplyMigration(migrationID: string) { throw new Error(`Tried to apply migration ${migrationID}, but it was already applied.`); } - const migration = MIGRATION_LOOKUP.get(migrationID); - - if (!migration) { - logger.error( - `Attempted to apply migration ${migrationID}, but that migration doesn't exist?` - ); - throw new Error( - `Attempted to apply migration ${migrationID}, but that migration doesn't exist?` - ); - } - try { await migration.up(); @@ -169,6 +182,10 @@ export async function ApplyMigration(migrationID: string) { // remove the stale migration so it can be re-ran in the future. await db.migrations.findOneAndDelete({ migrationID }); + if (Environment.nodeEnv === "test") { + throw new Error("Was going to exit with statusCode 1, but we're in testing."); + } + process.exit(1); } } diff --git a/server/src/test-utils/mock-db/migrations.json b/server/src/test-utils/mock-db/migrations.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/server/src/test-utils/mock-db/migrations.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/server/src/test-utils/test-data.ts b/server/src/test-utils/test-data.ts index 48ac7b64b..f202c5e3a 100644 --- a/server/src/test-utils/test-data.ts +++ b/server/src/test-utils/test-data.ts @@ -28,6 +28,7 @@ import { UGPTSettings, } from "tachi-common"; import { ApplyNTimes, RFA } from "utils/misc"; +import { Migration } from "utils/types"; const file = (name: string) => path.join(__dirname, "/test-data", name); @@ -637,3 +638,9 @@ export const FakeGameSettings: UGPTSettings = { rivals: [], userID: 1, }; + +export const FAKE_MIGRATION: Migration = { + id: "fake-migration", + up: () => db.users.update({}, { $set: { __wasMigrated: true } }, { multi: true }), + down: () => db.users.update({}, { $unset: { __wasMigrated: 1 } }, { multi: true }), +}; diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index a89a36c1b..e58606b90 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -1,7 +1,8 @@ import { exec } from "child_process"; import crypto from "crypto"; import { ONE_HOUR } from "lib/constants/time"; -import { TachiConfig } from "lib/setup/config"; +import { Environment, TachiConfig } from "lib/setup/config"; +import { exit } from "process"; import { Game, GamePTConfig, GetGameConfig, integer, Playtype, Playtypes } from "tachi-common"; import { URL } from "url";