Openstatus www.openstatus.dev

🧪 (#1010)

authored by

Thibault Le Ouay and committed by
GitHub
b1d3d9c7 e4dc94e9

+305 -11
+6 -4
apps/checker/handlers/checker.go
··· 8 8 9 9 "github.com/cenkalti/backoff/v4" 10 10 "github.com/gin-gonic/gin" 11 + "github.com/rs/zerolog/log" 12 + 11 13 "github.com/openstatushq/openstatus/apps/checker" 12 14 "github.com/openstatushq/openstatus/apps/checker/pkg/assertions" 13 15 "github.com/openstatushq/openstatus/apps/checker/request" 14 - "github.com/rs/zerolog/log" 15 16 ) 16 17 17 18 type statusCode int ··· 134 135 if err := json.Unmarshal(a, &target); err != nil { 135 136 return fmt.Errorf("unable to unmarshal IntTarget: %w", err) 136 137 } 137 - isSuccessfull = isSuccessfull && target.HeaderEvaluate(data.Headers) 138 138 139 + isSuccessfull = isSuccessfull && target.HeaderEvaluate(data.Headers) 139 140 case request.AssertionTextBody: 140 141 var target assertions.StringTargetType 141 142 if err := json.Unmarshal(a, &target); err != nil { 142 143 return fmt.Errorf("unable to unmarshal IntTarget: %w", err) 143 144 } 145 + 144 146 isSuccessfull = isSuccessfull && target.StringEvaluate(data.Body) 145 - 146 147 case request.AssertionStatus: 147 148 var target assertions.StatusTarget 148 149 if err := json.Unmarshal(a, &target); err != nil { 149 150 return fmt.Errorf("unable to unmarshal IntTarget: %w", err) 150 151 } 152 + 151 153 isSuccessfull = isSuccessfull && target.StatusEvaluate(int64(res.Status)) 152 154 case request.AssertionJsonBody: 153 155 fmt.Println("assertion type", assert.AssertionType) ··· 174 176 } 175 177 176 178 data.Assertions = assertionAsString 177 - // That part could be refactored 179 + 178 180 if !isSuccessfull && req.Status == "active" { 179 181 // Q: Why here we do not check if the status was previously active? 180 182 checker.UpdateStatus(ctx, checker.UpdateData{
+108
apps/checker/handlers/checker_test.go
··· 1 + package handlers_test 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "io" 7 + "net/http" 8 + "net/http/httptest" 9 + "strings" 10 + "testing" 11 + 12 + "github.com/gin-gonic/gin" 13 + "github.com/openstatushq/openstatus/apps/checker/handlers" 14 + "github.com/openstatushq/openstatus/apps/checker/pkg/tinybird" 15 + "github.com/openstatushq/openstatus/apps/checker/request" 16 + "github.com/stretchr/testify/assert" 17 + ) 18 + 19 + func TestHandler_HTTPCheckerHandler(t *testing.T) { 20 + hclient := &http.Client{Transport: RoundTripFunc(func(req *http.Request) *http.Response { 21 + return &http.Response{ 22 + StatusCode: http.StatusAccepted, 23 + Body: io.NopCloser(strings.NewReader(`Status Accepted`)), 24 + } 25 + })} 26 + client := tinybird.NewClient(hclient, "apiKey") 27 + 28 + t.Run("it should return 401 if there's no auth", func(t *testing.T) { 29 + 30 + region := "local" 31 + h := handlers.Handler{ 32 + TbClient: client, 33 + Secret: "", 34 + CloudProvider: "fly", 35 + Region: region, 36 + } 37 + router := gin.New() 38 + router.POST("/checker/:region", h.HTTPCheckerHandler) 39 + 40 + w := httptest.NewRecorder() 41 + 42 + data := request.HttpCheckerRequest{ 43 + URL: "https://www.openstatus.dev", 44 + } 45 + dataJson, _ := json.Marshal(data) 46 + req, _ := http.NewRequest(http.MethodPost, "/checker/"+region, strings.NewReader(string(dataJson))) 47 + router.ServeHTTP(w, req) 48 + 49 + assert.Equal(t, 401, w.Code) 50 + }) 51 + 52 + t.Run("it should return 400 if the payload is not ok", func(t *testing.T) { 53 + region := "local" 54 + 55 + h := handlers.Handler{ 56 + TbClient: client, 57 + Secret: "test", 58 + CloudProvider: "fly", 59 + Region: region, 60 + } 61 + router := gin.New() 62 + router.POST("/checker/:region", h.HTTPCheckerHandler) 63 + 64 + w := httptest.NewRecorder() 65 + 66 + data := request.PingRequest{ 67 + URL: "https://www.openstatus.dev", 68 + } 69 + dataJson, _ := json.Marshal(data) 70 + req, _ := http.NewRequest(http.MethodPost, "/checker/"+region, strings.NewReader(string(dataJson))) 71 + req.Header.Set("Authorization", "Basic test") 72 + router.ServeHTTP(w, req) 73 + 74 + assert.Equal(t, 400, w.Code) 75 + assert.Contains(t, w.Body.String(), "{\"error\":\"invalid request\"}") 76 + }) 77 + 78 + t.Run("it should return 200 if the payload is not ok", func(t *testing.T) { 79 + region := "local" 80 + 81 + httptest.NewRequest(http.MethodGet, "http://www.openstatus.dev", nil) 82 + httptest.NewRecorder() 83 + 84 + h := handlers.Handler{ 85 + TbClient: client, 86 + Secret: "test", 87 + CloudProvider: "fly", 88 + Region: region, 89 + } 90 + router := gin.New() 91 + router.POST("/checker/:region", h.HTTPCheckerHandler) 92 + 93 + w := httptest.NewRecorder() 94 + 95 + data := request.HttpCheckerRequest{ 96 + URL: "https://www.openstatus.dev", 97 + Method: "GET", 98 + Body: "", 99 + } 100 + dataJson, _ := json.Marshal(data) 101 + req, _ := http.NewRequest(http.MethodPost, "/checker/"+region, strings.NewReader(string(dataJson))) 102 + req.Header.Set("Authorization", "Basic test") 103 + router.ServeHTTP(w, req) 104 + 105 + assert.Equal(t, 200, w.Code) 106 + fmt.Println(w.Body.String()) 107 + }) 108 + }
+6
apps/checker/handlers/handler.go
··· 1 1 package handlers 2 2 3 3 import ( 4 + "net/http" 5 + 4 6 "github.com/openstatushq/openstatus/apps/checker/pkg/tinybird" 5 7 ) 6 8 ··· 12 14 } 13 15 14 16 // Authorization could be handle by middleware 17 + 18 + func NewHTTPClient() *http.Client { 19 + return &http.Client{} 20 + }
+1
apps/checker/handlers/ping.go
··· 72 72 requestClient := &http.Client{ 73 73 Timeout: 45 * time.Second, 74 74 } 75 + 75 76 defer requestClient.CloseIdleConnections() 76 77 77 78 var req request.PingRequest
+117
apps/checker/handlers/ping_test.go
··· 1 + package handlers_test 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "io" 7 + "net/http" 8 + "net/http/httptest" 9 + "strings" 10 + "testing" 11 + 12 + "github.com/openstatushq/openstatus/apps/checker/handlers" 13 + "github.com/openstatushq/openstatus/apps/checker/pkg/tinybird" 14 + "github.com/openstatushq/openstatus/apps/checker/request" 15 + 16 + "github.com/gin-gonic/gin" 17 + "github.com/stretchr/testify/assert" 18 + ) 19 + 20 + type RoundTripFunc func(req *http.Request) *http.Response 21 + 22 + func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { 23 + return f(req), nil 24 + } 25 + 26 + func TestHandler_PingRegion(t *testing.T) { 27 + 28 + hclient := &http.Client{Transport: RoundTripFunc(func(req *http.Request) *http.Response { 29 + return &http.Response{ 30 + StatusCode: http.StatusAccepted, 31 + Body: io.NopCloser(strings.NewReader(`Status Accepted`)), 32 + } 33 + })} 34 + client := tinybird.NewClient(hclient, "apiKey") 35 + 36 + t.Run("it should return 401 if there's no auth", func(t *testing.T) { 37 + 38 + region := "local" 39 + h := handlers.Handler{ 40 + TbClient: client, 41 + Secret: "", 42 + CloudProvider: "fly", 43 + Region: region, 44 + } 45 + router := gin.New() 46 + router.POST("/checker/:region", h.PingRegionHandler) 47 + 48 + w := httptest.NewRecorder() 49 + 50 + data := request.PingRequest{ 51 + URL: "https://www.openstatus.dev", 52 + } 53 + dataJson, _ := json.Marshal(data) 54 + req, _ := http.NewRequest(http.MethodPost, "/checker/"+region, strings.NewReader(string(dataJson))) 55 + router.ServeHTTP(w, req) 56 + 57 + assert.Equal(t, 401, w.Code) 58 + }) 59 + 60 + t.Run("it should return 400 if the payload is not ok", func(t *testing.T) { 61 + region := "local" 62 + 63 + h := handlers.Handler{ 64 + TbClient: client, 65 + Secret: "test", 66 + CloudProvider: "fly", 67 + Region: region, 68 + } 69 + router := gin.New() 70 + router.POST("/checker/:region", h.PingRegionHandler) 71 + 72 + w := httptest.NewRecorder() 73 + 74 + data := request.HttpCheckerRequest{ 75 + URL: "https://www.openstatus.dev", 76 + } 77 + dataJson, _ := json.Marshal(data) 78 + req, _ := http.NewRequest(http.MethodPost, "/checker/"+region, strings.NewReader(string(dataJson))) 79 + req.Header.Set("Authorization", "Basic test") 80 + router.ServeHTTP(w, req) 81 + 82 + assert.Equal(t, 400, w.Code) 83 + assert.Contains(t, w.Body.String(), "{\"error\":\"invalid request\"}") 84 + }) 85 + 86 + t.Run("it should return 200 if the payload is ok", func(t *testing.T) { 87 + region := "local" 88 + 89 + httptest.NewRequest(http.MethodGet, "http://www.openstatus.dev", nil) 90 + httptest.NewRecorder() 91 + 92 + h := handlers.Handler{ 93 + TbClient: client, 94 + Secret: "test", 95 + CloudProvider: "fly", 96 + Region: region, 97 + } 98 + router := gin.New() 99 + router.POST("/checker/:region", h.PingRegionHandler) 100 + 101 + w := httptest.NewRecorder() 102 + 103 + data := request.PingRequest{ 104 + URL: "https://www.openstatus.dev", 105 + Method: "GET", 106 + Headers: map[string]string{}, 107 + Body: "", 108 + } 109 + dataJson, _ := json.Marshal(data) 110 + req, _ := http.NewRequest(http.MethodPost, "/checker/"+region, strings.NewReader(string(dataJson))) 111 + req.Header.Set("Authorization", "Basic test") 112 + router.ServeHTTP(w, req) 113 + 114 + assert.Equal(t, 200, w.Code) 115 + fmt.Println(w.Body.String()) 116 + }) 117 + }
+13
apps/checker/handlers/tcp.go
··· 25 25 func (h Handler) TCPHandler(c *gin.Context) { 26 26 ctx := c.Request.Context() 27 27 dataSourceName := "tcp_response__v0" 28 + 28 29 if c.GetHeader("Authorization") != fmt.Sprintf("Basic %s", h.Secret) { 29 30 c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) 31 + 30 32 return 31 33 } 32 34 ··· 36 38 if region != "" && region != h.Region { 37 39 c.Header("fly-replay", fmt.Sprintf("region=%s", region)) 38 40 c.String(http.StatusAccepted, "Forwarding request to %s", region) 41 + 39 42 return 40 43 } 41 44 } 45 + 42 46 var req request.TCPCheckerRequest 43 47 if err := c.ShouldBindJSON(&req); err != nil { 44 48 log.Ctx(ctx).Error().Err(err).Msg("failed to decode checker request") 45 49 c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) 50 + 46 51 return 47 52 } 48 53 workspaceId, err := strconv.ParseInt(req.WorkspaceID, 10, 64) 54 + 49 55 if err != nil { 50 56 c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) 57 + 51 58 return 52 59 } 53 60 monitorId, err := strconv.ParseInt(req.MonitorID, 10, 64) 61 + 54 62 if err != nil { 55 63 c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) 64 + 56 65 return 57 66 } 58 67 ··· 60 69 op := func() error { 61 70 called++ 62 71 res, err := checker.PingTcp(int(req.Timeout), req.URL) 72 + 63 73 if err != nil { 64 74 return fmt.Errorf("unable to check tcp %s", err) 65 75 } ··· 75 85 MonitorID: monitorId, 76 86 } 77 87 latency := res.TCPDone - res.TCPStart 88 + 78 89 if req.Status == "active" && req.DegradedAfter > 0 && latency > req.DegradedAfter { 79 90 checker.UpdateStatus(ctx, checker.UpdateData{ 80 91 MonitorId: req.MonitorID, ··· 83 94 CronTimestamp: req.CronTimestamp, 84 95 }) 85 96 } 97 + 86 98 if req.Status == "degraded" && req.DegradedAfter > 0 && latency <= req.DegradedAfter { 87 99 checker.UpdateStatus(ctx, checker.UpdateData{ 88 100 MonitorId: req.MonitorID, ··· 129 141 }) 130 142 } 131 143 } 144 + 132 145 c.JSON(http.StatusOK, gin.H{"message": "ok"}) 133 146 } 134 147
+54 -7
apps/checker/http_test.go
··· 1 1 package checker_test 2 2 3 3 import ( 4 + "bytes" 4 5 "context" 6 + "io" 5 7 "net/http" 6 8 "testing" 9 + 10 + "github.com/stretchr/testify/assert" 7 11 8 12 "github.com/openstatushq/openstatus/apps/checker" 9 13 "github.com/openstatushq/openstatus/apps/checker/request" 10 14 ) 11 15 16 + // RoundTripFunc . 17 + type RoundTripFunc func(req *http.Request) *http.Response 18 + 19 + // RoundTrip . 20 + func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { 21 + return f(req), nil 22 + } 23 + 24 + // NewTestClient returns *http.Client with Transport replaced to avoid making real calls 25 + func NewTestClient(fn RoundTripFunc) *http.Client { 26 + return &http.Client{ 27 + Transport: RoundTripFunc(fn), 28 + } 29 + } 30 + 12 31 func Test_ping(t *testing.T) { 32 + 13 33 type args struct { 14 34 client *http.Client 15 35 inputData request.HttpCheckerRequest ··· 20 40 want checker.Response 21 41 wantErr bool 22 42 }{ 23 - {name: "200", args: args{client: &http.Client{}, inputData: request.HttpCheckerRequest{URL: "https://openstat.us", CronTimestamp: 1, Headers: []struct { 43 + {name: "200", args: args{client: NewTestClient(func(req *http.Request) *http.Response { 44 + return &http.Response{ 45 + StatusCode: http.StatusOK, 46 + Body: io.NopCloser(bytes.NewBufferString(`OK`)), 47 + Header: make(http.Header), 48 + } 49 + }), inputData: request.HttpCheckerRequest{URL: "https://openstat.us", CronTimestamp: 1, Headers: []struct { 24 50 Key string `json:"key"` 25 51 Value string `json:"value"` 26 - }{{Key: "", Value: ""}}}}, want: checker.Response{Status: 200}, wantErr: false}, 27 - {name: "200", args: args{client: &http.Client{}, inputData: request.HttpCheckerRequest{URL: "https://openstat.us", CronTimestamp: 1, Headers: []struct { 52 + }{{Key: "", Value: ""}}}}, want: checker.Response{Status: 200, Body: "OK"}, wantErr: false}, 53 + 54 + {name: "200 with headers", args: args{client: NewTestClient(func(req *http.Request) *http.Response { 55 + assert.Equal(t, "Value", req.Header.Get("Test")) 56 + return &http.Response{ 57 + StatusCode: http.StatusOK, 58 + Body: io.NopCloser(bytes.NewBufferString(`OK`)), 59 + Header: make(http.Header), 60 + } 61 + }), inputData: request.HttpCheckerRequest{URL: "https://openstat.us", CronTimestamp: 1, Headers: []struct { 28 62 Key string `json:"key"` 29 63 Value string `json:"value"` 30 - }{{Key: "Test", Value: ""}}}}, want: checker.Response{Status: 200}, wantErr: false}, 31 - {name: "500", args: args{client: &http.Client{}, inputData: request.HttpCheckerRequest{URL: "https://openstat.us/500", CronTimestamp: 1}}, want: checker.Response{Status: 500}, wantErr: false}, 32 - {name: "500", args: args{client: &http.Client{}, inputData: request.HttpCheckerRequest{URL: "https://somethingthatwillfail.ed", CronTimestamp: 1}}, want: checker.Response{Status: 0}, wantErr: true}, 64 + }{{Key: "Test", Value: "Value"}}}}, want: checker.Response{Status: 200, Body: "OK"}, wantErr: false}, 33 65 34 - // TODO: Add test cases. 66 + {name: "500", args: args{client: NewTestClient(func(req *http.Request) *http.Response { 67 + return &http.Response{ 68 + StatusCode: http.StatusInternalServerError, 69 + Body: io.NopCloser(bytes.NewBufferString(`OK`)), 70 + Header: make(http.Header), 71 + } 72 + }), inputData: request.HttpCheckerRequest{URL: "https://openstat.us/500", CronTimestamp: 1}}, 73 + want: checker.Response{Status: 500, Body: "OK"}, wantErr: false}, 74 + 75 + {name: "Wrong url should return an error", args: args{client: &http.Client{}, inputData: request.HttpCheckerRequest{URL: "https://somethingthatwillfail.ed", CronTimestamp: 1}}, 76 + want: checker.Response{Status: 0}, wantErr: true}, 35 77 } 36 78 for _, tt := range tests { 37 79 t.Run(tt.name, func(t *testing.T) { ··· 41 83 t.Errorf("Ping() error = %v, wantErr %v", err, tt.wantErr) 42 84 return 43 85 } 86 + 44 87 if got.Status != tt.want.Status { 88 + t.Errorf("Ping() = %v, want %v", got, tt.want) 89 + } 90 + 91 + if got.Body != tt.want.Body { 45 92 t.Errorf("Ping() = %v, want %v", got, tt.want) 46 93 } 47 94 })