package kalshi import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" ) func TestParseKalshiURL(t *testing.T) { tests := []struct { name string url string wantTicker string wantErr bool }{ { name: "market URL", url: "https://kalshi.com/markets/KXHIGHNY-26FEB21-T50", wantTicker: "KXHIGHNY-26FEB21-T50", }, { name: "www prefix", url: "https://www.kalshi.com/markets/KXHIGHNY-26FEB21-T50", wantTicker: "KXHIGHNY-26FEB21-T50", }, { name: "wrong host", url: "https://example.com/markets/FOO", wantErr: true, }, { name: "too short path", url: "https://kalshi.com/markets", wantErr: true, }, { name: "wrong path", url: "https://kalshi.com/browse/FOO", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ticker, err := parseKalshiURL(tt.url) if tt.wantErr { if err == nil { t.Fatal("expected error, got nil") } return } if err != nil { t.Fatalf("unexpected error: %v", err) } if ticker != tt.wantTicker { t.Errorf("ticker = %q, want %q", ticker, tt.wantTicker) } }) } } func TestMatchesQuery(t *testing.T) { tests := []struct { text string query string want bool }{ {"USA vs Canada Olympic Hockey", "olympic hockey", true}, {"USA vs Canada Olympic Hockey", "usa canada", true}, {"USA vs Canada Olympic Hockey", "baseball", false}, {"Will it rain tomorrow?", "rain", true}, {"Will it rain tomorrow?", "rain snow", false}, } for _, tt := range tests { words := []string{} for _, w := range splitWords(tt.query) { words = append(words, w) } got := matchesQuery(tt.text, words) if got != tt.want { t.Errorf("matchesQuery(%q, %q) = %v, want %v", tt.text, tt.query, got, tt.want) } } } func splitWords(s string) []string { var words []string for _, w := range []byte(s) { if w == ' ' { continue } if len(words) == 0 || s[len(s)-1] == ' ' { words = append(words, "") } } // Simple split for tests result := []string{} current := "" for _, c := range s { if c == ' ' { if current != "" { result = append(result, current) current = "" } } else { current += string(c) } } if current != "" { result = append(result, current) } return result } func newTestServer(t *testing.T, handler http.HandlerFunc) (*httptest.Server, Config) { t.Helper() srv := httptest.NewServer(handler) t.Cleanup(srv.Close) return srv, Config{BaseURL: srv.URL} } func TestGetMarket(t *testing.T) { _, cfg := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/markets/KXHIGHNY-26FEB21-T50" { http.NotFound(w, r) return } resp := apiMarketResponse{ Market: apiMarket{ Ticker: "KXHIGHNY-26FEB21-T50", EventTicker: "KXHIGHNY-26FEB21", Title: "Will the high temp in NYC be >50?", Subtitle: "51 or above", Status: "active", YesBid: 3, YesAsk: 4, NoBid: 96, NoAsk: 97, LastPrice: 3, Volume: 1849, Volume24h: 500, OpenInterest: 1203, RulesPrimary: "Resolves Yes if temp > 50", CloseTime: "2026-02-22T04:59:00Z", }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) }) m, err := cfg.GetMarket(context.Background(), "KXHIGHNY-26FEB21-T50") if err != nil { t.Fatalf("GetMarket error: %v", err) } if m.Ticker != "KXHIGHNY-26FEB21-T50" { t.Errorf("ticker = %q", m.Ticker) } if m.Title != "Will the high temp in NYC be >50?" { t.Errorf("title = %q", m.Title) } if m.YesBid != 0.03 { t.Errorf("yes bid = %v, want 0.03", m.YesBid) } if m.YesAsk != 0.04 { t.Errorf("yes ask = %v, want 0.04", m.YesAsk) } if m.LastPrice != 0.03 { t.Errorf("last price = %v, want 0.03", m.LastPrice) } if m.Volume != 1849 { t.Errorf("volume = %d, want 1849", m.Volume) } if m.Rules != "Resolves Yes if temp > 50" { t.Errorf("rules = %q", m.Rules) } if m.URL() != "https://kalshi.com/markets/KXHIGHNY-26FEB21-T50" { t.Errorf("URL = %q", m.URL()) } } func TestGetEvent(t *testing.T) { _, cfg := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/events/KXHIGHNY-26FEB21" { http.NotFound(w, r) return } resp := apiEventResponse{ Event: apiEvent{ EventTicker: "KXHIGHNY-26FEB21", SeriesTicker: "KXHIGHNY", Title: "Highest temperature in NYC on Feb 21", Category: "Weather", Markets: []apiMarket{ { Ticker: "KXHIGHNY-26FEB21-T50", EventTicker: "KXHIGHNY-26FEB21", Title: "Will the high temp be >50?", Status: "active", YesBid: 3, YesAsk: 4, LastPrice: 3, }, { Ticker: "KXHIGHNY-26FEB21-T40", EventTicker: "KXHIGHNY-26FEB21", Title: "Will the high temp be >40?", Status: "active", YesBid: 65, YesAsk: 68, LastPrice: 66, }, }, }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) }) ev, err := cfg.GetEvent(context.Background(), "KXHIGHNY-26FEB21") if err != nil { t.Fatalf("GetEvent error: %v", err) } if ev.EventTicker != "KXHIGHNY-26FEB21" { t.Errorf("event ticker = %q", ev.EventTicker) } if ev.Title != "Highest temperature in NYC on Feb 21" { t.Errorf("title = %q", ev.Title) } if ev.Category != "Weather" { t.Errorf("category = %q", ev.Category) } if len(ev.Markets) != 2 { t.Fatalf("got %d markets, want 2", len(ev.Markets)) } if ev.Markets[1].LastPrice != 0.66 { t.Errorf("market 2 last price = %v, want 0.66", ev.Markets[1].LastPrice) } } func TestSearch(t *testing.T) { _, cfg := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/events" { http.NotFound(w, r) return } resp := apiEventsResponse{ Events: []apiEvent{ { EventTicker: "HOCKEY-USA-CAN-MEN", Title: "USA vs Canada Olympic Hockey (Men)", Category: "Sports", }, { EventTicker: "HOCKEY-USA-CAN-WOM", Title: "USA vs Canada Olympic Hockey (Women)", Category: "Sports", }, { EventTicker: "WEATHER-NYC", Title: "NYC Temperature Today", Category: "Weather", }, }, Cursor: "", } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) }) events, err := cfg.Search(context.Background(), "hockey") if err != nil { t.Fatalf("Search error: %v", err) } if len(events) != 2 { t.Fatalf("got %d events, want 2", len(events)) } if events[0].EventTicker != "HOCKEY-USA-CAN-MEN" { t.Errorf("event 0 ticker = %q", events[0].EventTicker) } if events[1].EventTicker != "HOCKEY-USA-CAN-WOM" { t.Errorf("event 1 ticker = %q", events[1].EventTicker) } } func TestSearch_Pagination(t *testing.T) { callCount := 0 _, cfg := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { callCount++ var resp apiEventsResponse if callCount == 1 { resp = apiEventsResponse{ Events: []apiEvent{ {EventTicker: "OTHER", Title: "Something else"}, }, Cursor: "page2", } } else { resp = apiEventsResponse{ Events: []apiEvent{ {EventTicker: "MATCH", Title: "Hockey Match"}, }, Cursor: "", } } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) }) events, err := cfg.Search(context.Background(), "hockey") if err != nil { t.Fatalf("Search error: %v", err) } if len(events) != 1 { t.Fatalf("got %d events, want 1", len(events)) } if events[0].EventTicker != "MATCH" { t.Errorf("ticker = %q", events[0].EventTicker) } if callCount != 2 { t.Errorf("API called %d times, want 2", callCount) } } func TestSearch_Empty(t *testing.T) { _, cfg := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { resp := apiEventsResponse{Events: []apiEvent{}, Cursor: ""} w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) }) events, err := cfg.Search(context.Background(), "nonexistent") if err != nil { t.Fatalf("Search error: %v", err) } if len(events) != 0 { t.Errorf("got %d events, want 0", len(events)) } } func TestMarketURL(t *testing.T) { m := Market{Ticker: "FOO-BAR"} if m.URL() != "https://kalshi.com/markets/FOO-BAR" { t.Errorf("URL = %q", m.URL()) } m2 := Market{} if m2.URL() != "" { t.Errorf("empty ticker URL = %q, want empty", m2.URL()) } } func TestEventURL(t *testing.T) { e := Event{EventTicker: "FOO-BAR"} if e.URL() != "https://kalshi.com/markets/FOO-BAR" { t.Errorf("URL = %q", e.URL()) } } func TestGetMarketFromURL(t *testing.T) { _, cfg := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/markets/KXHIGHNY-26FEB21-T50" { http.NotFound(w, r) return } resp := apiMarketResponse{ Market: apiMarket{ Ticker: "KXHIGHNY-26FEB21-T50", Title: "Will the high temp be >50?", Status: "active", LastPrice: 3, }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) }) m, err := cfg.GetMarketFromURL(context.Background(), "https://kalshi.com/markets/KXHIGHNY-26FEB21-T50") if err != nil { t.Fatalf("GetMarketFromURL error: %v", err) } if m.Ticker != "KXHIGHNY-26FEB21-T50" { t.Errorf("ticker = %q", m.Ticker) } }