mirror of
https://gitlab.com/pulsechaincom/erigon-pulse.git
synced 2024-12-22 19:50:36 +00:00
437 lines
9.7 KiB
Go
437 lines
9.7 KiB
Go
package beacontest
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/md5"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"text/template"
|
|
|
|
"github.com/Masterminds/sprig/v3"
|
|
|
|
"github.com/google/cel-go/cel"
|
|
"github.com/google/cel-go/common/types"
|
|
"github.com/spf13/afero"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"sigs.k8s.io/yaml"
|
|
)
|
|
|
|
type HarnessOption func(*Harness) error
|
|
|
|
func WithTesting(t *testing.T) func(*Harness) error {
|
|
return func(h *Harness) error {
|
|
h.t = t
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func WithTests(name string, xs []Test) func(*Harness) error {
|
|
return func(h *Harness) error {
|
|
h.tests[name] = xs
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func WithHandler(name string, handler http.Handler) func(*Harness) error {
|
|
return func(h *Harness) error {
|
|
h.handlers[name] = handler
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func WithFilesystem(name string, handler afero.Fs) func(*Harness) error {
|
|
return func(h *Harness) error {
|
|
h.fss[name] = handler
|
|
return nil
|
|
}
|
|
}
|
|
func WithTestFromFs(fs afero.Fs, name string) func(*Harness) error {
|
|
return func(h *Harness) error {
|
|
filename := name
|
|
for _, fn := range []string{name, name + ".yaml", name + ".yml", name + ".json"} {
|
|
// check if file exists
|
|
_, err := fs.Stat(fn)
|
|
if err == nil {
|
|
filename = fn
|
|
break
|
|
}
|
|
}
|
|
xs, err := afero.ReadFile(fs, filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return WithTestFromBytes(name, xs)(h)
|
|
}
|
|
}
|
|
|
|
type Extra struct {
|
|
Vars map[string]any `json:"vars"`
|
|
|
|
RawBodyZZZZ json.RawMessage `json:"tests"`
|
|
}
|
|
|
|
func WithTestFromBytes(name string, xs []byte) func(*Harness) error {
|
|
return func(h *Harness) error {
|
|
var t struct {
|
|
T []Test `json:"tests"`
|
|
}
|
|
x := &Extra{}
|
|
s := md5.New()
|
|
s.Write(xs)
|
|
hsh := hex.EncodeToString(s.Sum(nil))
|
|
// unmarshal just the extra data
|
|
err := yaml.Unmarshal(xs, &x, yaml.JSONOpt(func(d *json.Decoder) *json.Decoder {
|
|
return d
|
|
}))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tmpl := template.Must(template.New(hsh).Funcs(sprig.FuncMap()).Parse(string(xs)))
|
|
// execute the template using the extra data as the provided top level object
|
|
// we can use the original buffer as the output since the original buffer has already been copied when it was passed into template
|
|
buf := bytes.NewBuffer(xs)
|
|
buf.Reset()
|
|
err = tmpl.Execute(buf, x)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = yaml.Unmarshal(buf.Bytes(), &t)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(t.T) == 0 {
|
|
return fmt.Errorf("suite with name %s had no tests", name)
|
|
}
|
|
h.tests[name] = t.T
|
|
return nil
|
|
}
|
|
}
|
|
|
|
type Harness struct {
|
|
tests map[string][]Test
|
|
t *testing.T
|
|
|
|
handlers map[string]http.Handler
|
|
fss map[string]afero.Fs
|
|
}
|
|
|
|
func Execute(options ...HarnessOption) {
|
|
h := &Harness{
|
|
handlers: map[string]http.Handler{},
|
|
tests: map[string][]Test{},
|
|
fss: map[string]afero.Fs{
|
|
"": afero.NewOsFs(),
|
|
},
|
|
}
|
|
for _, v := range options {
|
|
err := v(h)
|
|
if err != nil {
|
|
h.t.Error(err)
|
|
}
|
|
}
|
|
h.Execute()
|
|
}
|
|
|
|
func (h *Harness) Execute() {
|
|
ctx := context.Background()
|
|
for suiteName, tests := range h.tests {
|
|
for idx, v := range tests {
|
|
v.Actual.h = h
|
|
v.Expect.h = h
|
|
name := v.Name
|
|
if name == "" {
|
|
name = "test"
|
|
}
|
|
fullname := fmt.Sprintf("%s_%s_%d", suiteName, name, idx)
|
|
h.t.Run(fullname, func(t *testing.T) {
|
|
err := v.Execute(ctx, t)
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
type Test struct {
|
|
Name string `json:"name"`
|
|
Expect Source `json:"expect"`
|
|
Actual Source `json:"actual"`
|
|
Compare Comparison `json:"compare"`
|
|
}
|
|
|
|
func (c *Test) Execute(ctx context.Context, t *testing.T) error {
|
|
a, aCode, err := c.Expect.Execute(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("get expect data: %w", err)
|
|
}
|
|
b, bCode, err := c.Actual.Execute(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("get actual data: %w", err)
|
|
}
|
|
err = c.Compare.Compare(t, a, b, aCode, bCode)
|
|
if err != nil {
|
|
return fmt.Errorf("compare: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type Comparison struct {
|
|
Expr string `json:"expr"`
|
|
Exprs []string `json:"exprs"`
|
|
Literal bool `json:"literal"`
|
|
}
|
|
|
|
func (c *Comparison) Compare(t *testing.T, aRaw, bRaw json.RawMessage, aCode, bCode int) error {
|
|
var err error
|
|
var a, b any
|
|
var aType, bType *types.Type
|
|
|
|
if !c.Literal {
|
|
var aMap, bMap any
|
|
err = yaml.Unmarshal(aRaw, &aMap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = yaml.Unmarshal(bRaw, &bMap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a = aMap
|
|
b = bMap
|
|
if a != nil {
|
|
switch reflect.TypeOf(a).Kind() {
|
|
case reflect.Slice:
|
|
aType = cel.ListType(cel.MapType(cel.StringType, cel.DynType))
|
|
default:
|
|
aType = cel.MapType(cel.StringType, cel.DynType)
|
|
}
|
|
} else {
|
|
aType = cel.MapType(cel.StringType, cel.DynType)
|
|
}
|
|
if b != nil {
|
|
switch reflect.TypeOf(b).Kind() {
|
|
case reflect.Slice:
|
|
bType = cel.ListType(cel.MapType(cel.StringType, cel.DynType))
|
|
default:
|
|
bType = cel.MapType(cel.StringType, cel.DynType)
|
|
}
|
|
} else {
|
|
bType = cel.MapType(cel.StringType, cel.DynType)
|
|
}
|
|
} else {
|
|
a = string(aRaw)
|
|
b = string(bRaw)
|
|
aType = cel.StringType
|
|
bType = cel.StringType
|
|
}
|
|
|
|
exprs := []string{}
|
|
// if no default expr set and no exprs are set, then add the default expr
|
|
if len(c.Exprs) == 0 && c.Expr == "" {
|
|
exprs = append(exprs, "actual_code == 200", "actual == expect")
|
|
}
|
|
env, err := cel.NewEnv(
|
|
cel.Variable("expect", aType),
|
|
cel.Variable("actual", bType),
|
|
cel.Variable("expect_code", cel.IntType),
|
|
cel.Variable("actual_code", cel.IntType),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, expr := range append(c.Exprs, exprs...) {
|
|
ast, issues := env.Compile(expr)
|
|
if issues != nil && issues.Err() != nil {
|
|
return issues.Err()
|
|
}
|
|
prg, err := env.Program(ast)
|
|
if err != nil {
|
|
return fmt.Errorf("program construction error: %w", err)
|
|
}
|
|
res, _, err := prg.Eval(map[string]any{
|
|
"expect": a,
|
|
"actual": b,
|
|
"expect_code": aCode,
|
|
"actual_code": bCode,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if res.Type() != cel.BoolType {
|
|
return ErrExpressionMustReturnBool
|
|
}
|
|
bres, ok := res.Value().(bool)
|
|
if !ok {
|
|
return ErrExpressionMustReturnBool
|
|
}
|
|
if !assert.Equal(t, bres, true, `expr: %s`, expr) {
|
|
if os.Getenv("HIDE_HARNESS_LOG") != "1" {
|
|
t.Logf(`name: %s
|
|
expect%d: %v
|
|
actual%d: %v
|
|
expr: %s
|
|
`, t.Name(), aCode, a, bCode, b, expr)
|
|
|
|
}
|
|
t.FailNow()
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type Source struct {
|
|
// backref to the harness
|
|
h *Harness `json:"-"`
|
|
|
|
// remote type
|
|
Remote *string `json:"remote,omitempty"`
|
|
Handler *string `json:"handler,omitempty"`
|
|
Method string `json:"method"`
|
|
Path string `json:"path"`
|
|
Query map[string]string `json:"query"`
|
|
Headers map[string]string `json:"headers"`
|
|
Body *Source `json:"body,omitempty"`
|
|
|
|
// data type
|
|
Data any `json:"data,omitempty"`
|
|
|
|
// file type
|
|
File *string `json:"file,omitempty"`
|
|
Fs string `json:"fs,omitempty"`
|
|
|
|
// for raw type
|
|
Raw *string `json:"raw,omitempty"`
|
|
}
|
|
|
|
func (s *Source) Execute(ctx context.Context) (json.RawMessage, int, error) {
|
|
if s.Raw != nil {
|
|
return s.executeRaw(ctx)
|
|
}
|
|
if s.File != nil {
|
|
return s.executeFile(ctx)
|
|
}
|
|
if s.Remote != nil || s.Handler != nil {
|
|
return s.executeRemote(ctx)
|
|
}
|
|
if s.Data != nil {
|
|
return s.executeData(ctx)
|
|
}
|
|
return s.executeEmpty(ctx)
|
|
}
|
|
func (s *Source) executeRemote(ctx context.Context) (json.RawMessage, int, error) {
|
|
method := "GET"
|
|
if s.Method != "" {
|
|
method = s.Method
|
|
}
|
|
method = strings.ToUpper(method)
|
|
var body io.Reader
|
|
// hydrate the harness
|
|
if s.Body != nil {
|
|
s.Body.h = s.h
|
|
msg, _, err := s.Body.Execute(ctx)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("getting body: %w", err)
|
|
}
|
|
body = bytes.NewBuffer(msg)
|
|
}
|
|
var purl *url.URL
|
|
if s.Remote != nil {
|
|
niceUrl, err := url.Parse(*s.Remote)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
purl = niceUrl
|
|
|
|
} else if s.Handler != nil {
|
|
handler, ok := s.h.handlers[*s.Handler]
|
|
if !ok {
|
|
return nil, 0, fmt.Errorf("handler not registered: %s", *s.Handler)
|
|
}
|
|
server := httptest.NewServer(handler)
|
|
defer server.Close()
|
|
niceUrl, err := url.Parse(server.URL)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
purl = niceUrl
|
|
} else {
|
|
panic("impossible code path. bug? source.Execute() should ensure this never happens")
|
|
}
|
|
|
|
purl = purl.JoinPath(s.Path)
|
|
q := purl.Query()
|
|
for k, v := range s.Query {
|
|
q.Add(k, v)
|
|
}
|
|
purl.RawQuery = q.Encode()
|
|
request, err := http.NewRequest(method, purl.String(), body)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
for k, v := range s.Headers {
|
|
request.Header.Set(k, v)
|
|
}
|
|
resp, err := http.DefaultClient.Do(request)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, resp.StatusCode, nil
|
|
}
|
|
out, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, 200, err
|
|
}
|
|
return json.RawMessage(out), 200, nil
|
|
}
|
|
|
|
func (s *Source) executeData(ctx context.Context) (json.RawMessage, int, error) {
|
|
ans, err := json.Marshal(s.Data)
|
|
if err != nil {
|
|
return nil, 400, nil
|
|
}
|
|
return ans, 200, nil
|
|
}
|
|
|
|
func (s *Source) executeFile(ctx context.Context) (json.RawMessage, int, error) {
|
|
afs, ok := s.h.fss[s.Fs]
|
|
if !ok {
|
|
return nil, 404, fmt.Errorf("filesystem %s not defined", s.Fs)
|
|
}
|
|
name := *s.File
|
|
filename := name
|
|
for _, fn := range []string{name, name + ".yaml", name + ".yml", name + ".json"} {
|
|
// check if file exists
|
|
_, err := afs.Stat(fn)
|
|
if err == nil {
|
|
filename = fn
|
|
break
|
|
}
|
|
}
|
|
fileBytes, err := afero.ReadFile(afs, filename)
|
|
if err != nil {
|
|
return nil, 404, err
|
|
}
|
|
return json.RawMessage(fileBytes), 200, nil
|
|
}
|
|
func (s *Source) executeRaw(ctx context.Context) (json.RawMessage, int, error) {
|
|
return json.RawMessage(*s.Raw), 200, nil
|
|
}
|
|
|
|
func (s *Source) executeEmpty(ctx context.Context) (json.RawMessage, int, error) {
|
|
return []byte("{}"), 200, nil
|
|
}
|