fetcher_test.go

  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}