From f60a8bb255beb3bebecbade13b731a93d265e785 Mon Sep 17 00:00:00 2001 From: Daniel Anugerah Date: Fri, 1 Sep 2023 19:26:17 +0800 Subject: [PATCH] feat: callback service --- internal/server/http/callback.go | 96 ++++++++++++++ internal/server/http/callback_test.go | 119 ++++++++++++++++++ internal/server/http/handler.go | 12 ++ internal/server/http/imposter.go | 12 +- internal/server/http/server.go | 5 +- .../test_request_with_callback.imp.json | 26 ++++ 6 files changed, 263 insertions(+), 7 deletions(-) create mode 100644 internal/server/http/callback.go create mode 100644 internal/server/http/callback_test.go create mode 100644 internal/server/http/test/testdata/imposters/test_request_with_callback.imp.json diff --git a/internal/server/http/callback.go b/internal/server/http/callback.go new file mode 100644 index 0000000..c57e70d --- /dev/null +++ b/internal/server/http/callback.go @@ -0,0 +1,96 @@ +package http + +import ( + "bytes" + "encoding/json" + "io" + "log" + "net/http" + "sync" + "time" +) + +// CallbackMap is a map of all the callbacks +type callbackMap map[*time.Ticker]*Callback + +var mutex = &sync.Mutex{} + +// Callback represent the structure of real callback +// with additional delay +type Callback struct { + Ticker *time.Ticker `json:"-"` + Request Request `json:"request" yaml:"request"` + Delay ResponseDelay `json:"delay" yaml:"delay"` +} + +func (c Callback) Call() { + log.Println("Initiated Callback") + buf := new(bytes.Buffer) + + req, err := http.NewRequest(c.Request.Method, c.Request.Endpoint, buf) + if err != nil { + return + } + + if c.Request.Body != nil { + json.NewEncoder(buf).Encode(c.Request.Body) + } + + if c.Request.Headers != nil { + for key, val := range *c.Request.Headers { + req.Header.Set(key, val) + } + } + req.Header.Set("User-Agent", "Killgrave") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Println("there's error in your defined callback service: ", err.Error()) + return + } + if resp.StatusCode != http.StatusOK { + log.Printf("Callback service returned non-OK status code: %d\n", resp.StatusCode) + + // Read and log the response body if it's available. + responseBody, readErr := io.ReadAll(resp.Body) + if readErr != nil { + log.Println("Error reading response body:", readErr.Error()) + } else { + log.Printf("Response Body: %s\n", responseBody) + } + + resp.Body.Close() + return + } + + log.Println("Callback completed successfully - ", req.URL.String()) +} + +// global variable to store all the callbacks +var callbackMapInstance = make(callbackMap) + +// remove a callback from the map +func (c callbackMap) Remove(t *time.Ticker) { + delete(c, t) +} + +// add a callback to the map +func (c callbackMap) Add(t *time.Ticker, callback *Callback) { + mutex.Lock() + c[t] = callback + mutex.Unlock() +} + +func (s *Server) callbackCron() { + for { + for ticker, callback := range callbackMapInstance { + select { + case <-ticker.C: + callback.Call() + callbackMapInstance.Remove(ticker) + default: + continue + } + } + } +} diff --git a/internal/server/http/callback_test.go b/internal/server/http/callback_test.go new file mode 100644 index 0000000..b97f59e --- /dev/null +++ b/internal/server/http/callback_test.go @@ -0,0 +1,119 @@ +package http + +import ( + "net/http" + "net/http/httptest" + "sync" + "testing" + "time" + + "github.com/gorilla/mux" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" +) + +var iitermutex = new(sync.Mutex) + +func TestCallback(t *testing.T) { + router := mux.NewRouter() + httpServer := &http.Server{Handler: router} + imposterFs := NewImposterFS(afero.NewOsFs()) + server := NewServer("test/testdata/imposters", router, httpServer, &Proxy{}, false, imposterFs) + + testCases := map[string]struct { + imposter Imposter + expectedStatusCode int + expectedError bool + }{ + "valid callback": { + imposter: Imposter{ + Request: Request{ + Method: "POST", + Endpoint: "/gophers", + }, + Callback: &Callback{ + Request: Request{ + Method: "GET", + Endpoint: "http://localhost:8080/test", + }, + Delay: ResponseDelay{ + delay: 2 * int64(time.Second), + offset: 0, + }, + }, + Response: Response{ + Status: http.StatusOK, + Body: "hello", + }, + }, + expectedStatusCode: 200, + }, + "default callback": { + imposter: Imposter{ + Request: Request{ + Method: "POST", + Endpoint: "/gophers", + }, + Callback: &Callback{ + Request: Request{ + Method: "GET", + Endpoint: "http://localhost:8080/test", + }, + Delay: ResponseDelay{ + delay: 0, + offset: 0, + }, + }, + Response: Response{ + Status: http.StatusOK, + Body: "hello", + }, + }, + expectedStatusCode: 200, + }, + } + + for key, tt := range testCases { + wg := new(sync.WaitGroup) + wg.Add(1) + t.Run(key, func(t *testing.T) { + defer func() { + if tt.expectedError { + rec := recover() + assert.NotNil(t, rec) + } + }() + + go func() { + router := http.NewServeMux() + + router.HandleFunc("/test", helloHandler(wg)) + http.ListenAndServe(":8080", router) + }() + + rec := httptest.NewRecorder() + handler := http.HandlerFunc(ImposterHandler(tt.imposter)) + + err := server.Build() + assert.Nil(t, err) + + // due the caller service were ahead 1 seconds for the default. + handler.ServeHTTP(rec, httptest.NewRequest(tt.imposter.Request.Method, tt.imposter.Request.Endpoint, nil)) + assert.Equal(t, tt.expectedStatusCode, rec.Code) + }) + wg.Wait() + } + + +} + +// if this function were called then we knew this function were called from the callback +func helloHandler(wg *sync.WaitGroup) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Write([]byte("hello")) + + wg.Done() + // done <- struct{}{} + } +} diff --git a/internal/server/http/handler.go b/internal/server/http/handler.go index 83ee85a..e69dd3f 100644 --- a/internal/server/http/handler.go +++ b/internal/server/http/handler.go @@ -14,6 +14,18 @@ func ImposterHandler(imposter Imposter) http.HandlerFunc { if imposter.Delay() > 0 { time.Sleep(imposter.Delay()) } + + if imposter.Callback != nil { + tickerTime := imposter.Callback.Delay.Delay() + if tickerTime <= 0 { + tickerTime = time.Second + } + + tickerInstance := time.NewTicker(tickerTime) + imposter.Callback.Ticker = tickerInstance + callbackMapInstance.Add(tickerInstance, imposter.Callback) + } + writeHeaders(imposter, w) w.WriteHeader(imposter.Response.Status) writeBody(imposter, w) diff --git a/internal/server/http/imposter.go b/internal/server/http/imposter.go index 0d4e831..1f51743 100644 --- a/internal/server/http/imposter.go +++ b/internal/server/http/imposter.go @@ -38,10 +38,11 @@ type ImposterConfig struct { // Imposter define an imposter structure type Imposter struct { - BasePath string `json:"-" yaml:"-"` - Path string `json:"-" yaml:"-"` - Request Request `json:"request"` - Response Response `json:"response"` + BasePath string `json:"-" yaml:"-"` + Path string `json:"-" yaml:"-"` + Request Request `json:"request"` + Callback *Callback `json:"callback"` + Response Response `json:"response"` } // Delay returns delay for response that user can specify in imposter config @@ -60,6 +61,7 @@ type Request struct { Endpoint string `json:"endpoint"` SchemaFile *string `json:"schemaFile"` Params *map[string]string `json:"params"` + Body *map[string]string `json:"body"` Headers *map[string]string `json:"headers"` } @@ -132,7 +134,7 @@ func (i ImposterFs) unmarshalImposters(imposterConfig ImposterConfig) ([]Imposte return nil, fmt.Errorf("%w: error while unmarshalling imposter's file %s", parseError, imposterConfig.FilePath) } - for i, _ := range imposters { + for i := range imposters { imposters[i].BasePath = filepath.Dir(imposterConfig.FilePath) imposters[i].Path = imposterConfig.FilePath } diff --git a/internal/server/http/server.go b/internal/server/http/server.go index f34d438..e6c54d1 100644 --- a/internal/server/http/server.go +++ b/internal/server/http/server.go @@ -92,8 +92,9 @@ func (s *Server) Build() error { if _, err := os.Stat(s.impostersPath); os.IsNotExist(err) { return fmt.Errorf("%w: the directory %s doesn't exists", err, s.impostersPath) } - var impostersCh = make(chan []Imposter) - var done = make(chan struct{}) + impostersCh := make(chan []Imposter) + done := make(chan struct{}) + go s.callbackCron() go func() { s.imposterFs.FindImposters(s.impostersPath, impostersCh) diff --git a/internal/server/http/test/testdata/imposters/test_request_with_callback.imp.json b/internal/server/http/test/testdata/imposters/test_request_with_callback.imp.json new file mode 100644 index 0000000..04ea637 --- /dev/null +++ b/internal/server/http/test/testdata/imposters/test_request_with_callback.imp.json @@ -0,0 +1,26 @@ +[ + { + "request": { + "method": "GET", + "endpoint": "/testRequest" + }, + "callback": { + "request":{ + "method": "POST", + "endpoint": "http://localhost:3000/users", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "email":"mail@mail.com", + "password":"ngab" + } + }, + "delay": "1s:1s" + }, + "response": { + "status": 200, + "body": "test" + } + } +]