import { execFile } from "node:child_process"; import fs from "node:fs/promises"; import path from "node:path"; import { promisify } from "node:util"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { cliPath } from "./utils"; const execFileAsync = promisify(execFile); let tmpDir = "."; const runInfoCommand = (args: string[] = []) => { return execFileAsync(process.execPath, [cliPath, "info", ...args, "--json"], { cwd: tmpDir, }); }; const runInfoTextCommand = (args: string[] = []) => { return execFileAsync(process.execPath, [cliPath, "info", ...args], { cwd: tmpDir, }); }; const writeInstalledPackage = async (name: string, version: string) => { const packageDirectory = path.join( tmpDir, "node_modules", ...name.split("/"), ); await fs.mkdir(packageDirectory, { recursive: true }); await fs.writeFile( path.join(packageDirectory, "package.json"), JSON.stringify({ name, version }), ); }; describe("info command", () => { beforeEach(async () => { const tmp = path.join( process.cwd(), "node_modules", ".cache", "info_test-", ); await fs.mkdir(path.join(tmp, "node_modules", ".cache"), { recursive: true, }); tmpDir = await fs.mkdtemp(tmp); // Mock console methods to avoid noise in test output vi.spyOn(console, "log").mockImplementation(() => {}); vi.spyOn(console, "error").mockImplementation(() => {}); }); afterEach(async () => { await fs.rm(tmpDir, { recursive: true }); }); it("should display system information without auth config", async () => { // Create a minimal package.json await fs.writeFile( path.join(tmpDir, "package.json"), JSON.stringify({ name: "test-project", version: "1.0.0", dependencies: { "better-auth": "^1.0.0", }, }), ); const { stdout } = await runInfoCommand(); const output = JSON.parse(stdout); // Check system information expect(output.system).toHaveProperty("platform"); expect(output.system).toHaveProperty("arch"); expect(output.system).toHaveProperty("cpuCount"); expect(output.system).toHaveProperty("totalMemory"); // Check node information expect(output.node).toHaveProperty("version"); expect(output.node).toHaveProperty("env"); // Check package manager expect(output.packageManager).toHaveProperty("name"); expect(output.packageManager).toHaveProperty("version"); // Better Auth config should have an error since no auth file exists expect(output.betterAuth).toHaveProperty("version"); expect(output.betterAuth.config).toBeNull(); }); /** * @see https://github.com/better-auth/better-auth/issues/10997 */ it("reports installed versions for protocol-based dependencies", async () => { await fs.writeFile( path.join(tmpDir, "package.json"), JSON.stringify({ name: "test-project", version: "1.0.0", dependencies: { "better-auth": "catalog:auth", next: "catalog:frontend", "drizzle-orm": "workspace:*", }, }), ); await Promise.all([ writeInstalledPackage("better-auth", "1.7.2"), writeInstalledPackage("next", "16.1.0"), writeInstalledPackage("drizzle-orm", "0.45.1"), ]); const { stdout } = await runInfoCommand(); const output = JSON.parse(stdout); expect(output.betterAuth.version).toBe("1.7.2"); expect(output.frameworks).toContainEqual({ name: "next", version: "16.1.0", }); expect(output.databases).toContainEqual({ name: "drizzle", version: "0.45.1", }); }); /** * @see https://github.com/better-auth/better-auth/pull/11126#discussion_r3920527164 */ it("reports the installed version when config loading fails", async () => { await fs.writeFile( path.join(tmpDir, "package.json"), JSON.stringify({ name: "test-project", dependencies: { "better-auth": "catalog:" }, }), ); await writeInstalledPackage("better-auth", "1.7.2"); const { stdout } = await runInfoTextCommand([ "--config", "missing-auth.ts", ]); expect(stdout).toContain("Version: 1.7.2"); expect(stdout).toContain("Error:"); }); it("should load and sanitize auth configuration", async () => { // Create package.json with dependencies await fs.writeFile( path.join(tmpDir, "package.json"), JSON.stringify({ name: "test-project", version: "1.0.0", dependencies: { "better-auth": "^1.0.0", next: "^14.0.0", react: "^18.0.0", }, }), ); // Create auth.ts with sensitive data - using in-memory database to avoid adapter errors await fs.writeFile( path.join(tmpDir, "auth.ts"), `import { betterAuth } from "better-auth"; export const auth = betterAuth({ secret: "super-secret-key-123", baseURL: "https://example.com", emailAndPassword: { enabled: true, }, socialProviders: { github: { clientId: "github-client-id", clientSecret: "github-client-secret" }, google: { clientId: "google-client-id", clientSecret: "google-client-secret" } } })`, ); const { stdout } = await runInfoCommand(); const output = JSON.parse(stdout); // Check that sensitive data is redacted expect(output.betterAuth.config).toBeDefined(); expect(output.betterAuth.config.secret).toBe("[REDACTED]"); // Check social providers are sanitized expect(output.betterAuth.config.socialProviders).toBeDefined(); expect(output.betterAuth.config.socialProviders.github.clientId).toBe( "[REDACTED]", ); expect(output.betterAuth.config.socialProviders.github.clientSecret).toBe( "[REDACTED]", ); expect(output.betterAuth.config.socialProviders.google.clientId).toBe( "[REDACTED]", ); expect(output.betterAuth.config.socialProviders.google.clientSecret).toBe( "[REDACTED]", ); // Check non-sensitive data is preserved expect(output.betterAuth.config.emailAndPassword).toEqual({ enabled: true, }); expect(output.betterAuth.config.baseURL).toBe("https://example.com"); }); it("should redact versioned secrets", async () => { await fs.writeFile( path.join(tmpDir, "package.json"), JSON.stringify({ name: "test-project", version: "1.0.0", dependencies: { "better-auth": "^1.0.0", }, }), ); await fs.writeFile( path.join(tmpDir, "auth.ts"), `import { betterAuth } from "better-auth"; export const auth = betterAuth({ secrets: [ { version: 1, value: "old-rotation-secret-123" }, { version: 2, value: "new-rotation-secret-456" }, ], emailAndPassword: { enabled: true, } })`, ); const { stdout } = await runInfoCommand(); const output = JSON.parse(stdout); expect(output.betterAuth.config).toBeDefined(); expect(output.betterAuth.config.secrets).toBe("[REDACTED]"); const configStr = JSON.stringify(output.betterAuth.config); expect(configStr).not.toContain("old-rotation-secret-123"); expect(configStr).not.toContain("new-rotation-secret-456"); }); it("should detect installed frameworks", async () => { // Create package.json with various frameworks await fs.writeFile( path.join(tmpDir, "package.json"), JSON.stringify({ name: "test-project", version: "1.0.0", dependencies: { "better-auth": "^1.0.0", next: "^14.0.0", react: "^18.0.0", }, devDependencies: { "@sveltejs/kit": "^2.0.0", svelte: "^4.0.0", }, }), ); await Promise.all([ writeInstalledPackage("next", "14.0.0"), writeInstalledPackage("react", "18.0.0"), writeInstalledPackage("@sveltejs/kit", "2.0.0"), writeInstalledPackage("svelte", "4.0.0"), ]); const { stdout } = await runInfoCommand(); const output = JSON.parse(stdout); // Check frameworks are detected expect(output.frameworks).toContainEqual({ name: "next", version: "14.0.0", }); expect(output.frameworks).toContainEqual({ name: "react", version: "18.0.0", }); expect(output.frameworks).toContainEqual({ name: "@sveltejs/kit", version: "2.0.0", }); expect(output.frameworks).toContainEqual({ name: "svelte", version: "4.0.0", }); }); it("should detect database clients", async () => { // Create package.json with database clients await fs.writeFile( path.join(tmpDir, "package.json"), JSON.stringify({ name: "test-project", version: "1.0.0", dependencies: { "better-auth": "^1.0.0", "@prisma/client": "^5.0.0", kysely: "^0.26.0", }, devDependencies: { "drizzle-orm": "^0.29.0", "better-sqlite3": "^9.0.0", }, }), ); await Promise.all([ writeInstalledPackage("@prisma/client", "5.0.0"), writeInstalledPackage("kysely", "0.26.0"), writeInstalledPackage("drizzle-orm", "0.29.0"), writeInstalledPackage("better-sqlite3", "9.0.0"), ]); const { stdout } = await runInfoCommand(); const output = JSON.parse(stdout); // Check database clients are detected expect(output.databases).toContainEqual({ name: "@prisma/client", version: "5.0.0", }); expect(output.databases).toContainEqual({ name: "kysely", version: "0.26.0", }); expect(output.databases).toContainEqual({ name: "drizzle", version: "0.29.0", }); expect(output.databases).toContainEqual({ name: "better-sqlite3", version: "9.0.0", }); }); it("should support custom config path", async () => { // Create package.json await fs.writeFile( path.join(tmpDir, "package.json"), JSON.stringify({ name: "test-project", version: "1.0.0", dependencies: { "better-auth": "^1.0.0", }, }), ); // Create custom directory for auth config const customPath = path.join(tmpDir, "config"); await fs.mkdir(customPath, { recursive: true }); // Create auth config in custom location await fs.writeFile( path.join(customPath, "auth.config.ts"), `import { betterAuth } from "better-auth"; export const auth = betterAuth({ secret: "my-secret", appName: "Custom Config App", emailAndPassword: { enabled: true, } })`, ); const { stdout } = await runInfoCommand([ "--config", "config/auth.config.ts", ]); const output = JSON.parse(stdout); // Check that custom config was loaded expect(output.betterAuth.config).toBeDefined(); expect(output.betterAuth.config.appName).toBe("Custom Config App"); expect(output.betterAuth.config.secret).toBe("[REDACTED]"); expect(output.betterAuth.config.emailAndPassword).toEqual({ enabled: true, }); }); it("should sanitize plugin configurations", async () => { // Create package.json await fs.writeFile( path.join(tmpDir, "package.json"), JSON.stringify({ name: "test-project", version: "1.0.0", dependencies: { "better-auth": "^1.0.0", }, }), ); // Create auth.ts with plugins await fs.writeFile( path.join(tmpDir, "auth.ts"), `import { betterAuth } from "better-auth"; import { twoFactor, organization } from "better-auth/plugins"; export const auth = betterAuth({ plugins: [ twoFactor({ otpOptions: { secret: "otp-secret-key" } }), organization({ apiKey: "org-api-key", webhookSecret: "webhook-secret" }) ] })`, ); const { stdout } = await runInfoCommand(); const output = JSON.parse(stdout); // Check that plugin configs are sanitized expect(output.betterAuth.config.plugins).toBeDefined(); expect(Array.isArray(output.betterAuth.config.plugins)).toBe(true); // Plugin sensitive data should be redacted const plugins = output.betterAuth.config.plugins; plugins.forEach((plugin: any) => { if (plugin.config) { // Check that sensitive keys are redacted const configStr = JSON.stringify(plugin.config); expect(configStr).toContain("[REDACTED]"); } }); }); it("should handle missing package.json gracefully", async () => { // Don't create package.json const { stdout } = await runInfoCommand(); const output = JSON.parse(stdout); // Should still return system info expect(output.system).toBeDefined(); expect(output.node).toBeDefined(); expect(output.packageManager).toBeDefined(); // Frameworks and databases should be null expect(output.frameworks).toBeNull(); expect(output.databases).toBeNull(); }); });