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
78// TestFetchEmails is an integration test that requires a live IMAP server and valid credentials.
79// NOTE: This test will be skipped if it cannot load a configuration file,
80// making it safe to run in a CI environment without credentials.
81// To run this test locally, ensure you have a valid `config.json` file.
82func TestFetchEmails(t *testing.T) {
83 // Attempt to load the configuration.
84 cfg, err := config.LoadConfig()
85 if err != nil {
86 // If config doesn't exist, skip the test. This is useful for CI environments.
87 t.Skipf("Skipping TestFetchEmails: could not load config: %v", err)
88 }
89
90 // Check if there are any accounts configured
91 if !cfg.HasAccounts() {
92 t.Skip("Skipping TestFetchEmails: no accounts configured.")
93 }
94
95 // Get the first account
96 account := cfg.GetFirstAccount()
97 if account == nil {
98 t.Skip("Skipping TestFetchEmails: no accounts available.")
99 }
100
101 // If the password is a placeholder, skip the test to avoid failed auth attempts.
102 if account.Password == "" || account.Password == "supersecret" {
103 t.Skip("Skipping TestFetchEmails: placeholder or empty password found in config.")
104 }
105
106 emails, err := FetchEmails(account, 10, 10)
107 if err != nil {
108 t.Fatalf("FetchEmails() failed with error: %v", err)
109 }
110
111 if len(emails) == 0 {
112 // This is not necessarily a failure, but we can log it.
113 t.Log("FetchEmails() returned 0 emails. This might be expected.")
114 }
115
116 // Check that the emails are sorted from newest to oldest.
117 // Skip emails with zero/invalid dates when checking sort order.
118 if len(emails) > 1 {
119 var validEmails []Email
120 for _, e := range emails {
121 if !e.Date.IsZero() {
122 validEmails = append(validEmails, e)
123 }
124 }
125 if len(validEmails) > 1 {
126 if validEmails[0].Date.Before(validEmails[len(validEmails)-1].Date) {
127 t.Error("Emails do not appear to be sorted from newest to oldest.")
128 }
129 }
130 }
131
132 // Check a sample email for expected content.
133 for _, email := range emails {
134 if email.Subject == "" && email.From == "" {
135 t.Errorf("Fetched email has empty subject and from fields: %+v", email)
136 }
137 }
138
139 // Verify that AccountID is set on fetched emails
140 for _, email := range emails {
141 if email.AccountID != account.ID {
142 t.Errorf("Expected AccountID %s, got %s", account.ID, email.AccountID)
143 }
144 }
145}
146
147// TestFetchEmailsWithCustomServer tests fetching with a custom server configuration.
148// This test is skipped unless a custom account is configured.
149func TestFetchEmailsWithCustomServer(t *testing.T) {
150 // Attempt to load the configuration.
151 cfg, err := config.LoadConfig()
152 if err != nil {
153 t.Skipf("Skipping TestFetchEmailsWithCustomServer: could not load config: %v", err)
154 }
155
156 // Look for a custom account
157 var customAccount *config.Account
158 for i := range cfg.Accounts {
159 if cfg.Accounts[i].ServiceProvider == "custom" {
160 customAccount = &cfg.Accounts[i]
161 break
162 }
163 }
164
165 if customAccount == nil {
166 t.Skip("Skipping TestFetchEmailsWithCustomServer: no custom account configured.")
167 }
168
169 if customAccount.Password == "" || customAccount.Password == "supersecret" {
170 t.Skip("Skipping TestFetchEmailsWithCustomServer: placeholder or empty password found.")
171 }
172
173 if customAccount.IMAPServer == "" {
174 t.Skip("Skipping TestFetchEmailsWithCustomServer: no IMAP server configured.")
175 }
176
177 emails, err := FetchEmails(customAccount, 5, 0)
178 if err != nil {
179 t.Fatalf("FetchEmails() with custom server failed: %v", err)
180 }
181
182 t.Logf("Fetched %d emails from custom server %s", len(emails), customAccount.IMAPServer)
183}