inboxer/src/internal/worker/worker_test.go
cclohmar 8bb9ff067b Phase 2: IMAP Client & Background Worker (Version: 2026-04.2)
- 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
2026-04-23 11:02:02 +00:00

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)
}
}