Cybersecurity-Projects/PROJECTS/intermediate/security-news-scraper/cmd/nadezhda/watch_test.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))
}
}