Files
better-auth/packages/cli/test/init.test.ts
2026-05-06 17:45:32 +00:00

1374 lines
39 KiB
TypeScript

import { exec as execMock } from "node:child_process";
import path from "node:path";
import { vol } from "memfs";
import prompts from "prompts";
import { afterEach, beforeEach, describe, expect, vi } from "vitest";
import { initAction } from "../src/commands/init";
import { generateDrizzleSchema } from "../src/generators/drizzle";
import { generatePrismaSchema } from "../src/generators/prisma";
import { detectPackageManager } from "../src/utils/check-package-managers";
import { getPackageInfo, hasDependency } from "../src/utils/get-package-info";
import { installDependencies } from "../src/utils/install-dependencies";
import { testWithTmpDir } from "./test-utils";
// Mock src/index.ts to prevent it from executing during tests
vi.mock("../src/index.ts", () => ({
cliVersion: "1.0.0",
}));
const fs = vol.promises;
vi.mock("node:fs", () => ({
...vol,
default: vol,
}));
vi.mock("node:fs/promises", () => ({
...vol.promises,
default: vol.promises,
}));
// Mock all external dependencies
vi.mock("prompts", () => ({
default: vi.fn(),
}));
vi.mock("node:child_process", () => ({
exec: vi.fn(),
}));
vi.mock("../src/utils/get-package-info", () => {
const mockGetPackageInfo = vi.fn();
mockGetPackageInfo.mockResolvedValue({
name: "auth",
version: "1.0.0",
});
return {
getPackageInfo: mockGetPackageInfo,
hasDependency: vi.fn(),
};
});
vi.mock("../src/utils/check-package-managers", async () => {
const actual = await vi.importActual<
typeof import("../src/utils/check-package-managers")
>("../src/utils/check-package-managers");
return {
...actual,
detectPackageManager: vi.fn(),
getPkgManagerStr: vi.fn(({ packageManager }) => packageManager),
};
});
vi.mock("../src/utils/install-dependencies", () => ({
installDependencies: vi.fn(),
}));
vi.mock("../src/generators/prisma", () => ({
generatePrismaSchema: vi.fn(),
}));
vi.mock("../src/generators/drizzle", () => ({
generateDrizzleSchema: vi.fn(),
}));
// Type the mocked functions
const mockPrompts = vi.mocked(prompts);
const mockExec = vi.mocked(execMock);
const mockGetPackageInfo = vi.mocked(getPackageInfo);
const mockHasDependency = vi.mocked(hasDependency);
const mockDetectPackageManager = vi.mocked(detectPackageManager);
const mockInstallDependencies = vi.mocked(installDependencies);
const mockGeneratePrismaSchema = vi.mocked(generatePrismaSchema);
const mockGenerateDrizzleSchema = vi.mocked(generateDrizzleSchema);
describe("initAction", () => {
let originalExit: typeof process.exit;
let _originalCwd: string;
beforeEach(async () => {
// Save original process.exit and cwd
originalExit = process.exit;
_originalCwd = process.cwd();
// Mock process.exit to prevent tests from exiting
vi.spyOn(process, "exit").mockImplementation((code) => code as never);
// Mock console methods to reduce noise
vi.spyOn(console, "log").mockImplementation(() => {});
vi.spyOn(console, "error").mockImplementation(() => {});
vi.spyOn(console, "warn").mockImplementation(() => {});
// Reset all mocks
vi.clearAllMocks();
mockGetPackageInfo.mockResolvedValue({
name: "test-project",
version: "1.0.0",
});
mockHasDependency.mockResolvedValue(false);
mockDetectPackageManager.mockResolvedValue({
packageManager: "npm",
version: "10.0.0",
});
mockInstallDependencies.mockResolvedValue(false);
mockExec.mockImplementation((_command, _options, callback) => {
if (callback) {
callback(null, { stdout: "", stderr: "" } as any, "");
}
return {} as any;
});
});
afterEach(() => {
// Restore original functions
process.exit = originalExit;
vi.restoreAllMocks();
});
testWithTmpDir(
"should complete full init flow with database setup",
async ({ tmp, skip }) => {
// Setup: Create package.json
const packageJson = {
name: "test-project",
version: "1.0.0",
};
await fs.writeFile(
path.join(tmp, "package.json"),
JSON.stringify(packageJson),
);
// Mock all prompts in order
mockPrompts.mockImplementation(async (questions: any) => {
const question = Array.isArray(questions) ? questions[0] : questions;
// Install Better Auth
if (question.message?.includes("install better-auth")) {
return { value: true };
}
// Create .env file - confirm() wrapper returns value directly
if (question.message?.includes("set environment variables")) {
return { value: true };
}
// Handle confirm prompts (they use type: "confirm" and return { value: boolean })
if (
question.type === "confirm" &&
question.message?.includes(".env file")
) {
return { value: true };
}
if (question.name === "providedSecret") {
return { providedSecret: "test-secret-123" };
}
if (question.name === "providedURL") {
return { providedURL: "http://localhost:3000" };
}
// Auth config file path
if (
question.name === "filePath" &&
question.message?.includes("auth instance")
) {
return { filePath: "src/lib/auth.ts" };
}
if (question.message?.includes("Select the plugins")) {
return { value: [] }; // no plugins
}
// Database selection
if (question.message?.includes("configure a database")) {
return { value: "yes" };
}
if (question.message?.includes("Select the database")) {
return { value: "drizzle-postgresql" };
}
if (question.message?.includes("Select the database dialect")) {
return { value: "drizzle-postgresql" };
}
// Install database dependencies
if (question.name === "shouldInstallDeps") {
return { shouldInstallDeps: true };
}
// Generate schema
if (question.name === "shouldGenerate") {
return { shouldGenerate: true };
}
// Email & password
if (question.message?.includes("email & password")) {
return { value: true };
}
// Social providers
if (question.message?.includes("setup social providers")) {
return { value: false };
}
// Auth client
if (question.message?.includes("auth client configuration")) {
return { value: false };
}
// Route handler
if (question.message?.includes("route handler")) {
return { filePath: "src/app/api/auth/[...all]/route.ts" };
}
return {};
});
// Mock schema generation
mockGenerateDrizzleSchema.mockResolvedValue({
code: "export const schema = {};",
fileName: "auth-schema.ts",
overwrite: false,
});
// Run init action
await initAction({
cwd: tmp,
});
// Assertions
expect(mockHasDependency).toHaveBeenCalledWith(
packageJson,
"better-auth",
);
expect(mockInstallDependencies).toHaveBeenCalledWith(
expect.objectContaining({
cwd: path.resolve(tmp),
dependencies: expect.arrayContaining(["better-auth"]),
}),
);
// Check that auth config file was created
const authConfigPath = path.join(tmp, "src/lib/auth.ts");
const authConfigExists = await fs
.access(authConfigPath)
.then(() => true)
.catch(() => false);
expect(authConfigExists).toBe(true);
// Check that .env file was created
const envPath = path.resolve(path.join(tmp, ".env"));
const envExists = await fs
.access(envPath)
.then(() => true)
.catch(() => false);
expect(envExists).toBe(true);
},
);
testWithTmpDir(
"should write files only at the end, before installing dependencies",
async ({ tmp }) => {
await fs.writeFile(
path.join(tmp, "package.json"),
JSON.stringify({ name: "test-project", version: "1.0.0" }),
);
const callOrder: Array<{ type: "writeFile" | "installDependencies" }> =
[];
const originalWriteFile = vol.promises.writeFile.bind(vol.promises);
vi.spyOn(vol.promises, "writeFile").mockImplementation(
async (...args: Parameters<typeof vol.promises.writeFile>) => {
callOrder.push({ type: "writeFile" });
return originalWriteFile(...args);
},
);
mockInstallDependencies.mockImplementation(async () => {
callOrder.push({ type: "installDependencies" });
return false;
});
mockPrompts.mockImplementation(async (questions: any) => {
const question = Array.isArray(questions) ? questions[0] : questions;
if (question.message?.includes("install better-auth")) {
return { value: true };
}
if (question.message?.includes("set environment variables")) {
return { value: true };
}
if (
question.type === "confirm" &&
question.message?.includes(".env file")
) {
return { value: true };
}
if (question.name === "providedSecret") {
return { providedSecret: "test-secret-123" };
}
if (question.name === "providedURL") {
return { providedURL: "http://localhost:3000" };
}
if (
question.name === "filePath" &&
question.message?.includes("auth instance")
) {
return { filePath: "src/lib/auth.ts" };
}
if (question.message?.includes("Select the plugins")) {
return { value: [] };
}
if (question.message?.includes("configure a database")) {
return { value: "yes" };
}
if (question.message?.includes("Select the database")) {
return { value: "drizzle-postgresql" };
}
if (question.message?.includes("Select the database dialect")) {
return { value: "drizzle-postgresql" };
}
if (question.name === "shouldInstallDeps") {
return { shouldInstallDeps: true };
}
if (question.name === "shouldGenerate") {
return { shouldGenerate: true };
}
if (question.message?.includes("email & password")) {
return { value: true };
}
if (question.message?.includes("setup social providers")) {
return { value: false };
}
if (question.message?.includes("auth client configuration")) {
return { value: false };
}
if (question.message?.includes("route handler")) {
return { filePath: "src/app/api/auth/[...all]/route.ts" };
}
return {};
});
mockGenerateDrizzleSchema.mockResolvedValue({
code: "export const schema = {};",
fileName: "auth-schema.ts",
overwrite: false,
});
await initAction({ cwd: tmp });
const writeFileIndices: number[] = [];
callOrder.forEach((c, i) => {
if (c.type === "writeFile") writeFileIndices.push(i);
});
const firstInstallIndex = callOrder.findIndex(
(c) => c.type === "installDependencies",
);
expect(writeFileIndices.length).toBeGreaterThan(0);
expect(firstInstallIndex).toBeGreaterThanOrEqual(0);
const lastWriteFileIndex = writeFileIndices[writeFileIndices.length - 1]!;
expect(lastWriteFileIndex).toBeLessThan(firstInstallIndex);
},
);
testWithTmpDir(
"should handle stateless mode (no database)",
async ({ tmp }) => {
await fs.writeFile(
path.join(tmp, "package.json"),
JSON.stringify({ name: "test-project", version: "1.0.0" }),
);
mockPrompts.mockImplementation(async (questions: any) => {
const question = Array.isArray(questions) ? questions[0] : questions;
if (question.message?.includes("install better-auth")) {
return { value: true };
}
if (question.name === "providedSecret") {
return { providedSecret: "" };
}
if (question.name === "providedURL") {
return { providedURL: "http://localhost:3000" };
}
if (
question.name === "filePath" &&
question.message?.includes("auth instance")
) {
return { filePath: "auth.ts" };
}
if (
question.name === "filePath" &&
question.message?.includes("route handler")
) {
return { filePath: "src/app/api/auth/[...all]/route.ts" };
}
if (question.message?.includes("configure a database")) {
return { value: "stateless" };
}
if (question.message?.includes("auth client configuration")) {
return { value: false };
}
return {};
});
await initAction({ cwd: tmp });
// Should not call database-related functions
expect(mockGenerateDrizzleSchema).not.toHaveBeenCalled();
expect(mockGeneratePrismaSchema).not.toHaveBeenCalled();
},
);
testWithTmpDir("should handle existing auth config file", async ({ tmp }) => {
await fs.writeFile(
path.join(tmp, "package.json"),
JSON.stringify({ name: "test-project", version: "1.0.0" }),
);
// Create existing auth config - use one of the possible paths that init checks
// The init command checks possibleAuthConfigPaths, which includes "src/lib/auth.ts"
await fs.mkdir(path.join(tmp, "src/lib"), { recursive: true });
const existingAuthPath = path.join(tmp, "src/lib/auth.ts");
const originalContent = "export const auth = betterAuth({});";
await fs.writeFile(existingAuthPath, originalContent);
// Mock prompts - should not be called for auth config since it exists
mockPrompts.mockImplementation(async (questions: any) => {
const question = Array.isArray(questions) ? questions[0] : questions;
// Handle other prompts that might be called
if (question.message?.includes("install better-auth")) {
return { value: true };
}
if (
question.type === "confirm" &&
question.message?.includes(".env file")
) {
return { value: false };
}
if (question.message?.includes("configure a database")) {
return { value: "skip" };
}
if (
question.name === "filePath" &&
question.message?.includes("route handler")
) {
return { filePath: "src/app/api/auth/[...all]/route.ts" };
}
return {};
});
await initAction({ cwd: tmp });
// Should preserve existing auth config (not overwrite it)
// Note: The init command will still generate the config code, but it should
// detect the existing file and skip the generation step
const authConfigContent = await fs.readFile(existingAuthPath, "utf-8");
// The file might be updated, but let's check it at least exists
expect(authConfigContent).toContain("betterAuth");
});
testWithTmpDir("should handle cancellation gracefully", async ({ tmp }) => {
await fs.writeFile(
path.join(tmp, "package.json"),
JSON.stringify({ name: "test-project", version: "1.0.0" }),
);
// Mock prompts to simulate cancellation
// Note: The actual code has a bug where it doesn't check for null before destructuring
// For testing, we'll make the first prompt return null to trigger cancellation handling
let callCount = 0;
mockPrompts.mockImplementation(async (questions: any): Promise<any> => {
callCount++;
// Return null on first call to simulate cancellation
if (callCount === 1) {
return null as any;
}
return {};
});
const _exitSpy = vi.spyOn(process, "exit");
// The code will throw when trying to destructure null, so we expect it to fail
// This test documents the current behavior - the code should be fixed to handle null properly
await expect(initAction({ cwd: tmp })).rejects.toThrow();
});
testWithTmpDir("should handle missing package.json", async ({ tmp }) => {
mockGetPackageInfo.mockReset();
await mockGetPackageInfo.mockRejectedValue(
new Error("Package.json not found"),
);
mockPrompts.mockImplementation(async (questions: any) => {
const question = Array.isArray(questions) ? questions[0] : questions;
if (question.message?.includes("install better-auth")) {
return { value: true };
}
if (
question.type === "confirm" &&
question.message?.includes(".env file")
) {
return { value: false };
}
if (
question.name === "filePath" &&
question.message?.includes("auth instance")
) {
return { filePath: "auth.ts" };
}
if (
question.name === "filePath" &&
question.message?.includes("route handler")
) {
return { filePath: "src/app/api/auth/[...all]/route.ts" };
}
if (question.message?.includes("configure a database")) {
return { value: "yes" };
}
// First select "sqlite" which triggers SQLite variant selection
if (
question.message?.includes("Select the database") &&
!question.message?.includes("SQLite driver")
) {
return { value: "sqlite" };
}
// Then select the SQLite driver variant
if (question.message?.includes("Select SQLite driver")) {
return { value: "sqlite-better-sqlite3" };
}
if (question.name === "shouldInstallDeps") {
return { shouldInstallDeps: true };
}
if (question.name === "shouldMigrate") {
return { shouldMigrate: true };
}
if (question.message?.includes("email & password")) {
return { value: true };
}
if (question.message?.includes("setup social providers")) {
return { value: false };
}
if (question.message?.includes("auth client configuration")) {
return { value: false };
}
return {};
});
mockHasDependency.mockReset();
mockHasDependency.mockResolvedValue(false);
const exitSpy = vi.spyOn(process, "exit");
await initAction({ cwd: tmp });
expect(exitSpy).toHaveBeenCalledWith(1);
expect(mockGetPackageInfo).toHaveBeenCalledWith(path.resolve(tmp));
});
testWithTmpDir("should setup Prisma database", async ({ tmp }) => {
await fs.writeFile(
path.join(tmp, "package.json"),
JSON.stringify({ name: "test-project", version: "1.0.0" }),
);
mockPrompts.mockImplementation(async (questions: any) => {
const question = Array.isArray(questions) ? questions[0] : questions;
if (question.message?.includes("install better-auth")) {
return { value: true };
}
if (
question.type === "confirm" &&
question.message?.includes(".env file")
) {
return { value: false };
}
if (
question.name === "filePath" &&
question.message?.includes("auth instance")
) {
return { filePath: "src/auth.ts" };
}
if (
question.name === "filePath" &&
question.message?.includes("route handler")
) {
return { filePath: "src/app/api/auth/[...all]/route.ts" };
}
if (question.message?.includes("configure a database")) {
return { value: "yes" };
}
if (question.message?.includes("Select the database")) {
return { value: "prisma-postgresql" };
}
if (question.message?.includes("Select the database dialect")) {
return { value: "prisma-postgresql" };
}
if (question.name === "shouldInstallDeps") {
return { shouldInstallDeps: true };
}
if (question.name === "shouldGenerate") {
return { shouldGenerate: true };
}
if (question.message?.includes("email & password")) {
return { value: true };
}
if (question.message?.includes("setup social providers")) {
return { value: false };
}
if (question.message?.includes("auth client configuration")) {
return { value: false };
}
return {};
});
mockGeneratePrismaSchema.mockResolvedValue({
code: "model User { id String @id }",
fileName: "schema.prisma",
overwrite: false,
});
await initAction({ cwd: tmp });
expect(mockGeneratePrismaSchema).toHaveBeenCalled();
expect(mockGenerateDrizzleSchema).not.toHaveBeenCalled();
});
testWithTmpDir("should setup MongoDB database", async ({ tmp }) => {
await fs.writeFile(
path.join(tmp, "package.json"),
JSON.stringify({ name: "test-project", version: "1.0.0" }),
);
mockPrompts.mockImplementation(async (questions: any) => {
const question = Array.isArray(questions) ? questions[0] : questions;
if (question.message?.includes("install better-auth")) {
return { value: true };
}
if (
question.type === "confirm" &&
question.message?.includes(".env file")
) {
return { value: false };
}
if (
question.name === "filePath" &&
question.message?.includes("auth instance")
) {
return { filePath: "auth.ts" };
}
if (
question.name === "filePath" &&
question.message?.includes("route handler")
) {
return { filePath: "src/app/api/auth/[...all]/route.ts" };
}
if (question.message?.includes("configure a database")) {
return { value: "yes" };
}
if (question.message?.includes("Select the database")) {
return { value: "mongodb" };
}
if (question.name === "shouldInstallDeps") {
return { shouldInstallDeps: true };
}
if (question.message?.includes("email & password")) {
return { value: true };
}
if (question.message?.includes("setup social providers")) {
return { value: false };
}
if (question.message?.includes("auth client configuration")) {
return { value: false };
}
return {};
});
await initAction({ cwd: tmp });
// MongoDB doesn't generate schema or run migrations
expect(mockGenerateDrizzleSchema).not.toHaveBeenCalled();
expect(mockGeneratePrismaSchema).not.toHaveBeenCalled();
expect(mockExec).not.toHaveBeenCalled();
});
for (const db of [
{
database: "postgresql",
expectedPool: "const database = new Pool",
legacyType: 'type: "postgresql"',
},
{
database: "mysql",
expectedPool: "const database = createPool",
legacyType: 'type: "mysql"',
},
]) {
testWithTmpDir(
/**
* @see https://github.com/better-auth/better-auth/issues/9367
*/
`should generate a working Kysely ${db.database} auth config`,
async ({ tmp }) => {
await fs.writeFile(
path.join(tmp, "package.json"),
JSON.stringify({ name: "test-project", version: "1.0.0" }),
);
mockPrompts.mockImplementation(async (questions: any) => {
const question = Array.isArray(questions) ? questions[0] : questions;
if (question.message?.includes("install better-auth")) {
return { value: false };
}
if (question.message?.includes("set environment variables")) {
return { value: false };
}
if (
question.name === "filePath" &&
question.message?.includes("auth instance")
) {
return { filePath: "auth.ts" };
}
if (
question.name === "filePath" &&
question.message?.includes("route handler")
) {
return { filePath: "src/app/api/auth/[...all]/route.ts" };
}
if (question.message?.includes("configure a database")) {
return { value: "yes" };
}
if (question.message?.includes("Select the database")) {
return { value: db.database };
}
if (question.name === "shouldInstallDeps") {
return { shouldInstallDeps: false };
}
if (question.name === "shouldMigrate") {
return { shouldMigrate: false };
}
if (question.message?.includes("email & password")) {
return { value: true };
}
if (question.message?.includes("setup social providers")) {
return { value: false };
}
if (question.message?.includes("auth client configuration")) {
return { value: false };
}
if (question.name === "connect") {
return { connect: false };
}
return {};
});
await initAction({ cwd: tmp });
const authConfigContent = await fs.readFile(
path.join(tmp, "auth.ts"),
"utf-8",
);
expect(authConfigContent).toContain(db.expectedPool);
expect(authConfigContent).toContain("database: database");
expect(authConfigContent).not.toContain("const dialect");
expect(authConfigContent).not.toContain("database: {");
expect(authConfigContent).not.toContain(db.legacyType);
},
);
}
testWithTmpDir(
"should setup social providers (Google and GitHub)",
async ({ tmp }) => {
await fs.writeFile(
path.join(tmp, "package.json"),
JSON.stringify({ name: "test-project", version: "1.0.0" }),
);
await fs.writeFile(
path.join(tmp, ".env"),
"BETTER_AUTH_SECRET=test\nBETTER_AUTH_URL=http://localhost:3000",
);
mockPrompts.mockImplementation(async (questions: any) => {
const question = Array.isArray(questions) ? questions[0] : questions;
if (question.message?.includes("install better-auth")) {
return { value: true };
}
if (
question.name === "filePath" &&
question.message?.includes("auth instance")
) {
return { filePath: "auth.ts" };
}
if (
question.name === "filePath" &&
question.message?.includes("route handler")
) {
return { filePath: "src/app/api/auth/[...all]/route.ts" };
}
if (question.message?.includes("configure a database")) {
return { value: "yes" };
}
if (question.message?.includes("Select the database")) {
return { value: "drizzle-sqlite" };
}
if (question.message?.includes("Select the database dialect")) {
return { value: "drizzle-sqlite" };
}
if (question.name === "shouldInstallDeps") {
return { shouldInstallDeps: false };
}
if (question.name === "shouldGenerate") {
return { shouldGenerate: false };
}
if (question.message?.includes("email & password")) {
return { value: true };
}
if (question.message?.includes("setup social providers")) {
return { value: true };
}
if (question.message?.includes("Select the social providers")) {
// multiselect returns { value: array }
return { value: ["google", "github"] };
}
if (question.message?.includes("auth client configuration")) {
return { value: false };
}
return {};
});
await initAction({ cwd: tmp });
// Check that env file was updated with social provider vars
const envContent = await fs.readFile(path.join(tmp, ".env"), "utf-8");
expect(envContent).toContain("GOOGLE_CLIENT_ID");
expect(envContent).toContain("GOOGLE_CLIENT_SECRET");
expect(envContent).toContain("GITHUB_CLIENT_ID");
expect(envContent).toContain("GITHUB_CLIENT_SECRET");
},
);
testWithTmpDir(
"should disable email & password authentication",
async ({ tmp }) => {
await fs.writeFile(
path.join(tmp, "package.json"),
JSON.stringify({ name: "test-project", version: "1.0.0" }),
);
mockPrompts.mockImplementation(async (questions: any) => {
const question = Array.isArray(questions) ? questions[0] : questions;
if (question.message?.includes("install better-auth")) {
return { value: true };
}
if (
question.type === "confirm" &&
question.message?.includes(".env file")
) {
return { value: false };
}
if (
question.name === "filePath" &&
question.message?.includes("auth instance")
) {
return { filePath: "auth.ts" };
}
if (
question.name === "filePath" &&
question.message?.includes("route handler")
) {
return { filePath: "src/app/api/auth/[...all]/route.ts" };
}
if (question.message?.includes("configure a database")) {
return { value: "yes" };
}
if (question.message?.includes("Select the database")) {
return { value: "drizzle-postgresql" };
}
if (question.message?.includes("Select the database dialect")) {
return { value: "drizzle-postgresql" };
}
if (question.name === "shouldInstallDeps") {
return { shouldInstallDeps: false };
}
if (question.name === "shouldGenerate") {
return { shouldGenerate: false };
}
if (question.message?.includes("email & password")) {
return { value: false };
}
if (question.message?.includes("setup social providers")) {
return { value: false };
}
if (question.message?.includes("auth client configuration")) {
return { value: false };
}
return {};
});
await initAction({ cwd: tmp });
// Check that auth config doesn't include emailAndPassword
const authConfigPath = path.join(tmp, "auth.ts");
const authConfigContent = await fs.readFile(authConfigPath, "utf-8");
expect(authConfigContent).not.toContain("emailAndPassword");
},
);
testWithTmpDir(
"should generate auth client configuration",
async ({ tmp }) => {
await fs.writeFile(
path.join(tmp, "package.json"),
JSON.stringify({ name: "test-project", version: "1.0.0" }),
);
await fs.mkdir(path.join(tmp, "src"), { recursive: true });
let authClientPromptCalled = false;
mockPrompts.mockImplementation(async (questions: any) => {
const question = Array.isArray(questions) ? questions[0] : questions;
if (question.message?.includes("install better-auth")) {
return { value: true };
}
if (
question.type === "confirm" &&
question.message?.includes(".env file")
) {
return { value: false };
}
if (
question.name === "filePath" &&
question.message?.includes("auth instance")
) {
return { filePath: "src/lib/auth.ts" };
}
if (
question.name === "filePath" &&
question.message?.includes("route handler")
) {
return { filePath: "src/app/api/auth/[...all]/route.ts" };
}
if (question.message?.includes("configure a database")) {
return { value: "skip" };
}
if (question.message?.includes("auth client configuration")) {
return { value: true };
}
if (
question.name === "filePath" &&
question.message?.includes("auth-client.ts")
) {
authClientPromptCalled = true;
return { filePath: "src/lib/auth-client.ts" };
}
return {};
});
await initAction({ cwd: tmp });
// Verify the prompt was called
expect(authClientPromptCalled).toBe(true);
// Check that auth client file was created
const authClientPath = path.join(tmp, "src/lib/auth-client.ts");
const authClientExists = await fs
.access(authClientPath)
.then(() => true)
.catch(() => false);
expect(authClientExists).toBe(true);
},
);
testWithTmpDir(
"should handle schema overwrite confirmation",
async ({ tmp }) => {
await fs.writeFile(
path.join(tmp, "package.json"),
JSON.stringify({ name: "test-project", version: "1.0.0" }),
);
await fs.mkdir(path.join(tmp, "src/lib"), { recursive: true });
await fs.writeFile(
path.join(tmp, "src/lib/auth-schema.ts"),
"export const oldSchema = {};",
);
let overwritePromptCalled = false;
mockPrompts.mockImplementation(async (questions: any) => {
const question = Array.isArray(questions) ? questions[0] : questions;
if (question.message?.includes("install better-auth")) {
return { value: true };
}
if (
question.type === "confirm" &&
question.message?.includes(".env file")
) {
return { value: false };
}
if (
question.name === "filePath" &&
question.message?.includes("auth instance")
) {
return { filePath: "src/lib/auth.ts" };
}
if (
question.name === "filePath" &&
question.message?.includes("route handler")
) {
return { filePath: "src/app/api/auth/[...all]/route.ts" };
}
if (question.message?.includes("configure a database")) {
return { value: "yes" };
}
if (question.message?.includes("Select the database")) {
return { value: "drizzle-postgresql" };
}
if (question.message?.includes("Select the database dialect")) {
return { value: "drizzle-postgresql" };
}
if (question.name === "shouldInstallDeps") {
return { shouldInstallDeps: false };
}
if (question.name === "shouldGenerate") {
return { shouldGenerate: true };
}
if (question.message?.includes("email & password")) {
return { value: true };
}
if (question.message?.includes("setup social providers")) {
return { value: false };
}
if (question.message?.includes("auth client configuration")) {
return { value: false };
}
// Overwrite confirmation - check for the confirm type
if (
question.type === "confirm" &&
question.message?.includes("already exists") &&
question.message?.includes("overwrite")
) {
overwritePromptCalled = true;
return { value: true };
}
return {};
});
mockGenerateDrizzleSchema.mockResolvedValue({
code: "export const schema = {};",
fileName: "auth-schema.ts",
overwrite: true,
});
await initAction({ cwd: tmp });
expect(mockGenerateDrizzleSchema).toHaveBeenCalled();
// If overwrite was confirmed, the file should be updated
if (overwritePromptCalled) {
const schemaContent = await fs.readFile(
path.join(tmp, "src/lib/auth-schema.ts"),
"utf-8",
);
expect(schemaContent).toBe("export const schema = {};");
}
},
);
testWithTmpDir(
"should skip schema generation when user declines",
async ({ tmp }) => {
await fs.writeFile(
path.join(tmp, "package.json"),
JSON.stringify({ name: "test-project", version: "1.0.0" }),
);
mockPrompts.mockImplementation(async (questions: any) => {
const question = Array.isArray(questions) ? questions[0] : questions;
if (question.message?.includes("install better-auth")) {
return { value: true };
}
if (
question.type === "confirm" &&
question.message?.includes(".env file")
) {
return { value: false };
}
if (
question.name === "filePath" &&
question.message?.includes("auth instance")
) {
return { filePath: "auth.ts" };
}
if (
question.name === "filePath" &&
question.message?.includes("route handler")
) {
return { filePath: "src/app/api/auth/[...all]/route.ts" };
}
if (question.message?.includes("configure a database")) {
return { value: "yes" };
}
if (question.message?.includes("Select the database")) {
return { value: "drizzle-postgresql" };
}
if (question.message?.includes("Select the database dialect")) {
return { value: "drizzle-postgresql" };
}
if (question.name === "shouldInstallDeps") {
return { shouldInstallDeps: false };
}
if (question.name === "shouldGenerate") {
return { shouldGenerate: false };
}
if (question.message?.includes("email & password")) {
return { value: true };
}
if (question.message?.includes("setup social providers")) {
return { value: false };
}
if (question.message?.includes("auth client configuration")) {
return { value: false };
}
return {};
});
await initAction({ cwd: tmp });
expect(mockGenerateDrizzleSchema).not.toHaveBeenCalled();
},
);
testWithTmpDir(
"should skip migration when user declines",
async ({ tmp }) => {
await fs.writeFile(
path.join(tmp, "package.json"),
JSON.stringify({ name: "test-project", version: "1.0.0" }),
);
mockPrompts.mockImplementation(async (questions: any) => {
const question = Array.isArray(questions) ? questions[0] : questions;
if (question.message?.includes("install better-auth")) {
return { value: true };
}
if (
question.type === "confirm" &&
question.message?.includes(".env file")
) {
return { value: false };
}
if (
question.name === "filePath" &&
question.message?.includes("auth instance")
) {
return { filePath: "auth.ts" };
}
if (
question.name === "filePath" &&
question.message?.includes("route handler")
) {
return { filePath: "src/app/api/auth/[...all]/route.ts" };
}
if (question.message?.includes("configure a database")) {
return { value: "yes" };
}
if (question.message?.includes("Select the database")) {
return { value: "sqlite-better-sqlite3" };
}
if (question.name === "shouldInstallDeps") {
return { shouldInstallDeps: false };
}
if (question.name === "shouldMigrate") {
return { shouldMigrate: false };
}
if (question.message?.includes("email & password")) {
return { value: true };
}
if (question.message?.includes("setup social providers")) {
return { value: false };
}
if (question.message?.includes("auth client configuration")) {
return { value: false };
}
return {};
});
await initAction({ cwd: tmp });
expect(mockExec).not.toHaveBeenCalled();
},
);
testWithTmpDir("should handle multiple env files", async ({ tmp }) => {
await fs.writeFile(
path.join(tmp, "package.json"),
JSON.stringify({ name: "test-project", version: "1.0.0" }),
);
await fs.writeFile(path.join(tmp, ".env"), "EXISTING_VAR=value");
await fs.writeFile(path.join(tmp, ".env.local"), "OTHER_VAR=value");
mockPrompts.mockImplementation(async (questions: any) => {
const question = Array.isArray(questions) ? questions[0] : questions;
if (question.message?.includes("install better-auth")) {
return { value: true };
}
if (
question.name === "filePath" &&
question.message?.includes("auth instance")
) {
return { filePath: "auth.ts" };
}
if (
question.name === "filePath" &&
question.message?.includes("route handler")
) {
return { filePath: "src/app/api/auth/[...all]/route.ts" };
}
if (question.message?.includes("configure a database")) {
return { value: "skip" };
}
if (question.message?.includes("auth client configuration")) {
return { value: false };
}
// Select which env files to update
if (question.message?.includes("Add required environment variables")) {
return [path.join(tmp, ".env"), path.join(tmp, ".env.local")];
}
return {};
});
await initAction({ cwd: tmp });
// Both env files should have been checked
const envContent = await fs.readFile(path.join(tmp, ".env"), "utf-8");
const envLocalContent = await fs.readFile(
path.join(tmp, ".env.local"),
"utf-8",
);
expect(envContent).toBeTruthy();
expect(envLocalContent).toBeTruthy();
});
testWithTmpDir("should use different package managers", async ({ tmp }) => {
await fs.writeFile(
path.join(tmp, "package.json"),
JSON.stringify({ name: "test-project", version: "1.0.0" }),
);
mockDetectPackageManager.mockResolvedValue({
packageManager: "pnpm",
version: "9.0.0",
});
mockPrompts.mockImplementation(async (questions: any) => {
const question = Array.isArray(questions) ? questions[0] : questions;
if (question.message?.includes("install better-auth")) {
return { value: true };
}
if (
question.type === "confirm" &&
question.message?.includes(".env file")
) {
return { value: false };
}
if (
question.name === "filePath" &&
question.message?.includes("auth instance")
) {
return { filePath: "auth.ts" };
}
if (
question.name === "filePath" &&
question.message?.includes("route handler")
) {
return { filePath: "src/app/api/auth/[...all]/route.ts" };
}
if (question.message?.includes("configure a database")) {
return { value: "skip" };
}
if (question.message?.includes("auth client configuration")) {
return { value: false };
}
return {};
});
await initAction({ cwd: tmp });
// Verify package manager was used
expect(mockDetectPackageManager).toHaveBeenCalled();
expect(mockInstallDependencies).toHaveBeenCalled();
});
testWithTmpDir(
"should skip database setup when user selects skip",
async ({ tmp }) => {
await fs.writeFile(
path.join(tmp, "package.json"),
JSON.stringify({ name: "test-project", version: "1.0.0" }),
);
mockPrompts.mockImplementation(async (questions: any) => {
const question = Array.isArray(questions) ? questions[0] : questions;
if (question.message?.includes("install better-auth")) {
return { value: true };
}
if (
question.type === "confirm" &&
question.message?.includes(".env file")
) {
return { value: false };
}
if (
question.name === "filePath" &&
question.message?.includes("auth instance")
) {
return { filePath: "auth.ts" };
}
if (
question.name === "filePath" &&
question.message?.includes("route handler")
) {
return { filePath: "src/app/api/auth/[...all]/route.ts" };
}
if (question.message?.includes("configure a database")) {
return { value: "skip" };
}
if (question.message?.includes("auth client configuration")) {
return { value: false };
}
return {};
});
await initAction({ cwd: tmp });
expect(mockGenerateDrizzleSchema).not.toHaveBeenCalled();
expect(mockGeneratePrismaSchema).not.toHaveBeenCalled();
expect(mockExec).not.toHaveBeenCalled();
},
);
});