Spaces:
Sleeping
Sleeping
| /** | |
| * Script to seed test users using Better Auth's APIs | |
| * Creates 21 users with proper password hashing via Better Auth | |
| * | |
| * Usage: | |
| * pnpm test:e2e:seed | |
| */ | |
| import { config } from "dotenv"; | |
| import { TEST_USERS } from "../tests/constants/test-users"; | |
| // Load environment variables FIRST | |
| if (process.env.CI) { | |
| config({ path: ".env.test" }); | |
| } else { | |
| config(); | |
| } | |
| import { auth } from "auth/auth-instance"; | |
| import { USER_ROLES } from "app-types/roles"; | |
| import { sql } from "drizzle-orm"; | |
| import { drizzle } from "drizzle-orm/node-postgres"; | |
| import { Pool } from "pg"; | |
| import { | |
| UserTable, | |
| ChatMessageTable, | |
| ChatThreadTable, | |
| } from "lib/db/pg/schema.pg"; | |
| import { like, eq } from "drizzle-orm"; | |
| // Create database connection with Pool | |
| const pool = new Pool({ | |
| connectionString: process.env.POSTGRES_URL!, | |
| }); | |
| const db = drizzle(pool); | |
| // Helper function to get user by email | |
| async function getUserByEmail(email: string) { | |
| const [user] = await db | |
| .select() | |
| .from(UserTable) | |
| .where(eq(UserTable.email, email)); | |
| return user || null; | |
| } | |
| async function clearExistingTestUsers() { | |
| console.log("π§Ή Clearing existing test users..."); | |
| try { | |
| // Clean up ALL test users with reliable patterns | |
| const testEmailPatterns = [ | |
| "%@test-seed.local%", // Our main seeded test domain | |
| "%playwright%", // Dynamically created playwright users | |
| "%@example.com%", // General test signup users | |
| "%testuser%@testuser.com%", // Legacy test users | |
| "%testuser%@gmail.com%", // Legacy test users | |
| ]; | |
| // First, get all test user IDs | |
| const testUsers: { id: string }[] = []; | |
| for (const pattern of testEmailPatterns) { | |
| const users = await db | |
| .select({ id: UserTable.id }) | |
| .from(UserTable) | |
| .where(like(UserTable.email, pattern)); | |
| testUsers.push(...users); | |
| } | |
| // Also get legacy test users by exact email match | |
| const legacyTestEmails = [ | |
| "admin@testuser.com", | |
| "editor@testuser.com", | |
| "user@testuser.com", | |
| ]; | |
| for (let i = 4; i <= 21; i++) { | |
| legacyTestEmails.push(`testuser${i}@testuser.com`); | |
| legacyTestEmails.push(`testuser${i}@gmail.com`); | |
| } | |
| for (const email of legacyTestEmails) { | |
| const users = await db | |
| .select({ id: UserTable.id }) | |
| .from(UserTable) | |
| .where(sql`email = ${email}`); | |
| testUsers.push(...users); | |
| } | |
| if (testUsers.length > 0) { | |
| const userIds = testUsers.map((u) => u.id); | |
| console.log(`Found ${userIds.length} test users to clean up`); | |
| // Delete in dependency order | |
| console.log("Deleting chat messages..."); | |
| // Messages reference threads, not users directly | |
| if (userIds.length > 0) { | |
| const threads = await db | |
| .select({ id: ChatThreadTable.id }) | |
| .from(ChatThreadTable) | |
| .where(sql`${ChatThreadTable.userId} = ANY(${userIds})`); | |
| const threadIds = threads.map((t) => t.id); | |
| if (threadIds.length > 0) { | |
| await db | |
| .delete(ChatMessageTable) | |
| .where(sql`${ChatMessageTable.threadId} = ANY(${threadIds})`); | |
| } | |
| } | |
| console.log("Deleting chat threads..."); | |
| for (const userId of userIds) { | |
| await db | |
| .delete(ChatThreadTable) | |
| .where(sql`${ChatThreadTable.userId} = ${userId}`); | |
| } | |
| // Now delete the users | |
| console.log("Deleting users..."); | |
| for (const pattern of testEmailPatterns) { | |
| await db.delete(UserTable).where(like(UserTable.email, pattern)); | |
| } | |
| for (const email of legacyTestEmails) { | |
| await db.delete(UserTable).where(sql`email = ${email}`); | |
| } | |
| } | |
| } catch (error) { | |
| console.log( | |
| "Note: Error during cleanup (may be expected if tables are empty):", | |
| error, | |
| ); | |
| } | |
| } | |
| async function createUserWithBetterAuth(userData: { | |
| email: string; | |
| password: string; | |
| name: string; | |
| role?: string; | |
| banned?: boolean; | |
| banReason?: string; | |
| }) { | |
| try { | |
| // First, check if user already exists | |
| const existingUser = await getUserByEmail(userData.email); | |
| let user; | |
| if (existingUser) { | |
| console.log( | |
| ` User ${userData.email} already exists, using existing user (ID: ${existingUser.id})`, | |
| ); | |
| user = existingUser; | |
| } else { | |
| // Use Better Auth's signUp API to create user with proper password hashing | |
| const result = await auth.api.signUpEmail({ | |
| body: { | |
| email: userData.email, | |
| password: userData.password, | |
| name: userData.name, | |
| }, | |
| headers: new Headers({ | |
| "content-type": "application/json", | |
| }), | |
| }); | |
| if (!result.user) { | |
| throw new Error("User creation failed"); | |
| } | |
| user = result.user; | |
| console.log(` Created new user ${userData.email} (ID: ${user.id})`); | |
| } | |
| // Update user role if needed | |
| // IMPORTANT: Check current role first to avoid overwriting first-user admin | |
| const [currentUser] = await db | |
| .select() | |
| .from(UserTable) | |
| .where(sql`id = ${user.id}`); | |
| if (userData.role && currentUser) { | |
| // If this is the first user and they already have admin role from Better Auth hook, | |
| // and we're trying to set admin role, that's fine - they match | |
| // const _isFirstUserAdmin = currentUser.role === USER_ROLES.ADMIN && userData.role === USER_ROLES.ADMIN; | |
| // Only update if the role is different and it's not the first-user-admin case | |
| if (currentUser.role !== userData.role) { | |
| try { | |
| console.log( | |
| ` Updating role from ${currentUser.role} to ${userData.role} for ${userData.email}`, | |
| ); | |
| await db | |
| .update(UserTable) | |
| .set({ role: userData.role }) | |
| .where(sql`id = ${user.id}`); | |
| } catch (error) { | |
| console.warn(`Could not set role for ${userData.email}:`, error); | |
| } | |
| } else { | |
| console.log( | |
| ` Role already correct (${currentUser.role}) for ${userData.email}`, | |
| ); | |
| } | |
| } | |
| // Ban user if needed - do this via direct database update since we don't have admin auth | |
| if (userData.banned && userData.banReason) { | |
| try { | |
| await db | |
| .update(UserTable) | |
| .set({ | |
| banned: true, | |
| banReason: userData.banReason, | |
| banExpires: null, // Permanent ban for testing | |
| }) | |
| .where(sql`id = ${user.id}`); | |
| } catch (error) { | |
| console.warn(`Could not ban user ${userData.email}:`, error); | |
| } | |
| } | |
| return user; | |
| } catch (error) { | |
| console.error(`Failed to create user ${userData.email}:`, error); | |
| // Try to get existing user as fallback | |
| try { | |
| const existingUser = await getUserByEmail(userData.email); | |
| if (existingUser) { | |
| console.log( | |
| ` Found existing user ${userData.email} after error, using existing user (ID: ${existingUser.id})`, | |
| ); | |
| return existingUser; | |
| } | |
| } catch (fallbackError) { | |
| console.warn( | |
| `Could not retrieve existing user ${userData.email}:`, | |
| fallbackError, | |
| ); | |
| } | |
| return null; | |
| } | |
| } | |
| async function seedTestUsers() { | |
| console.log("π± Starting test user seeding using Better Auth APIs..."); | |
| try { | |
| // Clear existing test users first | |
| await clearExistingTestUsers(); | |
| console.log("β Existing test users cleared"); | |
| console.log("π€ Creating main test users..."); | |
| // 1. Admin User | |
| const adminUser = await createUserWithBetterAuth({ | |
| email: TEST_USERS.admin.email, | |
| password: TEST_USERS.admin.password, | |
| name: TEST_USERS.admin.name, | |
| role: USER_ROLES.ADMIN, | |
| }); | |
| console.log("β Created admin user:", adminUser?.id); | |
| // 2. Editor User | |
| const editorUser = await createUserWithBetterAuth({ | |
| email: TEST_USERS.editor.email, | |
| password: TEST_USERS.editor.password, | |
| name: TEST_USERS.editor.name, | |
| role: USER_ROLES.EDITOR, | |
| }); | |
| console.log("β Created editor user:", editorUser?.id); | |
| // 3. Editor2 User | |
| const editor2User = await createUserWithBetterAuth({ | |
| email: TEST_USERS.editor2.email, | |
| password: TEST_USERS.editor2.password, | |
| name: TEST_USERS.editor2.name, | |
| role: USER_ROLES.EDITOR, | |
| }); | |
| console.log("β Created editor2 user:", editor2User?.id); | |
| // 4. Regular User | |
| const regularUser = await createUserWithBetterAuth({ | |
| email: TEST_USERS.regular.email, | |
| password: TEST_USERS.regular.password, | |
| name: TEST_USERS.regular.name, | |
| role: USER_ROLES.USER, | |
| }); | |
| console.log("β Created regular user:", regularUser?.id); | |
| // 5. Create additional test users | |
| console.log("π₯ Creating additional test users..."); | |
| let createdCount = 4; | |
| for (let i = 4; i <= 21; i++) { | |
| try { | |
| const isEditor = i <= 9; | |
| const isBanned = i === 21; | |
| const email = `testuser${i}@test-seed.local`; | |
| await createUserWithBetterAuth({ | |
| email, | |
| password: `TestPass${i}!`, | |
| name: `Test User ${i}`, | |
| role: isEditor ? USER_ROLES.EDITOR : USER_ROLES.USER, | |
| banned: isBanned, | |
| banReason: isBanned ? "Test ban for E2E testing" : undefined, | |
| }); | |
| createdCount++; | |
| console.log(`β Created user ${i}`); | |
| } catch (_error) { | |
| console.warn(`β οΈ Failed to create user ${i}, continuing...`); | |
| } | |
| } | |
| // 6. Seed some basic message/model data for stats testing | |
| console.log("π Creating sample AI usage data for stats testing..."); | |
| const userIdsForSampleData = [ | |
| adminUser?.id, | |
| editorUser?.id, | |
| editor2User?.id, | |
| regularUser?.id, | |
| ].filter(Boolean) as string[]; | |
| if (userIdsForSampleData.length > 0) { | |
| await seedSampleUsageData(userIdsForSampleData); | |
| } else { | |
| console.warn("β οΈ No valid user IDs found for sample data creation"); | |
| } | |
| console.log( | |
| `\nβ Test data seeded successfully! Created ${createdCount} users with sample usage data.`, | |
| ); | |
| console.log("\nπ Test Credentials:"); | |
| console.log( | |
| ` Admin: ${TEST_USERS.admin.email} / ${TEST_USERS.admin.password}`, | |
| ); | |
| console.log( | |
| ` Editor: ${TEST_USERS.editor.email} / ${TEST_USERS.editor.password}`, | |
| ); | |
| console.log( | |
| ` Editor2: ${TEST_USERS.editor2.email} / ${TEST_USERS.editor2.password}`, | |
| ); | |
| console.log( | |
| ` Regular: ${TEST_USERS.regular.email} / ${TEST_USERS.regular.password}`, | |
| ); | |
| console.log(` Others: testuser{4-21}@test-seed.local / TestPass{n}!`); | |
| console.log("\nπ Auth Files Will Be Created:"); | |
| console.log(` - tests/.auth/${TEST_USERS.admin.authFile} (admin user)`); | |
| console.log(` - tests/.auth/${TEST_USERS.editor.authFile} (editor user)`); | |
| console.log( | |
| ` - tests/.auth/${TEST_USERS.editor2.authFile} (editor2 user)`, | |
| ); | |
| console.log( | |
| ` - tests/.auth/${TEST_USERS.regular.authFile} (regular user)`, | |
| ); | |
| } catch (error) { | |
| console.error("β Error seeding test users:", error); | |
| throw error; | |
| } | |
| } | |
| async function seedSampleUsageData(userIds: string[]) { | |
| try { | |
| for (const userId of userIds) { | |
| if (!userId) { | |
| console.warn("β οΈ Skipping sample data creation for undefined user ID"); | |
| continue; | |
| } | |
| // Create sample threads and messages for user (should have stats) | |
| const thread = await db | |
| .insert(ChatThreadTable) | |
| .values({ | |
| userId: userId, | |
| title: `Test AI Conversation ${userId}`, | |
| }) | |
| .returning(); | |
| if (thread[0]) { | |
| // Create sample messages with token usage | |
| const timestamp = Date.now(); | |
| await db.insert(ChatMessageTable).values([ | |
| { | |
| id: `${userId}-msg-1-${timestamp}`, | |
| threadId: thread[0].id, | |
| role: "user" as const, | |
| parts: [{ type: "text", text: "Test user message" }], | |
| }, | |
| { | |
| id: `${userId}-msg-2-${timestamp}`, | |
| threadId: thread[0].id, | |
| role: "assistant" as const, | |
| parts: [{ type: "text", text: "Test assistant response" }], | |
| metadata: { | |
| chatModel: { provider: "openai", model: "gpt-4o" }, | |
| usage: { | |
| totalTokens: Math.floor(Math.random() * 100) + 100, | |
| inputTokens: Math.floor(Math.random() * 100) + 50, | |
| outputTokens: Math.floor(Math.random() * 100) + 50, | |
| }, | |
| }, | |
| }, | |
| { | |
| id: `${userId}-msg-3-${timestamp}`, | |
| threadId: thread[0].id, | |
| role: "assistant" as const, | |
| parts: [{ type: "text", text: "Another test response" }], | |
| metadata: { | |
| chatModel: { | |
| provider: "anthropic", | |
| model: "claude-3-5-sonnet-20241022", | |
| }, | |
| usage: { | |
| totalTokens: Math.floor(Math.random() * 100) + 100, | |
| inputTokens: Math.floor(Math.random() * 100) + 50, | |
| outputTokens: Math.floor(Math.random() * 100) + 50, | |
| }, | |
| }, | |
| }, | |
| ]); | |
| } | |
| } | |
| console.log(`β Created sample usage data for admin user`); | |
| // Editor user has no messages (should show empty state) | |
| console.log( | |
| `β Editor user left without usage data for empty state testing`, | |
| ); | |
| } catch (error) { | |
| console.warn("β οΈ Failed to seed usage data:", error); | |
| } | |
| } | |
| // Run the seeding if this script is executed directly | |
| if (import.meta.url === `file://${process.argv[1]}`) { | |
| seedTestUsers() | |
| .then(async () => { | |
| console.log("π Seeding completed!"); | |
| await pool.end(); | |
| process.exit(0); | |
| }) | |
| .catch(async (error) => { | |
| console.error("π₯ Seeding failed:", error); | |
| await pool.end(); | |
| process.exit(1); | |
| }); | |
| } | |