Files
2026-02-22 15:21:18 -05:00

151 lines
3.9 KiB
Go

package scheduler_test
import (
"context"
"os"
"sync/atomic"
"testing"
"time"
"github.com/kjannette/trahn-backend/internal/external"
"github.com/kjannette/trahn-backend/internal/repository"
"github.com/kjannette/trahn-backend/internal/scheduler"
"github.com/kjannette/trahn-backend/internal/strategy"
"github.com/kjannette/trahn-backend/internal/testutil"
)
func TestSRScheduler_FetchNow(t *testing.T) {
apiKey := os.Getenv("DUNE_API_KEY")
if apiKey == "" {
t.Skip("DUNE_API_KEY not set, skipping")
}
pool := testutil.SetupPool(t)
srRepo := repository.NewSRRepo(pool)
dune := external.NewDuneClient(apiKey, external.DuneOptions{
Method: "simple",
LookbackDays: 14,
RefreshHours: 48,
})
var srUpdated atomic.Bool
var recalculated atomic.Bool
sched := scheduler.NewSRScheduler(dune, srRepo, scheduler.SRSchedulerConfig{
CronInterval: 1 * time.Hour,
SRChangeThreshold: 5,
OnSRUpdate: func(sr *external.SRResult) {
srUpdated.Store(true)
},
OnGridRecalculate: func(sr *external.SRResult) {
recalculated.Store(true)
},
})
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
defer cancel()
err := sched.FetchNow(ctx)
if err != nil {
t.Fatalf("FetchNow: %v", err)
}
if !srUpdated.Load() {
t.Fatal("OnSRUpdate callback was not called")
}
t.Log("OnSRUpdate: called")
t.Logf("OnGridRecalculate called: %v", recalculated.Load())
// Verify it was stored in DB
latest, err := srRepo.GetLatest(ctx)
if err != nil {
t.Fatalf("GetLatest: %v", err)
}
if latest == nil {
t.Fatal("expected S/R record in DB")
}
t.Logf("DB record: support=$%.2f resistance=$%.2f midpoint=$%.2f recalc=%v",
latest.Support, latest.Resistance, latest.Midpoint, latest.GridRecalculated)
}
func TestSRScheduler_WithBotState_PriceOutside(t *testing.T) {
apiKey := os.Getenv("DUNE_API_KEY")
if apiKey == "" {
t.Skip("DUNE_API_KEY not set, skipping")
}
pool := testutil.SetupPool(t)
srRepo := repository.NewSRRepo(pool)
dune := external.NewDuneClient(apiKey, external.DuneOptions{
Method: "simple",
LookbackDays: 14,
RefreshHours: 48,
})
var recalculated atomic.Bool
var recalcReasons string
// Simulate a grid that is far from current market price
fakeBotState := &scheduler.BotState{
Grid: []strategy.GridLevel{
{Index: 0, Price: 1000, Side: "buy"},
{Index: 1, Price: 1050, Side: "buy"},
{Index: 2, Price: 1100, Side: "sell"},
{Index: 3, Price: 1150, Side: "sell"},
},
LastETHPrice: 1962, // current real price, way outside the grid
}
sched := scheduler.NewSRScheduler(dune, srRepo, scheduler.SRSchedulerConfig{
CronInterval: 1 * time.Hour,
SRChangeThreshold: 50, // high threshold so only bot state triggers recalc
GetBotState: func() *scheduler.BotState {
return fakeBotState
},
OnGridRecalculate: func(sr *external.SRResult) {
recalculated.Store(true)
recalcReasons = "price outside grid"
},
})
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
defer cancel()
err := sched.FetchNow(ctx)
if err != nil {
t.Fatalf("FetchNow: %v", err)
}
if !recalculated.Load() {
t.Fatal("expected recalculation due to price outside grid")
}
t.Logf("Recalculated: true (reason: %s)", recalcReasons)
}
func TestSRScheduler_StartStop(t *testing.T) {
pool := testutil.SetupPool(t)
srRepo := repository.NewSRRepo(pool)
// Use a dummy Dune client (no API key — won't actually fetch)
dune := external.NewDuneClient("", external.DuneOptions{})
sched := scheduler.NewSRScheduler(dune, srRepo, scheduler.SRSchedulerConfig{
CronInterval: 1 * time.Hour,
})
sched.Start()
if !sched.Running() {
t.Fatal("expected running after Start")
}
// Give initial goroutine a moment (it will fail due to no API key, that's fine)
time.Sleep(200 * time.Millisecond)
sched.Stop()
if sched.Running() {
t.Fatal("expected not running after Stop")
}
t.Log("Start/Stop lifecycle: OK")
}