Files
OpsLog/internal/watchlist/watchlist_test.go
T
rouggy c9e98d5fdd feat(watchlist): exact match by default, * for a family
The prefix matching ported from DXHunter swallowed too much: a three-letter
special-event entry N8W lit up NEEDED for N8WCR, a different station entirely.
The implicit becomes explicit — a bare entry matches exactly that call, a
trailing star makes it a family: VK9* catches every VK9…, RI0SP* the
expedition's portable forms. A star anywhere else (or alone) is refused at Add
rather than silently matching nothing. Same rule in the backend Match and the
tab's own matcher, pinned by test.
2026-08-29 01:25:34 +02:00

93 lines
2.7 KiB
Go

package watchlist
import (
"encoding/json"
"os"
"path/filepath"
"testing"
)
func TestGlobMatch(t *testing.T) {
s := New(filepath.Join(t.TempDir(), "watchlist.json"))
if err := s.Add("RI0SP*", false); err != nil {
t.Fatal(err)
}
if err := s.Add("N8W", false); err != nil {
t.Fatal(err)
}
// A starred entry is a family: the expedition's portable forms.
for _, call := range []string{"RI0SP", "RI0SP/MM", "RI0SP/P"} {
if _, ok := s.Match(call); !ok {
t.Errorf("Match(%q) = false, want true", call)
}
}
// A bare entry is THAT call — the reported bug was N8W lighting up for
// N8WCR, a different station entirely.
if _, ok := s.Match("N8W"); !ok {
t.Error("exact entry must match its own call")
}
if _, ok := s.Match("N8WCR"); ok {
t.Error("exact entry must NOT match a longer call")
}
if _, ok := s.Match("RI0S"); ok {
t.Error("a SHORTER call must not match a starred entry")
}
if err := s.Add("VK*9", false); err == nil {
t.Error("a mid-string star must be refused")
}
if err := s.Add("*", false); err == nil {
t.Error("a bare star must be refused")
}
}
func TestDXHunterFileRoundTrips(t *testing.T) {
// A real DXHunter entry, ClubLog block included. It must survive
// load → save byte-meaningfully: same keys, values preserved.
src := `[{"callsign":"C5SP","lastSeen":"0001-01-01T00:00:00Z","lastSeenStr":"Never",
"addedAt":"2026-01-17T00:18:43.89Z","spotCount":7,"isContest":true,"notify":true,
"isExpedition":true,"clubLogQSOs24h":120,"clubLogTotalQSOs":30500,
"clubLogHasOQRS":true,"clubLogLiveStream":true,"clubLogUpdatedAt":"2026-08-28T22:32:18Z"}]`
dir := t.TempDir()
path := filepath.Join(dir, "watchlist.json")
if err := os.WriteFile(path, []byte(src), 0o644); err != nil {
t.Fatal(err)
}
s := New(path)
s.Flush()
out, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
var list []map[string]any
if err := json.Unmarshal(out, &list); err != nil {
t.Fatal(err)
}
if len(list) != 1 {
t.Fatalf("got %d entries", len(list))
}
e := list[0]
for k, want := range map[string]any{
"callsign": "C5SP", "isContest": true, "notify": true,
"isExpedition": true, "clubLogQSOs24h": float64(120),
"clubLogTotalQSOs": float64(30500), "clubLogHasOQRS": true,
} {
if e[k] != want {
t.Errorf("%s = %v, want %v", k, e[k], want)
}
}
}
func TestMarkSeenAndNotify(t *testing.T) {
s := New(filepath.Join(t.TempDir(), "watchlist.json"))
_ = s.Add("HB040A", false)
_ = s.SetNotify("HB040A", true)
entry, notify, ok := s.MarkSeen("HB040A")
if !ok || !notify || entry != "HB040A" {
t.Fatalf("MarkSeen = %q %v %v", entry, notify, ok)
}
list := s.Entries()
if list[0].SpotCount != 1 || list[0].LastSeenStr != "Just now" {
t.Errorf("entry after MarkSeen: %+v", list[0])
}
}