package imapclient import ( "context" "errors" "testing" "email-mcp/internal/secretstore" ) type backendStub struct { listMailboxes func(context.Context, secretstore.Credential) ([]Mailbox, error) listMessages func(context.Context, secretstore.Credential, string, int) ([]MessageSummary, error) getMessage func(context.Context, secretstore.Credential, string, uint32) (Message, error) } func (b backendStub) ListMailboxes(ctx context.Context, cred secretstore.Credential) ([]Mailbox, error) { return b.listMailboxes(ctx, cred) } func (b backendStub) ListMessages(ctx context.Context, cred secretstore.Credential, mailbox string, limit int) ([]MessageSummary, error) { return b.listMessages(ctx, cred, mailbox, limit) } func (b backendStub) GetMessage(ctx context.Context, cred secretstore.Credential, mailbox string, uid uint32) (Message, error) { return b.getMessage(ctx, cred, mailbox, uid) } func TestServiceListMailboxesUsesBackend(t *testing.T) { svc := NewService(backendStub{ listMailboxes: func(_ context.Context, cred secretstore.Credential) ([]Mailbox, error) { if cred.Host != "imap.example.com" || cred.Username != "alice" || cred.Password != "secret" { t.Fatalf("unexpected credential: %#v", cred) } return []Mailbox{{Name: "INBOX"}}, nil }, listMessages: func(context.Context, secretstore.Credential, string, int) ([]MessageSummary, error) { t.Fatal("ListMessages should not be called") return nil, nil }, getMessage: func(context.Context, secretstore.Credential, string, uint32) (Message, error) { t.Fatal("GetMessage should not be called") return Message{}, nil }, }) boxes, err := svc.ListMailboxes(context.Background(), secretstore.Credential{ Host: "imap.example.com", Username: "alice", Password: "secret", }) if err != nil { t.Fatalf("ListMailboxes returned error: %v", err) } if len(boxes) != 1 || boxes[0].Name != "INBOX" { t.Fatalf("unexpected mailboxes: %#v", boxes) } } func TestServiceListMessagesUsesBackend(t *testing.T) { svc := NewService(backendStub{ listMailboxes: func(context.Context, secretstore.Credential) ([]Mailbox, error) { t.Fatal("ListMailboxes should not be called") return nil, nil }, listMessages: func(_ context.Context, cred secretstore.Credential, mailbox string, limit int) ([]MessageSummary, error) { if cred.Host != "imap.example.com" || mailbox != "INBOX" || limit != 10 { t.Fatalf("unexpected call: cred=%#v mailbox=%q limit=%d", cred, mailbox, limit) } return []MessageSummary{{ UID: 42, Subject: "hello", From: "alice@example.com", }}, nil }, getMessage: func(context.Context, secretstore.Credential, string, uint32) (Message, error) { t.Fatal("GetMessage should not be called") return Message{}, nil }, }) messages, err := svc.ListMessages(context.Background(), secretstore.Credential{ Host: "imap.example.com", Username: "alice", Password: "secret", }, "INBOX", 10) if err != nil { t.Fatalf("ListMessages returned error: %v", err) } if len(messages) != 1 || messages[0].UID != 42 { t.Fatalf("unexpected messages: %#v", messages) } } func TestServiceGetMessageUsesBackend(t *testing.T) { svc := NewService(backendStub{ listMailboxes: func(context.Context, secretstore.Credential) ([]Mailbox, error) { t.Fatal("ListMailboxes should not be called") return nil, nil }, listMessages: func(context.Context, secretstore.Credential, string, int) ([]MessageSummary, error) { t.Fatal("ListMessages should not be called") return nil, nil }, getMessage: func(_ context.Context, cred secretstore.Credential, mailbox string, uid uint32) (Message, error) { if cred.Host != "imap.example.com" || mailbox != "INBOX" || uid != 42 { t.Fatalf("unexpected call: cred=%#v mailbox=%q uid=%d", cred, mailbox, uid) } return Message{ UID: 42, Mailbox: "INBOX", Headers: []Header{ {Name: "Received", Value: "first"}, {Name: "Received", Value: "second"}, {Name: "Subject", Value: "hello"}, }, Body: "body", }, nil }, }) message, err := svc.GetMessage(context.Background(), secretstore.Credential{ Host: "imap.example.com", Username: "alice", Password: "secret", }, "INBOX", 42) if err != nil { t.Fatalf("GetMessage returned error: %v", err) } if message.UID != 42 { t.Fatalf("unexpected message UID: %#v", message) } if len(message.Headers) != 3 { t.Fatalf("unexpected headers: %#v", message.Headers) } if message.Headers[0].Name != "Received" || message.Headers[0].Value != "first" { t.Fatalf("unexpected first header: %#v", message.Headers[0]) } if message.Headers[1].Name != "Received" || message.Headers[1].Value != "second" { t.Fatalf("unexpected second header: %#v", message.Headers[1]) } } func TestServicePropagatesBackendErrors(t *testing.T) { wantErr := errors.New("backend failed") tests := []struct { name string run func(Service) error }{ { name: "ListMailboxes", run: func(svc Service) error { _, err := svc.ListMailboxes(context.Background(), secretstore.Credential{}) return err }, }, { name: "ListMessages", run: func(svc Service) error { _, err := svc.ListMessages(context.Background(), secretstore.Credential{}, "INBOX", 10) return err }, }, { name: "GetMessage", run: func(svc Service) error { _, err := svc.GetMessage(context.Background(), secretstore.Credential{}, "INBOX", 42) return err }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { svc := NewService(backendStub{ listMailboxes: func(context.Context, secretstore.Credential) ([]Mailbox, error) { return nil, wantErr }, listMessages: func(context.Context, secretstore.Credential, string, int) ([]MessageSummary, error) { return nil, wantErr }, getMessage: func(context.Context, secretstore.Credential, string, uint32) (Message, error) { return Message{}, wantErr }, }) if err := tc.run(svc); !errors.Is(err, wantErr) { t.Fatalf("expected error %v, got %v", wantErr, err) } }) } }