- IMAP client package with TLS/STARTTLS connection management, unseen fetching, batch fetching by UID range, message move/copy operations, folder management - Background worker with steady-state (unseen polling) and catch-up (UID range backlog) modes, per-user IMAP connections, test mode support, placeholder AI classifier - Database: GetUsersWithAutoStart() for worker user discovery - Wired worker into main.go with graceful shutdown
219 lines
5.2 KiB
Go
219 lines
5.2 KiB
Go
package worker_test
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"inboxer/src/internal/db"
|
|
"inboxer/src/internal/imap"
|
|
"inboxer/src/internal/worker"
|
|
"inboxer/src/pkg/config"
|
|
)
|
|
|
|
// mockClassifier implements worker.AIClassifier for testing
|
|
type mockClassifier struct {
|
|
folder string
|
|
confidence int
|
|
err error
|
|
}
|
|
|
|
func (m *mockClassifier) Classify(email imap.EmailSummary) (string, int, error) {
|
|
return m.folder, m.confidence, m.err
|
|
}
|
|
|
|
// mockDB wraps the real database for testing (uses in-memory SQLite)
|
|
type mockDB struct {
|
|
*db.Database
|
|
}
|
|
|
|
func newMockDB(t *testing.T) *db.Database {
|
|
t.Helper()
|
|
|
|
// We need a different approach since NewDatabase needs a file path
|
|
// Use temporary file
|
|
database, err := db.NewDatabase(t.TempDir()+"/test.db", "test-secret-key-32-bytes-long!!")
|
|
if err != nil {
|
|
t.Fatalf("failed to create test database: %v", err)
|
|
}
|
|
return database
|
|
}
|
|
|
|
func TestNewWorker(t *testing.T) {
|
|
cfg := &config.Config{
|
|
Worker: config.WorkerConfig{
|
|
SteadyStateBatchSize: 10,
|
|
SteadyStateIntervalMinutes: 5,
|
|
CatchUpBatchSize: 50,
|
|
CatchUpCooldownSeconds: 5,
|
|
},
|
|
Folders: config.FolderConfig{
|
|
Important: "Important",
|
|
Ecommerce: "eCommerce",
|
|
Other: "Other",
|
|
Spam: "Spam",
|
|
},
|
|
}
|
|
|
|
classifier := worker.NewPlaceholderClassifier("Other")
|
|
w := worker.NewWorker(nil, cfg, classifier)
|
|
|
|
if w == nil {
|
|
t.Fatal("NewWorker returned nil")
|
|
}
|
|
}
|
|
|
|
func TestNewPlaceholderClassifier(t *testing.T) {
|
|
c := worker.NewPlaceholderClassifier("Other")
|
|
if c == nil {
|
|
t.Fatal("NewPlaceholderClassifier returned nil")
|
|
}
|
|
|
|
folder, confidence, err := c.Classify(imap.EmailSummary{
|
|
UID: 1,
|
|
Subject: "Test",
|
|
From: "test@example.com",
|
|
})
|
|
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if folder != "Other" {
|
|
t.Errorf("expected folder 'Other', got %q", folder)
|
|
}
|
|
if confidence != 0 {
|
|
t.Errorf("expected confidence 0, got %d", confidence)
|
|
}
|
|
}
|
|
|
|
func TestPlaceholderClassifierFields(t *testing.T) {
|
|
c := worker.NewPlaceholderClassifier("Work")
|
|
if c.DefaultFolder != "Work" {
|
|
t.Errorf("expected DefaultFolder 'Work', got %q", c.DefaultFolder)
|
|
}
|
|
|
|
folder, _, _ := c.Classify(imap.EmailSummary{UID: 42})
|
|
if folder != "Work" {
|
|
t.Errorf("expected folder 'Work', got %q", folder)
|
|
}
|
|
}
|
|
|
|
func TestPlaceholderClassifierSubjectFrom(t *testing.T) {
|
|
c := worker.NewPlaceholderClassifier("Spam")
|
|
|
|
tests := []struct {
|
|
name string
|
|
email imap.EmailSummary
|
|
}{
|
|
{"normal email", imap.EmailSummary{UID: 1, Subject: "Hello", From: "friend@example.com"}},
|
|
{"empty subject", imap.EmailSummary{UID: 2, Subject: "", From: "noreply@example.com"}},
|
|
{"empty from", imap.EmailSummary{UID: 3, Subject: "Test", From: ""}},
|
|
{"all empty", imap.EmailSummary{UID: 4}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
folder, _, err := c.Classify(tt.email)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if folder != "Spam" {
|
|
t.Errorf("expected 'Spam', got %q", folder)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStartStopWorker(t *testing.T) {
|
|
database := newMockDB(t)
|
|
cfg := &config.Config{
|
|
Worker: config.WorkerConfig{
|
|
SteadyStateBatchSize: 10,
|
|
SteadyStateIntervalMinutes: 5,
|
|
CatchUpBatchSize: 50,
|
|
CatchUpCooldownSeconds: 1,
|
|
},
|
|
Folders: config.FolderConfig{
|
|
Important: "Important",
|
|
Ecommerce: "eCommerce",
|
|
Other: "Other",
|
|
Spam: "Spam",
|
|
},
|
|
}
|
|
|
|
classifier := worker.NewPlaceholderClassifier("Other")
|
|
w := worker.NewWorker(database, cfg, classifier)
|
|
|
|
// Start and immediately stop to test lifecycle
|
|
w.Start()
|
|
time.Sleep(100 * time.Millisecond) // Let it process
|
|
w.Stop()
|
|
|
|
// No assertion errors = pass
|
|
}
|
|
|
|
func TestProcessNowHandler(t *testing.T) {
|
|
// Test that calling Start/Stop multiple times is safe
|
|
database := newMockDB(t)
|
|
cfg := &config.Config{
|
|
Worker: config.WorkerConfig{
|
|
SteadyStateBatchSize: 10,
|
|
SteadyStateIntervalMinutes: 5,
|
|
CatchUpBatchSize: 50,
|
|
CatchUpCooldownSeconds: 1,
|
|
},
|
|
Folders: config.FolderConfig{
|
|
Important: "Important",
|
|
Ecommerce: "eCommerce",
|
|
Other: "Other",
|
|
Spam: "Spam",
|
|
},
|
|
}
|
|
|
|
classifier := worker.NewPlaceholderClassifier("Other")
|
|
w := worker.NewWorker(database, cfg, classifier)
|
|
|
|
// Start and stop multiple times (should be safe)
|
|
w.Start()
|
|
w.Start() // Second start should be safe
|
|
time.Sleep(50 * time.Millisecond)
|
|
w.Stop()
|
|
w.Stop() // Second stop should be safe
|
|
|
|
// Start one final time
|
|
w.Start()
|
|
time.Sleep(50 * time.Millisecond)
|
|
w.Stop()
|
|
}
|
|
|
|
func TestMockClassifier(t *testing.T) {
|
|
// Test with error
|
|
errorClassifier := &mockClassifier{
|
|
folder: "",
|
|
confidence: 0,
|
|
err: fmt.Errorf("classification failed"),
|
|
}
|
|
|
|
_, _, err := errorClassifier.Classify(imap.EmailSummary{UID: 1})
|
|
if err == nil {
|
|
t.Fatal("expected error from mock classifier")
|
|
}
|
|
|
|
// Test with specific folder
|
|
successClassifier := &mockClassifier{
|
|
folder: "Important",
|
|
confidence: 95,
|
|
err: nil,
|
|
}
|
|
|
|
folder, confidence, err := successClassifier.Classify(imap.EmailSummary{UID: 2})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if folder != "Important" {
|
|
t.Errorf("expected 'Important', got %q", folder)
|
|
}
|
|
if confidence != 95 {
|
|
t.Errorf("expected 95, got %d", confidence)
|
|
}
|
|
}
|