test: add test infrastructure for frontend and server
- Frontend: vitest.config.ts with vue plugin + jsdom, dummy component test - Server: in-memory SQLite test DB factory + Hono testClient helper + smoke test - CI: add pnpm audit and server coverage threshold steps - Root: vitest workspace config for multi-project test discovery Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
6f0eb92ebb
commit
d9e32123fe
@@ -33,3 +33,9 @@ jobs:
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
|
||||
- name: Security audit
|
||||
run: pnpm audit --audit-level=high
|
||||
|
||||
- name: Server test coverage
|
||||
run: pnpm test -- --run --project server --coverage --coverage.provider=v8 --coverage.reporter=text --coverage.thresholds.lines=30
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@testing-library/vue": "^8.1.0",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"jsdom": "^28.1.0",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^7.3.1",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import DummyComponent from './DummyComponent.vue'
|
||||
|
||||
describe('DummyComponent', () => {
|
||||
it('renders the message prop as text content', () => {
|
||||
const wrapper = mount(DummyComponent, {
|
||||
props: { message: 'BOTFIGHTS' },
|
||||
})
|
||||
expect(wrapper.text()).toBe('BOTFIGHTS')
|
||||
})
|
||||
|
||||
it('renders different message', () => {
|
||||
const wrapper = mount(DummyComponent, {
|
||||
props: { message: 'Stack sats, fight bots' },
|
||||
})
|
||||
expect(wrapper.text()).toBe('Stack sats, fight bots')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{ message: string }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dummy">{{ message }}</div>
|
||||
</template>
|
||||
@@ -0,0 +1,2 @@
|
||||
// Frontend test setup for @vue/test-utils + jsdom
|
||||
// Add global test utilities and mocks here as needed
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src'),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
include: ['src/**/*.test.ts'],
|
||||
setupFiles: ['src/test-setup.ts'],
|
||||
},
|
||||
})
|
||||
Generated
+676
-2
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { createTestDb, insertTestBot } from './db.js'
|
||||
|
||||
describe('test helpers', () => {
|
||||
it('creates in-memory DB, inserts a bot, and queries it back', () => {
|
||||
const { db, schema } = createTestDb()
|
||||
|
||||
const bot = insertTestBot(db, { name: 'SatoshiBot' })
|
||||
|
||||
const found = db
|
||||
.select()
|
||||
.from(schema.bots)
|
||||
.where(eq(schema.bots.id, bot.id))
|
||||
.get()
|
||||
|
||||
expect(found).toBeDefined()
|
||||
expect(found!.name).toBe('SatoshiBot')
|
||||
expect(found!.eloRating).toBe(1200)
|
||||
expect(found!.webhookUrl).toBe('http://test.local/webhook')
|
||||
})
|
||||
|
||||
it('each createTestDb is isolated', () => {
|
||||
const { db: db1, schema: s1 } = createTestDb()
|
||||
const { db: db2, schema: s2 } = createTestDb()
|
||||
|
||||
insertTestBot(db1, { name: 'BotInDb1' })
|
||||
|
||||
const inDb1 = db1.select().from(s1.bots).all()
|
||||
const inDb2 = db2.select().from(s2.bots).all()
|
||||
|
||||
expect(inDb1).toHaveLength(1)
|
||||
expect(inDb2).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,189 @@
|
||||
import Database from 'better-sqlite3'
|
||||
import { drizzle, type BetterSQLite3Database } from 'drizzle-orm/better-sqlite3'
|
||||
import * as schema from '../db/schema.js'
|
||||
|
||||
/**
|
||||
* Creates an isolated in-memory SQLite database with full schema.
|
||||
* Each call returns a fresh DB — no test pollution.
|
||||
*/
|
||||
export function createTestDb() {
|
||||
const sqlite = new Database(':memory:')
|
||||
sqlite.pragma('journal_mode = WAL')
|
||||
sqlite.pragma('foreign_keys = ON')
|
||||
|
||||
// Create all tables (mirrors startup.ts)
|
||||
sqlite.exec(`
|
||||
CREATE TABLE IF NOT EXISTS bots (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
webhook_url TEXT NOT NULL,
|
||||
avatar_seed TEXT NOT NULL,
|
||||
archetype TEXT NOT NULL DEFAULT 'standard',
|
||||
secret_hash TEXT NOT NULL,
|
||||
public_key TEXT,
|
||||
profile_pic_url TEXT,
|
||||
elo_rating REAL NOT NULL DEFAULT 1200,
|
||||
wins INTEGER NOT NULL DEFAULT 0,
|
||||
losses INTEGER NOT NULL DEFAULT 0,
|
||||
win_streak INTEGER NOT NULL DEFAULT 0,
|
||||
best_streak INTEGER NOT NULL DEFAULT 0,
|
||||
tier INTEGER NOT NULL DEFAULT 0,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
last_fight_at TEXT,
|
||||
consecutive_errors INTEGER NOT NULL DEFAULT 0,
|
||||
last_error_at TEXT,
|
||||
customization TEXT,
|
||||
sats_won INTEGER NOT NULL DEFAULT 0,
|
||||
sats_wagered INTEGER NOT NULL DEFAULT 0,
|
||||
has_wallet INTEGER NOT NULL DEFAULT 0,
|
||||
zaps_received INTEGER NOT NULL DEFAULT 0,
|
||||
bot_type TEXT NOT NULL DEFAULT 'regular',
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fights (
|
||||
id TEXT PRIMARY KEY,
|
||||
bot_a_id TEXT NOT NULL REFERENCES bots(id),
|
||||
bot_b_id TEXT NOT NULL REFERENCES bots(id),
|
||||
arena TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'scheduled',
|
||||
winner_id TEXT REFERENCES bots(id),
|
||||
bot_a_hp INTEGER NOT NULL DEFAULT 200,
|
||||
bot_b_hp INTEGER NOT NULL DEFAULT 200,
|
||||
total_rounds INTEGER NOT NULL DEFAULT 0,
|
||||
scheduled_at TEXT,
|
||||
started_at TEXT,
|
||||
ended_at TEXT,
|
||||
mode TEXT NOT NULL DEFAULT 'free',
|
||||
pot_sats INTEGER NOT NULL DEFAULT 0,
|
||||
payout_status TEXT,
|
||||
current_season TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS rounds (
|
||||
id TEXT PRIMARY KEY,
|
||||
fight_id TEXT NOT NULL REFERENCES fights(id),
|
||||
round_number INTEGER NOT NULL,
|
||||
challenge_type TEXT NOT NULL,
|
||||
challenge_data TEXT NOT NULL,
|
||||
bot_a_response TEXT,
|
||||
bot_a_time_ms INTEGER,
|
||||
bot_a_score REAL,
|
||||
bot_b_response TEXT,
|
||||
bot_b_time_ms INTEGER,
|
||||
bot_b_score REAL,
|
||||
winner_id TEXT REFERENCES bots(id),
|
||||
narration TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payments (
|
||||
id TEXT PRIMARY KEY,
|
||||
fight_id TEXT REFERENCES fights(id),
|
||||
bot_id TEXT NOT NULL REFERENCES bots(id),
|
||||
direction TEXT NOT NULL,
|
||||
amount_sats INTEGER NOT NULL,
|
||||
method TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
invoice TEXT,
|
||||
preimage TEXT,
|
||||
cashu_token TEXT,
|
||||
error_reason TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
confirmed_at TEXT,
|
||||
refunded_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS wallet_connections (
|
||||
id TEXT PRIMARY KEY,
|
||||
bot_id TEXT NOT NULL UNIQUE REFERENCES bots(id),
|
||||
method TEXT NOT NULL,
|
||||
connection_data TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
last_used_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bets (
|
||||
id TEXT PRIMARY KEY,
|
||||
fight_id TEXT NOT NULL REFERENCES fights(id),
|
||||
bettor_pubkey TEXT NOT NULL,
|
||||
bot_id TEXT NOT NULL REFERENCES bots(id),
|
||||
amount_sats INTEGER NOT NULL,
|
||||
odds_at_placement REAL NOT NULL,
|
||||
cashu_token TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
payout_sats INTEGER,
|
||||
payout_token TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
settled_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tournaments (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
format TEXT NOT NULL DEFAULT 'single_elim',
|
||||
size INTEGER NOT NULL,
|
||||
entry_sats INTEGER NOT NULL DEFAULT 0,
|
||||
prize_sats INTEGER NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'open',
|
||||
current_round INTEGER NOT NULL DEFAULT 0,
|
||||
season_id TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
started_at TEXT,
|
||||
finished_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tournament_entries (
|
||||
id TEXT PRIMARY KEY,
|
||||
tournament_id TEXT NOT NULL REFERENCES tournaments(id),
|
||||
bot_id TEXT NOT NULL REFERENCES bots(id),
|
||||
seed INTEGER NOT NULL DEFAULT 0,
|
||||
eliminated INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tournament_matches (
|
||||
id TEXT PRIMARY KEY,
|
||||
tournament_id TEXT NOT NULL REFERENCES tournaments(id),
|
||||
round INTEGER NOT NULL,
|
||||
match_index INTEGER NOT NULL,
|
||||
bot_a_id TEXT REFERENCES bots(id),
|
||||
bot_b_id TEXT REFERENCES bots(id),
|
||||
fight_id TEXT REFERENCES fights(id),
|
||||
winner_id TEXT REFERENCES bots(id),
|
||||
status TEXT NOT NULL DEFAULT 'pending'
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS analytics (
|
||||
date TEXT NOT NULL,
|
||||
metric TEXT NOT NULL,
|
||||
value INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_analytics_date_metric ON analytics(date, metric);
|
||||
`)
|
||||
|
||||
const db = drizzle(sqlite, { schema })
|
||||
|
||||
return { db, sqlite, schema }
|
||||
}
|
||||
|
||||
/** Insert a minimal test bot and return it */
|
||||
export function insertTestBot(
|
||||
db: BetterSQLite3Database<typeof schema>,
|
||||
overrides: Partial<typeof schema.bots.$inferInsert> = {},
|
||||
) {
|
||||
const id = overrides.id || `bot-${Math.random().toString(36).slice(2, 8)}`
|
||||
const bot = {
|
||||
id,
|
||||
name: overrides.name || `TestBot-${id.slice(4)}`,
|
||||
webhookUrl: overrides.webhookUrl || 'http://test.local/webhook',
|
||||
avatarSeed: overrides.avatarSeed || 'test-seed',
|
||||
archetype: overrides.archetype || 'standard',
|
||||
secretHash: overrides.secretHash || 'testhash',
|
||||
createdAt: overrides.createdAt || new Date().toISOString(),
|
||||
...overrides,
|
||||
}
|
||||
db.insert(schema.bots).values(bot).run()
|
||||
return bot
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Hono } from 'hono'
|
||||
|
||||
/**
|
||||
* Test helper for Hono route testing without HTTP server.
|
||||
* Uses Hono's built-in `app.request()` for direct request simulation.
|
||||
*/
|
||||
export function testClient(app: Hono) {
|
||||
return {
|
||||
get: (path: string, init?: RequestInit) =>
|
||||
app.request(path, { method: 'GET', ...init }),
|
||||
|
||||
post: (path: string, body?: unknown, init?: RequestInit) =>
|
||||
app.request(path, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...init?.headers },
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
...init,
|
||||
}),
|
||||
|
||||
put: (path: string, body?: unknown, init?: RequestInit) =>
|
||||
app.request(path, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', ...init?.headers },
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
...init,
|
||||
}),
|
||||
|
||||
delete: (path: string, init?: RequestInit) =>
|
||||
app.request(path, { method: 'DELETE', ...init }),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineWorkspace } from 'vitest/config'
|
||||
|
||||
export default defineWorkspace([
|
||||
{
|
||||
test: {
|
||||
name: 'server',
|
||||
include: ['server/src/**/*.test.ts'],
|
||||
},
|
||||
},
|
||||
'frontend/vitest.config.ts',
|
||||
])
|
||||
Reference in New Issue
Block a user