Skip to content

Commit

Permalink
added error handler, small refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
sgmv committed Sep 9, 2024
1 parent 9678fcc commit 92c4ad6
Show file tree
Hide file tree
Showing 11 changed files with 350 additions and 63 deletions.
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ module github.com/stackmon/otc-status-dashboard
go 1.22

require (
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/gin-gonic/gin v1.10.0
github.com/joho/godotenv v1.5.1
github.com/kelseyhightower/envconfig v1.4.0
github.com/stretchr/testify v1.9.0
go.uber.org/zap v1.27.0
gorm.io/driver/postgres v1.5.9
gorm.io/gorm v1.25.11
Expand All @@ -16,6 +18,7 @@ require (
github.com/bytedance/sonic/loader v0.2.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
Expand All @@ -35,6 +38,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/bytedance/sonic v1.12.1 h1:jWl5Qz1fy7X1ioY74WqO0KjAMtAGQs4sYnjiEBiyX24=
github.com/bytedance/sonic v1.12.1/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
Expand Down Expand Up @@ -47,6 +49,7 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
Expand Down
20 changes: 18 additions & 2 deletions internal/app/errors.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
package app

import "fmt"
import (
"errors"
)

var ErrComponentValidation = fmt.Errorf("component value validation error")
func ReturnError(err error) error {
return &ErrorMsg{ErrMsg: err.Error()}
}

type ErrorMsg struct {

Check failure on line 11 in internal/app/errors.go

View workflow job for this annotation

GitHub Actions / lint

the type name `ErrorMsg` should conform to the `XxxError` format (errname)
ErrMsg string `json:"errMsg"`
}

func (e *ErrorMsg) Error() string {
return e.ErrMsg
}

var ErrPageNotFound = errors.New("page not found")

var ErrComponentIsNotPresent = errors.New("component is not present")
101 changes: 92 additions & 9 deletions internal/app/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,28 @@ func (a *App) GetIncidentsHandler(c *gin.Context) {
c.AbortWithError(http.StatusInternalServerError, err) //nolint:nolintlint,errcheck
return
}
c.JSON(http.StatusOK, gin.H{"data": r})
incidents := make([]*Incident, len(r))
for i, inc := range r {
components := make([]int, len(inc.Components))
for ind, comp := range inc.Components {
components[ind] = int(comp.ID)
}

incidents[i] = &Incident{
IncidentID: IncidentID{int(inc.ID)},
IncidentData: IncidentData{
Title: *inc.Text,
Impact: inc.Impact,
Components: components,
StartDate: *inc.StartDate,
EndDate: inc.EndDate,
System: *inc.System,
Updates: inc.Statuses,
},
}
}

c.JSON(http.StatusOK, gin.H{"data": incidents})
}

func (a *App) GetIncidentHandler(c *gin.Context) {
Expand All @@ -63,12 +84,12 @@ func (a *App) GetIncidentHandler(c *gin.Context) {
}

incData := IncidentData{
Title: r.Text,
Impact: &r.Impact,
Title: *r.Text,
Impact: r.Impact,
Components: components,
StartDate: r.StartDate,
StartDate: *r.StartDate,
EndDate: r.EndDate,
System: r.System,
System: *r.System,
Updates: r.Statuses,
}

Expand All @@ -88,11 +109,11 @@ func (a *App) PostIncidentHandler(c *gin.Context) {
}

dbInc := db.Incident{
Text: incData.Title,
StartDate: incData.StartDate,
Text: &incData.Title,
StartDate: &incData.StartDate,
EndDate: incData.EndDate,
Impact: *incData.Impact,
System: incData.System,
Impact: incData.Impact,
System: &incData.System,
Components: components,
}

Expand All @@ -108,6 +129,68 @@ func (a *App) PostIncidentHandler(c *gin.Context) {
})
}

type PatchIncidentData struct {
Title *string `json:"title,omitempty"`
// INCIDENT_IMPACTS = {
// 0: Impact(0, "maintenance", "Scheduled maintenance"),
// 1: Impact(1, "minor", "Minor incident (i.e. performance impact)"),
// 2: Impact(2, "major", "Major incident"),
// 3: Impact(3, "outage", "Service outage"),
// }
Impact *int `json:"impact,omitempty"`
Components []int `json:"components,omitempty"`
// Datetime format is standard: "2006-01-01T12:00:00Z"
StartDate *time.Time `json:"start_date,omitempty"`
EndDate *time.Time `json:"end_date,omitempty"`
System *bool `json:"system,omitempty"`
Update *db.IncidentStatus `json:"update,omitempty"`
}

func (a *App) PatchIncidentHandler(c *gin.Context) {
var incID IncidentID
if err := c.ShouldBindUri(&incID); err != nil {
c.AbortWithError(http.StatusBadRequest, err)

Check failure on line 152 in internal/app/handlers.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `c.AbortWithError` is not checked (errcheck)
}

var incData PatchIncidentData
if err := c.ShouldBindBodyWithJSON(&incData); err != nil {
c.AbortWithError(http.StatusBadRequest, err) //nolint:nolintlint,errcheck
return
}

var components []db.Component
if len(incData.Components) != 0 {
components = make([]db.Component, len(incData.Components))
for i, comp := range incData.Components {
components[i] = db.Component{ID: uint(comp)}
}
}

var statuses []db.IncidentStatus
if incData.Update != nil {
statuses = append(statuses, *incData.Update)
}

dbInc := db.Incident{
ID: uint(incID.ID),
Text: incData.Title,
StartDate: incData.StartDate,
EndDate: incData.EndDate,
Impact: incData.Impact,
System: incData.System,
Components: components,
Statuses: statuses,
}

err := a.DB.ModifyIncident(&dbInc)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err) //nolint:nolintlint,errcheck
return
}

c.JSON(http.StatusOK, gin.H{"msg": "incident updated"})
}

type Component struct {
ID int `json:"id" uri:"id"`
Attributes []ComponentAttribute `json:"attributes"`
Expand Down
107 changes: 107 additions & 0 deletions internal/app/handlers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package app

import (
"database/sql/driver"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/DATA-DOG/go-sqlmock"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"

"github.com/stackmon/otc-status-dashboard/internal/db"
)

var testApp *App

Check failure on line 18 in internal/app/handlers_test.go

View workflow job for this annotation

GitHub Actions / lint

testApp is a global variable (gochecknoglobals)
var mock sqlmock.Sqlmock

Check failure on line 19 in internal/app/handlers_test.go

View workflow job for this annotation

GitHub Actions / lint

mock is a global variable (gochecknoglobals)

func TestGetIncidentsHandler(t *testing.T) {
initTests(t)

str := "2024-09-01T11:45:26.371Z"

testTime, err := time.Parse(time.RFC3339, str)
assert.NoError(t, err)

prepareDB(t, testTime)

var response = `{"data":[{"id":1,"title":"Incident title","impact":0,"components":[150],"start_date":"%s","system":false,"updates":[{"id":1,"status":"resolved","text":"Issue solved.","timestamp":"%s"}]}]}`

w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/v1/incidents", nil)
testApp.router.ServeHTTP(w, req)

assert.Equal(t, 200, w.Code)

assert.Equal(t, fmt.Sprintf(response, str, str), w.Body.String())
}

func TestReturn404Handler(t *testing.T) {
initTests(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/anyendpoint", nil)
testApp.router.ServeHTTP(w, req)

assert.Equal(t, 404, w.Code)
assert.Equal(t, `{"errorMsg":"page not found"}`, w.Body.String())
}

func prepareDB(t *testing.T, testTime time.Time) {
t.Helper()

rows := sqlmock.NewRows([]string{"id", "text", "start_date", "end_date", "impact", "system"}).
AddRow(1, "Incident title", testTime, nil, 0, false)
mock.ExpectQuery("^SELECT (.+) FROM \"incident\"$").WillReturnRows(rows)

rowsIncComp := sqlmock.NewRows([]string{"incident_id", "component_id"}).
AddRow(1, 150)
mock.ExpectQuery("^SELECT (.+) FROM \"incident_component_relation\"(.+)").WillReturnRows(rowsIncComp)

rowsComp := sqlmock.NewRows([]string{"id", "name"}).
AddRow(150, "Cloud Container Engine")
mock.ExpectQuery("^SELECT (.+) FROM \"component\"(.+)").WillReturnRows(rowsComp)

rowsStatus := sqlmock.NewRows([]string{"id", "incident_id", "timestamp", "text", "status"}).
AddRow(1, 1, testTime, "Issue solved.", "resolved")
mock.ExpectQuery("^SELECT (.+) FROM \"incident_status\"").WillReturnRows(rowsStatus)

rowsCompAttr := sqlmock.NewRows([]string{"id", "component_id", "name", "value"}).
AddRows([][]driver.Value{
{
859, 150, "category", "Container",
},
{
860, 150, "region", "EU-DE",
},
{
861, 150, "type", "cce",
},
}...,
)
mock.ExpectQuery("^SELECT (.+) FROM \"component_attribute\"").WillReturnRows(rowsCompAttr)

mock.NewRowsWithColumnDefinition()
}

func initTests(t *testing.T) {
t.Helper()

if testApp != nil && mock != nil {
t.Log("testApp and mock are initialized")
}

t.Log("start initialisation")
r := gin.Default()
r.Use(ErrorHandle())
r.NoRoute(Return404)

d, m, err := db.NewWithMock()
assert.NoError(t, err)

testApp = &App{router: r, Log: nil, conf: nil, DB: d, srv: nil}
testApp.InitRoutes()
mock = m
}
57 changes: 31 additions & 26 deletions internal/app/middleware.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package app

import (
"errors"
"fmt"
"net/http"

Expand All @@ -9,41 +10,47 @@ import (

func (a *App) ValidateComponentsMW() gin.HandlerFunc {
return func(c *gin.Context) {

Check failure on line 12 in internal/app/middleware.go

View workflow job for this annotation

GitHub Actions / lint

unnecessary leading newline (whitespace)
var incData IncidentData
if err := c.ShouldBindBodyWithJSON(&incData); err != nil {

type Components struct {
Components []int `json:"components"`
}

var components Components

if err := c.ShouldBindBodyWithJSON(&components); err != nil {
c.AbortWithError( //nolint:nolintlint,errcheck
http.StatusBadRequest,
fmt.Errorf("%w: %w", ErrComponentValidation, err))
fmt.Errorf("%w: %w", ErrComponentIsNotPresent, err))
return
}

// TODO: move this list to the memory cache
// We should check, that all components are presented in our db.
components, err := a.DB.GetComponents()
err := a.IsPresentComponent(components.Components)
if err != nil {
c.AbortWithError( //nolint:nolintlint,errcheck
http.StatusInternalServerError,
fmt.Errorf("%w: %w", ErrComponentValidation, err),
)
if errors.Is(err, ErrComponentIsNotPresent) {
c.AbortWithError(http.StatusBadRequest, err)

Check failure on line 32 in internal/app/middleware.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `c.AbortWithError` is not checked (errcheck)
}
c.AbortWithError(http.StatusInternalServerError, err)

Check failure on line 34 in internal/app/middleware.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `c.AbortWithError` is not checked (errcheck)
}
c.Next()
}
}

for _, comp := range incData.Components {
var isPresent bool
func (a *App) IsPresentComponent(components []int) error {

Check failure on line 40 in internal/app/middleware.go

View workflow job for this annotation

GitHub Actions / lint

unnecessary leading newline (whitespace)

for _, dbComp := range components {
if uint(comp) == dbComp.ID {
isPresent = true
}
}
if !isPresent {
c.AbortWithError( //nolint:nolintlint,errcheck
http.StatusBadRequest,
fmt.Errorf("%w: component id %d is not presented", ErrComponentValidation, comp))
}
}
dbComps, err := a.DB.GetComponentsAsMap()
if err != nil {
return err
}

c.Next()
for _, comp := range components {
if _, ok := dbComps[comp]; !ok {
return ErrComponentIsNotPresent
}
}

return nil
}

func ErrorHandle() gin.HandlerFunc {
Expand All @@ -54,12 +61,10 @@ func ErrorHandle() gin.HandlerFunc {
return
}

c.JSON(-1, gin.H{
"errorMsg": err.Error(),
})
c.JSON(-1, ReturnError(err))
}
}

func Return404(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{"errorMsg": "page not found"})
c.JSON(http.StatusNotFound, ReturnError(ErrPageNotFound))
}
Loading

0 comments on commit 92c4ad6

Please sign in to comment.