From 7c575585e9e305a28cc138d778f3f0bef5a6909a Mon Sep 17 00:00:00 2001 From: Eyitayo Ogunbiyi Date: Mon, 6 Jul 2020 15:27:12 +0100 Subject: [PATCH 1/9] added conditional json flag when trying to view healthchecks --- admin.go | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/admin.go b/admin.go index 3e538a0e..c5ae686d 100644 --- a/admin.go +++ b/admin.go @@ -279,9 +279,7 @@ func profIndex(rw http.ResponseWriter, r *http.Request) { http.Error(rw, err.Error(), http.StatusInternalServerError) return } - - rw.Header().Set("Content-Type", "application/json") - rw.Write(dataJSON) + execJSON(rw, dataJSON) return } @@ -295,7 +293,7 @@ func profIndex(rw http.ResponseWriter, r *http.Request) { // Healthcheck is a http.Handler calling health checking and showing the result. // it's in "/healthcheck" pattern in admin module. -func healthcheck(rw http.ResponseWriter, _ *http.Request) { +func healthcheck(rw http.ResponseWriter, r *http.Request) { var ( result []string data = make(map[interface{}]interface{}) @@ -322,12 +320,44 @@ func healthcheck(rw http.ResponseWriter, _ *http.Request) { *resultList = append(*resultList, result) } + queryParams := r.URL.Query() + + if queryParams["json"] != nil { + + type Result map[string]interface{} + + response := make([]Result, len(*resultList)) + + for i, currentResult := range *resultList { + currentResultMap := make(Result) + currentResultMap["name"] = currentResult[0] + currentResultMap["message"] = currentResult[1] + currentResultMap["status"] = currentResult[2] + response[i] = currentResultMap + } + + JSONResponse, err := json.Marshal(response) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + } else { + execJSON(rw, JSONResponse) + } + + return + } + content["Data"] = resultList data["Content"] = content data["Title"] = "Health Check" + execTpl(rw, data, healthCheckTpl, defaultScriptsTpl) } +func execJSON(rw http.ResponseWriter, jsonData []byte) { + rw.Header().Set("Content-Type", "application/json") + rw.Write(jsonData) +} + // TaskStatus is a http.Handler with running task status (task name, status and the last execution). // it's in "/task" pattern in admin module. func taskStatus(rw http.ResponseWriter, req *http.Request) { From db547a7c84aa7957b65b84d33f92d79893d3c7ae Mon Sep 17 00:00:00 2001 From: Eyitayo Ogunbiyi Date: Mon, 6 Jul 2020 16:04:29 +0100 Subject: [PATCH 2/9] added test for execJson --- admin_test.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/admin_test.go b/admin_test.go index 71cc209e..b7a9af9c 100644 --- a/admin_test.go +++ b/admin_test.go @@ -1,7 +1,9 @@ package beego import ( + "encoding/json" "fmt" + "net/http/httptest" "testing" ) @@ -75,3 +77,27 @@ func oldMap() M { m["BConfig.Log.Outputs"] = BConfig.Log.Outputs return m } + +func TestExecJSON(t *testing.T) { + t.Log("Testing the adding of JSON to the response") + + w := httptest.NewRecorder() + originalBody := []int{1, 2, 3} + + res, _ := json.Marshal(originalBody) + + execJSON(w, res) + + decodedBody := []int{} + err := json.NewDecoder(w.Body).Decode(&decodedBody) + + if err != nil { + t.Fatal("Should be able to decode response body into decodedBody slice") + } + + for i := range decodedBody { + if decodedBody[i] != originalBody[i] { + t.Fatalf("Expected %d but got %d in decoded body slice", originalBody[i], decodedBody[i]) + } + } +} From 8d1a9bc92e758e6e174076fcae16ccebb4bc7fba Mon Sep 17 00:00:00 2001 From: Eyitayo Ogunbiyi Date: Mon, 6 Jul 2020 19:34:48 +0100 Subject: [PATCH 3/9] added tests for health check endpoints --- admin_test.go | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/admin_test.go b/admin_test.go index b7a9af9c..3875a4bb 100644 --- a/admin_test.go +++ b/admin_test.go @@ -2,11 +2,30 @@ package beego import ( "encoding/json" + "errors" "fmt" + "net/http" "net/http/httptest" + "strings" "testing" + + "github.com/astaxie/beego/toolbox" ) +type SampleDatabaseCheck struct { +} + +type SampleCacheCheck struct { +} + +func (dc *SampleDatabaseCheck) Check() error { + return nil +} + +func (cc *SampleCacheCheck) Check() error { + return errors.New("no cache detected") +} + func TestList_01(t *testing.T) { m := make(M) list("BConfig", BConfig, m) @@ -101,3 +120,57 @@ func TestExecJSON(t *testing.T) { } } } + +func TestHealthCheckHandlerDefault(t *testing.T) { + endpointPath := "/healthcheck" + + toolbox.AddHealthCheck("database", &SampleDatabaseCheck{}) + toolbox.AddHealthCheck("cache", &SampleCacheCheck{}) + + req, err := http.NewRequest("GET", endpointPath, nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(healthcheck) + + handler.ServeHTTP(w, req) + + if status := w.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusOK) + } + if !strings.Contains(w.Body.String(), "database") { + t.Errorf("Expected 'database' in generated template.") + } + +} + +func TestHealthCheckHandlerReturnsJSON(t *testing.T) { + + toolbox.AddHealthCheck("database", &SampleDatabaseCheck{}) + toolbox.AddHealthCheck("cache", &SampleCacheCheck{}) + + req, err := http.NewRequest("GET", "/healthcheck?json=true", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(healthcheck) + + handler.ServeHTTP(w, req) + if status := w.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusOK) + } + + expectedResponseBody := `[{"message":"database","name":"success","status":"OK"},{"message":"cache","name":"error","status":"no cache detected"}]` + if w.Body.String() != expectedResponseBody { + t.Errorf("handler returned unexpected body: got %v want %v", + w.Body.String(), expectedResponseBody) + } +} From 5a4a082af07dbecb996627e544e09fc32b097521 Mon Sep 17 00:00:00 2001 From: Eyitayo Ogunbiyi Date: Tue, 7 Jul 2020 14:54:21 +0100 Subject: [PATCH 4/9] renamed functions for clarity --- admin.go | 22 +++++++++++----------- admin_test.go | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/admin.go b/admin.go index c5ae686d..cc9043e7 100644 --- a/admin.go +++ b/admin.go @@ -71,7 +71,7 @@ func init() { // AdminIndex is the default http.Handler for admin module. // it matches url pattern "/". func adminIndex(rw http.ResponseWriter, _ *http.Request) { - execTpl(rw, map[interface{}]interface{}{}, indexTpl, defaultScriptsTpl) + writeTemplate(rw, map[interface{}]interface{}{}, indexTpl, defaultScriptsTpl) } // QpsIndex is the http.Handler for writing qps statistics map result info in http.ResponseWriter. @@ -91,7 +91,7 @@ func qpsIndex(rw http.ResponseWriter, _ *http.Request) { } } - execTpl(rw, data, qpsTpl, defaultScriptsTpl) + writeTemplate(rw, data, qpsTpl, defaultScriptsTpl) } // ListConf is the http.Handler of displaying all beego configuration values as key/value pair. @@ -128,7 +128,7 @@ func listConf(rw http.ResponseWriter, r *http.Request) { } data["Content"] = content data["Title"] = "Routers" - execTpl(rw, data, routerAndFilterTpl, defaultScriptsTpl) + writeTemplate(rw, data, routerAndFilterTpl, defaultScriptsTpl) case "filter": var ( content = M{ @@ -171,7 +171,7 @@ func listConf(rw http.ResponseWriter, r *http.Request) { data["Content"] = content data["Title"] = "Filters" - execTpl(rw, data, routerAndFilterTpl, defaultScriptsTpl) + writeTemplate(rw, data, routerAndFilterTpl, defaultScriptsTpl) default: rw.Write([]byte("command not support")) } @@ -279,7 +279,7 @@ func profIndex(rw http.ResponseWriter, r *http.Request) { http.Error(rw, err.Error(), http.StatusInternalServerError) return } - execJSON(rw, dataJSON) + writeJSON(rw, dataJSON) return } @@ -288,7 +288,7 @@ func profIndex(rw http.ResponseWriter, r *http.Request) { if command == "gc summary" { defaultTpl = gcAjaxTpl } - execTpl(rw, data, profillingTpl, defaultTpl) + writeTemplate(rw, data, profillingTpl, defaultTpl) } // Healthcheck is a http.Handler calling health checking and showing the result. @@ -340,7 +340,7 @@ func healthcheck(rw http.ResponseWriter, r *http.Request) { if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) } else { - execJSON(rw, JSONResponse) + writeJSON(rw, JSONResponse) } return @@ -350,10 +350,10 @@ func healthcheck(rw http.ResponseWriter, r *http.Request) { data["Content"] = content data["Title"] = "Health Check" - execTpl(rw, data, healthCheckTpl, defaultScriptsTpl) + writeTemplate(rw, data, healthCheckTpl, defaultScriptsTpl) } -func execJSON(rw http.ResponseWriter, jsonData []byte) { +func writeJSON(rw http.ResponseWriter, jsonData []byte) { rw.Header().Set("Content-Type", "application/json") rw.Write(jsonData) } @@ -401,10 +401,10 @@ func taskStatus(rw http.ResponseWriter, req *http.Request) { content["Data"] = resultList data["Content"] = content data["Title"] = "Tasks" - execTpl(rw, data, tasksTpl, defaultScriptsTpl) + writeTemplate(rw, data, tasksTpl, defaultScriptsTpl) } -func execTpl(rw http.ResponseWriter, data map[interface{}]interface{}, tpls ...string) { +func writeTemplate(rw http.ResponseWriter, data map[interface{}]interface{}, tpls ...string) { tmpl := template.Must(template.New("dashboard").Parse(dashboardTpl)) for _, tpl := range tpls { tmpl = template.Must(tmpl.Parse(tpl)) diff --git a/admin_test.go b/admin_test.go index 3875a4bb..1bf8700a 100644 --- a/admin_test.go +++ b/admin_test.go @@ -97,7 +97,7 @@ func oldMap() M { return m } -func TestExecJSON(t *testing.T) { +func TestWriteJSON(t *testing.T) { t.Log("Testing the adding of JSON to the response") w := httptest.NewRecorder() @@ -105,7 +105,7 @@ func TestExecJSON(t *testing.T) { res, _ := json.Marshal(originalBody) - execJSON(w, res) + writeJSON(w, res) decodedBody := []int{} err := json.NewDecoder(w.Body).Decode(&decodedBody) From ca0c64b69e3357ff8ce94a0e52f445bdf471d4b5 Mon Sep 17 00:00:00 2001 From: Eyitayo Ogunbiyi Date: Tue, 7 Jul 2020 15:21:38 +0100 Subject: [PATCH 5/9] refactored tests for health check endpoint --- admin_test.go | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/admin_test.go b/admin_test.go index 1bf8700a..4a614721 100644 --- a/admin_test.go +++ b/admin_test.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "reflect" "strings" "testing" @@ -111,7 +112,7 @@ func TestWriteJSON(t *testing.T) { err := json.NewDecoder(w.Body).Decode(&decodedBody) if err != nil { - t.Fatal("Should be able to decode response body into decodedBody slice") + t.Fatal("Could not decode response body into slice.") } for i := range decodedBody { @@ -168,9 +169,31 @@ func TestHealthCheckHandlerReturnsJSON(t *testing.T) { status, http.StatusOK) } - expectedResponseBody := `[{"message":"database","name":"success","status":"OK"},{"message":"cache","name":"error","status":"no cache detected"}]` - if w.Body.String() != expectedResponseBody { + decodedResponseBody := []map[string]interface{}{} + expectedResponseBody := []map[string]interface{}{} + + expectedJSONString := []byte(` + [ + { + "message":"database", + "name":"success", + "status":"OK" + }, + { + "message":"cache", + "name":"error", + "status":"no cache detected" + } + ] + `) + + json.Unmarshal(expectedJSONString, &expectedResponseBody) + + json.Unmarshal(w.Body.Bytes(), &decodedResponseBody) + + if !reflect.DeepEqual(decodedResponseBody, expectedResponseBody) { t.Errorf("handler returned unexpected body: got %v want %v", - w.Body.String(), expectedResponseBody) + decodedResponseBody, expectedResponseBody) } + } From 469dc7bea9b08add170177cc0a8615dbd0efa944 Mon Sep 17 00:00:00 2001 From: Eyitayo Ogunbiyi Date: Tue, 7 Jul 2020 16:09:22 +0100 Subject: [PATCH 6/9] refactored the building of healthcheck response map --- admin.go | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/admin.go b/admin.go index cc9043e7..2ee415d4 100644 --- a/admin.go +++ b/admin.go @@ -21,6 +21,7 @@ import ( "net/http" "os" "reflect" + "strconv" "text/template" "time" @@ -321,28 +322,18 @@ func healthcheck(rw http.ResponseWriter, r *http.Request) { } queryParams := r.URL.Query() + jsonFlag := queryParams.Get("json") + shouldReturnJSON, _ := strconv.ParseBool(jsonFlag) - if queryParams["json"] != nil { + if shouldReturnJSON { + responseMap := buildHealthCheckResponseMap(resultList) + jsonResponse, err := json.Marshal(responseMap) - type Result map[string]interface{} - - response := make([]Result, len(*resultList)) - - for i, currentResult := range *resultList { - currentResultMap := make(Result) - currentResultMap["name"] = currentResult[0] - currentResultMap["message"] = currentResult[1] - currentResultMap["status"] = currentResult[2] - response[i] = currentResultMap - } - - JSONResponse, err := json.Marshal(response) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) } else { - writeJSON(rw, JSONResponse) + writeJSON(rw, jsonResponse) } - return } @@ -353,6 +344,23 @@ func healthcheck(rw http.ResponseWriter, r *http.Request) { writeTemplate(rw, data, healthCheckTpl, defaultScriptsTpl) } +func buildHealthCheckResponseMap(resultList *[][]string) []map[string]interface{} { + response := make([]map[string]interface{}, len(*resultList)) + + for i, currentResult := range *resultList { + currentResultMap := make(map[string]interface{}) + + currentResultMap["name"] = currentResult[0] + currentResultMap["message"] = currentResult[1] + currentResultMap["status"] = currentResult[2] + + response[i] = currentResultMap + } + + return response + +} + func writeJSON(rw http.ResponseWriter, jsonData []byte) { rw.Header().Set("Content-Type", "application/json") rw.Write(jsonData) From e0f8c6832d5477e7b239b60ce25db4b42f01ed49 Mon Sep 17 00:00:00 2001 From: Eyitayo Ogunbiyi Date: Tue, 7 Jul 2020 16:28:16 +0100 Subject: [PATCH 7/9] added test for buildingHealthCheckResponse --- admin.go | 16 ++++++++-------- admin_test.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/admin.go b/admin.go index 2ee415d4..db52647e 100644 --- a/admin.go +++ b/admin.go @@ -326,8 +326,8 @@ func healthcheck(rw http.ResponseWriter, r *http.Request) { shouldReturnJSON, _ := strconv.ParseBool(jsonFlag) if shouldReturnJSON { - responseMap := buildHealthCheckResponseMap(resultList) - jsonResponse, err := json.Marshal(responseMap) + response := buildHealthCheckResponseList(resultList) + jsonResponse, err := json.Marshal(response) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) @@ -344,15 +344,15 @@ func healthcheck(rw http.ResponseWriter, r *http.Request) { writeTemplate(rw, data, healthCheckTpl, defaultScriptsTpl) } -func buildHealthCheckResponseMap(resultList *[][]string) []map[string]interface{} { - response := make([]map[string]interface{}, len(*resultList)) +func buildHealthCheckResponseList(healthCheckResults *[][]string) []map[string]interface{} { + response := make([]map[string]interface{}, len(*healthCheckResults)) - for i, currentResult := range *resultList { + for i, healthCheckResult := range *healthCheckResults { currentResultMap := make(map[string]interface{}) - currentResultMap["name"] = currentResult[0] - currentResultMap["message"] = currentResult[1] - currentResultMap["status"] = currentResult[2] + currentResultMap["name"] = healthCheckResult[0] + currentResultMap["message"] = healthCheckResult[1] + currentResultMap["status"] = healthCheckResult[2] response[i] = currentResultMap } diff --git a/admin_test.go b/admin_test.go index 4a614721..24da3a2b 100644 --- a/admin_test.go +++ b/admin_test.go @@ -149,6 +149,41 @@ func TestHealthCheckHandlerDefault(t *testing.T) { } +func TestBuildHealthCheckResponseList(t *testing.T) { + healthCheckResults := [][]string{ + []string{ + "error", + "Database", + "Error occured whie starting the db", + }, + []string{ + "success", + "Cache", + "Cache started successfully", + }, + } + + responseList := buildHealthCheckResponseList(&healthCheckResults) + + if len(responseList) != len(healthCheckResults) { + t.Errorf("invalid response map length: got %d want %d", + len(responseList), len(healthCheckResults)) + } + + responseFields := []string{"name", "message", "status"} + + for _, response := range responseList { + for _, field := range responseFields { + _, ok := response[field] + if !ok { + t.Errorf("expected %s to be in the response %v", field, response) + } + } + + } + +} + func TestHealthCheckHandlerReturnsJSON(t *testing.T) { toolbox.AddHealthCheck("database", &SampleDatabaseCheck{}) From 728bf340064803d5ace628c07f715a2532c1311c Mon Sep 17 00:00:00 2001 From: Eyitayo Ogunbiyi Date: Tue, 7 Jul 2020 16:46:59 +0100 Subject: [PATCH 8/9] refacted cache health check from toolbox --- admin_test.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/admin_test.go b/admin_test.go index 24da3a2b..d9a66b34 100644 --- a/admin_test.go +++ b/admin_test.go @@ -187,7 +187,6 @@ func TestBuildHealthCheckResponseList(t *testing.T) { func TestHealthCheckHandlerReturnsJSON(t *testing.T) { toolbox.AddHealthCheck("database", &SampleDatabaseCheck{}) - toolbox.AddHealthCheck("cache", &SampleCacheCheck{}) req, err := http.NewRequest("GET", "/healthcheck?json=true", nil) if err != nil { @@ -213,11 +212,6 @@ func TestHealthCheckHandlerReturnsJSON(t *testing.T) { "message":"database", "name":"success", "status":"OK" - }, - { - "message":"cache", - "name":"error", - "status":"no cache detected" } ] `) From d7b0d55357dce068c8b216fc49d024071f865f42 Mon Sep 17 00:00:00 2001 From: Eyitayo Ogunbiyi Date: Tue, 7 Jul 2020 17:23:52 +0100 Subject: [PATCH 9/9] added extra check for same response lengths --- admin_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/admin_test.go b/admin_test.go index d9a66b34..3f3612e4 100644 --- a/admin_test.go +++ b/admin_test.go @@ -187,6 +187,7 @@ func TestBuildHealthCheckResponseList(t *testing.T) { func TestHealthCheckHandlerReturnsJSON(t *testing.T) { toolbox.AddHealthCheck("database", &SampleDatabaseCheck{}) + toolbox.AddHealthCheck("cache", &SampleCacheCheck{}) req, err := http.NewRequest("GET", "/healthcheck?json=true", nil) if err != nil { @@ -212,6 +213,11 @@ func TestHealthCheckHandlerReturnsJSON(t *testing.T) { "message":"database", "name":"success", "status":"OK" + }, + { + "message":"cache", + "name":"error", + "status":"no cache detected" } ] `) @@ -220,6 +226,11 @@ func TestHealthCheckHandlerReturnsJSON(t *testing.T) { json.Unmarshal(w.Body.Bytes(), &decodedResponseBody) + if len(expectedResponseBody) != len(decodedResponseBody) { + t.Errorf("invalid response map length: got %d want %d", + len(decodedResponseBody), len(expectedResponseBody)) + } + if !reflect.DeepEqual(decodedResponseBody, expectedResponseBody) { t.Errorf("handler returned unexpected body: got %v want %v", decodedResponseBody, expectedResponseBody)