Files
mailgo/internal/web/handlers/mail_delete_test.go
T
kevin dc164fdf66 feat(mailbox): 文件夹数据驱动,Web 通过 IMAP 共享服务层操作邮箱
- 新增 mailboxes 表与 MailboxStore(系统文件夹幂等创建,自定义文件夹 CRUD)
- 提取 MailboxService:IMAP 会话与 Web handler 共用,LIST 返回什么 Web 就显示什么
- IMAP 支持 CREATE/DELETE/RENAME/SUBSCRIBE(系统文件夹禁删改、非空禁删)
- Web 删除改为 IMAP 语义:移入 Trash,新增恢复/彻底删除/清空
- 新增通用 /folder/:name 页面与动态侧边栏,/inbox /sent /drafts 保留兼容重定向
2026-08-20 01:25:40 +08:00

189 lines
5.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handlers
// Web 删除语义回归测试:删除=移入垃圾箱(IMAP 语义)、垃圾箱中删除=彻底
// 删除、恢复=回到收件箱、清空=永久删除。文件夹操作全部经由 MailboxService
// (与 IMAP 会话共用同一份实现)。
import (
"html/template"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"time"
"mail_go/internal/db"
"mail_go/internal/imap_server"
"mail_go/internal/store"
"github.com/gin-gonic/gin"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func newMailTestHandler(t *testing.T) (*MailHandler, *store.Stores) {
t.Helper()
gdb, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "test.db")), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err := gdb.AutoMigrate(&db.User{}, &db.Domain{}, &db.Message{}, &db.Attachment{}, &db.Mailbox{}, &db.MailboxState{}); err != nil {
t.Fatal(err)
}
stores := store.NewStores(gdb)
if err := stores.Users.Create(&db.User{ID: 1, Username: "alice", Domain: db.Domain{Name: "example.com"}, DomainID: 1}); err != nil {
t.Fatal(err)
}
return NewMailHandler(stores, nil, nil, imap_server.NewMailboxService(stores), nil), stores
}
// newMailTestRouter 注册删除/恢复/清空路由并注入认证上下文。
func newMailTestRouter(h *MailHandler) *gin.Engine {
gin.SetMode(gin.TestMode)
r := gin.New()
// Folder 页渲染需要模板(与 oauth2 测试共用同一份测试函数表)
tmpl := template.Must(template.New("").Funcs(testTemplateFuncs()).ParseGlob(filepath.Join("..", "templates", "*.html")))
r.SetHTMLTemplate(tmpl)
r.Use(func(c *gin.Context) {
c.Set("userID", uint(1))
c.Set("currentUser", &db.User{ID: 1, Username: "alice", Domain: db.Domain{Name: "example.com"}})
c.Next()
})
r.POST("/mail/delete/:id", h.Delete)
r.POST("/mail/restore/:id", h.Restore)
r.POST("/mail/purge/:id", h.Purge)
r.POST("/folder/:name/empty", h.EmptyFolder)
r.GET("/folder/:name", h.Folder)
return r
}
func seedWebMsg(t *testing.T, stores *store.Stores, folder string) *db.Message {
t.Helper()
msg := &db.Message{
UserID: 1,
Folder: folder,
FromAddr: "sender@other.com",
ToAddr: "alice@example.com",
Subject: "测试邮件",
Date: time.Now(),
}
if err := stores.Mails.Create(msg); err != nil {
t.Fatal(err)
}
return msg
}
func msgFolder(t *testing.T, stores *store.Stores, id uint) (string, bool) {
t.Helper()
msg, err := stores.Mails.GetByID(id)
if err != nil {
return "", false
}
return msg.Folder, true
}
func TestWebDeleteMovesToTrash(t *testing.T) {
h, stores := newMailTestHandler(t)
msg := seedWebMsg(t, stores, "INBOX")
r := newMailTestRouter(h)
// 收件箱删除 → 移入垃圾箱(非物理删除)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/mail/delete/"+itoa(msg.ID), nil)
req.Header.Set("Referer", "/folder/INBOX")
r.ServeHTTP(w, req)
if w.Code != http.StatusFound {
t.Fatalf("delete status = %d, want 302", w.Code)
}
folder, ok := msgFolder(t, stores, msg.ID)
if !ok || folder != "Trash" {
t.Fatalf("deleted msg folder = %q, want Trash", folder)
}
// 垃圾箱中再删除 → 彻底删除
w2 := httptest.NewRecorder()
req2 := httptest.NewRequest(http.MethodPost, "/mail/delete/"+itoa(msg.ID), nil)
req2.Header.Set("Referer", "/folder/Trash")
r.ServeHTTP(w2, req2)
if _, ok := msgFolder(t, stores, msg.ID); ok {
t.Fatal("trash delete should purge the message")
}
}
func TestWebRestoreToInbox(t *testing.T) {
h, stores := newMailTestHandler(t)
msg := seedWebMsg(t, stores, "Trash")
r := newMailTestRouter(h)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/mail/restore/"+itoa(msg.ID), nil)
r.ServeHTTP(w, req)
if w.Code != http.StatusFound {
t.Fatalf("restore status = %d, want 302", w.Code)
}
folder, ok := msgFolder(t, stores, msg.ID)
if !ok || folder != "INBOX" {
t.Fatalf("restored msg folder = %q, want INBOX", folder)
}
}
func TestWebEmptyFolderPurgesAll(t *testing.T) {
h, stores := newMailTestHandler(t)
seedWebMsg(t, stores, "Trash")
seedWebMsg(t, stores, "Trash")
r := newMailTestRouter(h)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/folder/Trash/empty", nil)
r.ServeHTTP(w, req)
if w.Code != http.StatusFound {
t.Fatalf("empty status = %d, want 302", w.Code)
}
count, err := stores.Mails.CountByUserAndFolder(1, "Trash")
if err != nil || count != 0 {
t.Fatalf("Trash count after empty = %d, want 0", count)
}
}
func TestWebFolderPageListsDynamicFolders(t *testing.T) {
h, stores := newMailTestHandler(t)
seedWebMsg(t, stores, "INBOX")
r := newMailTestRouter(h)
// IMAP CREATE 语义创建的自定义文件夹(经同一 MailboxService
svc := imap_server.NewMailboxService(stores)
if err := svc.Create(1, "工作"); err != nil {
t.Fatalf("create custom mailbox: %v", err)
}
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/folder/工作", nil)
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("custom folder page status = %d, want 200", w.Code)
}
// 不存在的文件夹 → 404
w2 := httptest.NewRecorder()
req2 := httptest.NewRequest(http.MethodGet, "/folder/不存在", nil)
r.ServeHTTP(w2, req2)
if w2.Code != http.StatusNotFound {
t.Fatalf("missing folder status = %d, want 404", w2.Code)
}
}
func itoa(n uint) string {
if n == 0 {
return "0"
}
var b []byte
for n > 0 {
b = append([]byte{byte('0' + n%10)}, b...)
n /= 10
}
return string(b)
}