Files
mailgo/internal/connhub/hub_test.go
T
kevin 6a9dcd0285 feat(imap): 多客户端状态实时同步 + 当前连接「断开并封禁」
- 推送扩展:IMAP STORE(已读/星标/\Deleted)推送 FETCH 标志更新、
  EXPUNGE 推送 ExpungeUpdate(删除前序号)、APPEND/COPY/MOVE 推送
  新邮件;POP3 QUIT 删除、Web 标已读/删除同样实时同步到 IMAP 客户端
- Pusher 接口统一 SMTP/POP3/Web 的推送入口,IMAP 内部操作经会话
  通道直接入队(非阻塞,满则丢弃)
- 当前连接页新增「断开并封禁」:connhub 支持断开回调,SMTP/POP3
  关底层连接、IMAP 经 ForEachConn 按地址断开;一键封禁 180 天并
  断开该 IP 全部在线连接,黑名单页可随时解封
- 修复:POP3 PASS 成功后保留完整邮箱(此前被裸用户名覆盖)
- 新增测试:断开/按 IP 断开、flags/expunge 推送内容、POP3 删除推送、
  Web 断开封禁处理器;全量 -race 通过
2026-08-19 19:56:04 +08:00

178 lines
4.2 KiB
Go

package connhub
import (
"sync"
"sync/atomic"
"testing"
"time"
)
func TestRegisterListCountClose(t *testing.T) {
h := New()
c1 := h.Register("smtp", "10.0.0.1", 25, false)
c2 := h.Register("imap", "10.0.0.2", 993, true)
c3 := h.Register("pop3", "10.0.0.3", 110, false)
if n := len(h.List()); n != 3 {
t.Fatalf("list len = %d, want 3", n)
}
counts := h.Counts()
if counts["smtp"] != 1 || counts["imap"] != 1 || counts["pop3"] != 1 {
t.Fatalf("counts = %v", counts)
}
c2.SetUser("alice@example.com")
c1.SetTLS(true)
time.Sleep(time.Millisecond)
c3.Touch()
// 用户名 / TLS 状态生效
var imapUser string
for _, c := range h.List() {
if c.Protocol == "imap" {
imapUser = c.User
}
if c.Protocol == "smtp" && !c.TLS {
t.Fatal("smtp conn should be TLS after SetTLS(true)")
}
if c.Protocol == "pop3" && !c.LastActive.After(c.Connected) {
t.Fatal("pop3 conn LastActive should be after Connected after Touch")
}
}
if imapUser != "alice@example.com" {
t.Fatalf("imap user = %q", imapUser)
}
c1.Close()
c2.Close()
if n := len(h.List()); n != 1 {
t.Fatalf("after close, len = %d, want 1", n)
}
if h.Counts()["imap"] != 0 {
t.Fatalf("imap count after close = %d, want 0", h.Counts()["imap"])
}
}
func TestNilHubSafe(t *testing.T) {
var h *Hub
if c := h.Register("smtp", "1.2.3.4", 25, false); c != nil {
t.Fatal("nil hub Register must return nil")
}
if h.List() != nil || h.Counts() != nil {
t.Fatal("nil hub List/Counts must be nil")
}
var c *Conn
c.SetUser("x") // 不应 panic
c.Touch()
c.SetTLS(true)
c.Close()
}
func TestConcurrentRegisterClose(t *testing.T) {
h := New()
const n = 50
var wg sync.WaitGroup
conns := make([]*Conn, n)
for i := 0; i < n; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
conns[i] = h.Register("smtp", "10.0.0.1", 25, false)
conns[i].SetUser("u")
conns[i].Touch()
}(i)
}
wg.Wait()
if len(h.List()) != n {
t.Fatalf("len = %d, want %d", len(h.List()), n)
}
for i := 0; i < n; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
conns[i].Close()
}(i)
}
wg.Wait()
if len(h.List()) != 0 {
t.Fatalf("after concurrent close, len = %d, want 0", len(h.List()))
}
}
func TestListOrderedByID(t *testing.T) {
h := New()
h.Register("smtp", "1.1.1.1", 25, false)
h.Register("imap", "2.2.2.2", 143, false)
h.Register("pop3", "3.3.3.3", 110, false)
list := h.List()
for i := 1; i < len(list); i++ {
if list[i].ID <= list[i-1].ID {
t.Fatalf("list not ordered by ID: %+v", list)
}
}
}
// TestDisconnect 验证强制断开回调被调用。
func TestDisconnect(t *testing.T) {
h := New()
var closed atomic.Int32
c1 := h.Register("smtp", "10.0.0.1", 25, false)
c1.SetDisconnect(func() { closed.Add(1) })
c2 := h.Register("imap", "10.0.0.2", 993, true)
c2.SetDisconnect(func() { closed.Add(1) })
if !h.Disconnect(c1.ID) {
t.Fatal("Disconnect should report success")
}
if closed.Load() != 1 {
t.Fatalf("closed = %d, want 1", closed.Load())
}
// 已断开(未注销)仍可查到
if _, ok := h.Get(c1.ID); !ok {
t.Fatal("conn should still be registered until Close")
}
// 不存在的 ID
if h.Disconnect(99999) {
t.Fatal("Disconnect of unknown id must fail")
}
}
// TestDisconnectByIP 验证封禁时断开该 IP 全部连接。
func TestDisconnectByIP(t *testing.T) {
h := New()
var closed atomic.Int32
// 同一 IP 三个协议连接
for _, proto := range []string{"smtp", "imap", "pop3"} {
c := h.Register(proto, "203.0.113.5", 25, false)
c.SetDisconnect(func() { closed.Add(1) })
}
// 另一 IP 不受影响
other := h.Register("smtp", "203.0.113.6", 25, false)
other.SetDisconnect(func() { closed.Add(1) })
n := h.DisconnectByIP("203.0.113.5")
if n != 3 {
t.Fatalf("disconnected = %d, want 3", n)
}
if closed.Load() != 3 {
t.Fatalf("closed = %d, want 3", closed.Load())
}
}
// TestDisconnectNoCallback 验证未注册断开回调的连接安全跳过。
func TestDisconnectNoCallback(t *testing.T) {
h := New()
c := h.Register("pop3", "10.0.0.9", 110, false)
if !h.Disconnect(c.ID) {
t.Fatal("Disconnect should report success even without callback")
}
if n := h.DisconnectByIP("10.0.0.9"); n != 0 {
t.Fatalf("disconnected = %d, want 0", n)
}
}