Files
notifier/tests/e2e/critical_1_test.go
T
igodwin eda033ff9b
CI / Lint (push) Successful in 2m29s
Build and Publish Container / build-and-publish (push) Successful in 2m58s
CI / Vulnerability scan (push) Successful in 44s
CI / Test (push) Successful in 1m45s
fix: clear golangci-lint backlog and make lint job blocking
Addresses errcheck, gosec, revive, staticcheck, and unused findings
across the codebase (unchecked error returns, unsafe file inclusion
warnings on operator/test-controlled paths, missing package comments,
unused parameters, deprecated API usage). Also fixes two suppression
comments that were silently no-ops due to wrong syntax (#nosec needs
a leading '#', nolint reasons need '//' not '--').

With the backlog clear, drop continue-on-error from the CI lint job
per the plan left in b4b4806.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 10:32:51 -07:00

416 lines
12 KiB
Go

package e2e
import (
"context"
"fmt"
"testing"
"time"
"github.com/igodwin/notifier/pkg/client"
)
// TestCRITICAL1_TTLBasedCleanup verifies notifications older than TTL are removed
func TestCRITICAL1_TTLBasedCleanup(t *testing.T) {
if testing.Short() {
t.Skip("Skipping E2E test in short mode")
}
// Use very short TTL for faster testing
retention := "NOTIFIER_RETENTION_ENABLED=true"
retention += "|NOTIFIER_RETENTION_TTL=2s"
retention += "|NOTIFIER_RETENTION_CHECK_FREQUENCY=500ms"
retention += "|NOTIFIER_RETENTION_MAX_SIZE=10000"
suite := SetupSuite(t, retention)
defer suite.TeardownSuite(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Send a notification that will have old timestamp
req := client.NotificationRequest{
Type: "stdout",
Subject: "Test TTL",
Body: "This should be cleaned up",
Recipients: []string{"test@example.com"},
}
resp, err := suite.Client.Send(ctx, req)
if err != nil {
t.Fatalf("Failed to send notification: %v", err)
}
// Verify notification exists
notif, err := suite.Client.GetNotification(ctx, resp.NotificationID)
if err != nil {
t.Fatalf("Failed to get notification: %v", err)
}
t.Logf("Created notification %s at %v", resp.NotificationID, notif.CreatedAt)
// Get initial stats
stats1, err := suite.Client.GetStats(ctx)
if err != nil {
t.Fatalf("Failed to get stats: %v", err)
}
t.Logf("Initial stats: total_sent=%d", stats1.TotalSent)
// Wait for TTL + cleanup check frequency
time.Sleep(3 * time.Second)
// Get stats after cleanup
stats2, err := suite.Client.GetStats(ctx)
if err != nil {
t.Fatalf("Failed to get stats: %v", err)
}
t.Logf("After cleanup stats: total_sent=%d", stats2.TotalSent)
// Verify the notification was cleaned up
if stats2.TotalSent >= stats1.TotalSent {
t.Logf("Container logs:\n%s", suite.GetLogs(ctx))
t.Fatalf("Expected cleanup to remove old notification, but total_sent remained %d >= %d",
stats2.TotalSent, stats1.TotalSent)
}
t.Logf("✓ TTL-based cleanup verified: %d -> %d notifications", stats1.TotalSent, stats2.TotalSent)
}
// TestCRITICAL1_MaxSizeEnforcement verifies max_size limit is enforced
func TestCRITICAL1_MaxSizeEnforcement(t *testing.T) {
if testing.Short() {
t.Skip("Skipping E2E test in short mode")
}
// Use small max_size for testing
retention := "NOTIFIER_RETENTION_ENABLED=true"
retention += "|NOTIFIER_RETENTION_TTL=24h"
retention += "|NOTIFIER_RETENTION_CHECK_FREQUENCY=500ms"
retention += "|NOTIFIER_RETENTION_MAX_SIZE=5"
suite := SetupSuite(t, retention)
defer suite.TeardownSuite(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Send more notifications than max_size
notificationCount := 10
for i := 0; i < notificationCount; i++ {
req := client.NotificationRequest{
Type: "stdout",
Subject: fmt.Sprintf("Test %d", i),
Body: fmt.Sprintf("Notification %d", i),
Recipients: []string{"test@example.com"},
}
_, err := suite.Client.Send(ctx, req)
if err != nil {
t.Fatalf("Failed to send notification %d: %v", i, err)
}
}
t.Logf("Sent %d notifications", notificationCount)
// Get stats before cleanup
statsBeforeCleanup, err := suite.Client.GetStats(ctx)
if err != nil {
t.Fatalf("Failed to get stats: %v", err)
}
t.Logf("Before cleanup: total_sent=%d", statsBeforeCleanup.TotalSent)
// Wait for cleanup to enforce max_size
time.Sleep(2 * time.Second)
// Get stats after cleanup
statsAfterCleanup, err := suite.Client.GetStats(ctx)
if err != nil {
t.Fatalf("Failed to get stats: %v", err)
}
t.Logf("After cleanup: total_sent=%d (max_size=5)", statsAfterCleanup.TotalSent)
// Verify max_size is enforced
if statsAfterCleanup.TotalSent > 5 {
t.Logf("Container logs:\n%s", suite.GetLogs(ctx))
t.Fatalf("Expected max_size enforcement, but have %d notifications (max=5)",
statsAfterCleanup.TotalSent)
}
t.Logf("✓ Max size enforcement verified: capped at %d", statsAfterCleanup.TotalSent)
}
// TestCRITICAL1_CleanupDisabled verifies cleanup doesn't run when disabled
func TestCRITICAL1_CleanupDisabled(t *testing.T) {
if testing.Short() {
t.Skip("Skipping E2E test in short mode")
}
// Disable cleanup
retention := "NOTIFIER_RETENTION_ENABLED=false"
suite := SetupSuite(t, retention)
defer suite.TeardownSuite(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Send many notifications
notificationCount := 10
for i := 0; i < notificationCount; i++ {
req := client.NotificationRequest{
Type: "stdout",
Subject: fmt.Sprintf("Test %d", i),
Body: fmt.Sprintf("Notification %d", i),
Recipients: []string{"test@example.com"},
}
_, err := suite.Client.Send(ctx, req)
if err != nil {
t.Fatalf("Failed to send notification: %v", err)
}
}
// Get stats before waiting
statsBefore, err := suite.Client.GetStats(ctx)
if err != nil {
t.Fatalf("Failed to get stats: %v", err)
}
t.Logf("Before wait: total_sent=%d", statsBefore.TotalSent)
// Wait a bit (cleanup should not run)
time.Sleep(3 * time.Second)
// Get stats after waiting
statsAfter, err := suite.Client.GetStats(ctx)
if err != nil {
t.Fatalf("Failed to get stats: %v", err)
}
t.Logf("After wait: total_sent=%d", statsAfter.TotalSent)
// Verify notifications are still there (no cleanup)
if statsAfter.TotalSent < statsBefore.TotalSent {
t.Fatalf("Expected no cleanup when disabled, but notification count decreased from %d to %d",
statsBefore.TotalSent, statsAfter.TotalSent)
}
t.Logf("✓ Cleanup disabled verified: all %d notifications retained", statsAfter.TotalSent)
}
// TestCRITICAL1_ConcurrentSends verifies concurrent client access works correctly
func TestCRITICAL1_ConcurrentSends(t *testing.T) {
if testing.Short() {
t.Skip("Skipping E2E test in short mode")
}
retention := "NOTIFIER_RETENTION_ENABLED=true"
retention += "|NOTIFIER_RETENTION_TTL=24h"
retention += "|NOTIFIER_RETENTION_CHECK_FREQUENCY=1s"
retention += "|NOTIFIER_RETENTION_MAX_SIZE=100"
suite := SetupSuite(t, retention)
defer suite.TeardownSuite(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Send notifications concurrently
errChan := make(chan error, 10)
for i := 0; i < 10; i++ {
go func(idx int) {
req := client.NotificationRequest{
Type: "stdout",
Subject: fmt.Sprintf("Concurrent %d", idx),
Body: fmt.Sprintf("Test %d", idx),
Recipients: []string{"test@example.com"},
}
_, err := suite.Client.Send(ctx, req)
errChan <- err
}(i)
}
// Collect results
for i := 0; i < 10; i++ {
if err := <-errChan; err != nil {
t.Fatalf("Failed to send concurrent notification %d: %v", i, err)
}
}
// Get stats
stats, err := suite.Client.GetStats(ctx)
if err != nil {
t.Fatalf("Failed to get stats: %v", err)
}
if stats.TotalSent != 10 {
t.Fatalf("Expected 10 notifications, got %d", stats.TotalSent)
}
t.Logf("✓ Concurrent sends verified: %d notifications sent successfully", stats.TotalSent)
}
// TestCRITICAL1_OldestRemovedFirst verifies oldest notifications are removed when max_size exceeded
func TestCRITICAL1_OldestRemovedFirst(t *testing.T) {
if testing.Short() {
t.Skip("Skipping E2E test in short mode")
}
retention := "NOTIFIER_RETENTION_ENABLED=true"
retention += "|NOTIFIER_RETENTION_TTL=24h"
retention += "|NOTIFIER_RETENTION_CHECK_FREQUENCY=500ms"
retention += "|NOTIFIER_RETENTION_MAX_SIZE=3"
suite := SetupSuite(t, retention)
defer suite.TeardownSuite(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Send 5 notifications with time between them
notificationIDs := make([]string, 5)
for i := 0; i < 5; i++ {
req := client.NotificationRequest{
Type: "stdout",
Subject: fmt.Sprintf("Oldest %d", i),
Body: fmt.Sprintf("Test %d", i),
Recipients: []string{"test@example.com"},
}
resp, err := suite.Client.Send(ctx, req)
if err != nil {
t.Fatalf("Failed to send notification: %v", err)
}
notificationIDs[i] = resp.NotificationID
time.Sleep(100 * time.Millisecond)
}
t.Logf("Sent 5 notifications: %v", notificationIDs[:5])
// Wait for cleanup
time.Sleep(2 * time.Second)
// Verify that only 3 remain (and they're the newest)
stats, err := suite.Client.GetStats(ctx)
if err != nil {
t.Fatalf("Failed to get stats: %v", err)
}
if stats.TotalSent != 3 {
t.Logf("Container logs:\n%s", suite.GetLogs(ctx))
t.Fatalf("Expected 3 notifications after cleanup, got %d", stats.TotalSent)
}
t.Logf("✓ Oldest removed first verified: 5 notifications -> %d (max_size)", stats.TotalSent)
}
// TestCRITICAL1_MemoryBounded verifies memory usage stays bounded
func TestCRITICAL1_MemoryBounded(t *testing.T) {
if testing.Short() {
t.Skip("Skipping E2E test in short mode")
}
retention := "NOTIFIER_RETENTION_ENABLED=true"
retention += "|NOTIFIER_RETENTION_TTL=5s"
retention += "|NOTIFIER_RETENTION_CHECK_FREQUENCY=500ms"
retention += "|NOTIFIER_RETENTION_MAX_SIZE=50"
suite := SetupSuite(t, retention)
defer suite.TeardownSuite(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
// Send bursts of notifications repeatedly
for batch := 0; batch < 3; batch++ {
// Send 30 notifications
for i := 0; i < 30; i++ {
req := client.NotificationRequest{
Type: "stdout",
Subject: fmt.Sprintf("Batch %d Notif %d", batch, i),
Body: "Test data for notification",
Recipients: []string{"test@example.com"},
}
_, err := suite.Client.Send(ctx, req)
if err != nil {
t.Fatalf("Failed to send notification: %v", err)
}
}
// Wait for cleanup to run
time.Sleep(2 * time.Second)
// Check stats - should still be under max_size
stats, err := suite.Client.GetStats(ctx)
if err != nil {
t.Fatalf("Failed to get stats: %v", err)
}
if stats.TotalSent > 50 {
t.Logf("Container logs:\n%s", suite.GetLogs(ctx))
t.Fatalf("Batch %d: notification count %d exceeded max_size of 50", batch, stats.TotalSent)
}
t.Logf("Batch %d: %d notifications (within bound)", batch, stats.TotalSent)
}
t.Logf("✓ Memory bounded verified: multiple batches stayed within max_size limit")
}
// TestCRITICAL1_ServiceHealthy verifies service stays healthy throughout cleanup
func TestCRITICAL1_ServiceHealthy(t *testing.T) {
if testing.Short() {
t.Skip("Skipping E2E test in short mode")
}
retention := "NOTIFIER_RETENTION_ENABLED=true"
retention += "|NOTIFIER_RETENTION_TTL=2s"
retention += "|NOTIFIER_RETENTION_CHECK_FREQUENCY=500ms"
retention += "|NOTIFIER_RETENTION_MAX_SIZE=1000"
suite := SetupSuite(t, retention)
defer suite.TeardownSuite(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Send notifications and check health periodically
for iteration := 0; iteration < 5; iteration++ {
// Send 20 notifications
for i := 0; i < 20; i++ {
req := client.NotificationRequest{
Type: "stdout",
Subject: fmt.Sprintf("Health %d", i),
Body: "Test",
Recipients: []string{"test@example.com"},
}
_, err := suite.Client.Send(ctx, req)
if err != nil {
t.Fatalf("Failed to send notification: %v", err)
}
}
// Check health
healthy, err := suite.Client.HealthCheck(ctx)
if err != nil {
t.Fatalf("Iteration %d: health check failed: %v", iteration, err)
}
if !healthy {
t.Fatalf("Iteration %d: service reported unhealthy", iteration)
}
// Get stats to verify service is responsive
stats, err := suite.Client.GetStats(ctx)
if err != nil {
t.Fatalf("Iteration %d: failed to get stats: %v", iteration, err)
}
t.Logf("Iteration %d: health=ok, stats received (total_sent=%d)", iteration, stats.TotalSent)
time.Sleep(1 * time.Second)
}
t.Logf("✓ Service health verified: remained responsive during cleanup cycles")
}