package cli import ( "bytes" "context" "io" "strings" "testing" "email-mcp/internal/imapclient" "email-mcp/internal/mcpserver" "email-mcp/internal/secretstore" "email-mcp/internal/secretstore/kwallet" ) 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") stdout := &bytes.Buffer{} stderr := &bytes.Buffer{} prompter := &promptStub{ credential: secretstore.Credential{ Host: "imap.example.com", Username: "alice", Password: "secret", }, } 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) } 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) } 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") } 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") } }