feat(01-05): add built-in templates, seed data, wizard tests, and template tests
- Built-in blind structures: Turbo (15min), Standard (20min), Deep Stack (30min), WSOP-style (60min, BB ante) - Built-in payout structure: Standard with 4 entry-count brackets (8-20, 21-30, 31-40, 41+) - Built-in buy-in configs: Basic 200 DKK through WSOP 1000 DKK with rake splits - 4 built-in tournament templates composing above building blocks - 005_builtin_templates.sql seed migration (INSERT OR IGNORE, safe to re-run) - Wizard tests: standard, various player counts, denomination alignment, edge cases - Template tests: create/expand/duplicate/save-as/delete-builtin/list Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
dd2f9bbfd9
commit
7dbb4cab1a
3 changed files with 786 additions and 0 deletions
315
internal/blind/wizard_test.go
Normal file
315
internal/blind/wizard_test.go
Normal file
|
|
@ -0,0 +1,315 @@
|
||||||
|
package blind
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
_ "github.com/tursodatabase/go-libsql"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setupTestDB creates a temporary test database with the required schema.
|
||||||
|
func setupTestDB(t *testing.T) *sql.DB {
|
||||||
|
t.Helper()
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dbPath := filepath.Join(tmpDir, "test.db")
|
||||||
|
|
||||||
|
db, err := sql.Open("libsql", "file:"+dbPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open test db: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { db.Close() })
|
||||||
|
|
||||||
|
// Create required tables
|
||||||
|
stmts := []string{
|
||||||
|
`CREATE TABLE chip_sets (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, is_builtin INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL DEFAULT 0)`,
|
||||||
|
`CREATE TABLE chip_denominations (id INTEGER PRIMARY KEY AUTOINCREMENT, chip_set_id INTEGER NOT NULL, value INTEGER NOT NULL, color_hex TEXT NOT NULL DEFAULT '#FFF', label TEXT NOT NULL DEFAULT '', sort_order INTEGER NOT NULL DEFAULT 0)`,
|
||||||
|
`CREATE TABLE blind_structures (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, is_builtin INTEGER NOT NULL DEFAULT 0, game_type_default TEXT NOT NULL DEFAULT 'nlhe', notes TEXT NOT NULL DEFAULT '', created_at INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL DEFAULT 0)`,
|
||||||
|
`CREATE TABLE blind_levels (id INTEGER PRIMARY KEY AUTOINCREMENT, structure_id INTEGER NOT NULL, position INTEGER NOT NULL, level_type TEXT NOT NULL DEFAULT 'round', game_type TEXT NOT NULL DEFAULT 'nlhe', small_blind INTEGER NOT NULL DEFAULT 0, big_blind INTEGER NOT NULL DEFAULT 0, ante INTEGER NOT NULL DEFAULT 0, bb_ante INTEGER NOT NULL DEFAULT 0, duration_seconds INTEGER NOT NULL DEFAULT 900, chip_up_denomination_value INTEGER, notes TEXT NOT NULL DEFAULT '')`,
|
||||||
|
`CREATE TABLE tournaments (id TEXT PRIMARY KEY, name TEXT, template_id INTEGER, chip_set_id INTEGER, blind_structure_id INTEGER, payout_structure_id INTEGER, buyin_config_id INTEGER, points_formula_id INTEGER, status TEXT DEFAULT 'created', min_players INTEGER DEFAULT 2, max_players INTEGER, early_signup_bonus_chips INTEGER DEFAULT 0, early_signup_cutoff TEXT, punctuality_bonus_chips INTEGER DEFAULT 0, is_pko INTEGER DEFAULT 0, current_level INTEGER DEFAULT 0, clock_state TEXT DEFAULT 'stopped', clock_remaining_ns INTEGER DEFAULT 0, total_elapsed_ns INTEGER DEFAULT 0, hand_for_hand INTEGER DEFAULT 0, started_at INTEGER, ended_at INTEGER, created_at INTEGER DEFAULT 0, updated_at INTEGER DEFAULT 0)`,
|
||||||
|
}
|
||||||
|
for _, stmt := range stmts {
|
||||||
|
if _, err := db.Exec(stmt); err != nil {
|
||||||
|
t.Fatalf("create table: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert standard chip set
|
||||||
|
if _, err := db.Exec(`INSERT INTO chip_sets (id, name) VALUES (1, 'Standard')`); err != nil {
|
||||||
|
t.Fatalf("insert chip set: %v", err)
|
||||||
|
}
|
||||||
|
denominations := []struct {
|
||||||
|
value int64
|
||||||
|
colorHex string
|
||||||
|
}{
|
||||||
|
{25, "#FFFFFF"},
|
||||||
|
{100, "#FF0000"},
|
||||||
|
{500, "#00AA00"},
|
||||||
|
{1000, "#000000"},
|
||||||
|
{5000, "#0000FF"},
|
||||||
|
}
|
||||||
|
for _, d := range denominations {
|
||||||
|
if _, err := db.Exec(`INSERT INTO chip_denominations (chip_set_id, value, color_hex) VALUES (1, ?, ?)`, d.value, d.colorHex); err != nil {
|
||||||
|
t.Fatalf("insert denomination: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWizardGenerate_StandardTournament(t *testing.T) {
|
||||||
|
if os.Getenv("CGO_ENABLED") == "0" {
|
||||||
|
t.Skip("requires CGO for libsql")
|
||||||
|
}
|
||||||
|
|
||||||
|
db := setupTestDB(t)
|
||||||
|
ws := NewWizardService(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
levels, err := ws.Generate(ctx, WizardInput{
|
||||||
|
PlayerCount: 20,
|
||||||
|
StartingChips: 15000,
|
||||||
|
TargetDurationMinutes: 240,
|
||||||
|
ChipSetID: 1,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("generate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(levels) == 0 {
|
||||||
|
t.Fatal("expected at least 1 level")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check all levels have valid types
|
||||||
|
hasRound := false
|
||||||
|
hasBreak := false
|
||||||
|
for _, l := range levels {
|
||||||
|
if l.LevelType == "round" {
|
||||||
|
hasRound = true
|
||||||
|
}
|
||||||
|
if l.LevelType == "break" {
|
||||||
|
hasBreak = true
|
||||||
|
}
|
||||||
|
if l.LevelType != "round" && l.LevelType != "break" {
|
||||||
|
t.Errorf("invalid level type: %s", l.LevelType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasRound {
|
||||||
|
t.Error("expected at least one round level")
|
||||||
|
}
|
||||||
|
if !hasBreak {
|
||||||
|
t.Error("expected at least one break")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check positions are contiguous
|
||||||
|
for i, l := range levels {
|
||||||
|
if l.Position != i {
|
||||||
|
t.Errorf("level %d: expected position %d, got %d", i, i, l.Position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check blinds are increasing for round levels
|
||||||
|
prevBB := int64(0)
|
||||||
|
for _, l := range levels {
|
||||||
|
if l.LevelType == "round" {
|
||||||
|
if l.BigBlind <= 0 {
|
||||||
|
t.Errorf("level %d: big blind must be positive, got %d", l.Position, l.BigBlind)
|
||||||
|
}
|
||||||
|
if l.BigBlind < prevBB {
|
||||||
|
t.Errorf("level %d: big blind %d not increasing from previous %d", l.Position, l.BigBlind, prevBB)
|
||||||
|
}
|
||||||
|
if l.SmallBlind >= l.BigBlind {
|
||||||
|
t.Errorf("level %d: small blind %d >= big blind %d", l.Position, l.SmallBlind, l.BigBlind)
|
||||||
|
}
|
||||||
|
prevBB = l.BigBlind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWizardGenerate_VariousPlayerCounts(t *testing.T) {
|
||||||
|
if os.Getenv("CGO_ENABLED") == "0" {
|
||||||
|
t.Skip("requires CGO for libsql")
|
||||||
|
}
|
||||||
|
|
||||||
|
db := setupTestDB(t)
|
||||||
|
ws := NewWizardService(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
for _, pc := range []int{10, 20, 40, 80} {
|
||||||
|
t.Run(stringFromInt(pc)+"players", func(t *testing.T) {
|
||||||
|
levels, err := ws.Generate(ctx, WizardInput{
|
||||||
|
PlayerCount: pc,
|
||||||
|
StartingChips: 15000,
|
||||||
|
TargetDurationMinutes: 240,
|
||||||
|
ChipSetID: 1,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("generate for %d players: %v", pc, err)
|
||||||
|
}
|
||||||
|
if len(levels) < 5 {
|
||||||
|
t.Errorf("expected at least 5 levels for %d players, got %d", pc, len(levels))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify structure is increasing
|
||||||
|
prevBB := int64(0)
|
||||||
|
for _, l := range levels {
|
||||||
|
if l.LevelType == "round" && l.BigBlind < prevBB {
|
||||||
|
t.Errorf("level %d: big blind %d < previous %d for %d players", l.Position, l.BigBlind, prevBB, pc)
|
||||||
|
}
|
||||||
|
if l.LevelType == "round" {
|
||||||
|
prevBB = l.BigBlind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWizardGenerate_BlindsAlignWithDenominations(t *testing.T) {
|
||||||
|
if os.Getenv("CGO_ENABLED") == "0" {
|
||||||
|
t.Skip("requires CGO for libsql")
|
||||||
|
}
|
||||||
|
|
||||||
|
db := setupTestDB(t)
|
||||||
|
ws := NewWizardService(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
levels, err := ws.Generate(ctx, WizardInput{
|
||||||
|
PlayerCount: 20,
|
||||||
|
StartingChips: 15000,
|
||||||
|
TargetDurationMinutes: 240,
|
||||||
|
ChipSetID: 1,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("generate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
denoms := []int64{25, 100, 500, 1000, 5000}
|
||||||
|
|
||||||
|
for _, l := range levels {
|
||||||
|
if l.LevelType != "round" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !isDenomAligned(l.SmallBlind, denoms) {
|
||||||
|
t.Errorf("level %d: SB %d not aligned with denominations", l.Position, l.SmallBlind)
|
||||||
|
}
|
||||||
|
if !isDenomAligned(l.BigBlind, denoms) {
|
||||||
|
t.Errorf("level %d: BB %d not aligned with denominations", l.Position, l.BigBlind)
|
||||||
|
}
|
||||||
|
if l.Ante > 0 && !isDenomAligned(l.Ante, denoms) {
|
||||||
|
t.Errorf("level %d: ante %d not aligned with denominations", l.Position, l.Ante)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWizardGenerate_ShortTournament(t *testing.T) {
|
||||||
|
if os.Getenv("CGO_ENABLED") == "0" {
|
||||||
|
t.Skip("requires CGO for libsql")
|
||||||
|
}
|
||||||
|
|
||||||
|
db := setupTestDB(t)
|
||||||
|
ws := NewWizardService(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
levels, err := ws.Generate(ctx, WizardInput{
|
||||||
|
PlayerCount: 10,
|
||||||
|
StartingChips: 10000,
|
||||||
|
TargetDurationMinutes: 60,
|
||||||
|
ChipSetID: 1,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("generate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(levels) < 3 {
|
||||||
|
t.Errorf("expected at least 3 levels for 1hr tournament, got %d", len(levels))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWizardGenerate_LongTournament(t *testing.T) {
|
||||||
|
if os.Getenv("CGO_ENABLED") == "0" {
|
||||||
|
t.Skip("requires CGO for libsql")
|
||||||
|
}
|
||||||
|
|
||||||
|
db := setupTestDB(t)
|
||||||
|
ws := NewWizardService(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
levels, err := ws.Generate(ctx, WizardInput{
|
||||||
|
PlayerCount: 20,
|
||||||
|
StartingChips: 50000,
|
||||||
|
TargetDurationMinutes: 480,
|
||||||
|
ChipSetID: 1,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("generate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(levels) < 5 {
|
||||||
|
t.Errorf("expected at least 5 levels for 8hr tournament, got %d", len(levels))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check breaks are present
|
||||||
|
breakCount := 0
|
||||||
|
for _, l := range levels {
|
||||||
|
if l.LevelType == "break" {
|
||||||
|
breakCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if breakCount == 0 {
|
||||||
|
t.Error("expected at least one break in 8hr tournament")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWizardGenerate_InvalidInputs(t *testing.T) {
|
||||||
|
if os.Getenv("CGO_ENABLED") == "0" {
|
||||||
|
t.Skip("requires CGO for libsql")
|
||||||
|
}
|
||||||
|
|
||||||
|
db := setupTestDB(t)
|
||||||
|
ws := NewWizardService(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input WizardInput
|
||||||
|
}{
|
||||||
|
{"too few players", WizardInput{PlayerCount: 1, StartingChips: 10000, TargetDurationMinutes: 120, ChipSetID: 1}},
|
||||||
|
{"zero chips", WizardInput{PlayerCount: 10, StartingChips: 0, TargetDurationMinutes: 120, ChipSetID: 1}},
|
||||||
|
{"too short", WizardInput{PlayerCount: 10, StartingChips: 10000, TargetDurationMinutes: 10, ChipSetID: 1}},
|
||||||
|
{"missing chip set", WizardInput{PlayerCount: 10, StartingChips: 10000, TargetDurationMinutes: 120, ChipSetID: 999}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
_, err := ws.Generate(ctx, tc.input)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for invalid input")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isDenomAligned checks if a value is a multiple of any denomination.
|
||||||
|
func isDenomAligned(value int64, denoms []int64) bool {
|
||||||
|
for _, d := range denoms {
|
||||||
|
if value%d == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// stringFromInt converts int to string for test names.
|
||||||
|
func stringFromInt(n int) string {
|
||||||
|
s := ""
|
||||||
|
if n == 0 {
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
for n > 0 {
|
||||||
|
s = string(rune('0'+n%10)) + s
|
||||||
|
n /= 10
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
192
internal/store/migrations/005_builtin_templates.sql
Normal file
192
internal/store/migrations/005_builtin_templates.sql
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
-- 005_builtin_templates.sql
|
||||||
|
-- Built-in blind structures, payout structures, buy-in configs, and tournament templates.
|
||||||
|
-- All marked is_builtin = 1, cannot be deleted, can be duplicated.
|
||||||
|
-- Uses INSERT OR IGNORE so re-running is safe.
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- Built-in Blind Structures
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
-- Turbo (~2hr for 20 players, 15-min levels, starting chips 10000)
|
||||||
|
INSERT OR IGNORE INTO blind_structures (id, name, is_builtin, game_type_default, notes) VALUES
|
||||||
|
(1, 'Turbo', 1, 'nlhe', '~2hr for 20 players, 15-min levels, starting chips 10,000');
|
||||||
|
|
||||||
|
INSERT OR IGNORE INTO blind_levels (structure_id, position, level_type, game_type, small_blind, big_blind, ante, bb_ante, duration_seconds, notes) VALUES
|
||||||
|
(1, 0, 'round', 'nlhe', 25, 50, 0, 0, 900, ''),
|
||||||
|
(1, 1, 'round', 'nlhe', 50, 100, 0, 0, 900, ''),
|
||||||
|
(1, 2, 'round', 'nlhe', 75, 150, 0, 0, 900, ''),
|
||||||
|
(1, 3, 'round', 'nlhe', 100, 200, 0, 0, 900, ''),
|
||||||
|
(1, 4, 'break', 'nlhe', 0, 0, 0, 0, 600, ''),
|
||||||
|
(1, 5, 'round', 'nlhe', 150, 300, 300, 0, 900, ''),
|
||||||
|
(1, 6, 'round', 'nlhe', 200, 400, 400, 0, 900, ''),
|
||||||
|
(1, 7, 'round', 'nlhe', 300, 600, 600, 0, 900, ''),
|
||||||
|
(1, 8, 'round', 'nlhe', 400, 800, 800, 0, 900, ''),
|
||||||
|
(1, 9, 'break', 'nlhe', 0, 0, 0, 0, 600, ''),
|
||||||
|
(1, 10, 'round', 'nlhe', 600, 1200, 1200, 0, 900, ''),
|
||||||
|
(1, 11, 'round', 'nlhe', 800, 1600, 1600, 0, 900, ''),
|
||||||
|
(1, 12, 'round', 'nlhe', 1000, 2000, 2000, 0, 900, ''),
|
||||||
|
(1, 13, 'round', 'nlhe', 1500, 3000, 3000, 0, 900, ''),
|
||||||
|
(1, 14, 'round', 'nlhe', 2000, 4000, 4000, 0, 900, '');
|
||||||
|
|
||||||
|
-- Standard (~3-4hr for 20 players, 20-min levels, starting chips 15000)
|
||||||
|
INSERT OR IGNORE INTO blind_structures (id, name, is_builtin, game_type_default, notes) VALUES
|
||||||
|
(2, 'Standard', 1, 'nlhe', '~3-4hr for 20 players, 20-min levels, starting chips 15,000');
|
||||||
|
|
||||||
|
INSERT OR IGNORE INTO blind_levels (structure_id, position, level_type, game_type, small_blind, big_blind, ante, bb_ante, duration_seconds, notes) VALUES
|
||||||
|
(2, 0, 'round', 'nlhe', 25, 50, 0, 0, 1200, ''),
|
||||||
|
(2, 1, 'round', 'nlhe', 50, 100, 0, 0, 1200, ''),
|
||||||
|
(2, 2, 'round', 'nlhe', 75, 150, 0, 0, 1200, ''),
|
||||||
|
(2, 3, 'round', 'nlhe', 100, 200, 0, 0, 1200, ''),
|
||||||
|
(2, 4, 'round', 'nlhe', 150, 300, 0, 0, 1200, ''),
|
||||||
|
(2, 5, 'break', 'nlhe', 0, 0, 0, 0, 600, ''),
|
||||||
|
(2, 6, 'round', 'nlhe', 200, 400, 400, 0, 1200, ''),
|
||||||
|
(2, 7, 'round', 'nlhe', 300, 600, 600, 0, 1200, ''),
|
||||||
|
(2, 8, 'round', 'nlhe', 400, 800, 800, 0, 1200, ''),
|
||||||
|
(2, 9, 'round', 'nlhe', 500, 1000, 1000, 0, 1200, ''),
|
||||||
|
(2, 10, 'break', 'nlhe', 0, 0, 0, 0, 600, ''),
|
||||||
|
(2, 11, 'round', 'nlhe', 600, 1200, 1200, 0, 1200, ''),
|
||||||
|
(2, 12, 'round', 'nlhe', 800, 1600, 1600, 0, 1200, ''),
|
||||||
|
(2, 13, 'round', 'nlhe', 1000, 2000, 2000, 0, 1200, ''),
|
||||||
|
(2, 14, 'round', 'nlhe', 1500, 3000, 3000, 0, 1200, ''),
|
||||||
|
(2, 15, 'round', 'nlhe', 2000, 4000, 4000, 0, 1200, ''),
|
||||||
|
(2, 16, 'round', 'nlhe', 3000, 6000, 6000, 0, 1200, '');
|
||||||
|
|
||||||
|
-- Deep Stack (~5-6hr for 20 players, 30-min levels, starting chips 25000)
|
||||||
|
INSERT OR IGNORE INTO blind_structures (id, name, is_builtin, game_type_default, notes) VALUES
|
||||||
|
(3, 'Deep Stack', 1, 'nlhe', '~5-6hr for 20 players, 30-min levels, starting chips 25,000');
|
||||||
|
|
||||||
|
INSERT OR IGNORE INTO blind_levels (structure_id, position, level_type, game_type, small_blind, big_blind, ante, bb_ante, duration_seconds, notes) VALUES
|
||||||
|
(3, 0, 'round', 'nlhe', 25, 50, 0, 0, 1800, ''),
|
||||||
|
(3, 1, 'round', 'nlhe', 50, 100, 0, 0, 1800, ''),
|
||||||
|
(3, 2, 'round', 'nlhe', 75, 150, 0, 0, 1800, ''),
|
||||||
|
(3, 3, 'round', 'nlhe', 100, 200, 0, 0, 1800, ''),
|
||||||
|
(3, 4, 'round', 'nlhe', 150, 300, 0, 0, 1800, ''),
|
||||||
|
(3, 5, 'break', 'nlhe', 0, 0, 0, 0, 900, ''),
|
||||||
|
(3, 6, 'round', 'nlhe', 200, 400, 0, 0, 1800, ''),
|
||||||
|
(3, 7, 'round', 'nlhe', 250, 500, 500, 0, 1800, ''),
|
||||||
|
(3, 8, 'round', 'nlhe', 300, 600, 600, 0, 1800, ''),
|
||||||
|
(3, 9, 'round', 'nlhe', 400, 800, 800, 0, 1800, ''),
|
||||||
|
(3, 10, 'break', 'nlhe', 0, 0, 0, 0, 900, ''),
|
||||||
|
(3, 11, 'round', 'nlhe', 500, 1000, 1000, 0, 1800, ''),
|
||||||
|
(3, 12, 'round', 'nlhe', 600, 1200, 1200, 0, 1800, ''),
|
||||||
|
(3, 13, 'round', 'nlhe', 800, 1600, 1600, 0, 1800, ''),
|
||||||
|
(3, 14, 'round', 'nlhe', 1000, 2000, 2000, 0, 1800, ''),
|
||||||
|
(3, 15, 'break', 'nlhe', 0, 0, 0, 0, 900, ''),
|
||||||
|
(3, 16, 'round', 'nlhe', 1500, 3000, 3000, 0, 1800, ''),
|
||||||
|
(3, 17, 'round', 'nlhe', 2000, 4000, 4000, 0, 1800, ''),
|
||||||
|
(3, 18, 'round', 'nlhe', 3000, 6000, 6000, 0, 1800, ''),
|
||||||
|
(3, 19, 'round', 'nlhe', 4000, 8000, 8000, 0, 1800, ''),
|
||||||
|
(3, 20, 'round', 'nlhe', 5000, 10000,10000,0, 1800, '');
|
||||||
|
|
||||||
|
-- WSOP-style (60-min levels, BB ante from level 4, starting chips 50000)
|
||||||
|
INSERT OR IGNORE INTO blind_structures (id, name, is_builtin, game_type_default, notes) VALUES
|
||||||
|
(4, 'WSOP-style', 1, 'nlhe', '60-min levels, BB ante from level 4, starting chips 50,000');
|
||||||
|
|
||||||
|
INSERT OR IGNORE INTO blind_levels (structure_id, position, level_type, game_type, small_blind, big_blind, ante, bb_ante, duration_seconds, notes) VALUES
|
||||||
|
(4, 0, 'round', 'nlhe', 25, 50, 0, 0, 3600, ''),
|
||||||
|
(4, 1, 'round', 'nlhe', 50, 100, 0, 0, 3600, ''),
|
||||||
|
(4, 2, 'round', 'nlhe', 75, 150, 0, 0, 3600, ''),
|
||||||
|
(4, 3, 'round', 'nlhe', 100, 200, 0, 200, 3600, 'BB ante starts'),
|
||||||
|
(4, 4, 'break', 'nlhe', 0, 0, 0, 0, 1200, ''),
|
||||||
|
(4, 5, 'round', 'nlhe', 150, 300, 0, 300, 3600, ''),
|
||||||
|
(4, 6, 'round', 'nlhe', 200, 400, 0, 400, 3600, ''),
|
||||||
|
(4, 7, 'round', 'nlhe', 250, 500, 0, 500, 3600, ''),
|
||||||
|
(4, 8, 'round', 'nlhe', 300, 600, 0, 600, 3600, ''),
|
||||||
|
(4, 9, 'break', 'nlhe', 0, 0, 0, 0, 1200, ''),
|
||||||
|
(4, 10, 'round', 'nlhe', 400, 800, 0, 800, 3600, ''),
|
||||||
|
(4, 11, 'round', 'nlhe', 500, 1000, 0, 1000,3600, ''),
|
||||||
|
(4, 12, 'round', 'nlhe', 600, 1200, 0, 1200,3600, ''),
|
||||||
|
(4, 13, 'round', 'nlhe', 800, 1600, 0, 1600,3600, ''),
|
||||||
|
(4, 14, 'break', 'nlhe', 0, 0, 0, 0, 1200, ''),
|
||||||
|
(4, 15, 'round', 'nlhe', 1000, 2000, 0, 2000,3600, ''),
|
||||||
|
(4, 16, 'round', 'nlhe', 1500, 3000, 0, 3000,3600, ''),
|
||||||
|
(4, 17, 'round', 'nlhe', 2000, 4000, 0, 4000,3600, ''),
|
||||||
|
(4, 18, 'round', 'nlhe', 2500, 5000, 0, 5000,3600, ''),
|
||||||
|
(4, 19, 'round', 'nlhe', 3000, 6000, 0, 6000,3600, '');
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- Built-in Payout Structure (Standard)
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
INSERT OR IGNORE INTO payout_structures (id, name, is_builtin) VALUES
|
||||||
|
(1, 'Standard', 1);
|
||||||
|
|
||||||
|
-- Bracket: 8-20 entries (3 prizes: 50/30/20)
|
||||||
|
INSERT OR IGNORE INTO payout_brackets (id, structure_id, min_entries, max_entries) VALUES (1, 1, 8, 20);
|
||||||
|
INSERT OR IGNORE INTO payout_tiers (bracket_id, position, percentage_basis_points) VALUES
|
||||||
|
(1, 1, 5000),
|
||||||
|
(1, 2, 3000),
|
||||||
|
(1, 3, 2000);
|
||||||
|
|
||||||
|
-- Bracket: 21-30 entries (4 prizes: 45/26/17/12)
|
||||||
|
INSERT OR IGNORE INTO payout_brackets (id, structure_id, min_entries, max_entries) VALUES (2, 1, 21, 30);
|
||||||
|
INSERT OR IGNORE INTO payout_tiers (bracket_id, position, percentage_basis_points) VALUES
|
||||||
|
(2, 1, 4500),
|
||||||
|
(2, 2, 2600),
|
||||||
|
(2, 3, 1700),
|
||||||
|
(2, 4, 1200);
|
||||||
|
|
||||||
|
-- Bracket: 31-40 entries (5 prizes: 40/24/15/12/9)
|
||||||
|
INSERT OR IGNORE INTO payout_brackets (id, structure_id, min_entries, max_entries) VALUES (3, 1, 31, 40);
|
||||||
|
INSERT OR IGNORE INTO payout_tiers (bracket_id, position, percentage_basis_points) VALUES
|
||||||
|
(3, 1, 4000),
|
||||||
|
(3, 2, 2400),
|
||||||
|
(3, 3, 1500),
|
||||||
|
(3, 4, 1200),
|
||||||
|
(3, 5, 900);
|
||||||
|
|
||||||
|
-- Bracket: 41+ entries (6 prizes: 35/22/14/12/9/8)
|
||||||
|
INSERT OR IGNORE INTO payout_brackets (id, structure_id, min_entries, max_entries) VALUES (4, 1, 41, 999);
|
||||||
|
INSERT OR IGNORE INTO payout_tiers (bracket_id, position, percentage_basis_points) VALUES
|
||||||
|
(4, 1, 3500),
|
||||||
|
(4, 2, 2200),
|
||||||
|
(4, 3, 1400),
|
||||||
|
(4, 4, 1200),
|
||||||
|
(4, 5, 900),
|
||||||
|
(4, 6, 800);
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- Built-in Buy-in Configs
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
-- Basic buy-in (200 DKK buyin, 10000 starting chips, 20 DKK rake)
|
||||||
|
INSERT OR IGNORE INTO buyin_configs (id, name, buyin_amount, starting_chips, rake_total) VALUES
|
||||||
|
(1, 'Basic (200 DKK)', 20000, 10000, 2000);
|
||||||
|
|
||||||
|
INSERT OR IGNORE INTO rake_splits (buyin_config_id, category, amount) VALUES
|
||||||
|
(1, 'house', 2000);
|
||||||
|
|
||||||
|
-- Standard buy-in (300 DKK, 15000 chips, 30 DKK rake)
|
||||||
|
INSERT OR IGNORE INTO buyin_configs (id, name, buyin_amount, starting_chips, rake_total) VALUES
|
||||||
|
(2, 'Standard (300 DKK)', 30000, 15000, 3000);
|
||||||
|
|
||||||
|
INSERT OR IGNORE INTO rake_splits (buyin_config_id, category, amount) VALUES
|
||||||
|
(2, 'house', 2000),
|
||||||
|
(2, 'staff', 1000);
|
||||||
|
|
||||||
|
-- Deep Stack buy-in (500 DKK, 25000 chips, 50 DKK rake)
|
||||||
|
INSERT OR IGNORE INTO buyin_configs (id, name, buyin_amount, starting_chips, rake_total) VALUES
|
||||||
|
(3, 'Deep Stack (500 DKK)', 50000, 25000, 5000);
|
||||||
|
|
||||||
|
INSERT OR IGNORE INTO rake_splits (buyin_config_id, category, amount) VALUES
|
||||||
|
(3, 'house', 3000),
|
||||||
|
(3, 'staff', 2000);
|
||||||
|
|
||||||
|
-- WSOP-style buy-in (1000 DKK, 50000 chips, 100 DKK rake)
|
||||||
|
INSERT OR IGNORE INTO buyin_configs (id, name, buyin_amount, starting_chips, rake_total) VALUES
|
||||||
|
(4, 'WSOP-style (1000 DKK)', 100000, 50000, 10000);
|
||||||
|
|
||||||
|
INSERT OR IGNORE INTO rake_splits (buyin_config_id, category, amount) VALUES
|
||||||
|
(4, 'house', 5000),
|
||||||
|
(4, 'staff', 3000),
|
||||||
|
(4, 'league', 2000);
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- Built-in Tournament Templates
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
INSERT OR IGNORE INTO tournament_templates (id, name, description, chip_set_id, blind_structure_id, payout_structure_id, buyin_config_id, min_players, is_builtin) VALUES
|
||||||
|
(1, 'Turbo', 'Fast tournament ~2hr for 20 players', 1, 1, 1, 1, 6, 1),
|
||||||
|
(2, 'Standard', 'Standard tournament ~3-4hr for 20 players', 1, 2, 1, 2, 6, 1),
|
||||||
|
(3, 'Deep Stack', 'Deep stack tournament ~5-6hr for 20 players', 1, 3, 1, 3, 6, 1),
|
||||||
|
(4, 'WSOP-style', 'Long tournament with BB ante and slow progression', 1, 4, 1, 4, 6, 1);
|
||||||
279
internal/template/tournament_test.go
Normal file
279
internal/template/tournament_test.go
Normal file
|
|
@ -0,0 +1,279 @@
|
||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
_ "github.com/tursodatabase/go-libsql"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setupTestDB creates a temporary test database with required schema and seed data.
|
||||||
|
func setupTestDB(t *testing.T) *sql.DB {
|
||||||
|
t.Helper()
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dbPath := filepath.Join(tmpDir, "test.db")
|
||||||
|
|
||||||
|
db, err := sql.Open("libsql", "file:"+dbPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open test db: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { db.Close() })
|
||||||
|
|
||||||
|
stmts := []string{
|
||||||
|
`CREATE TABLE chip_sets (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, is_builtin INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL DEFAULT 0)`,
|
||||||
|
`CREATE TABLE chip_denominations (id INTEGER PRIMARY KEY AUTOINCREMENT, chip_set_id INTEGER NOT NULL, value INTEGER NOT NULL, color_hex TEXT NOT NULL DEFAULT '#FFF', label TEXT NOT NULL DEFAULT '', sort_order INTEGER NOT NULL DEFAULT 0)`,
|
||||||
|
`CREATE TABLE blind_structures (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, is_builtin INTEGER NOT NULL DEFAULT 0, game_type_default TEXT NOT NULL DEFAULT 'nlhe', notes TEXT NOT NULL DEFAULT '', created_at INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL DEFAULT 0)`,
|
||||||
|
`CREATE TABLE blind_levels (id INTEGER PRIMARY KEY AUTOINCREMENT, structure_id INTEGER NOT NULL, position INTEGER NOT NULL, level_type TEXT NOT NULL DEFAULT 'round', game_type TEXT NOT NULL DEFAULT 'nlhe', small_blind INTEGER NOT NULL DEFAULT 0, big_blind INTEGER NOT NULL DEFAULT 0, ante INTEGER NOT NULL DEFAULT 0, bb_ante INTEGER NOT NULL DEFAULT 0, duration_seconds INTEGER NOT NULL DEFAULT 900, chip_up_denomination_value INTEGER, notes TEXT NOT NULL DEFAULT '')`,
|
||||||
|
`CREATE TABLE payout_structures (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, is_builtin INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL DEFAULT 0)`,
|
||||||
|
`CREATE TABLE payout_brackets (id INTEGER PRIMARY KEY AUTOINCREMENT, structure_id INTEGER NOT NULL, min_entries INTEGER NOT NULL, max_entries INTEGER NOT NULL)`,
|
||||||
|
`CREATE TABLE payout_tiers (id INTEGER PRIMARY KEY AUTOINCREMENT, bracket_id INTEGER NOT NULL, position INTEGER NOT NULL, percentage_basis_points INTEGER NOT NULL)`,
|
||||||
|
`CREATE TABLE buyin_configs (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, buyin_amount INTEGER DEFAULT 0, starting_chips INTEGER DEFAULT 0, rake_total INTEGER DEFAULT 0, bounty_amount INTEGER DEFAULT 0, bounty_chip INTEGER DEFAULT 0, rebuy_allowed INTEGER DEFAULT 0, rebuy_cost INTEGER DEFAULT 0, rebuy_chips INTEGER DEFAULT 0, rebuy_rake INTEGER DEFAULT 0, rebuy_limit INTEGER DEFAULT 0, rebuy_level_cutoff INTEGER, rebuy_time_cutoff_seconds INTEGER, rebuy_chip_threshold INTEGER, addon_allowed INTEGER DEFAULT 0, addon_cost INTEGER DEFAULT 0, addon_chips INTEGER DEFAULT 0, addon_rake INTEGER DEFAULT 0, addon_level_start INTEGER, addon_level_end INTEGER, reentry_allowed INTEGER DEFAULT 0, reentry_limit INTEGER DEFAULT 0, late_reg_level_cutoff INTEGER, late_reg_time_cutoff_seconds INTEGER, created_at INTEGER DEFAULT 0, updated_at INTEGER DEFAULT 0)`,
|
||||||
|
`CREATE TABLE rake_splits (id INTEGER PRIMARY KEY AUTOINCREMENT, buyin_config_id INTEGER NOT NULL, category TEXT NOT NULL, amount INTEGER NOT NULL DEFAULT 0)`,
|
||||||
|
`CREATE TABLE points_formulas (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, expression TEXT DEFAULT '', variables TEXT DEFAULT '{}', is_builtin INTEGER DEFAULT 0, created_at INTEGER DEFAULT 0, updated_at INTEGER DEFAULT 0)`,
|
||||||
|
`CREATE TABLE tournament_templates (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, description TEXT DEFAULT '', chip_set_id INTEGER NOT NULL, blind_structure_id INTEGER NOT NULL, payout_structure_id INTEGER NOT NULL, buyin_config_id INTEGER NOT NULL, points_formula_id INTEGER, min_players INTEGER DEFAULT 2, max_players INTEGER, early_signup_bonus_chips INTEGER DEFAULT 0, early_signup_cutoff TEXT, punctuality_bonus_chips INTEGER DEFAULT 0, is_pko INTEGER DEFAULT 0, is_builtin INTEGER DEFAULT 0, created_at INTEGER DEFAULT 0, updated_at INTEGER DEFAULT 0)`,
|
||||||
|
`CREATE TABLE tournaments (id TEXT PRIMARY KEY, name TEXT, template_id INTEGER, chip_set_id INTEGER NOT NULL, blind_structure_id INTEGER NOT NULL, payout_structure_id INTEGER NOT NULL, buyin_config_id INTEGER NOT NULL, points_formula_id INTEGER, status TEXT DEFAULT 'created', min_players INTEGER DEFAULT 2, max_players INTEGER, early_signup_bonus_chips INTEGER DEFAULT 0, early_signup_cutoff TEXT, punctuality_bonus_chips INTEGER DEFAULT 0, is_pko INTEGER DEFAULT 0, current_level INTEGER DEFAULT 0, clock_state TEXT DEFAULT 'stopped', clock_remaining_ns INTEGER DEFAULT 0, total_elapsed_ns INTEGER DEFAULT 0, hand_for_hand INTEGER DEFAULT 0, started_at INTEGER, ended_at INTEGER, created_at INTEGER DEFAULT 0, updated_at INTEGER DEFAULT 0)`,
|
||||||
|
}
|
||||||
|
for _, stmt := range stmts {
|
||||||
|
if _, err := db.Exec(stmt); err != nil {
|
||||||
|
t.Fatalf("create table: %v\nSQL: %s", err, stmt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed building blocks
|
||||||
|
db.Exec(`INSERT INTO chip_sets (id, name) VALUES (1, 'Standard')`)
|
||||||
|
db.Exec(`INSERT INTO chip_denominations (chip_set_id, value, color_hex) VALUES (1, 25, '#FFF'), (1, 100, '#F00')`)
|
||||||
|
|
||||||
|
db.Exec(`INSERT INTO blind_structures (id, name) VALUES (1, 'Test Structure')`)
|
||||||
|
db.Exec(`INSERT INTO blind_levels (structure_id, position, level_type, small_blind, big_blind, duration_seconds) VALUES (1, 0, 'round', 25, 50, 900)`)
|
||||||
|
|
||||||
|
db.Exec(`INSERT INTO payout_structures (id, name) VALUES (1, 'Test Payout')`)
|
||||||
|
db.Exec(`INSERT INTO payout_brackets (id, structure_id, min_entries, max_entries) VALUES (1, 1, 2, 20)`)
|
||||||
|
db.Exec(`INSERT INTO payout_tiers (bracket_id, position, percentage_basis_points) VALUES (1, 1, 6000), (1, 2, 4000)`)
|
||||||
|
|
||||||
|
db.Exec(`INSERT INTO buyin_configs (id, name, buyin_amount, starting_chips, rake_total) VALUES (1, 'Test Buy-in', 20000, 10000, 2000)`)
|
||||||
|
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateTemplate_ValidReferences(t *testing.T) {
|
||||||
|
if os.Getenv("CGO_ENABLED") == "0" {
|
||||||
|
t.Skip("requires CGO for libsql")
|
||||||
|
}
|
||||||
|
|
||||||
|
db := setupTestDB(t)
|
||||||
|
svc := NewTournamentTemplateService(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
tmpl, err := svc.CreateTemplate(ctx, &TournamentTemplate{
|
||||||
|
Name: "My Tournament",
|
||||||
|
Description: "Test template",
|
||||||
|
ChipSetID: 1,
|
||||||
|
BlindStructureID: 1,
|
||||||
|
PayoutStructureID: 1,
|
||||||
|
BuyinConfigID: 1,
|
||||||
|
MinPlayers: 6,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create template: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tmpl.ID == 0 {
|
||||||
|
t.Error("expected non-zero ID")
|
||||||
|
}
|
||||||
|
if tmpl.Name != "My Tournament" {
|
||||||
|
t.Errorf("expected name 'My Tournament', got %q", tmpl.Name)
|
||||||
|
}
|
||||||
|
if tmpl.ChipSetName != "Standard" {
|
||||||
|
t.Errorf("expected chip set name 'Standard', got %q", tmpl.ChipSetName)
|
||||||
|
}
|
||||||
|
if tmpl.BlindStructureName != "Test Structure" {
|
||||||
|
t.Errorf("expected blind structure name 'Test Structure', got %q", tmpl.BlindStructureName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateTemplate_InvalidReference(t *testing.T) {
|
||||||
|
if os.Getenv("CGO_ENABLED") == "0" {
|
||||||
|
t.Skip("requires CGO for libsql")
|
||||||
|
}
|
||||||
|
|
||||||
|
db := setupTestDB(t)
|
||||||
|
svc := NewTournamentTemplateService(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
_, err := svc.CreateTemplate(ctx, &TournamentTemplate{
|
||||||
|
Name: "Bad Template",
|
||||||
|
ChipSetID: 999, // doesn't exist
|
||||||
|
BlindStructureID: 1,
|
||||||
|
PayoutStructureID: 1,
|
||||||
|
BuyinConfigID: 1,
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for invalid chip set reference")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTemplateExpanded(t *testing.T) {
|
||||||
|
if os.Getenv("CGO_ENABLED") == "0" {
|
||||||
|
t.Skip("requires CGO for libsql")
|
||||||
|
}
|
||||||
|
|
||||||
|
db := setupTestDB(t)
|
||||||
|
svc := NewTournamentTemplateService(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
tmpl, err := svc.CreateTemplate(ctx, &TournamentTemplate{
|
||||||
|
Name: "Expandable",
|
||||||
|
ChipSetID: 1,
|
||||||
|
BlindStructureID: 1,
|
||||||
|
PayoutStructureID: 1,
|
||||||
|
BuyinConfigID: 1,
|
||||||
|
MinPlayers: 6,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create template: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expanded, err := svc.GetTemplateExpanded(ctx, tmpl.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get expanded: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expanded.ChipSet == nil {
|
||||||
|
t.Fatal("expected chip set to be populated")
|
||||||
|
}
|
||||||
|
if expanded.ChipSet.Name != "Standard" {
|
||||||
|
t.Errorf("expected chip set name 'Standard', got %q", expanded.ChipSet.Name)
|
||||||
|
}
|
||||||
|
if len(expanded.ChipSet.Denominations) != 2 {
|
||||||
|
t.Errorf("expected 2 denominations, got %d", len(expanded.ChipSet.Denominations))
|
||||||
|
}
|
||||||
|
|
||||||
|
if expanded.BlindStructure == nil {
|
||||||
|
t.Fatal("expected blind structure to be populated")
|
||||||
|
}
|
||||||
|
if len(expanded.BlindStructure.Levels) != 1 {
|
||||||
|
t.Errorf("expected 1 level, got %d", len(expanded.BlindStructure.Levels))
|
||||||
|
}
|
||||||
|
|
||||||
|
if expanded.PayoutStructure == nil {
|
||||||
|
t.Fatal("expected payout structure to be populated")
|
||||||
|
}
|
||||||
|
if len(expanded.PayoutStructure.Brackets) != 1 {
|
||||||
|
t.Errorf("expected 1 bracket, got %d", len(expanded.PayoutStructure.Brackets))
|
||||||
|
}
|
||||||
|
|
||||||
|
if expanded.BuyinConfig == nil {
|
||||||
|
t.Fatal("expected buyin config to be populated")
|
||||||
|
}
|
||||||
|
if expanded.BuyinConfig.BuyinAmount != 20000 {
|
||||||
|
t.Errorf("expected buyin amount 20000, got %d", expanded.BuyinConfig.BuyinAmount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveAsTemplate(t *testing.T) {
|
||||||
|
if os.Getenv("CGO_ENABLED") == "0" {
|
||||||
|
t.Skip("requires CGO for libsql")
|
||||||
|
}
|
||||||
|
|
||||||
|
db := setupTestDB(t)
|
||||||
|
svc := NewTournamentTemplateService(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Create a tournament
|
||||||
|
db.Exec(`INSERT INTO tournaments (id, name, chip_set_id, blind_structure_id, payout_structure_id, buyin_config_id, min_players, is_pko) VALUES ('t1', 'Live Tournament', 1, 1, 1, 1, 8, 0)`)
|
||||||
|
|
||||||
|
tmpl, err := svc.SaveAsTemplate(ctx, "t1", "Saved From Live")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("save as template: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tmpl.Name != "Saved From Live" {
|
||||||
|
t.Errorf("expected name 'Saved From Live', got %q", tmpl.Name)
|
||||||
|
}
|
||||||
|
if tmpl.ChipSetID != 1 {
|
||||||
|
t.Errorf("expected chip_set_id 1, got %d", tmpl.ChipSetID)
|
||||||
|
}
|
||||||
|
if tmpl.MinPlayers != 8 {
|
||||||
|
t.Errorf("expected min_players 8, got %d", tmpl.MinPlayers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDuplicateTemplate(t *testing.T) {
|
||||||
|
if os.Getenv("CGO_ENABLED") == "0" {
|
||||||
|
t.Skip("requires CGO for libsql")
|
||||||
|
}
|
||||||
|
|
||||||
|
db := setupTestDB(t)
|
||||||
|
svc := NewTournamentTemplateService(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
original, err := svc.CreateTemplate(ctx, &TournamentTemplate{
|
||||||
|
Name: "Original",
|
||||||
|
ChipSetID: 1,
|
||||||
|
BlindStructureID: 1,
|
||||||
|
PayoutStructureID: 1,
|
||||||
|
BuyinConfigID: 1,
|
||||||
|
MinPlayers: 6,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create template: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dup, err := svc.DuplicateTemplate(ctx, original.ID, "Duplicate")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("duplicate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dup.ID == original.ID {
|
||||||
|
t.Error("duplicate should have different ID")
|
||||||
|
}
|
||||||
|
if dup.Name != "Duplicate" {
|
||||||
|
t.Errorf("expected name 'Duplicate', got %q", dup.Name)
|
||||||
|
}
|
||||||
|
if dup.ChipSetID != original.ChipSetID {
|
||||||
|
t.Error("duplicate should have same chip set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteBuiltinTemplate(t *testing.T) {
|
||||||
|
if os.Getenv("CGO_ENABLED") == "0" {
|
||||||
|
t.Skip("requires CGO for libsql")
|
||||||
|
}
|
||||||
|
|
||||||
|
db := setupTestDB(t)
|
||||||
|
svc := NewTournamentTemplateService(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Insert a builtin template directly
|
||||||
|
db.Exec(`INSERT INTO tournament_templates (id, name, chip_set_id, blind_structure_id, payout_structure_id, buyin_config_id, is_builtin) VALUES (99, 'Built-in', 1, 1, 1, 1, 1)`)
|
||||||
|
|
||||||
|
err := svc.DeleteTemplate(ctx, 99)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error deleting built-in template")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListTemplates(t *testing.T) {
|
||||||
|
if os.Getenv("CGO_ENABLED") == "0" {
|
||||||
|
t.Skip("requires CGO for libsql")
|
||||||
|
}
|
||||||
|
|
||||||
|
db := setupTestDB(t)
|
||||||
|
svc := NewTournamentTemplateService(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Create two templates
|
||||||
|
svc.CreateTemplate(ctx, &TournamentTemplate{Name: "A", ChipSetID: 1, BlindStructureID: 1, PayoutStructureID: 1, BuyinConfigID: 1})
|
||||||
|
svc.CreateTemplate(ctx, &TournamentTemplate{Name: "B", ChipSetID: 1, BlindStructureID: 1, PayoutStructureID: 1, BuyinConfigID: 1})
|
||||||
|
|
||||||
|
list, err := svc.ListTemplates(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("list templates: %v", err)
|
||||||
|
}
|
||||||
|
if len(list) < 2 {
|
||||||
|
t.Errorf("expected at least 2 templates, got %d", len(list))
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue