diff --git a/README.md b/README.md index 8cf0d89..8cb762b 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Built with Go - Mmock runs without installation on multiple platforms. * Public interface auto discover * Lightweight and portable * No installation required +* Server Side Events ### Example @@ -236,7 +237,7 @@ See https://pkg.go.dev/regexp/syntax for regexp syntax * *statusCode*: Response status code * *headers*: Array of headers. It allows more than one value for the same key and vars. * *cookies*: Array of cookies. It allows vars. -* *body*: Body string. It allows vars. +* *body*: Body string. It allows vars. For SSE, pass the body in array of JSON format. #### Callback (Optional) @@ -631,6 +632,8 @@ You can always disable this behavior adding the following flag `-server-statisti - Improved logging with levels thanks to [@jcdietrich](https://github.com/jcdietrich) [@jdietrich-tc](https://github.com/jdietrich-tc) - Support for Regular Expressions for QueryStringParameters [@jcdietrich](https://github.com/jcdietrich) [@jdietrich-tc](https://github.com/jdietrich-tc) - Support for URI and Description tags [@jcdietrich](https://github.com/jcdietrich) [@jdietrich-tc](https://github.com/jdietrich-tc) +- Support for Server Side Events [@rosspatil](https://github.com/rosspatil) + ### Contributing diff --git a/config/sse.yaml b/config/sse.yaml new file mode 100644 index 0000000..7e94a17 --- /dev/null +++ b/config/sse.yaml @@ -0,0 +1,42 @@ +request: + method: POST + path: /events +response: + statusCode: 200 + headers: + Content-Type: + - "text/event-stream" + Connection: + - "keep-alive" + Cache-Control: + - "no-cache" + body: > + [ + { + "test":"1" + }, + { + "test":"2" + }, + { + "test":"3" + }, + { + "test":"4" + }, + { + "test":"5" + }, + { + "test":"6" + }, + { + "test":"7" + }, + { + "test":"8" + }, + { + "test":"9" + } + ] diff --git a/internal/server/dispatcher.go b/internal/server/dispatcher.go index fc51691..81c7454 100644 --- a/internal/server/dispatcher.go +++ b/internal/server/dispatcher.go @@ -95,7 +95,7 @@ func (di *Dispatcher) ServeHTTP(w http.ResponseWriter, req *http.Request) { } //translate request - di.Translator.WriteHTTPResponseFromDefinition(transaction.Response, w) + di.Translator.WriteHTTPResponseFromDefinition(transaction.Response, w, req) if mock.Callback.Url != "" { go func() { diff --git a/pkg/mock/http.go b/pkg/mock/http.go index 810a7f7..30d8bf3 100644 --- a/pkg/mock/http.go +++ b/pkg/mock/http.go @@ -1,11 +1,14 @@ package mock import ( + "encoding/json" "fmt" "io" - "io/ioutil" "net/http" "strings" + "time" + + "github.com/tidwall/gjson" ) // HTTP is and adaptor beteewn the http and mock config. @@ -39,7 +42,7 @@ func (t HTTP) BuildRequestDefinitionFromHTTP(req *http.Request) Request { res.QueryStringParameters[name] = values } - body, _ := ioutil.ReadAll(req.Body) + body, _ := io.ReadAll(req.Body) res.Body = string(body) return res @@ -68,14 +71,34 @@ func getHostAndPort(req *http.Request) (string, string) { } // WriteHTTPResponseFromDefinition read a mock response and write a http response. -func (t HTTP) WriteHTTPResponseFromDefinition(fr *Response, w http.ResponseWriter) { +func (t HTTP) WriteHTTPResponseFromDefinition(fr *Response, w http.ResponseWriter, req *http.Request) { + if isSSE(fr) { + streamResponse(fr, w, req) + return + } + addHeadersAndCookies(fr, w) + w.WriteHeader(fr.StatusCode) + io.WriteString(w, fr.Body) +} +// Check if the response is of type SSE +func isSSE(fr *Response) bool { + values, ok := fr.Headers["content-type"] + if ok { + for _, value := range values { + return strings.ToLower(value) == "text/event-stream" + } + } + return false +} + +func addHeadersAndCookies(fr *Response, w http.ResponseWriter) { for header, values := range fr.Headers { for _, value := range values { w.Header().Add(header, value) } - } + if len(fr.Cookies) > 0 { cookies := []string{} for cookie, value := range fr.Cookies { @@ -83,7 +106,21 @@ func (t HTTP) WriteHTTPResponseFromDefinition(fr *Response, w http.ResponseWrite } w.Header().Add("Set-Cookie", strings.Join(cookies, ";")) } +} - w.WriteHeader(fr.StatusCode) - io.WriteString(w, fr.Body) +// streamResponse - stream response +func streamResponse(fr *Response, w http.ResponseWriter, req *http.Request) { + addHeadersAndCookies(fr, w) + + for _, response := range gjson.Parse(fr.Body).Array() { + time.Sleep(time.Second * 2) + select { + case <-req.Context().Done(): + return + default: + ba, _ := json.Marshal((response.Value())) + fmt.Fprintf(w, "data: %s\n\n", string(ba)) + w.(http.Flusher).Flush() + } + } } diff --git a/pkg/mock/message_translator.go b/pkg/mock/message_translator.go index 9ef4d26..b3b0df4 100644 --- a/pkg/mock/message_translator.go +++ b/pkg/mock/message_translator.go @@ -11,7 +11,7 @@ type MockRequestBuilder interface { // MockResponseWriter defines the translator from config.Response to http.ResponseWriter type MockResponseWriter interface { - WriteHTTPResponseFromDefinition(fr *Response, w http.ResponseWriter) + WriteHTTPResponseFromDefinition(fr *Response, w http.ResponseWriter, req *http.Request) } // MessageTranslator defines the translator contract between http and mock and viceversa.