// Copyright 2026 tamakyi. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package db import ( "context" "testing" "time" "github.com/stretchr/testify/require" ) // TestHoneypotRetentionRunOnce 覆盖蜜罐记录的两道清理闸: // 时间闸(保留期)与数量闸(行数上限),以及清理后的空间回收。 // // 这是「数据库会不会被扫描撑爆」的核心防线:蜜罐表只增不减, // 写入完全由外部扫描流量驱动。 func TestHoneypotRetentionRunOnce(t *testing.T) { database, cleanup := newTestDB(t) ctx := context.Background() t.Cleanup(func() { if err := cleanup("honeypot_hits"); err != nil { t.Fatal(err) } }) store := NewHoneypotStore(database) now := time.Now() // 5 条旧记录(超出保留期)+ 5 条新记录(在保留期内) rows := []*HoneypotHit{} for i := 0; i < 5; i++ { rows = append(rows, &HoneypotHit{IP: "1.1.1.1", Method: "GET", Path: "/old", CreatedAt: now.Add(-90 * 24 * time.Hour)}) rows = append(rows, &HoneypotHit{IP: "2.2.2.2", Method: "GET", Path: "/new", CreatedAt: now.Add(-time.Hour)}) } for _, r := range rows { require.NoError(t, store.Create(ctx, r)) } total, err := store.CountSince(ctx, now.Add(-365*24*time.Hour)) require.NoError(t, err) require.Equal(t, int64(10), total) // 保留期 30 天、行数上限 7: // 时间闸删掉 5 条 90 天前的旧记录 → 剩 5 条新的; // 数量闸:5 < 7,未超上限,不删 → 最终剩 5 条。 ret := NewHoneypotRetention(store, 30, 7, 0) ret.runOnce() remaining, err := store.CountSince(ctx, now.Add(-365*24*time.Hour)) require.NoError(t, err) require.Equal(t, int64(5), remaining, "时间闸删掉 5 条旧记录后剩 5 条,未超数量上限") // 剩下的应该全是新记录 list, err := store.ListSince(ctx, now.Add(-365*24*time.Hour), 10) require.NoError(t, err) require.Len(t, list, 5) for _, r := range list { require.Equal(t, "/new", r.Path, "旧的记录应已被时间闸删除") } } // TestHoneypotRetentionDisabled 保留期 <= 0 时不删任何数据。 func TestHoneypotRetentionDisabled(t *testing.T) { database, cleanup := newTestDB(t) ctx := context.Background() t.Cleanup(func() { if err := cleanup("honeypot_hits"); err != nil { t.Fatal(err) } }) store := NewHoneypotStore(database) now := time.Now() for i := 0; i < 3; i++ { require.NoError(t, store.Create(ctx, &HoneypotHit{ IP: "3.3.3.3", Method: "GET", Path: "/x", CreatedAt: now.Add(-365 * 24 * time.Hour), })) } ret := NewHoneypotRetention(store, 0, 100, 0) ret.runOnce() total, err := store.CountSince(ctx, now.Add(-365*24*time.Hour)) require.NoError(t, err) require.Equal(t, int64(3), total, "保留期 <= 0 时不应删除任何记录") } // TestHoneypotRetentionMaxRowsOnly 只设行数上限、不设保留期时,数量闸仍生效。 func TestHoneypotRetentionMaxRowsOnly(t *testing.T) { database, cleanup := newTestDB(t) ctx := context.Background() t.Cleanup(func() { if err := cleanup("honeypot_hits"); err != nil { t.Fatal(err) } }) store := NewHoneypotStore(database) now := time.Now() for i := 0; i < 20; i++ { require.NoError(t, store.Create(ctx, &HoneypotHit{ IP: "4.4.4.4", Method: "GET", Path: "/p", CreatedAt: now.Add(-time.Duration(i) * time.Minute), })) } ret := NewHoneypotRetention(store, 30, 5, 0) ret.runOnce() total, err := store.CountSince(ctx, now.Add(-365*24*time.Hour)) require.NoError(t, err) require.Equal(t, int64(5), total) } // TestHoneypotRetentionBothGates 两道闸叠加:时间闸先删超期的, // 剩余仍超数量上限时,数量闸再删最旧的。 func TestHoneypotRetentionBothGates(t *testing.T) { database, cleanup := newTestDB(t) ctx := context.Background() t.Cleanup(func() { if err := cleanup("honeypot_hits"); err != nil { t.Fatal(err) } }) store := NewHoneypotStore(database) now := time.Now() // 5 条超保留期(90 天前)+ 10 条保留期内 for i := 0; i < 5; i++ { require.NoError(t, store.Create(ctx, &HoneypotHit{ IP: "6.6.6.6", Method: "GET", Path: "/ancient", CreatedAt: now.Add(-90 * 24 * time.Hour), })) } for i := 0; i < 10; i++ { require.NoError(t, store.Create(ctx, &HoneypotHit{ IP: "7.7.7.7", Method: "GET", Path: "/recent", CreatedAt: now.Add(-time.Duration(i) * time.Minute), })) } // 保留期 30 天、数量上限 4: // 时间闸删 5 条 ancient → 剩 10 条 recent;数量闸删到 4 条(删最旧的 6 条) ret := NewHoneypotRetention(store, 30, 4, 0) ret.runOnce() total, err := store.CountSince(ctx, now.Add(-365*24*time.Hour)) require.NoError(t, err) require.Equal(t, int64(4), total, "两道闸叠加后应回到数量上限") list, err := store.ListSince(ctx, now.Add(-365*24*time.Hour), 10) require.NoError(t, err) require.Len(t, list, 4) for _, r := range list { require.Equal(t, "/recent", r.Path) } // 留下的应是最新的 4 条(now-0m ~ now-3m),最旧的应已被数量闸删掉 require.Contains(t, list[0].Path, "recent") } // TestHoneypotDeleteOldestBeyond 直接测数量闸的删除语义。 func TestHoneypotDeleteOldestBeyond(t *testing.T) { database, cleanup := newTestDB(t) ctx := context.Background() t.Cleanup(func() { if err := cleanup("honeypot_hits"); err != nil { t.Fatal(err) } }) store := NewHoneypotStore(database) now := time.Now() for i := 0; i < 10; i++ { require.NoError(t, store.Create(ctx, &HoneypotHit{ IP: "7.7.7.7", Method: "GET", Path: "/p", CreatedAt: now.Add(-time.Duration(i) * time.Minute), })) } // 保留 3 条 → 删掉最旧的 7 条 n, err := store.DeleteOldestBeyond(ctx, 3) require.NoError(t, err) require.Equal(t, int64(7), n) total, err := store.CountSince(ctx, now.Add(-24*time.Hour)) require.NoError(t, err) require.Equal(t, int64(3), total) // 行数不足时不应删任何东西 n2, err := store.DeleteOldestBeyond(ctx, 100) require.NoError(t, err) require.Equal(t, int64(0), n2) total2, err := store.CountSince(ctx, now.Add(-24*time.Hour)) require.NoError(t, err) require.Equal(t, int64(3), total2) // keepRows <= 0 视为不限制 n3, err := store.DeleteOldestBeyond(ctx, 0) require.NoError(t, err) require.Equal(t, int64(0), n3) } // TestHoneypotListSinceByIP 验证按 IP 过滤在 SQL 层生效。 // // 背景:原先 CLI 的 --ip 过滤是在取回最近 N 条后在内存里筛, // 导致「指定 IP 的历史命中」基本查不出来(被其他 IP 的更近记录挤出窗口)。 func TestHoneypotListSinceByIP(t *testing.T) { database, cleanup := newTestDB(t) ctx := context.Background() t.Cleanup(func() { if err := cleanup("honeypot_hits"); err != nil { t.Fatal(err) } }) store := NewHoneypotStore(database) now := time.Now() // 目标 IP 只有 1 条很老的记录;其余 20 条都是别的 IP 且更新 require.NoError(t, store.Create(ctx, &HoneypotHit{ IP: "9.9.9.9", Method: "GET", Path: "/target", CreatedAt: now.Add(-10 * time.Hour), })) for i := 0; i < 20; i++ { require.NoError(t, store.Create(ctx, &HoneypotHit{ IP: "8.8.8.8", Method: "GET", Path: "/other", CreatedAt: now.Add(-time.Duration(i) * time.Minute), })) } rows, err := store.ListSinceByIP(ctx, now.Add(-24*time.Hour), "9.9.9.9", 200) require.NoError(t, err) require.Len(t, rows, 1, "指定 IP 的老记录应能被查出(不能被其他 IP 的新记录挤出窗口)") require.Equal(t, "/target", rows[0].Path) // 空 IP 退化为不过滤 all, err := store.ListSinceByIP(ctx, now.Add(-24*time.Hour), "", 200) require.NoError(t, err) require.Len(t, all, 21) } // TestHoneypotCompact 验证空间回收在 PostgreSQL 下可执行且不报错。 func TestHoneypotCompact(t *testing.T) { database, cleanup := newTestDB(t) ctx := context.Background() t.Cleanup(func() { if err := cleanup("honeypot_hits"); err != nil { t.Fatal(err) } }) store := NewHoneypotStore(database) now := time.Now() for i := 0; i < 10; i++ { require.NoError(t, store.Create(ctx, &HoneypotHit{ IP: "5.5.5.5", Method: "GET", Path: "/p", CreatedAt: now.Add(-time.Duration(i) * time.Hour), })) } n, err := store.DeleteBefore(ctx, now.Add(-2*time.Hour)) require.NoError(t, err) // 10 条记录分别落在 now-0h … now-9h;created_at < now-2h 是严格小于, // 恰好落在 now-2h 的那条保留 → 删掉 now-3h 起的 7 条。 require.Equal(t, int64(7), n) require.NoError(t, store.Compact(ctx), "空间回收不应报错") total, err := store.CountSince(ctx, now.Add(-24*time.Hour)) require.NoError(t, err) require.Equal(t, int64(3), total, "回收空间不应影响数据正确性") }