/** * @jest-environment node */ import { DrizzleDatabase } from "../drizzle"; import { DatabaseConfig } from "../types"; import Database from "better-sqlite3"; import { drizzle } from "drizzle-orm/better-sqlite3"; import * as schema from "@fitai/database"; describe("DrizzleDatabase", () => { let db: DrizzleDatabase; let sqlite: Database.Database; let drizzleDb: ReturnType; beforeEach(async () => { // Create in-memory SQLite database sqlite = new Database(":memory:"); drizzleDb = drizzle(sqlite, { schema }); const config: DatabaseConfig = { type: "sqlite", connection: { filename: ":memory:" }, options: { logging: false }, }; // Initialize DrizzleDatabase with the in-memory db db = new DrizzleDatabase(config, drizzleDb as any); await db.connect(); }); afterEach(async () => { await db.disconnect(); sqlite.close(); }); describe("User Operations", () => { it("should create and retrieve a user", async () => { const newUser = { email: "test@example.com", firstName: "Test", lastName: "User", password: "password123", role: "client" as const, }; const createdUser = await db.createUser(newUser); expect(createdUser).toBeDefined(); expect(createdUser.id).toBeDefined(); expect(createdUser.email).toBe(newUser.email); const fetchedUser = await db.getUserById(createdUser.id); expect(fetchedUser).toBeDefined(); expect(fetchedUser?.id).toBe(createdUser.id); expect(fetchedUser?.email).toBe(createdUser.email); // SQLite/Drizzle might truncate ms or handle dates differently in test env expect(new Date(fetchedUser!.createdAt).getTime()).toBeLessThanOrEqual( createdUser.createdAt.getTime(), ); expect(new Date(fetchedUser!.updatedAt).getTime()).toBeLessThanOrEqual( createdUser.updatedAt.getTime(), ); }); it("should update a user", async () => { const user = await db.createUser({ email: "update@example.com", firstName: "Update", lastName: "Me", password: "password", role: "client", }); const updated = await db.updateUser(user.id, { firstName: "Updated" }); expect(updated?.firstName).toBe("Updated"); const fetched = await db.getUserById(user.id); expect(fetched?.firstName).toBe("Updated"); }); it("should delete a user", async () => { const user = await db.createUser({ email: "delete@example.com", firstName: "Delete", lastName: "Me", password: "password", role: "client", }); const deleted = await db.deleteUser(user.id); expect(deleted).toBe(true); const fetched = await db.getUserById(user.id); expect(fetched).toBeNull(); }); }); describe("Client Operations", () => { it("should create and retrieve a client", async () => { const user = await db.createUser({ email: "client@example.com", firstName: "Client", lastName: "User", password: "password", role: "client", }); const newClient = { userId: user.id, membershipType: "basic" as const, membershipStatus: "active" as const, joinDate: new Date(), }; const createdClient = await db.createClient(newClient); expect(createdClient).toBeDefined(); expect(createdClient.userId).toBe(user.id); const fetchedClient = await db.getClientById(createdClient.id); expect(fetchedClient).toBeDefined(); expect(fetchedClient?.id).toBe(createdClient.id); expect(fetchedClient?.userId).toBe(createdClient.userId); expect(fetchedClient?.membershipType).toBe(createdClient.membershipType); }); }); describe("Fitness Profile Operations", () => { it("should create and retrieve a fitness profile", async () => { const user = await db.createUser({ email: "profile@example.com", firstName: "Profile", lastName: "User", password: "password", role: "client", }); const newProfile = { userId: user.id, height: 180, weight: 75, age: 30, gender: "male" as const, activityLevel: "moderately_active" as const, fitnessGoals: ["weight_loss"], exerciseHabits: "None", dietHabits: "None", medicalConditions: "None", }; const createdProfile = await db.createFitnessProfile(newProfile as any); expect(createdProfile).toBeDefined(); expect(createdProfile.userId).toBe(user.id); const fetchedProfile = await db.getFitnessProfileByUserId(user.id); expect(fetchedProfile).toBeDefined(); expect(fetchedProfile?.userId).toBe(user.id); expect(fetchedProfile?.fitnessGoals).toEqual( expect.arrayContaining(["weight_loss"]), ); }); }); describe("Attendance Operations", () => { it("should check in and check out", async () => { const user = await db.createUser({ email: "attendance@example.com", firstName: "Attendance", lastName: "User", password: "password", role: "client", }); const checkIn = await db.checkIn(user.id, "gym"); expect(checkIn).toBeDefined(); expect(checkIn.checkOutTime).toBeUndefined(); const activeCheckIn = await db.getActiveCheckIn(user.id); expect(activeCheckIn).toBeDefined(); expect(activeCheckIn?.id).toBe(checkIn.id); const checkOut = await db.checkOut(checkIn.id); expect(checkOut?.checkOutTime).toBeDefined(); const activeCheckInAfter = await db.getActiveCheckIn(user.id); expect(activeCheckInAfter).toBeNull(); }); }); describe("Batch Query Operations (Phase 9)", () => { describe("getClientsByUserIds", () => { it("should return clients for multiple users", async () => { // Create users const user1 = await db.createUser({ email: "batch1@example.com", firstName: "Batch", lastName: "User1", password: "password", role: "client", }); const user2 = await db.createUser({ email: "batch2@example.com", firstName: "Batch", lastName: "User2", password: "password", role: "client", }); const user3 = await db.createUser({ email: "batch3@example.com", firstName: "Batch", lastName: "User3", password: "password", role: "client", }); // Create clients for user1 and user2 only await db.createClient({ userId: user1.id, membershipType: "basic", membershipStatus: "active", joinDate: new Date(), }); await db.createClient({ userId: user2.id, membershipType: "premium", membershipStatus: "active", joinDate: new Date(), }); // Batch load clients const clients = await db.getClientsByUserIds([ user1.id, user2.id, user3.id, ]); expect(clients).toHaveLength(2); expect(clients.map((c) => c.userId).sort()).toEqual( [user1.id, user2.id].sort(), ); expect(clients.find((c) => c.userId === user3.id)).toBeUndefined(); }); it("should return empty array for empty input", async () => { const clients = await db.getClientsByUserIds([]); expect(clients).toEqual([]); }); it("should return empty array when no clients exist for users", async () => { const clients = await db.getClientsByUserIds(["nonexistent-id"]); expect(clients).toEqual([]); }); }); describe("getAttendanceHistoriesByUserIds", () => { it("should return attendance records for multiple users", async () => { const user1 = await db.createUser({ email: "attendance1@example.com", firstName: "Attendance", lastName: "User1", password: "password", role: "client", }); const user2 = await db.createUser({ email: "attendance2@example.com", firstName: "Attendance", lastName: "User2", password: "password", role: "client", }); // Create attendance records await db.checkIn(user1.id, "gym"); await db.checkIn(user1.id, "gym"); await db.checkIn(user2.id, "gym"); const attendances = await db.getAttendanceHistoriesByUserIds([ user1.id, user2.id, ]); expect(attendances.length).toBeGreaterThanOrEqual(3); const user1Records = attendances.filter((a) => a.userId === user1.id); const user2Records = attendances.filter((a) => a.userId === user2.id); expect(user1Records.length).toBeGreaterThanOrEqual(2); expect(user2Records.length).toBeGreaterThanOrEqual(1); }); it("should return records ordered by checkInTime DESC", async () => { const user = await db.createUser({ email: "order@example.com", firstName: "Order", lastName: "Test", password: "password", role: "client", }); // Create multiple check-ins with slight delays const checkIn1 = await db.checkIn(user.id, "gym"); await new Promise((resolve) => setTimeout(resolve, 10)); const checkIn2 = await db.checkIn(user.id, "gym"); const attendances = await db.getAttendanceHistoriesByUserIds([user.id]); expect(attendances.length).toBeGreaterThanOrEqual(2); // Most recent should be first expect( new Date(attendances[0].checkInTime).getTime(), ).toBeGreaterThanOrEqual( new Date(attendances[1].checkInTime).getTime(), ); }); it("should return empty array for empty input", async () => { const attendances = await db.getAttendanceHistoriesByUserIds([]); expect(attendances).toEqual([]); }); }); describe("getActiveCheckInsByUserIds", () => { it("should return only active check-ins", async () => { const user1 = await db.createUser({ email: "active1@example.com", firstName: "Active", lastName: "User1", password: "password", role: "client", }); const user2 = await db.createUser({ email: "active2@example.com", firstName: "Active", lastName: "User2", password: "password", role: "client", }); // User1: check in and check out const checkIn1 = await db.checkIn(user1.id, "gym"); await db.checkOut(checkIn1.id); // User2: check in only (active) const checkIn2 = await db.checkIn(user2.id, "gym"); const activeCheckIns = await db.getActiveCheckInsByUserIds([ user1.id, user2.id, ]); expect(activeCheckIns).toHaveLength(1); expect(activeCheckIns[0].userId).toBe(user2.id); expect(activeCheckIns[0].checkOutTime).toBeUndefined(); }); it("should return empty array when no active check-ins exist", async () => { const user = await db.createUser({ email: "noactive@example.com", firstName: "NoActive", lastName: "User", password: "password", role: "client", }); const checkIn = await db.checkIn(user.id, "gym"); await db.checkOut(checkIn.id); const activeCheckIns = await db.getActiveCheckInsByUserIds([user.id]); expect(activeCheckIns).toEqual([]); }); it("should return empty array for empty input", async () => { const activeCheckIns = await db.getActiveCheckInsByUserIds([]); expect(activeCheckIns).toEqual([]); }); }); describe("getRecommendationsByUserIds", () => { it("should return recommendations for multiple users", async () => { const user1 = await db.createUser({ email: "rec1@example.com", firstName: "Rec", lastName: "User1", password: "password", role: "client", }); const user2 = await db.createUser({ email: "rec2@example.com", firstName: "Rec", lastName: "User2", password: "password", role: "client", }); // Create recommendations // Create fitness profiles (required for recommendations) const profile1 = await db.createFitnessProfile({ userId: user1.id, height: 180, weight: 75, age: 30, gender: "male", activityLevel: "moderately_active", } as any); const profile2 = await db.createFitnessProfile({ userId: user2.id, height: 165, weight: 60, age: 25, gender: "female", activityLevel: "lightly_active", } as any); await db.createRecommendation({ userId: user1.id, fitnessProfileId: profile1.id, recommendationText: "Test recommendation 1", activityPlan: "Activity plan 1", dietPlan: "Diet plan 1", status: "pending", } as any); await db.createRecommendation({ userId: user2.id, fitnessProfileId: profile2.id, recommendationText: "Test recommendation 2", activityPlan: "Activity plan 2", dietPlan: "Diet plan 2", status: "pending", } as any); const recommendations = await db.getRecommendationsByUserIds([ user1.id, user2.id, ]); expect(recommendations.length).toBeGreaterThanOrEqual(2); const user1Recs = recommendations.filter((r) => r.userId === user1.id); const user2Recs = recommendations.filter((r) => r.userId === user2.id); expect(user1Recs.length).toBeGreaterThanOrEqual(1); expect(user2Recs.length).toBeGreaterThanOrEqual(1); }); it("should return empty array for empty input", async () => { const recommendations = await db.getRecommendationsByUserIds([]); expect(recommendations).toEqual([]); }); }); describe("getFitnessGoalsByUserIds", () => { it("should return fitness goals for multiple users", async () => { const user1 = await db.createUser({ email: "goal1@example.com", firstName: "Goal", lastName: "User1", password: "password", role: "client", }); const user2 = await db.createUser({ email: "goal2@example.com", firstName: "Goal", lastName: "User2", password: "password", role: "client", }); // Create fitness goals await db.createFitnessGoal({ userId: user1.id, goalType: "weight_target", title: "Lose weight", targetValue: 10, currentValue: 0, unit: "kg", targetDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), status: "active", } as any); await db.createFitnessGoal({ userId: user2.id, goalType: "strength_milestone", title: "Gain muscle", targetValue: 5, currentValue: 0, unit: "kg", targetDate: new Date(Date.now() + 60 * 24 * 60 * 60 * 1000), status: "active", } as any); const goals = await db.getFitnessGoalsByUserIds([user1.id, user2.id]); expect(goals.length).toBeGreaterThanOrEqual(2); const user1Goals = goals.filter((g) => g.userId === user1.id); const user2Goals = goals.filter((g) => g.userId === user2.id); expect(user1Goals.length).toBeGreaterThanOrEqual(1); expect(user2Goals.length).toBeGreaterThanOrEqual(1); }); it("should return empty array for empty input", async () => { const goals = await db.getFitnessGoalsByUserIds([]); expect(goals).toEqual([]); }); }); describe("Batch Query Performance", () => { it("should be more efficient than individual queries", async () => { // Create 10 users with clients const users = []; for (let i = 0; i < 10; i++) { const user = await db.createUser({ email: `perf${i}@example.com`, firstName: "Perf", lastName: `User${i}`, password: "password", role: "client", }); await db.createClient({ userId: user.id, membershipType: "basic", membershipStatus: "active", joinDate: new Date(), }); users.push(user); } const userIds = users.map((u) => u.id); // Batch query (should be 1 query) const batchStart = Date.now(); const batchClients = await db.getClientsByUserIds(userIds); const batchTime = Date.now() - batchStart; // Individual queries (should be 10 queries) const individualStart = Date.now(); const individualClients = []; for (const userId of userIds) { const client = await db.getClientByUserId(userId); if (client) individualClients.push(client); } const individualTime = Date.now() - individualStart; // Verify same results expect(batchClients.length).toBe(individualClients.length); expect(batchClients.length).toBe(10); // Batch should be faster (or at least not significantly slower) // In practice, batch is much faster, but in-memory DB might not show huge difference expect(batchTime).toBeLessThanOrEqual(individualTime * 2); }); }); }); });