From 12dc31ee7211c23fca54708bcb8625784776e391 Mon Sep 17 00:00:00 2001 From: Stan Runge Date: Fri, 13 Mar 2026 18:09:10 +0100 Subject: [PATCH] make tests passing in cicd --- .github/workflows/ci.yml | 5 +- e2e/smoke.test.ts | 37 ++++++++++++ src/lib/server/match/engine.test.ts | 89 +++++++++++++++++------------ src/lib/server/match/queue.test.ts | 46 +++++++-------- 4 files changed, 118 insertions(+), 59 deletions(-) create mode 100644 e2e/smoke.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed614f9..ee5e045 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,5 +29,8 @@ jobs: - name: Build run: bun run build + - name: Install Playwright browsers + run: bunx playwright install --with-deps chromium + - name: Tests - run: bun test + run: bun run test diff --git a/e2e/smoke.test.ts b/e2e/smoke.test.ts new file mode 100644 index 0000000..3f70ce2 --- /dev/null +++ b/e2e/smoke.test.ts @@ -0,0 +1,37 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Smoke Tests', () => { + test('landing page loads with sign-in link', async ({ page }) => { + await page.goto('/'); + await expect(page.getByRole('heading', { name: /Vash Esports/i })).toBeVisible(); + await expect( + page.getByRole('banner').getByRole('link', { name: 'Sign in with osu!' }) + ).toBeVisible(); + }); + + test('protected API returns 401 without auth', async ({ request }) => { + const response = await request.get('/api/matches'); + expect(response.status()).toBe(401); + }); + + test('health endpoint responds', async ({ request }) => { + const response = await request.get('/api/health'); + // 200 if DB is up, 503 if not — both are valid from the endpoint + expect([200, 503]).toContain(response.status()); + const body = await response.json(); + expect(body).toHaveProperty('status'); + expect(body).toHaveProperty('timestamp'); + }); + + test('leaderboard page renders', async ({ page }) => { + await page.goto('/leaderboard'); + await expect(page.locator('h1')).toContainText('Leaderboard'); + }); + + test('privacy and terms pages render', async ({ page }) => { + for (const path of ['/privacy', '/terms']) { + const response = await page.goto(path); + expect(response?.ok()).toBe(true); + } + }); +}); diff --git a/src/lib/server/match/engine.test.ts b/src/lib/server/match/engine.test.ts index 8f40d76..a56034d 100644 --- a/src/lib/server/match/engine.test.ts +++ b/src/lib/server/match/engine.test.ts @@ -1,35 +1,40 @@ -import { beforeEach, describe, expect, test, mock } from 'bun:test'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; import { MATCH_STATES } from './types'; -import { pickMap } from './engine'; const mocks = { db: { query: { - match: { findFirst: mock() }, - matchParticipant: { findMany: mock() }, - matchGame: { findMany: mock() }, - mappoolSlot: { findFirst: mock() } + match: { findFirst: vi.fn() }, + matchParticipant: { findMany: vi.fn() }, + matchGame: { findMany: vi.fn() }, + mappoolSlot: { findFirst: vi.fn() } }, - insert: mock(), - update: mock(), - delete: mock(), - select: mock() + insert: vi.fn(), + update: vi.fn(), + delete: vi.fn(), + select: vi.fn() + }, + helpers: { + getMatchOrThrow: vi.fn(), + getExpectedPicker: vi.fn(), + assertState: vi.fn(), + getMatchFull: vi.fn() } }; -mock.module('$lib/server/db', () => ({ db: mocks.db })); +vi.mock('$lib/server/db', () => ({ db: mocks.db })); -mock.module('drizzle-orm', () => ({ - eq: mock(() => ({})), - and: mock(() => ({})), - asc: mock(() => ({})), - lt: mock(() => ({})), - inArray: mock(() => ({})), - sql: mock(() => ({})), - isNotNull: mock(() => ({})) +vi.mock('drizzle-orm', () => ({ + eq: vi.fn(() => ({})), + and: vi.fn(() => ({})), + asc: vi.fn(() => ({})), + lt: vi.fn(() => ({})), + inArray: vi.fn(() => ({})), + sql: vi.fn(() => ({})), + isNotNull: vi.fn(() => ({})) })); -mock.module('$lib/server/db/schema', () => ({ +vi.mock('$lib/server/db/schema', () => ({ match: {}, matchParticipant: {}, matchParticipantPlayer: {}, @@ -41,40 +46,54 @@ mock.module('$lib/server/db/schema', () => ({ teamMember: {} })); -mock.module('$lib/server/db/auth.schema', () => ({ account: {} })); +vi.mock('$lib/server/db/auth.schema', () => ({ account: {} })); -mock.module('$lib/server/discord/client', () => ({ - notifyMatchCreated: mock(), - notifyMatchFinished: mock() +vi.mock('$lib/server/discord/client', () => ({ + notifyMatchCreated: vi.fn(), + notifyMatchFinished: vi.fn() })); -mock.module('$lib/server/osu/api', () => ({ - getUser: mock() +vi.mock('$lib/server/osu/api', () => ({ + getUser: vi.fn() })); +vi.mock('./helpers', () => ({ + getExpectedPicker: (...args: unknown[]) => mocks.helpers.getExpectedPicker(...args), + getMatchOrThrow: (...args: unknown[]) => mocks.helpers.getMatchOrThrow(...args), + assertState: (...args: unknown[]) => mocks.helpers.assertState(...args), + getMatchFull: (...args: unknown[]) => mocks.helpers.getMatchFull(...args) +})); + +vi.mock('./rating', () => ({ + updateElo: vi.fn(), + calculateInitialElo: vi.fn(), + selectMappoolForRating: vi.fn() +})); + +const { pickMap } = await import('./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(); + vi.clearAllMocks(); - mocks.db.query.match.findFirst.mockResolvedValue({ + // getMatchOrThrow returns a match object + mocks.helpers.getMatchOrThrow.mockResolvedValue({ id: 'match-1', state: MATCH_STATES.PICKING, config: { bestOf: 5, teamSize: 1, scoringType: 'score_v2' } }); + // db.query.matchParticipant.findMany returns participants mocks.db.query.matchParticipant.findMany.mockResolvedValue([ { id: 'p1', pickOrder: 1, score: 1 }, { id: 'p2', pickOrder: 2, score: 1 } ]); + // db.query.matchGame.findMany returns no games played yet mocks.db.query.matchGame.findMany.mockResolvedValue([]); + + // getExpectedPicker returns p1 (first picker) + mocks.helpers.getExpectedPicker.mockReturnValue({ id: 'p1', pickOrder: 1, score: 1 }); }); test('throws when a non-current picker tries to pick', async () => { diff --git a/src/lib/server/match/queue.test.ts b/src/lib/server/match/queue.test.ts index 5041ee4..d17bd87 100644 --- a/src/lib/server/match/queue.test.ts +++ b/src/lib/server/match/queue.test.ts @@ -1,44 +1,44 @@ -import { beforeEach, describe, expect, test, mock } from 'bun:test'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; import { MATCH_STATES } from './types'; const mocks = { db: { query: { - matchParticipantPlayer: { findMany: mock() }, - playerRating: { findFirst: mock() }, - matchQueue: { findFirst: mock() } + matchParticipantPlayer: { findMany: vi.fn() }, + playerRating: { findFirst: vi.fn() }, + matchQueue: { findFirst: vi.fn() } }, - delete: mock() + delete: vi.fn() } }; -mock.module('$lib/server/db', () => ({ db: mocks.db })); +vi.mock('$lib/server/db', () => ({ db: mocks.db })); -mock.module('drizzle-orm', () => ({ - eq: mock(() => ({})), - and: mock(() => ({})), - asc: mock(() => ({})), - lt: mock(() => ({})), - inArray: mock(() => ({})), - sql: mock(() => ({})), - isNotNull: mock(() => ({})) +vi.mock('drizzle-orm', () => ({ + eq: vi.fn(() => ({})), + and: vi.fn(() => ({})), + asc: vi.fn(() => ({})), + lt: vi.fn(() => ({})), + inArray: vi.fn(() => ({})), + sql: vi.fn(() => ({})), + isNotNull: vi.fn(() => ({})) })); -mock.module('$lib/server/db/schema', () => ({ +vi.mock('$lib/server/db/schema', () => ({ match: {}, matchQueue: {}, matchParticipantPlayer: {}, playerRating: {} })); -mock.module('$lib/server/db/auth.schema', () => ({ account: {} })); +vi.mock('$lib/server/db/auth.schema', () => ({ account: {} })); -mock.module('$lib/server/osu/api', () => ({ - getUser: mock() +vi.mock('$lib/server/osu/api', () => ({ + getUser: vi.fn() })); -mock.module('$lib/server/match/engine', () => ({ - createMatch: mock() +vi.mock('$lib/server/match/engine', () => ({ + createMatch: vi.fn() })); const { joinQueue } = await import('./queue'); @@ -53,7 +53,7 @@ describe('joinQueue', () => { test('throws when user is already in an active match', async () => { mocks.db.delete.mockReturnValue({ - where: mock(() => ({ returning: mock(() => Promise.resolve([])) })) + where: vi.fn(() => ({ returning: vi.fn(() => Promise.resolve([])) })) }); mocks.db.query.matchParticipantPlayer.findMany.mockResolvedValue([ @@ -72,7 +72,7 @@ describe('joinQueue', () => { test('throws when user has no rating', async () => { mocks.db.delete.mockReturnValue({ - where: mock(() => ({ returning: mock(() => Promise.resolve([])) })) + where: vi.fn(() => ({ returning: vi.fn(() => Promise.resolve([])) })) }); mocks.db.query.matchParticipantPlayer.findMany.mockResolvedValue([]); @@ -85,7 +85,7 @@ describe('joinQueue', () => { test('throws when user is already in queue', async () => { mocks.db.delete.mockReturnValue({ - where: mock(() => ({ returning: mock(() => Promise.resolve([])) })) + where: vi.fn(() => ({ returning: vi.fn(() => Promise.resolve([])) })) }); mocks.db.query.matchParticipantPlayer.findMany.mockResolvedValue([]);