-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement net/http.Handler for local development
This commit modifies the lmdrouter.Router type to implement the net/http.Handler interface (meaning the ServeHTTP method is now available on *lmdrouter.Route objects). This allows usage of applications using lmdrouter in environments other than AWS Lambda, but is mostly useful for local development purposes. It should be noted that this means applications will now have to use the `netgo` build tag (`go build -tags netgo`) to make sure binaries are still statically compiled. Resolves: 1
- Loading branch information
Ido Perlmuter
committed
Mar 9, 2022
1 parent
d0b6229
commit f5483e1
Showing
5 changed files
with
243 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
package lmdrouter | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
|
||
"github.com/aws/aws-lambda-go/events" | ||
) | ||
|
||
// ServerHTTP implements the net/http.Handler interface in order to allow | ||
// lmdrouter applications to be used outside of AWS Lambda environments, most | ||
// likely for local development purposes | ||
func (l *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||
// convert request into an events.APIGatewayProxyRequest object | ||
singleValueHeaders := convertMap(map[string][]string(r.Header)) | ||
singleValueQuery := convertMap( | ||
map[string][]string(r.URL.Query()), | ||
) | ||
|
||
body, err := io.ReadAll(r.Body) | ||
if err != nil { | ||
w.Header().Set("Content-Type", "application/json; charset=UTF-8") | ||
w.WriteHeader(500) | ||
json.NewEncoder(w).Encode(map[string]interface{}{ | ||
"error": fmt.Sprintf("Failed reading request body: %s", err), | ||
}) // nolint: errcheck | ||
return | ||
} | ||
|
||
event := events.APIGatewayProxyRequest{ | ||
Path: r.URL.Path, | ||
HTTPMethod: r.Method, | ||
Headers: singleValueHeaders, | ||
MultiValueHeaders: map[string][]string(r.Header), | ||
QueryStringParameters: singleValueQuery, | ||
MultiValueQueryStringParameters: map[string][]string(r.URL.Query()), | ||
Body: string(body), | ||
} | ||
|
||
res, err := l.Handler(r.Context(), event) | ||
if err != nil { | ||
w.Header().Set("Content-Type", "application/json; charset=UTF-8") | ||
w.WriteHeader(500) | ||
json.NewEncoder(w).Encode(map[string]interface{}{ | ||
"error": fmt.Sprintf("Failed executing handler: %s", err), | ||
}) // nolint: errcheck | ||
return | ||
} | ||
|
||
for header, values := range res.MultiValueHeaders { | ||
for i, value := range values { | ||
if i == 0 { | ||
w.Header().Set(header, value) | ||
} else { | ||
w.Header().Add(header, value) | ||
} | ||
} | ||
} | ||
|
||
for header, value := range res.Headers { | ||
if w.Header().Get(header) == "" { | ||
w.Header().Set(header, value) | ||
} | ||
} | ||
|
||
w.WriteHeader(res.StatusCode) | ||
w.Write([]byte(res.Body)) // nolint: errcheck | ||
} | ||
|
||
func convertMap(in map[string][]string) map[string]string { | ||
singleValue := make(map[string]string) | ||
|
||
for key, value := range in { | ||
if len(value) == 1 { | ||
singleValue[key] = value[0] | ||
} | ||
} | ||
|
||
return singleValue | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
package lmdrouter | ||
|
||
import ( | ||
"encoding/json" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
"time" | ||
|
||
"github.com/jgroeneveld/trial/assert" | ||
) | ||
|
||
func TestHTTPHandler(t *testing.T) { | ||
lmd := NewRouter("/api", logger) | ||
lmd.Route("GET", "/", listSomethings) | ||
lmd.Route("POST", "/", postSomething, auth) | ||
lmd.Route("GET", "/:id", getSomething) | ||
lmd.Route("GET", "/:id/stuff", listStuff) | ||
lmd.Route("GET", "/:id/stuff/:fake", listStuff) | ||
|
||
ts := httptest.NewServer(http.HandlerFunc(lmd.ServeHTTP)) | ||
|
||
defer ts.Close() | ||
|
||
t.Run("POST /api without auth", func(t *testing.T) { | ||
res, err := http.Post( | ||
ts.URL+"/api", | ||
"application/json; charset=UTF-8", | ||
nil, | ||
) | ||
|
||
assert.Equal(t, nil, err, "Error must not be nil") | ||
assert.Equal(t, http.StatusUnauthorized, res.StatusCode, "Status code must be 401") | ||
assert.True(t, len(log) > 0, "Log must have items") | ||
}) | ||
|
||
t.Run("POST /api with auth", func(t *testing.T) { | ||
req, err := http.NewRequest( | ||
"POST", | ||
ts.URL+"/api", | ||
nil, | ||
) | ||
if err != nil { | ||
t.Fatalf("Request creation unexpectedly failed: %s", err) | ||
} | ||
|
||
req.Header.Set("Authorization", "Bearer fake-token") | ||
|
||
res, err := http.DefaultClient.Do(req) | ||
assert.Equal(t, nil, err, "Error must not be nil") | ||
assert.Equal(t, http.StatusBadRequest, res.StatusCode, "Status code must be 400") | ||
}) | ||
|
||
t.Run("GET /api", func(t *testing.T) { | ||
res, err := http.Get(ts.URL + "/api") | ||
assert.Equal(t, nil, err, "Error must not be nil") | ||
assert.Equal(t, http.StatusOK, res.StatusCode, "Status code must be 200") | ||
assert.True(t, len(log) > 0, "Log must have items") | ||
}) | ||
|
||
t.Run("GET /api/something/stuff", func(t *testing.T) { | ||
req, _ := http.NewRequest( | ||
"GET", | ||
ts.URL+"/api/something/stuff?terms=one&terms=two&terms=three", | ||
nil, | ||
) | ||
req.Header.Set("Accept-Language", "en-us") | ||
|
||
res, err := http.DefaultClient.Do(req) | ||
assert.Equal(t, nil, err, "Response error must be nil") | ||
assert.Equal(t, http.StatusOK, res.StatusCode, "Status code must be 200") | ||
|
||
var data []mockItem | ||
err = json.NewDecoder(res.Body).Decode(&data) | ||
assert.Equal(t, nil, err, "Decode error must be nil") | ||
assert.DeepEqual( | ||
t, | ||
[]mockItem{ | ||
{ | ||
ID: "something", | ||
Name: "one in en-us", | ||
Date: time.Time{}, | ||
}, | ||
{ | ||
ID: "something", | ||
Name: "two in en-us", | ||
Date: time.Time{}, | ||
}, | ||
{ | ||
ID: "something", | ||
Name: "three in en-us", | ||
Date: time.Time{}, | ||
}, | ||
}, | ||
data, | ||
"Response body must match", | ||
) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters