stuff
This commit is contained in:
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user