package cli import ( "bytes" "context" "errors" "io" "testing" "email-mcp/internal/imapclient" "email-mcp/internal/mcpserver" "email-mcp/internal/secretstore" "email-mcp/internal/secretstore/kwallet" ) type integrationPromptStub struct { credential secretstore.Credential called bool } func (p *integrationPromptStub) PromptSetup(context.Context) (secretstore.Credential, error) { p.called = true return p.credential, nil } type integrationStoreStub struct { saved secretstore.Credential savedKey string saveCalled bool loadErr error } func (s *integrationStoreStub) Save(_ context.Context, key string, cred secretstore.Credential) error { s.saveCalled = true s.savedKey = key s.saved = cred return nil } func (s *integrationStoreStub) Load(context.Context, string) (secretstore.Credential, error) { if s.loadErr != nil { return secretstore.Credential{}, s.loadErr } return s.saved, nil } type integrationMailServiceStub struct{} func (integrationMailServiceStub) ListMailboxes(context.Context, secretstore.Credential) ([]imapclient.Mailbox, error) { return nil, nil } func (integrationMailServiceStub) ListMessages(context.Context, secretstore.Credential, string, int) ([]imapclient.MessageSummary, error) { return nil, nil } func (integrationMailServiceStub) GetMessage(context.Context, secretstore.Credential, string, uint32) (imapclient.Message, error) { return imapclient.Message{}, nil } type integrationRunnerStub struct { called bool err error } func (r *integrationRunnerStub) Run(context.Context) error { r.called = true return r.err } func TestAppRunSetupPersistsPromptedCredential(t *testing.T) { prompter := &integrationPromptStub{ credential: secretstore.Credential{ Host: "imap.example.com", Username: "alice", Password: "secret", }, } store := &integrationStoreStub{} runner := &integrationRunnerStub{} app := buildApp(bytes.NewBuffer(nil), bytes.NewBuffer(nil), bytes.NewBuffer(nil), runtimeFactories{ newPrompter: func(io.Reader, io.Writer) SetupPrompter { return prompter }, newWalletClient: func() kwallet.Client { return nil }, newStore: func(kwallet.Client) secretstore.Store { return store }, newMailService: func() mcpserver.MailService { return integrationMailServiceStub{} }, newRunner: func(secretstore.Store, mcpserver.MailService, io.Reader, io.Writer, io.Writer) MCPRunner { 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 setup to persist credentials") } if store.savedKey != secretstore.DefaultAccountKey { t.Fatalf("expected save key %q, got %q", secretstore.DefaultAccountKey, store.savedKey) } if store.saved != prompter.credential { t.Fatalf("unexpected saved credential: %#v", store.saved) } if runner.called { t.Fatal("setup should not invoke the MCP runner") } } func TestAppRunMCPReturnsSetupHintWhenCredentialsAreMissing(t *testing.T) { store := &integrationStoreStub{ loadErr: kwallet.ErrCredentialNotFound, } output := &bytes.Buffer{} mail := integrationMailServiceStub{} app := buildApp(bytes.NewBuffer(nil), output, &bytes.Buffer{}, runtimeFactories{ newPrompter: func(io.Reader, io.Writer) SetupPrompter { return &integrationPromptStub{} }, newWalletClient: func() kwallet.Client { return nil }, newStore: func(kwallet.Client) secretstore.Store { return store }, newMailService: func() mcpserver.MailService { return mail }, newRunner: func(secretstore.Store, mcpserver.MailService, io.Reader, io.Writer, io.Writer) MCPRunner { return mcpserver.NewRunner(mcpserver.New(store, mail), nil, output, &bytes.Buffer{}) }, }) err := app.Run([]string{"mcp"}) if err == nil { t.Fatal("expected error") } if !errors.Is(err, mcpserver.ErrCredentialsNotConfigured) { t.Fatalf("expected credentials-not-configured sentinel, got %v", err) } if err.Error() != "credentials not configured; run `email-mcp setup`" { t.Fatalf("unexpected user-facing error: %q", err.Error()) } if output.Len() != 0 { t.Fatalf("expected no MCP protocol output on missing credentials, got %q", output.String()) } }