Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions test/integration/auth_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,18 @@ func TestOutputConfigWithAuthHeaders(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

// Use an in-process mock backend to avoid Docker dependency
mockBackend := createMinimalMockMCPBackend(t)
defer mockBackend.Close()

// Prepare config JSON for stdin with API key
apiKey := "test-secret-key-12345"
port := 13010
configJSON := map[string]interface{}{
"mcpServers": map[string]interface{}{
"echoserver": map[string]interface{}{
"type": "local",
"container": "echo",
"type": "http",
"url": mockBackend.URL + "/mcp",
},
},
"gateway": map[string]interface{}{
Expand Down Expand Up @@ -232,18 +236,24 @@ func TestOutputConfigUnifiedMode(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

// Use in-process mock backends to avoid Docker dependency
mockBackend1 := createMinimalMockMCPBackend(t)
defer mockBackend1.Close()
mockBackend2 := createMinimalMockMCPBackend(t)
defer mockBackend2.Close()

// Prepare config JSON for stdin with API key
apiKey := "unified-test-key"
port := 13011
configJSON := map[string]interface{}{
"mcpServers": map[string]interface{}{
"server1": map[string]interface{}{
"type": "local",
"container": "echo",
"type": "http",
"url": mockBackend1.URL + "/mcp",
},
"server2": map[string]interface{}{
"type": "local",
"container": "echo",
"type": "http",
"url": mockBackend2.URL + "/mcp",
},
},
"gateway": map[string]interface{}{
Expand Down
53 changes: 45 additions & 8 deletions test/integration/binary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"

sdk "github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/stretchr/testify/require"
"time"
)

// TestBinaryInvocation_RoutedMode tests the awmg binary in routed mode
Expand Down Expand Up @@ -220,12 +222,16 @@ func TestBinaryInvocation_ConfigStdin(t *testing.T) {
"--routed",
)

// Use an in-process mock backend to avoid Docker dependency
mockBackend := createMinimalMockMCPBackend(t)
defer mockBackend.Close()

// Prepare config JSON for stdin
configJSON := map[string]interface{}{
"mcpServers": map[string]interface{}{
"testserver": map[string]interface{}{
"type": "local",
"container": "echo",
"type": "http",
"url": mockBackend.URL + "/mcp",
},
},
"gateway": map[string]interface{}{
Expand Down Expand Up @@ -385,10 +391,14 @@ func TestBinaryInvocation_PipeInputOutput(t *testing.T) {
t.Skip("Skipping binary integration test in short mode")
}

// Start a mock HTTP MCP backend so the gateway can connect without Docker
mockBackend := createMinimalMockMCPBackend(t)
defer mockBackend.Close()

binaryPath := findBinary(t)
t.Logf("Using binary: %s", binaryPath)

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

port := "13005"
Expand All @@ -398,12 +408,12 @@ func TestBinaryInvocation_PipeInputOutput(t *testing.T) {
"--unified",
)

// Prepare config JSON for stdin pipe
// Prepare config JSON for stdin pipe using the mock HTTP backend
configJSON := map[string]interface{}{
"mcpServers": map[string]interface{}{
"pipetest": map[string]interface{}{
"type": "local",
"container": "echo",
"type": "http",
"url": mockBackend.URL + "/mcp",
},
},
"gateway": map[string]interface{}{
Expand Down Expand Up @@ -433,7 +443,7 @@ func TestBinaryInvocation_PipeInputOutput(t *testing.T) {

// Wait for server to start
serverURL := "http://127.0.0.1:" + port
if !waitForServer(t, serverURL+"/health", 5*time.Second) {
if !waitForServer(t, serverURL+"/health", 15*time.Second) {
t.Logf("STDOUT: %s", stdout.String())
t.Logf("STDERR: %s", stderr.String())
t.Fatal("Server did not start in time")
Expand Down Expand Up @@ -538,6 +548,33 @@ func TestBinaryInvocation_Version(t *testing.T) {

// Helper functions

// createMinimalMockMCPBackend creates a minimal MCP HTTP server suitable for use as a
// gateway backend in tests that don't need a real Docker container. It responds correctly
// to initialize and tools/list so the gateway can register it and start the HTTP server.
func createMinimalMockMCPBackend(t *testing.T) *httptest.Server {
t.Helper()
impl := &sdk.Implementation{Name: "mock-backend", Version: "1.0.0"}
mcpServer := sdk.NewServer(impl, nil)
mcpServer.AddTool(&sdk.Tool{
Name: "mock_tool",
Description: "A mock tool for testing",
InputSchema: map[string]interface{}{"type": "object"},
}, func(_ context.Context, _ *sdk.CallToolRequest) (*sdk.CallToolResult, error) {
return &sdk.CallToolResult{
Content: []sdk.Content{&sdk.TextContent{Text: "mock response"}},
}, nil
})
handler := sdk.NewStreamableHTTPHandler(func(_ *http.Request) *sdk.Server {
return mcpServer
}, &sdk.StreamableHTTPOptions{
Stateless: false,
})
mux := http.NewServeMux()
mux.Handle("/mcp", handler)
mux.Handle("/mcp/", handler)
return httptest.NewServer(mux)
}

// findBinary locates the awmg binary
func findBinary(t *testing.T) string {
t.Helper()
Expand Down
25 changes: 24 additions & 1 deletion test/integration/github_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ func TestGitHubMCPRealBackend(t *testing.T) {

// Test 2: Initialize connection
var sessionID string
var mcpSessionID string // Mcp-Session-Id from SDK, needed for stateful session reuse
t.Run("InitializeConnection", func(t *testing.T) {
initReq := map[string]interface{}{
"jsonrpc": "2.0",
Expand Down Expand Up @@ -374,6 +375,11 @@ func TestGitHubMCPRealBackend(t *testing.T) {
body, _ := io.ReadAll(resp.Body)
t.Logf("Initialize response: %s", string(body))

// Capture Mcp-Session-Id for stateful session reuse in subsequent requests
mcpSessionID = resp.Header.Get("Mcp-Session-Id")
t.Logf("Captured Mcp-Session-Id: %q", mcpSessionID)
require.NotEmpty(t, mcpSessionID, "Mcp-Session-Id header must be present for stateful session reuse; ensure the gateway/SDK returns this header on initialize")

require.Equal(t, http.StatusOK, resp.StatusCode, "Initialize failed with status %d: %s", resp.StatusCode, string(body))

// Check if response uses SSE-formatted streaming
Expand Down Expand Up @@ -406,6 +412,9 @@ func TestGitHubMCPRealBackend(t *testing.T) {
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json, text/event-stream")
req.Header.Set("Authorization", "test-github-key")
if mcpSessionID != "" {
req.Header.Set("Mcp-Session-Id", mcpSessionID)
}
resp, err = client.Do(req)
if err == nil {
resp.Body.Close()
Expand Down Expand Up @@ -434,6 +443,9 @@ func TestGitHubMCPRealBackend(t *testing.T) {
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json, text/event-stream")
req.Header.Set("Authorization", sessionID)
if mcpSessionID != "" {
req.Header.Set("Mcp-Session-Id", mcpSessionID)
}

client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
Expand All @@ -442,11 +454,19 @@ func TestGitHubMCPRealBackend(t *testing.T) {

body, _ := io.ReadAll(resp.Body)
t.Logf("Tools list response length: %d bytes", len(body))
bodyStr := string(body)
const maxLoggedBodyLen = 1024
if len(bodyStr) > maxLoggedBodyLen {
t.Logf("Tools list response body (truncated to %d bytes): %q", maxLoggedBodyLen, bodyStr[:maxLoggedBodyLen])
} else {
t.Logf("Tools list response body: %q", bodyStr)
}

require.Equal(t, http.StatusOK, resp.StatusCode, "Tools list failed with status %d: %s", resp.StatusCode, string(body))
require.Equal(t, http.StatusOK, resp.StatusCode, "Tools list failed with status %d: %s", resp.StatusCode, bodyStr)

// Check if response uses SSE-formatted streaming
contentType := resp.Header.Get("Content-Type")
t.Logf("Tools list content-type: %q", contentType)
var result map[string]interface{}
if contentType == "text/event-stream" {
// Parse SSE-formatted response
Expand Down Expand Up @@ -583,6 +603,9 @@ func TestGitHubMCPRealBackend(t *testing.T) {
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json, text/event-stream")
req.Header.Set("Authorization", sessionID)
if mcpSessionID != "" {
req.Header.Set("Mcp-Session-Id", mcpSessionID)
}

client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
Expand Down
9 changes: 8 additions & 1 deletion test/integration/pipe_launch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import (
"strconv"
"strings"
"testing"
"time"

"github.com/stretchr/testify/require"
"time"
)

// TestPipeBasedLaunch tests launching the gateway using pipes via shell script.
Expand Down Expand Up @@ -45,6 +45,12 @@ func TestPipeBasedLaunch(t *testing.T) {
t.Skip("Skipping pipe-based launch integration test in short mode")
}

// Start a mock HTTP MCP backend so gateways don't need Docker to register tools.
// This is shared across all subtests; each subtest connects to it via HTTP.
mockBackend := createMinimalMockMCPBackend(t)
defer mockBackend.Close()
t.Logf("Mock MCP backend started at %s", mockBackend.URL)

// Find the binary
binaryPath := findBinary(t)
t.Logf("Using binary: %s", binaryPath)
Expand Down Expand Up @@ -99,6 +105,7 @@ func TestPipeBasedLaunch(t *testing.T) {
"PIPE_TYPE="+tt.pipeType,
"TIMEOUT=30",
"NO_CLEANUP=1", // Don't cleanup gateway so tests can interact with it
"MOCK_BACKEND_URL="+mockBackend.URL+"/mcp", // Use mock HTTP backend instead of Docker container
)

// Create context with timeout
Expand Down
9 changes: 5 additions & 4 deletions test/integration/playwright_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ CMD ["node", "mock-mcp-server.js"]
},
},
"gateway": map[string]interface{}{
"port": 13101,
"port": 13109,
"domain": "localhost",
"apiKey": "test-mock-key",
},
Expand All @@ -424,14 +424,15 @@ CMD ["node", "mock-mcp-server.js"]
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()

port := "13101"
port := "13109"

// Kill any stale processes on this port from previous test runs
killProcessOnPort(t, port)

cmd := exec.CommandContext(ctx, binaryPath,
"--config-stdin",
"--listen", "127.0.0.1:"+port,
"--unified",
)

cmd.Stdin = bytes.NewReader(configJSON)
Expand Down Expand Up @@ -487,7 +488,7 @@ CMD ["node", "mock-mcp-server.js"]
},
}

result := sendMCPRequest(t, serverURL+"/mcp/mock-playwright", "test-mock-key", initReq)
result := sendMCPRequest(t, serverURL+"/mcp", "test-mock-key", initReq)

// Verify initialize succeeded
if _, ok := result["error"]; ok {
Expand All @@ -502,7 +503,7 @@ CMD ["node", "mock-mcp-server.js"]
"params": map[string]interface{}{},
}

result = sendMCPRequest(t, serverURL+"/mcp/mock-playwright", "test-mock-key", listReq)
result = sendMCPRequest(t, serverURL+"/mcp", "test-mock-key", listReq)

// The key test is that we didn't panic, not whether tools/list works perfectly
// Check if we got any tools registered (may be zero if backend connection failed)
Expand Down
11 changes: 9 additions & 2 deletions test/integration/safeinputs_http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ func TestSafeinputsHTTPBackend(t *testing.T) {

cmd := exec.CommandContext(ctx, binaryPath,
"--config-stdin",
"--listen", "127.0.0.1:3001",
"--routed",
)
Comment on lines 168 to 172
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test hard-codes port 3001 for the gateway listen address, but doesn’t free it (e.g., via killProcessOnPort) or pick an ephemeral port. This can make the test flaky if 3001 is already in use on a dev machine/CI runner. Consider selecting a free port at runtime (net.Listen("127.0.0.1:0") → close → reuse) or calling killProcessOnPort(t, "3001") before starting the gateway, and use that same port consistently in both configJSON and --listen.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback


Expand All @@ -185,8 +186,14 @@ func TestSafeinputsHTTPBackend(t *testing.T) {
cmd.Wait()
}()

// Wait for gateway to start and read the configuration output
time.Sleep(2 * time.Second)
// Wait for the gateway HTTP server to be ready
if !waitForServer(t, "http://127.0.0.1:3001/health", 20*time.Second) {
t.Logf("STDOUT: %s", stdout.String())
t.Logf("STDERR: %s", stderr.String())
t.Fatal("Gateway did not start in time")
}
// Small delay to ensure stdout JSON is written
time.Sleep(200 * time.Millisecond)

// Parse the gateway output to get the actual port
var gatewayConfig struct {
Expand Down
Loading
Loading