diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5e93675..14ea9cc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -161,7 +161,8 @@ export default { ### Testing - Write tests for services and utilities. -- Use the existing mock utilities in `src/test/mocks/`. +- Use `createTestDb()` from `src/test/create-test-db.ts` for service tests that need a real database. +- Use the existing mock utilities in `src/test/mocks/` for Discord and API mocks. - Test both success and error cases. - Keep tests focused and independent. diff --git a/package.json b/package.json index e4b536d..1aa0926 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "lint:fix": "oxlint src --fix", "test": "vitest run", "test:watch": "vitest", - "verify": "run-p format:check lint tsc test", + "verify": "run-p format lint:fix tsc test", "postinstall": "git config core.hooksPath .hooks && chmod +x .hooks/*" }, "dependencies": { diff --git a/src/services/poll.service.test.ts b/src/services/poll.service.test.ts index 9f24329..1af5c75 100644 --- a/src/services/poll.service.test.ts +++ b/src/services/poll.service.test.ts @@ -1,32 +1,23 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { createMockPoll, createMockPollVote } from "../test/mocks/database.mock"; +import { cleanAllTables, createTestDb } from "../test/create-test-db"; import { PollService } from "./poll.service"; -const { mockDb } = vi.hoisted(() => { - const mockDb: any = { - select: vi.fn(() => mockDb), - from: vi.fn(() => mockDb), - where: vi.fn(() => Promise.resolve([])), - insert: vi.fn(() => mockDb), - values: vi.fn(() => mockDb), - returning: vi.fn(() => Promise.resolve([])), - }; - - return { mockDb }; -}); +let testDb: Awaited>; -vi.mock("../database/db", () => ({ db: mockDb })); +vi.mock("../database/db", () => ({ + get db() { + return testDb; + }, +})); describe("Service: PollService", () => { - beforeEach(() => { - // ... reset all mocks before each test ... - mockDb.select.mockClear().mockReturnValue(mockDb); - mockDb.from.mockClear().mockReturnValue(mockDb); - mockDb.where.mockClear().mockResolvedValue([]); - mockDb.insert.mockClear().mockReturnValue(mockDb); - mockDb.values.mockClear().mockReturnValue(mockDb); - mockDb.returning.mockClear().mockResolvedValue([]); + beforeAll(async () => { + testDb = await createTestDb(); + }); + + beforeEach(async () => { + await cleanAllTables(testDb); }); describe("createPoll", () => { @@ -36,21 +27,6 @@ describe("Service: PollService", () => { }); it("creates a new poll with the provided details", async () => { - // ARRANGE - const mockPoll = createMockPoll({ - messageId: "msg123", - channelId: "ch456", - creatorId: "user789", - question: "What's your favorite color?", - options: JSON.stringify([ - { text: "Red", votes: [] }, - { text: "Blue", votes: [] }, - { text: "Green", votes: [] }, - ]), - endTime: null, - }); - mockDb.returning.mockResolvedValue([mockPoll]); - // ACT const result = await PollService.createPoll( "msg123", @@ -61,27 +37,22 @@ describe("Service: PollService", () => { ); // ASSERT - expect(mockDb.insert).toHaveBeenCalledWith(expect.anything()); - expect(mockDb.values).toHaveBeenCalledWith({ - messageId: "msg123", - channelId: "ch456", - creatorId: "user789", - question: "What's your favorite color?", - options: JSON.stringify([ - { text: "Red", votes: [] }, - { text: "Blue", votes: [] }, - { text: "Green", votes: [] }, - ]), - endTime: undefined, - }); - expect(result).toEqual(mockPoll); + expect(result.messageId).toEqual("msg123"); + expect(result.channelId).toEqual("ch456"); + expect(result.creatorId).toEqual("user789"); + expect(result.question).toEqual("What's your favorite color?"); + expect(JSON.parse(result.options)).toEqual([ + { text: "Red", votes: [] }, + { text: "Blue", votes: [] }, + { text: "Green", votes: [] }, + ]); + expect(result.endTime).toBeNull(); + expect(result.id).toBeDefined(); }); it("creates a poll with an end time when provided", async () => { // ARRANGE const endTime = new Date("2024-12-31T23:59:59Z"); - const mockPoll = createMockPoll({ endTime }); - mockDb.returning.mockResolvedValue([mockPoll]); // ACT const result = await PollService.createPoll( @@ -94,11 +65,6 @@ describe("Service: PollService", () => { ); // ASSERT - expect(mockDb.values).toHaveBeenCalledWith( - expect.objectContaining({ - endTime, - }), - ); expect(result.endTime).toEqual(endTime); }); }); @@ -111,23 +77,17 @@ describe("Service: PollService", () => { it("returns a poll when found", async () => { // ARRANGE - const mockPoll = createMockPoll({ messageId: "msg123" }); - mockDb.where.mockResolvedValue([mockPoll]); + await PollService.createPoll("msg123", "ch456", "user789", "Question?", ["A", "B"]); // ACT const result = await PollService.getPoll("msg123"); // ASSERT - expect(mockDb.select).toHaveBeenCalled(); - expect(mockDb.from).toHaveBeenCalledWith(expect.anything()); - expect(mockDb.where).toHaveBeenCalled(); - expect(result).toEqual(mockPoll); + expect(result).not.toBeNull(); + expect(result?.messageId).toEqual("msg123"); }); it("returns null when poll is not found", async () => { - // ARRANGE - mockDb.where.mockResolvedValue([]); - // ACT const result = await PollService.getPoll("nonexistent"); @@ -144,50 +104,43 @@ describe("Service: PollService", () => { it("adds a vote when user has not voted", async () => { // ARRANGE - // ... mock getUserVote to return null ... - mockDb.where.mockResolvedValueOnce([]); + const poll = await PollService.createPoll("msg1", "ch1", "creator1", "Q?", ["A", "B"]); // ACT - const result = await PollService.addVote(1, "user123", 0); + const result = await PollService.addVote(poll.id, "user123", 0); // ASSERT - expect(mockDb.insert).toHaveBeenCalledWith(expect.anything()); - expect(mockDb.values).toHaveBeenCalledWith({ - pollId: 1, - userId: "user123", - optionIndex: 0, - }); expect(result).toEqual(true); + + const vote = await PollService.getUserVote(poll.id, "user123"); + expect(vote).not.toBeNull(); + expect(vote?.optionIndex).toEqual(0); }); it("returns false when user has already voted", async () => { // ARRANGE - const existingVote = createMockPollVote(); - // ... mock getUserVote to return existing vote ... - mockDb.where.mockResolvedValueOnce([existingVote]); + const poll = await PollService.createPoll("msg1", "ch1", "creator1", "Q?", ["A", "B"]); + await PollService.addVote(poll.id, "user123", 0); // ACT - const result = await PollService.addVote(1, "user123", 1); + const result = await PollService.addVote(poll.id, "user123", 1); // ASSERT - expect(mockDb.insert).not.toHaveBeenCalled(); expect(result).toEqual(false); }); it("allows voting for different option indices", async () => { // ARRANGE - mockDb.where.mockResolvedValueOnce([]); // ... no existing vote ... + const poll = await PollService.createPoll("msg1", "ch1", "creator1", "Q?", ["A", "B", "C"]); // ACT - const result = await PollService.addVote(1, "user123", 2); + const result = await PollService.addVote(poll.id, "user123", 2); // ASSERT - expect(mockDb.values).toHaveBeenCalledWith({ - pollId: 1, - userId: "user123", - optionIndex: 2, - }); expect(result).toEqual(true); + + const vote = await PollService.getUserVote(poll.id, "user123"); + expect(vote?.optionIndex).toEqual(2); }); }); @@ -199,29 +152,25 @@ describe("Service: PollService", () => { it("returns a vote when user has voted", async () => { // ARRANGE - const mockVote = createMockPollVote({ - pollId: 1, - userId: "user123", - optionIndex: 1, - }); - mockDb.where.mockResolvedValue([mockVote]); + const poll = await PollService.createPoll("msg1", "ch1", "creator1", "Q?", ["A", "B"]); + await PollService.addVote(poll.id, "user123", 1); // ACT - const result = await PollService.getUserVote(1, "user123"); + const result = await PollService.getUserVote(poll.id, "user123"); // ASSERT - expect(mockDb.select).toHaveBeenCalled(); - expect(mockDb.from).toHaveBeenCalledWith(expect.anything()); - expect(mockDb.where).toHaveBeenCalled(); - expect(result).toEqual(mockVote); + expect(result).not.toBeNull(); + expect(result?.pollId).toEqual(poll.id); + expect(result?.userId).toEqual("user123"); + expect(result?.optionIndex).toEqual(1); }); it("returns null when user has not voted", async () => { // ARRANGE - mockDb.where.mockResolvedValue([]); + const poll = await PollService.createPoll("msg1", "ch1", "creator1", "Q?", ["A", "B"]); // ACT - const result = await PollService.getUserVote(1, "user456"); + const result = await PollService.getUserVote(poll.id, "user456"); // ASSERT expect(result).toBeNull(); @@ -236,17 +185,15 @@ describe("Service: PollService", () => { it("returns vote counts by option index", async () => { // ARRANGE - const mockVotes = [ - createMockPollVote({ optionIndex: 0 }), - createMockPollVote({ optionIndex: 0 }), - createMockPollVote({ optionIndex: 1 }), - createMockPollVote({ optionIndex: 0 }), - createMockPollVote({ optionIndex: 2 }), - ]; - mockDb.where.mockResolvedValue(mockVotes); + const poll = await PollService.createPoll("msg1", "ch1", "creator1", "Q?", ["A", "B", "C"]); + await PollService.addVote(poll.id, "user1", 0); + await PollService.addVote(poll.id, "user2", 0); + await PollService.addVote(poll.id, "user3", 1); + await PollService.addVote(poll.id, "user4", 0); + await PollService.addVote(poll.id, "user5", 2); // ACT - const results = await PollService.getPollResults(1); + const results = await PollService.getPollResults(poll.id); // ASSERT expect(results).toBeInstanceOf(Map); @@ -257,10 +204,10 @@ describe("Service: PollService", () => { it("returns empty map when there are no votes", async () => { // ARRANGE - mockDb.where.mockResolvedValue([]); + const poll = await PollService.createPoll("msg1", "ch1", "creator1", "Q?", ["A", "B"]); // ACT - const results = await PollService.getPollResults(1); + const results = await PollService.getPollResults(poll.id); // ASSERT expect(results.size).toEqual(0); @@ -268,15 +215,20 @@ describe("Service: PollService", () => { it("handles votes for non-sequential option indices", async () => { // ARRANGE - const mockVotes = [ - createMockPollVote({ optionIndex: 0 }), - createMockPollVote({ optionIndex: 5 }), - createMockPollVote({ optionIndex: 5 }), - ]; - mockDb.where.mockResolvedValue(mockVotes); + const poll = await PollService.createPoll("msg1", "ch1", "creator1", "Q?", [ + "A", + "B", + "C", + "D", + "E", + "F", + ]); + await PollService.addVote(poll.id, "user1", 0); + await PollService.addVote(poll.id, "user2", 5); + await PollService.addVote(poll.id, "user3", 5); // ACT - const results = await PollService.getPollResults(1); + const results = await PollService.getPollResults(poll.id); // ASSERT expect(results.get(0)).toEqual(1); @@ -293,25 +245,20 @@ describe("Service: PollService", () => { it("returns polls with no end time", async () => { // ARRANGE - const mockPolls = [ - createMockPoll({ id: 1, endTime: null }), - createMockPoll({ id: 2, endTime: null }), - ]; - mockDb.where.mockResolvedValue(mockPolls); + await PollService.createPoll("msg1", "ch1", "creator1", "Q1?", ["A", "B"]); + await PollService.createPoll("msg2", "ch1", "creator1", "Q2?", ["A", "B"]); + await PollService.createPoll("msg3", "ch1", "creator1", "Q3?", ["A", "B"], new Date()); // ACT const result = await PollService.getActivePolls(); // ASSERT - expect(mockDb.select).toHaveBeenCalled(); - expect(mockDb.from).toHaveBeenCalledWith(expect.anything()); - expect(mockDb.where).toHaveBeenCalled(); - expect(result).toEqual(mockPolls); + expect(result).toHaveLength(2); }); it("returns empty array when there are no active polls", async () => { // ARRANGE - mockDb.where.mockResolvedValue([]); + await PollService.createPoll("msg1", "ch1", "creator1", "Q?", ["A", "B"], new Date()); // ACT const result = await PollService.getActivePolls(); diff --git a/src/services/team.service.test.ts b/src/services/team.service.test.ts index 144216f..04743f7 100644 --- a/src/services/team.service.test.ts +++ b/src/services/team.service.test.ts @@ -1,36 +1,23 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { createMockTeam, createMockTeamMember } from "../test/mocks/database.mock"; +import { cleanAllTables, createTestDb } from "../test/create-test-db"; import { TeamService } from "./team.service"; -const { mockDb } = vi.hoisted(() => { - const mockDb: any = { - select: vi.fn(() => mockDb), - from: vi.fn(() => mockDb), - where: vi.fn(() => Promise.resolve([])), - insert: vi.fn(() => mockDb), - values: vi.fn(() => mockDb), - returning: vi.fn(() => Promise.resolve([])), - onConflictDoNothing: vi.fn(() => Promise.resolve()), - delete: vi.fn(() => mockDb), - }; - - return { mockDb }; -}); +let testDb: Awaited>; -vi.mock("../database/db", () => ({ db: mockDb })); +vi.mock("../database/db", () => ({ + get db() { + return testDb; + }, +})); describe("Service: TeamService", () => { - beforeEach(() => { - // ... reset all mocks before each test ... - mockDb.select.mockClear().mockReturnValue(mockDb); - mockDb.from.mockClear().mockReturnValue(mockDb); - mockDb.where.mockClear().mockResolvedValue([]); - mockDb.insert.mockClear().mockReturnValue(mockDb); - mockDb.values.mockClear().mockReturnValue(mockDb); - mockDb.returning.mockClear().mockResolvedValue([]); - mockDb.onConflictDoNothing.mockClear().mockResolvedValue(undefined); - mockDb.delete.mockClear().mockReturnValue(mockDb); + beforeAll(async () => { + testDb = await createTestDb(); + }); + + beforeEach(async () => { + await cleanAllTables(testDb); }); describe("createTeam", () => { @@ -40,21 +27,14 @@ describe("Service: TeamService", () => { }); it("creates a new team with the provided details", async () => { - // ARRANGE - const mockTeam = createMockTeam({ id: "test-team-id", name: "test-team" }); - mockDb.returning.mockResolvedValue([mockTeam]); - // ACT const result = await TeamService.createTeam("test-team-id", "test-team", "admin123"); // ASSERT - expect(mockDb.insert).toHaveBeenCalledWith(expect.anything()); - expect(mockDb.values).toHaveBeenCalledWith({ - id: "test-team-id", - name: "test-team", - addedBy: "admin123", - }); - expect(result).toEqual(mockTeam); + expect(result.id).toEqual("test-team-id"); + expect(result.name).toEqual("test-team"); + expect(result.addedBy).toEqual("admin123"); + expect(result.addedAt).toBeDefined(); }); }); @@ -66,23 +46,18 @@ describe("Service: TeamService", () => { it("returns a team when found", async () => { // ARRANGE - const mockTeam = createMockTeam({ id: "team123" }); - mockDb.where.mockResolvedValue([mockTeam]); + await TeamService.createTeam("team123", "My Team", "admin1"); // ACT const result = await TeamService.getTeam("team123"); // ASSERT - expect(mockDb.select).toHaveBeenCalled(); - expect(mockDb.from).toHaveBeenCalledWith(expect.anything()); - expect(mockDb.where).toHaveBeenCalled(); - expect(result).toEqual(mockTeam); + expect(result).not.toBeNull(); + expect(result?.id).toEqual("team123"); + expect(result?.name).toEqual("My Team"); }); it("returns null when team is not found", async () => { - // ARRANGE - mockDb.where.mockResolvedValue([]); - // ACT const result = await TeamService.getTeam("nonexistent"); @@ -99,18 +74,27 @@ describe("Service: TeamService", () => { it("adds a member to a team", async () => { // ARRANGE + await TeamService.createTeam("team123", "Team", "admin1"); // ACT await TeamService.addMember("team123", "user456", "admin789"); // ASSERT - expect(mockDb.insert).toHaveBeenCalledWith(expect.anything()); - expect(mockDb.values).toHaveBeenCalledWith({ - teamId: "team123", - userId: "user456", - addedBy: "admin789", - }); - expect(mockDb.onConflictDoNothing).toHaveBeenCalled(); + const isMember = await TeamService.isTeamMember("team123", "user456"); + expect(isMember).toEqual(true); + }); + + it("silently handles duplicate additions", async () => { + // ARRANGE + await TeamService.createTeam("team123", "Team", "admin1"); + await TeamService.addMember("team123", "user456", "admin789"); + + // ACT - adding the same member again should not throw. + await TeamService.addMember("team123", "user456", "admin789"); + + // ASSERT + const members = await TeamService.getTeamMembers("team123"); + expect(members).toEqual(["user456"]); }); }); @@ -122,31 +106,27 @@ describe("Service: TeamService", () => { it("removes an existing member from a team", async () => { // ARRANGE - const mockMember = createMockTeamMember(); - - // ... mock isTeamMember to return true ... - mockDb.where.mockResolvedValueOnce([mockMember]); + await TeamService.createTeam("team123", "Team", "admin1"); + await TeamService.addMember("team123", "user456", "admin1"); // ACT const result = await TeamService.removeMember("team123", "user456"); // ASSERT - expect(mockDb.delete).toHaveBeenCalledWith(expect.anything()); - expect(mockDb.where).toHaveBeenCalled(); expect(result).toEqual(true); + + const isMember = await TeamService.isTeamMember("team123", "user456"); + expect(isMember).toEqual(false); }); it("returns false when member does not exist", async () => { // ARRANGE - - // ... mock isTeamMember to return false ... - mockDb.where.mockResolvedValueOnce([]); + await TeamService.createTeam("team123", "Team", "admin1"); // ACT const result = await TeamService.removeMember("team123", "user456"); // ASSERT - expect(mockDb.delete).not.toHaveBeenCalled(); expect(result).toEqual(false); }); }); @@ -159,20 +139,24 @@ describe("Service: TeamService", () => { it("returns an array of user IDs for team members", async () => { // ARRANGE - const mockMembers = [{ userId: "user1" }, { userId: "user2" }, { userId: "user3" }]; - mockDb.where.mockResolvedValue(mockMembers); + await TeamService.createTeam("team123", "Team", "admin1"); + await TeamService.addMember("team123", "user1", "admin1"); + await TeamService.addMember("team123", "user2", "admin1"); + await TeamService.addMember("team123", "user3", "admin1"); // ACT const result = await TeamService.getTeamMembers("team123"); // ASSERT - expect(mockDb.select).toHaveBeenCalledWith({ userId: expect.anything() }); - expect(result).toEqual(["user1", "user2", "user3"]); + expect(result).toHaveLength(3); + expect(result).toContain("user1"); + expect(result).toContain("user2"); + expect(result).toContain("user3"); }); it("returns an empty array when team has no members", async () => { // ARRANGE - mockDb.where.mockResolvedValue([]); + await TeamService.createTeam("team123", "Team", "admin1"); // ACT const result = await TeamService.getTeamMembers("team123"); @@ -190,7 +174,8 @@ describe("Service: TeamService", () => { it("returns true when user is a team member", async () => { // ARRANGE - mockDb.where.mockResolvedValue([createMockTeamMember()]); + await TeamService.createTeam("team123", "Team", "admin1"); + await TeamService.addMember("team123", "user456", "admin1"); // ACT const result = await TeamService.isTeamMember("team123", "user456"); @@ -201,7 +186,7 @@ describe("Service: TeamService", () => { it("returns false when user is not a team member", async () => { // ARRANGE - mockDb.where.mockResolvedValue([]); + await TeamService.createTeam("team123", "Team", "admin1"); // ACT const result = await TeamService.isTeamMember("team123", "user456"); @@ -219,19 +204,14 @@ describe("Service: TeamService", () => { it("returns all teams", async () => { // ARRANGE - const mockTeams = [ - createMockTeam({ id: "team1", name: "Team One" }), - createMockTeam({ id: "team2", name: "Team Two" }), - ]; - mockDb.from.mockReturnValue(Promise.resolve(mockTeams)); + await TeamService.createTeam("team1", "Team One", "admin1"); + await TeamService.createTeam("team2", "Team Two", "admin1"); // ACT const result = await TeamService.getAllTeams(); // ASSERT - expect(mockDb.select).toHaveBeenCalled(); - expect(mockDb.from).toHaveBeenCalledWith(expect.anything()); - expect(result).toEqual(mockTeams); + expect(result).toHaveLength(2); }); }); @@ -243,21 +223,17 @@ describe("Service: TeamService", () => { it("returns a team when found by name", async () => { // ARRANGE - const mockTeam = createMockTeam({ name: "test-team" }); - mockDb.where.mockResolvedValue([mockTeam]); + await TeamService.createTeam("team-id", "test-team", "admin1"); // ACT const result = await TeamService.getTeamByName("test-team"); // ASSERT - expect(mockDb.where).toHaveBeenCalled(); - expect(result).toEqual(mockTeam); + expect(result).not.toBeNull(); + expect(result?.name).toEqual("test-team"); }); it("returns null when team is not found by name", async () => { - // ARRANGE - mockDb.where.mockResolvedValue([]); - // ACT const result = await TeamService.getTeamByName("nonexistent"); @@ -274,27 +250,17 @@ describe("Service: TeamService", () => { it("adds a member when team exists", async () => { // ARRANGE - const mockTeam = createMockTeam({ id: "team123", name: "test-team" }); - - // ... mock getTeamByName ... - mockDb.where.mockResolvedValueOnce([mockTeam]); + await TeamService.createTeam("team123", "test-team", "admin1"); // ACT await TeamService.addMemberByTeamName("test-team", "user456", "admin789"); // ASSERT - expect(mockDb.insert).toHaveBeenCalled(); - expect(mockDb.values).toHaveBeenCalledWith({ - teamId: "team123", - userId: "user456", - addedBy: "admin789", - }); + const isMember = await TeamService.isTeamMember("team123", "user456"); + expect(isMember).toEqual(true); }); it("throws an error when team does not exist", async () => { - // ARRANGE - mockDb.where.mockResolvedValueOnce([]); - // ASSERT await expect( TeamService.addMemberByTeamName("nonexistent", "user456", "admin789"), @@ -310,31 +276,24 @@ describe("Service: TeamService", () => { it("removes a member when team and member exist", async () => { // ARRANGE - const mockTeam = createMockTeam({ id: "team123", name: "test-team" }); - const mockMember = createMockTeamMember(); - - // ... mock getTeamByName ... - mockDb.where.mockResolvedValueOnce([mockTeam]); - // ... mock isTeamMember ... - mockDb.where.mockResolvedValueOnce([mockMember]); + await TeamService.createTeam("team123", "test-team", "admin1"); + await TeamService.addMember("team123", "user456", "admin1"); // ACT const result = await TeamService.removeMemberByTeamName("test-team", "user456"); // ASSERT - expect(mockDb.delete).toHaveBeenCalled(); expect(result).toEqual(true); + + const isMember = await TeamService.isTeamMember("team123", "user456"); + expect(isMember).toEqual(false); }); it("returns false when team does not exist", async () => { - // ARRANGE - mockDb.where.mockResolvedValueOnce([]); - // ACT const result = await TeamService.removeMemberByTeamName("nonexistent", "user456"); // ASSERT - expect(mockDb.delete).not.toHaveBeenCalled(); expect(result).toEqual(false); }); }); @@ -347,25 +306,20 @@ describe("Service: TeamService", () => { it("returns team members when team exists", async () => { // ARRANGE - const mockTeam = createMockTeam({ id: "team123", name: "test-team" }); - const mockMembers = [{ userId: "user1" }, { userId: "user2" }]; - - // ... mock getTeamByName ... - mockDb.where.mockResolvedValueOnce([mockTeam]); - // ... mock getTeamMembers ... - mockDb.where.mockResolvedValueOnce(mockMembers); + await TeamService.createTeam("team123", "test-team", "admin1"); + await TeamService.addMember("team123", "user1", "admin1"); + await TeamService.addMember("team123", "user2", "admin1"); // ACT const result = await TeamService.getTeamMembersByName("test-team"); // ASSERT - expect(result).toEqual(["user1", "user2"]); + expect(result).toHaveLength(2); + expect(result).toContain("user1"); + expect(result).toContain("user2"); }); it("returns empty array when team does not exist", async () => { - // ARRANGE - mockDb.where.mockResolvedValueOnce([]); - // ACT const result = await TeamService.getTeamMembersByName("nonexistent"); diff --git a/src/services/uwc-poll.service.test.ts b/src/services/uwc-poll.service.test.ts index 36e4131..b07dc93 100644 --- a/src/services/uwc-poll.service.test.ts +++ b/src/services/uwc-poll.service.test.ts @@ -1,14 +1,23 @@ -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { db } from "../database/db"; -import { uwcPollResults, uwcPolls } from "../database/schema"; +import { cleanAllTables, createTestDb } from "../test/create-test-db"; import { UwcPollService } from "./uwc-poll.service"; +let testDb: Awaited>; + +vi.mock("../database/db", () => ({ + get db() { + return testDb; + }, +})); + describe("UwcPollService", () => { - // Clean up database after each test. - afterEach(async () => { - await db.delete(uwcPollResults); - await db.delete(uwcPolls); + beforeAll(async () => { + testDb = await createTestDb(); + }); + + beforeEach(async () => { + await cleanAllTables(testDb); }); describe("createUwcPoll", () => { @@ -31,11 +40,11 @@ describe("UwcPollService", () => { // ASSERT expect(poll).toBeDefined(); - expect(poll.messageId).toBe(pollData.messageId); - expect(poll.channelId).toBe(pollData.channelId); - expect(poll.threadId).toBe(pollData.threadId); - expect(poll.achievementId).toBe(pollData.achievementId); - expect(poll.status).toBe("active"); + expect(poll.messageId).toEqual(pollData.messageId); + expect(poll.channelId).toEqual(pollData.channelId); + expect(poll.threadId).toEqual(pollData.threadId); + expect(poll.achievementId).toEqual(pollData.achievementId); + expect(poll.status).toEqual("active"); expect(poll.endedAt).toBeNull(); }); @@ -74,7 +83,7 @@ describe("UwcPollService", () => { // ASSERT expect(poll).toBeDefined(); - expect(poll?.messageId).toBe("123456789"); + expect(poll?.messageId).toEqual("123456789"); }); it("returns null for non-existent poll", async () => { @@ -108,11 +117,11 @@ describe("UwcPollService", () => { ); // ASSERT - expect(poll.status).toBe("completed"); + expect(poll.status).toEqual("completed"); expect(poll.endedAt).toBeDefined(); expect(storedResults).toHaveLength(3); - expect(storedResults[0]?.optionText).toBe("No, leave as is"); - expect(storedResults[0]?.voteCount).toBe(5); + expect(storedResults[0]?.optionText).toEqual("No, leave as is"); + expect(storedResults[0]?.voteCount).toEqual(5); }); it("throws error for non-existent poll", async () => { @@ -145,7 +154,7 @@ describe("UwcPollService", () => { // ASSERT expect(activePolls).toHaveLength(1); - expect(activePolls[0]?.messageId).toBe("active1"); + expect(activePolls[0]?.messageId).toEqual("active1"); }); }); @@ -182,7 +191,7 @@ describe("UwcPollService", () => { // ASSERT expect(polls).toHaveLength(2); - expect(polls.every((p) => p.achievementId === 14402)).toBe(true); + expect(polls.every((p) => p.achievementId === 14402)).toEqual(true); }); }); @@ -218,7 +227,7 @@ describe("UwcPollService", () => { // ASSERT expect(polls).toHaveLength(1); - expect(polls[0]?.achievementName).toBe("Sonic Speed"); + expect(polls[0]?.achievementName).toEqual("Sonic Speed"); }); it("searches by game name", async () => { @@ -227,7 +236,7 @@ describe("UwcPollService", () => { // ASSERT expect(polls).toHaveLength(1); - expect(polls[0]?.gameName).toBe("Super Mario Bros."); + expect(polls[0]?.gameName).toEqual("Super Mario Bros."); }); it("returns empty array for no matches", async () => { diff --git a/src/test/create-test-db.ts b/src/test/create-test-db.ts new file mode 100644 index 0000000..4e75bbf --- /dev/null +++ b/src/test/create-test-db.ts @@ -0,0 +1,25 @@ +import { drizzle } from "drizzle-orm/libsql"; +import { migrate } from "drizzle-orm/libsql/migrator"; +import { resolve } from "node:path"; + +import * as schema from "../database/schema"; + +const migrationsFolder = resolve(import.meta.dirname, "../../drizzle"); + +export async function createTestDb() { + const db = drizzle({ connection: { url: "file::memory:" } }); + + await migrate(db, { migrationsFolder }); + + return db; +} + +// Tables are deleted in reverse dependency order to respect foreign key constraints. +export async function cleanAllTables(db: ReturnType) { + await db.delete(schema.uwcPollResults); + await db.delete(schema.pollVotes); + await db.delete(schema.teamMembers); + await db.delete(schema.uwcPolls); + await db.delete(schema.polls); + await db.delete(schema.teams); +} diff --git a/src/test/mocks/database.mock.ts b/src/test/mocks/database.mock.ts deleted file mode 100644 index d1cd3e5..0000000 --- a/src/test/mocks/database.mock.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { teamMembers, teams } from "../../database/schema"; - -type Team = typeof teams.$inferSelect; -type TeamMember = typeof teamMembers.$inferSelect; - -export function createMockTeam(overrides?: Partial): Team { - return { - id: "team123", - name: "test-team", - addedBy: "admin123", - addedAt: new Date("2023-01-01"), - ...overrides, - }; -} - -export function createMockTeamMember(overrides?: Partial): TeamMember { - return { - userId: "user123", - teamId: "team123", - addedBy: "admin123", - addedAt: new Date("2023-01-01"), - ...overrides, - }; -} - -export function createMockPoll(overrides?: any) { - return { - id: 1, - messageId: "msg123", - channelId: "channel123", - creatorId: "user123", - question: "Test poll question?", - options: JSON.stringify([ - { text: "Option A", votes: [] }, - { text: "Option B", votes: [] }, - ]), - endTime: null, - createdAt: new Date("2023-01-01"), - ...overrides, - }; -} - -export function createMockPollVote(overrides?: any) { - return { - pollId: 1, - userId: "voter123", - optionIndex: 0, - votedAt: new Date("2023-01-01"), - ...overrides, - }; -}