Files
oikos/internal/mcp/secrets_tools_test.go
dtoro e3449b24c1
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
ci / web (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
feat: wire Infisical secret store into API server and MCP tools
- Wire secretsManager in NewHandler() — instantiate InfisicalBackend
  when OIKOS_INFISICAL_SITE_URL is set (previously always nil)
- Add get_secret, list_secrets, set_secret MCP tools with nil-backend
  graceful degradation
- Add oikos secret get|set|list CLI subcommands for Infisical
- Fix Set() bug: create-before-update so new keys are created;
  add Type: "shared" to Update so it finds the right secret;
  disable SDK cache so Get returns fresh data after Set
- Clean enrollment response: remove fake infisical_client_id/
  infisical_client_secret stubs, store age key in Infisical for real
2026-08-05 23:03:27 +02:00

201 lines
5.5 KiB
Go

package mcp
import (
"context"
"encoding/json"
"testing"
"github.com/google/uuid"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// mockSecretBackend implements secretBackend for testing.
type mockSecretBackend struct {
data map[string]string
}
func (m *mockSecretBackend) Get(ctx context.Context, key string) (string, error) {
v, ok := m.data[key]
if !ok {
return "", &secretErr{msg: "secret not found: " + key}
}
return v, nil
}
func (m *mockSecretBackend) Set(ctx context.Context, key string, value string) error {
m.data[key] = value
return nil
}
func (m *mockSecretBackend) List(ctx context.Context) ([]string, error) {
keys := make([]string, 0, len(m.data))
for k := range m.data {
keys = append(keys, k)
}
return keys, nil
}
type secretErr struct{ msg string }
func (e *secretErr) Error() string { return e.msg }
// findToolHandler locates a tool's handler from allTools by name.
func findToolHandler(t *testing.T, pool interface{}, name string, sec secretBackend) func(context.Context, *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
t.Helper()
for _, r := range allTools(nil, uuid.Nil, sec) {
if r.tool.Name == name {
return r.handler
}
}
t.Fatalf("tool %q not found", name)
return nil
}
func callToolJSON(t *testing.T, name string, sec secretBackend, args map[string]any) any {
t.Helper()
handler := findToolHandler(t, nil, name, sec)
argBytes, _ := json.Marshal(args)
req := &mcp.CallToolRequest{
Params: &mcp.CallToolParamsRaw{Arguments: argBytes},
}
result, err := handler(context.Background(), req)
if err != nil {
t.Fatalf("tool %q error: %v", name, err)
}
if len(result.Content) == 0 {
t.Fatalf("tool %q returned no content", name)
}
tc := result.Content[0].(*mcp.TextContent)
var out any
if err := json.Unmarshal([]byte(tc.Text), &out); err != nil {
// Not JSON — return raw string
return tc.Text
}
return out
}
func callToolText(t *testing.T, name string, sec secretBackend, args map[string]any) string {
t.Helper()
handler := findToolHandler(t, nil, name, sec)
argBytes, _ := json.Marshal(args)
req := &mcp.CallToolRequest{
Params: &mcp.CallToolParamsRaw{Arguments: argBytes},
}
result, err := handler(context.Background(), req)
if err != nil {
t.Fatalf("tool %q error: %v", name, err)
}
if len(result.Content) == 0 {
t.Fatalf("tool %q returned no content", name)
}
return result.Content[0].(*mcp.TextContent).Text
}
func TestGetSecret(t *testing.T) {
sec := &mockSecretBackend{
data: map[string]string{
"matrix-token": "bot-token-123",
"clients/host:hubris/age-key": "AGE-SECRET-KEY",
},
}
// Get existing key
val := callToolText(t, "get_secret", sec, map[string]any{"key": "matrix-token"})
if val != "bot-token-123" {
t.Errorf("get_secret = %q, want bot-token-123", val)
}
// Get missing key
errText := callToolText(t, "get_secret", sec, map[string]any{"key": "nonexistent"})
if errText == "" {
t.Error("expected error for missing key")
}
// Missing key arg
errText = callToolText(t, "get_secret", sec, map[string]any{})
if errText != "error: key is required" {
t.Errorf("missing key error = %q, want error: key is required", errText)
}
}
func TestListSecrets(t *testing.T) {
sec := &mockSecretBackend{
data: map[string]string{
"clients/host:hubris/age-key": "val1",
"clients/host:strong/age-key": "val2",
"shared/matrix-token": "val3",
},
}
// List all
out := callToolJSON(t, "list_secrets", sec, map[string]any{})
keys, ok := out.([]any)
if !ok {
t.Fatalf("list_secrets returned non-array: %T", out)
}
if len(keys) != 3 {
t.Errorf("list_secrets count = %d, want 3", len(keys))
}
// List with prefix filter
out = callToolJSON(t, "list_secrets", sec, map[string]any{"path_prefix": "clients/"})
keys, ok = out.([]any)
if !ok {
t.Fatalf("filtered list returned non-array: %T", out)
}
if len(keys) != 2 {
t.Errorf("filtered list count = %d, want 2", len(keys))
}
}
func TestSetSecret(t *testing.T) {
sec := &mockSecretBackend{
data: map[string]string{},
}
// Set a key
result := callToolText(t, "set_secret", sec, map[string]any{"key": "test-key", "value": "test-value"})
if result != "secret test-key stored" {
t.Errorf("set_secret = %q, want 'secret test-key stored'", result)
}
// Verify it was stored
val, err := sec.Get(context.Background(), "test-key")
if err != nil {
t.Fatalf("verify get: %v", err)
}
if val != "test-value" {
t.Errorf("stored value = %q, want test-value", val)
}
// Missing key
errText := callToolText(t, "set_secret", sec, map[string]any{})
if errText != "error: key is required" {
t.Errorf("missing key error = %q", errText)
}
// Missing value
errText = callToolText(t, "set_secret", sec, map[string]any{"key": "x"})
if errText != "error: value is required" {
t.Errorf("missing value error = %q", errText)
}
}
func TestSecretToolsNilBackend(t *testing.T) {
// All tools should return a graceful error when no backend is configured
errText := callToolText(t, "get_secret", nil, map[string]any{"key": "x"})
if errText != "error: no secrets backend configured (set OIKOS_INFISICAL_SITE_URL)" {
t.Errorf("nil backend get_secret = %q", errText)
}
errText = callToolText(t, "list_secrets", nil, map[string]any{})
if errText != "error: no secrets backend configured (set OIKOS_INFISICAL_SITE_URL)" {
t.Errorf("nil backend list_secrets = %q", errText)
}
errText = callToolText(t, "set_secret", nil, map[string]any{"key": "x", "value": "y"})
if errText != "error: no secrets backend configured (set OIKOS_INFISICAL_SITE_URL)" {
t.Errorf("nil backend set_secret = %q", errText)
}
}