package beacon_api import ( "bytes" "context" "encoding/json" "errors" "net/url" "testing" "github.com/golang/mock/gomock" "github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/apimiddleware" "github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/eth/helpers" "github.com/prysmaticlabs/prysm/v4/consensus-types/primitives" "github.com/prysmaticlabs/prysm/v4/testing/assert" "github.com/prysmaticlabs/prysm/v4/testing/require" "github.com/prysmaticlabs/prysm/v4/validator/client/beacon-api/mock" ) func TestBeaconApiHelpers(t *testing.T) { tests := []struct { name string input string valid bool }{ { name: "correct format", input: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", valid: true, }, { name: "root too small", input: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f", valid: false, }, { name: "root too big", input: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f22", valid: false, }, { name: "empty root", input: "", valid: false, }, { name: "no 0x prefix", input: "cf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", valid: false, }, { name: "invalid characters", input: "0xzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", valid: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.valid, validRoot(tt.input)) }) } } func TestBeaconApiHelpers_TestUint64ToString(t *testing.T) { const expectedResult = "1234" const val = uint64(1234) assert.Equal(t, expectedResult, uint64ToString(val)) assert.Equal(t, expectedResult, uint64ToString(primitives.Slot(val))) assert.Equal(t, expectedResult, uint64ToString(primitives.ValidatorIndex(val))) assert.Equal(t, expectedResult, uint64ToString(primitives.CommitteeIndex(val))) assert.Equal(t, expectedResult, uint64ToString(primitives.Epoch(val))) } func TestBuildURL_NoParams(t *testing.T) { wanted := "/aaa/bbb/ccc" actual := buildURL("/aaa/bbb/ccc") assert.Equal(t, wanted, actual) } func TestBuildURL_WithParams(t *testing.T) { params := url.Values{} params.Add("xxxx", "1") params.Add("yyyy", "2") params.Add("zzzz", "3") wanted := "/aaa/bbb/ccc?xxxx=1&yyyy=2&zzzz=3" actual := buildURL("/aaa/bbb/ccc", params) assert.Equal(t, wanted, actual) } const forkEndpoint = "/eth/v1/beacon/states/head/fork" func TestGetFork_Nominal(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() stateForkResponseJson := apimiddleware.StateForkResponseJson{} jsonRestHandler := mock.NewMockjsonRestHandler(ctrl) expected := apimiddleware.StateForkResponseJson{ Data: &apimiddleware.ForkJson{ PreviousVersion: "0x1", CurrentVersion: "0x2", Epoch: "3", }, } ctx := context.Background() jsonRestHandler.EXPECT().GetRestJsonResponse( ctx, forkEndpoint, &stateForkResponseJson, ).Return( nil, nil, ).SetArg( 2, expected, ).Times(1) validatorClient := beaconApiValidatorClient{ jsonRestHandler: jsonRestHandler, } fork, err := validatorClient.getFork(ctx) require.NoError(t, err) assert.DeepEqual(t, &expected, fork) } func TestGetFork_Invalid(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() jsonRestHandler := mock.NewMockjsonRestHandler(ctrl) ctx := context.Background() jsonRestHandler.EXPECT().GetRestJsonResponse( ctx, forkEndpoint, gomock.Any(), ).Return( nil, errors.New("custom error"), ).Times(1) validatorClient := beaconApiValidatorClient{ jsonRestHandler: jsonRestHandler, } _, err := validatorClient.getFork(ctx) require.ErrorContains(t, "failed to get json response from `/eth/v1/beacon/states/head/fork` REST endpoint", err) } const headersEndpoint = "/eth/v1/beacon/headers" func TestGetHeaders_Nominal(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() blockHeadersResponseJson := apimiddleware.BlockHeadersResponseJson{} jsonRestHandler := mock.NewMockjsonRestHandler(ctrl) expected := apimiddleware.BlockHeadersResponseJson{ Data: []*apimiddleware.BlockHeaderContainerJson{ { Header: &apimiddleware.BeaconBlockHeaderContainerJson{ Message: &apimiddleware.BeaconBlockHeaderJson{ Slot: "42", }, }, }, }, } ctx := context.Background() jsonRestHandler.EXPECT().GetRestJsonResponse( ctx, headersEndpoint, &blockHeadersResponseJson, ).Return( nil, nil, ).SetArg( 2, expected, ).Times(1) validatorClient := beaconApiValidatorClient{ jsonRestHandler: jsonRestHandler, } headers, err := validatorClient.getHeaders(ctx) require.NoError(t, err) assert.DeepEqual(t, &expected, headers) } func TestGetHeaders_Invalid(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() jsonRestHandler := mock.NewMockjsonRestHandler(ctrl) ctx := context.Background() jsonRestHandler.EXPECT().GetRestJsonResponse( ctx, headersEndpoint, gomock.Any(), ).Return( nil, errors.New("custom error"), ).Times(1) validatorClient := beaconApiValidatorClient{ jsonRestHandler: jsonRestHandler, } _, err := validatorClient.getHeaders(ctx) require.ErrorContains(t, "failed to get json response from `/eth/v1/beacon/headers` REST endpoint", err) } const livenessEndpoint = "/eth/v1/validator/liveness/42" func TestGetLiveness_Nominal(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() livenessResponseJson := apimiddleware.LivenessResponseJson{} indexes := []string{"1", "2"} marshalledIndexes, err := json.Marshal(indexes) require.NoError(t, err) expected := apimiddleware.LivenessResponseJson{ Data: []*struct { Index string `json:"index"` IsLive bool `json:"is_live"` }{ { Index: "1", IsLive: true, }, { Index: "2", IsLive: false, }, }, } ctx := context.Background() jsonRestHandler := mock.NewMockjsonRestHandler(ctrl) jsonRestHandler.EXPECT().PostRestJson( ctx, livenessEndpoint, nil, bytes.NewBuffer(marshalledIndexes), &livenessResponseJson, ).SetArg( 4, expected, ).Return( nil, nil, ).Times(1) validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} liveness, err := validatorClient.getLiveness(ctx, 42, indexes) require.NoError(t, err) assert.DeepEqual(t, &expected, liveness) } func TestGetLiveness_Invalid(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() ctx := context.Background() jsonRestHandler := mock.NewMockjsonRestHandler(ctrl) jsonRestHandler.EXPECT().PostRestJson( ctx, livenessEndpoint, nil, gomock.Any(), gomock.Any(), ).Return( nil, errors.New("custom error"), ).Times(1) validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} _, err := validatorClient.getLiveness(ctx, 42, nil) require.ErrorContains(t, "failed to send POST data to `/eth/v1/validator/liveness/42` REST URL", err) } const syncingEnpoint = "/eth/v1/node/syncing" func TestGetIsSyncing_Nominal(t *testing.T) { testCases := []struct { name string isSyncing bool }{ { name: "Syncing", isSyncing: true, }, { name: "Not syncing", isSyncing: false, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() syncingResponseJson := apimiddleware.SyncingResponseJson{} jsonRestHandler := mock.NewMockjsonRestHandler(ctrl) expected := apimiddleware.SyncingResponseJson{ Data: &helpers.SyncDetailsJson{ IsSyncing: testCase.isSyncing, }, } ctx := context.Background() jsonRestHandler.EXPECT().GetRestJsonResponse( ctx, syncingEnpoint, &syncingResponseJson, ).Return( nil, nil, ).SetArg( 2, expected, ).Times(1) validatorClient := beaconApiValidatorClient{ jsonRestHandler: jsonRestHandler, } isSyncing, err := validatorClient.isSyncing(ctx) require.NoError(t, err) assert.Equal(t, testCase.isSyncing, isSyncing) }) } } func TestGetIsSyncing_Invalid(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() syncingResponseJson := apimiddleware.SyncingResponseJson{} jsonRestHandler := mock.NewMockjsonRestHandler(ctrl) ctx := context.Background() jsonRestHandler.EXPECT().GetRestJsonResponse( ctx, syncingEnpoint, &syncingResponseJson, ).Return( nil, errors.New("custom error"), ).Times(1) validatorClient := beaconApiValidatorClient{ jsonRestHandler: jsonRestHandler, } isSyncing, err := validatorClient.isSyncing(ctx) assert.Equal(t, true, isSyncing) assert.ErrorContains(t, "failed to get syncing status", err) }