package services import ( "log/slog" "path/filepath" "testing" "time" ) func TestBanServiceMatchesIPAndCIDR(t *testing.T) { bans := newTestBanService(t) now := time.Date(2026, 5, 31, 12, 0, 0, 0, time.UTC) ipBan, err := bans.createBan("203.0.113.5", "single IP", BanSourceManual, "test", now.Add(time.Hour), now) if err != nil { t.Fatalf("createBan IP returned error: %v", err) } cidrBan, err := bans.createBan("198.51.100.0/24", "CIDR", BanSourceManual, "test", now.Add(time.Hour), now) if err != nil { t.Fatalf("createBan CIDR returned error: %v", err) } if matched, ok, err := bans.Match("203.0.113.5", now); err != nil || !ok || matched.Ban.ID != ipBan.ID { t.Fatalf("Match IP = %+v, %v, %v", matched, ok, err) } if matched, ok, err := bans.Match("198.51.100.42", now); err != nil || !ok || matched.Ban.ID != cidrBan.ID { t.Fatalf("Match CIDR = %+v, %v, %v", matched, ok, err) } if _, ok, err := bans.Match("192.0.2.1", now); err != nil || ok { t.Fatalf("Match unrelated = %v, %v", ok, err) } } func TestBanServiceIgnoresExpiredAndUnbanned(t *testing.T) { bans := newTestBanService(t) now := time.Date(2026, 5, 31, 12, 0, 0, 0, time.UTC) expired, err := bans.createBan("203.0.113.6", "expired", BanSourceManual, "test", now.Add(time.Hour), now) if err != nil { t.Fatalf("createBan expired returned error: %v", err) } if _, ok, err := bans.Match("203.0.113.6", now.Add(2*time.Hour)); err != nil || ok { t.Fatalf("expired Match = %v, %v", ok, err) } active, err := bans.createBan("203.0.113.7", "active", BanSourceManual, "test", now.Add(time.Hour), now) if err != nil { t.Fatalf("createBan active returned error: %v", err) } if err := bans.Unban(active.ID, now.Add(time.Minute)); err != nil { t.Fatalf("Unban returned error: %v", err) } if _, ok, err := bans.Match("203.0.113.7", now.Add(2*time.Minute)); err != nil || ok { t.Fatalf("unbanned Match = %v, %v", ok, err) } if expired.Status(now.Add(2*time.Hour)) != "expired" { t.Fatalf("expired status = %q", expired.Status(now.Add(2*time.Hour))) } } func TestBanServiceAutoBanThresholdsAndDisabled(t *testing.T) { bans := newTestBanService(t) now := time.Date(2026, 5, 31, 12, 0, 0, 0, time.UTC) if result, err := bans.RecordAbuse("203.0.113.8", AbuseKindMaliciousPath, "/.env", 3, now); err != nil || result.Enabled { t.Fatalf("disabled RecordAbuse = %+v, %v", result, err) } settings, err := bans.Settings() if err != nil { t.Fatalf("Settings returned error: %v", err) } settings.AutoBanEnabled = true if err := bans.UpdateSettings(settings); err != nil { t.Fatalf("UpdateSettings returned error: %v", err) } for i := 0; i < 2; i++ { result, err := bans.RecordAbuse("203.0.113.8", AbuseKindMaliciousPath, "/.env", 3, now.Add(time.Duration(i)*time.Minute)) if err != nil || result.Triggered { t.Fatalf("RecordAbuse before threshold = %+v, %v", result, err) } } result, err := bans.RecordAbuse("203.0.113.8", AbuseKindMaliciousPath, "/.env", 3, now.Add(3*time.Minute)) if err != nil || !result.Triggered || result.Ban.ID == "" { t.Fatalf("RecordAbuse threshold = %+v, %v", result, err) } again, err := bans.RecordAbuse("203.0.113.8", AbuseKindMaliciousPath, "/.env", 3, now.Add(4*time.Minute)) if err != nil || !again.Triggered || again.Ban.ID != result.Ban.ID { t.Fatalf("RecordAbuse duplicate = %+v, %v", again, err) } records, err := bans.ListBans() if err != nil { t.Fatalf("ListBans returned error: %v", err) } if len(records) != 1 { t.Fatalf("ban count = %d, want 1", len(records)) } } func TestBanServiceMaliciousPathRules(t *testing.T) { bans := newTestBanService(t) if pattern, err := bans.MaliciousPattern("/foo/.ENV"); err != nil || pattern == "" { t.Fatalf("MaliciousPattern .env = %q, %v", pattern, err) } if pattern, err := bans.MaliciousPattern("/static/.env"); err != nil || pattern != "" { t.Fatalf("MaliciousPattern static = %q, %v", pattern, err) } if err := bans.SaveRules([]string{"/custom-probe"}, time.Now().UTC()); err != nil { t.Fatalf("SaveRules returned error: %v", err) } if pattern, err := bans.MaliciousPattern("/x/CUSTOM-probe"); err != nil || pattern != "/custom-probe" { t.Fatalf("MaliciousPattern custom = %q, %v", pattern, err) } } func newTestBanService(t *testing.T) *BanService { t.Helper() root := t.TempDir() upload, err := NewUploadService(1024*1024, filepath.Join(root, "data"), "http://example.test", slog.Default()) if err != nil { t.Fatalf("NewUploadService returned error: %v", err) } t.Cleanup(func() { if err := upload.Close(); err != nil { t.Fatalf("Close returned error: %v", err) } }) bans, err := NewBanService(upload.DB()) if err != nil { t.Fatalf("NewBanService returned error: %v", err) } return bans }