diff --git a/.changeset/icy-clouds-begin.md b/.changeset/icy-clouds-begin.md new file mode 100644 index 00000000000..40a377f76a4 --- /dev/null +++ b/.changeset/icy-clouds-begin.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +#bugfix passes http capability headers via MultiHeaders diff --git a/core/scripts/cre/environment/examples/workflows/v2/proof-of-reserve/cron-based/go.mod b/core/scripts/cre/environment/examples/workflows/v2/proof-of-reserve/cron-based/go.mod index 10f53809ce1..3496a6f96f1 100644 --- a/core/scripts/cre/environment/examples/workflows/v2/proof-of-reserve/cron-based/go.mod +++ b/core/scripts/cre/environment/examples/workflows/v2/proof-of-reserve/cron-based/go.mod @@ -4,10 +4,10 @@ go 1.25.5 require ( github.com/ethereum/go-ethereum v1.16.8 - github.com/smartcontractkit/chainlink-common v0.9.6-0.20260202175443-ee6c9d2f8935 - github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20251022073203-7d8ae8cf67c1 - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20251124151448-0448aefdaab9 - github.com/smartcontractkit/cre-sdk-go v1.0.1-0.20251111122439-00032d582c18 + github.com/smartcontractkit/chainlink-common v0.10.0 + github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20251222115927-36a18321243c + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260210221717-2546aed27ebe + github.com/smartcontractkit/cre-sdk-go v1.3.0 github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm v0.10.0 github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http v0.10.0 github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v0.10.0 @@ -49,6 +49,7 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pion/dtls/v2 v2.2.12 // indirect github.com/pion/transport/v2 v2.2.10 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect diff --git a/core/scripts/cre/environment/examples/workflows/v2/proof-of-reserve/cron-based/go.sum b/core/scripts/cre/environment/examples/workflows/v2/proof-of-reserve/cron-based/go.sum index 4cf3da6c47b..12a0eacd03f 100644 --- a/core/scripts/cre/environment/examples/workflows/v2/proof-of-reserve/cron-based/go.sum +++ b/core/scripts/cre/environment/examples/workflows/v2/proof-of-reserve/cron-based/go.sum @@ -233,14 +233,14 @@ github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKl github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/smartcontractkit/chainlink-common v0.9.6-0.20260202175443-ee6c9d2f8935 h1:Uj+LW45bBgdaDRF8C7hUtM8LOh7rZ5qZJxZhsTTusvY= -github.com/smartcontractkit/chainlink-common v0.9.6-0.20260202175443-ee6c9d2f8935/go.mod h1:zBuRC7el/pQQB95t7JnLOvCfZ3lmi5jjXYRUY2XUD+g= -github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20251022073203-7d8ae8cf67c1 h1:NTODgwAil7BLoijS7y6KnEuNbQ9v60VUhIR9FcAzIhg= -github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20251022073203-7d8ae8cf67c1/go.mod h1:oyfOm4k0uqmgZIfxk1elI/59B02shbbJQiiUdPdbMgI= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20251124151448-0448aefdaab9 h1:QRWXJusIj/IRY5Pl3JclNvDre0cZPd/5NbILwc4RV2M= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20251124151448-0448aefdaab9/go.mod h1:jUC52kZzEnWF9tddHh85zolKybmLpbQ1oNA4FjOHt1Q= -github.com/smartcontractkit/cre-sdk-go v1.0.1-0.20251111122439-00032d582c18 h1:x8NX+vQzScvg4XbKDA0NF8hfxpruOjR78fag3SxhwOo= -github.com/smartcontractkit/cre-sdk-go v1.0.1-0.20251111122439-00032d582c18/go.mod h1:sgiRyHUiPcxp1e/EMnaJ+ddMFL4MbE3UMZ2MORAAS9U= +github.com/smartcontractkit/chainlink-common v0.10.0 h1:d90b9UPJecrIryzhl43F1oQwkJQoug3TaANlJ1xLHyI= +github.com/smartcontractkit/chainlink-common v0.10.0/go.mod h1:13YN2kb3Vqpw2S7d4IwhX/578WPGC0JHN5JrOnAEsOc= +github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20251222115927-36a18321243c h1:eX7SCn5AGUGduv5OrjbVJkUSOnyeal0BtVem6zBSB2Y= +github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20251222115927-36a18321243c/go.mod h1:oyfOm4k0uqmgZIfxk1elI/59B02shbbJQiiUdPdbMgI= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260210221717-2546aed27ebe h1:Vc4zoSc/j6/FdCQ7vcyHTTB7kzHI2f+lHCHqFuiCcJQ= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260210221717-2546aed27ebe/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= +github.com/smartcontractkit/cre-sdk-go v1.3.0 h1:zzbNf8CDjadz4xLZPmv0UQIphxs8ChXs4ow+bmF+2OI= +github.com/smartcontractkit/cre-sdk-go v1.3.0/go.mod h1:LpkUDTXm7DUL0JljsZN1or9mR4/QcGdBai+G1Ng5LPA= github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm v0.10.0 h1:G0w0cLzHy/5m74IzSGz1Ynjffym4ZxLeUrRLp8EFP5w= github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm v0.10.0/go.mod h1:VVJ4mvA7wOU1Ic5b/vTaBMHEUysyxd0gdPPXkAu8CmY= github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http v0.10.0 h1:nP6PVWrrTIICvjwQuFitsQecQWbqpPaYzaTEjx92eTQ= diff --git a/core/scripts/go.mod b/core/scripts/go.mod index 4bb0dbe766f..6806402e313 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -46,7 +46,7 @@ require ( github.com/shopspring/decimal v1.4.0 github.com/smartcontractkit/chainlink-automation v0.8.1 github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260203202624-5101f4d33736 - github.com/smartcontractkit/chainlink-common v0.9.6-0.20260211140822-b833b412cdd9 + github.com/smartcontractkit/chainlink-common v0.10.0 github.com/smartcontractkit/chainlink-common/keystore v1.0.2-0.20260211140822-b833b412cdd9 github.com/smartcontractkit/chainlink-data-streams v0.1.11 github.com/smartcontractkit/chainlink-deployments-framework v0.80.1-0.20260209182815-b296b7df28a6 @@ -514,7 +514,7 @@ require ( github.com/smartcontractkit/chainlink-ton v0.0.0-20260209205928-e7e034ed7976 // indirect github.com/smartcontractkit/chainlink-ton/deployment v0.0.0-20260209205928-e7e034ed7976 // indirect github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20251014143056-a0c6328c91e9 // indirect - github.com/smartcontractkit/cre-sdk-go v0.7.1-0.20250919133015-2df149f34a81 // indirect + github.com/smartcontractkit/cre-sdk-go v1.3.0 // indirect github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e // indirect github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 // indirect github.com/smartcontractkit/mcms v0.35.1-0.20260209175626-b68b54b6e8d0 // indirect diff --git a/core/scripts/go.sum b/core/scripts/go.sum index c96d0203a49..941093f1a9a 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -1612,8 +1612,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260129103204-4c84 github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260129103204-4c8453dd8139/go.mod h1:gUbichNQBqk+fBF2aV40ZkzFmAJ8SygH6DEPd3cJkQE= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260209181943-d6573e8f312f h1:F++iE5sQU020cJbbTosGgyPn2m3A0h2IOWoCKt/QWU8= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260209181943-d6573e8f312f/go.mod h1:qvqpDbul4XF237R3zElpI0gyxWINg46YjQh68N4K5CU= -github.com/smartcontractkit/chainlink-common v0.9.6-0.20260211140822-b833b412cdd9 h1:HX9SbpoRWUB1w8KtkXfN8gGI8+dE7NYuNYfuDQ3E8sI= -github.com/smartcontractkit/chainlink-common v0.9.6-0.20260211140822-b833b412cdd9/go.mod h1:13YN2kb3Vqpw2S7d4IwhX/578WPGC0JHN5JrOnAEsOc= +github.com/smartcontractkit/chainlink-common v0.10.0 h1:d90b9UPJecrIryzhl43F1oQwkJQoug3TaANlJ1xLHyI= +github.com/smartcontractkit/chainlink-common v0.10.0/go.mod h1:13YN2kb3Vqpw2S7d4IwhX/578WPGC0JHN5JrOnAEsOc= github.com/smartcontractkit/chainlink-common/keystore v1.0.2-0.20260211140822-b833b412cdd9 h1:dWqd2lOW3GbwtgZEeDSDI1b1X175MPOlCU6GDQQVBjk= github.com/smartcontractkit/chainlink-common/keystore v1.0.2-0.20260211140822-b833b412cdd9/go.mod h1:SMegDBf3KDs2tuKApmTRyO2xQthMu3gV2J+IuHEs0Y0= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20251211140724-319861e514c4 h1:NOUsjsMzNecbjiPWUQGlRSRAutEvCFrqqyETDJeh5q4= @@ -1700,8 +1700,8 @@ github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20251014143056-a0c6 github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20251014143056-a0c6328c91e9/go.mod h1:h9hMs6K4hT1+mjYnJD3/SW1o7yC/sKjNi0Qh8hLfiCE= github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20251014124537-af6b1684fe15 h1:idp/RjsFznR48JWGfZICsrpcl9JTrnMzoUNVz8MhQMI= github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20251014124537-af6b1684fe15/go.mod h1:ea1LESxlSSOgc2zZBqf1RTkXTMthHaspdqUHd7W4lF0= -github.com/smartcontractkit/cre-sdk-go v0.7.1-0.20250919133015-2df149f34a81 h1:CfnjzJvn3iX93PzdGucyGJmgv/KDXv8DfKcLw/mix24= -github.com/smartcontractkit/cre-sdk-go v0.7.1-0.20250919133015-2df149f34a81/go.mod h1:CQY8hCISjctPmt8ViDVgFm4vMGLs5fYI198QhkBS++Y= +github.com/smartcontractkit/cre-sdk-go v1.3.0 h1:zzbNf8CDjadz4xLZPmv0UQIphxs8ChXs4ow+bmF+2OI= +github.com/smartcontractkit/cre-sdk-go v1.3.0/go.mod h1:LpkUDTXm7DUL0JljsZN1or9mR4/QcGdBai+G1Ng5LPA= github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v0.8.0 h1:aO++xdGcQ8TpxAfXrm7EHeIVLDitB8xg7J8/zSxbdBY= github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v0.8.0/go.mod h1:PWyrIw16It4TSyq6mDXqmSR0jF2evZRKuBxu7pK1yDw= github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e h1:Hv9Mww35LrufCdM9wtS9yVi/rEWGI1UnjHbcKKU0nVY= diff --git a/core/services/gateway/handlers/capabilities/v2/http_handler.go b/core/services/gateway/handlers/capabilities/v2/http_handler.go index 1622eb672cb..98d2fced73e 100644 --- a/core/services/gateway/handlers/capabilities/v2/http_handler.go +++ b/core/services/gateway/handlers/capabilities/v2/http_handler.go @@ -286,6 +286,7 @@ func (h *gatewayHandler) createHTTPRequestCallback(ctx context.Context, requestI return gateway_common.OutboundHTTPResponse{ StatusCode: resp.StatusCode, Headers: resp.Headers, + MultiHeaders: resp.MultiHeaders, Body: resp.Body, ExternalEndpointLatency: externalEndpointLatency, } @@ -331,7 +332,8 @@ func (h *gatewayHandler) makeOutgoingRequest(ctx context.Context, resp *jsonrpc. httpReq := network.HTTPRequest{ Method: req.Method, URL: req.URL, - Headers: req.Headers, + Headers: req.Headers, //nolint:staticcheck // forward deprecated Headers for backward compatibility; request uses MultiHeaders when set + MultiHeaders: req.MultiHeaders, Body: req.Body, MaxResponseBytes: req.MaxResponseBytes, Timeout: timeout, diff --git a/core/services/gateway/handlers/capabilities/v2/http_handler_test.go b/core/services/gateway/handlers/capabilities/v2/http_handler_test.go index 16205a4e135..dfa5f499476 100644 --- a/core/services/gateway/handlers/capabilities/v2/http_handler_test.go +++ b/core/services/gateway/handlers/capabilities/v2/http_handler_test.go @@ -5,11 +5,11 @@ import ( "encoding/json" "errors" "fmt" + "maps" "testing" "time" "github.com/google/uuid" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -148,6 +148,86 @@ func TestHandleNodeMessage(t *testing.T) { handler.wg.Wait() }) + t.Run("successful node message handling with MultiHeaders", func(t *testing.T) { + mockDon := handler.don.(*handlermocks.DON) + mockHTTPClient := handler.httpClient.(*httpmocks.HTTPClient) + + // Prepare outbound request + outboundReq := gateway_common.OutboundHTTPRequest{ + Method: "GET", + URL: "https://example.com/api/multiheaders-test", + TimeoutMs: 5000, + MultiHeaders: map[string][]string{"Content-Type": {"application/json"}}, + Body: []byte(`{"test": "data"}`), + CacheSettings: gateway_common.CacheSettings{}, + } + reqBytes, err := json.Marshal(outboundReq) + require.NoError(t, err) + + id := fmt.Sprintf("%s/%s", gateway_common.MethodHTTPAction, uuid.New().String()) + rawRequest := json.RawMessage(reqBytes) + resp := &jsonrpc.Response[json.RawMessage]{ + ID: id, + Result: &rawRequest, + } + + // Response with multiple Set-Cookie headers + httpResp := &network.HTTPResponse{ + StatusCode: 200, + Headers: map[string]string{ + "Set-Cookie": "sessionid=abc123; Path=/; HttpOnly", + }, + MultiHeaders: map[string][]string{ + "Set-Cookie": { + "sessionid=abc123; Path=/; HttpOnly", + "csrf_token=xyz789; Path=/; Secure", + }, + }, + Body: []byte(`{"result": "success"}`), + } + + mockHTTPClient.EXPECT().Send(mock.Anything, mock.MatchedBy(func(req network.HTTPRequest) bool { + return req.Method == "GET" && req.URL == "https://example.com/api/multiheaders-test" + })).Return(httpResp, nil).Once() + + capturedResponse := &gateway_common.OutboundHTTPResponse{} + mockDon.EXPECT().SendToNode(mock.Anything, "node1", mock.MatchedBy(func(req *jsonrpc.Request[json.RawMessage]) bool { + if req.Params == nil { + return false + } + paramsStr := string(*req.Params) + if !json.Valid(*req.Params) { + return false + } + err2 := json.Unmarshal(*req.Params, capturedResponse) + if err2 != nil { + t.Logf("Failed to unmarshal response: %v, params: %s", err2, paramsStr) + return false + } + if capturedResponse.StatusCode != 200 { + return false + } + return req.ID == id + })).Return(nil) + + err = handler.HandleNodeMessage(testutils.Context(t), resp, "node1") + require.NoError(t, err) + handler.wg.Wait() + + // Verify the response was captured + require.Equal(t, 200, capturedResponse.StatusCode, "Response should have status code 200") + require.NotNil(t, capturedResponse.MultiHeaders, "MultiHeaders should not be nil") + require.NotEmpty(t, capturedResponse.MultiHeaders, "MultiHeaders should not be empty") + setCookieValues, ok := capturedResponse.MultiHeaders["Set-Cookie"] + require.True(t, ok, "Set-Cookie header should be in MultiHeaders, got: %+v", capturedResponse.MultiHeaders) + require.Len(t, setCookieValues, 2, "Should have 2 Set-Cookie headers, got: %v", setCookieValues) + require.Contains(t, setCookieValues, "sessionid=abc123; Path=/; HttpOnly") + require.Contains(t, setCookieValues, "csrf_token=xyz789; Path=/; Secure") + + // Verify backward compatibility: all keys in MultiHeaders should be in Headers + verifyBackwardCompatibility(t, capturedResponse.Headers, capturedResponse.MultiHeaders) //nolint:staticcheck // SA1019: intentionally asserting deprecated Headers for backward compatibility + }) + t.Run("returns cached response if available", func(t *testing.T) { outboundReq := gateway_common.OutboundHTTPRequest{ Method: "GET", @@ -282,6 +362,7 @@ func TestServiceLifecycle(t *testing.T) { require.NoError(t, err) }) } + func TestHandleNodeMessage_RoutesToTriggerHandler(t *testing.T) { // This test covers the case where the response ID does not contain a "/" // and should be routed to the triggerHandler.HandleNodeTriggerResponse. @@ -415,6 +496,14 @@ func createTestHandler(t *testing.T) *gatewayHandler { return createTestHandlerWithConfig(t, cfg) } +// verifyBackwardCompatibility checks that all keys in MultiHeaders are also present in Headers +// with non-empty values. Same logic as in gateway/network/httpclient_test.go (package boundary). +func verifyBackwardCompatibility(t *testing.T, headers map[string]string, multiHeaders map[string][]string) { + for key := range maps.Keys(multiHeaders) { + require.NotEmpty(t, headers[key], "Headers should contain %s for backward compatibility", key) + } +} + func createTestHandlerWithConfig(t *testing.T, cfg ServiceConfig) *gatewayHandler { configBytes, err := json.Marshal(cfg) require.NoError(t, err) @@ -494,6 +583,83 @@ func TestCreateHTTPRequestCallback(t *testing.T) { require.Nil(t, response.Body) }) + t.Run("response with MultiHeaders is passed through correctly", func(t *testing.T) { + handler := createTestHandler(t) + mockHTTPClient := handler.httpClient.(*httpmocks.HTTPClient) + + expectedResp := &network.HTTPResponse{ + StatusCode: 200, + Headers: map[string]string{ + "Set-Cookie": "sessionid=abc123; Path=/; HttpOnly, csrf_token=xyz789; Path=/; Secure", + "Via": "1.0 proxy1,1.1 proxy2", + }, + MultiHeaders: map[string][]string{ + "Set-Cookie": { + "sessionid=abc123; Path=/; HttpOnly", + "csrf_token=xyz789; Path=/; Secure", + }, + "Via": { + "1.0 proxy1", + "1.1 proxy2", + }, + }, + Body: []byte(`{"result": "success"}`), + } + + mockHTTPClient.EXPECT().Send(mock.Anything, mock.Anything).Return(expectedResp, nil) + + callback := handler.createHTTPRequestCallback(ctx, requestID, httpReq, outboundReq) + response := callback() + + require.Equal(t, expectedResp.StatusCode, response.StatusCode) + require.Equal(t, expectedResp.Body, response.Body) + require.Empty(t, response.ErrorMessage) + + // Verify MultiHeaders are passed through + require.NotNil(t, response.MultiHeaders, "MultiHeaders should not be nil") + require.Len(t, response.MultiHeaders["Set-Cookie"], 2, "Should have 2 Set-Cookie headers") + require.Contains(t, response.MultiHeaders["Set-Cookie"], "sessionid=abc123; Path=/; HttpOnly") + require.Contains(t, response.MultiHeaders["Set-Cookie"], "csrf_token=xyz789; Path=/; Secure") + require.Len(t, response.MultiHeaders["Via"], 2, "Should have 2 Via headers") + require.Contains(t, response.MultiHeaders["Via"], "1.0 proxy1") + require.Contains(t, response.MultiHeaders["Via"], "1.1 proxy2") + + // Verify Headers field is also set (for backward compatibility) + require.NotNil(t, response.Headers, "Headers should not be nil") //nolint:staticcheck // SA1019: assert deprecated Headers for backward compatibility + require.NotEmpty(t, response.Headers["Set-Cookie"], "Headers should contain Set-Cookie") //nolint:staticcheck // SA1019: assert deprecated Headers for backward compatibility + require.NotEmpty(t, response.Headers["Via"], "Headers should contain Via") //nolint:staticcheck // SA1019: assert deprecated Headers for backward compatibility + + // Verify backward compatibility: all keys in MultiHeaders should be in Headers + verifyBackwardCompatibility(t, response.Headers, response.MultiHeaders) //nolint:staticcheck // SA1019: intentionally asserting deprecated Headers for backward compatibility + }) + + t.Run("response with empty MultiHeaders still sets Headers", func(t *testing.T) { + handler := createTestHandler(t) + mockHTTPClient := handler.httpClient.(*httpmocks.HTTPClient) + + expectedResp := &network.HTTPResponse{ + StatusCode: 200, + Headers: map[string]string{"Content-Type": "application/json"}, + MultiHeaders: map[string][]string{ + "Content-Type": {"application/json"}, + }, + Body: []byte(`{"result": "success"}`), + } + + mockHTTPClient.EXPECT().Send(mock.Anything, mock.Anything).Return(expectedResp, nil) + + callback := handler.createHTTPRequestCallback(ctx, requestID, httpReq, outboundReq) + response := callback() + + require.Equal(t, expectedResp.StatusCode, response.StatusCode) + require.NotNil(t, response.MultiHeaders) + require.Equal(t, []string{"application/json"}, response.MultiHeaders["Content-Type"]) + require.Equal(t, "application/json", response.Headers["Content-Type"]) //nolint:staticcheck // SA1019: assert deprecated Headers for backward compatibility + + // Verify backward compatibility: all keys in MultiHeaders should be in Headers + verifyBackwardCompatibility(t, response.Headers, response.MultiHeaders) //nolint:staticcheck // SA1019: intentionally asserting deprecated Headers for backward compatibility + }) + t.Run("HTTP read error sets IsExternalEndpointError to true", func(t *testing.T) { handler := createTestHandler(t) mockHTTPClient := handler.httpClient.(*httpmocks.HTTPClient) diff --git a/core/services/gateway/handlers/capabilities/v2/http_trigger_handler_test.go b/core/services/gateway/handlers/capabilities/v2/http_trigger_handler_test.go index 9cf4e32611c..42e5e8dde74 100644 --- a/core/services/gateway/handlers/capabilities/v2/http_trigger_handler_test.go +++ b/core/services/gateway/handlers/capabilities/v2/http_trigger_handler_test.go @@ -28,9 +28,11 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/utils" ) -const workflowID = "0x1234567890abcdef1234567890abcdef12345678901234567890abcdef123456" -const workflowOwner = "0x1234567890abcdef1234567890abcdef12345678" -const requestID = "test-request-id" +const ( + workflowID = "0x1234567890abcdef1234567890abcdef12345678901234567890abcdef123456" + workflowOwner = "0x1234567890abcdef1234567890abcdef12345678" + requestID = "test-request-id" +) func createTestMetrics(t *testing.T, donConfig *config.DONConfig) *metrics.Metrics { m, err := metrics.NewMetrics(donConfig) @@ -512,7 +514,7 @@ func TestHttpTriggerHandler_ServiceLifecycle(t *testing.T) { }) } -func registerWorkflow(t *testing.T, handler *httpTriggerHandler, workflowID string, privateKey *ecdsa.PrivateKey) { +func registerWorkflow(_ *testing.T, handler *httpTriggerHandler, workflowID string, privateKey *ecdsa.PrivateKey) { handler.workflowMetadataHandler.authorizedKeys[workflowID] = map[gateway_common.AuthorizedKey]struct{}{ { KeyType: gateway_common.KeyTypeECDSAEVM, @@ -1058,6 +1060,7 @@ func TestHttpTriggerHandler_HandleUserTriggerRequest_WorkflowLookup(t *testing.T requireUserErrorSent(t, r, jsonrpc.ErrInvalidRequest) }) } + func TestHttpTriggerHandler_HandleUserTriggerRequest_Validation(t *testing.T) { handler, mockDon := createTestTriggerHandler(t) diff --git a/core/services/gateway/handlers/capabilities/v2/response_cache.go b/core/services/gateway/handlers/capabilities/v2/response_cache.go index 1a7a5eaa29a..be862c4a5f9 100644 --- a/core/services/gateway/handlers/capabilities/v2/response_cache.go +++ b/core/services/gateway/handlers/capabilities/v2/response_cache.go @@ -43,7 +43,7 @@ func isCacheableStatusCode(statusCode int) bool { // isExpiredOrNotCached returns true if the cached response is expired or not cached. // IMPORTANT: this method does not lock the cache map. MUST be called with the cacheMu locked. -func (rc *responseCache) isExpiredOrNotCached(workflowID string, req gateway.OutboundHTTPRequest) bool { +func (rc *responseCache) isExpiredOrNotCached(_ string, req gateway.OutboundHTTPRequest) bool { cachedResp, exists := rc.cache[req.Hash()] if !exists || time.Now().After(cachedResp.storedAt.Add(rc.ttl)) { return true diff --git a/core/services/gateway/network/httpclient.go b/core/services/gateway/network/httpclient.go index 0a860e805fd..ecd6148d4d8 100644 --- a/core/services/gateway/network/httpclient.go +++ b/core/services/gateway/network/httpclient.go @@ -6,7 +6,9 @@ import ( "errors" "fmt" "io" + "maps" "net/http" + "slices" "strings" "time" @@ -68,41 +70,35 @@ var ( func (c *HTTPClientConfig) ApplyDefaults() { if len(c.AllowedPorts) == 0 { - c.AllowedPorts = defaultAllowedPorts + c.AllowedPorts = slices.Clone(defaultAllowedPorts) } - if len(c.AllowedSchemes) == 0 { - c.AllowedSchemes = defaultAllowedSchemes + c.AllowedSchemes = slices.Clone(defaultAllowedSchemes) } - if len(c.AllowedMethods) == 0 { - c.AllowedMethods = defaultAllowedMethods + c.AllowedMethods = slices.Clone(defaultAllowedMethods) } - if len(c.BlockedHeaders) == 0 { - c.BlockedHeaders = defaultBlockedHeaders + c.BlockedHeaders = slices.Clone(defaultBlockedHeaders) } - if c.MaxResponseBytes == 0 { c.MaxResponseBytes = defaultMaxResponseBytes } - if c.DefaultTimeout == 0 { c.DefaultTimeout = defaultTimeout } - c.maxRequestDuration = defaultMaxRequestDuration - - // safeurl automatically blocks internal IPs so no need - // to set defaults here. + // safeurl automatically blocks internal IPs so no need to set defaults here. } type HTTPRequest struct { Method string URL string - Headers map[string]string - Body []byte - Timeout time.Duration + Headers map[string]string // request headers (deprecated: use MultiHeaders when multiple values per key are needed) + // MultiHeaders holds multiple values per header name; when set, Headers is ignored for the outgoing request. + MultiHeaders map[string][]string + Body []byte + Timeout time.Duration // Maximum number of bytes to read from the response body. If 0, the default value is used. // Does not override a request specific value gte 0. @@ -110,9 +106,41 @@ type HTTPRequest struct { } type HTTPResponse struct { - StatusCode int // HTTP status code - Headers map[string]string // HTTP headers - Body []byte // HTTP response body + StatusCode int // HTTP status code + Headers map[string]string // HTTP headers (deprecated: use MultiHeaders, contains first value only for backward compatibility) + MultiHeaders map[string][]string // HTTP headers with all values preserved + Body []byte // HTTP response body +} + +// requestToNetHeader builds net/http.Header from req. Uses MultiHeaders when set, otherwise Headers. +func requestToNetHeader(req HTTPRequest) http.Header { + out := make(http.Header) + if len(req.MultiHeaders) > 0 { + for k, values := range req.MultiHeaders { + for _, v := range values { + out.Add(k, v) + } + } + return out + } + for k, v := range req.Headers { + out.Add(k, v) + } + return out +} + +// responseHeadersFromNetHeader builds Headers (comma-joined) and MultiHeaders from net/http.Header. Skips keys with no values. +func responseHeadersFromNetHeader(h http.Header) (map[string]string, map[string][]string) { + headers := make(map[string]string, len(h)) + multiHeaders := make(map[string][]string, len(h)) + for k, v := range h { + if len(v) == 0 { + continue + } + multiHeaders[k] = slices.Clone(v) + headers[k] = strings.Join(v, ",") + } + return headers, multiHeaders } type httpClient struct { @@ -171,27 +199,37 @@ func isBlockedRequest(err error) bool { } func (c *httpClient) validateMethod(method string) error { - methodUpper := strings.ToUpper(method) - for _, allowedMethod := range c.config.AllowedMethods { - if strings.ToUpper(allowedMethod) == methodUpper { - return nil - } + isAllowed := func(allowed string) bool { + return strings.EqualFold(allowed, method) + } + if slices.ContainsFunc(c.config.AllowedMethods, isAllowed) { + return nil } return fmt.Errorf("%w: HTTP method not allowed: %s", ErrBlockedRequest, method) } -func (c *httpClient) validateHeaders(headers map[string]string) error { - for headerName := range headers { - headerNameLower := strings.ToLower(headerName) - for _, blockedHeader := range c.config.BlockedHeaders { - if strings.ToLower(blockedHeader) == headerNameLower { - return fmt.Errorf("%w: HTTP header not allowed: %s", ErrBlockedRequest, headerName) - } +// validateHeaderNames checks that none of the given header names are in the blocked list (case-insensitive). +func (c *httpClient) validateHeaderNames(names []string) error { + blockedSet := make(map[string]struct{}, len(c.config.BlockedHeaders)) + for _, b := range c.config.BlockedHeaders { + blockedSet[strings.ToLower(b)] = struct{}{} + } + for _, name := range names { + if _, blocked := blockedSet[strings.ToLower(name)]; blocked { + return fmt.Errorf("%w: HTTP header not allowed: %s", ErrBlockedRequest, name) } } return nil } +func (c *httpClient) validateHeaders(headers map[string]string) error { + return c.validateHeaderNames(slices.Collect(maps.Keys(headers))) +} + +func (c *httpClient) validateMultiHeaders(multiHeaders map[string][]string) error { + return c.validateHeaderNames(slices.Collect(maps.Keys(multiHeaders))) +} + // Send executes an http request that is always time limited by at least the // default timeout. Override the default timeout with a non-zero duration by // passing a Timeout value on the request. @@ -199,7 +237,11 @@ func (c *httpClient) Send(ctx context.Context, req HTTPRequest) (*HTTPResponse, if err := c.validateMethod(req.Method); err != nil { return nil, err } - if err := c.validateHeaders(req.Headers); err != nil { + if len(req.MultiHeaders) > 0 { + if err := c.validateMultiHeaders(req.MultiHeaders); err != nil { + return nil, err + } + } else if err := c.validateHeaders(req.Headers); err != nil { return nil, err } @@ -221,9 +263,10 @@ func (c *httpClient) Send(ctx context.Context, req HTTPRequest) (*HTTPResponse, if err != nil { return nil, err } - - for k, v := range req.Headers { - r.Header.Add(k, v) + for k, values := range requestToNetHeader(req) { + for _, v := range values { + r.Header.Add(k, v) + } } resp, err := c.client.Do(r) @@ -246,18 +289,14 @@ func (c *httpClient) Send(ctx context.Context, req HTTPRequest) (*HTTPResponse, c.lggr.Errorw("failed to read HTTP response body", "err", err) return nil, errors.Join(err, ErrHTTPRead) } - headers := make(map[string]string) - for k, v := range resp.Header { - // header values are usually an array of size 1 - // joining them to a single string in case array size is greater than 1 - headers[k] = strings.Join(v, ",") - } - c.lggr.Debugw("received HTTP response", "statusCode", resp.StatusCode) + headers, multiHeaders := responseHeadersFromNetHeader(resp.Header) + c.lggr.Debugw("received HTTP response", "statusCode", resp.StatusCode) return &HTTPResponse{ - Headers: headers, - StatusCode: resp.StatusCode, - Body: body, + Headers: headers, + MultiHeaders: multiHeaders, + StatusCode: resp.StatusCode, + Body: body, }, nil } @@ -270,12 +309,5 @@ func maxReadBytes(sizes readSize) uint32 { if sizes.requestSize == 0 { return sizes.defaultSize } - return minUint32(sizes.defaultSize, sizes.requestSize) -} - -func minUint32(a, b uint32) uint32 { - if a < b { - return a - } - return b + return min(sizes.defaultSize, sizes.requestSize) } diff --git a/core/services/gateway/network/httpclient_test.go b/core/services/gateway/network/httpclient_test.go index a05274e07ae..65a9cbc9ee6 100644 --- a/core/services/gateway/network/httpclient_test.go +++ b/core/services/gateway/network/httpclient_test.go @@ -4,6 +4,7 @@ package network import ( "context" "errors" + "maps" "net/http" "net/http/httptest" "net/url" @@ -827,3 +828,220 @@ func TestHTTPClient_BlockedRequests_ReturnErrBlockedRequest(t *testing.T) { }) } } + +// verifyBackwardCompatibility checks that all keys in MultiHeaders are also present in Headers +// with non-empty values. +func verifyBackwardCompatibility(t *testing.T, headers map[string]string, multiHeaders map[string][]string) { + for key := range maps.Keys(multiHeaders) { + require.NotEmpty(t, headers[key], "Headers should contain %s for backward compatibility", key) + } +} + +func TestHTTPClient_MultiHeaders(t *testing.T) { + t.Parallel() + + lggr := logger.Test(t) + + t.Run("response with multiple Set-Cookie headers", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Set multiple Set-Cookie headers (cannot be comma-separated per RFC 6265) + w.Header().Add("Set-Cookie", "sessionid=abc123; Path=/; HttpOnly") + w.Header().Add("Set-Cookie", "csrf_token=xyz789; Path=/; Secure") + w.Header().Add("Set-Cookie", "pref=dark; Path=/") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("success")) + })) + defer server.Close() + + u, err := url.Parse(server.URL) + require.NoError(t, err) + hostname, port := u.Hostname(), u.Port() + portInt, err := strconv.ParseInt(port, 10, 32) + require.NoError(t, err) + + config := &HTTPClientConfig{ + MaxResponseBytes: 1024, + AllowedIPs: []string{hostname}, + AllowedPorts: []int{int(portInt)}, + } + + client, err := NewHTTPClient(*config, lggr) + require.NoError(t, err) + + resp, err := client.Send(context.Background(), HTTPRequest{ + Method: "GET", + URL: server.URL, + Headers: map[string]string{}, + Body: nil, + Timeout: 2 * time.Second, + }) + + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + + // Verify MultiHeaders contains all Set-Cookie values + require.NotNil(t, resp.MultiHeaders, "MultiHeaders should not be nil") + setCookieValues, ok := resp.MultiHeaders["Set-Cookie"] + require.True(t, ok, "Set-Cookie header should be in MultiHeaders") + require.Len(t, setCookieValues, 3, "Should have 3 Set-Cookie headers") + require.Contains(t, setCookieValues, "sessionid=abc123; Path=/; HttpOnly") + require.Contains(t, setCookieValues, "csrf_token=xyz789; Path=/; Secure") + require.Contains(t, setCookieValues, "pref=dark; Path=/") + + // Verify Headers field has comma-joined values for backward compatibility + require.NotEmpty(t, resp.Headers["Set-Cookie"], "Headers should contain Set-Cookie") + require.Contains(t, resp.Headers["Set-Cookie"], "sessionid=abc123") + require.Contains(t, resp.Headers["Set-Cookie"], "csrf_token=xyz789") + require.Contains(t, resp.Headers["Set-Cookie"], "pref=dark") + + // Verify backward compatibility: all keys in MultiHeaders should be in Headers + verifyBackwardCompatibility(t, resp.Headers, resp.MultiHeaders) + }) + + t.Run("response with multiple Via headers", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Set multiple Via headers (can be comma-separated, but we preserve all values) + w.Header().Add("Via", "1.0 proxy1") + w.Header().Add("Via", "1.1 proxy2") + w.Header().Add("Via", "1.1 proxy3") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("success")) + })) + defer server.Close() + + u, err := url.Parse(server.URL) + require.NoError(t, err) + hostname, port := u.Hostname(), u.Port() + portInt, err := strconv.ParseInt(port, 10, 32) + require.NoError(t, err) + + config := &HTTPClientConfig{ + MaxResponseBytes: 1024, + AllowedIPs: []string{hostname}, + AllowedPorts: []int{int(portInt)}, + } + + client, err := NewHTTPClient(*config, lggr) + require.NoError(t, err) + + resp, err := client.Send(context.Background(), HTTPRequest{ + Method: "GET", + URL: server.URL, + Headers: map[string]string{}, + Body: nil, + Timeout: 2 * time.Second, + }) + + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + + // Verify MultiHeaders contains all Via values + require.NotNil(t, resp.MultiHeaders) + viaValues, ok := resp.MultiHeaders["Via"] + require.True(t, ok, "Via header should be in MultiHeaders") + require.Len(t, viaValues, 3, "Should have 3 Via headers") + require.Contains(t, viaValues, "1.0 proxy1") + require.Contains(t, viaValues, "1.1 proxy2") + require.Contains(t, viaValues, "1.1 proxy3") + + // Verify Headers field has comma-joined values + require.Equal(t, "1.0 proxy1,1.1 proxy2,1.1 proxy3", resp.Headers["Via"]) + + // Verify backward compatibility: all keys in MultiHeaders should be in Headers + verifyBackwardCompatibility(t, resp.Headers, resp.MultiHeaders) + }) + + t.Run("response with single header value", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("success")) + })) + defer server.Close() + + u, err := url.Parse(server.URL) + require.NoError(t, err) + hostname, port := u.Hostname(), u.Port() + portInt, err := strconv.ParseInt(port, 10, 32) + require.NoError(t, err) + + config := &HTTPClientConfig{ + MaxResponseBytes: 1024, + AllowedIPs: []string{hostname}, + AllowedPorts: []int{int(portInt)}, + } + + client, err := NewHTTPClient(*config, lggr) + require.NoError(t, err) + + resp, err := client.Send(context.Background(), HTTPRequest{ + Method: "GET", + URL: server.URL, + Headers: map[string]string{}, + Body: nil, + Timeout: 2 * time.Second, + }) + + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + + // Verify MultiHeaders contains single value + require.NotNil(t, resp.MultiHeaders) + contentTypeValues, ok := resp.MultiHeaders["Content-Type"] + require.True(t, ok, "Content-Type header should be in MultiHeaders") + require.Len(t, contentTypeValues, 1, "Should have 1 Content-Type header") + require.Equal(t, "application/json", contentTypeValues[0]) + + // Verify Headers field matches + require.Equal(t, "application/json", resp.Headers["Content-Type"]) + + // Verify backward compatibility: all keys in MultiHeaders should be in Headers + verifyBackwardCompatibility(t, resp.Headers, resp.MultiHeaders) + }) + + t.Run("response with no headers", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + u, err := url.Parse(server.URL) + require.NoError(t, err) + hostname, port := u.Hostname(), u.Port() + portInt, err := strconv.ParseInt(port, 10, 32) + require.NoError(t, err) + + config := &HTTPClientConfig{ + MaxResponseBytes: 1024, + AllowedIPs: []string{hostname}, + AllowedPorts: []int{int(portInt)}, + } + + client, err := NewHTTPClient(*config, lggr) + require.NoError(t, err) + + resp, err := client.Send(context.Background(), HTTPRequest{ + Method: "GET", + URL: server.URL, + Headers: map[string]string{}, + Body: nil, + Timeout: 2 * time.Second, + }) + + require.NoError(t, err) + require.Equal(t, http.StatusNoContent, resp.StatusCode) + + // MultiHeaders should not be nil + require.NotNil(t, resp.MultiHeaders, "MultiHeaders should not be nil even if empty") + // If there are headers (like Date), they should be in MultiHeaders + if len(resp.MultiHeaders) > 0 { + // Verify that any headers present have at least one value + for key, values := range resp.MultiHeaders { + require.NotEmpty(t, values, "Header %s should have at least one value", key) + } + // Verify backward compatibility: all keys in MultiHeaders should be in Headers + verifyBackwardCompatibility(t, resp.Headers, resp.MultiHeaders) + } + require.NotNil(t, resp.Headers, "Headers should not be nil even if empty") + }) +} diff --git a/core/services/workflows/test/wasm/v2/cmd/main.go b/core/services/workflows/test/wasm/v2/cmd/main.go index 6d596933666..a19552bf24b 100644 --- a/core/services/workflows/test/wasm/v2/cmd/main.go +++ b/core/services/workflows/test/wasm/v2/cmd/main.go @@ -3,10 +3,24 @@ package main import ( - "github.com/smartcontractkit/cre-sdk-go/cre/testutils" + "log/slog" + + "github.com/smartcontractkit/cre-sdk-go/cre" "github.com/smartcontractkit/cre-sdk-go/cre/wasm" + "github.com/smartcontractkit/cre-sdk-go/internal_testing/capabilities/basictrigger" ) +func CreateWorkflow(_ string, _ *slog.Logger, _ cre.SecretsProvider) (cre.Workflow[string], error) { + return cre.Workflow[string]{ + cre.Handler( + basictrigger.Trigger(&basictrigger.Config{Name: "test", Number: 0}), + func(_ string, _ cre.Runtime, _ *basictrigger.Outputs) (string, error) { + return "Hello, world!", nil + }, + ), + }, nil +} + func main() { - testutils.RunTestWorkflow(wasm.NewRunner(func(b []byte) (string, error) { return string(b), nil })) + wasm.NewRunner(func(b []byte) (string, error) { return string(b), nil }).Run(CreateWorkflow) } diff --git a/core/services/workflows/v2/engine_test.go b/core/services/workflows/v2/engine_test.go index 594f9c229c7..8597a2df549 100644 --- a/core/services/workflows/v2/engine_test.go +++ b/core/services/workflows/v2/engine_test.go @@ -59,8 +59,6 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/utils/matches" "github.com/smartcontractkit/cre-sdk-go/cre/testutils/registry" - "github.com/smartcontractkit/cre-sdk-go/internal_testing/capabilities/basicaction" - basicactionmock "github.com/smartcontractkit/cre-sdk-go/internal_testing/capabilities/basicaction/mock" "github.com/smartcontractkit/cre-sdk-go/internal_testing/capabilities/basictrigger" ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" ) @@ -1232,40 +1230,19 @@ func TestEngine_WASMBinary_Simple(t *testing.T) { }, } - basicActionMock := setupExpectedCalls(t) wrappedTriggerMock := &TriggerCapabilityWrapper{} - wrappedActionMock := &MockCapabilityWrapper{ - Capability: basicActionMock, - } t.Run("OK happy path", func(t *testing.T) { wantResponse := "Hello, world!" engine, err := v2.NewEngine(cfg) require.NoError(t, err) + // Simple wasm binary (v2/cmd) uses trigger-only workflow; no GetExecutable/ConfigForCapability. capreg.EXPECT(). GetTrigger(matches.AnyContext, triggerID). Return(wrappedTriggerMock, nil). Once() - capreg.EXPECT(). - GetExecutable(matches.AnyContext, wrappedActionMock.ID()). - Return(wrappedActionMock, nil). - Twice() - - testConf, _ := values.NewMap(map[string]any{ - "spendRatios": map[string]string{ - "spendTypeA": "0.4", - "spendTypeB": "0.6", - }, - }) - - capreg.EXPECT(). - ConfigForCapability(matches.AnyContext, mock.Anything, mock.Anything). - Return(capabilities.CapabilityConfiguration{ - RestrictedConfig: testConf, - }, nil) - require.NoError(t, engine.Start(t.Context())) require.NoError(t, <-initDoneCh) require.Equal(t, []string{triggerID}, <-subscribedToTriggersCh) @@ -1934,26 +1911,6 @@ func setupMockBillingClient(t *testing.T) *metmocks.BillingClient { return billingClient } -// setupExpectedCalls mocks single call to trigger and two calls to the basic action -// mock capability -func setupExpectedCalls(t *testing.T) *basicactionmock.BasicActionCapability { - basicAction := &basicactionmock.BasicActionCapability{} - - firstCall := true - callLock := &sync.Mutex{} - basicAction.PerformAction = func(ctx context.Context, input *basicaction.Inputs) (*basicaction.Outputs, error) { - callLock.Lock() - defer callLock.Unlock() - assert.NotEqual(t, firstCall, input.InputThing, "failed first call assertion") - firstCall = false - if input.InputThing { - return &basicaction.Outputs{AdaptedThing: "!"}, nil - } - return &basicaction.Outputs{AdaptedThing: "world"}, nil - } - return basicAction -} - func requireEventsLabels(t *testing.T, beholderObserver beholdertest.Observer, want map[string]string) { msgs := beholderObserver.Messages(t) for _, msg := range msgs { diff --git a/deployment/go.mod b/deployment/go.mod index 30a8eff5eac..8fe0b3bf160 100644 --- a/deployment/go.mod +++ b/deployment/go.mod @@ -43,7 +43,7 @@ require ( github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260121163256-85accaf3d28d github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250912190424-fd2e35d7deb5 github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260129103204-4c8453dd8139 - github.com/smartcontractkit/chainlink-common v0.9.6-0.20260211140822-b833b412cdd9 + github.com/smartcontractkit/chainlink-common v0.10.0 github.com/smartcontractkit/chainlink-common/keystore v1.0.2-0.20260211140822-b833b412cdd9 github.com/smartcontractkit/chainlink-deployments-framework v0.80.1-0.20260209182815-b296b7df28a6 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260205183656-836ec9472717 diff --git a/deployment/go.sum b/deployment/go.sum index 6911a1fe10a..d9814ee434c 100644 --- a/deployment/go.sum +++ b/deployment/go.sum @@ -1369,8 +1369,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260129103204-4c84 github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260129103204-4c8453dd8139/go.mod h1:gUbichNQBqk+fBF2aV40ZkzFmAJ8SygH6DEPd3cJkQE= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260209181943-d6573e8f312f h1:F++iE5sQU020cJbbTosGgyPn2m3A0h2IOWoCKt/QWU8= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260209181943-d6573e8f312f/go.mod h1:qvqpDbul4XF237R3zElpI0gyxWINg46YjQh68N4K5CU= -github.com/smartcontractkit/chainlink-common v0.9.6-0.20260211140822-b833b412cdd9 h1:HX9SbpoRWUB1w8KtkXfN8gGI8+dE7NYuNYfuDQ3E8sI= -github.com/smartcontractkit/chainlink-common v0.9.6-0.20260211140822-b833b412cdd9/go.mod h1:13YN2kb3Vqpw2S7d4IwhX/578WPGC0JHN5JrOnAEsOc= +github.com/smartcontractkit/chainlink-common v0.10.0 h1:d90b9UPJecrIryzhl43F1oQwkJQoug3TaANlJ1xLHyI= +github.com/smartcontractkit/chainlink-common v0.10.0/go.mod h1:13YN2kb3Vqpw2S7d4IwhX/578WPGC0JHN5JrOnAEsOc= github.com/smartcontractkit/chainlink-common/keystore v1.0.2-0.20260211140822-b833b412cdd9 h1:dWqd2lOW3GbwtgZEeDSDI1b1X175MPOlCU6GDQQVBjk= github.com/smartcontractkit/chainlink-common/keystore v1.0.2-0.20260211140822-b833b412cdd9/go.mod h1:SMegDBf3KDs2tuKApmTRyO2xQthMu3gV2J+IuHEs0Y0= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 h1:FJAFgXS9oqASnkS03RE1HQwYQQxrO4l46O5JSzxqLgg= @@ -1453,8 +1453,8 @@ github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20251014143056-a0c6 github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20251014143056-a0c6328c91e9/go.mod h1:h9hMs6K4hT1+mjYnJD3/SW1o7yC/sKjNi0Qh8hLfiCE= github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20251014124537-af6b1684fe15 h1:idp/RjsFznR48JWGfZICsrpcl9JTrnMzoUNVz8MhQMI= github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20251014124537-af6b1684fe15/go.mod h1:ea1LESxlSSOgc2zZBqf1RTkXTMthHaspdqUHd7W4lF0= -github.com/smartcontractkit/cre-sdk-go v0.7.1-0.20250919133015-2df149f34a81 h1:CfnjzJvn3iX93PzdGucyGJmgv/KDXv8DfKcLw/mix24= -github.com/smartcontractkit/cre-sdk-go v0.7.1-0.20250919133015-2df149f34a81/go.mod h1:CQY8hCISjctPmt8ViDVgFm4vMGLs5fYI198QhkBS++Y= +github.com/smartcontractkit/cre-sdk-go v1.3.0 h1:zzbNf8CDjadz4xLZPmv0UQIphxs8ChXs4ow+bmF+2OI= +github.com/smartcontractkit/cre-sdk-go v1.3.0/go.mod h1:LpkUDTXm7DUL0JljsZN1or9mR4/QcGdBai+G1Ng5LPA= github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e h1:Hv9Mww35LrufCdM9wtS9yVi/rEWGI1UnjHbcKKU0nVY= github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e/go.mod h1:T4zH9R8R8lVWKfU7tUvYz2o2jMv1OpGCdpY2j2QZXzU= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 h1:12ijqMM9tvYVEm+nR826WsrNi6zCKpwBhuApq127wHs= diff --git a/go.mod b/go.mod index 42c489d3f35..25f8513deff 100644 --- a/go.mod +++ b/go.mod @@ -88,7 +88,7 @@ require ( github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260121163256-85accaf3d28d github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250912190424-fd2e35d7deb5 github.com/smartcontractkit/chainlink-ccv v0.0.0-20260122132406-0ada7a3fe04a - github.com/smartcontractkit/chainlink-common v0.9.6-0.20260211140822-b833b412cdd9 + github.com/smartcontractkit/chainlink-common v0.10.0 github.com/smartcontractkit/chainlink-common/keystore v1.0.2-0.20260211140822-b833b412cdd9 github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 github.com/smartcontractkit/chainlink-data-streams v0.1.11 @@ -109,7 +109,7 @@ require ( github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260209164410-3aec83b0246f github.com/smartcontractkit/chainlink-sui v0.0.0-20260124000807-bff5e296dfb7 github.com/smartcontractkit/chainlink-ton v0.0.0-20260209205928-e7e034ed7976 - github.com/smartcontractkit/cre-sdk-go v0.7.1-0.20250919133015-2df149f34a81 + github.com/smartcontractkit/cre-sdk-go v1.3.0 github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http v0.7.0 github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v0.8.0 github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e diff --git a/go.sum b/go.sum index 5a64266a4b7..c12f1c85d02 100644 --- a/go.sum +++ b/go.sum @@ -1181,8 +1181,8 @@ github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250 github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250912190424-fd2e35d7deb5/go.mod h1:xtZNi6pOKdC3sLvokDvXOhgHzT+cyBqH/gWwvxTxqrg= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260122132406-0ada7a3fe04a h1:5FxRKkjXvQvPlKx60ELXgOsn7NQIkBj/Au1Z6jpMfjM= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260122132406-0ada7a3fe04a/go.mod h1:Xe0SH5IHtGkCW6sy/EdBRPKD5L+U52HgoGfl0KDP/lw= -github.com/smartcontractkit/chainlink-common v0.9.6-0.20260211140822-b833b412cdd9 h1:HX9SbpoRWUB1w8KtkXfN8gGI8+dE7NYuNYfuDQ3E8sI= -github.com/smartcontractkit/chainlink-common v0.9.6-0.20260211140822-b833b412cdd9/go.mod h1:13YN2kb3Vqpw2S7d4IwhX/578WPGC0JHN5JrOnAEsOc= +github.com/smartcontractkit/chainlink-common v0.10.0 h1:d90b9UPJecrIryzhl43F1oQwkJQoug3TaANlJ1xLHyI= +github.com/smartcontractkit/chainlink-common v0.10.0/go.mod h1:13YN2kb3Vqpw2S7d4IwhX/578WPGC0JHN5JrOnAEsOc= github.com/smartcontractkit/chainlink-common/keystore v1.0.2-0.20260211140822-b833b412cdd9 h1:dWqd2lOW3GbwtgZEeDSDI1b1X175MPOlCU6GDQQVBjk= github.com/smartcontractkit/chainlink-common/keystore v1.0.2-0.20260211140822-b833b412cdd9/go.mod h1:SMegDBf3KDs2tuKApmTRyO2xQthMu3gV2J+IuHEs0Y0= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 h1:FJAFgXS9oqASnkS03RE1HQwYQQxrO4l46O5JSzxqLgg= @@ -1243,8 +1243,8 @@ github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20251014143056-a0c6 github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20251014143056-a0c6328c91e9/go.mod h1:h9hMs6K4hT1+mjYnJD3/SW1o7yC/sKjNi0Qh8hLfiCE= github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20251014124537-af6b1684fe15 h1:idp/RjsFznR48JWGfZICsrpcl9JTrnMzoUNVz8MhQMI= github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20251014124537-af6b1684fe15/go.mod h1:ea1LESxlSSOgc2zZBqf1RTkXTMthHaspdqUHd7W4lF0= -github.com/smartcontractkit/cre-sdk-go v0.7.1-0.20250919133015-2df149f34a81 h1:CfnjzJvn3iX93PzdGucyGJmgv/KDXv8DfKcLw/mix24= -github.com/smartcontractkit/cre-sdk-go v0.7.1-0.20250919133015-2df149f34a81/go.mod h1:CQY8hCISjctPmt8ViDVgFm4vMGLs5fYI198QhkBS++Y= +github.com/smartcontractkit/cre-sdk-go v1.3.0 h1:zzbNf8CDjadz4xLZPmv0UQIphxs8ChXs4ow+bmF+2OI= +github.com/smartcontractkit/cre-sdk-go v1.3.0/go.mod h1:LpkUDTXm7DUL0JljsZN1or9mR4/QcGdBai+G1Ng5LPA= github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http v0.7.0 h1:4C0wM23L1aOLkBru8+iz4VEuryMjijt5RZeZK+EqhUs= github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http v0.7.0/go.mod h1:Lb1l60MI3D8OhvEJVu4GB7rmmTqXpK3smnJZxvdisqY= github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v0.8.0 h1:aO++xdGcQ8TpxAfXrm7EHeIVLDitB8xg7J8/zSxbdBY= diff --git a/integration-tests/go.mod b/integration-tests/go.mod index b3bc1059402..9acdcb47bae 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -51,7 +51,7 @@ require ( github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260203202624-5101f4d33736 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260121163256-85accaf3d28d github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250912190424-fd2e35d7deb5 - github.com/smartcontractkit/chainlink-common v0.9.6-0.20260211140822-b833b412cdd9 + github.com/smartcontractkit/chainlink-common v0.10.0 github.com/smartcontractkit/chainlink-common/keystore v1.0.2-0.20260211140822-b833b412cdd9 github.com/smartcontractkit/chainlink-deployments-framework v0.80.1-0.20260209182815-b296b7df28a6 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260205183656-836ec9472717 diff --git a/integration-tests/go.sum b/integration-tests/go.sum index 07a5ed410a7..e1b1263e237 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -1613,8 +1613,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260129103204-4c84 github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260129103204-4c8453dd8139/go.mod h1:gUbichNQBqk+fBF2aV40ZkzFmAJ8SygH6DEPd3cJkQE= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260209181943-d6573e8f312f h1:F++iE5sQU020cJbbTosGgyPn2m3A0h2IOWoCKt/QWU8= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260209181943-d6573e8f312f/go.mod h1:qvqpDbul4XF237R3zElpI0gyxWINg46YjQh68N4K5CU= -github.com/smartcontractkit/chainlink-common v0.9.6-0.20260211140822-b833b412cdd9 h1:HX9SbpoRWUB1w8KtkXfN8gGI8+dE7NYuNYfuDQ3E8sI= -github.com/smartcontractkit/chainlink-common v0.9.6-0.20260211140822-b833b412cdd9/go.mod h1:13YN2kb3Vqpw2S7d4IwhX/578WPGC0JHN5JrOnAEsOc= +github.com/smartcontractkit/chainlink-common v0.10.0 h1:d90b9UPJecrIryzhl43F1oQwkJQoug3TaANlJ1xLHyI= +github.com/smartcontractkit/chainlink-common v0.10.0/go.mod h1:13YN2kb3Vqpw2S7d4IwhX/578WPGC0JHN5JrOnAEsOc= github.com/smartcontractkit/chainlink-common/keystore v1.0.2-0.20260211140822-b833b412cdd9 h1:dWqd2lOW3GbwtgZEeDSDI1b1X175MPOlCU6GDQQVBjk= github.com/smartcontractkit/chainlink-common/keystore v1.0.2-0.20260211140822-b833b412cdd9/go.mod h1:SMegDBf3KDs2tuKApmTRyO2xQthMu3gV2J+IuHEs0Y0= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 h1:FJAFgXS9oqASnkS03RE1HQwYQQxrO4l46O5JSzxqLgg= @@ -1703,8 +1703,8 @@ github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20251014143056-a0c6 github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20251014143056-a0c6328c91e9/go.mod h1:h9hMs6K4hT1+mjYnJD3/SW1o7yC/sKjNi0Qh8hLfiCE= github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20251014124537-af6b1684fe15 h1:idp/RjsFznR48JWGfZICsrpcl9JTrnMzoUNVz8MhQMI= github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20251014124537-af6b1684fe15/go.mod h1:ea1LESxlSSOgc2zZBqf1RTkXTMthHaspdqUHd7W4lF0= -github.com/smartcontractkit/cre-sdk-go v0.7.1-0.20250919133015-2df149f34a81 h1:CfnjzJvn3iX93PzdGucyGJmgv/KDXv8DfKcLw/mix24= -github.com/smartcontractkit/cre-sdk-go v0.7.1-0.20250919133015-2df149f34a81/go.mod h1:CQY8hCISjctPmt8ViDVgFm4vMGLs5fYI198QhkBS++Y= +github.com/smartcontractkit/cre-sdk-go v1.3.0 h1:zzbNf8CDjadz4xLZPmv0UQIphxs8ChXs4ow+bmF+2OI= +github.com/smartcontractkit/cre-sdk-go v1.3.0/go.mod h1:LpkUDTXm7DUL0JljsZN1or9mR4/QcGdBai+G1Ng5LPA= github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e h1:Hv9Mww35LrufCdM9wtS9yVi/rEWGI1UnjHbcKKU0nVY= github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e/go.mod h1:T4zH9R8R8lVWKfU7tUvYz2o2jMv1OpGCdpY2j2QZXzU= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 h1:12ijqMM9tvYVEm+nR826WsrNi6zCKpwBhuApq127wHs= diff --git a/integration-tests/load/go.mod b/integration-tests/load/go.mod index 72d525bfdfb..00bc932adfb 100644 --- a/integration-tests/load/go.mod +++ b/integration-tests/load/go.mod @@ -32,7 +32,7 @@ require ( github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260203202624-5101f4d33736 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260121163256-85accaf3d28d github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250912190424-fd2e35d7deb5 - github.com/smartcontractkit/chainlink-common v0.9.6-0.20260211140822-b833b412cdd9 + github.com/smartcontractkit/chainlink-common v0.10.0 github.com/smartcontractkit/chainlink-deployments-framework v0.80.1-0.20260209182815-b296b7df28a6 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260205183656-836ec9472717 github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20251222115927-36a18321243c diff --git a/integration-tests/load/go.sum b/integration-tests/load/go.sum index 4dec689eb07..279d005a0cb 100644 --- a/integration-tests/load/go.sum +++ b/integration-tests/load/go.sum @@ -1591,8 +1591,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260129103204-4c84 github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260129103204-4c8453dd8139/go.mod h1:gUbichNQBqk+fBF2aV40ZkzFmAJ8SygH6DEPd3cJkQE= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260209181943-d6573e8f312f h1:F++iE5sQU020cJbbTosGgyPn2m3A0h2IOWoCKt/QWU8= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260209181943-d6573e8f312f/go.mod h1:qvqpDbul4XF237R3zElpI0gyxWINg46YjQh68N4K5CU= -github.com/smartcontractkit/chainlink-common v0.9.6-0.20260211140822-b833b412cdd9 h1:HX9SbpoRWUB1w8KtkXfN8gGI8+dE7NYuNYfuDQ3E8sI= -github.com/smartcontractkit/chainlink-common v0.9.6-0.20260211140822-b833b412cdd9/go.mod h1:13YN2kb3Vqpw2S7d4IwhX/578WPGC0JHN5JrOnAEsOc= +github.com/smartcontractkit/chainlink-common v0.10.0 h1:d90b9UPJecrIryzhl43F1oQwkJQoug3TaANlJ1xLHyI= +github.com/smartcontractkit/chainlink-common v0.10.0/go.mod h1:13YN2kb3Vqpw2S7d4IwhX/578WPGC0JHN5JrOnAEsOc= github.com/smartcontractkit/chainlink-common/keystore v1.0.2-0.20260211140822-b833b412cdd9 h1:dWqd2lOW3GbwtgZEeDSDI1b1X175MPOlCU6GDQQVBjk= github.com/smartcontractkit/chainlink-common/keystore v1.0.2-0.20260211140822-b833b412cdd9/go.mod h1:SMegDBf3KDs2tuKApmTRyO2xQthMu3gV2J+IuHEs0Y0= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 h1:FJAFgXS9oqASnkS03RE1HQwYQQxrO4l46O5JSzxqLgg= @@ -1681,8 +1681,8 @@ github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20251014143056-a0c6 github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20251014143056-a0c6328c91e9/go.mod h1:h9hMs6K4hT1+mjYnJD3/SW1o7yC/sKjNi0Qh8hLfiCE= github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20251014124537-af6b1684fe15 h1:idp/RjsFznR48JWGfZICsrpcl9JTrnMzoUNVz8MhQMI= github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20251014124537-af6b1684fe15/go.mod h1:ea1LESxlSSOgc2zZBqf1RTkXTMthHaspdqUHd7W4lF0= -github.com/smartcontractkit/cre-sdk-go v0.7.1-0.20250919133015-2df149f34a81 h1:CfnjzJvn3iX93PzdGucyGJmgv/KDXv8DfKcLw/mix24= -github.com/smartcontractkit/cre-sdk-go v0.7.1-0.20250919133015-2df149f34a81/go.mod h1:CQY8hCISjctPmt8ViDVgFm4vMGLs5fYI198QhkBS++Y= +github.com/smartcontractkit/cre-sdk-go v1.3.0 h1:zzbNf8CDjadz4xLZPmv0UQIphxs8ChXs4ow+bmF+2OI= +github.com/smartcontractkit/cre-sdk-go v1.3.0/go.mod h1:LpkUDTXm7DUL0JljsZN1or9mR4/QcGdBai+G1Ng5LPA= github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e h1:Hv9Mww35LrufCdM9wtS9yVi/rEWGI1UnjHbcKKU0nVY= github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e/go.mod h1:T4zH9R8R8lVWKfU7tUvYz2o2jMv1OpGCdpY2j2QZXzU= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 h1:12ijqMM9tvYVEm+nR826WsrNi6zCKpwBhuApq127wHs= diff --git a/plugins/plugins.private.yaml b/plugins/plugins.private.yaml index 1c2fbbb96ad..4264ce90bbd 100644 --- a/plugins/plugins.private.yaml +++ b/plugins/plugins.private.yaml @@ -33,7 +33,7 @@ plugins: installPath: "." httpaction: - moduleURI: "github.com/smartcontractkit/capabilities/http_action" - gitRef: "17f6545c0ff1742b774e0b87860563cde0ad14bb" + gitRef: "e925fcc435c163eebfaf149d1dd49ad71562d5de" installPath: "." httptrigger: - moduleURI: "github.com/smartcontractkit/capabilities/http_trigger" diff --git a/system-tests/lib/cre/workflow/compile.go b/system-tests/lib/cre/workflow/compile.go index 7c003d8c767..cf45e2c460a 100644 --- a/system-tests/lib/cre/workflow/compile.go +++ b/system-tests/lib/cre/workflow/compile.go @@ -98,8 +98,8 @@ func compileGoWorkflow(ctx context.Context, workflowFilePath, workflowName strin goModTidyCmd := exec.CommandContext(ctx, "go", "mod", "tidy") goModTidyCmd.Dir = filepath.Dir(workflowFilePath) - if err := goModTidyCmd.Run(); err != nil { - return "", errors.Wrap(err, "failed to run go mod tidy") + if output, err := goModTidyCmd.CombinedOutput(); err != nil { + return "", errors.Wrapf(err, "failed to run go mod tidy: %s", string(output)) } compileCmd := exec.CommandContext(ctx, "go", "build", "-o", workflowWasmPath, filepath.Base(workflowFilePath)) // #nosec G204 -- we control the value of the cmd so the lint/sec error is a false positive diff --git a/system-tests/lib/go.mod b/system-tests/lib/go.mod index d3ad8c342fd..d960ebf680f 100644 --- a/system-tests/lib/go.mod +++ b/system-tests/lib/go.mod @@ -32,7 +32,7 @@ require ( github.com/sethvargo/go-retry v0.3.0 github.com/smartcontractkit/chain-selectors v1.0.91 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260121163256-85accaf3d28d - github.com/smartcontractkit/chainlink-common v0.9.6-0.20260211140822-b833b412cdd9 + github.com/smartcontractkit/chainlink-common v0.10.0 github.com/smartcontractkit/chainlink-common/keystore v1.0.2-0.20260211140822-b833b412cdd9 github.com/smartcontractkit/chainlink-deployments-framework v0.80.1-0.20260209182815-b296b7df28a6 github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260205183656-836ec9472717 diff --git a/system-tests/lib/go.sum b/system-tests/lib/go.sum index 6295638db17..853f4111899 100644 --- a/system-tests/lib/go.sum +++ b/system-tests/lib/go.sum @@ -1568,8 +1568,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260129103204-4c84 github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260129103204-4c8453dd8139/go.mod h1:gUbichNQBqk+fBF2aV40ZkzFmAJ8SygH6DEPd3cJkQE= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260209181943-d6573e8f312f h1:F++iE5sQU020cJbbTosGgyPn2m3A0h2IOWoCKt/QWU8= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260209181943-d6573e8f312f/go.mod h1:qvqpDbul4XF237R3zElpI0gyxWINg46YjQh68N4K5CU= -github.com/smartcontractkit/chainlink-common v0.9.6-0.20260211140822-b833b412cdd9 h1:HX9SbpoRWUB1w8KtkXfN8gGI8+dE7NYuNYfuDQ3E8sI= -github.com/smartcontractkit/chainlink-common v0.9.6-0.20260211140822-b833b412cdd9/go.mod h1:13YN2kb3Vqpw2S7d4IwhX/578WPGC0JHN5JrOnAEsOc= +github.com/smartcontractkit/chainlink-common v0.10.0 h1:d90b9UPJecrIryzhl43F1oQwkJQoug3TaANlJ1xLHyI= +github.com/smartcontractkit/chainlink-common v0.10.0/go.mod h1:13YN2kb3Vqpw2S7d4IwhX/578WPGC0JHN5JrOnAEsOc= github.com/smartcontractkit/chainlink-common/keystore v1.0.2-0.20260211140822-b833b412cdd9 h1:dWqd2lOW3GbwtgZEeDSDI1b1X175MPOlCU6GDQQVBjk= github.com/smartcontractkit/chainlink-common/keystore v1.0.2-0.20260211140822-b833b412cdd9/go.mod h1:SMegDBf3KDs2tuKApmTRyO2xQthMu3gV2J+IuHEs0Y0= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 h1:FJAFgXS9oqASnkS03RE1HQwYQQxrO4l46O5JSzxqLgg= @@ -1656,8 +1656,8 @@ github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20251014143056-a0c6 github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20251014143056-a0c6328c91e9/go.mod h1:h9hMs6K4hT1+mjYnJD3/SW1o7yC/sKjNi0Qh8hLfiCE= github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20251014124537-af6b1684fe15 h1:idp/RjsFznR48JWGfZICsrpcl9JTrnMzoUNVz8MhQMI= github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20251014124537-af6b1684fe15/go.mod h1:ea1LESxlSSOgc2zZBqf1RTkXTMthHaspdqUHd7W4lF0= -github.com/smartcontractkit/cre-sdk-go v0.7.1-0.20250919133015-2df149f34a81 h1:CfnjzJvn3iX93PzdGucyGJmgv/KDXv8DfKcLw/mix24= -github.com/smartcontractkit/cre-sdk-go v0.7.1-0.20250919133015-2df149f34a81/go.mod h1:CQY8hCISjctPmt8ViDVgFm4vMGLs5fYI198QhkBS++Y= +github.com/smartcontractkit/cre-sdk-go v1.3.0 h1:zzbNf8CDjadz4xLZPmv0UQIphxs8ChXs4ow+bmF+2OI= +github.com/smartcontractkit/cre-sdk-go v1.3.0/go.mod h1:LpkUDTXm7DUL0JljsZN1or9mR4/QcGdBai+G1Ng5LPA= github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e h1:Hv9Mww35LrufCdM9wtS9yVi/rEWGI1UnjHbcKKU0nVY= github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e/go.mod h1:T4zH9R8R8lVWKfU7tUvYz2o2jMv1OpGCdpY2j2QZXzU= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 h1:12ijqMM9tvYVEm+nR826WsrNi6zCKpwBhuApq127wHs= diff --git a/system-tests/tests/go.mod b/system-tests/tests/go.mod index 1ca98c1a956..20984319bdd 100644 --- a/system-tests/tests/go.mod +++ b/system-tests/tests/go.mod @@ -54,7 +54,7 @@ require ( github.com/rs/zerolog v1.34.0 github.com/shopspring/decimal v1.4.0 github.com/smartcontractkit/chain-selectors v1.0.91 - github.com/smartcontractkit/chainlink-common v0.9.6-0.20260211140822-b833b412cdd9 + github.com/smartcontractkit/chainlink-common v0.10.0 github.com/smartcontractkit/chainlink-common/keystore v1.0.2-0.20260211140822-b833b412cdd9 github.com/smartcontractkit/chainlink-data-streams v0.1.11 github.com/smartcontractkit/chainlink-deployments-framework v0.80.1-0.20260209182815-b296b7df28a6 diff --git a/system-tests/tests/go.sum b/system-tests/tests/go.sum index a97df9ef04c..85759677e00 100644 --- a/system-tests/tests/go.sum +++ b/system-tests/tests/go.sum @@ -1777,8 +1777,8 @@ github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260129103204-4c84 github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260129103204-4c8453dd8139/go.mod h1:gUbichNQBqk+fBF2aV40ZkzFmAJ8SygH6DEPd3cJkQE= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260209181943-d6573e8f312f h1:F++iE5sQU020cJbbTosGgyPn2m3A0h2IOWoCKt/QWU8= github.com/smartcontractkit/chainlink-ccv v0.0.0-20260209181943-d6573e8f312f/go.mod h1:qvqpDbul4XF237R3zElpI0gyxWINg46YjQh68N4K5CU= -github.com/smartcontractkit/chainlink-common v0.9.6-0.20260211140822-b833b412cdd9 h1:HX9SbpoRWUB1w8KtkXfN8gGI8+dE7NYuNYfuDQ3E8sI= -github.com/smartcontractkit/chainlink-common v0.9.6-0.20260211140822-b833b412cdd9/go.mod h1:13YN2kb3Vqpw2S7d4IwhX/578WPGC0JHN5JrOnAEsOc= +github.com/smartcontractkit/chainlink-common v0.10.0 h1:d90b9UPJecrIryzhl43F1oQwkJQoug3TaANlJ1xLHyI= +github.com/smartcontractkit/chainlink-common v0.10.0/go.mod h1:13YN2kb3Vqpw2S7d4IwhX/578WPGC0JHN5JrOnAEsOc= github.com/smartcontractkit/chainlink-common/keystore v1.0.2-0.20260211140822-b833b412cdd9 h1:dWqd2lOW3GbwtgZEeDSDI1b1X175MPOlCU6GDQQVBjk= github.com/smartcontractkit/chainlink-common/keystore v1.0.2-0.20260211140822-b833b412cdd9/go.mod h1:SMegDBf3KDs2tuKApmTRyO2xQthMu3gV2J+IuHEs0Y0= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20251211140724-319861e514c4 h1:NOUsjsMzNecbjiPWUQGlRSRAutEvCFrqqyETDJeh5q4= @@ -1873,8 +1873,8 @@ github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.202510141 github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20251014124537-af6b1684fe15/go.mod h1:ea1LESxlSSOgc2zZBqf1RTkXTMthHaspdqUHd7W4lF0= github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/evmread v0.0.0-20250917232237-c4ecf802c6f8 h1:ZhpUCMDFATsyS1B+6YaAxWYfh/WsVx9WWtYSOkl5V0g= github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/evmread v0.0.0-20250917232237-c4ecf802c6f8/go.mod h1:96T5PZe9IRPcuMTnS2I2VGAtyDdkL5U9aWUykLtAYb8= -github.com/smartcontractkit/cre-sdk-go v1.1.0 h1:7tcN/uDty2ex39xw8sDnvLaEh6swQJBYRsrBVDpGSrw= -github.com/smartcontractkit/cre-sdk-go v1.1.0/go.mod h1:sgiRyHUiPcxp1e/EMnaJ+ddMFL4MbE3UMZ2MORAAS9U= +github.com/smartcontractkit/cre-sdk-go v1.3.0 h1:zzbNf8CDjadz4xLZPmv0UQIphxs8ChXs4ow+bmF+2OI= +github.com/smartcontractkit/cre-sdk-go v1.3.0/go.mod h1:LpkUDTXm7DUL0JljsZN1or9mR4/QcGdBai+G1Ng5LPA= github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e h1:Hv9Mww35LrufCdM9wtS9yVi/rEWGI1UnjHbcKKU0nVY= github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e/go.mod h1:T4zH9R8R8lVWKfU7tUvYz2o2jMv1OpGCdpY2j2QZXzU= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 h1:12ijqMM9tvYVEm+nR826WsrNi6zCKpwBhuApq127wHs= diff --git a/system-tests/tests/smoke/cre/cre_suite_test.go b/system-tests/tests/smoke/cre/cre_suite_test.go index 1c6675a24eb..1cee094ef73 100644 --- a/system-tests/tests/smoke/cre/cre_suite_test.go +++ b/system-tests/tests/smoke/cre/cre_suite_test.go @@ -155,6 +155,12 @@ func Test_CRE_V2_HTTP_Action_Suite(t *testing.T) { ExecuteHTTPActionCRUDSuccessTest(t, testEnv) } +func Test_CRE_V2_HTTP_Action_Regression_Suite(t *testing.T) { + testEnv := t_helpers.SetupTestEnvironmentWithConfig(t, t_helpers.GetDefaultTestConfig(t), v2RegistriesFlags...) + + ExecuteHTTPActionRegressionTest(t, testEnv) +} + func Test_CRE_V2_Beholder_Suite(t *testing.T) { testEnv := t_helpers.SetupTestEnvironmentWithConfig(t, t_helpers.GetDefaultTestConfig(t), append(v2RegistriesFlags, "--with-dashboards")...) diff --git a/system-tests/tests/smoke/cre/httpaction/README.md b/system-tests/tests/smoke/cre/httpaction/README.md new file mode 100644 index 00000000000..5349590961e --- /dev/null +++ b/system-tests/tests/smoke/cre/httpaction/README.md @@ -0,0 +1,71 @@ +# HTTP Action Workflow (Smoke & Regression) + +This package contains the **HTTP Action** CRE workflow used by the smoke and regression system tests. The workflow is a single WASM binary whose behavior is driven by configuration; the test runner chooses which suite (smoke vs regression) deploys it and which test case to run. + +## Theory and Design + +### Purpose + +The workflow exercises the **HTTP Action capability** (outbound HTTP from a CRE node) in a DON. It is used to verify: + +1. **Happy path (smoke)**: CRUD-style requests (GET, POST, PUT, DELETE), correct status handling, and response handling. +2. **Multi-value headers (smoke)**: Backwards compatibility of response `Headers` (e.g. comma-joined `Set-Cookie`) and the newer response `MultiHeaders` with distinct values. +3. **Validation regression**: When both `Headers` and `MultiHeaders` are set on a request, the capability must reject it with a user error (regression test). + +### Flow + +1. **Trigger**: A **cron** trigger runs the workflow on a schedule (e.g. every 30 seconds). +2. **Orchestration**: The workflow runs in **node mode** so that the HTTP client can perform outbound requests from a node. The same workflow binary is used for all test cases; the **config field `testCase`** selects the behavior. +3. **Dispatch**: A map of test case names to handler functions selects either a special case (e.g. `multi-headers`, `mh-regression-both`) or the default **generic CRUD** path. +4. **Consensus**: Results are aggregated with **consensus identical aggregation** (all nodes must return the same string result). +5. **Result**: The aggregated result string is returned and the test runner asserts on it (e.g. "HTTP Action CRUD success test completed: …" or "HTTP Action multi-headers regression completed"). + +### Configuration + +The workflow expects a YAML config (see `config/config.go`) with: + +- **URL**: Target base URL for HTTP requests (e.g. fake server from the test). +- **TestCase**: Identifies which test to run (`crud-post-success`, `multi-headers`, `mh-regression-both`, etc.). +- **Method**, **Body**: HTTP method and body for the request (used by the default CRUD path and by regression). + +Config is passed at workflow registration time and is read by the workflow when it runs. + +## Alignment with CRE Smoke Test Spec + +The parent [CRE README](../README.md) defines: + +- **Smoke vs regression**: *"Everything that is not a happy path functional system-tests (i.e. edge cases, negative conditions) should go to a regression package."* +- **Test architecture**: Environment is created once per topology; multiple tests can run against the same environment; tests follow the `Test_CRE_` naming convention and standard structure. + +### How this workflow fits + +| Aspect | Implementation | +|--------|----------------| +| **Smoke (happy path)** | `Test_CRE_V2_HTTP_Action_Suite` and `Test_CRE_V2_HTTP_Action_Regression_Suite` (see [cre_suite_test.go](../cre_suite_test.go)) use the same workflow binary. The **smoke suite** runs success cases only: CRUD operations and multi-headers response test. | +| **Regression (edge/negative)** | The **regression suite** (`Test_CRE_V2_HTTP_Action_Regression_Suite`) runs the same workflow with `testCase: mh-regression-both`. That case sends a request with both `Headers` and `MultiHeaders` set; the capability must return a user error. The workflow asserts the error message and returns a fixed success string so the test can verify the regression. | +| **Single binary, many cases** | One compiled workflow (`main.go`) handles all cases via `testCaseHandlers` and config. No separate regression binary. | +| **Workflow compilation** | The test runner compiles this Go package to WASM (e.g. `creworkflow.CompileWorkflow`), deploys it, and registers it with the contract as described in the parent README (§11). | +| **Naming** | Workflow names are kept short (e.g. `mh-regression-both`) to stay under the workflow name length limit (64 chars) used at deploy time. | + +### Test cases (config `testCase`) + +- **Default (CRUD)**: Any unrecognized `testCase` runs a single HTTP request with `Content-Type: application/json`, checks 2xx status, and returns a success message including the case name. +- **`multi-headers`**: Sends two requests (Headers-only, then MultiHeaders-only), asserts response headers and Set-Cookie handling (comma-joined in `Headers`, multiple values in `MultiHeaders`). +- **`mh-regression-both`**: Sends one request with both `Headers` and `MultiHeaders` set; expects the capability to reject it with a user error containing specific substrings; returns a fixed success message for the regression suite. + +## Files + +- **`main.go`**: Workflow entry, cron handler, test dispatch, and all test-case logic (CRUD, multi-headers, regression). Built with `GOOS=wasip1`, `GOARCH=wasm`. +- **`config/config.go`**: Config struct (URL, TestCase, Method, Body) used for YAML unmarshalling and passed into the workflow at runtime. + +## Running the tests + +From the repo root (see parent README for CRE environment setup): + +```bash +# Smoke: HTTP Action success cases only +go test -timeout 15m -run "^Test_CRE_V2_HTTP_Action_Suite" ./system-tests/tests/smoke/cre/... + +# Regression: HTTP Action regression cases (e.g. both Headers and MultiHeaders rejected) +go test -timeout 15m -run "^Test_CRE_V2_HTTP_Action_Regression_Suite" ./system-tests/tests/smoke/cre/... +``` diff --git a/system-tests/tests/smoke/cre/httpaction/go.mod b/system-tests/tests/smoke/cre/httpaction/go.mod index 210ca0b3f2b..41927f3971e 100644 --- a/system-tests/tests/smoke/cre/httpaction/go.mod +++ b/system-tests/tests/smoke/cre/httpaction/go.mod @@ -3,9 +3,9 @@ module github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/httpac go 1.25.5 require ( - github.com/smartcontractkit/cre-sdk-go v1.0.1-0.20251111122439-00032d582c18 - github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http v0.10.0 - github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v0.10.0 + github.com/smartcontractkit/cre-sdk-go v1.3.0 + github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http v1.3.0 + github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v1.3.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 ) @@ -17,7 +17,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/shopspring/decimal v1.4.0 // indirect - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20251124151448-0448aefdaab9 // indirect + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260210221717-2546aed27ebe // indirect github.com/stretchr/testify v1.11.1 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/system-tests/tests/smoke/cre/httpaction/go.sum b/system-tests/tests/smoke/cre/httpaction/go.sum index f87ee993d50..7e6fdbf20e3 100644 --- a/system-tests/tests/smoke/cre/httpaction/go.sum +++ b/system-tests/tests/smoke/cre/httpaction/go.sum @@ -20,14 +20,14 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20251124151448-0448aefdaab9 h1:QRWXJusIj/IRY5Pl3JclNvDre0cZPd/5NbILwc4RV2M= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20251124151448-0448aefdaab9/go.mod h1:jUC52kZzEnWF9tddHh85zolKybmLpbQ1oNA4FjOHt1Q= -github.com/smartcontractkit/cre-sdk-go v1.0.1-0.20251111122439-00032d582c18 h1:x8NX+vQzScvg4XbKDA0NF8hfxpruOjR78fag3SxhwOo= -github.com/smartcontractkit/cre-sdk-go v1.0.1-0.20251111122439-00032d582c18/go.mod h1:sgiRyHUiPcxp1e/EMnaJ+ddMFL4MbE3UMZ2MORAAS9U= -github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http v0.10.0 h1:nP6PVWrrTIICvjwQuFitsQecQWbqpPaYzaTEjx92eTQ= -github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http v0.10.0/go.mod h1:M83m3FsM1uqVu06OO58mKUSZJjjH8OGJsmvFpFlRDxI= -github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v0.10.0 h1:g7UrVaNKVEmIhVkJTk4f8raCM8Kp/RTFnAT64wqNmTY= -github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v0.10.0/go.mod h1:PWyrIw16It4TSyq6mDXqmSR0jF2evZRKuBxu7pK1yDw= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260210221717-2546aed27ebe h1:Vc4zoSc/j6/FdCQ7vcyHTTB7kzHI2f+lHCHqFuiCcJQ= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260210221717-2546aed27ebe/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= +github.com/smartcontractkit/cre-sdk-go v1.3.0 h1:zzbNf8CDjadz4xLZPmv0UQIphxs8ChXs4ow+bmF+2OI= +github.com/smartcontractkit/cre-sdk-go v1.3.0/go.mod h1:LpkUDTXm7DUL0JljsZN1or9mR4/QcGdBai+G1Ng5LPA= +github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http v1.3.0 h1:m0OkXuaLtIcYvBrLtxSfygrGtBJvPwaSoANe48434BA= +github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http v1.3.0/go.mod h1:QpLhMGMa//e4G9qMmmCK4NPMcadRBaWC2FDV9hniMrI= +github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v1.3.0 h1:qBZ4y6qlTOynSpU1QAi2Fgr3tUZQ332b6hit9EVZqkk= +github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v1.3.0/go.mod h1:Rzhy75vD3FqQo/SV6lypnxIwjWac6IOWzI5BYj3tYMU= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/system-tests/tests/smoke/cre/httpaction/main.go b/system-tests/tests/smoke/cre/httpaction/main.go index fd4068b9ce6..b10f5e71fe7 100644 --- a/system-tests/tests/smoke/cre/httpaction/main.go +++ b/system-tests/tests/smoke/cre/httpaction/main.go @@ -5,6 +5,8 @@ package main import ( "fmt" "log/slog" + "slices" + "strings" "github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/httpaction/config" "github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http" @@ -15,6 +17,34 @@ import ( "gopkg.in/yaml.v3" ) +const bothSetRegressionSuccess = "HTTP Action multi-headers regression completed" + +// Expected Set-Cookie values from the fake server (v2_http_action_test.go). +var expectedSetCookieSubstrings = []string{ + "sessionid=multi-e2e-1", + "csrf=multi-e2e-2", + "pref=multi-e2e-3", +} + +// bothSetRegressionExpectedSubstrings must all appear in the capability's user error +// when both Headers and MultiHeaders are set on a request (validation rejects). +var bothSetRegressionExpectedSubstrings = []string{ + "Headers or MultiHeaders", + "not both", +} + +// nodeTestFunc is the type of a test run inside node mode (cfg, runtime, client, logger) -> (result, err). +// Logger is the CRE runtime logger; see cre.Runtime.Logger() which returns *slog.Logger. +type nodeTestFunc func(config.Config, cre.NodeRuntime, *http.Client, *slog.Logger) (string, error) + +// testCaseHandlers dispatch by cfg.TestCase; special cases run first, default runs generic CRUD. +var testCaseHandlers = map[string]nodeTestFunc{ + "multi-headers": runMultiHeadersTest, + "mh-regression-both": runBothSetRegressionTest, +} + +// --- Entry and workflow --- + func main() { wasm.NewRunner(func(b []byte) (config.Config, error) { wfCfg := config.Config{} @@ -34,7 +64,7 @@ func RunHTTPActionSuccessWorkflow(wfCfg config.Config, _ *slog.Logger, _ cre.Sec }, nil } -func onCronTrigger(wfCfg config.Config, runtime cre.Runtime, payload *cron.Payload) (_ any, _ error) { +func onCronTrigger(wfCfg config.Config, runtime cre.Runtime, payload *cron.Payload) (any, error) { logger := runtime.Logger() logger.Info( "HTTP Action workflow triggered", @@ -49,76 +79,19 @@ func onCronTrigger(wfCfg config.Config, runtime cre.Runtime, payload *cron.Paylo return runCRUDSuccessTest(wfCfg, runtime) } +// --- Test orchestration and test functions --- + func runCRUDSuccessTest(wfCfg config.Config, runtime cre.Runtime) (string, error) { logger := runtime.Logger() - logger.Info( - "Running HTTP Action capability", - "testCase", - wfCfg.TestCase, - "method", - wfCfg.Method, - "url", - wfCfg.URL, - ) + logger.Info("Running HTTP Action capability", "testCase", wfCfg.TestCase, "method", wfCfg.Method, "url", wfCfg.URL) - // Execute CRUD operations using HTTP Action capability crudPromise := cre.RunInNodeMode(wfCfg, runtime, func(cfg config.Config, nodeRuntime cre.NodeRuntime) (string, error) { client := &http.Client{} - - req := &http.Request{ - Url: cfg.URL, - Method: cfg.Method, - Headers: map[string]string{"Content-Type": "application/json"}, - Body: []byte(cfg.Body), - Timeout: &durationpb.Duration{Seconds: 10}, + if fn, ok := testCaseHandlers[cfg.TestCase]; ok { + return fn(cfg, nodeRuntime, client, logger) } - - logger.Info("Testing HTTP Action capability with configuration", - "url", req.Url, - "method", req.Method, - "hasBody", len(cfg.Body) > 0) - - resp, err := client.SendRequest(nodeRuntime, req).Await() - if err != nil { - logger.Error( - "Failed to complete HTTP Action request", - "error", - err, - "url", - req.Url, - "method", - req.Method, - ) - return "", fmt.Errorf("HTTP Action %s request failed: %w", req.Method, err) - } - - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - logger.Error( - "Failed response to HTTP Action request", - "status", - resp.StatusCode, - "url", - req.Url, - "method", - req.Method, - ) - return "", fmt.Errorf("HTTP Action %s request failed with status: %d", req.Method, resp.StatusCode) - } - - logger.Info( - "HTTP Action completed", - "url", - req.Url, - "method", - req.Method, - "status", - resp.StatusCode, - "body", - string(resp.Body), - ) - - return fmt.Sprintf("HTTP Action CRUD success test completed: %s", cfg.TestCase), nil + return runDefaultCRUDTest(cfg, nodeRuntime, client, logger) }, cre.ConsensusIdenticalAggregation[string](), ) @@ -128,7 +101,179 @@ func runCRUDSuccessTest(wfCfg config.Config, runtime cre.Runtime) (string, error logger.Error("Failed to complete HTTP Action capability", "error", err) return "", fmt.Errorf("HTTP Action test failed: %w", err) } - logger.Info("HTTP Action test completed", "result", result) return result, nil } + +func runDefaultCRUDTest(cfg config.Config, nodeRuntime cre.NodeRuntime, client *http.Client, logger *slog.Logger) (string, error) { + req := &http.Request{ + Url: cfg.URL, + Method: cfg.Method, + Headers: map[string]string{"Content-Type": "application/json"}, + Body: []byte(cfg.Body), + Timeout: &durationpb.Duration{Seconds: 10}, + } + logger.Info("Testing HTTP Action capability with configuration", "url", req.Url, "method", req.Method, "hasBody", len(cfg.Body) > 0) + + resp, err := client.SendRequest(nodeRuntime, req).Await() + if err != nil { + logger.Error("Failed to complete HTTP Action request", "error", err, "url", req.Url, "method", req.Method) + return "", fmt.Errorf("HTTP Action %s request failed: %w", req.Method, err) + } + if !statusOK(resp.StatusCode) { + logger.Error("Failed response to HTTP Action request", "status", resp.StatusCode, "url", req.Url, "method", req.Method) + return "", fmt.Errorf("HTTP Action %s request failed with status: %d", req.Method, resp.StatusCode) + } + logger.Info("HTTP Action completed", "url", req.Url, "method", req.Method, "status", resp.StatusCode, "body", string(resp.Body)) + return fmt.Sprintf("HTTP Action CRUD success test completed: %s", cfg.TestCase), nil +} + +// runMultiHeadersTest sends two requests (Headers-only and MultiHeaders-only), then asserts +// backwards compatibility (response Headers) and the new feature (response MultiHeaders). +func runMultiHeadersTest(cfg config.Config, nodeRuntime cre.NodeRuntime, client *http.Client, log *slog.Logger) (string, error) { + timeout := &durationpb.Duration{Seconds: 10} + + // 1) Headers-only request: assert response Headers match sent and Set-Cookie is comma-joined (backwards compat). + sentHeaders := map[string]string{ + "Content-Type": "application/json", + "Accept-Language": "en,fr", + } + resp1, err := client.SendRequest(nodeRuntime, &http.Request{ + Url: cfg.URL, + Method: cfg.Method, + Headers: sentHeaders, + Body: []byte(cfg.Body), + Timeout: timeout, + }).Await() + if err != nil { + return "", fmt.Errorf("HTTP Action multi-headers (Headers request) failed: %w", err) + } + if !statusOK(resp1.StatusCode) { + return "", fmt.Errorf("HTTP Action multi-headers (Headers request) status: %d", resp1.StatusCode) + } + h := resp1.GetHeaders() //nolint:staticcheck + if err := assertMapContains(h, sentHeaders, "HTTP Action multi-headers response Headers (backwards compat)"); err != nil { + return "", err + } + setCookieJoined, ok := h["Set-Cookie"] + if !ok { + return "", fmt.Errorf("HTTP Action multi-headers test failed: Set-Cookie not in response Headers (backwards compat)") + } + if slices.IndexFunc(expectedSetCookieSubstrings, func(sub string) bool { return !strings.Contains(setCookieJoined, sub) }) != -1 { + return "", fmt.Errorf("HTTP Action multi-headers test failed: Set-Cookie in Headers should be comma-joined with all three values, got %q", setCookieJoined) + } + log.Info("HTTP Action multi-headers test: Headers (backwards compat) OK") + + // 2) MultiHeaders-only request: assert response MultiHeaders match sent and Set-Cookie has three distinct values. + sentMultiHeaders := map[string]*http.HeaderValues{ + "Content-Type": {Values: []string{"application/json"}}, + "Accept-Language": {Values: []string{"en", "fr"}}, + } + resp2, err := client.SendRequest(nodeRuntime, &http.Request{ + Url: cfg.URL, + Method: cfg.Method, + MultiHeaders: sentMultiHeaders, + Body: []byte(cfg.Body), + Timeout: timeout, + }).Await() + if err != nil { + return "", fmt.Errorf("HTTP Action multi-headers (MultiHeaders request) failed: %w", err) + } + if !statusOK(resp2.StatusCode) { + return "", fmt.Errorf("HTTP Action multi-headers (MultiHeaders request) status: %d", resp2.StatusCode) + } + mh := resp2.GetMultiHeaders() + if err := assertMultiHeadersContains(mh, sentMultiHeaders, "HTTP Action multi-headers response MultiHeaders"); err != nil { + return "", err + } + setCookieHV, ok := mh["Set-Cookie"] + if !ok || setCookieHV == nil { + return "", fmt.Errorf("Set-Cookie not in MultiHeaders") + } + vals := setCookieHV.GetValues() + if len(vals) != 3 { + return "", fmt.Errorf("Set-Cookie in MultiHeaders should have 3 distinct values, got %d: %v", len(vals), vals) + } + for _, sub := range expectedSetCookieSubstrings { + if !slices.ContainsFunc(vals, func(v string) bool { return strings.Contains(v, sub) }) { + return "", fmt.Errorf("Set-Cookie MultiHeaders missing expected value containing %q, got %v", sub, vals) + } + } + log.Info("HTTP Action multi-headers test passed", "setCookieCount", len(vals)) + return "HTTP Action multi-headers test completed", nil +} + +// runBothSetRegressionTest sends a request with both Headers and MultiHeaders set; the capability +// must reject it with a user error. This is a regression test to ensure validation is enforced. +func runBothSetRegressionTest(cfg config.Config, nodeRuntime cre.NodeRuntime, client *http.Client, log *slog.Logger) (string, error) { + timeout := &durationpb.Duration{Seconds: 10} + req := &http.Request{ + Url: cfg.URL, + Method: cfg.Method, + Headers: map[string]string{"X-Test": "value"}, + MultiHeaders: map[string]*http.HeaderValues{ + "Accept": {Values: []string{"application/json"}}, + }, + Body: []byte(cfg.Body), + Timeout: timeout, + } + _, err := client.SendRequest(nodeRuntime, req).Await() + if err == nil { + return "", fmt.Errorf("multi-headers regression: expected user error when both Headers and MultiHeaders are set, but request succeeded") + } + if !errorContainsAll(err, bothSetRegressionExpectedSubstrings) { + return "", fmt.Errorf("multi-headers regression: expected user error containing %v, got: %w", bothSetRegressionExpectedSubstrings, err) + } + log.Info("HTTP Action multi-headers regression passed: both Headers and MultiHeaders set correctly rejected") + return bothSetRegressionSuccess, nil +} + +// --- Helper functions --- + +// statusOK reports whether code is in the 2xx range. +func statusOK(code uint32) bool { return code >= 200 && code < 300 } + +// errorContainsAll reports whether err is non-nil and its message contains every substring. +func errorContainsAll(err error, substrings []string) bool { + if err == nil { + return false + } + s := err.Error() + return slices.IndexFunc(substrings, func(sub string) bool { return !strings.Contains(s, sub) }) == -1 +} + +// assertMapContains checks that got contains every key from want with the same value. +func assertMapContains(got map[string]string, want map[string]string, context string) error { + if got == nil { + return fmt.Errorf("%s: response map is nil", context) + } + for name, wantVal := range want { + gotVal, ok := got[name] + if !ok { + return fmt.Errorf("%s: missing key %q", context, name) + } + if gotVal != wantVal { + return fmt.Errorf("%s: %q = %q, want %q", context, name, gotVal, wantVal) + } + } + return nil +} + +// assertMultiHeadersContains checks that got contains every key from want with the same Values slice. +func assertMultiHeadersContains(got map[string]*http.HeaderValues, want map[string]*http.HeaderValues, context string) error { + if got == nil { + return fmt.Errorf("%s: response MultiHeaders is nil", context) + } + for name, wantHV := range want { + gotHV, ok := got[name] + if !ok || gotHV == nil { + return fmt.Errorf("%s: missing or nil key %q", context, name) + } + gotVals := gotHV.GetValues() + wantVals := wantHV.GetValues() + if !slices.Equal(gotVals, wantVals) { + return fmt.Errorf("%s: %q = %v, want %v", context, name, gotVals, wantVals) + } + } + return nil +} diff --git a/system-tests/tests/smoke/cre/v2_http_action_test.go b/system-tests/tests/smoke/cre/v2_http_action_test.go index c5fa6a200b2..1de69a01df9 100644 --- a/system-tests/tests/smoke/cre/v2_http_action_test.go +++ b/system-tests/tests/smoke/cre/v2_http_action_test.go @@ -1,23 +1,31 @@ package cre import ( + "net/http" "testing" "time" + "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/stretchr/testify/require" commonevents "github.com/smartcontractkit/chainlink-protos/workflows/go/common" workflowevents "github.com/smartcontractkit/chainlink-protos/workflows/go/events" - "github.com/smartcontractkit/chainlink-testing-framework/framework" "github.com/smartcontractkit/chainlink-testing-framework/framework/components/fake" - httpactionconfig "github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/httpaction/config" t_helpers "github.com/smartcontractkit/chainlink/system-tests/tests/test-helpers" ttypes "github.com/smartcontractkit/chainlink/system-tests/tests/test-helpers/configuration" ) +// HTTP Action multi-headers test: workflow asserts response MultiHeaders contain multiple Set-Cookie values. +const ( + multiHeadersTestCase = "multi-headers" + multiHeadersSuccessMsg = "HTTP Action multi-headers test completed" + multiHeadersRegressionTestCase = "mh-regression-both" // short to stay under workflow name length limit (64) + multiHeadersRegressionSuccessMsg = "HTTP Action multi-headers regression completed" +) + // HTTP Action test cases for successful CRUD operations type httpActionSuccessTest struct { name string @@ -66,6 +74,67 @@ var httpActionSuccessTests = []httpActionSuccessTest{ endpoint: "/api/resources/test-resource-3", url: "", }, + { + name: "multi-headers response", + testCase: "multi-headers", + method: "GET", + body: ``, + statusCode: 200, + endpoint: "/api/multi-headers", + url: "", + }, +} + +// ExecuteHTTPActionRegressionTest runs HTTP Action regression tests (e.g. both Headers and MultiHeaders set rejected). +func ExecuteHTTPActionRegressionTest(t *testing.T, testEnv *ttypes.TestEnvironment) { + testLogger := framework.L + + fakeHTTP, err := fake.NewFakeDataProvider(testEnv.Config.FakeHTTP) + require.NoError(t, err, "Failed to start fake HTTP") + testLogger.Info().Msg("Fake HTTP started for regression test") + defer func() { + testLogger.Info().Msgf("Cleaning up fake server on port %d", testEnv.Config.FakeHTTP.Port) + }() + + response := map[string]any{"status": "success"} + err = fake.JSON("GET", "/api/resources/", response, 200) + require.NoError(t, err, "failed to set up regression endpoint") + + testLogger.Info().Msgf("Test HTTP server started on port %d at: %s (%s)", testEnv.Config.FakeHTTP.Port, fakeHTTP.BaseURLHost, fakeHTTP.BaseURLDocker) + + t.Run("[v2] HTTP Action multi-headers regression: both Headers and MultiHeaders set rejected", func(t *testing.T) { + HTTPActionRegressionTest(t, testEnv, fakeHTTP.BaseURLDocker+"/api/resources/") + }) +} + +// HTTPActionRegressionTest runs a single HTTP Action regression test (deploy workflow, expect success message). +func HTTPActionRegressionTest(t *testing.T, testEnv *ttypes.TestEnvironment, url string) { + testLogger := framework.L + const workflowFileLocation = "./httpaction/main.go" + + workflowConfig := httpactionconfig.Config{ + URL: url, + TestCase: multiHeadersRegressionTestCase, + Method: "GET", + Body: ``, + } + + testID := uuid.New().String()[0:8] + workflowName := "http-action-regression-workflow-" + multiHeadersRegressionTestCase + "-" + testID + _ = t_helpers.CompileAndDeployWorkflow(t, testEnv, testLogger, workflowName, &workflowConfig, workflowFileLocation) + + userLogsCh := make(chan *workflowevents.UserLogs, 1000) + baseMessageCh := make(chan *commonevents.BaseMessage, 1000) + server := t_helpers.StartChipTestSink(t, t_helpers.GetPublishFn(testLogger, userLogsCh, baseMessageCh)) + t.Cleanup(func() { + server.Shutdown(t.Context()) + close(userLogsCh) + close(baseMessageCh) + }) + + testLogger.Info().Msg("Waiting for HTTP Action regression workflow to complete...") + t_helpers.WatchWorkflowLogs(t, testLogger, userLogsCh, baseMessageCh, t_helpers.WorkflowEngineInitErrorLog, multiHeadersRegressionSuccessMsg, 4*time.Minute) + testLogger.Info().Msg("HTTP Action regression test completed") } // ExecuteHTTPActionCRUDSuccessTest executes HTTP Action CRUD operations success test @@ -85,6 +154,22 @@ func ExecuteHTTPActionCRUDSuccessTest(t *testing.T, testEnv *ttypes.TestEnvironm } for _, testCase := range httpActionSuccessTests { + if testCase.testCase == multiHeadersTestCase { + err = fake.Func("GET", testCase.endpoint, func(c *gin.Context) { + for name, values := range c.Request.Header { + for _, value := range values { + c.Writer.Header().Add(name, value) + } + } + c.Writer.Header().Add("Set-Cookie", "sessionid=multi-e2e-1; Path=/") + c.Writer.Header().Add("Set-Cookie", "csrf=multi-e2e-2; Path=/") + c.Writer.Header().Add("Set-Cookie", "pref=multi-e2e-3; Path=/") + c.JSON(http.StatusOK, response) + }) + require.NoError(t, err, "failed to set up %s endpoint for %s", testCase.endpoint, testCase.method) + continue + } + err = fake.JSON(testCase.method, testCase.endpoint, response, testCase.statusCode) require.NoError(t, err, "failed to set up %s endpoint for %s", testCase.endpoint, testCase.method) } @@ -139,8 +224,13 @@ func HTTPActionSuccessTest(t *testing.T, testEnv *ttypes.TestEnvironment, httpAc // Wait for workflow execution to complete and verify success testLogger.Info().Msg("Waiting for HTTP Action CRUD operations to complete...") - // Expect exact success message for this test case - expectedMessage := "HTTP Action CRUD success test completed: " + httpActionTest.testCase + var expectedMessage string + switch httpActionTest.testCase { + case multiHeadersTestCase: + expectedMessage = multiHeadersSuccessMsg + default: + expectedMessage = "HTTP Action CRUD success test completed: " + httpActionTest.testCase + } t_helpers.WatchWorkflowLogs(t, testLogger, userLogsCh, baseMessageCh, t_helpers.WorkflowEngineInitErrorLog, expectedMessage, 4*time.Minute)