Skip to content

Commit

Permalink
Added support for Server Side events
Browse files Browse the repository at this point in the history
Signed-off-by: Roshan Patil <[email protected]>
  • Loading branch information
rosspatil committed Jul 30, 2024
1 parent d69f0da commit a698eed
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 9 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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

Expand Down
42 changes: 42 additions & 0 deletions config/sse.yaml
Original file line number Diff line number Diff line change
@@ -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"
}
]
2 changes: 1 addition & 1 deletion internal/server/dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
49 changes: 43 additions & 6 deletions pkg/mock/http.go
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -68,22 +71,56 @@ 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 {
cookies = append(cookies, fmt.Sprintf("%s=%s", cookie, value))
}
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()
}
}
}
2 changes: 1 addition & 1 deletion pkg/mock/message_translator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit a698eed

Please sign in to comment.