2026-04-10 10:35:08 +00:00
|
|
|
package cli
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"email-mcp/internal/imapclient"
|
|
|
|
|
"email-mcp/internal/mcpserver"
|
|
|
|
|
"email-mcp/internal/secretstore"
|
|
|
|
|
"email-mcp/internal/secretstore/kwallet"
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-10 10:39:10 +00:00
|
|
|
type entrypointPromptStub struct {
|
2026-04-10 10:35:08 +00:00
|
|
|
credential secretstore.Credential
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 10:39:10 +00:00
|
|
|
func (p *entrypointPromptStub) PromptSetup(context.Context) (secretstore.Credential, error) {
|
2026-04-10 10:35:08 +00:00
|
|
|
return p.credential, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 10:39:10 +00:00
|
|
|
type entrypointStoreStub struct {
|
|
|
|
|
saveErr error
|
|
|
|
|
loadErr error
|
2026-04-10 10:35:08 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 10:39:10 +00:00
|
|
|
func (s *entrypointStoreStub) Save(context.Context, string, secretstore.Credential) error {
|
|
|
|
|
return s.saveErr
|
2026-04-10 10:35:08 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 10:39:10 +00:00
|
|
|
func (s *entrypointStoreStub) Load(context.Context, string) (secretstore.Credential, error) {
|
|
|
|
|
return secretstore.Credential{}, s.loadErr
|
2026-04-10 10:35:08 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 10:39:10 +00:00
|
|
|
type entrypointMailServiceStub struct{}
|
2026-04-10 10:35:08 +00:00
|
|
|
|
2026-04-10 10:39:10 +00:00
|
|
|
func (entrypointMailServiceStub) ListMailboxes(context.Context, secretstore.Credential) ([]imapclient.Mailbox, error) {
|
2026-04-10 10:35:08 +00:00
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 10:39:10 +00:00
|
|
|
func (entrypointMailServiceStub) ListMessages(context.Context, secretstore.Credential, string, int) ([]imapclient.MessageSummary, error) {
|
2026-04-10 10:35:08 +00:00
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 10:39:10 +00:00
|
|
|
func (entrypointMailServiceStub) GetMessage(context.Context, secretstore.Credential, string, uint32) (imapclient.Message, error) {
|
2026-04-10 10:35:08 +00:00
|
|
|
return imapclient.Message{}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 10:39:10 +00:00
|
|
|
func TestExecuteSetupWritesWalletGuidanceAndReturnsExitCodeOne(t *testing.T) {
|
|
|
|
|
app := NewAppWithDependencies(
|
|
|
|
|
&entrypointPromptStub{
|
|
|
|
|
credential: secretstore.Credential{
|
|
|
|
|
Host: "imap.example.com",
|
|
|
|
|
Username: "alice",
|
|
|
|
|
Password: "secret",
|
|
|
|
|
},
|
2026-04-10 10:35:08 +00:00
|
|
|
},
|
2026-04-10 10:39:10 +00:00
|
|
|
&entrypointStoreStub{saveErr: kwallet.ErrKWalletUnavailable},
|
|
|
|
|
nil,
|
|
|
|
|
nil,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
stderr := &bytes.Buffer{}
|
|
|
|
|
if code := Execute(app, []string{"setup"}, stderr); code != 1 {
|
|
|
|
|
t.Fatalf("expected exit code 1, got %d", code)
|
2026-04-10 10:35:08 +00:00
|
|
|
}
|
2026-04-10 10:39:10 +00:00
|
|
|
if got := stderr.String(); got != "kwallet is not available; make sure KDE Wallet is installed and your session D-Bus is running\n" {
|
|
|
|
|
t.Fatalf("unexpected stderr: %q", got)
|
2026-04-10 10:35:08 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 13:46:53 +00:00
|
|
|
func TestExecuteMCPReturnsMissingCredentialErrorOnToolCall(t *testing.T) {
|
2026-04-10 10:39:10 +00:00
|
|
|
store := &entrypointStoreStub{loadErr: kwallet.ErrCredentialNotFound}
|
|
|
|
|
mail := entrypointMailServiceStub{}
|
2026-04-10 13:46:53 +00:00
|
|
|
input := bytes.NewBufferString(
|
|
|
|
|
"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"test\",\"version\":\"1.0.0\"}}}\n" +
|
|
|
|
|
"{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}\n" +
|
|
|
|
|
"{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"list_mailboxes\"}}\n",
|
|
|
|
|
)
|
|
|
|
|
output := &bytes.Buffer{}
|
|
|
|
|
runner := mcpserver.NewRunner(mcpserver.New(store, mail), input, output, &bytes.Buffer{})
|
2026-04-10 10:39:10 +00:00
|
|
|
app := NewAppWithDependencies(nil, store, runner, nil)
|
2026-04-10 10:35:08 +00:00
|
|
|
|
2026-04-10 10:39:10 +00:00
|
|
|
stderr := &bytes.Buffer{}
|
2026-04-10 13:46:53 +00:00
|
|
|
if code := Execute(app, []string{"mcp"}, stderr); code != 0 {
|
|
|
|
|
t.Fatalf("expected exit code 0, got %d; stderr: %s", code, stderr.String())
|
2026-04-10 10:35:08 +00:00
|
|
|
}
|
2026-04-10 13:46:53 +00:00
|
|
|
|
|
|
|
|
// Verify the credential error appears in the tool call response
|
|
|
|
|
got := output.String()
|
|
|
|
|
if !bytes.Contains([]byte(got), []byte("credentials not configured")) {
|
|
|
|
|
t.Fatalf("expected credential error in output, got %q", got)
|
2026-04-10 10:35:08 +00:00
|
|
|
}
|
|
|
|
|
}
|