diff --git a/go.mod b/go.mod index bd40769..65766a7 100644 --- a/go.mod +++ b/go.mod @@ -17,13 +17,14 @@ require ( github.com/satori/go.uuid v1.2.0 github.com/sirupsen/logrus v1.3.0 github.com/streadway/quantile v0.0.0-20150917103942-b0c588724d25 // indirect + github.com/stretchr/testify v1.2.2 github.com/tsenart/vegeta v12.1.0+incompatible github.com/ugorji/go/codec v0.0.0-20190128213124-ee1426cffec0 // indirect golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 // indirect golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect golang.org/x/text v0.3.0 // indirect gopkg.in/alecthomas/kingpin.v2 v2.2.6 - gopkg.in/go-playground/assert.v1 v1.2.1 // indirect + gopkg.in/go-playground/assert.v1 v1.2.1 gopkg.in/go-playground/validator.v8 v8.18.2 // indirect gopkg.in/yaml.v2 v2.2.2 // indirect ) diff --git a/go.sum b/go.sum index 0d2a998..77fd488 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,7 @@ github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/streadway/quantile v0.0.0-20150917103942-b0c588724d25 h1:7z3LSn867ex6VSaahyKadf4WtSsJIgne6A1WLOAGM8A= github.com/streadway/quantile v0.0.0-20150917103942-b0c588724d25/go.mod h1:lbP8tGiBjZ5YWIc2fzuRpTaz0b/53vT6PEs3QuAWzuU= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= diff --git a/internal/dispatcher/mocks/IDispatcher.go b/internal/dispatcher/mocks/IDispatcher.go new file mode 100644 index 0000000..f86bef7 --- /dev/null +++ b/internal/dispatcher/mocks/IDispatcher.go @@ -0,0 +1,94 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" +import models "vegeta-server/models" + +// IDispatcher is an autogenerated mock type for the IDispatcher type +type IDispatcher struct { + mock.Mock +} + +// Cancel provides a mock function with given fields: _a0, _a1 +func (_m *IDispatcher) Cancel(_a0 string, _a1 bool) (*models.AttackResponse, error) { + ret := _m.Called(_a0, _a1) + + var r0 *models.AttackResponse + if rf, ok := ret.Get(0).(func(string, bool) *models.AttackResponse); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.AttackResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, bool) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Dispatch provides a mock function with given fields: _a0 +func (_m *IDispatcher) Dispatch(_a0 models.AttackParams) *models.AttackResponse { + ret := _m.Called(_a0) + + var r0 *models.AttackResponse + if rf, ok := ret.Get(0).(func(models.AttackParams) *models.AttackResponse); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.AttackResponse) + } + } + + return r0 +} + +// Get provides a mock function with given fields: _a0 +func (_m *IDispatcher) Get(_a0 string) (*models.AttackResponse, error) { + ret := _m.Called(_a0) + + var r0 *models.AttackResponse + if rf, ok := ret.Get(0).(func(string) *models.AttackResponse); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.AttackResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// List provides a mock function with given fields: +func (_m *IDispatcher) List() []*models.AttackResponse { + ret := _m.Called() + + var r0 []*models.AttackResponse + if rf, ok := ret.Get(0).(func() []*models.AttackResponse); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*models.AttackResponse) + } + } + + return r0 +} + +// Run provides a mock function with given fields: _a0 +func (_m *IDispatcher) Run(_a0 chan struct{}) { + _m.Called(_a0) +} diff --git a/internal/dispatcher/mocks/ITask.go b/internal/dispatcher/mocks/ITask.go new file mode 100644 index 0000000..a93ae64 --- /dev/null +++ b/internal/dispatcher/mocks/ITask.go @@ -0,0 +1,111 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import io "io" +import mock "github.com/stretchr/testify/mock" +import models "vegeta-server/models" +import vegeta "vegeta-server/pkg/vegeta" + +// ITask is an autogenerated mock type for the ITask type +type ITask struct { + mock.Mock +} + +// Cancel provides a mock function with given fields: +func (_m *ITask) Cancel() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Complete provides a mock function with given fields: _a0 +func (_m *ITask) Complete(_a0 io.Reader) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(io.Reader) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Fail provides a mock function with given fields: +func (_m *ITask) Fail() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ID provides a mock function with given fields: +func (_m *ITask) ID() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Params provides a mock function with given fields: +func (_m *ITask) Params() models.AttackParams { + ret := _m.Called() + + var r0 models.AttackParams + if rf, ok := ret.Get(0).(func() models.AttackParams); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(models.AttackParams) + } + + return r0 +} + +// Run provides a mock function with given fields: _a0 +func (_m *ITask) Run(_a0 vegeta.AttackFunc) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(vegeta.AttackFunc) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Status provides a mock function with given fields: +func (_m *ITask) Status() models.AttackStatus { + ret := _m.Called() + + var r0 models.AttackStatus + if rf, ok := ret.Get(0).(func() models.AttackStatus); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(models.AttackStatus) + } + + return r0 +} diff --git a/internal/endpoints/attack.go b/internal/endpoints/attack.go index 1fd89e7..9c780aa 100644 --- a/internal/endpoints/attack.go +++ b/internal/endpoints/attack.go @@ -11,14 +11,7 @@ import ( func (e *Endpoints) PostAttackEndpoint(c *gin.Context) { var attackParams models.AttackParams if err := c.ShouldBindJSON(&attackParams); err != nil { - c.JSON( - http.StatusBadRequest, - gin.H{ - "message": "Bad request params", - "code": http.StatusBadRequest, - "error": err.Error(), - }, - ) + ginErrBadRequest(c, err) return } @@ -33,14 +26,7 @@ func (e *Endpoints) GetAttackByIDEndpoint(c *gin.Context) { id := c.Param("attackID") resp, err := e.dispatcher.Get(id) if err != nil { - c.JSON( - http.StatusNotFound, - gin.H{ - "message": "Not found", - "code": http.StatusNotFound, - "error": err.Error(), - }, - ) + ginErrNotFound(c, err) return } @@ -59,40 +45,19 @@ func (e *Endpoints) PostAttackByIDCancelEndpoint(c *gin.Context) { id := c.Param("attackID") var attackCancelParams models.AttackCancel if err := c.ShouldBindJSON(&attackCancelParams); err != nil { - c.JSON( - http.StatusBadRequest, - gin.H{ - "message": "Bad request params", - "code": http.StatusBadRequest, - "error": err.Error(), - }, - ) + ginErrBadRequest(c, err) return } _, err := e.dispatcher.Get(id) if err != nil { - c.JSON( - http.StatusNotFound, - gin.H{ - "message": "Not Found", - "code": http.StatusNotFound, - "error": err.Error(), - }, - ) + ginErrNotFound(c, err) return } resp, err := e.dispatcher.Cancel(id, attackCancelParams.Cancel) if err != nil { - c.JSON( - http.StatusInternalServerError, - gin.H{ - "message": "Internal server error", - "code": http.StatusInternalServerError, - "error": err.Error(), - }, - ) + ginErrInternalServerError(c, err) return } diff --git a/internal/endpoints/attack_test.go b/internal/endpoints/attack_test.go new file mode 100644 index 0000000..6706023 --- /dev/null +++ b/internal/endpoints/attack_test.go @@ -0,0 +1,77 @@ +package endpoints + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "vegeta-server/internal/dispatcher" + dmocks "vegeta-server/internal/dispatcher/mocks" + "vegeta-server/models" + + "github.com/stretchr/testify/mock" + + assert "gopkg.in/go-playground/assert.v1" +) + +func setupTestRouter(d dispatcher.IDispatcher, req *http.Request) *httptest.ResponseRecorder { + router := SetupRouter(d, nil) + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + return w +} + +func TestEndpoints_PostAttackEndpoint(t *testing.T) { + +} + +func TestEndpoints_GetAttackByIDEndpoint_NotFound(t *testing.T) { + d := &dmocks.IDispatcher{} + + // Prepare mock + wantErr := fmt.Errorf("not found") + d. + On("Get", mock.AnythingOfType("string")). + Return(nil, wantErr) + + // Setup router + req, _ := http.NewRequest("GET", "/api/v1/attack/123", nil) + w := setupTestRouter(d, req) + + // Assert results + assert.Equal(t, http.StatusNotFound, w.Code) +} + +func TestEndpoints_GetAttackByIDEndpoint(t *testing.T) { + d := &dmocks.IDispatcher{} + + // Prepare mock + wantBody := &models.AttackResponse{ + ID: "123", + Status: models.AttackResponseStatusScheduled, + } + + d. + On("Get", "123"). + Return(wantBody, nil) + + // Setup router + req, _ := http.NewRequest("GET", "/api/v1/attack/123", nil) + w := setupTestRouter(d, req) + + // Assert results + assert.Equal(t, http.StatusOK, w.Code) + gotBody := &models.AttackResponse{} + _ = json.Unmarshal(w.Body.Bytes(), gotBody) + assert.Equal(t, *wantBody, *gotBody) +} + +func TestEndpoints_GetAttackEndpoint(t *testing.T) { + +} + +func TestEndpoints_PostAttackByIDCancelEndpoint(t *testing.T) { + +} diff --git a/internal/endpoints/endpoints.go b/internal/endpoints/endpoints.go index 3dd0e70..5cbf65e 100644 --- a/internal/endpoints/endpoints.go +++ b/internal/endpoints/endpoints.go @@ -1,10 +1,46 @@ package endpoints import ( + "net/http" "vegeta-server/internal/dispatcher" "vegeta-server/internal/reporter" - gin "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin" +) + +var ( + ginErrNotFound = func(c *gin.Context, err error) { + c.JSON( + http.StatusNotFound, + gin.H{ + "message": "Not found", + "code": http.StatusNotFound, + "error": err.Error(), + }, + ) + } + + ginErrBadRequest = func(c *gin.Context, err error) { + c.JSON( + http.StatusBadRequest, + gin.H{ + "message": "Bad request params", + "code": http.StatusBadRequest, + "error": err.Error(), + }, + ) + } + + ginErrInternalServerError = func(c *gin.Context, err error) { + c.JSON( + http.StatusInternalServerError, + gin.H{ + "message": "Internal server error", + "code": http.StatusInternalServerError, + "error": err.Error(), + }, + ) + } ) // Endpoints provides an encapsulation for all dependencies required by the diff --git a/internal/endpoints/endpoints_test.go b/internal/endpoints/endpoints_test.go new file mode 100644 index 0000000..67e0f8a --- /dev/null +++ b/internal/endpoints/endpoints_test.go @@ -0,0 +1,13 @@ +package endpoints + +import ( + "testing" +) + +func TestNewEndpoints(t *testing.T) { + +} + +func TestSetupRouter(t *testing.T) { + +} diff --git a/internal/endpoints/report.go b/internal/endpoints/report.go index 4d5b8cb..af3d34a 100644 --- a/internal/endpoints/report.go +++ b/internal/endpoints/report.go @@ -19,14 +19,7 @@ func (e *Endpoints) GetReportEndpoint(c *gin.Context) { var jsonReport models.JSONReportResponse err := json.Unmarshal(report, &jsonReport) if err != nil { - c.JSON( - http.StatusInternalServerError, - gin.H{ - "message": "Failed to decode", - "code": http.StatusInternalServerError, - "error": err.Error(), - }, - ) + ginErrNotFound(c, err) return } jsonReports = append(jsonReports, jsonReport) @@ -41,14 +34,7 @@ func (e *Endpoints) GetReportByIDEndpoint(c *gin.Context) { format := c.DefaultQuery("format", "json") resp, err := e.reporter.GetInFormat(id, vegeta.Format(format)) if err != nil { - c.JSON( - http.StatusNotFound, - gin.H{ - "message": "Not found", - "code": http.StatusNotFound, - "error": err.Error(), - }, - ) + ginErrNotFound(c, err) return } @@ -58,14 +44,7 @@ func (e *Endpoints) GetReportByIDEndpoint(c *gin.Context) { var jsonReport models.JSONReportResponse err = json.Unmarshal(resp, &jsonReport) if err != nil { - c.JSON( - http.StatusInternalServerError, - gin.H{ - "message": "Failed to decode", - "code": http.StatusInternalServerError, - "error": err.Error(), - }, - ) + ginErrInternalServerError(c, err) } c.JSON(http.StatusOK, jsonReport) case "text": diff --git a/internal/endpoints/report_test.go b/internal/endpoints/report_test.go new file mode 100644 index 0000000..ff25352 --- /dev/null +++ b/internal/endpoints/report_test.go @@ -0,0 +1,13 @@ +package endpoints + +import ( + "testing" +) + +func TestEndpoints_GetReportEndpoint(t *testing.T) { + +} + +func TestEndpoints_GetReportByIDEndpoint(t *testing.T) { + +}