Files
CyberStrikeAI/internal/handler/external_mcp_activation_test.go
T

130 lines
4.1 KiB
Go

package handler
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
"cyberstrike-ai/internal/config"
)
func TestMCPEditPreservesStoppedState(t *testing.T) {
router, h, path := setupTestRouter()
defer cleanupTestConfig(path)
defer h.manager.StopAll()
var calls atomic.Int64
called := make(chan struct{}, 16)
endpoint := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls.Add(1)
select {
case called <- struct{}{}:
default:
}
http.Error(w, "test endpoint", 503)
}))
defer endpoint.Close()
cfg := config.ExternalMCPServerConfig{Type: "http", URL: endpoint.URL, Description: "before"}
h.config.ExternalMCP.Servers["stopped"] = cfg
if err := h.manager.AddOrUpdateConfig("stopped", cfg); err != nil {
t.Fatal(err)
}
request := func(method, url string, body []byte) *httptest.ResponseRecorder {
t.Helper()
w := httptest.NewRecorder()
r := httptest.NewRequest(method, url, bytes.NewReader(body))
r.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, r)
if w.Code != 200 {
t.Fatalf("%s %s: %d %s", method, url, w.Code, w.Body)
}
return w
}
request("POST", "/api/external-mcp/stopped/stop", nil)
// Shape emitted by the edit UI: enable omitted, descriptive metadata changed.
body, _ := json.Marshal(map[string]any{"config": map[string]any{"type": "http", "url": endpoint.URL, "description": "after"}})
request("PUT", "/api/external-mcp/stopped", body)
if h.manager.GetConfigs()["stopped"].ExternalMCPEnable {
t.Fatal("metadata edit enabled a stopped MCP")
}
persisted, err := config.Load(path)
if err != nil {
t.Fatal(err)
}
if persisted.ExternalMCP.Servers["stopped"].ExternalMCPEnable {
t.Fatal("stop state lost on config reload")
}
select {
case <-called:
t.Fatal("metadata edit initiated a connection")
case <-time.After(100 * time.Millisecond):
}
if calls.Load() != 0 {
t.Fatal("stopped edit started external work")
}
request("POST", "/api/external-mcp/stopped/start", nil)
select {
case <-called:
case <-time.After(3 * time.Second):
t.Fatal("explicit Start did not connect")
}
if !h.manager.GetConfigs()["stopped"].ExternalMCPEnable {
t.Fatal("explicit Start stayed disabled")
}
response := request("GET", "/api/external-mcp/stopped", nil)
var server ExternalMCPResponse
if err := json.Unmarshal(response.Body.Bytes(), &server); err != nil {
t.Fatal(err)
}
if server.Config.Disabled {
t.Fatal("Start left a stale disabled field in the edit response")
}
// Round-trip the actual GET payload just as the UI does, removing enable.
encoded, _ := json.Marshal(server.Config)
var edit map[string]any
_ = json.Unmarshal(encoded, &edit)
delete(edit, "external_mcp_enable")
edit["description"] = "after explicit start"
body, _ = json.Marshal(map[string]any{"config": edit})
request("PUT", "/api/external-mcp/stopped", body)
if !h.manager.GetConfigs()["stopped"].ExternalMCPEnable {
t.Fatal("edit after Start stopped the MCP")
}
persisted, err = config.Load(path)
if err != nil {
t.Fatal(err)
}
if !persisted.ExternalMCP.Servers["stopped"].ExternalMCPEnable {
t.Fatal("explicit Start not persisted")
}
}
func TestMCPRequestActivationPresence(t *testing.T) {
for _, tc := range []struct {
name, fields string
previous, exists, want bool
}{
{"new default", "", false, false, true},
{"stopped edit", "", false, true, false},
{"running edit", "", true, true, true},
{"explicit enable", `,"external_mcp_enable":true`, false, true, true},
{"explicit stop", `,"external_mcp_enable":false`, true, true, false},
{"explicit disabled", `,"disabled":true`, true, true, false},
{"explicit enabled official", `,"disabled":false`, false, true, true},
{"disabled wins", `,"disabled":true,"external_mcp_enable":true`, true, true, false},
} {
t.Run(tc.name, func(t *testing.T) {
var r AddOrUpdateExternalMCPRequest
if err := json.Unmarshal([]byte(`{"config":{"description":"edit"`+tc.fields+`}}`), &r); err != nil {
t.Fatal(err)
}
if got := r.activation(tc.previous, tc.exists); got != tc.want {
t.Fatalf("activation=%v want=%v", got, tc.want)
}
})
}
}