Skip to content

Commit

Permalink
chore: add more httpingress integration tests
Browse files Browse the repository at this point in the history
  • Loading branch information
wesbillman committed Feb 1, 2024
1 parent 44d1479 commit 1925465
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 50 deletions.
4 changes: 3 additions & 1 deletion backend/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,8 +258,10 @@ func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
for k, v := range response.Headers {
w.Header()[k] = v
}
w.WriteHeader(response.Status)

if response.Status != 0 {
w.WriteHeader(response.Status)
}
} else {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
Expand Down
20 changes: 16 additions & 4 deletions backend/controller/ingress/ingress.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ func ResponseBodyForVerb(sch *schema.Schema, verb *schema.Verb, body []byte, hea
return nil, err
}

if bodyType, ok := bodyField.Type.(*schema.DataRef); ok {
switch bodyType := bodyField.Type.(type) {
case *schema.DataRef:
var responseMap map[string]any
err := json.Unmarshal(body, &responseMap)
if err != nil {
Expand All @@ -175,11 +176,22 @@ func ResponseBodyForVerb(sch *schema.Schema, verb *schema.Verb, body []byte, hea
if err != nil {
return nil, err
}

return json.Marshal(aliasedResponseMap)
}

return body, nil
case *schema.Bytes:
var base64String string
if err := json.Unmarshal(body, &base64String); err != nil {
return nil, fmt.Errorf("HTTP response body is not valid base64: %w", err)
}
decodedBody, err := base64.StdEncoding.DecodeString(base64String)
if err != nil {
return nil, fmt.Errorf("failed to decode base64 response body: %w", err)
}
return decodedBody, nil

default:
return body, nil
}
}

func getBodyField(dataRef *schema.DataRef, sch *schema.Schema) (*schema.Field, error) {
Expand Down
18 changes: 8 additions & 10 deletions examples/go/httpingress/httpingress.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ import (
"ftl/builtin"
)

type Nested struct {
GoodStuff string `alias:"good_stuff"`
}

type GetRequest struct {
UserID string `alias:"userId"`
PostID string `alias:"postId"`
}

type Nested struct {
GoodStuff string `alias:"good_stuff"`
}

type GetResponse struct {
Message string `alias:"random"`
Nested Nested `alias:"nested"`
Expand All @@ -28,9 +28,11 @@ type GetResponse struct {
//ftl:ingress http GET /http/users/{userID}/posts
func Get(ctx context.Context, req builtin.HttpRequest[GetRequest]) (builtin.HttpResponse[GetResponse], error) {
return builtin.HttpResponse[GetResponse]{
Status: 200,
Headers: map[string][]string{"Get": {"Header from FTL"}},
Body: GetResponse{Message: fmt.Sprintf("UserID, %s : PostID %s", req.Body.UserID, req.Body.PostID), Nested: Nested{GoodStuff: "Nested Good Stuff"}},
Body: GetResponse{
Message: fmt.Sprintf("Got userId %s and postId %s", req.Body.UserID, req.Body.PostID),
Nested: Nested{GoodStuff: "Nested Good Stuff"},
},
}, nil
}

Expand Down Expand Up @@ -68,7 +70,6 @@ type PutResponse struct{}
//ftl:ingress http PUT /http/users/{userID}
func Put(ctx context.Context, req builtin.HttpRequest[PutRequest]) (builtin.HttpResponse[PutResponse], error) {
return builtin.HttpResponse[PutResponse]{
Status: 200,
Headers: map[string][]string{"Put": {"Header from FTL"}},
Body: PutResponse{},
}, nil
Expand All @@ -86,7 +87,6 @@ type DeleteResponse struct{}
//ftl:ingress http DELETE /http/users/{userID}
func Delete(ctx context.Context, req builtin.HttpRequest[DeleteRequest]) (builtin.HttpResponse[DeleteResponse], error) {
return builtin.HttpResponse[DeleteResponse]{
Status: 200,
Headers: map[string][]string{"Put": {"Header from FTL"}},
Body: DeleteResponse{},
}, nil
Expand All @@ -98,7 +98,6 @@ type HtmlRequest struct{}
//ftl:ingress http GET /http/html
func Html(ctx context.Context, req builtin.HttpRequest[HtmlRequest]) (builtin.HttpResponse[string], error) {
return builtin.HttpResponse[string]{
Status: 200,
Headers: map[string][]string{"Content-Type": {"text/html; charset=utf-8"}},
Body: "<html><body><h1>HTML Page From FTL 🚀!</h1></body></html>",
}, nil
Expand All @@ -110,7 +109,6 @@ func Html(ctx context.Context, req builtin.HttpRequest[HtmlRequest]) (builtin.Ht
//ftl:ingress http POST /http/bytes
func Bytes(ctx context.Context, req builtin.HttpRequest[[]byte]) (builtin.HttpResponse[[]byte], error) {
return builtin.HttpResponse[[]byte]{
Status: 200,
Headers: map[string][]string{"Content-Type": {"application/octet-stream"}},
Body: req.Body,
}, nil
Expand Down
70 changes: 52 additions & 18 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,25 +66,52 @@ func TestHttpIngress(t *testing.T) {
run("ftl", rd.initOpts...),
scaffoldTestData(runtime, "httpingress", rd.modulePath),
run("ftl", "deploy", rd.moduleRoot),
httpCall(rd, http.MethodGet, "/users/123/posts/456", obj{}, func(t testing.TB, resp *httpResponse) {
httpCall(rd, http.MethodGet, "/users/123/posts/456", jsonData(t, obj{}), func(t testing.TB, resp *httpResponse) {
assert.Equal(t, 200, resp.status)
message, ok := resp.body["message"].(string)
assert.Equal(t, []string{"Header from FTL"}, resp.headers["Get"])

message, ok := resp.body["msg"].(string)
assert.True(t, ok, "message is not a string")
assert.Equal(t, "UserID: 123, PostID: 456", message)

nested, ok := resp.body["nested"].(map[string]any)
assert.True(t, ok, "nested is not a map")
goodStuff, ok := nested["good_stuff"].(string)
assert.True(t, ok, "good_stuff is not a string")
assert.Equal(t, "This is good stuff", goodStuff)
}),
httpCall(rd, http.MethodPost, "/users", obj{"userID": "123", "postID": "345"}, func(t testing.TB, resp *httpResponse) {
httpCall(rd, http.MethodPost, "/users", jsonData(t, obj{"userID": 123, "postID": 345}), func(t testing.TB, resp *httpResponse) {
assert.Equal(t, 201, resp.status)
assert.Equal(t, []string{"Header from FTL"}, resp.headers["Post"])
success, ok := resp.body["success"].(bool)
assert.True(t, ok, "success is not a bool")
assert.True(t, success)
}),
// contains aliased field
httpCall(rd, http.MethodPost, "/users", obj{"id": "123", "postID": "345"}, func(t testing.TB, resp *httpResponse) {
httpCall(rd, http.MethodPost, "/users", jsonData(t, obj{"user_id": 123, "postID": 345}), func(t testing.TB, resp *httpResponse) {
assert.Equal(t, 201, resp.status)
}),
httpCall(rd, http.MethodPut, "/users/123", obj{"postID": "346"}, func(t testing.TB, resp *httpResponse) {
httpCall(rd, http.MethodPut, "/users/123", jsonData(t, obj{"postID": "346"}), func(t testing.TB, resp *httpResponse) {
assert.Equal(t, 200, resp.status)
assert.Equal(t, []string{"Header from FTL"}, resp.headers["Put"])
assert.Equal(t, map[string]any{}, resp.body)
}),
httpCall(rd, http.MethodDelete, "/users/123", jsonData(t, obj{}), func(t testing.TB, resp *httpResponse) {
assert.Equal(t, 200, resp.status)
assert.Equal(t, []string{"Header from FTL"}, resp.headers["Delete"])
assert.Equal(t, map[string]any{}, resp.body)
}),
httpCall(rd, http.MethodDelete, "/users/123", obj{}, func(t testing.TB, resp *httpResponse) {

httpCall(rd, http.MethodGet, "/html", jsonData(t, obj{}), func(t testing.TB, resp *httpResponse) {
assert.Equal(t, 200, resp.status)
assert.Equal(t, []string{"text/html; charset=utf-8"}, resp.headers["Content-Type"])
assert.Equal(t, "<html><body><h1>HTML Page From FTL 🚀!</h1></body></html>", string(resp.bodyBytes))
}),

httpCall(rd, http.MethodPost, "/bytes", []byte("Hello, World!"), func(t testing.TB, resp *httpResponse) {
assert.Equal(t, 200, resp.status)
assert.Equal(t, []string{"application/octet-stream"}, resp.headers["Content-Type"])
assert.Equal(t, []byte("Hello, World!"), resp.bodyBytes)
}),
}},
}
Expand Down Expand Up @@ -338,19 +365,24 @@ func call[Resp any](module, verb string, req obj, onResponse func(t testing.TB,
}

type httpResponse struct {
status int
body map[string]any
status int
headers map[string][]string
body map[string]any
bodyBytes []byte
}

func httpCall(rd runtimeData, method string, path string, body obj, onResponse func(t testing.TB, resp *httpResponse)) assertion {
return func(t testing.TB, ic itContext) error {
b, err := json.Marshal(body)
assert.NoError(t, err)
func jsonData(t testing.TB, body obj) []byte {
b, err := json.Marshal(body)
assert.NoError(t, err)
return b
}

baseURL, err := url.Parse(fmt.Sprintf("http://localhost:8892/ingress/%s", rd.moduleName))
func httpCall(rd runtimeData, method string, path string, body []byte, onResponse func(t testing.TB, resp *httpResponse)) assertion {
return func(t testing.TB, ic itContext) error {
baseURL, err := url.Parse(fmt.Sprintf("http://localhost:8892/ingress"))
assert.NoError(t, err)

r, err := http.NewRequestWithContext(ic, method, baseURL.JoinPath(path).String(), bytes.NewReader(b))
r, err := http.NewRequestWithContext(ic, method, baseURL.JoinPath(path).String(), bytes.NewReader(body))
assert.NoError(t, err)

r.Header.Add("Content-Type", "application/json")
Expand All @@ -369,12 +401,14 @@ func httpCall(rd runtimeData, method string, path string, body obj, onResponse f
assert.NoError(t, err)

var resBody map[string]any
err = json.Unmarshal(bodyBytes, &resBody)
assert.NoError(t, err)
// ignore the error here since some responses are just `[]byte`.
_ = json.Unmarshal(bodyBytes, &resBody)

onResponse(t, &httpResponse{
status: resp.StatusCode,
body: resBody,
status: resp.StatusCode,
headers: resp.Header,
body: resBody,
bodyBytes: bodyBytes,
})
return nil
}
Expand Down
67 changes: 50 additions & 17 deletions integration/testdata/go/httpingress/echo.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,50 +9,61 @@ import (
)

type GetRequest struct {
UserID string `json:"userId"`
PostID string `json:"postId"`
UserID string `alias:"userId"`
PostID string `alias:"postId"`
}

type Nested struct {
GoodStuff string `alias:"good_stuff"`
}

type GetResponse struct {
Message string `json:"message"`
Message string `alias:"msg"`
Nested Nested `alias:"nested"`
}

//ftl:verb
//ftl:ingress http GET /echo/users/{userID}/posts/{postID}
//ftl:ingress http GET /users/{userId}/posts/{postId}
func Get(ctx context.Context, req builtin.HttpRequest[GetRequest]) (builtin.HttpResponse[GetResponse], error) {
return builtin.HttpResponse[GetResponse]{
Status: 200,
Headers: map[string][]string{"Get": {"Header from FTL"}},
Body: GetResponse{Message: fmt.Sprintf("UserID: %s, PostID: %s", req.Body.UserID, req.Body.PostID)},
Body: GetResponse{
Message: fmt.Sprintf("UserID: %s, PostID: %s", req.Body.UserID, req.Body.PostID),
Nested: Nested{
GoodStuff: "This is good stuff",
},
},
}, nil
}

type PostRequest struct {
UserID string `json:"userId" alias:"id"`
PostID string `json:"postId"`
UserID int `alias:"user_id"`
PostID int
}

type PostResponse struct{}
type PostResponse struct {
Success bool `alias:"success"`
}

//ftl:verb
//ftl:ingress http POST /echo/users
//ftl:ingress http POST /users
func Post(ctx context.Context, req builtin.HttpRequest[PostRequest]) (builtin.HttpResponse[PostResponse], error) {
return builtin.HttpResponse[PostResponse]{
Status: 201,
Headers: map[string][]string{"Post": {"Header from FTL"}},
Body: PostResponse{},
Body: PostResponse{Success: true},
}, nil
}

type PutRequest struct {
UserID string `json:"userId"`
PostID string `json:"postId"`
UserID string `alias:"userId"`
PostID string `alias:"postId"`
}

type PutResponse struct{}

//ftl:verb
//ftl:ingress http PUT /echo/users/{userID}
//ftl:ingress http PUT /users/{userID}
func Put(ctx context.Context, req builtin.HttpRequest[PutRequest]) (builtin.HttpResponse[PutResponse], error) {
return builtin.HttpResponse[PutResponse]{
Status: 200,
Expand All @@ -62,17 +73,39 @@ func Put(ctx context.Context, req builtin.HttpRequest[PutRequest]) (builtin.Http
}

type DeleteRequest struct {
UserID string `json:"userId"`
UserID string `alias:"userId"`
}

type DeleteResponse struct{}

//ftl:verb
//ftl:ingress http DELETE /echo/users/{userID}
//ftl:ingress http DELETE /users/{userId}
func Delete(ctx context.Context, req builtin.HttpRequest[DeleteRequest]) (builtin.HttpResponse[DeleteResponse], error) {
return builtin.HttpResponse[DeleteResponse]{
Status: 200,
Headers: map[string][]string{"Put": {"Header from FTL"}},
Headers: map[string][]string{"Delete": {"Header from FTL"}},
Body: DeleteResponse{},
}, nil
}

type HtmlRequest struct{}

//ftl:verb
//ftl:ingress http GET /html
func Html(ctx context.Context, req builtin.HttpRequest[HtmlRequest]) (builtin.HttpResponse[string], error) {
return builtin.HttpResponse[string]{
Status: 200,
Headers: map[string][]string{"Content-Type": {"text/html; charset=utf-8"}},
Body: "<html><body><h1>HTML Page From FTL 🚀!</h1></body></html>",
}, nil
}

//ftl:verb
//ftl:ingress http POST /bytes
func Bytes(ctx context.Context, req builtin.HttpRequest[[]byte]) (builtin.HttpResponse[[]byte], error) {
return builtin.HttpResponse[[]byte]{
Status: 200,
Headers: map[string][]string{"Content-Type": {"application/octet-stream"}},
Body: req.Body,
}, nil
}

0 comments on commit 1925465

Please sign in to comment.