Files
mailgoserver/internal/db/crud_mailbox_folders_test.go
T

298 lines
9.8 KiB
Go

package db
import "testing"
// TestCreateMailboxFolderDoesNotDisturbOrder is a regression test for a real bug
// caught via live testing: CreateMailboxFolder's INSERT OR IGNORE used to leave a
// fresh row's position at the column's SQL default (0), which is indistinguishable
// from "explicitly dragged to the very top" — so creating a brand-new custom folder
// made it jump above INBOX in the sidebar the instant it was created, before the user
// ever touched drag-and-drop. Fixed by writing an explicit sentinel (unpositionedFolder,
// -1) for a row that exists but was never dragged.
func TestCreateMailboxFolderDoesNotDisturbOrder(t *testing.T) {
d := openTestDB(t)
const mailboxID = int64(1)
if err := d.CreateMailboxFolder(mailboxID, "Receipts"); err != nil {
t.Fatal(err)
}
folders, err := d.AllFoldersForMailbox(mailboxID)
if err != nil {
t.Fatal(err)
}
if len(folders) == 0 || folders[0] != "INBOX" {
t.Fatalf("expected INBOX first, got %v", folders)
}
if folders[len(folders)-1] != "Receipts" {
t.Fatalf("expected Receipts last (never dragged), got %v", folders)
}
}
// treeNames flattens a []*FolderNode into a depth-first name list for easy assertion.
func treeNames(nodes []*FolderNode) []string {
var out []string
var walk func(n *FolderNode)
walk = func(n *FolderNode) {
out = append(out, n.Name)
for _, c := range n.Children {
walk(c)
}
}
for _, n := range nodes {
walk(n)
}
return out
}
// TestAllFoldersForMailboxRootOrderIsFixed confirms the 5 standard folders are always
// returned in their fixed StandardMailboxFolders order — root folders are static and
// were deliberately made non-reorderable, so even a stray saved position for one
// (however it got there) must never change their relative order.
func TestAllFoldersForMailboxRootOrderIsFixed(t *testing.T) {
d := openTestDB(t)
const mailboxID = int64(1)
if err := d.SetFolderOrder(mailboxID, []string{"Trash", "Junk", "INBOX", "Drafts", "Sent"}); err != nil {
t.Fatal(err)
}
folders, err := d.AllFoldersForMailbox(mailboxID)
if err != nil {
t.Fatal(err)
}
want := []string{"INBOX", "Junk", "Sent", "Drafts", "Trash"}
if len(folders) != len(want) {
t.Fatalf("folders = %v, want %v", folders, want)
}
for i, name := range want {
if folders[i] != name {
t.Fatalf("folders = %v, want %v", folders, want)
}
}
}
// TestFolderTreeNestsUnderParentAndOrdersSiblings confirms CreateMailboxFolderUnder
// actually nests a folder under its chosen parent in FolderTree, and that a saved
// sibling order (SetFolderOrder, scoped to just that parent's children) is respected
// — a folder created afterwards (never dragged) appends after the ordered siblings.
func TestFolderTreeNestsUnderParentAndOrdersSiblings(t *testing.T) {
d := openTestDB(t)
const mailboxID = int64(1)
if err := d.CreateMailboxFolderUnder(mailboxID, "Projects", "INBOX"); err != nil {
t.Fatal(err)
}
if err := d.CreateMailboxFolderUnder(mailboxID, "ClientA", "Projects"); err != nil {
t.Fatal(err)
}
if err := d.CreateMailboxFolderUnder(mailboxID, "ClientB", "Projects"); err != nil {
t.Fatal(err)
}
if err := d.SetFolderOrder(mailboxID, []string{"ClientB", "ClientA"}); err != nil {
t.Fatal(err)
}
tree, err := d.FolderTree(mailboxID)
if err != nil {
t.Fatal(err)
}
got := treeNames(tree)
want := []string{"INBOX", "Projects", "ClientB", "ClientA", "Junk", "Sent", "Drafts", "Trash"}
if len(got) != len(want) {
t.Fatalf("tree = %v, want %v", got, want)
}
for i, name := range want {
if got[i] != name {
t.Fatalf("tree = %v, want %v", got, want)
}
}
if err := d.CreateMailboxFolderUnder(mailboxID, "ClientC", "Projects"); err != nil {
t.Fatal(err)
}
tree, err = d.FolderTree(mailboxID)
if err != nil {
t.Fatal(err)
}
got = treeNames(tree)
if got[len(got)-5] != "ClientC" { // last of Projects' 3 children, before Junk/Sent/Drafts/Trash
t.Fatalf("expected ClientC to append after the ordered siblings, got %v", got)
}
}
// TestRenameMailboxFolderKeepsChildrenNested confirms renaming a folder that has
// children leaves them correctly nested (they reference their parent by row id, not
// by name, so a plain single-row UPDATE never orphans them).
func TestRenameMailboxFolderKeepsChildrenNested(t *testing.T) {
d := openTestDB(t)
const mailboxID = int64(1)
if err := d.CreateMailboxFolderUnder(mailboxID, "Projects", "INBOX"); err != nil {
t.Fatal(err)
}
if err := d.CreateMailboxFolderUnder(mailboxID, "ClientA", "Projects"); err != nil {
t.Fatal(err)
}
if err := d.RenameMailboxFolder(mailboxID, "Projects", "Work"); err != nil {
t.Fatal(err)
}
root, err := d.FolderRoot(mailboxID, "ClientA")
if err != nil || root != "INBOX" {
t.Fatalf("expected ClientA to still resolve under INBOX after its parent was renamed, root=%q err=%v", root, err)
}
tree, err := d.FolderTree(mailboxID)
if err != nil {
t.Fatal(err)
}
got := treeNames(tree)
want := []string{"INBOX", "Work", "ClientA", "Junk", "Sent", "Drafts", "Trash"}
if len(got) != len(want) {
t.Fatalf("tree = %v, want %v", got, want)
}
for i, name := range want {
if got[i] != name {
t.Fatalf("tree = %v, want %v", got, want)
}
}
}
// TestMoveFolderToTrashKeepsSubtreeIntact confirms deleting a folder that has its own
// children re-parents the whole subtree under Trash in one move — the children,
// referencing their parent by id, come along automatically with zero further writes.
func TestMoveFolderToTrashKeepsSubtreeIntact(t *testing.T) {
d := openTestDB(t)
const mailboxID = int64(1)
if err := d.CreateMailboxFolderUnder(mailboxID, "Projects", "INBOX"); err != nil {
t.Fatal(err)
}
if err := d.CreateMailboxFolderUnder(mailboxID, "ClientA", "Projects"); err != nil {
t.Fatal(err)
}
if err := d.MoveFolderToTrash(mailboxID, "Projects"); err != nil {
t.Fatal(err)
}
for _, name := range []string{"Projects", "ClientA"} {
root, err := d.FolderRoot(mailboxID, name)
if err != nil || root != "Trash" {
t.Fatalf("expected %s to resolve under Trash, got root=%q (err=%v)", name, root, err)
}
}
names, err := d.FolderSubtreeNames(mailboxID, "Trash")
if err != nil {
t.Fatal(err)
}
want := map[string]bool{"Trash": true, "Projects": true, "ClientA": true}
if len(names) != len(want) {
t.Fatalf("FolderSubtreeNames(Trash) = %v, want exactly %v", names, want)
}
for _, n := range names {
if !want[n] {
t.Fatalf("FolderSubtreeNames(Trash) = %v, want exactly %v", names, want)
}
}
}
// TestRestoreFolderReturnsToOriginalParent confirms deleting a nested folder and then
// restoring it puts it back exactly where it was (not just dumped at INBOX's top
// level) — its sibling, untouched throughout, proves the parent (Projects) itself
// never moved.
func TestRestoreFolderReturnsToOriginalParent(t *testing.T) {
d := openTestDB(t)
const mailboxID = int64(1)
if err := d.CreateMailboxFolderUnder(mailboxID, "Projects", "INBOX"); err != nil {
t.Fatal(err)
}
if err := d.CreateMailboxFolderUnder(mailboxID, "ClientA", "Projects"); err != nil {
t.Fatal(err)
}
if err := d.MoveFolderToTrash(mailboxID, "ClientA"); err != nil {
t.Fatal(err)
}
if root, err := d.FolderRoot(mailboxID, "ClientA"); err != nil || root != "Trash" {
t.Fatalf("expected ClientA under Trash after delete, root=%q err=%v", root, err)
}
if err := d.RestoreFolder(mailboxID, "ClientA"); err != nil {
t.Fatal(err)
}
tree, err := d.FolderTree(mailboxID)
if err != nil {
t.Fatal(err)
}
got := treeNames(tree)
want := []string{"INBOX", "Projects", "ClientA", "Junk", "Sent", "Drafts", "Trash"}
if len(got) != len(want) {
t.Fatalf("tree after restore = %v, want %v", got, want)
}
for i, name := range want {
if got[i] != name {
t.Fatalf("tree after restore = %v, want %v", got, want)
}
}
}
// TestRestoreFolderFallsBackToInboxWhenOriginalParentStillTrashed confirms restoring
// a folder whose former parent is itself still in Trash doesn't leave it looking
// un-restored (nested inside another trashed folder) — it falls back to INBOX
// instead.
func TestRestoreFolderFallsBackToInboxWhenOriginalParentStillTrashed(t *testing.T) {
d := openTestDB(t)
const mailboxID = int64(1)
if err := d.CreateMailboxFolderUnder(mailboxID, "Projects", "INBOX"); err != nil {
t.Fatal(err)
}
if err := d.CreateMailboxFolderUnder(mailboxID, "ClientA", "Projects"); err != nil {
t.Fatal(err)
}
if err := d.MoveFolderToTrash(mailboxID, "ClientA"); err != nil {
t.Fatal(err)
}
// Now trash Projects too — ClientA's restore target (Projects) is itself trashed.
if err := d.MoveFolderToTrash(mailboxID, "Projects"); err != nil {
t.Fatal(err)
}
if err := d.RestoreFolder(mailboxID, "ClientA"); err != nil {
t.Fatal(err)
}
root, err := d.FolderRoot(mailboxID, "ClientA")
if err != nil || root != "INBOX" {
t.Fatalf("expected ClientA to fall back under INBOX, got root=%q (err=%v)", root, err)
}
}
// TestDeleteFolderRowsRemovesRecords confirms DeleteFolderRows actually removes the
// esrv_mailbox_folders rows (not just messages) — used by permanent deletes so a
// permanently-removed folder doesn't linger as an empty shell in FolderTree.
func TestDeleteFolderRowsRemovesRecords(t *testing.T) {
d := openTestDB(t)
const mailboxID = int64(1)
if err := d.CreateMailboxFolderUnder(mailboxID, "Projects", "INBOX"); err != nil {
t.Fatal(err)
}
if err := d.CreateMailboxFolderUnder(mailboxID, "ClientA", "Projects"); err != nil {
t.Fatal(err)
}
if err := d.DeleteFolderRows(mailboxID, []string{"Projects", "ClientA"}); err != nil {
t.Fatal(err)
}
tree, err := d.FolderTree(mailboxID)
if err != nil {
t.Fatal(err)
}
got := treeNames(tree)
want := []string{"INBOX", "Junk", "Sent", "Drafts", "Trash"}
if len(got) != len(want) {
t.Fatalf("tree after DeleteFolderRows = %v, want %v (Projects/ClientA gone)", got, want)
}
for i, name := range want {
if got[i] != name {
t.Fatalf("tree after DeleteFolderRows = %v, want %v", got, want)
}
}
}