1package main
2
3import (
4 "path/filepath"
5 "strings"
6 "testing"
7 "unicode/utf8"
8
9 "github.com/floatpane/matcha/config"
10 "github.com/floatpane/matcha/fetcher"
11 "github.com/floatpane/matcha/tui"
12)
13
14func TestSanitizeFilenameTruncatesCJKOnUTF8Boundary(t *testing.T) {
15 name := strings.Repeat("文", 100) + ".txt"
16
17 got := sanitizeFilename(name)
18
19 if !utf8.ValidString(got) {
20 t.Fatalf("sanitizeFilename returned invalid UTF-8: %q", got)
21 }
22 if len(got) > 255 {
23 t.Fatalf("sanitizeFilename returned %d bytes, want at most 255", len(got))
24 }
25 if filepath.Ext(got) != ".txt" {
26 t.Fatalf("sanitizeFilename lost extension: got %q", got)
27 }
28}
29
30func TestSanitizeFilenameTruncatesEmojiOnUTF8Boundary(t *testing.T) {
31 name := strings.Repeat("🚀", 80) + ".log"
32
33 got := sanitizeFilename(name)
34
35 if !utf8.ValidString(got) {
36 t.Fatalf("sanitizeFilename returned invalid UTF-8: %q", got)
37 }
38 if len(got) > 255 {
39 t.Fatalf("sanitizeFilename returned %d bytes, want at most 255", len(got))
40 }
41 if filepath.Ext(got) != ".log" {
42 t.Fatalf("sanitizeFilename lost extension: got %q", got)
43 }
44}
45
46func TestParseGlobalFlagsEnablesLogPanel(t *testing.T) {
47 args, _, show := parseGlobalFlags([]string{"matcha", "--debug", "--logs", "--version"})
48 if !show {
49 t.Fatal("expected log panel flag to be enabled")
50 }
51 if got := strings.Join(args, " "); got != "matcha --version" {
52 t.Fatalf("args = %q, want %q", got, "matcha --version")
53 }
54}
55
56func TestParseGlobalFlagsDoesNotConsumeSubcommandFlags(t *testing.T) {
57 args, _, show := parseGlobalFlags([]string{"matcha", "send", "--logs"})
58 if show {
59 t.Fatal("did not expect log panel flag after subcommand to be consumed")
60 }
61 if got := strings.Join(args, " "); got != "matcha send --logs" {
62 t.Fatalf("args = %q, want %q", got, "matcha send --logs")
63 }
64}
65
66// newFolderCounterModel builds a minimal mainModel whose INBOX folder holds the
67// given emails and reports the given unread count. It is used to verify that the
68// per-folder unread counter shown in the sidebar is updated immediately after
69// delete/archive operations, without waiting for the next fetch.
70func newFolderCounterModel(emails []fetcher.Email, unread int) *mainModel {
71 accountID := "acct-a"
72 cfg := &config.Config{
73 Accounts: []config.Account{{ID: accountID, Email: "a@example.com"}},
74 }
75
76 fi := tui.NewFolderInbox([]string{folderInbox}, cfg.Accounts)
77 fi.SetUnreadCounts(map[string]int{folderInbox: unread})
78 fi.SetEmails(emails, cfg.Accounts)
79
80 byAcct := make(map[string][]fetcher.Email)
81 for _, e := range emails {
82 byAcct[e.AccountID] = append(byAcct[e.AccountID], e)
83 }
84
85 m := &mainModel{
86 config: cfg,
87 folderInbox: fi,
88 current: fi,
89 emails: emails,
90 emailsByAcct: byAcct,
91 folderEmails: map[string][]fetcher.Email{folderInbox: emails},
92 }
93 return m
94}
95
96// TestDeleteUnreadEmailUpdatesFolderCounter verifies that deleting an unread
97// email immediately decrements the folder's unread counter in the sidebar,
98// rather than waiting for the next fetch (issue #1404).
99func TestDeleteUnreadEmailUpdatesFolderCounter(t *testing.T) {
100 email := fetcher.Email{UID: 42, AccountID: "acct-a", IsRead: false}
101 m := newFolderCounterModel([]fetcher.Email{email}, 1)
102
103 m.Update(tui.DeleteEmailMsg{UID: 42, AccountID: "acct-a", Mailbox: folderInbox})
104
105 if got := m.folderInbox.GetUnreadCountsCopy()[folderInbox]; got != 0 {
106 t.Fatalf("after deleting an unread email, folder unread count = %d, want 0", got)
107 }
108}
109
110// TestArchiveUnreadEmailUpdatesFolderCounter verifies the same immediate update
111// for archive operations.
112func TestArchiveUnreadEmailUpdatesFolderCounter(t *testing.T) {
113 email := fetcher.Email{UID: 7, AccountID: "acct-a", IsRead: false}
114 m := newFolderCounterModel([]fetcher.Email{email}, 1)
115
116 m.Update(tui.ArchiveEmailMsg{UID: 7, AccountID: "acct-a", Mailbox: folderInbox})
117
118 if got := m.folderInbox.GetUnreadCountsCopy()[folderInbox]; got != 0 {
119 t.Fatalf("after archiving an unread email, folder unread count = %d, want 0", got)
120 }
121}
122
123// TestBatchDeleteUnreadEmailsUpdatesFolderCounter verifies that batch delete
124// decrements the folder counter once per unread email removed, while leaving
125// already-read emails untouched.
126func TestBatchDeleteUnreadEmailsUpdatesFolderCounter(t *testing.T) {
127 emails := []fetcher.Email{
128 {UID: 1, AccountID: "acct-a", IsRead: false},
129 {UID: 2, AccountID: "acct-a", IsRead: false},
130 {UID: 3, AccountID: "acct-a", IsRead: true},
131 }
132 m := newFolderCounterModel(emails, 2)
133
134 m.Update(tui.BatchDeleteEmailsMsg{UIDs: []uint32{1, 2, 3}, AccountID: "acct-a", Mailbox: folderInbox})
135
136 if got := m.folderInbox.GetUnreadCountsCopy()[folderInbox]; got != 0 {
137 t.Fatalf("after batch-deleting two unread emails, folder unread count = %d, want 0", got)
138 }
139}
140
141// TestDeleteReadEmailLeavesFolderCounter verifies that deleting an already-read
142// email does not change the folder unread counter.
143func TestDeleteReadEmailLeavesFolderCounter(t *testing.T) {
144 emails := []fetcher.Email{
145 {UID: 10, AccountID: "acct-a", IsRead: true},
146 {UID: 11, AccountID: "acct-a", IsRead: false},
147 }
148 m := newFolderCounterModel(emails, 1)
149
150 m.Update(tui.DeleteEmailMsg{UID: 10, AccountID: "acct-a", Mailbox: folderInbox})
151
152 if got := m.folderInbox.GetUnreadCountsCopy()[folderInbox]; got != 1 {
153 t.Fatalf("after deleting a read email, folder unread count = %d, want 1", got)
154 }
155}
156
157// TestUndoDeleteRestoresFolderCounter verifies that undoing a delete restores
158// the folder unread counter that was decremented when the email was deleted.
159func TestUndoDeleteRestoresFolderCounter(t *testing.T) {
160 email := fetcher.Email{UID: 99, AccountID: "acct-a", IsRead: false}
161 m := newFolderCounterModel([]fetcher.Email{email}, 1)
162
163 m.Update(tui.DeleteEmailMsg{UID: 99, AccountID: "acct-a", Mailbox: folderInbox})
164 if got := m.folderInbox.GetUnreadCountsCopy()[folderInbox]; got != 0 {
165 t.Fatalf("after delete, folder unread count = %d, want 0", got)
166 }
167
168 m.restorePendingAction()
169 if got := m.folderInbox.GetUnreadCountsCopy()[folderInbox]; got != 1 {
170 t.Fatalf("after undo, folder unread count = %d, want 1", got)
171 }
172}
173
174func TestUnreadBadgeCountDeduplicatesOverlappingStores(t *testing.T) {
175 email := fetcher.Email{UID: 42, AccountID: "acct-a"}
176 got := unreadBadgeCount(
177 map[string][]fetcher.Email{
178 "acct-a": {email},
179 },
180 map[string][]fetcher.Email{
181 folderInbox: {email},
182 },
183 )
184
185 if got != 1 {
186 t.Fatalf("unreadBadgeCount() = %d, want 1", got)
187 }
188}