diff --git a/api/message.go b/api/message.go new file mode 100644 index 0000000..1d4cbd3 --- /dev/null +++ b/api/message.go @@ -0,0 +1,83 @@ +package api + +import ( + "errors" + "github.com/gin-gonic/gin" + "github.com/jmattheis/memo/auth" + "github.com/jmattheis/memo/model" + "strconv" + "time" +) + +// The MessageDatabase interface for encapsulating database access. +type MessageDatabase interface { + GetMessagesByUserAndApplication(userID uint, tokenID string) []*model.Message + GetApplicationByID(id string) *model.Application + GetMessagesByUser(userID uint) []*model.Message + DeleteMessageByID(id uint) error + GetMessageByID(id uint) *model.Message + DeleteMessagesByUser(userID uint) error + DeleteMessagesByApplication(applicationID string) error + CreateMessage(message *model.Message) error +} + +// The MessageAPI provides handlers for managing messages. +type MessageAPI struct { + DB MessageDatabase +} + +// GetMessages returns all messages from a user. +func (a *MessageAPI) GetMessages(ctx *gin.Context) { + userID := auth.GetUserID(ctx) + messages := a.DB.GetMessagesByUser(userID) + ctx.JSON(200, messages) +} + +// GetMessagesWithApplication returns all messages from a specific application. +func (a *MessageAPI) GetMessagesWithApplication(ctx *gin.Context) { + appID := ctx.Param("appid") + userID := auth.GetUserID(ctx) + messages := a.DB.GetMessagesByUserAndApplication(userID, appID) + ctx.JSON(200, messages) +} + +// DeleteMessages delete all messages from a user. +func (a *MessageAPI) DeleteMessages(ctx *gin.Context) { + userID := auth.GetUserID(ctx) + a.DB.DeleteMessagesByUser(userID) +} + +// DeleteMessageWithApplication deletes all messages from a specific application. +func (a *MessageAPI) DeleteMessageWithApplication(ctx *gin.Context) { + appID := ctx.Param("appid") + if application := a.DB.GetApplicationByID(appID); application != nil && application.UserID == auth.GetUserID(ctx) { + a.DB.DeleteMessagesByApplication(appID) + } else { + ctx.AbortWithError(404, errors.New("application does not exists")) + } +} + +// DeleteMessage deletes a message with an id. +func (a *MessageAPI) DeleteMessage(ctx *gin.Context) { + id := ctx.Param("id") + if parsedUInt, err := strconv.ParseUint(id, 10, 32); err == nil { + if msg := a.DB.GetMessageByID(uint(parsedUInt)); msg != nil && a.DB.GetApplicationByID(msg.TokenID).UserID == auth.GetUserID(ctx) { + a.DB.DeleteMessageByID(uint(parsedUInt)) + } else { + ctx.AbortWithError(404, errors.New("message does not exists")) + } + } else { + ctx.AbortWithError(400, errors.New("message does not exist")) + } +} + +// CreateMessage creates a message, authentication via application-token is required. +func (a *MessageAPI) CreateMessage(ctx *gin.Context) { + message := model.Message{} + if err := ctx.Bind(&message); err == nil { + message.TokenID = auth.GetTokenID(ctx) + message.Date = time.Now() + a.DB.CreateMessage(&message) + ctx.JSON(200, message) + } +} diff --git a/api/message_test.go b/api/message_test.go new file mode 100644 index 0000000..ae49c9e --- /dev/null +++ b/api/message_test.go @@ -0,0 +1,254 @@ +package api + +import ( + "github.com/bouk/monkey" + "github.com/gin-gonic/gin" + apimock "github.com/jmattheis/memo/api/mock" + "github.com/jmattheis/memo/auth" + "github.com/jmattheis/memo/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + "io/ioutil" + "net/http/httptest" + "strings" + "testing" + "time" +) + +func TestMessageSuite(t *testing.T) { + suite.Run(t, new(MessageSuite)) +} + +type MessageSuite struct { + suite.Suite + db *apimock.MockMessageDatabase + a *MessageAPI + ctx *gin.Context + recorder *httptest.ResponseRecorder +} + +func (s *MessageSuite) BeforeTest(suiteName, testName string) { + gin.SetMode(gin.TestMode) + s.recorder = httptest.NewRecorder() + s.ctx, _ = gin.CreateTestContext(s.recorder) + s.db = &apimock.MockMessageDatabase{} + s.a = &MessageAPI{DB: s.db} +} + +func (s *MessageSuite) Test_GetMessages() { + auth.RegisterAuthentication(s.ctx, nil, 5, "") + t, _ := time.Parse("2006/01/02", "2017/01/02") + s.db.On("GetMessagesByUser", uint(5)).Return([]*model.Message{{ID: 1, TokenID: "asd", Message: "OH HELLO THERE", Date: t, Title: "wup", Priority: 2}, {ID: 2, TokenID: "cloud", Message: "hi", Title: "hi", Date: t, Priority: 4}}) + + s.a.GetMessages(s.ctx) + + assert.Equal(s.T(), 200, s.recorder.Code) + bytes, _ := ioutil.ReadAll(s.recorder.Body) + + assert.JSONEq(s.T(), `[{"id":1,"tokenid":"asd","message":"OH HELLO THERE","title":"wup","priority":2,"date":"2017-01-02T00:00:00Z"},{"id":2,"tokenid":"cloud","message":"hi","title":"hi","priority":4,"date":"2017-01-02T00:00:00Z"}]`, string(bytes)) +} + +func (s *MessageSuite) Test_GetMessagesWithToken() { + auth.RegisterAuthentication(s.ctx, nil, 4, "") + t, _ := time.Parse("2006/01/02", "2021/01/02") + s.db.On("GetMessagesByUserAndApplication", uint(4), "mytoken").Return([]*model.Message{{ID: 2, TokenID: "mytoken", Message: "hi", Title: "hi", Date: t, Priority: 4}}) + s.ctx.Params = gin.Params{{Key: "appid", Value: "mytoken"}} + + s.a.GetMessagesWithApplication(s.ctx) + + assert.Equal(s.T(), 200, s.recorder.Code) + bytes, _ := ioutil.ReadAll(s.recorder.Body) + assert.JSONEq(s.T(), `[{"id":2,"tokenid":"mytoken","message":"hi","title":"hi","priority":4,"date":"2021-01-02T00:00:00Z"}]`, string(bytes)) +} + +func (s *MessageSuite) Test_DeleteMessage_invalidID() { + s.ctx.Params = gin.Params{{Key: "id", Value: "string"}} + + s.a.DeleteMessage(s.ctx) + + assert.Equal(s.T(), 400, s.recorder.Code) +} + +func (s *MessageSuite) Test_DeleteMessage_notExistingID() { + s.ctx.Params = gin.Params{{Key: "id", Value: "1"}} + s.db.On("GetMessageByID", uint(1)).Return(nil) + + s.a.DeleteMessage(s.ctx) + + assert.Equal(s.T(), 404, s.recorder.Code) +} + +func (s *MessageSuite) Test_DeleteMessage_existingIDButNotOwner() { + auth.RegisterAuthentication(s.ctx, nil, 6, "") + s.ctx.Params = gin.Params{{Key: "id", Value: "1"}} + s.db.On("GetMessageByID", uint(1)).Return(&model.Message{ID: 1, TokenID: "token"}) + s.db.On("GetApplicationByID", "token").Return(&model.Application{ID: "token", UserID: 2}) + + s.a.DeleteMessage(s.ctx) + + assert.Equal(s.T(), 404, s.recorder.Code) +} + +func (s *MessageSuite) Test_DeleteMessage() { + auth.RegisterAuthentication(s.ctx, nil, 2, "") + s.ctx.Params = gin.Params{{Key: "id", Value: "1"}} + s.db.On("GetMessageByID", uint(1)).Return(&model.Message{ID: 1, TokenID: "token"}) + s.db.On("GetApplicationByID", "token").Return(&model.Application{ID: "token", UserID: 2}) + s.db.On("DeleteMessageByID", uint(1)).Return(nil) + + s.a.DeleteMessage(s.ctx) + + s.db.AssertCalled(s.T(), "DeleteMessageByID", uint(1)) + assert.Equal(s.T(), 200, s.recorder.Code) +} + +func (s *MessageSuite) Test_DeleteMessageWithToken() { + auth.RegisterAuthentication(s.ctx, nil, 2, "") + s.ctx.Params = gin.Params{{Key: "appid", Value: "mytoken"}} + s.db.On("GetApplicationByID", "mytoken").Return(&model.Application{ID: "mytoken", UserID: 2}) + s.db.On("DeleteMessagesByApplication", "mytoken").Return(nil) + + s.a.DeleteMessageWithApplication(s.ctx) + + s.db.AssertCalled(s.T(), "DeleteMessagesByApplication", "mytoken") + assert.Equal(s.T(), 200, s.recorder.Code) +} + +func (s *MessageSuite) Test_DeleteMessageWithToken_notExistingToken() { + auth.RegisterAuthentication(s.ctx, nil, 2, "") + s.ctx.Params = gin.Params{{Key: "appid", Value: "asdasdasd"}} + s.db.On("GetApplicationByID", "asdasdasd").Return(nil) + s.db.On("DeleteMessagesByApplication", "asdasdasd").Return(nil) + + s.a.DeleteMessageWithApplication(s.ctx) + + s.db.AssertNotCalled(s.T(), "DeleteMessagesByApplication", "mytoken") + assert.Equal(s.T(), 404, s.recorder.Code) +} + +func (s *MessageSuite) Test_DeleteMessageWithToken_notOwner() { + auth.RegisterAuthentication(s.ctx, nil, 4, "") + s.ctx.Params = gin.Params{{Key: "appid", Value: "mytoken"}} + s.db.On("GetApplicationByID", "mytoken").Return(&model.Application{ID: "mytoken", UserID: 2}) + s.db.On("DeleteMessagesByApplication", "mytoken").Return(nil) + + s.a.DeleteMessageWithApplication(s.ctx) + + s.db.AssertNotCalled(s.T(), "DeleteMessagesByApplication", "mytoken") + assert.Equal(s.T(), 404, s.recorder.Code) +} + +func (s *MessageSuite) Test_DeleteMessages() { + auth.RegisterAuthentication(s.ctx, nil, 4, "") + s.db.On("DeleteMessagesByUser", uint(4)).Return(nil) + + s.a.DeleteMessages(s.ctx) + + s.db.AssertCalled(s.T(), "DeleteMessagesByUser", uint(4)) + assert.Equal(s.T(), 200, s.recorder.Code) +} + +func (s *MessageSuite) Test_CreateMessage_onJson_allParams() { + auth.RegisterAuthentication(s.ctx, nil, 4, "app-token") + + t, _ := time.Parse("2006/01/02", "2017/01/02") + monkey.Patch(time.Now, func() time.Time { return t }) + expected := &model.Message{ID: 0, TokenID: "app-token", Title: "mytitle", Message: "mymessage", Priority: 1, Date: t} + + s.ctx.Request = httptest.NewRequest("POST", "/token", strings.NewReader(`{"title": "mytitle", "message": "mymessage", "priority": 1}`)) + s.ctx.Request.Header.Set("Content-Type", "application/json") + s.db.On("CreateMessage", expected).Return(nil) + + s.a.CreateMessage(s.ctx) + + s.db.AssertCalled(s.T(), "CreateMessage", expected) + assert.Equal(s.T(), 200, s.recorder.Code) +} + +func (s *MessageSuite) Test_CreateMessage_onlyRequired() { + auth.RegisterAuthentication(s.ctx, nil, 4, "app-token") + + t, _ := time.Parse("2006/01/02", "2017/01/02") + monkey.Patch(time.Now, func() time.Time { return t }) + expected := &model.Message{ID: 0, TokenID: "app-token", Title: "mytitle", Message: "mymessage", Date: t} + + s.ctx.Request = httptest.NewRequest("POST", "/token", strings.NewReader(`{"title": "mytitle", "message": "mymessage"}`)) + s.ctx.Request.Header.Set("Content-Type", "application/json") + s.db.On("CreateMessage", expected).Return(nil) + + s.a.CreateMessage(s.ctx) + + s.db.AssertCalled(s.T(), "CreateMessage", expected) + assert.Equal(s.T(), 200, s.recorder.Code) +} + +func (s *MessageSuite) Test_CreateMessage_failWhenNoMessage() { + auth.RegisterAuthentication(s.ctx, nil, 4, "app-token") + + s.ctx.Request = httptest.NewRequest("POST", "/token", strings.NewReader(`{"title": "mytitle"}`)) + s.ctx.Request.Header.Set("Content-Type", "application/json") + + s.a.CreateMessage(s.ctx) + + s.db.AssertNotCalled(s.T(), "CreateMessage", mock.Anything) + assert.Equal(s.T(), 400, s.recorder.Code) +} + +func (s *MessageSuite) Test_CreateMessage_failWhenNoTitle() { + auth.RegisterAuthentication(s.ctx, nil, 4, "app-token") + + s.ctx.Request = httptest.NewRequest("POST", "/token", strings.NewReader(`{"message": "mymessage"}`)) + s.ctx.Request.Header.Set("Content-Type", "application/json") + + s.a.CreateMessage(s.ctx) + + s.db.AssertNotCalled(s.T(), "CreateMessage", mock.Anything) + assert.Equal(s.T(), 400, s.recorder.Code) +} + +func (s *MessageSuite) Test_CreateMessage_failWhenPriorityNotNumber() { + auth.RegisterAuthentication(s.ctx, nil, 4, "app-token") + + s.ctx.Request = httptest.NewRequest("POST", "/token", strings.NewReader(`{"title": "mytitle", "message": "mymessage", "priority": "asd"}`)) + s.ctx.Request.Header.Set("Content-Type", "application/json") + + s.a.CreateMessage(s.ctx) + + s.db.AssertNotCalled(s.T(), "CreateMessage", mock.Anything) + assert.Equal(s.T(), 400, s.recorder.Code) +} + +func (s *MessageSuite) Test_CreateMessage_onQueryData() { + auth.RegisterAuthentication(s.ctx, nil, 4, "app-token") + + t, _ := time.Parse("2006/01/02", "2017/01/02") + monkey.Patch(time.Now, func() time.Time { return t }) + expected := &model.Message{ID: 0, TokenID: "app-token", Title: "mytitle", Message: "mymessage", Priority: 1, Date: t} + + s.ctx.Request = httptest.NewRequest("POST", "/token?title=mytitle&message=mymessage&priority=1", nil) + s.ctx.Request.Header.Set("Content-Type", "application/x-www-form-urlencoded") + s.db.On("CreateMessage", expected).Return(nil) + + s.a.CreateMessage(s.ctx) + + s.db.AssertCalled(s.T(), "CreateMessage", expected) + assert.Equal(s.T(), 200, s.recorder.Code) +} + +func (s *MessageSuite) Test_CreateMessage_onFormData() { + auth.RegisterAuthentication(s.ctx, nil, 4, "app-token") + + t, _ := time.Parse("2006/01/02", "2017/01/02") + monkey.Patch(time.Now, func() time.Time { return t }) + expected := &model.Message{ID: 0, TokenID: "app-token", Title: "mytitle", Message: "mymessage", Priority: 1, Date: t} + + s.ctx.Request = httptest.NewRequest("POST", "/token", strings.NewReader("title=mytitle&message=mymessage&priority=1")) + s.ctx.Request.Header.Set("Content-Type", "application/x-www-form-urlencoded") + s.db.On("CreateMessage", expected).Return(nil) + + s.a.CreateMessage(s.ctx) + + s.db.AssertCalled(s.T(), "CreateMessage", expected) + assert.Equal(s.T(), 200, s.recorder.Code) +} diff --git a/api/mock/mock_messagedatabase.go b/api/mock/mock_messagedatabase.go new file mode 100644 index 0000000..5be0c63 --- /dev/null +++ b/api/mock/mock_messagedatabase.go @@ -0,0 +1,130 @@ +// Code generated by mockery v1.0.0 +package mock + +import mock "github.com/stretchr/testify/mock" +import model "github.com/jmattheis/memo/model" + +// MockMessageDatabase is an autogenerated mock type for the MessageDatabase type +type MockMessageDatabase struct { + mock.Mock +} + +// CreateMessage provides a mock function with given fields: message +func (_m *MockMessageDatabase) CreateMessage(message *model.Message) error { + ret := _m.Called(message) + + var r0 error + if rf, ok := ret.Get(0).(func(*model.Message) error); ok { + r0 = rf(message) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteMessageByID provides a mock function with given fields: id +func (_m *MockMessageDatabase) DeleteMessageByID(id uint) error { + ret := _m.Called(id) + + var r0 error + if rf, ok := ret.Get(0).(func(uint) error); ok { + r0 = rf(id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteMessagesByApplication provides a mock function with given fields: applicationID +func (_m *MockMessageDatabase) DeleteMessagesByApplication(applicationID string) error { + ret := _m.Called(applicationID) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(applicationID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteMessagesByUser provides a mock function with given fields: userID +func (_m *MockMessageDatabase) DeleteMessagesByUser(userID uint) error { + ret := _m.Called(userID) + + var r0 error + if rf, ok := ret.Get(0).(func(uint) error); ok { + r0 = rf(userID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetApplicationByID provides a mock function with given fields: id +func (_m *MockMessageDatabase) GetApplicationByID(id string) *model.Application { + ret := _m.Called(id) + + var r0 *model.Application + if rf, ok := ret.Get(0).(func(string) *model.Application); ok { + r0 = rf(id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Application) + } + } + + return r0 +} + +// GetMessageByID provides a mock function with given fields: id +func (_m *MockMessageDatabase) GetMessageByID(id uint) *model.Message { + ret := _m.Called(id) + + var r0 *model.Message + if rf, ok := ret.Get(0).(func(uint) *model.Message); ok { + r0 = rf(id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Message) + } + } + + return r0 +} + +// GetMessagesByUser provides a mock function with given fields: userID +func (_m *MockMessageDatabase) GetMessagesByUser(userID uint) []*model.Message { + ret := _m.Called(userID) + + var r0 []*model.Message + if rf, ok := ret.Get(0).(func(uint) []*model.Message); ok { + r0 = rf(userID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Message) + } + } + + return r0 +} + +// GetMessagesByUserAndApplication provides a mock function with given fields: userID, tokenID +func (_m *MockMessageDatabase) GetMessagesByUserAndApplication(userID uint, tokenID string) []*model.Message { + ret := _m.Called(userID, tokenID) + + var r0 []*model.Message + if rf, ok := ret.Get(0).(func(uint, string) []*model.Message); ok { + r0 = rf(userID, tokenID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Message) + } + } + + return r0 +} diff --git a/model/message.go b/model/message.go index 805677a..bd6c627 100644 --- a/model/message.go +++ b/model/message.go @@ -4,10 +4,10 @@ import "time" // The Message holds information about a message which was sent by an Application. type Message struct { - ID uint `gorm:"AUTO_INCREMENT;primary_key;index"` - TokenID string - Message string - Title string - Priority int - Date time.Time + ID uint `gorm:"AUTO_INCREMENT;primary_key;index" json:"id"` + TokenID string `json:"tokenid"` + Message string `form:"message" query:"message" json:"message" binding:"required"` + Title string `form:"title" query:"title" json:"title" binding:"required"` + Priority int `form:"priority" query:"priority" json:"priority"` + Date time.Time `json:"date"` }