208 lines
5.6 KiB
Go
208 lines
5.6 KiB
Go
package x402
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
x402 "github.com/x402-foundation/x402/go/v2"
|
|
x402http "github.com/x402-foundation/x402/go/v2/http"
|
|
)
|
|
|
|
type DummyAuthProvider struct{}
|
|
|
|
func (d *DummyAuthProvider) GetAuthHeaders(ctx context.Context) (x402http.AuthHeaders, error) {
|
|
return x402http.AuthHeaders{}, nil
|
|
}
|
|
|
|
func TestX402Server_HTTPBoundaries(t *testing.T) {
|
|
mockFacilitator := setupMockFacilitator()
|
|
defer mockFacilitator.Close()
|
|
|
|
srv, err := NewX402Server(
|
|
"Test Agent API",
|
|
"1.0.0",
|
|
mockFacilitator.URL,
|
|
&DummyAuthProvider{},
|
|
X402Middlewares{},
|
|
func(next http.Handler) http.Handler { return next },
|
|
func(w http.ResponseWriter, r *http.Request, resp *x402.SettleResponse) {},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("failed to create server: %v", err)
|
|
}
|
|
|
|
srv.AddPayableTool(
|
|
mcp.Tool{
|
|
Name: "getTestPost",
|
|
Description: "Gated test endpoint",
|
|
},
|
|
func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{&mcp.TextContent{Text: `{"status": "success"}`}},
|
|
}, nil
|
|
},
|
|
x402.PaymentRequirements{
|
|
Scheme: "exact",
|
|
Network: "eip155:8453",
|
|
Asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
Amount: "1000",
|
|
PayTo: "0x0000000000000000000000000000000000000000",
|
|
},
|
|
)
|
|
|
|
reqRec := httptest.NewRecorder()
|
|
reqHealth, _ := http.NewRequest("GET", "/healthz", nil)
|
|
srv.mux.ServeHTTP(reqRec, reqHealth)
|
|
if reqRec.Code != http.StatusOK {
|
|
t.Errorf("expected healthz status 200, got %d", reqRec.Code)
|
|
}
|
|
|
|
reqRecOpenAPI := httptest.NewRecorder()
|
|
reqOpenAPI, _ := http.NewRequest("GET", "/openapi.json", nil)
|
|
srv.mux.ServeHTTP(reqRecOpenAPI, reqOpenAPI)
|
|
if reqRecOpenAPI.Code != http.StatusOK {
|
|
t.Errorf("expected OpenAPI status 200, got %d", reqRecOpenAPI.Code)
|
|
}
|
|
if !strings.Contains(reqRecOpenAPI.Body.String(), "getTestPost") {
|
|
t.Error("OpenAPI spec did not render registered tool definitions")
|
|
}
|
|
|
|
reqRecPayable := httptest.NewRecorder()
|
|
reqPayable, _ := http.NewRequest("POST", "/v1/getTestPost", strings.NewReader(`{}`))
|
|
srv.mux.ServeHTTP(reqRecPayable, reqPayable)
|
|
|
|
if reqRecPayable.Code != http.StatusPaymentRequired {
|
|
t.Errorf("expected status 402 (Payment Required), got %d", reqRecPayable.Code)
|
|
}
|
|
|
|
payHeader := reqRecPayable.Header().Get("PAYMENT-REQUIRED")
|
|
if payHeader == "" {
|
|
t.Error("missing PAYMENT-REQUIRED response header on 402 challenge")
|
|
}
|
|
}
|
|
|
|
func TestX402Server_MCPBoundaries(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
serverTransport, clientTransport := mcp.NewInMemoryTransports()
|
|
|
|
mockFacilitator := setupMockFacilitator()
|
|
defer mockFacilitator.Close()
|
|
|
|
srv, err := NewX402Server(
|
|
"Test Agent API",
|
|
"1.0.0",
|
|
mockFacilitator.URL,
|
|
&DummyAuthProvider{},
|
|
X402Middlewares{},
|
|
func(next http.Handler) http.Handler { return next },
|
|
func(w http.ResponseWriter, r *http.Request, resp *x402.SettleResponse) {},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("failed to create server: %v", err)
|
|
}
|
|
|
|
srv.AddPayableTool(
|
|
mcp.Tool{
|
|
Name: "getTestPost",
|
|
Description: "Gated test endpoint",
|
|
},
|
|
func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{&mcp.TextContent{Text: `{"status": "success"}`}},
|
|
}, nil
|
|
},
|
|
x402.PaymentRequirements{
|
|
Scheme: "exact",
|
|
Network: "eip155:8453",
|
|
Asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
Amount: "1000",
|
|
PayTo: "0x0000000000000000000000000000000000000000",
|
|
},
|
|
)
|
|
|
|
serverSession, err := srv.mcpServer.Connect(ctx, serverTransport, nil)
|
|
if err != nil {
|
|
t.Fatalf("failed to connect server session: %v", err)
|
|
}
|
|
defer serverSession.Close()
|
|
|
|
client := mcp.NewClient(&mcp.Implementation{Name: "test-client", Version: "1.0.0"}, nil)
|
|
clientSession, err := client.Connect(ctx, clientTransport, nil)
|
|
if err != nil {
|
|
t.Fatalf("failed to connect client session: %v", err)
|
|
}
|
|
defer clientSession.Close()
|
|
|
|
toolsResponse, err := clientSession.ListTools(ctx, nil)
|
|
if err != nil {
|
|
t.Fatalf("failed to list tools over MCP: %v", err)
|
|
}
|
|
|
|
found := false
|
|
for _, tool := range toolsResponse.Tools {
|
|
if tool.Name == "getTestPost" {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("expected tool 'getTestPost' to be discovered by the MCP client, but it was missing")
|
|
}
|
|
|
|
result, err := clientSession.CallTool(ctx, &mcp.CallToolParams{
|
|
Name: "getTestPost",
|
|
Arguments: json.RawMessage(`{}`),
|
|
})
|
|
|
|
if err != nil {
|
|
t.Errorf("Unexpected protocol-level error: %v", err)
|
|
} else {
|
|
if !result.IsError {
|
|
t.Error("expected CallTool to return a payment challenge, but it returned a successful, free result")
|
|
} else {
|
|
t.Logf("Intercepted expected application-level payment challenge.")
|
|
t.Logf("Content: %v", result.Content)
|
|
t.Logf("Meta fields: %v", result.Meta)
|
|
}
|
|
}
|
|
}
|
|
|
|
func setupMockFacilitator() *httptest.Server {
|
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
switch {
|
|
case r.Method == "GET" && r.URL.Path == "/supported":
|
|
_, _ = w.Write([]byte(`{
|
|
"kinds": [
|
|
{
|
|
"x402Version": 2,
|
|
"scheme": "exact",
|
|
"network": "eip155:8453"
|
|
}
|
|
]
|
|
}`))
|
|
case r.Method == "POST" && r.URL.Path == "/verify":
|
|
_, _ = w.Write([]byte(`{
|
|
"isValid": true,
|
|
"invalidReason": ""
|
|
}`))
|
|
case r.Method == "POST" && r.URL.Path == "/settle":
|
|
_, _ = w.Write([]byte(`{
|
|
"success": true,
|
|
"transaction": "0x1234abcd",
|
|
"network": "eip155:8453",
|
|
"payer": "0xabcd1234"
|
|
}`))
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
return s
|
|
}
|