150 lines
4.5 KiB
Go
150 lines
4.5 KiB
Go
// ©AngelaMos | 2026
|
|
// watch_test.go
|
|
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/CarterPerez-dev/nadezhda/internal/config"
|
|
"github.com/CarterPerez-dev/nadezhda/internal/rank"
|
|
"github.com/CarterPerez-dev/nadezhda/internal/store"
|
|
)
|
|
|
|
func TestIsNotable(t *testing.T) {
|
|
kevCluster := store.DigestCluster{CVEs: []store.DigestCVE{{ID: "CVE-2026-1", IsKEV: true}}}
|
|
plainCluster := store.DigestCluster{}
|
|
tests := []struct {
|
|
name string
|
|
scored rank.Scored
|
|
watch config.Watch
|
|
want bool
|
|
}{
|
|
{"score above threshold", rank.Scored{Cluster: plainCluster, Score: 0.9}, config.Watch{NotifyMinScore: 0.5}, true},
|
|
{"score below threshold, not kev", rank.Scored{Cluster: plainCluster, Score: 0.2}, config.Watch{NotifyMinScore: 0.5}, false},
|
|
{"below threshold but kev with notify_on_kev", rank.Scored{Cluster: kevCluster, Score: 0.1}, config.Watch{NotifyMinScore: 0.5, NotifyOnKEV: true}, true},
|
|
{"kev but notify_on_kev off", rank.Scored{Cluster: kevCluster, Score: 0.1}, config.Watch{NotifyMinScore: 0.5, NotifyOnKEV: false}, false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := isNotable(tt.scored, tt.watch); got != tt.want {
|
|
t.Errorf("isNotable = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestToNotablePicksHighestAuthoritySource(t *testing.T) {
|
|
cvss := 9.8
|
|
sc := rank.Scored{
|
|
Score: 0.77,
|
|
Cluster: store.DigestCluster{
|
|
Articles: []store.DigestArticle{
|
|
{Title: "low authority", CanonicalURL: "https://low", SourceName: "low", SourceWeight: 0.5},
|
|
{Title: "high authority", CanonicalURL: "https://high", SourceName: "high", SourceWeight: 1.0},
|
|
},
|
|
CVEs: []store.DigestCVE{{ID: "CVE-2026-1", CVSSScore: &cvss, IsKEV: true}},
|
|
},
|
|
}
|
|
n := toNotable(sc)
|
|
if n.Title != "high authority" || n.URL != "https://high" {
|
|
t.Errorf("representative should be the highest-source-weight article, got %q / %q", n.Title, n.URL)
|
|
}
|
|
if !n.IsKEV || n.MaxCVSS != 9.8 {
|
|
t.Errorf("cve signals wrong: kev=%v cvss=%v", n.IsKEV, n.MaxCVSS)
|
|
}
|
|
if n.Sources != 2 {
|
|
t.Errorf("distinct sources = %d, want 2", n.Sources)
|
|
}
|
|
if len(n.CVEs) != 1 || n.CVEs[0] != "CVE-2026-1" {
|
|
t.Errorf("cves wrong: %v", n.CVEs)
|
|
}
|
|
if n.Score != 0.77 {
|
|
t.Errorf("score = %v, want 0.77", n.Score)
|
|
}
|
|
}
|
|
|
|
func TestResolveWatchInterval(t *testing.T) {
|
|
cfg := config.Default()
|
|
watchInterval = ""
|
|
defer func() { watchInterval = "" }()
|
|
|
|
if d, err := resolveWatchInterval(cfg); err != nil || d != time.Hour {
|
|
t.Errorf("default interval = %v, %v; want 1h, nil", d, err)
|
|
}
|
|
|
|
watchInterval = "30m"
|
|
if d, err := resolveWatchInterval(cfg); err != nil || d != 30*time.Minute {
|
|
t.Errorf("flag override = %v, %v; want 30m, nil", d, err)
|
|
}
|
|
|
|
watchInterval = "5s"
|
|
if _, err := resolveWatchInterval(cfg); err == nil {
|
|
t.Error("expected an error for a sub-minimum interval")
|
|
}
|
|
|
|
watchInterval = "nonsense"
|
|
if _, err := resolveWatchInterval(cfg); err == nil {
|
|
t.Error("expected an error for a garbage interval")
|
|
}
|
|
}
|
|
|
|
func TestValidateWebhookURL(t *testing.T) {
|
|
good := []string{"https://hooks.slack.com/services/x", "http://127.0.0.1:39871/hook"}
|
|
for _, u := range good {
|
|
if err := validateWebhookURL(u); err != nil {
|
|
t.Errorf("validateWebhookURL(%q) = %v, want nil", u, err)
|
|
}
|
|
}
|
|
bad := []string{"example.com/hook", "ftp://x/y", "not a url", ""}
|
|
for _, u := range bad {
|
|
if err := validateWebhookURL(u); err == nil {
|
|
t.Errorf("validateWebhookURL(%q) = nil, want error", u)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildNotableCapsToNotifyMax(t *testing.T) {
|
|
st, err := store.Open(filepath.Join(t.TempDir(), "watch.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer st.Close()
|
|
|
|
src, err := st.UpsertSource(store.SourceInput{Name: "s", URL: "https://s/f", Type: "rss", Weight: 1, Enabled: true})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var rows []store.ClusterRow
|
|
for i := 0; i < 5; i++ {
|
|
id, err := st.InsertArticle(store.Article{
|
|
SourceID: src, CanonicalURL: fmt.Sprintf("https://s/%d", i),
|
|
ContentHash: "c" + strconv.Itoa(i), TitleHash: "t" + strconv.Itoa(i),
|
|
Title: "story " + strconv.Itoa(i), PublishedAt: 10000, FetchedAt: 10000,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rows = append(rows, store.ClusterRow{Key: strconv.Itoa(i), Members: []int64{id}, FirstSeen: 10000, LastSeen: 10000})
|
|
}
|
|
if err := st.ReplaceClusters(rows); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cfg := config.Default()
|
|
cfg.Watch.NotifyMinScore = 0
|
|
cfg.Watch.NotifyMaxItems = 2
|
|
|
|
items, err := buildNotable(st, cfg, time.Unix(9999, 0))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(items) != 2 {
|
|
t.Fatalf("buildNotable returned %d items, want the notify_max_items cap of 2", len(items))
|
|
}
|
|
}
|