make tests passing in cicd

This commit is contained in:
2026-03-13 18:09:10 +01:00
parent f3edcb6995
commit 12dc31ee72
4 changed files with 118 additions and 59 deletions

View File

@@ -29,5 +29,8 @@ jobs:
- name: Build - name: Build
run: bun run build run: bun run build
- name: Install Playwright browsers
run: bunx playwright install --with-deps chromium
- name: Tests - name: Tests
run: bun test run: bun run test

37
e2e/smoke.test.ts Normal file
View File

@@ -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);
}
});
});

View File

@@ -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 { MATCH_STATES } from './types';
import { pickMap } from './engine';
const mocks = { const mocks = {
db: { db: {
query: { query: {
match: { findFirst: mock() }, match: { findFirst: vi.fn() },
matchParticipant: { findMany: mock() }, matchParticipant: { findMany: vi.fn() },
matchGame: { findMany: mock() }, matchGame: { findMany: vi.fn() },
mappoolSlot: { findFirst: mock() } mappoolSlot: { findFirst: vi.fn() }
}, },
insert: mock(), insert: vi.fn(),
update: mock(), update: vi.fn(),
delete: mock(), delete: vi.fn(),
select: mock() 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', () => ({ vi.mock('drizzle-orm', () => ({
eq: mock(() => ({})), eq: vi.fn(() => ({})),
and: mock(() => ({})), and: vi.fn(() => ({})),
asc: mock(() => ({})), asc: vi.fn(() => ({})),
lt: mock(() => ({})), lt: vi.fn(() => ({})),
inArray: mock(() => ({})), inArray: vi.fn(() => ({})),
sql: mock(() => ({})), sql: vi.fn(() => ({})),
isNotNull: mock(() => ({})) isNotNull: vi.fn(() => ({}))
})); }));
mock.module('$lib/server/db/schema', () => ({ vi.mock('$lib/server/db/schema', () => ({
match: {}, match: {},
matchParticipant: {}, matchParticipant: {},
matchParticipantPlayer: {}, matchParticipantPlayer: {},
@@ -41,40 +46,54 @@ mock.module('$lib/server/db/schema', () => ({
teamMember: {} teamMember: {}
})); }));
mock.module('$lib/server/db/auth.schema', () => ({ account: {} })); vi.mock('$lib/server/db/auth.schema', () => ({ account: {} }));
mock.module('$lib/server/discord/client', () => ({ vi.mock('$lib/server/discord/client', () => ({
notifyMatchCreated: mock(), notifyMatchCreated: vi.fn(),
notifyMatchFinished: mock() notifyMatchFinished: vi.fn()
})); }));
mock.module('$lib/server/osu/api', () => ({ vi.mock('$lib/server/osu/api', () => ({
getUser: mock() 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', () => { describe('pickMap', () => {
beforeEach(() => { beforeEach(() => {
mocks.db.query.match.findFirst.mockReset(); vi.clearAllMocks();
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({ // getMatchOrThrow returns a match object
mocks.helpers.getMatchOrThrow.mockResolvedValue({
id: 'match-1', id: 'match-1',
state: MATCH_STATES.PICKING, state: MATCH_STATES.PICKING,
config: { bestOf: 5, teamSize: 1, scoringType: 'score_v2' } config: { bestOf: 5, teamSize: 1, scoringType: 'score_v2' }
}); });
// db.query.matchParticipant.findMany returns participants
mocks.db.query.matchParticipant.findMany.mockResolvedValue([ mocks.db.query.matchParticipant.findMany.mockResolvedValue([
{ id: 'p1', pickOrder: 1, score: 1 }, { id: 'p1', pickOrder: 1, score: 1 },
{ id: 'p2', pickOrder: 2, score: 1 } { id: 'p2', pickOrder: 2, score: 1 }
]); ]);
// db.query.matchGame.findMany returns no games played yet
mocks.db.query.matchGame.findMany.mockResolvedValue([]); 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 () => { test('throws when a non-current picker tries to pick', async () => {

View File

@@ -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'; import { MATCH_STATES } from './types';
const mocks = { const mocks = {
db: { db: {
query: { query: {
matchParticipantPlayer: { findMany: mock() }, matchParticipantPlayer: { findMany: vi.fn() },
playerRating: { findFirst: mock() }, playerRating: { findFirst: vi.fn() },
matchQueue: { findFirst: mock() } 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', () => ({ vi.mock('drizzle-orm', () => ({
eq: mock(() => ({})), eq: vi.fn(() => ({})),
and: mock(() => ({})), and: vi.fn(() => ({})),
asc: mock(() => ({})), asc: vi.fn(() => ({})),
lt: mock(() => ({})), lt: vi.fn(() => ({})),
inArray: mock(() => ({})), inArray: vi.fn(() => ({})),
sql: mock(() => ({})), sql: vi.fn(() => ({})),
isNotNull: mock(() => ({})) isNotNull: vi.fn(() => ({}))
})); }));
mock.module('$lib/server/db/schema', () => ({ vi.mock('$lib/server/db/schema', () => ({
match: {}, match: {},
matchQueue: {}, matchQueue: {},
matchParticipantPlayer: {}, matchParticipantPlayer: {},
playerRating: {} playerRating: {}
})); }));
mock.module('$lib/server/db/auth.schema', () => ({ account: {} })); vi.mock('$lib/server/db/auth.schema', () => ({ account: {} }));
mock.module('$lib/server/osu/api', () => ({ vi.mock('$lib/server/osu/api', () => ({
getUser: mock() getUser: vi.fn()
})); }));
mock.module('$lib/server/match/engine', () => ({ vi.mock('$lib/server/match/engine', () => ({
createMatch: mock() createMatch: vi.fn()
})); }));
const { joinQueue } = await import('./queue'); const { joinQueue } = await import('./queue');
@@ -53,7 +53,7 @@ describe('joinQueue', () => {
test('throws when user is already in an active match', async () => { test('throws when user is already in an active match', async () => {
mocks.db.delete.mockReturnValue({ mocks.db.delete.mockReturnValue({
where: mock(() => ({ returning: mock(() => Promise.resolve([])) })) where: vi.fn(() => ({ returning: vi.fn(() => Promise.resolve([])) }))
}); });
mocks.db.query.matchParticipantPlayer.findMany.mockResolvedValue([ mocks.db.query.matchParticipantPlayer.findMany.mockResolvedValue([
@@ -72,7 +72,7 @@ describe('joinQueue', () => {
test('throws when user has no rating', async () => { test('throws when user has no rating', async () => {
mocks.db.delete.mockReturnValue({ mocks.db.delete.mockReturnValue({
where: mock(() => ({ returning: mock(() => Promise.resolve([])) })) where: vi.fn(() => ({ returning: vi.fn(() => Promise.resolve([])) }))
}); });
mocks.db.query.matchParticipantPlayer.findMany.mockResolvedValue([]); mocks.db.query.matchParticipantPlayer.findMany.mockResolvedValue([]);
@@ -85,7 +85,7 @@ describe('joinQueue', () => {
test('throws when user is already in queue', async () => { test('throws when user is already in queue', async () => {
mocks.db.delete.mockReturnValue({ mocks.db.delete.mockReturnValue({
where: mock(() => ({ returning: mock(() => Promise.resolve([])) })) where: vi.fn(() => ({ returning: vi.fn(() => Promise.resolve([])) }))
}); });
mocks.db.query.matchParticipantPlayer.findMany.mockResolvedValue([]); mocks.db.query.matchParticipantPlayer.findMany.mockResolvedValue([]);