2026-04-10 08:17:38 +00:00
|
|
|
package cli
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
2026-04-10 10:21:58 +00:00
|
|
|
"context"
|
|
|
|
|
"io"
|
2026-04-10 08:17:38 +00:00
|
|
|
"strings"
|
|
|
|
|
"testing"
|
2026-04-10 10:21:58 +00:00
|
|
|
|
|
|
|
|
"email-mcp/internal/imapclient"
|
|
|
|
|
"email-mcp/internal/mcpserver"
|
|
|
|
|
"email-mcp/internal/secretstore"
|
|
|
|
|
"email-mcp/internal/secretstore/kwallet"
|
2026-04-10 08:17:38 +00:00
|
|
|
)
|
|
|
|
|
|
2026-04-10 10:21:58 +00:00
|
|
|
type walletClientStub struct{}
|
|
|
|
|
|
|
|
|
|
func (walletClientStub) IsAvailable(context.Context) error {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (walletClientStub) Open(context.Context) error {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (walletClientStub) WriteEntry(context.Context, string, []byte) error {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (walletClientStub) ReadEntry(context.Context, string) ([]byte, error) {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type wireStoreStub struct {
|
|
|
|
|
saved secretstore.Credential
|
|
|
|
|
savedKey string
|
|
|
|
|
saveCalled bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *wireStoreStub) Save(_ context.Context, key string, cred secretstore.Credential) error {
|
|
|
|
|
s.saveCalled = true
|
|
|
|
|
s.savedKey = key
|
|
|
|
|
s.saved = cred
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *wireStoreStub) Load(context.Context, string) (secretstore.Credential, error) {
|
|
|
|
|
return secretstore.Credential{}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type wireMailServiceStub struct{}
|
|
|
|
|
|
|
|
|
|
func (wireMailServiceStub) ListMailboxes(context.Context, secretstore.Credential) ([]imapclient.Mailbox, error) {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (wireMailServiceStub) ListMessages(context.Context, secretstore.Credential, string, int) ([]imapclient.MessageSummary, error) {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (wireMailServiceStub) GetMessage(context.Context, secretstore.Credential, string, uint32) (imapclient.Message, error) {
|
|
|
|
|
return imapclient.Message{}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestBuildAppWiresSetupAndMCPCommands(t *testing.T) {
|
|
|
|
|
stdin := strings.NewReader("unused")
|
2026-04-10 08:17:38 +00:00
|
|
|
stdout := &bytes.Buffer{}
|
|
|
|
|
stderr := &bytes.Buffer{}
|
|
|
|
|
|
2026-04-10 10:21:58 +00:00
|
|
|
prompter := &promptStub{
|
|
|
|
|
credential: secretstore.Credential{
|
|
|
|
|
Host: "imap.example.com",
|
|
|
|
|
Username: "alice",
|
|
|
|
|
Password: "secret",
|
|
|
|
|
},
|
2026-04-10 08:17:38 +00:00
|
|
|
}
|
2026-04-10 10:21:58 +00:00
|
|
|
store := &wireStoreStub{}
|
|
|
|
|
mail := wireMailServiceStub{}
|
|
|
|
|
runner := &runnerStub{}
|
|
|
|
|
walletClient := &walletClientStub{}
|
|
|
|
|
|
|
|
|
|
var gotStoreClient kwallet.Client
|
|
|
|
|
var gotRunnerStore secretstore.Store
|
|
|
|
|
var gotRunnerMail mcpserver.MailService
|
|
|
|
|
|
|
|
|
|
app := buildApp(stdin, stdout, stderr, runtimeFactories{
|
|
|
|
|
newPrompter: func(in io.Reader, errOut io.Writer) SetupPrompter {
|
|
|
|
|
if in != stdin {
|
|
|
|
|
t.Fatalf("expected stdin to be forwarded to prompter")
|
|
|
|
|
}
|
|
|
|
|
if errOut != stderr {
|
|
|
|
|
t.Fatalf("expected stderr to be forwarded to prompter")
|
|
|
|
|
}
|
|
|
|
|
return prompter
|
|
|
|
|
},
|
|
|
|
|
newWalletClient: func() kwallet.Client {
|
|
|
|
|
return walletClient
|
|
|
|
|
},
|
|
|
|
|
newStore: func(client kwallet.Client) secretstore.Store {
|
|
|
|
|
gotStoreClient = client
|
|
|
|
|
return store
|
|
|
|
|
},
|
|
|
|
|
newMailService: func() mcpserver.MailService {
|
|
|
|
|
return mail
|
|
|
|
|
},
|
|
|
|
|
newRunner: func(store secretstore.Store, mail mcpserver.MailService, in io.Reader, out io.Writer, errOut io.Writer) MCPRunner {
|
|
|
|
|
gotRunnerStore = store
|
|
|
|
|
gotRunnerMail = mail
|
|
|
|
|
if in != stdin {
|
|
|
|
|
t.Fatalf("expected stdin to be forwarded to runner")
|
|
|
|
|
}
|
|
|
|
|
if out != stdout {
|
|
|
|
|
t.Fatalf("expected stdout to be forwarded to runner")
|
|
|
|
|
}
|
|
|
|
|
if errOut != stderr {
|
|
|
|
|
t.Fatalf("expected stderr to be forwarded to runner")
|
|
|
|
|
}
|
|
|
|
|
return runner
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if err := app.Run([]string{"setup"}); err != nil {
|
|
|
|
|
t.Fatalf("setup returned error: %v", err)
|
2026-04-10 08:17:38 +00:00
|
|
|
}
|
2026-04-10 10:21:58 +00:00
|
|
|
if !prompter.called {
|
|
|
|
|
t.Fatal("expected setup prompter to be called")
|
|
|
|
|
}
|
|
|
|
|
if !store.saveCalled {
|
|
|
|
|
t.Fatal("expected store Save to be called")
|
|
|
|
|
}
|
|
|
|
|
if store.savedKey != secretstore.DefaultAccountKey {
|
|
|
|
|
t.Fatalf("expected setup to save %q, got %q", secretstore.DefaultAccountKey, store.savedKey)
|
|
|
|
|
}
|
|
|
|
|
if store.saved.Host != "imap.example.com" || store.saved.Username != "alice" || store.saved.Password != "secret" {
|
|
|
|
|
t.Fatalf("unexpected saved credential: %#v", store.saved)
|
2026-04-10 08:17:38 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 10:21:58 +00:00
|
|
|
if err := app.Run([]string{"mcp"}); err != nil {
|
|
|
|
|
t.Fatalf("mcp returned error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if !runner.called {
|
|
|
|
|
t.Fatal("expected MCP runner to be called")
|
|
|
|
|
}
|
|
|
|
|
if gotStoreClient != walletClient {
|
|
|
|
|
t.Fatal("expected wallet client to be passed to the store factory")
|
2026-04-10 08:17:38 +00:00
|
|
|
}
|
2026-04-10 10:21:58 +00:00
|
|
|
if gotRunnerStore != store {
|
|
|
|
|
t.Fatal("expected runner to receive the assembled store")
|
|
|
|
|
}
|
|
|
|
|
if gotRunnerMail != mail {
|
|
|
|
|
t.Fatal("expected runner to receive the assembled mail service")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestBuildAppReturnsConfiguredApp(t *testing.T) {
|
|
|
|
|
app := BuildApp()
|
|
|
|
|
if app == nil {
|
|
|
|
|
t.Fatal("expected app instance")
|
2026-04-10 08:17:38 +00:00
|
|
|
}
|
|
|
|
|
}
|