diff --git a/test/match/engine.test.ts b/test/match/engine.test.ts new file mode 100644 index 0000000..e7589d1 --- /dev/null +++ b/test/match/engine.test.ts @@ -0,0 +1,96 @@ +import { beforeEach, describe, expect, it, mock } from 'bun:test'; +import { MATCH_STATES } from '../../src/lib/server/match/types'; + +const mocks = { + db: { + query: { + match: { findFirst: mock() }, + matchParticipant: { findMany: mock() }, + matchGame: { findMany: mock() }, + mappoolSlot: { findFirst: mock() } + }, + insert: mock(), + update: mock(), + delete: mock(), + select: mock() + } +}; + +mock.module('$lib/server/db', () => ({ db: mocks.db })); + +mock.module('drizzle-orm', () => ({ + eq: mock(() => ({})), + and: mock(() => ({})), + asc: mock(() => ({})), + lt: mock(() => ({})), + inArray: mock(() => ({})), + sql: mock(() => ({})) +})); + +mock.module('$lib/server/db/schema', () => ({ + match: {}, + matchParticipant: {}, + matchParticipantPlayer: {}, + matchGame: {}, + matchGameScore: {}, + matchQueue: {}, + mappoolSlot: {}, + playerRating: {}, + teamMember: {} +})); + +mock.module('$lib/server/db/auth.schema', () => ({ account: {} })); + +mock.module('$lib/server/discord/client', () => ({ + notifyMatchCreated: mock(), + notifyMatchFinished: mock() +})); + +mock.module('$lib/server/osu/api', () => ({ + getUser: mock() +})); + +const { pickMap } = await import('../../src/lib/server/match/engine'); + + describe('pickMap', () => { + beforeEach(() => { + mocks.db.query.match.findFirst.mockReset(); + mocks.db.query.matchParticipant.findMany.mockReset(); + mocks.db.query.matchGame.findMany.mockReset(); + mocks.db.query.mappoolSlot.findFirst.mockReset(); + mocks.db.insert.mockReset(); + mocks.db.update.mockReset(); + mocks.db.delete.mockReset(); + mocks.db.select.mockReset(); + + mocks.db.query.match.findFirst.mockResolvedValue({ + id: 'match-1', + state: MATCH_STATES.PICKING, + config: { bestOf: 5, teamSize: 1, scoringType: 'score_v2' } + }); + + mocks.db.query.matchParticipant.findMany.mockResolvedValue([ + { id: 'p1', pickOrder: 1, score: 1 }, + { id: 'p2', pickOrder: 2, score: 1 } + ]); + + mocks.db.query.matchGame.findMany.mockResolvedValue([]); + }); + + it('throws when a non-current picker tries to pick', async () => { + await expect(pickMap('match-1', 'p2', 'slot-1')).rejects.toThrow('Not your turn to pick'); + expect(mocks.db.query.mappoolSlot.findFirst).not.toHaveBeenCalled(); + }); + + it('rejects tiebreaker picks before match point', async () => { + mocks.db.query.mappoolSlot.findFirst.mockResolvedValue({ + id: 'slot-tb', + category: 'TB' + }); + + await expect(pickMap('match-1', 'p1', 'slot-tb')).rejects.toThrow( + 'Tiebreaker can only be picked at match point' + ); + expect(mocks.db.insert).not.toHaveBeenCalled(); + }); +});