This commit is contained in:
Dorian
2026-04-11 19:46:37 +01:00
parent 52752a92bf
commit 32e6c19f72
44 changed files with 5511 additions and 39 deletions
+30
View File
@@ -0,0 +1,30 @@
import { test, expect } from '@playwright/test'
test.describe('admin page access control', () => {
test('admin page redirects or shows forbidden without auth', async ({ page }) => {
const criticalErrors: string[] = []
page.on('pageerror', err => {
if (err.message.includes('ReferenceError') || err.message.includes('SyntaxError')) {
criticalErrors.push(err.message)
}
})
await page.goto('/admin')
await page.waitForTimeout(2000)
// Should not crash
expect(criticalErrors).toHaveLength(0)
// Should either show forbidden/unauthorized message or redirect away
const url = page.url()
const content = await page.textContent('body')
// Valid outcomes: redirected to login/home, or shows forbidden
const isRedirected = !url.includes('/admin')
const showsForbidden = content?.match(/forbidden|unauthorized|not authorized|403|login/i) !== null
const isEmptyAdmin = content?.trim().length === 0 || content?.includes('Loading')
// At least one of these should be true
expect(isRedirected || showsForbidden || isEmptyAdmin).toBe(true)
})
})
+96
View File
@@ -0,0 +1,96 @@
import { test, expect } from '@playwright/test'
const API_BASE = 'http://localhost:9100'
test.describe('API health and public endpoints', () => {
test('health endpoint returns 200', async ({ request }) => {
const res = await request.get(`${API_BASE}/api/health`)
expect(res.status()).toBe(200)
})
test('fights list returns valid JSON', async ({ request }) => {
const res = await request.get(`${API_BASE}/api/fights`)
expect(res.status()).toBe(200)
const data = await res.json()
expect(Array.isArray(data.fights)).toBe(true)
})
test('leaderboard returns valid JSON', async ({ request }) => {
const res = await request.get(`${API_BASE}/api/bots/leaderboard`)
expect(res.status()).toBe(200)
const data = await res.json()
expect(data).toHaveProperty('leaderboard')
})
test('public stats returns valid JSON', async ({ request }) => {
const res = await request.get(`${API_BASE}/api/stats/public`)
expect(res.status()).toBe(200)
const data = await res.json()
expect(data).toBeDefined()
})
test('tournaments list returns valid JSON', async ({ request }) => {
const res = await request.get(`${API_BASE}/api/tournaments`)
expect(res.status()).toBe(200)
const data = await res.json()
expect(data).toHaveProperty('tournaments')
})
test('check-name endpoint works', async ({ request }) => {
const res = await request.get(`${API_BASE}/api/auth/check-name/TestBotName123`)
expect(res.status()).toBe(200)
const data = await res.json()
expect(typeof data.available).toBe('boolean')
})
})
test.describe('API auth protection', () => {
test('admin stats requires auth', async ({ request }) => {
const res = await request.get(`${API_BASE}/api/admin/stats`)
expect(res.status()).toBe(403)
})
test('payment confirm without auth returns 400/404', async ({ request }) => {
const res = await request.post(`${API_BASE}/api/payments/confirm/nonexistent`, {
data: {},
})
// Should be 400 or 404, not 500
expect([400, 404]).toContain(res.status())
})
test('fight respond without valid fight returns 404', async ({ request }) => {
const res = await request.post(`${API_BASE}/api/fights/nonexistent/respond`, {
data: { botId: 'fake', answer: 'test' },
})
expect([400, 404]).toContain(res.status())
})
test('queue join with nonexistent bot returns 404', async ({ request }) => {
const res = await request.post(`${API_BASE}/api/queue/join/nonexistent-bot-id`)
expect([400, 404]).toContain(res.status())
})
})
test.describe('API rate limiting', () => {
test('payment create-invoice is rate limited', async ({ request }) => {
const responses: number[] = []
// Send 15 requests quickly (limit is 10/min)
for (let i = 0; i < 15; i++) {
const res = await request.post(`${API_BASE}/api/payments/create-invoice`, {
data: { botId: `test-${i}` },
})
responses.push(res.status())
}
// At least some should be 429 (rate limited)
expect(responses.some(s => s === 429)).toBe(true)
})
})
test.describe('API security headers', () => {
test('responses include security headers', async ({ request }) => {
const res = await request.get(`${API_BASE}/api/health`)
const headers = res.headers()
expect(headers['x-content-type-options']).toBe('nosniff')
expect(headers['x-frame-options']).toBe('DENY')
})
})
+93
View File
@@ -0,0 +1,93 @@
import { test, expect } from '@playwright/test'
test.describe('page navigation — all routes load without crashes', () => {
const routes = [
{ path: '/', name: 'homepage' },
{ path: '/arena', name: 'arena' },
{ path: '/fight-card', name: 'fight card' },
{ path: '/leaderboard', name: 'leaderboard' },
{ path: '/training', name: 'practice/training' },
{ path: '/feed', name: 'feed' },
{ path: '/sprites', name: 'sprite preview' },
{ path: '/docs', name: 'docs' },
{ path: '/tournaments', name: 'tournaments' },
{ path: '/join', name: 'join bout' },
{ path: '/register', name: 'register' },
{ path: '/schedule', name: 'schedule' },
]
for (const route of routes) {
test(`${route.name} (${route.path}) loads without JS crashes`, async ({ page }) => {
const criticalErrors: string[] = []
page.on('pageerror', err => {
const msg = err.message
if (msg.includes('TypeError') || msg.includes('ReferenceError') || msg.includes('SyntaxError')) {
criticalErrors.push(msg)
}
})
await page.goto(route.path)
await page.waitForTimeout(1500)
expect(criticalErrors).toHaveLength(0)
})
}
})
test.describe('navigation flow', () => {
test('can navigate from homepage to leaderboard via nav', async ({ page }) => {
await page.goto('/')
await page.waitForTimeout(500)
// Click leaderboard link in nav or body
const leaderboardLink = page.getByRole('link', { name: /leaderboard|rankings/i }).first()
if (await leaderboardLink.isVisible()) {
await leaderboardLink.click()
await expect(page).toHaveURL(/leaderboard/)
}
})
test('can navigate from homepage to arena', async ({ page }) => {
await page.goto('/')
await page.waitForTimeout(500)
const arenaLink = page.getByRole('link', { name: /arena|watch|fights/i }).first()
if (await arenaLink.isVisible()) {
await arenaLink.click()
await expect(page).toHaveURL(/arena/)
}
})
test('/practice redirects to /training', async ({ page }) => {
await page.goto('/practice')
await expect(page).toHaveURL(/training/)
})
})
test.describe('error handling', () => {
test('bot profile with unknown name shows error state', async ({ page }) => {
const criticalErrors: string[] = []
page.on('pageerror', err => {
if (err.message.includes('ReferenceError') || err.message.includes('SyntaxError')) {
criticalErrors.push(err.message)
}
})
await page.goto('/bot/nonexistent-bot-name-12345')
await page.waitForTimeout(2000)
expect(criticalErrors).toHaveLength(0)
})
test('tournament with unknown ID shows error state', async ({ page }) => {
const criticalErrors: string[] = []
page.on('pageerror', err => {
if (err.message.includes('ReferenceError') || err.message.includes('SyntaxError')) {
criticalErrors.push(err.message)
}
})
await page.goto('/tournament/nonexistent-id')
await page.waitForTimeout(2000)
expect(criticalErrors).toHaveLength(0)
})
})