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") }