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 }