From f7162aa34a4a710ec74815c604574dd38a977098 Mon Sep 17 00:00:00 2001 From: salvacybersec Date: Mon, 6 Apr 2026 17:32:58 +0300 Subject: [PATCH] test(17-04): add failing tests for notification dispatcher - TestSubscribeUnsubscribe: DB round-trip for add/remove subscriber - TestNotifyNewFindings_NoSubscribers: zero messages with empty table - TestNotifyNewFindings_ZeroFindings: no notification for 0 findings - TestFormatNotification: message contains job name, count, duration - TestFormatFindingNotification: masked key, never full key Co-Authored-By: Claude Opus 4.6 (1M context) --- pkg/bot/subscribe_test.go | 121 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 pkg/bot/subscribe_test.go diff --git a/pkg/bot/subscribe_test.go b/pkg/bot/subscribe_test.go new file mode 100644 index 0000000..9bc4ed8 --- /dev/null +++ b/pkg/bot/subscribe_test.go @@ -0,0 +1,121 @@ +package bot + +import ( + "testing" + "time" + + "github.com/salvacybersec/keyhunter/pkg/engine" + "github.com/salvacybersec/keyhunter/pkg/scheduler" + "github.com/salvacybersec/keyhunter/pkg/storage" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func openTestDB(t *testing.T) *storage.DB { + t.Helper() + db, err := storage.Open(":memory:") + require.NoError(t, err) + t.Cleanup(func() { _ = db.Close() }) + return db +} + +func TestSubscribeUnsubscribe(t *testing.T) { + db := openTestDB(t) + + // Initially not subscribed. + ok, err := db.IsSubscribed(12345) + require.NoError(t, err) + assert.False(t, ok, "should not be subscribed initially") + + // Subscribe. + err = db.AddSubscriber(12345, "testuser") + require.NoError(t, err) + + ok, err = db.IsSubscribed(12345) + require.NoError(t, err) + assert.True(t, ok, "should be subscribed after AddSubscriber") + + // Unsubscribe. + rows, err := db.RemoveSubscriber(12345) + require.NoError(t, err) + assert.Equal(t, int64(1), rows, "should have removed 1 row") + + ok, err = db.IsSubscribed(12345) + require.NoError(t, err) + assert.False(t, ok, "should not be subscribed after RemoveSubscriber") + + // Unsubscribe again returns 0 rows. + rows, err = db.RemoveSubscriber(12345) + require.NoError(t, err) + assert.Equal(t, int64(0), rows, "should have removed 0 rows when not subscribed") +} + +func TestNotifyNewFindings_NoSubscribers(t *testing.T) { + db := openTestDB(t) + + b := &Bot{cfg: Config{DB: db}} + sent, errs := b.NotifyNewFindings(scheduler.JobResult{ + JobName: "nightly-scan", + FindingCount: 5, + Duration: 10 * time.Second, + }) + assert.Equal(t, 0, sent, "should send 0 messages with no subscribers") + assert.Empty(t, errs, "should have no errors with no subscribers") +} + +func TestNotifyNewFindings_ZeroFindings(t *testing.T) { + db := openTestDB(t) + _ = db.AddSubscriber(12345, "user1") + + b := &Bot{cfg: Config{DB: db}} + sent, errs := b.NotifyNewFindings(scheduler.JobResult{ + JobName: "nightly-scan", + FindingCount: 0, + Duration: 3 * time.Second, + }) + assert.Equal(t, 0, sent, "should not notify for zero findings") + assert.Empty(t, errs, "should have no errors for zero findings") +} + +func TestFormatNotification(t *testing.T) { + result := scheduler.JobResult{ + JobName: "nightly-scan", + FindingCount: 7, + Duration: 2*time.Minute + 30*time.Second, + } + msg := formatNotification(result) + assert.Contains(t, msg, "nightly-scan", "message should contain job name") + assert.Contains(t, msg, "7", "message should contain finding count") + assert.Contains(t, msg, "2m30s", "message should contain duration") + assert.Contains(t, msg, "/stats", "message should reference /stats command") +} + +func TestFormatNotification_Error(t *testing.T) { + result := scheduler.JobResult{ + JobName: "daily-scan", + FindingCount: 0, + Duration: 5 * time.Second, + Error: assert.AnError, + } + msg := formatErrorNotification(result) + assert.Contains(t, msg, "daily-scan", "error message should contain job name") + assert.Contains(t, msg, "error", "error message should indicate error") +} + +func TestFormatFindingNotification(t *testing.T) { + finding := engine.Finding{ + ProviderName: "OpenAI", + KeyValue: "sk-proj-1234567890abcdef", + KeyMasked: "sk-proj-...cdef", + Confidence: "high", + Source: "/tmp/test.py", + LineNumber: 42, + } + msg := formatFindingNotification(finding) + assert.Contains(t, msg, "OpenAI", "should contain provider name") + assert.Contains(t, msg, "sk-proj-...cdef", "should contain masked key") + assert.NotContains(t, msg, "sk-proj-1234567890abcdef", "should NOT contain full key") + assert.Contains(t, msg, "/tmp/test.py", "should contain source path") + assert.Contains(t, msg, "42", "should contain line number") + assert.Contains(t, msg, "high", "should contain confidence") +}