Files
hyperguild/internal/mcpclient/client_test.go
Mathias 5950ef5f0f
All checks were successful
CI / Lint / Test / Vet (push) Successful in 10s
CI / Mirror to GitHub (push) Successful in 4s
feat(mcpclient): fail-fast on empty bearer token
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>
2026-05-18 16:28:09 +02:00

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))
}