mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-09 21:38:05 -05:00
<!-- Thanks for sending a PR! Before submitting: 1. If this is your first PR, check out our contribution guide here https://docs.prylabs.network/docs/contribute/contribution-guidelines You will then need to sign our Contributor License Agreement (CLA), which will show up as a comment from a bot in this pull request after you open it. We cannot review code without a signed CLA. 2. Please file an associated tracking issue if this pull request is non-trivial and requires context for our team to understand. All features and most bug fixes should have an associated issue with a design discussed and decided upon. Small bug fixes and documentation improvements don't need issues. 3. New features and bug fixes must have tests. Documentation may need to be updated. If you're unsure what to update, send the PR, and we'll discuss in review. 4. Note that PRs updating dependencies and new Go versions are not accepted. Please file an issue instead. 5. A changelog entry is required for user facing issues. --> **What type of PR is this?** Bug fix **What does this PR do? Why is it needed?** validator fallbacks shouldn't work on nodes that are syncing as many of the tasks validators perform require the node to be fully synced. - 206 or any other code is interpreted as "not ready" - 200 interpreted as "ready" **Which issues(s) does this PR fix?** continuation of https://github.com/OffchainLabs/prysm/pull/15401 **Other notes for review** **Acknowledgements** - [x] I have read [CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md). - [x] I have included a uniquely named [changelog fragment file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd). - [x] I have added a description with sufficient context for reviewers to understand this PR. - [x] I have tested that my changes work as expected and I added a testing plan to the PR description (if applicable).
425 lines
13 KiB
Go
425 lines
13 KiB
Go
package beacon_api
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/OffchainLabs/prysm/v7/api"
|
|
"github.com/OffchainLabs/prysm/v7/api/server/structs"
|
|
"github.com/OffchainLabs/prysm/v7/config/params"
|
|
"github.com/OffchainLabs/prysm/v7/network/httputil"
|
|
"github.com/OffchainLabs/prysm/v7/runtime/version"
|
|
"github.com/OffchainLabs/prysm/v7/testing/assert"
|
|
"github.com/OffchainLabs/prysm/v7/testing/require"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/sirupsen/logrus/hooks/test"
|
|
)
|
|
|
|
func TestGet(t *testing.T) {
|
|
ctx := t.Context()
|
|
const endpoint = "/example/rest/api/endpoint"
|
|
genesisJson := &structs.GetGenesisResponse{
|
|
Data: &structs.Genesis{
|
|
GenesisTime: "123",
|
|
GenesisValidatorsRoot: "0x456",
|
|
GenesisForkVersion: "0x789",
|
|
},
|
|
}
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) {
|
|
marshalledJson, err := json.Marshal(genesisJson)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, version.BuildData(), r.Header.Get("User-Agent"))
|
|
w.Header().Set("Content-Type", api.JsonMediaType)
|
|
_, err = w.Write(marshalledJson)
|
|
require.NoError(t, err)
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
jsonRestHandler := BeaconApiRestHandler{
|
|
client: http.Client{Timeout: time.Second * 5},
|
|
host: server.URL,
|
|
}
|
|
resp := &structs.GetGenesisResponse{}
|
|
require.NoError(t, jsonRestHandler.Get(ctx, endpoint+"?arg1=abc&arg2=def", resp))
|
|
assert.DeepEqual(t, genesisJson, resp)
|
|
}
|
|
|
|
func TestGetSSZ(t *testing.T) {
|
|
ctx := context.Background()
|
|
const endpoint = "/example/rest/api/ssz"
|
|
genesisJson := &structs.GetGenesisResponse{
|
|
Data: &structs.Genesis{
|
|
GenesisTime: "123",
|
|
GenesisValidatorsRoot: "0x456",
|
|
GenesisForkVersion: "0x789",
|
|
},
|
|
}
|
|
|
|
t.Run("Successful SSZ response", func(t *testing.T) {
|
|
expectedBody := []byte{10, 20, 30, 40}
|
|
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) {
|
|
assert.StringContains(t, api.OctetStreamMediaType, r.Header.Get("Accept"))
|
|
assert.Equal(t, version.BuildData(), r.Header.Get("User-Agent"))
|
|
w.Header().Set("Content-Type", api.OctetStreamMediaType)
|
|
_, err := w.Write(expectedBody)
|
|
require.NoError(t, err)
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
jsonRestHandler := BeaconApiRestHandler{
|
|
client: http.Client{Timeout: time.Second * 5},
|
|
host: server.URL,
|
|
}
|
|
|
|
body, header, err := jsonRestHandler.GetSSZ(ctx, endpoint)
|
|
require.NoError(t, err)
|
|
assert.DeepEqual(t, expectedBody, body)
|
|
require.StringContains(t, api.OctetStreamMediaType, header.Get("Content-Type"))
|
|
})
|
|
|
|
t.Run("Json Content-Type response", func(t *testing.T) {
|
|
logrus.SetLevel(logrus.DebugLevel)
|
|
defer logrus.SetLevel(logrus.InfoLevel) // reset it afterwards
|
|
logHook := test.NewGlobal()
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) {
|
|
assert.StringContains(t, api.OctetStreamMediaType, r.Header.Get("Accept"))
|
|
w.Header().Set("Content-Type", api.JsonMediaType)
|
|
|
|
marshalledJson, err := json.Marshal(genesisJson)
|
|
require.NoError(t, err)
|
|
|
|
_, err = w.Write(marshalledJson)
|
|
require.NoError(t, err)
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
jsonRestHandler := BeaconApiRestHandler{
|
|
client: http.Client{Timeout: time.Second * 5},
|
|
host: server.URL,
|
|
}
|
|
|
|
body, header, err := jsonRestHandler.GetSSZ(ctx, endpoint)
|
|
require.NoError(t, err)
|
|
assert.LogsContain(t, logHook, "Server responded with non primary accept type")
|
|
require.Equal(t, api.JsonMediaType, header.Get("Content-Type"))
|
|
resp := &structs.GetGenesisResponse{}
|
|
require.NoError(t, json.Unmarshal(body, resp))
|
|
require.Equal(t, "123", resp.Data.GenesisTime)
|
|
})
|
|
|
|
t.Run("Wrong Content-Type response, doesn't error out and instead handled downstream", func(t *testing.T) {
|
|
logrus.SetLevel(logrus.DebugLevel)
|
|
defer logrus.SetLevel(logrus.InfoLevel) // reset it afterwards
|
|
logHook := test.NewGlobal()
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) {
|
|
assert.StringContains(t, api.OctetStreamMediaType, r.Header.Get("Accept"))
|
|
w.Header().Set("Content-Type", "text/plain") // Invalid content type
|
|
_, err := w.Write([]byte("some text"))
|
|
require.NoError(t, err)
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
jsonRestHandler := BeaconApiRestHandler{
|
|
client: http.Client{Timeout: time.Second * 5},
|
|
host: server.URL,
|
|
}
|
|
|
|
_, _, err := jsonRestHandler.GetSSZ(ctx, endpoint)
|
|
require.NoError(t, err)
|
|
assert.LogsContain(t, logHook, "Server responded with non primary accept type")
|
|
})
|
|
}
|
|
|
|
func TestAcceptOverrideSSZ(t *testing.T) {
|
|
name := "TestAcceptOverride"
|
|
orig := os.Getenv(params.EnvNameOverrideAccept)
|
|
defer func() {
|
|
require.NoError(t, os.Setenv(params.EnvNameOverrideAccept, orig))
|
|
}()
|
|
require.NoError(t, os.Setenv(params.EnvNameOverrideAccept, name))
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
require.Equal(t, name, r.Header.Get("Accept"))
|
|
w.WriteHeader(200)
|
|
_, err := w.Write([]byte("ok"))
|
|
require.NoError(t, err)
|
|
}))
|
|
defer srv.Close()
|
|
c := NewBeaconApiRestHandler(http.Client{Timeout: time.Second * 5}, srv.URL)
|
|
_, _, err := c.GetSSZ(t.Context(), "/test")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestPost(t *testing.T) {
|
|
ctx := t.Context()
|
|
const endpoint = "/example/rest/api/endpoint"
|
|
dataBytes := []byte{1, 2, 3, 4, 5}
|
|
headers := map[string]string{"foo": "bar"}
|
|
|
|
genesisJson := &structs.GetGenesisResponse{
|
|
Data: &structs.Genesis{
|
|
GenesisTime: "123",
|
|
GenesisValidatorsRoot: "0x456",
|
|
GenesisForkVersion: "0x789",
|
|
},
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) {
|
|
// Make sure the request headers have been set
|
|
assert.Equal(t, "bar", r.Header.Get("foo"))
|
|
assert.Equal(t, version.BuildData(), r.Header.Get("User-Agent"))
|
|
assert.Equal(t, api.JsonMediaType, r.Header.Get("Content-Type"))
|
|
|
|
// Make sure the data matches
|
|
receivedBytes := make([]byte, len(dataBytes))
|
|
numBytes, err := r.Body.Read(receivedBytes)
|
|
assert.Equal(t, io.EOF, err)
|
|
assert.Equal(t, len(dataBytes), numBytes)
|
|
assert.DeepEqual(t, dataBytes, receivedBytes)
|
|
|
|
marshalledJson, err := json.Marshal(genesisJson)
|
|
require.NoError(t, err)
|
|
|
|
w.Header().Set("Content-Type", api.JsonMediaType)
|
|
_, err = w.Write(marshalledJson)
|
|
require.NoError(t, err)
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
jsonRestHandler := BeaconApiRestHandler{
|
|
client: http.Client{Timeout: time.Second * 5},
|
|
host: server.URL,
|
|
}
|
|
resp := &structs.GetGenesisResponse{}
|
|
require.NoError(t, jsonRestHandler.Post(ctx, endpoint, headers, bytes.NewBuffer(dataBytes), resp))
|
|
assert.DeepEqual(t, genesisJson, resp)
|
|
}
|
|
|
|
func Test_decodeResp(t *testing.T) {
|
|
type j struct {
|
|
Foo string `json:"foo"`
|
|
}
|
|
t.Run("200 JSON with charset", func(t *testing.T) {
|
|
body := bytes.Buffer{}
|
|
r := &http.Response{
|
|
Status: "200",
|
|
StatusCode: http.StatusOK,
|
|
Body: io.NopCloser(&body),
|
|
Header: map[string][]string{"Content-Type": {"application/json; charset=utf-8"}},
|
|
}
|
|
require.NoError(t, decodeResp(r, nil))
|
|
})
|
|
t.Run("200 non-JSON", func(t *testing.T) {
|
|
body := bytes.Buffer{}
|
|
r := &http.Response{
|
|
Status: "200",
|
|
StatusCode: http.StatusOK,
|
|
Body: io.NopCloser(&body),
|
|
Header: map[string][]string{"Content-Type": {api.OctetStreamMediaType}},
|
|
}
|
|
require.NoError(t, decodeResp(r, nil))
|
|
})
|
|
t.Run("204 non-JSON", func(t *testing.T) {
|
|
body := bytes.Buffer{}
|
|
r := &http.Response{
|
|
Status: "204",
|
|
StatusCode: http.StatusNoContent,
|
|
Body: io.NopCloser(&body),
|
|
Header: map[string][]string{"Content-Type": {api.OctetStreamMediaType}},
|
|
}
|
|
require.NoError(t, decodeResp(r, nil))
|
|
})
|
|
t.Run("500 non-JSON", func(t *testing.T) {
|
|
body := bytes.Buffer{}
|
|
_, err := body.WriteString("foo")
|
|
require.NoError(t, err)
|
|
r := &http.Response{
|
|
Status: "500",
|
|
StatusCode: http.StatusInternalServerError,
|
|
Body: io.NopCloser(&body),
|
|
Header: map[string][]string{"Content-Type": {api.OctetStreamMediaType}},
|
|
}
|
|
err = decodeResp(r, nil)
|
|
errJson := &httputil.DefaultJsonError{}
|
|
require.Equal(t, true, errors.As(err, &errJson))
|
|
assert.Equal(t, http.StatusInternalServerError, errJson.Code)
|
|
assert.Equal(t, "foo", errJson.Message)
|
|
})
|
|
t.Run("200 JSON with resp", func(t *testing.T) {
|
|
body := bytes.Buffer{}
|
|
b, err := json.Marshal(&j{Foo: "foo"})
|
|
require.NoError(t, err)
|
|
body.Write(b)
|
|
r := &http.Response{
|
|
Status: "200",
|
|
StatusCode: http.StatusOK,
|
|
Body: io.NopCloser(&body),
|
|
Header: map[string][]string{"Content-Type": {api.JsonMediaType}},
|
|
}
|
|
resp := &j{}
|
|
require.NoError(t, decodeResp(r, resp))
|
|
assert.Equal(t, "foo", resp.Foo)
|
|
})
|
|
t.Run("200 JSON without resp", func(t *testing.T) {
|
|
body := bytes.Buffer{}
|
|
r := &http.Response{
|
|
Status: "200",
|
|
StatusCode: http.StatusOK,
|
|
Body: io.NopCloser(&body),
|
|
Header: map[string][]string{"Content-Type": {api.JsonMediaType}},
|
|
}
|
|
require.NoError(t, decodeResp(r, nil))
|
|
})
|
|
t.Run("204 JSON", func(t *testing.T) {
|
|
body := bytes.Buffer{}
|
|
r := &http.Response{
|
|
Status: "204",
|
|
StatusCode: http.StatusNoContent,
|
|
Body: io.NopCloser(&body),
|
|
Header: map[string][]string{"Content-Type": {api.JsonMediaType}},
|
|
}
|
|
require.NoError(t, decodeResp(r, nil))
|
|
})
|
|
t.Run("500 JSON", func(t *testing.T) {
|
|
body := bytes.Buffer{}
|
|
b, err := json.Marshal(&httputil.DefaultJsonError{Code: http.StatusInternalServerError, Message: "error"})
|
|
require.NoError(t, err)
|
|
body.Write(b)
|
|
r := &http.Response{
|
|
Status: "500",
|
|
StatusCode: http.StatusInternalServerError,
|
|
Body: io.NopCloser(&body),
|
|
Header: map[string][]string{"Content-Type": {api.JsonMediaType}},
|
|
}
|
|
err = decodeResp(r, nil)
|
|
errJson := &httputil.DefaultJsonError{}
|
|
require.Equal(t, true, errors.As(err, &errJson))
|
|
assert.Equal(t, http.StatusInternalServerError, errJson.Code)
|
|
assert.Equal(t, "error", errJson.Message)
|
|
})
|
|
t.Run("200 JSON cannot decode", func(t *testing.T) {
|
|
body := bytes.Buffer{}
|
|
_, err := body.WriteString("foo")
|
|
require.NoError(t, err)
|
|
r := &http.Response{
|
|
Status: "200",
|
|
StatusCode: http.StatusOK,
|
|
Body: io.NopCloser(&body),
|
|
Header: map[string][]string{"Content-Type": {api.JsonMediaType}},
|
|
Request: &http.Request{},
|
|
}
|
|
resp := &j{}
|
|
err = decodeResp(r, resp)
|
|
assert.ErrorContains(t, "failed to decode response body into json", err)
|
|
})
|
|
t.Run("500 JSON cannot decode", func(t *testing.T) {
|
|
body := bytes.Buffer{}
|
|
_, err := body.WriteString("foo")
|
|
require.NoError(t, err)
|
|
r := &http.Response{
|
|
Status: "500",
|
|
StatusCode: http.StatusInternalServerError,
|
|
Body: io.NopCloser(&body),
|
|
Header: map[string][]string{"Content-Type": {api.JsonMediaType}},
|
|
Request: &http.Request{},
|
|
}
|
|
err = decodeResp(r, nil)
|
|
assert.ErrorContains(t, "failed to decode response body into error json", err)
|
|
})
|
|
t.Run("500 not JSON", func(t *testing.T) {
|
|
body := bytes.Buffer{}
|
|
_, err := body.WriteString("foo")
|
|
require.NoError(t, err)
|
|
r := &http.Response{
|
|
Status: "500",
|
|
StatusCode: http.StatusInternalServerError,
|
|
Body: io.NopCloser(&body),
|
|
Header: map[string][]string{"Content-Type": {"text/plain"}},
|
|
Request: &http.Request{},
|
|
}
|
|
err = decodeResp(r, nil)
|
|
assert.ErrorContains(t, "HTTP request unsuccessful (500: foo)", err)
|
|
})
|
|
}
|
|
|
|
func TestGetStatusCode(t *testing.T) {
|
|
ctx := t.Context()
|
|
const endpoint = "/eth/v1/node/health"
|
|
|
|
testCases := []struct {
|
|
name string
|
|
serverStatusCode int
|
|
expectedStatusCode int
|
|
}{
|
|
{
|
|
name: "returns 200 OK",
|
|
serverStatusCode: http.StatusOK,
|
|
expectedStatusCode: http.StatusOK,
|
|
},
|
|
{
|
|
name: "returns 206 Partial Content",
|
|
serverStatusCode: http.StatusPartialContent,
|
|
expectedStatusCode: http.StatusPartialContent,
|
|
},
|
|
{
|
|
name: "returns 503 Service Unavailable",
|
|
serverStatusCode: http.StatusServiceUnavailable,
|
|
expectedStatusCode: http.StatusServiceUnavailable,
|
|
},
|
|
{
|
|
name: "returns 500 Internal Server Error",
|
|
serverStatusCode: http.StatusInternalServerError,
|
|
expectedStatusCode: http.StatusInternalServerError,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) {
|
|
assert.Equal(t, version.BuildData(), r.Header.Get("User-Agent"))
|
|
w.WriteHeader(tc.serverStatusCode)
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
jsonRestHandler := BeaconApiRestHandler{
|
|
client: http.Client{Timeout: time.Second * 5},
|
|
host: server.URL,
|
|
}
|
|
|
|
statusCode, err := jsonRestHandler.GetStatusCode(ctx, endpoint)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tc.expectedStatusCode, statusCode)
|
|
})
|
|
}
|
|
|
|
t.Run("returns error on connection failure", func(t *testing.T) {
|
|
jsonRestHandler := BeaconApiRestHandler{
|
|
client: http.Client{Timeout: time.Millisecond * 100},
|
|
host: "http://localhost:99999", // Invalid port
|
|
}
|
|
|
|
_, err := jsonRestHandler.GetStatusCode(ctx, endpoint)
|
|
require.ErrorContains(t, "failed to perform request", err)
|
|
})
|
|
}
|