github.com/smartcontractkit/chainlink-testing-framework/libs@v0.0.0-20240227141906-ec710b4eb1a3/docker/test_env/killgrave.go (about)

     1  package test_env
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"net/http"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/google/uuid"
    14  	"github.com/otiai10/copy"
    15  	"github.com/rs/zerolog"
    16  	"github.com/rs/zerolog/log"
    17  	tc "github.com/testcontainers/testcontainers-go"
    18  	"github.com/testcontainers/testcontainers-go/wait"
    19  
    20  	"github.com/smartcontractkit/chainlink-testing-framework/libs/logging"
    21  	"github.com/smartcontractkit/chainlink-testing-framework/libs/mirror"
    22  	"github.com/smartcontractkit/chainlink-testing-framework/libs/utils/testcontext"
    23  )
    24  
    25  type Killgrave struct {
    26  	EnvComponent
    27  	ExternalEndpoint    string
    28  	InternalPort        string
    29  	InternalEndpoint    string
    30  	impostersPath       string
    31  	impostersDirBinding string
    32  	t                   *testing.T
    33  	l                   zerolog.Logger
    34  }
    35  
    36  // Imposter define an imposter structure
    37  type KillgraveImposter struct {
    38  	Request  KillgraveRequest  `json:"request"`
    39  	Response KillgraveResponse `json:"response"`
    40  }
    41  
    42  type KillgraveRequest struct {
    43  	Method     string             `json:"method"`
    44  	Endpoint   string             `json:"endpoint,omitempty"`
    45  	SchemaFile *string            `json:"schemaFile,omitempty"`
    46  	Params     *map[string]string `json:"params,omitempty"`
    47  	Headers    *map[string]string `json:"headers"`
    48  }
    49  
    50  // Response represent the structure of real response
    51  type KillgraveResponse struct {
    52  	Status   int                     `json:"status"`
    53  	Body     string                  `json:"body,omitempty"`
    54  	BodyFile *string                 `json:"bodyFile,omitempty"`
    55  	Headers  *map[string]string      `json:"headers,omitempty"`
    56  	Delay    *KillgraveResponseDelay `json:"delay,omitempty"`
    57  }
    58  
    59  // ResponseDelay represent time delay before server responds.
    60  type KillgraveResponseDelay struct {
    61  	Delay  int64 `json:"delay,omitempty"`
    62  	Offset int64 `json:"offset,omitempty"`
    63  }
    64  
    65  // AdapterResponse represents a response from an adapter
    66  type KillgraveAdapterResponse struct {
    67  	Id    string                 `json:"id"`
    68  	Data  KillgraveAdapterResult `json:"data"`
    69  	Error interface{}            `json:"error"`
    70  }
    71  
    72  // AdapterResult represents an int result for an adapter
    73  type KillgraveAdapterResult struct {
    74  	Result interface{} `json:"result"`
    75  }
    76  
    77  func NewKillgrave(networks []string, impostersDirectoryPath string, opts ...EnvComponentOption) *Killgrave {
    78  	k := &Killgrave{
    79  		EnvComponent: EnvComponent{
    80  			ContainerName: fmt.Sprintf("%s-%s", "killgrave", uuid.NewString()[0:3]),
    81  			Networks:      networks,
    82  		},
    83  		InternalPort:  "3000",
    84  		impostersPath: impostersDirectoryPath,
    85  		l:             log.Logger,
    86  	}
    87  	k.SetDefaultHooks()
    88  	for _, opt := range opts {
    89  		opt(&k.EnvComponent)
    90  	}
    91  	return k
    92  }
    93  
    94  func (k *Killgrave) WithTestInstance(t *testing.T) *Killgrave {
    95  	k.l = logging.GetTestLogger(t)
    96  	k.t = t
    97  	return k
    98  }
    99  
   100  func (k *Killgrave) StartContainer() error {
   101  	err := k.setupImposters()
   102  	if err != nil {
   103  		return err
   104  	}
   105  	if k.t != nil {
   106  		k.t.Cleanup(func() {
   107  			os.RemoveAll(k.impostersDirBinding)
   108  		})
   109  	}
   110  	l := logging.GetTestContainersGoTestLogger(k.t)
   111  	cr, err := k.getContainerRequest()
   112  	if err != nil {
   113  		return err
   114  	}
   115  	c, err := tc.GenericContainer(testcontext.Get(k.t), tc.GenericContainerRequest{
   116  		ContainerRequest: cr,
   117  		Started:          true,
   118  		Reuse:            true,
   119  		Logger:           l,
   120  	})
   121  	if err != nil {
   122  		return fmt.Errorf("cannot start Killgrave container: %w", err)
   123  	}
   124  	endpoint, err := GetEndpoint(testcontext.Get(k.t), c, "http")
   125  	if err != nil {
   126  		return err
   127  	}
   128  	k.Container = c
   129  	k.ExternalEndpoint = endpoint
   130  	k.InternalEndpoint = fmt.Sprintf("http://%s:%s", k.ContainerName, k.InternalPort)
   131  
   132  	k.l.Info().Str("External Endpoint", k.ExternalEndpoint).
   133  		Str("Internal Endpoint", k.InternalEndpoint).
   134  		Str("Container Name", k.ContainerName).
   135  		Msgf("Started Killgrave Container")
   136  	return nil
   137  }
   138  
   139  func (k *Killgrave) getContainerRequest() (tc.ContainerRequest, error) {
   140  	killgraveImage, err := mirror.GetImage("friendsofgo/killgrave")
   141  	if err != nil {
   142  		return tc.ContainerRequest{}, err
   143  	}
   144  	return tc.ContainerRequest{
   145  		Name:         k.ContainerName,
   146  		Networks:     k.Networks,
   147  		Image:        killgraveImage,
   148  		ExposedPorts: []string{NatPortFormat(k.InternalPort)},
   149  		Cmd:          []string{"-host=0.0.0.0", "-imposters=/imposters", "-watcher"},
   150  		Mounts: tc.ContainerMounts{
   151  			tc.ContainerMount{
   152  				Source: tc.GenericBindMountSource{
   153  					HostPath: k.impostersDirBinding,
   154  				},
   155  				Target: "/imposters",
   156  			},
   157  		},
   158  		WaitingFor: wait.ForLog("The fake server is on tap now"),
   159  		LifecycleHooks: []tc.ContainerLifecycleHooks{
   160  			{
   161  				PostStarts: k.PostStartsHooks,
   162  				PostStops:  k.PostStopsHooks,
   163  			},
   164  		},
   165  	}, nil
   166  }
   167  
   168  func (k *Killgrave) setupImposters() error {
   169  	// create temporary directory for imposters
   170  	var err error
   171  	k.impostersDirBinding, err = os.MkdirTemp(k.impostersDirBinding, "imposters*")
   172  	if err != nil {
   173  		return err
   174  	}
   175  	k.l.Info().Str("Path", k.impostersDirBinding).Msg("Imposters directory created at")
   176  
   177  	// copy user imposters
   178  	if len(k.impostersPath) != 0 {
   179  		err = copy.Copy(k.impostersPath, k.impostersDirBinding)
   180  		if err != nil {
   181  			return err
   182  		}
   183  	}
   184  
   185  	// add default five imposter
   186  	return k.SetAdapterBasedIntValuePath("/five", []string{http.MethodGet, http.MethodPost}, 5)
   187  }
   188  
   189  // AddImposter adds an imposter to the killgrave container
   190  func (k *Killgrave) AddImposter(imposters []KillgraveImposter) error {
   191  	// if the endpoint paths do not start with '/' then add it
   192  	for i, imposter := range imposters {
   193  		if !strings.HasPrefix(imposter.Request.Endpoint, "/") {
   194  			imposter.Request.Endpoint = fmt.Sprintf("/%s", imposter.Request.Endpoint)
   195  			imposters[i] = imposter
   196  		}
   197  	}
   198  
   199  	req := imposters[0].Request
   200  	data, err := json.Marshal(imposters)
   201  	if err != nil {
   202  		return err
   203  	}
   204  
   205  	// build the file name from the req.Endpoint
   206  	unsafeFileName := strings.TrimPrefix(req.Endpoint, "/")
   207  	safeFileName := strings.ReplaceAll(unsafeFileName, "/", ".")
   208  	f, err := os.Create(filepath.Join(k.impostersDirBinding, fmt.Sprintf("%s.imp.json", safeFileName)))
   209  	if err != nil {
   210  		return err
   211  	}
   212  	defer f.Close()
   213  
   214  	_, err = f.WriteString(string(data))
   215  	if err != nil {
   216  		return err
   217  	}
   218  
   219  	// when adding default imposters, the container is not yet started and the container will be nil
   220  	// this allows us to add them without having to wait for the imposter to load later
   221  	if k.Container != nil {
   222  		// wait for the log saying the imposter was loaded
   223  		containerFile := filepath.Join("/imposters", fmt.Sprintf("%s.imp.json", safeFileName))
   224  		logWaitStrategy := wait.ForLog(fmt.Sprintf("imposter %s loaded", containerFile)).WithStartupTimeout(15 * time.Second)
   225  		err = logWaitStrategy.WaitUntilReady(testcontext.Get(k.t), k.Container)
   226  	}
   227  	return err
   228  }
   229  
   230  // SetStringValuePath sets a path to return a string value
   231  func (k *Killgrave) SetStringValuePath(path string, methods []string, headers map[string]string, v string) error {
   232  	imposters := []KillgraveImposter{}
   233  	for _, method := range methods {
   234  		imposters = append(imposters, KillgraveImposter{
   235  			Request: KillgraveRequest{
   236  				Method:   method,
   237  				Endpoint: path,
   238  			},
   239  			Response: KillgraveResponse{
   240  				Status:  200,
   241  				Body:    v,
   242  				Headers: &headers,
   243  			},
   244  		})
   245  	}
   246  
   247  	return k.AddImposter(imposters)
   248  }
   249  
   250  // SetAdapterBasedAnyValuePath sets a path to return a value as though it was from an adapter
   251  func (k *Killgrave) SetAdapterBasedAnyValuePath(path string, methods []string, v interface{}) error {
   252  	ar := KillgraveAdapterResponse{
   253  		Id: "",
   254  		Data: KillgraveAdapterResult{
   255  			Result: v,
   256  		},
   257  		Error: nil,
   258  	}
   259  	data, err := json.Marshal(ar)
   260  	if err != nil {
   261  		return err
   262  	}
   263  
   264  	return k.SetStringValuePath(path, methods, map[string]string{
   265  		"Content-Type": "application/json",
   266  	}, string(data))
   267  }
   268  
   269  func (k *Killgrave) SetAnyValueResponse(path string, methods []string, v interface{}) error {
   270  	data, err := json.Marshal(v)
   271  	if err != nil {
   272  		return err
   273  	}
   274  
   275  	return k.SetStringValuePath(path, methods, map[string]string{
   276  		"Content-Type": "application/json",
   277  	}, string(data))
   278  }
   279  
   280  // SetAdapterBasedAnyValuePathObject sets a path to return a value as though it was from an adapter
   281  func (k *Killgrave) SetAdapterBasedIntValuePath(path string, methods []string, v int) error {
   282  	return k.SetAdapterBasedAnyValuePath(path, methods, v)
   283  }