diff --git a/convex/expenses.test.ts b/convex/expenses.test.ts index 58b3d53..4690a12 100644 --- a/convex/expenses.test.ts +++ b/convex/expenses.test.ts @@ -1,30 +1,25 @@ import { convexTest } from "convex-test"; -import { expect, test } from "vitest"; +import { beforeEach, expect, test, describe } from "vitest"; import { api } from "./_generated/api"; -import schema from "./schema"; -import { setupAuthMock } from "../lib/auth"; import { addMemberToGroup } from "./groups"; - -const authMock = setupAuthMock(); +import { createUser, setup } from "./test.setup"; async function setupTestGroup(t: ReturnType) { // Create a test user and group - const userId = await t.run(async (ctx) => { - return ctx.db.insert("users", { name: "User", email: "test@example.com" }); + const { userId, authSubject } = await createUser(t, { + name: "User", + email: "test@example.com", }); - authMock.setMockUserId(userId); - const asUser = t.withIdentity({ name: "User" }); + const asUser = t.withIdentity({ subject: authSubject }); const groupId = await asUser.mutation(api.groups.create, { name: "Test Group", }); // Create another test user for split testing - const otherUserId = await t.run(async (ctx) => { - return ctx.db.insert("users", { - name: "Other User", - email: "other@example.com", - }); + const { userId: otherUserId } = await createUser(t, { + name: "Other User", + email: "other@example.com", }); // Add the other user to the group @@ -35,221 +30,224 @@ async function setupTestGroup(t: ReturnType) { return { userId, otherUserId, groupId, asUser }; } -test("creating and listing expenses", async () => { - const t = convexTest(schema); - const { userId, otherUserId, groupId, asUser } = await setupTestGroup(t); - - // Create an expense with default split - const createResult = await asUser.mutation(api.expenses.create, { - groupId, - description: "Test Expense", - amount: 100, - splitType: "default", - splits: [], - note: "Test note", - }); - if (!createResult.success) { - throw new Error(`Failed to create expense: ${createResult.message}`); - } - - // List expenses and verify - const result = await asUser.query(api.expenses.listByGroup, { - groupId, - showSettled: false, - }); +describe("expenses", () => { + let t: ReturnType; - expect(result.expenses).toHaveLength(1); - expect(result.expenses[0]).toMatchObject({ - description: "Test Expense", - amount: 100, - note: "Test note", - status: "active", - paidBy: userId, + beforeEach(async () => { + t = setup(); }); - // Verify splits - expect(result.expenses[0].splits).toHaveLength(2); - expect(result.expenses[0].splits).toContainEqual( - expect.objectContaining({ - userId: userId, - amount: 50, - settled: false, - }), - ); - expect(result.expenses[0].splits).toContainEqual( - expect.objectContaining({ - userId: otherUserId, - amount: 50, - settled: false, - }), - ); -}); + test("creating and listing expenses", async () => { + const { userId, otherUserId, groupId, asUser } = await setupTestGroup(t); -test("creating expense with custom split", async () => { - const t = convexTest(schema); - const { userId, otherUserId, groupId, asUser } = await setupTestGroup(t); - - // Create an expense with custom split - const createResult = await asUser.mutation(api.expenses.create, { - groupId, - description: "Custom Split Expense", - amount: 100, - splitType: "custom", - splits: [ - { userId: userId, amount: 30 }, - { userId: otherUserId, amount: 70 }, - ], - }); - if (!createResult.success) { - throw new Error(`Failed to create expense: ${createResult.message}`); - } - - // Verify the splits - const result = await asUser.query(api.expenses.listByGroup, { - groupId, - showSettled: false, - }); + // Create an expense with default split + const createResult = await asUser.mutation(api.expenses.create, { + groupId, + description: "Test Expense", + amount: 100, + splitType: "default", + splits: [], + note: "Test note", + }); + if (!createResult.success) { + throw new Error(`Failed to create expense: ${createResult.message}`); + } + + // List expenses and verify + const result = await asUser.query(api.expenses.listByGroup, { + groupId, + showSettled: false, + }); - expect(result.expenses[0].splits).toContainEqual( - expect.objectContaining({ - userId: userId, - amount: 30, - settled: false, - }), - ); - expect(result.expenses[0].splits).toContainEqual( - expect.objectContaining({ - userId: otherUserId, - amount: 70, - settled: false, - }), - ); -}); + expect(result.expenses).toHaveLength(1); + expect(result.expenses[0]).toMatchObject({ + description: "Test Expense", + amount: 100, + note: "Test note", + status: "active", + paidBy: userId, + }); -test("settling expense splits", async () => { - const t = convexTest(schema); - const { userId, otherUserId, groupId, asUser } = await setupTestGroup(t); - - // Create an expense - const createResult = await asUser.mutation(api.expenses.create, { - groupId, - description: "Expense to Settle", - amount: 100, - splitType: "default", - splits: [], - }); - if (!createResult.success) { - throw new Error(`Failed to create expense: ${createResult.message}`); - } - - // Settle one user's split - const settleResult = await asUser.mutation(api.expenses.settleExpense, { - expenseId: createResult.value, - userId: otherUserId, - }); - if (!settleResult.success) { - throw new Error(`Failed to settle expense: ${settleResult.message}`); - } - - // Verify the settlement - const result = await asUser.query(api.expenses.listByGroup, { - groupId, - showSettled: false, - }); + // Verify splits + expect(result.expenses[0].splits).toHaveLength(2); + expect(result.expenses[0].splits).toContainEqual( + expect.objectContaining({ + userId: userId, + amount: 50, + settled: false, + }), + ); + expect(result.expenses[0].splits).toContainEqual( + expect.objectContaining({ + userId: otherUserId, + amount: 50, + settled: false, + }), + ); + }); + + test("creating expense with custom split", async () => { + const { userId, otherUserId, groupId, asUser } = await setupTestGroup(t); + + // Create an expense with custom split + const createResult = await asUser.mutation(api.expenses.create, { + groupId, + description: "Custom Split Expense", + amount: 100, + splitType: "custom", + splits: [ + { userId: userId, amount: 30 }, + { userId: otherUserId, amount: 70 }, + ], + }); + if (!createResult.success) { + throw new Error(`Failed to create expense: ${createResult.message}`); + } + + // Verify the splits + const result = await asUser.query(api.expenses.listByGroup, { + groupId, + showSettled: false, + }); - const expense = result.expenses[0]; - const settledSplit = expense.splits.find( - (split) => split.userId === otherUserId, - ); - const unsettledSplit = expense.splits.find( - (split) => split.userId === userId, - ); - - expect(settledSplit?.settled).toBe(true); - expect(unsettledSplit?.settled).toBe(false); - expect(expense.status).toBe("active"); // Still active because not all splits are settled -}); + expect(result.expenses[0].splits).toContainEqual( + expect.objectContaining({ + userId: userId, + amount: 30, + settled: false, + }), + ); + expect(result.expenses[0].splits).toContainEqual( + expect.objectContaining({ + userId: otherUserId, + amount: 70, + settled: false, + }), + ); + }); + + test("settling expense splits", async () => { + const { userId, otherUserId, groupId, asUser } = await setupTestGroup(t); + + // Create an expense + const createResult = await asUser.mutation(api.expenses.create, { + groupId, + description: "Expense to Settle", + amount: 100, + splitType: "default", + splits: [], + }); + if (!createResult.success) { + throw new Error(`Failed to create expense: ${createResult.message}`); + } -test("settling entire group", async () => { - const t = convexTest(schema); - const { groupId, asUser } = await setupTestGroup(t); - - // Create a couple of expenses - const expense1Result = await asUser.mutation(api.expenses.create, { - groupId, - description: "Expense 1", - amount: 100, - splitType: "default", - splits: [], - }); - if (!expense1Result.success) { - throw new Error(`Failed to create expense 1: ${expense1Result.message}`); - } - - const expense2Result = await asUser.mutation(api.expenses.create, { - groupId, - description: "Expense 2", - amount: 50, - splitType: "default", - splits: [], - }); - if (!expense2Result.success) { - throw new Error(`Failed to create expense 2: ${expense2Result.message}`); - } + // Settle one user's split + const settleResult = await asUser.mutation(api.expenses.settleExpense, { + expenseId: createResult.value, + userId: otherUserId, + }); + if (!settleResult.success) { + throw new Error(`Failed to settle expense: ${settleResult.message}`); + } + + // Verify the settlement + const result = await asUser.query(api.expenses.listByGroup, { + groupId, + showSettled: false, + }); - // Settle the entire group - const settleResult = await asUser.mutation(api.expenses.settleGroup, { - groupId, - }); - if (!settleResult.success) { - throw new Error(`Failed to settle group: ${settleResult.message}`); - } - - // Verify all expenses are settled - const result = await asUser.query(api.expenses.listByGroup, { - groupId, - showSettled: true, - }); + const expense = result.expenses[0]; + const settledSplit = expense.splits.find( + (split) => split.userId === otherUserId, + ); + const unsettledSplit = expense.splits.find( + (split) => split.userId === userId, + ); + + expect(settledSplit?.settled).toBe(true); + expect(unsettledSplit?.settled).toBe(false); + expect(expense.status).toBe("active"); // Still active because not all splits are settled + }); + + test("settling entire group", async () => { + const { groupId, asUser } = await setupTestGroup(t); + + // Create a couple of expenses + const expense1Result = await asUser.mutation(api.expenses.create, { + groupId, + description: "Expense 1", + amount: 100, + splitType: "default", + splits: [], + }); + if (!expense1Result.success) { + throw new Error(`Failed to create expense 1: ${expense1Result.message}`); + } - expect(result.expenses.every((expense) => expense.status === "settled")).toBe( - true, - ); - expect( - result.expenses.every((expense) => - expense.splits.every((split) => split.settled), - ), - ).toBe(true); -}); + const expense2Result = await asUser.mutation(api.expenses.create, { + groupId, + description: "Expense 2", + amount: 50, + splitType: "default", + splits: [], + }); + if (!expense2Result.success) { + throw new Error(`Failed to create expense 2: ${expense2Result.message}`); + } -test("deleting expense", async () => { - const t = convexTest(schema); - const { groupId, asUser } = await setupTestGroup(t); - - // Create an expense - const createResult = await asUser.mutation(api.expenses.create, { - groupId, - description: "Expense to Delete", - amount: 100, - splitType: "default", - splits: [], - }); - if (!createResult.success) { - throw new Error(`Failed to create expense: ${createResult.message}`); - } + // Settle the entire group + const settleResult = await asUser.mutation(api.expenses.settleGroup, { + groupId, + }); + if (!settleResult.success) { + throw new Error(`Failed to settle group: ${settleResult.message}`); + } + + // Verify all expenses are settled + const result = await asUser.query(api.expenses.listByGroup, { + groupId, + showSettled: true, + }); - // Delete the expense - const deleteResult = await asUser.mutation(api.expenses.deleteExpense, { - expenseId: createResult.value, - }); - if (!deleteResult.success) { - throw new Error(`Failed to delete expense: ${deleteResult.message}`); - } - - // Verify the expense is gone - const result = await asUser.query(api.expenses.listByGroup, { - groupId, - showSettled: true, - }); + expect( + result.expenses.every((expense) => expense.status === "settled"), + ).toBe(true); + expect( + result.expenses.every((expense) => + expense.splits.every((split) => split.settled), + ), + ).toBe(true); + }); + + test("deleting expense", async () => { + const { groupId, asUser } = await setupTestGroup(t); + + // Create an expense + const createResult = await asUser.mutation(api.expenses.create, { + groupId, + description: "Expense to Delete", + amount: 100, + splitType: "default", + splits: [], + }); + if (!createResult.success) { + throw new Error(`Failed to create expense: ${createResult.message}`); + } - expect(result.expenses).toHaveLength(0); + // Delete the expense + const deleteResult = await asUser.mutation(api.expenses.deleteExpense, { + expenseId: createResult.value, + }); + if (!deleteResult.success) { + throw new Error(`Failed to delete expense: ${deleteResult.message}`); + } + + // Verify the expense is gone + const result = await asUser.query(api.expenses.listByGroup, { + groupId, + showSettled: true, + }); + + expect(result.expenses).toHaveLength(0); + }); }); diff --git a/convex/invites.test.ts b/convex/invites.test.ts index f30948d..2457094 100644 --- a/convex/invites.test.ts +++ b/convex/invites.test.ts @@ -1,38 +1,40 @@ -import { convexTest } from "convex-test"; -import { expect, test } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import { api, internal } from "./_generated/api"; -import schema from "./schema"; -import { setupAuthMock } from "../lib/auth"; +import { createUser, setup } from "./test.setup"; -const authMock = setupAuthMock(); +describe("invites", () => { + let t: ReturnType; -test("sending invites", async () => { - const t = convexTest(schema); - - const userId = await t.run(async (ctx) => { - return ctx.db.insert("users", { name: "User", email: "test@example.com" }); + beforeEach(async () => { + t = setup(); }); - authMock.setMockUserId(userId); - const asUser = t.withIdentity({ name: "User" }); + test("sending invites", async () => { + const { userId, authSubject } = await createUser(t, { + name: "User", + email: "test@example.com", + }); - // Create a group which will create the user - const groupId = await asUser.mutation(api.groups.create, { - name: "Test Group", - }); - expect(groupId).toBeDefined(); + const asUser = t.withIdentity({ subject: authSubject }); - // Test creating an invite - const invite = await asUser.mutation(internal.invites.getOrCreateInvite, { - groupId, - email: "invited@example.com", - invitedBy: userId, - }); + // Create a group which will create the user + const groupId = await asUser.mutation(api.groups.create, { + name: "Test Group", + }); + expect(groupId).toBeDefined(); + + // Test creating an invite + const invite = await asUser.mutation(internal.invites.getOrCreateInvite, { + groupId, + email: "invited@example.com", + invitedBy: userId, + }); - expect(invite).toMatchObject({ - groupName: "Test Group", - inviterEmail: "test@example.com", - isAdmin: true, // First user in a group is admin + expect(invite).toMatchObject({ + groupName: "Test Group", + inviterEmail: "test@example.com", + isAdmin: true, // First user in a group is admin + }); + expect(invite.inviteId).toBeDefined(); }); - expect(invite.inviteId).toBeDefined(); }); diff --git a/convex/test.setup.ts b/convex/test.setup.ts new file mode 100644 index 0000000..840e5d4 --- /dev/null +++ b/convex/test.setup.ts @@ -0,0 +1,24 @@ +import { convexTest } from "convex-test"; +import schema from "./schema"; +import { Doc } from "./_generated/dataModel"; + +export function setup() { + const t = convexTest(schema); + return t; +} + +export async function createUser( + t: ReturnType, + user: Partial>, +) { + const { userId, sessionId } = await t.run(async (ctx) => { + const userId = await ctx.db.insert("users", user); + const sessionId = await ctx.db.insert("authSessions", { + userId, + expirationTime: Date.now() + 1000 * 60 * 60 * 24 * 30, + }); + return { userId, sessionId }; + }); + const authSubject = `${userId}|${sessionId}`; + return { userId, sessionId, authSubject }; +}