Files
mailgo/internal/store/protocol_log_store_test.go
kevin 353bfa88f2 feat: 新增 SMTP/IMAP/POP3 协议调用日志(含攻击分析筛选)
- 每个连接记录一条日志:协议、端口、来源 IP、用户名、成功/失败、
  失败原因(密码错误/IP封禁/中继被拒/发件人伪造/未认证发信等)、
  操作摘要、消息数与会话时长
- 管理后台新增「协议日志」页:按协议/状态/IP/用户名/时间筛选,
  今日与历史成功/失败统计卡片,分页查看,可手动清理
- 后台每 6 小时自动清理超出 protocol_log_keep_days(默认30天)
  的日志;新增 [web] protocol_log_keep_days 配置项
- 修复 POP3 认证既有 bug:handleUSER 丢弃邮箱域名导致 PASS 永远失败
- 新增 store 单测、SMTP/POP3 端到端测试与模板渲染测试
2026-08-19 18:49:29 +08:00

172 lines
5.1 KiB
Go

package store
import (
"testing"
"time"
"mail_go/internal/db"
)
func TestProtocolLogCreateAndUpdateDuration(t *testing.T) {
s := newTestStores(t)
entry := &db.ProtocolLog{
Protocol: db.ProtocolIMAP,
Port: 143,
ClientIP: "203.0.113.9",
Username: "alice",
Success: true,
Detail: "LOGIN 成功",
CreatedAt: time.Now(),
}
if err := s.ProtocolLogs.Create(entry); err != nil {
t.Fatalf("create: %v", err)
}
if entry.ID == 0 {
t.Fatal("expected generated ID")
}
if err := s.ProtocolLogs.UpdateDuration(entry.ID, 3210); err != nil {
t.Fatalf("update duration: %v", err)
}
logs, total, err := s.ProtocolLogs.List(1, 10, ProtocolLogFilter{})
if err != nil {
t.Fatalf("list: %v", err)
}
if total != 1 || len(logs) != 1 {
t.Fatalf("expected 1 log, got total=%d len=%d", total, len(logs))
}
if logs[0].DurationMs != 3210 {
t.Fatalf("duration = %d, want 3210", logs[0].DurationMs)
}
if logs[0].Success != true || logs[0].Username != "alice" {
t.Fatalf("unexpected log: %+v", logs[0])
}
}
func TestProtocolLogListFilters(t *testing.T) {
s := newTestStores(t)
now := time.Now()
entries := []*db.ProtocolLog{
{Protocol: db.ProtocolSMTP, Port: 25, ClientIP: "10.0.0.1", Username: "", Success: true, FailReason: "", Detail: "投递", CreatedAt: now.Add(-3 * time.Hour)},
{Protocol: db.ProtocolSMTP, Port: 25, ClientIP: "10.0.0.2", Username: "admin", Success: false, FailReason: "中继访问被拒绝", Detail: "RCPT", CreatedAt: now.Add(-2 * time.Hour)},
{Protocol: db.ProtocolIMAP, Port: 993, ClientIP: "10.0.0.2", Username: "admin", Success: false, FailReason: "用户名或密码错误", Detail: "LOGIN 失败", CreatedAt: now.Add(-1 * time.Hour)},
{Protocol: db.ProtocolPOP3, Port: 110, ClientIP: "10.0.0.3", Username: "bob", Success: true, FailReason: "", Detail: "STAT", CreatedAt: now},
}
for _, e := range entries {
if err := s.ProtocolLogs.Create(e); err != nil {
t.Fatalf("create: %v", err)
}
}
cases := []struct {
name string
filter ProtocolLogFilter
want int64
}{
{"全部", ProtocolLogFilter{}, 4},
{"按协议", ProtocolLogFilter{Protocol: db.ProtocolSMTP}, 2},
{"按失败", ProtocolLogFilter{Success: boolPtr(false)}, 2},
{"按成功", ProtocolLogFilter{Success: boolPtr(true)}, 2},
{"协议+失败", ProtocolLogFilter{Protocol: db.ProtocolIMAP, Success: boolPtr(false)}, 1},
{"按IP模糊", ProtocolLogFilter{IP: "10.0.0.2"}, 2},
{"按用户名", ProtocolLogFilter{Username: "admin"}, 2},
{"按时间起", ProtocolLogFilter{From: now.Add(-90 * time.Minute)}, 2},
{"按时间止", ProtocolLogFilter{To: now.Add(-2 * time.Hour)}, 2},
{"无匹配", ProtocolLogFilter{Protocol: db.ProtocolPOP3, Success: boolPtr(false)}, 0},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
_, total, err := s.ProtocolLogs.List(1, 50, tc.filter)
if err != nil {
t.Fatalf("list: %v", err)
}
if total != tc.want {
t.Fatalf("total = %d, want %d", total, tc.want)
}
})
}
}
func TestProtocolLogListPagination(t *testing.T) {
s := newTestStores(t)
for i := 0; i < 5; i++ {
if err := s.ProtocolLogs.Create(&db.ProtocolLog{
Protocol: db.ProtocolSMTP, ClientIP: "10.0.0.1",
Success: true, CreatedAt: time.Now(),
}); err != nil {
t.Fatalf("create: %v", err)
}
}
page1, total, err := s.ProtocolLogs.List(1, 2, ProtocolLogFilter{})
if err != nil {
t.Fatalf("list: %v", err)
}
if total != 5 || len(page1) != 2 {
t.Fatalf("page1 total=%d len=%d", total, len(page1))
}
// 新记录在前
if page1[0].ID < page1[1].ID {
t.Fatal("expected newest first")
}
page3, _, err := s.ProtocolLogs.List(3, 2, ProtocolLogFilter{})
if err != nil {
t.Fatalf("list page3: %v", err)
}
if len(page3) != 1 {
t.Fatalf("page3 len = %d, want 1", len(page3))
}
}
func TestProtocolLogCountStatsAndCleanup(t *testing.T) {
s := newTestStores(t)
now := time.Now()
entries := []*db.ProtocolLog{
{Protocol: db.ProtocolSMTP, ClientIP: "a", Success: true, CreatedAt: now.Add(-10 * time.Minute)},
{Protocol: db.ProtocolSMTP, ClientIP: "b", Success: false, CreatedAt: now.Add(-20 * time.Minute)},
{Protocol: db.ProtocolIMAP, ClientIP: "c", Success: false, CreatedAt: now.Add(-30 * time.Minute)},
{Protocol: db.ProtocolIMAP, ClientIP: "d", Success: false, CreatedAt: now.AddDate(0, 0, -40)},
}
for _, e := range entries {
if err := s.ProtocolLogs.Create(e); err != nil {
t.Fatalf("create: %v", err)
}
}
stats, err := s.ProtocolLogs.CountStats(now.Add(-24 * time.Hour))
if err != nil {
t.Fatalf("stats: %v", err)
}
if stats[db.ProtocolSMTP]["success"] != 1 || stats[db.ProtocolSMTP]["fail"] != 1 {
t.Fatalf("smtp stats: %+v", stats[db.ProtocolSMTP])
}
if stats[db.ProtocolIMAP]["fail"] != 1 {
t.Fatalf("imap fail stats: %+v", stats[db.ProtocolIMAP])
}
// 清理 30 天前的记录
n, err := s.ProtocolLogs.CleanupBefore(now.AddDate(0, 0, -30))
if err != nil {
t.Fatalf("cleanup: %v", err)
}
if n != 1 {
t.Fatalf("deleted = %d, want 1", n)
}
_, total, err := s.ProtocolLogs.List(1, 50, ProtocolLogFilter{})
if err != nil {
t.Fatalf("list: %v", err)
}
if total != 3 {
t.Fatalf("total = %d, want 3", total)
}
}
func boolPtr(v bool) *bool {
return &v
}