1package fetcher
2
3import (
4 "bytes"
5 "strings"
6 "testing"
7
8 "github.com/floatpane/matcha/config"
9)
10
11type testPartHeader map[string]string
12
13func (h testPartHeader) Add(key, value string) {
14 h[key] = value
15}
16
17func (h testPartHeader) Del(key string) {
18 delete(h, key)
19}
20
21func (h testPartHeader) Get(key string) string {
22 return h[key]
23}
24
25func (h testPartHeader) Set(key, value string) {
26 h[key] = value
27}
28
29func TestDecodePartUsesCharsetWhenContentTypeIsMalformed(t *testing.T) {
30 header := testPartHeader{}
31 header.Set("Content-Type", "text/plain; charset=iso-8859-1; broken")
32
33 decoded, err := decodePart(bytes.NewReader([]byte{0x63, 0x61, 0x66, 0xe9}), header)
34 if err != nil {
35 t.Fatalf("decodePart() returned error: %v", err)
36 }
37
38 if decoded != "café" {
39 t.Fatalf("decodePart() = %q, want %q", decoded, "café")
40 }
41}
42
43func TestDecodePartFallsBackToUTF8WhenMalformedContentTypeHasNoCharset(t *testing.T) {
44 header := testPartHeader{}
45 header.Set("Content-Type", "text/plain; broken")
46
47 decoded, err := decodePart(strings.NewReader("hello"), header)
48 if err != nil {
49 t.Fatalf("decodePart() returned error: %v", err)
50 }
51
52 if decoded != "hello" {
53 t.Fatalf("decodePart() = %q, want %q", decoded, "hello")
54 }
55}
56
57func TestDecodeReaderWithCharsetSurvivesUnknownCharset(t *testing.T) {
58 decoded, err := decodeReaderWithCharset(strings.NewReader("hello"), "bogus-charset-name")
59 if err != nil {
60 t.Fatalf("decodeReaderWithCharset() returned error: %v", err)
61 }
62 if string(decoded) != "hello" {
63 t.Fatalf("decodeReaderWithCharset() = %q, want %q", string(decoded), "hello")
64 }
65}
66
67func TestLookupCharsetEncodingAlwaysReturnsNonNil(t *testing.T) {
68 cases := []string{"", "utf-8", "iso-8859-1", "bogus-charset-name", "this/is/not/real"}
69 for _, name := range cases {
70 t.Run(name, func(t *testing.T) {
71 if enc := lookupCharsetEncoding(name); enc == nil {
72 t.Fatalf("lookupCharsetEncoding(%q) returned nil", name)
73 }
74 })
75 }
76}
77
78func TestFormatPartPathEmptyPath(t *testing.T) {
79 cases := map[string][]int{
80 "nil": nil,
81 "empty": {},
82 }
83 for name, path := range cases {
84 t.Run(name, func(t *testing.T) {
85 if got := formatPartPath(path); got != "" {
86 t.Fatalf("formatPartPath(%v) = %q, want empty string", path, got)
87 }
88 })
89 }
90}
91
92// TestFetchEmails is an integration test that requires a live IMAP server and valid credentials.
93// NOTE: This test will be skipped if it cannot load a configuration file,
94// making it safe to run in a CI environment without credentials.
95// To run this test locally, ensure you have a valid `config.json` file.
96func TestFetchEmails(t *testing.T) {
97 // Attempt to load the configuration.
98 cfg, err := config.LoadConfig()
99 if err != nil {
100 // If config doesn't exist, skip the test. This is useful for CI environments.
101 t.Skipf("Skipping TestFetchEmails: could not load config: %v", err)
102 }
103
104 // Check if there are any accounts configured
105 if !cfg.HasAccounts() {
106 t.Skip("Skipping TestFetchEmails: no accounts configured.")
107 }
108
109 // Get the first account
110 account := cfg.GetFirstAccount()
111 if account == nil {
112 t.Skip("Skipping TestFetchEmails: no accounts available.")
113 }
114
115 // If the password is a placeholder, skip the test to avoid failed auth attempts.
116 if account.Password == "" || account.Password == "supersecret" {
117 t.Skip("Skipping TestFetchEmails: placeholder or empty password found in config.")
118 }
119
120 emails, err := FetchEmails(account, 10, 10)
121 if err != nil {
122 t.Fatalf("FetchEmails() failed with error: %v", err)
123 }
124
125 if len(emails) == 0 {
126 // This is not necessarily a failure, but we can log it.
127 t.Log("FetchEmails() returned 0 emails. This might be expected.")
128 }
129
130 // Check that the emails are sorted from newest to oldest.
131 // Skip emails with zero/invalid dates when checking sort order.
132 if len(emails) > 1 {
133 var validEmails []Email
134 for _, e := range emails {
135 if !e.Date.IsZero() {
136 validEmails = append(validEmails, e)
137 }
138 }
139 if len(validEmails) > 1 {
140 if validEmails[0].Date.Before(validEmails[len(validEmails)-1].Date) {
141 t.Error("Emails do not appear to be sorted from newest to oldest.")
142 }
143 }
144 }
145
146 // Check a sample email for expected content.
147 for _, email := range emails {
148 if email.Subject == "" && email.From == "" {
149 t.Errorf("Fetched email has empty subject and from fields: %+v", email)
150 }
151 }
152
153 // Verify that AccountID is set on fetched emails
154 for _, email := range emails {
155 if email.AccountID != account.ID {
156 t.Errorf("Expected AccountID %s, got %s", account.ID, email.AccountID)
157 }
158 }
159}
160
161// TestFetchEmailsWithCustomServer tests fetching with a custom server configuration.
162// This test is skipped unless a custom account is configured.
163func TestFetchEmailsWithCustomServer(t *testing.T) {
164 // Attempt to load the configuration.
165 cfg, err := config.LoadConfig()
166 if err != nil {
167 t.Skipf("Skipping TestFetchEmailsWithCustomServer: could not load config: %v", err)
168 }
169
170 // Look for a custom account
171 var customAccount *config.Account
172 for i := range cfg.Accounts {
173 if cfg.Accounts[i].ServiceProvider == "custom" {
174 customAccount = &cfg.Accounts[i]
175 break
176 }
177 }
178
179 if customAccount == nil {
180 t.Skip("Skipping TestFetchEmailsWithCustomServer: no custom account configured.")
181 }
182
183 if customAccount.Password == "" || customAccount.Password == "supersecret" {
184 t.Skip("Skipping TestFetchEmailsWithCustomServer: placeholder or empty password found.")
185 }
186
187 if customAccount.IMAPServer == "" {
188 t.Skip("Skipping TestFetchEmailsWithCustomServer: no IMAP server configured.")
189 }
190
191 emails, err := FetchEmails(customAccount, 5, 0)
192 if err != nil {
193 t.Fatalf("FetchEmails() with custom server failed: %v", err)
194 }
195
196 t.Logf("Fetched %d emails from custom server %s", len(emails), customAccount.IMAPServer)
197}