2026-04-10 09:11:01 +00:00
|
|
|
package imapclient
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2026-04-10 09:17:36 +00:00
|
|
|
"errors"
|
2026-04-10 09:11:01 +00:00
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"email-mcp/internal/secretstore"
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-10 09:17:36 +00:00
|
|
|
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)
|
|
|
|
|
}
|
2026-04-10 09:11:01 +00:00
|
|
|
|
2026-04-10 09:17:36 +00:00
|
|
|
func (b backendStub) ListMailboxes(ctx context.Context, cred secretstore.Credential) ([]Mailbox, error) {
|
|
|
|
|
return b.listMailboxes(ctx, cred)
|
2026-04-10 09:11:01 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 09:17:36 +00:00
|
|
|
func (b backendStub) ListMessages(ctx context.Context, cred secretstore.Credential, mailbox string, limit int) ([]MessageSummary, error) {
|
|
|
|
|
return b.listMessages(ctx, cred, mailbox, limit)
|
2026-04-10 09:11:01 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 09:17:36 +00:00
|
|
|
func (b backendStub) GetMessage(ctx context.Context, cred secretstore.Credential, mailbox string, uid uint32) (Message, error) {
|
|
|
|
|
return b.getMessage(ctx, cred, mailbox, uid)
|
2026-04-10 09:11:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestServiceListMailboxesUsesBackend(t *testing.T) {
|
2026-04-10 09:17:36 +00:00
|
|
|
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
|
|
|
|
|
},
|
|
|
|
|
})
|
2026-04-10 09:11:01 +00:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-10 09:17:36 +00:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|