diff --git a/packages/better-auth/src/db/release-migration.ts b/packages/better-auth/src/db/release-migration.ts index b2d3e3aaa3..8b53cfbd18 100644 --- a/packages/better-auth/src/db/release-migration.ts +++ b/packages/better-auth/src/db/release-migration.ts @@ -1494,6 +1494,9 @@ export async function retireScimAccountsFrom16( DELETE FROM ${sql.table(accountTable)} WHERE ${sql.ref(idColumn)} IN (${sql.join(accounts.map((account) => account.id))}) `.execute(kysely); + // The upgrade requires a maintenance window with every SCIM and account + // writer stopped. Recheck here so an in-flight write that crossed the + // shutdown boundary blocks the migration before it can continue. const { accounts: remainingAccounts } = await readScimAccountsFrom16( config, state, diff --git a/packages/cli/test/migrate.test.ts b/packages/cli/test/migrate.test.ts index ffca4d8adc..8437387777 100644 --- a/packages/cli/test/migrate.test.ts +++ b/packages/cli/test/migrate.test.ts @@ -836,40 +836,56 @@ describe("migrate command modes", () => { /** * @see https://github.com/better-auth/better-auth/pull/10575#discussion_r3830648963 */ - it("reports release inspection failures as a blocked JSON plan", async () => { + it.each([ + "plan", + "apply", + ] as const)("reports release inspection failures as a blocked JSON %s result", async (mode) => { const db = new Database(":memory:"); const auth = betterAuth({ database: db }); vi.spyOn(config, "getConfig").mockImplementation(async () => auth.options); - vi.spyOn( - releaseMigration, - "inspectLegacyReleaseDataFrom16", - ).mockRejectedValueOnce(new Error("legacy data is unreadable")); + const inspectLegacyReleaseData = vi + .spyOn(releaseMigration, "inspectLegacyReleaseDataFrom16") + .mockRejectedValueOnce(new Error("legacy data is unreadable")); const migrationFile = await writeMigrationDecisions({ formatVersion: 1, migration: "1.6-to-1.7", }); const consoleLog = vi.spyOn(console, "log").mockImplementation(() => {}); - await expect( - migrateAction({ - cwd: process.cwd(), - migrationFile, - mode: "plan", - outputFormat: "json", - }), - ).resolves.toBeUndefined(); + try { + await expect( + migrateAction({ + approved: true, + cwd: process.cwd(), + migrationFile, + mode, + outputFormat: "json", + }), + ).resolves.toBeUndefined(); - expect(process.exitCode).toBe(1); - expect(JSON.parse(String(consoleLog.mock.calls[0]?.[0]))).toMatchObject({ - blockers: [ - { - code: "release-migration-error", - message: "legacy data is unreadable", - }, - ], - status: "blocked", - }); - process.exitCode = undefined; + expect(inspectLegacyReleaseData).toHaveBeenCalledOnce(); + if (mode === "plan") { + expect(process.exitCode).toBe(1); + } else { + expect(process.exit).toHaveBeenCalledWith(1); + } + const output = JSON.parse(String(consoleLog.mock.calls[0]?.[0])); + const plan = mode === "apply" ? output.plan : output; + expect(plan).toMatchObject({ + blockers: [ + { + code: "release-migration-error", + message: "legacy data is unreadable", + }, + ], + status: "blocked", + }); + if (mode === "apply") { + expect(output).toMatchObject({ status: "blocked" }); + } + } finally { + process.exitCode = undefined; + } }); it("rejects interactive JSON application before inspecting the database", async () => {