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.
93 lines
2.7 KiB
Go
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])
|
|
}
|
|
}
|