From 5af501fd3be18d9f514e89d40239558647accc73 Mon Sep 17 00:00:00 2001 From: Gustavo Valverde Date: Fri, 28 Aug 2026 22:34:57 +0000 Subject: [PATCH] test(demo): validate migrated 1.6 SCIM and SSO workflows (#10986) --- demo/nextjs/.env.example | 3 + demo/nextjs/lib/auth.ts | 12 + .../nextjs-demo/e2e/scim-demo.spec.ts | 362 ++++++++++++++++-- .../nextjs-demo/published-1-6-migration.ts | 334 ++++++++++++++++ e2e/integration/package.json | 1 + pnpm-lock.yaml | 3 + 6 files changed, 685 insertions(+), 30 deletions(-) create mode 100644 e2e/integration/nextjs-demo/published-1-6-migration.ts diff --git a/demo/nextjs/.env.example b/demo/nextjs/.env.example index 1a1ed9d3cf..b3ac5cdc3e 100644 --- a/demo/nextjs/.env.example +++ b/demo/nextjs/.env.example @@ -19,6 +19,9 @@ SCIM_DEMO_OIDC_CLIENT_SECRET= SCIM_DEMO_OIDC_SIGNING_PRIVATE_KEY= # Optional local SQLite database path used by the demo and its end-to-end tests. DEMO_SQLITE_PATH= +# Keep this empty only for v1.7 issuer compatibility mode. Use provider-id for +# a new database or one migrated from v1.6 with provider-id identity. +DEMO_ACCOUNT_IDENTITY_STRATEGY= TURSO_DATABASE_URL= TURSO_AUTH_TOKEN= APPLE_CLIENT_ID= diff --git a/demo/nextjs/lib/auth.ts b/demo/nextjs/lib/auth.ts index 0bd3b1a81e..4a22c2d79e 100644 --- a/demo/nextjs/lib/auth.ts +++ b/demo/nextjs/lib/auth.ts @@ -38,6 +38,15 @@ import { getSCIMDemoOIDCProvider } from "./scim-demo-oidc.ts"; const DEMO_SQLITE_CONTENTION_TIMEOUT_MS = 5_000; +const demoAccountIdentityStrategy = (() => { + const strategy = process.env.DEMO_ACCOUNT_IDENTITY_STRATEGY; + if (!strategy) return undefined; + if (strategy === "issuer" || strategy === "provider-id") return strategy; + throw new Error( + 'DEMO_ACCOUNT_IDENTITY_STRATEGY must be either "issuer" or "provider-id"', + ); +})(); + const database = (() => { if (process.env.DEMO_SQLITE_PATH) { return { @@ -129,6 +138,9 @@ const authOptions = { }, }, account: { + ...(demoAccountIdentityStrategy && { + identityStrategy: demoAccountIdentityStrategy, + }), accountLinking: { trustedProviders: [ "email-password", diff --git a/e2e/integration/nextjs-demo/e2e/scim-demo.spec.ts b/e2e/integration/nextjs-demo/e2e/scim-demo.spec.ts index f97eff750b..3dbbcd624f 100644 --- a/e2e/integration/nextjs-demo/e2e/scim-demo.spec.ts +++ b/e2e/integration/nextjs-demo/e2e/scim-demo.spec.ts @@ -7,11 +7,15 @@ import { join, resolve } from "node:path"; import { DatabaseSync } from "node:sqlite"; import type { APIRequestContext, BrowserContext, Page } from "@playwright/test"; import { expect, test } from "@playwright/test"; +import type { MigratedPublished16Database } from "../published-1-6-migration"; +import { prepareMigratedPublished16Database } from "../published-1-6-migration"; const USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User"; const GROUP_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:Group"; const PATCH_SCHEMA = "urn:ietf:params:scim:api:messages:2.0:PatchOp"; const demoDirectory = resolve(process.cwd(), "../../demo/nextjs"); +const demoAuthSecret = + "better-auth-scim-demo-e2e-secret-at-least-thirty-two-characters"; interface NextDemoRuntime { baseURL: string; @@ -21,6 +25,11 @@ interface NextDemoRuntime { temporaryDirectory: string; } +interface NextDemoRuntimeOptions { + accountIdentityStrategy?: "issuer" | "provider-id"; + prepareDatabase?: (databasePath: string) => Promise; +} + async function getAvailablePort(): Promise { const server = createServer(); const port = await new Promise((resolvePort, reject) => { @@ -48,18 +57,26 @@ async function stopProcess( await terminate(child.pid); } -async function startNextDemoRuntime(): Promise { +async function startNextDemoRuntime( + options: NextDemoRuntimeOptions = {}, +): Promise { const temporaryDirectory = await mkdtemp( join(tmpdir(), "better-auth-scim-managed-demo-"), ); const port = await getAvailablePort(); const baseURL = `http://127.0.0.1:${port}`; const databasePath = join(temporaryDirectory, "demo.sqlite"); + try { + await options.prepareDatabase?.(databasePath); + } catch (error) { + await rm(temporaryDirectory, { force: true, recursive: true }); + throw error; + } const environment: NodeJS.ProcessEnv = { ...process.env, - BETTER_AUTH_SECRET: - "better-auth-scim-demo-e2e-secret-at-least-thirty-two-characters", + BETTER_AUTH_SECRET: demoAuthSecret, BETTER_AUTH_URL: baseURL, + DEMO_ACCOUNT_IDENTITY_STRATEGY: options.accountIdentityStrategy, DEMO_SQLITE_PATH: databasePath, NO_COLOR: "1", SCIM_DEMO_CREDENTIAL_PEPPER: @@ -167,6 +184,23 @@ async function signUp( expect(response.ok(), await response.text()).toBe(true); } +async function signIn( + context: BrowserContext, + baseURL: string, + email: string, +): Promise { + const response = await context.request.post( + `${baseURL}/api/auth/sign-in/email`, + { + data: { + email, + password: "correct-horse-battery-staple", + }, + }, + ); + expect(response.ok(), await response.text()).toBe(true); +} + async function createOrganization( request: APIRequestContext, baseURL: string, @@ -199,6 +233,32 @@ function managementURL(baseURL: string, organizationId: string): string { return `${baseURL}/api/scim-demo/organizations/${organizationId}/connections`; } +async function createEmployeeLink( + request: APIRequestContext, + baseURL: string, + organizationId: string, + scimUserId: string, +): Promise { + const response = await request.post( + `${managementURL(baseURL, organizationId)}/employee-links`, + { + headers: { origin: baseURL }, + data: { organizationId, scimUserId }, + }, + ); + expect(response.status(), await response.text()).toBe(201); + const body: unknown = await response.json(); + if ( + typeof body !== "object" || + body === null || + !("url" in body) || + typeof body.url !== "string" + ) { + throw new Error("Employee link response did not return a URL"); + } + return body.url; +} + async function readIssuedCredential(response: { json(): Promise; }): Promise<{ id: string; token: string }> { @@ -328,11 +388,20 @@ async function completeEmployeeSignIn(page: Page): Promise { page.getByRole("heading", { name: "Sign in with Acme Identity" }), ).toBeVisible(); await page.getByRole("button", { name: "Continue as Maya" }).click(); - await expect( - page.getByRole("heading", { name: "You’re signed in" }), - ).toBeVisible(); + try { + await expect( + page.getByRole("heading", { name: "You’re signed in" }), + ).toBeVisible(); + } catch (error) { + throw new Error( + `Employee SSO did not complete at ${page.url()}:\n${await page.locator("body").innerText()}`, + { cause: error }, + ); + } } +test.describe.configure({ mode: "serial" }); + test.describe("Next.js managed SCIM catalog demo", () => { test.describe.configure({ mode: "serial" }); test.setTimeout(120_000); @@ -898,33 +967,16 @@ test.describe("Next.js managed SCIM catalog demo", () => { database.close(); } - const employeeLinkResponse = await page.request.post( - `${managementURL(runtime.baseURL, organizationId)}/employee-links`, - { - headers: { origin: runtime.baseURL }, - data: { - organizationId, - scimUserId: maya.id, - }, - }, + const employeeLinkURL = await createEmployeeLink( + page.request, + runtime.baseURL, + organizationId, + String(maya.id), ); - expect( - employeeLinkResponse.status(), - await employeeLinkResponse.text(), - ).toBe(201); - const employeeLinkBody: unknown = await employeeLinkResponse.json(); - if ( - typeof employeeLinkBody !== "object" || - employeeLinkBody === null || - !("url" in employeeLinkBody) || - typeof employeeLinkBody.url !== "string" - ) { - throw new Error("Employee link response did not return a URL"); - } const employeeContext = await browser.newContext(); try { const employeePage = await employeeContext.newPage(); - await employeePage.goto(employeeLinkBody.url); + await employeePage.goto(employeeLinkURL); await completeEmployeeSignIn(employeePage); } finally { await employeeContext.close(); @@ -936,13 +988,14 @@ test.describe("Next.js managed SCIM catalog demo", () => { try { const accounts = accountDatabase .prepare( - `SELECT "providerId", "accountId", "userId" + `SELECT "issuer", "providerId", "accountId", "userId" FROM "account" WHERE "providerId" = 'scim-demo-sso'`, ) .all(); expect(accounts).toEqual([ { + issuer: `${runtime.baseURL}/api/scim-demo/idp`, providerId: "scim-demo-sso", accountId: maya.externalId, userId: maya.userId, @@ -962,3 +1015,252 @@ test.describe("Next.js managed SCIM catalog demo", () => { } }); }); + +test.describe("Next.js SCIM and SSO demo on a migrated published 1.6 database", () => { + test.describe.configure({ mode: "serial" }); + test.setTimeout(180_000); + + let migration: MigratedPublished16Database | undefined; + let runtime: NextDemoRuntime | undefined; + + test.beforeAll(async () => { + runtime = await startNextDemoRuntime({ + accountIdentityStrategy: "provider-id", + prepareDatabase: async (databasePath) => { + migration = await prepareMigratedPublished16Database( + databasePath, + demoAuthSecret, + ); + }, + }); + }); + + test.afterAll(async () => { + await stopProcess(runtime?.process); + if (runtime) { + await rm(runtime.temporaryDirectory, { + recursive: true, + force: true, + }); + } + }); + + test("runs the real SCIM lifecycle and repeated OIDC SSO sign-in without re-keying migrated identities", async ({ + browser, + page, + }) => { + if (!runtime || !migration) { + throw new Error("The migrated Next.js demo runtime was not prepared"); + } + + await signIn( + page.context(), + runtime.baseURL, + "administrator@migration.example.com", + ); + const organizationId = await createOrganization( + page.request, + runtime.baseURL, + "Migrated SCIM Demo", + ); + const provisioningDomainId = `scim-demo-org:${organizationId}`; + + await page.goto(`${runtime.baseURL}/dashboard/scim`); + const createResponsePromise = page.waitForResponse( + (response) => + response.url() === managementURL(runtime.baseURL, organizationId) && + response.request().method() === "POST", + ); + await page.getByRole("button", { name: "Create SCIM connection" }).click(); + const createResponse = await createResponsePromise; + expect(createResponse.status()).toBe(201); + const originalCredential = await readIssuedCredential(createResponse); + + await page.getByRole("button", { name: "Run local recipe" }).click(); + await expect( + page.getByRole("heading", { name: "Recipe complete" }), + ).toBeVisible(); + await expect( + page + .getByTestId("entra-local-recipe-steps") + .locator('[data-step-status="passed"]'), + ).toHaveCount(10); + await expect( + page.getByRole("heading", { name: "Users (2)" }), + ).toBeVisible(); + await expect( + page.getByRole("heading", { name: "Groups (1)" }), + ).toBeVisible(); + + const database = new DatabaseSync(runtime.databasePath, { + readOnly: true, + }); + let demoEmployee: { + externalId: string; + id: string; + userId: string; + }; + try { + const employee = database + .prepare( + `SELECT "id", "userId", "externalId" + FROM "scimUser" + WHERE "provisioningDomainId" = ? + AND "serializedAttributes" LIKE '%Finance Director%'`, + ) + .get(provisioningDomainId); + if ( + typeof employee?.id !== "string" || + typeof employee.userId !== "string" || + typeof employee.externalId !== "string" + ) { + throw new Error("The migrated demo did not provision its SSO employee"); + } + demoEmployee = { + externalId: employee.externalId, + id: employee.id, + userId: employee.userId, + }; + expect( + database + .prepare( + `SELECT COUNT(*) AS "count" + FROM "account" + WHERE "providerId" = 'scim-demo-sso'`, + ) + .get(), + ).toEqual({ count: 0 }); + } finally { + database.close(); + } + + for (let signInAttempt = 0; signInAttempt < 2; signInAttempt += 1) { + const employeeLink = await createEmployeeLink( + page.request, + runtime.baseURL, + organizationId, + demoEmployee.id, + ); + const employeeContext = await browser.newContext(); + try { + const employeePage = await employeeContext.newPage(); + await employeePage.goto(employeeLink); + await completeEmployeeSignIn(employeePage); + } finally { + await employeeContext.close(); + } + } + + const migratedDatabase = new DatabaseSync(runtime.databasePath, { + readOnly: true, + }); + try { + const accounts = migratedDatabase + .prepare( + `SELECT "issuer", "providerId", "accountId", "userId" + FROM "account" + WHERE "providerId" IN ( + 'credential', + 'scim-demo-sso', + 'workforce-scim', + 'workforce-sso' + ) + ORDER BY "providerId", "accountId"`, + ) + .all(); + expect(accounts).toEqual([ + expect.objectContaining({ + issuer: "local:credential", + providerId: "credential", + userId: migration.source.administratorUserId, + }), + { + issuer: "local:oauth:scim-demo-sso", + providerId: "scim-demo-sso", + accountId: demoEmployee.externalId, + userId: demoEmployee.userId, + }, + { + issuer: "local:oauth:workforce-sso", + providerId: "workforce-sso", + accountId: migration.source.directorySubject, + userId: migration.verified.ssoUserId, + }, + ]); + expect( + migratedDatabase + .prepare( + `SELECT "userId" + FROM "scimUser" + WHERE "id" = ?`, + ) + .get(migration.verified.reprovisionedSCIMUserId), + ).toEqual({ userId: migration.verified.scimUserId }); + } finally { + migratedDatabase.close(); + } + + const rotateResponsePromise = page.waitForResponse( + (response) => + response.url() === + `${managementURL(runtime.baseURL, organizationId)}/rotate` && + response.request().method() === "POST", + ); + await page.getByRole("button", { name: "Rotate credential" }).click(); + const rotateResponse = await rotateResponsePromise; + expect(rotateResponse.status()).toBe(201); + const rotatedCredential = await readIssuedCredential(rotateResponse); + expect(rotatedCredential.token).not.toBe(originalCredential.token); + + const revokeResponsePromise = page.waitForResponse( + (response) => + response.url() === + `${managementURL( + runtime.baseURL, + organizationId, + )}/credentials/${originalCredential.id}/revoke` && + response.request().method() === "POST", + ); + await page + .getByRole("button", { + name: `Revoke credential ${originalCredential.id}`, + }) + .click(); + expect((await revokeResponsePromise).status()).toBe(200); + expect( + ( + await scimRequest( + page.request, + runtime.baseURL, + originalCredential.token, + { method: "GET", path: "/Users" }, + ) + ).status(), + ).toBe(401); + expect( + ( + await scimRequest( + page.request, + runtime.baseURL, + rotatedCredential.token, + { method: "GET", path: "/Users" }, + ) + ).status(), + ).toBe(200); + + const decommissionResponsePromise = page.waitForResponse( + (response) => + response.url() === + `${managementURL(runtime.baseURL, organizationId)}/decommission` && + response.request().method() === "POST", + ); + await page.getByRole("button", { name: "Decommission connection" }).click(); + await page + .getByRole("button", { name: "Decommission permanently" }) + .click(); + expect((await decommissionResponsePromise).status()).toBe(200); + await expect( + page.getByText("Connection permanently decommissioned"), + ).toBeVisible(); + }); +}); diff --git a/e2e/integration/nextjs-demo/published-1-6-migration.ts b/e2e/integration/nextjs-demo/published-1-6-migration.ts new file mode 100644 index 0000000000..b05aaa0968 --- /dev/null +++ b/e2e/integration/nextjs-demo/published-1-6-migration.ts @@ -0,0 +1,334 @@ +import { spawn } from "node:child_process"; +import { existsSync } from "node:fs"; +import { readFile, writeFile } from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; + +interface CommandResult { + exitCode: number | null; + stderr: string; + stdout: string; +} + +interface PublishedAccount { + accountId: string; + id: string; + providerId: string; + userId: string; +} + +interface Published16Result { + accounts: PublishedAccount[]; + administratorUserId: string; + clientId: string; + clientSecret: string; + directorySubject: string; + scimAccountId: string; + tableCounts: Record; +} + +interface GuidedVerificationResult { + credentialUserId: string; + reprovisionedSCIMUserId: string; + scimUserId: string; + ssoUserId: string; +} + +export interface MigratedPublished16Database { + source: Published16Result; + verified: GuidedVerificationResult; +} + +const workspaceRootDirectory = resolve(process.cwd(), "../.."); +const migrationFixtureDirectory = join( + workspaceRootDirectory, + "e2e/adapter/test/kysely-adapter", +); +const published16Directory = join( + migrationFixtureDirectory, + "published-1-6-app", +); +const published16Seed = join(published16Directory, "seed.mjs"); +const guidedConfig = join(migrationFixtureDirectory, "guided-auth.mjs"); +const guidedVerifier = join( + migrationFixtureDirectory, + "verify-guided-migration.mjs", +); +const cliEntry = join(workspaceRootDirectory, "packages/cli/dist/index.mjs"); +const commandTimeoutMs = 60_000; + +function redactCommandOutput(output: string): string { + return output + .replace( + /^PUBLISHED_FIXTURE_RESULT=.*$/gm, + "PUBLISHED_FIXTURE_RESULT=[redacted]", + ) + .replace( + /^GUIDED_MIGRATION_RESULT=.*$/gm, + "GUIDED_MIGRATION_RESULT=[redacted]", + ); +} + +function runNode( + cwd: string, + args: string[], + environment: NodeJS.ProcessEnv = {}, +): Promise { + return new Promise((resolveCommand, rejectCommand) => { + const child = spawn(process.execPath, args, { + cwd, + env: { ...process.env, ...environment, NO_COLOR: "1" }, + stdio: ["ignore", "pipe", "pipe"], + }); + let stdout = ""; + let stderr = ""; + let settled = false; + let timeout: ReturnType; + const settle = (callback: () => void) => { + if (settled) return; + settled = true; + clearTimeout(timeout); + callback(); + }; + timeout = setTimeout(() => { + child.kill(); + settle(() => + rejectCommand( + new Error( + `Subprocess timed out after ${commandTimeoutMs}ms:\n${redactCommandOutput(stdout)}\n${redactCommandOutput(stderr)}`, + ), + ), + ); + }, commandTimeoutMs); + child.stdout.setEncoding("utf8"); + child.stderr.setEncoding("utf8"); + child.stdout.on("data", (chunk: string) => { + stdout += chunk; + }); + child.stderr.on("data", (chunk: string) => { + stderr += chunk; + }); + child.once("error", (error) => { + settle(() => rejectCommand(error)); + }); + child.once("close", (exitCode) => { + settle(() => resolveCommand({ exitCode, stderr, stdout })); + }); + }); +} + +function requireSuccessfulCommand( + result: CommandResult, + context: string, +): void { + if (result.exitCode === 0) return; + throw new Error( + `${context} failed with exit code ${result.exitCode}:\n${redactCommandOutput(result.stdout)}\n${redactCommandOutput(result.stderr)}`, + ); +} + +function readSentinelResult(stdout: string, sentinel: string): Result { + const line = stdout + .split("\n") + .find((candidate) => candidate.startsWith(`${sentinel}=`)); + if (!line) { + throw new Error( + `Missing ${sentinel} in subprocess output:\n${redactCommandOutput(stdout)}`, + ); + } + return JSON.parse(line.slice(sentinel.length + 1)) as Result; +} + +async function requireFixtureFiles(): Promise { + for (const requiredFile of [ + cliEntry, + published16Seed, + guidedConfig, + guidedVerifier, + ]) { + if (!existsSync(requiredFile)) { + throw new Error(`Missing published migration fixture: ${requiredFile}`); + } + } + const packageMetadata = JSON.parse( + await readFile( + join(published16Directory, "node_modules/better-auth/package.json"), + "utf8", + ), + ) as { version?: string }; + if (packageMetadata.version !== "1.6.30") { + throw new Error( + `Expected the published 1.6 fixture to use better-auth 1.6.30, received ${packageMetadata.version ?? "unknown"}`, + ); + } +} + +export async function prepareMigratedPublished16Database( + databasePath: string, + authSecret: string, +): Promise { + await requireFixtureFiles(); + + const environment = { + BETTER_AUTH_MIGRATION_DATABASE: databasePath, + BETTER_AUTH_SECRET: authSecret, + }; + const seed = await runNode( + published16Directory, + [published16Seed, databasePath], + environment, + ); + requireSuccessfulCommand(seed, "Published Better Auth 1.6 seed"); + const source = readSentinelResult( + seed.stdout, + "PUBLISHED_FIXTURE_RESULT", + ); + const sourceProviders = source.accounts.map(({ providerId }) => providerId); + if ( + source.tableCounts.account !== 3 || + !sourceProviders.includes("credential") || + !sourceProviders.includes("workforce-scim") || + !sourceProviders.includes("workforce-sso") + ) { + throw new Error( + `Published Better Auth 1.6 did not create the required populated workflows: accountCount=${source.tableCounts.account ?? "unknown"}, providers=${sourceProviders.join(",")}`, + ); + } + + const decisionsFile = join( + dirname(databasePath), + "better-auth-migration.json", + ); + await writeFile( + decisionsFile, + `${JSON.stringify( + { + formatVersion: 1, + migration: "1.6-to-1.7", + oauth: { + clientSecrets: { source: "plain", target: "hashed" }, + consents: "migrate", + }, + scim: { retireAccountIds: [source.scimAccountId] }, + }, + null, + 2, + )}\n`, + "utf8", + ); + + const plan = await runNode( + migrationFixtureDirectory, + [ + cliEntry, + "migrate", + "plan", + decisionsFile, + "--config", + guidedConfig, + "--json", + ], + environment, + ); + requireSuccessfulCommand(plan, "Guided Better Auth 1.7 migration plan"); + const parsedPlan = JSON.parse(plan.stdout) as { + accountIdentity?: { selectedStrategy?: string }; + blockers?: unknown[]; + status?: string; + }; + if ( + parsedPlan.status !== "ready" || + parsedPlan.blockers?.length !== 0 || + parsedPlan.accountIdentity?.selectedStrategy !== "provider-id" + ) { + throw new Error( + `Guided migration did not produce a ready provider-id plan: ${plan.stdout}`, + ); + } + + const apply = await runNode( + migrationFixtureDirectory, + [ + cliEntry, + "migrate", + "apply", + decisionsFile, + "--config", + guidedConfig, + "--yes", + "--json", + ], + environment, + ); + requireSuccessfulCommand(apply, "Guided Better Auth 1.7 migration apply"); + const parsedApply = JSON.parse(apply.stdout) as { status?: string }; + if (parsedApply.status !== "applied") { + throw new Error(`Guided migration was not applied: ${apply.stdout}`); + } + + const repeatedPlan = await runNode( + migrationFixtureDirectory, + [ + cliEntry, + "migrate", + "plan", + decisionsFile, + "--config", + guidedConfig, + "--json", + ], + environment, + ); + requireSuccessfulCommand( + repeatedPlan, + "Repeated Better Auth 1.7 migration plan", + ); + const parsedRepeatedPlan = JSON.parse(repeatedPlan.stdout) as { + blockers?: unknown[]; + changes?: { + addColumns?: unknown[]; + addIndexes?: unknown[]; + createTables?: unknown[]; + }; + releaseMigration?: unknown; + status?: string; + }; + if ( + parsedRepeatedPlan.status !== "up-to-date" || + parsedRepeatedPlan.blockers?.length !== 0 || + parsedRepeatedPlan.changes?.addColumns?.length !== 0 || + parsedRepeatedPlan.changes?.addIndexes?.length !== 0 || + parsedRepeatedPlan.changes?.createTables?.length !== 0 || + parsedRepeatedPlan.releaseMigration !== undefined + ) { + throw new Error( + `Guided migration did not become up-to-date after apply: ${repeatedPlan.stdout}`, + ); + } + + const provisionedSourceAccount = source.accounts.find( + ({ providerId }) => providerId === "workforce-scim", + ); + if (!provisionedSourceAccount) { + throw new Error("Published Better Auth 1.6 did not create a SCIM account"); + } + const verification = await runNode( + migrationFixtureDirectory, + [guidedVerifier, databasePath], + { + ...environment, + BETTER_AUTH_MIGRATION_CLIENT_ID: source.clientId, + BETTER_AUTH_MIGRATION_CLIENT_SECRET: source.clientSecret, + BETTER_AUTH_MIGRATION_SCIM_USER_ID: provisionedSourceAccount.userId, + }, + ); + requireSuccessfulCommand( + verification, + "Migrated Better Auth 1.7 workflow verification", + ); + const verified = readSentinelResult( + verification.stdout, + "GUIDED_MIGRATION_RESULT", + ); + + return { source, verified }; +} diff --git a/e2e/integration/package.json b/e2e/integration/package.json index f29c0ac4da..8ff0e03cc3 100644 --- a/e2e/integration/package.json +++ b/e2e/integration/package.json @@ -14,6 +14,7 @@ "@better-auth/scim": "workspace:*", "@better-auth/sso": "workspace:*", "@better-auth/stripe": "workspace:*", + "auth": "workspace:*", "better-auth": "workspace:*" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fe8d2c753f..16955a1318 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -802,6 +802,9 @@ importers: '@better-auth/stripe': specifier: workspace:* version: link:../../packages/stripe + auth: + specifier: workspace:* + version: link:../../packages/cli better-auth: specifier: workspace:* version: link:../../packages/better-auth