From 4b22b759e7feeca1e3a22744477cc7c0936c36ad Mon Sep 17 00:00:00 2001 From: zenfun Date: Sun, 4 Jan 2026 01:45:31 +0800 Subject: [PATCH] test(service): add tests for IP ban update edge cases Add integration tests for `IPBanService.Update` to verify: - Reactivating an expired ban correctly detects overlaps with existing active bans. - Explicitly clearing the `expires_at` field (setting to null) works as expected. --- internal/service/ip_ban_test.go | 96 +++++++++++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 5 deletions(-) diff --git a/internal/service/ip_ban_test.go b/internal/service/ip_ban_test.go index 264f13d..653b457 100644 --- a/internal/service/ip_ban_test.go +++ b/internal/service/ip_ban_test.go @@ -1,7 +1,14 @@ package service import ( + "context" + "errors" "testing" + "time" + + "github.com/ez-api/ez-api/internal/model" + "gorm.io/driver/sqlite" + "gorm.io/gorm" ) func TestNormalizeCIDR_IPv4Address(t *testing.T) { @@ -127,11 +134,11 @@ func TestCIDROverlaps_HasOverlap(t *testing.T) { tests := []struct { a, b string }{ - {"10.0.0.0/24", "10.0.0.0/24"}, // Exact same - {"10.0.0.0/24", "10.0.0.0/25"}, // a contains b - {"10.0.0.0/25", "10.0.0.0/24"}, // b contains a - {"10.0.0.0/16", "10.0.1.0/24"}, // a contains b - {"10.0.0.1/32", "10.0.0.0/24"}, // b contains a + {"10.0.0.0/24", "10.0.0.0/24"}, // Exact same + {"10.0.0.0/24", "10.0.0.0/25"}, // a contains b + {"10.0.0.0/25", "10.0.0.0/24"}, // b contains a + {"10.0.0.0/16", "10.0.1.0/24"}, // a contains b + {"10.0.0.1/32", "10.0.0.0/24"}, // b contains a } for _, tt := range tests { @@ -150,3 +157,82 @@ func TestCIDROverlaps_InvalidCIDR(t *testing.T) { t.Error("expected no overlap with invalid CIDR") } } + +func TestIPBanService_Update_ReactivateOverlap(t *testing.T) { + t.Parallel() + + db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{}) + if err != nil { + t.Fatalf("open sqlite: %v", err) + } + if err := db.AutoMigrate(&model.IPBan{}); err != nil { + t.Fatalf("migrate: %v", err) + } + + active := model.IPBan{CIDR: "10.0.0.0/24", Status: model.IPBanStatusActive} + if err := db.Create(&active).Error; err != nil { + t.Fatalf("create active ban: %v", err) + } + + expired := model.IPBan{CIDR: "10.0.0.128/25", Status: model.IPBanStatusExpired} + if err := db.Create(&expired).Error; err != nil { + t.Fatalf("create expired ban: %v", err) + } + + svc := NewIPBanService(db, nil) + _, err = svc.Update(context.Background(), expired.ID, UpdateIPBanRequest{ + Status: func() *string { + s := model.IPBanStatusActive + return &s + }(), + }) + if !errors.Is(err, ErrCIDROverlap) { + t.Fatalf("expected overlap error, got %v", err) + } + + var reloaded model.IPBan + if err := db.First(&reloaded, expired.ID).Error; err != nil { + t.Fatalf("reload expired ban: %v", err) + } + if reloaded.Status != model.IPBanStatusExpired { + t.Fatalf("expected status to remain expired, got %s", reloaded.Status) + } +} + +func TestIPBanService_Update_ClearExpiresAt(t *testing.T) { + t.Parallel() + + db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{}) + if err != nil { + t.Fatalf("open sqlite: %v", err) + } + if err := db.AutoMigrate(&model.IPBan{}); err != nil { + t.Fatalf("migrate: %v", err) + } + + exp := time.Now().Add(time.Hour).Unix() + ban := model.IPBan{ + CIDR: "192.168.1.1/32", + Status: model.IPBanStatusActive, + ExpiresAt: &exp, + } + if err := db.Create(&ban).Error; err != nil { + t.Fatalf("create ban: %v", err) + } + + svc := NewIPBanService(db, nil) + if _, err := svc.Update(context.Background(), ban.ID, UpdateIPBanRequest{ + ExpiresAt: nil, + ExpiresAtSet: true, + }); err != nil { + t.Fatalf("update expires_at: %v", err) + } + + var reloaded model.IPBan + if err := db.First(&reloaded, ban.ID).Error; err != nil { + t.Fatalf("reload ban: %v", err) + } + if reloaded.ExpiresAt != nil { + t.Fatalf("expected expires_at to be cleared, got %v", *reloaded.ExpiresAt) + } +}