Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: callback service #146

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions internal/server/http/callback.go
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
119 changes: 119 additions & 0 deletions internal/server/http/callback_test.go
Original file line number Diff line number Diff line change
@@ -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{}{}
}
}
12 changes: 12 additions & 0 deletions internal/server/http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 7 additions & 5 deletions internal/server/http/imposter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"`
}

Expand Down Expand Up @@ -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
}
Expand Down
5 changes: 3 additions & 2 deletions internal/server/http/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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":"[email protected]",
"password":"ngab"
}
},
"delay": "1s:1s"
},
"response": {
"status": 200,
"body": "test"
}
}
]