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

chore: add more httpingress integration tests #868

Merged
merged 2 commits into from
Feb 2, 2024
Merged
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
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if you make this a []byte then json.Unmarshal will Just Work

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
}