feat: add cli setup boundary tests
This commit is contained in:
parent
e09b4afa0a
commit
afec7612aa
3 changed files with 193 additions and 2 deletions
158
internal/cli/integration_test.go
Normal file
158
internal/cli/integration_test.go
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
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())
|
||||
}
|
||||
}
|
||||
|
|
@ -267,7 +267,7 @@ func (s Server) loadCredential(ctx context.Context) (secretstore.Credential, err
|
|||
|
||||
cred, err := s.store.Load(ctx, secretstore.DefaultAccountKey)
|
||||
if err != nil {
|
||||
if errors.Is(err, kwallet.ErrCredentialNotFound) {
|
||||
if errors.Is(err, kwallet.ErrCredentialNotFound) || errors.Is(err, ErrCredentialsNotConfigured) {
|
||||
return secretstore.Credential{}, ErrCredentialsNotConfigured
|
||||
}
|
||||
return secretstore.Credential{}, err
|
||||
|
|
|
|||
|
|
@ -230,6 +230,7 @@ func TestRunnerRunReturnsFriendlyMissingCredentialError(t *testing.T) {
|
|||
store := &storeStub{
|
||||
loadErr: kwallet.ErrCredentialNotFound,
|
||||
}
|
||||
output := &bytes.Buffer{}
|
||||
runner := NewRunner(New(store, serviceStub{
|
||||
listMailboxes: func(context.Context, secretstore.Credential) ([]imapclient.Mailbox, error) {
|
||||
t.Fatal("ListMailboxes should not be called")
|
||||
|
|
@ -243,12 +244,44 @@ func TestRunnerRunReturnsFriendlyMissingCredentialError(t *testing.T) {
|
|||
t.Fatal("GetMessage should not be called")
|
||||
return imapclient.Message{}, nil
|
||||
},
|
||||
}), bytes.NewBuffer(nil), &bytes.Buffer{}, &bytes.Buffer{})
|
||||
}), bytes.NewBuffer(nil), output, &bytes.Buffer{})
|
||||
|
||||
err := runner.Run(context.Background())
|
||||
if !errors.Is(err, ErrCredentialsNotConfigured) {
|
||||
t.Fatalf("expected missing credential error, got %v", err)
|
||||
}
|
||||
if output.Len() != 0 {
|
||||
t.Fatalf("expected no output when credentials are missing, got %q", output.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunnerRunReturnsFriendlyMissingCredentialErrorWhenStoreAlreadyTranslatedIt(t *testing.T) {
|
||||
store := &storeStub{
|
||||
loadErr: ErrCredentialsNotConfigured,
|
||||
}
|
||||
output := &bytes.Buffer{}
|
||||
runner := NewRunner(New(store, serviceStub{
|
||||
listMailboxes: func(context.Context, secretstore.Credential) ([]imapclient.Mailbox, error) {
|
||||
t.Fatal("ListMailboxes should not be called")
|
||||
return nil, nil
|
||||
},
|
||||
listMessages: func(context.Context, secretstore.Credential, string, int) ([]imapclient.MessageSummary, error) {
|
||||
t.Fatal("ListMessages should not be called")
|
||||
return nil, nil
|
||||
},
|
||||
getMessage: func(context.Context, secretstore.Credential, string, uint32) (imapclient.Message, error) {
|
||||
t.Fatal("GetMessage should not be called")
|
||||
return imapclient.Message{}, nil
|
||||
},
|
||||
}), bytes.NewBuffer(nil), output, &bytes.Buffer{})
|
||||
|
||||
err := runner.Run(context.Background())
|
||||
if !errors.Is(err, ErrCredentialsNotConfigured) {
|
||||
t.Fatalf("expected missing credential error, got %v", err)
|
||||
}
|
||||
if output.Len() != 0 {
|
||||
t.Fatalf("expected no output when credentials are missing, got %q", output.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerToolsAdvertiseValidatedArgumentContracts(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue