mcpclient.New previously accepted an empty token and silently omitted the Authorization header at request time. When the env var sourcing the token was missing from a Kubernetes Secret (envFrom doesn't warn on missing keys), this surfaced as an opaque 401 from the upstream MCP server with no log trail — see hyperguild #13 and brain entry "mcpclient-empty-token-silent-401-envfrom-missing-key". mcpclient.New now returns ErrTokenRequired when token is empty. The routing pod's project_create init checks the error and exits with a clear message pointing at routing-secrets, turning a runtime 401 storm into a startup crashloop the operator can fix immediately. Tests pass a dummy "test" token (httptest servers don't enforce bearer auth, so any non-empty value works). Added a regression test asserting empty-token construction returns ErrTokenRequired. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
93 lines
2.8 KiB
Go
93 lines
2.8 KiB
Go
package mcpclient_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/mathiasbq/supervisor/internal/mcpclient"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestNew_EmptyTokenFailsFast(t *testing.T) {
|
|
c, err := mcpclient.New("http://example.invalid", "")
|
|
require.Error(t, err)
|
|
require.Nil(t, c)
|
|
require.ErrorIs(t, err, mcpclient.ErrTokenRequired)
|
|
}
|
|
|
|
func TestCallTool_Success(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
assert.Equal(t, http.MethodPost, r.Method)
|
|
assert.Equal(t, "Bearer tok", r.Header.Get("Authorization"))
|
|
b, _ := io.ReadAll(r.Body)
|
|
var got map[string]any
|
|
_ = json.Unmarshal(b, &got)
|
|
assert.Equal(t, "tools/call", got["method"])
|
|
params := got["params"].(map[string]any)
|
|
assert.Equal(t, "x_y", params["name"])
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"{\"ok\":true,\"n\":7}"}]}}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c, err := mcpclient.New(srv.URL, "tok")
|
|
require.NoError(t, err)
|
|
var out struct {
|
|
OK bool `json:"ok"`
|
|
N int `json:"n"`
|
|
}
|
|
require.NoError(t, c.CallTool(context.Background(), "x_y", map[string]any{"a": 1}, &out))
|
|
assert.True(t, out.OK)
|
|
assert.Equal(t, 7, out.N)
|
|
}
|
|
|
|
func TestCallTool_RPCError(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"error":{"code":-32003,"message":"already exists"}}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c, err := mcpclient.New(srv.URL, "test")
|
|
require.NoError(t, err)
|
|
err = c.CallTool(context.Background(), "x", nil, nil)
|
|
require.Error(t, err)
|
|
var me *mcpclient.Error
|
|
require.True(t, errors.As(err, &me))
|
|
assert.Equal(t, -32003, me.Code)
|
|
assert.Contains(t, me.Message, "already exists")
|
|
}
|
|
|
|
func TestCallTool_HTTPError(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
_, _ = w.Write([]byte(`unauthorized`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c, err := mcpclient.New(srv.URL, "test")
|
|
require.NoError(t, err)
|
|
err = c.CallTool(context.Background(), "x", nil, nil)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "401")
|
|
}
|
|
|
|
func TestCallTool_NilResult(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"{}"}]}}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c, err := mcpclient.New(srv.URL, "test")
|
|
require.NoError(t, err)
|
|
require.NoError(t, c.CallTool(context.Background(), "x", nil, nil))
|
|
}
|