add extras to message model

This commit is contained in:
eternal-flame-AD 2019-02-02 11:15:21 +08:00 committed by Jannis Mattheis
parent 98710db507
commit de09aae987
12 changed files with 241 additions and 109 deletions

View File

@ -1,6 +1,7 @@
package api package api
import ( import (
"encoding/json"
"errors" "errors"
"strconv" "strconv"
"strings" "strings"
@ -30,7 +31,7 @@ var timeNow = time.Now
// Notifier notifies when a new message was created. // Notifier notifies when a new message was created.
type Notifier interface { type Notifier interface {
Notify(userID uint, message *model.Message) Notify(userID uint, message *model.MessageExternal)
} }
// The MessageAPI provides handlers for managing messages. // The MessageAPI provides handlers for managing messages.
@ -110,7 +111,7 @@ func buildWithPaging(ctx *gin.Context, paging *pagingParams, messages []*model.M
} }
return &model.PagedMessages{ return &model.PagedMessages{
Paging: model.Paging{Size: len(useMessages), Limit: paging.Limit, Next: next, Since: since}, Paging: model.Paging{Size: len(useMessages), Limit: paging.Limit, Next: next, Since: since},
Messages: useMessages, Messages: toExternalMessages(useMessages),
} }
} }
@ -329,7 +330,7 @@ func (a *MessageAPI) DeleteMessage(ctx *gin.Context) {
// schema: // schema:
// $ref: "#/definitions/Error" // $ref: "#/definitions/Error"
func (a *MessageAPI) CreateMessage(ctx *gin.Context) { func (a *MessageAPI) CreateMessage(ctx *gin.Context) {
message := model.Message{} message := model.MessageExternal{}
if err := ctx.Bind(&message); err == nil { if err := ctx.Bind(&message); err == nil {
application := a.DB.GetApplicationByToken(auth.GetTokenID(ctx)) application := a.DB.GetApplicationByToken(auth.GetTokenID(ctx))
message.ApplicationID = application.ID message.ApplicationID = application.ID
@ -337,8 +338,48 @@ func (a *MessageAPI) CreateMessage(ctx *gin.Context) {
message.Title = application.Name message.Title = application.Name
} }
message.Date = timeNow() message.Date = timeNow()
a.DB.CreateMessage(&message) msgInternal := toInternalMessage(&message)
a.Notifier.Notify(auth.GetUserID(ctx), &message) a.DB.CreateMessage(msgInternal)
ctx.JSON(200, message) a.Notifier.Notify(auth.GetUserID(ctx), toExternalMessage(msgInternal))
ctx.JSON(200, toExternalMessage(msgInternal))
} }
} }
func toInternalMessage(msg *model.MessageExternal) *model.Message {
res := &model.Message{
ID: msg.ID,
ApplicationID: msg.ApplicationID,
Message: msg.Message,
Title: msg.Title,
Priority: msg.Priority,
Date: msg.Date,
}
if msg.Extras != nil {
res.Extras, _ = json.Marshal(msg.Extras)
}
return res
}
func toExternalMessage(msg *model.Message) *model.MessageExternal {
res := &model.MessageExternal{
ID: msg.ID,
ApplicationID: msg.ApplicationID,
Message: msg.Message,
Title: msg.Title,
Priority: msg.Priority,
Date: msg.Date,
}
if len(msg.Extras) != 0 {
res.Extras = make(map[string]interface{})
json.Unmarshal(msg.Extras, &res.Extras)
}
return res
}
func toExternalMessages(msg []*model.Message) []*model.MessageExternal {
res := make([]*model.MessageExternal, len(msg))
for i := range msg {
res[i] = toExternalMessage(msg[i])
}
return res
}

View File

@ -22,11 +22,11 @@ func TestMessageSuite(t *testing.T) {
type MessageSuite struct { type MessageSuite struct {
suite.Suite suite.Suite
db *test.Database db *test.Database
a *MessageAPI a *MessageAPI
ctx *gin.Context ctx *gin.Context
recorder *httptest.ResponseRecorder recorder *httptest.ResponseRecorder
notified bool notifiedMessage *model.MessageExternal
} }
func (s *MessageSuite) BeforeTest(suiteName, testName string) { func (s *MessageSuite) BeforeTest(suiteName, testName string) {
@ -35,7 +35,7 @@ func (s *MessageSuite) BeforeTest(suiteName, testName string) {
s.ctx, _ = gin.CreateTestContext(s.recorder) s.ctx, _ = gin.CreateTestContext(s.recorder)
s.ctx.Request = httptest.NewRequest("GET", "/irrelevant", nil) s.ctx.Request = httptest.NewRequest("GET", "/irrelevant", nil)
s.db = test.NewDB(s.T()) s.db = test.NewDB(s.T())
s.notified = false s.notifiedMessage = nil
s.a = &MessageAPI{DB: s.db, Notifier: s} s.a = &MessageAPI{DB: s.db, Notifier: s}
} }
@ -43,32 +43,39 @@ func (s *MessageSuite) AfterTest(string, string) {
s.db.Close() s.db.Close()
} }
func (s *MessageSuite) Notify(userID uint, msg *model.Message) { func (s *MessageSuite) Notify(userID uint, msg *model.MessageExternal) {
s.notified = true s.notifiedMessage = msg
} }
func (s *MessageSuite) Test_ensureCorrectJsonRepresentation() { func (s *MessageSuite) Test_ensureCorrectJsonRepresentation() {
t, _ := time.Parse("2006/01/02", "2017/01/02") t, _ := time.Parse("2006/01/02", "2017/01/02")
actual := &model.PagedMessages{ actual := &model.PagedMessages{
Paging: model.Paging{Limit: 5, Since: 122, Size: 5, Next: "http://example.com/message?limit=5&since=122"}, Paging: model.Paging{Limit: 5, Since: 122, Size: 5, Next: "http://example.com/message?limit=5&since=122"},
Messages: []*model.Message{{ID: 55, ApplicationID: 2, Message: "hi", Title: "hi", Date: t, Priority: 4}}, Messages: []*model.MessageExternal{{ID: 55, ApplicationID: 2, Message: "hi", Title: "hi", Date: t, Priority: 4, Extras: map[string]interface{}{
"test::string": "string",
"test::array": []interface{}{1, 2, 3},
"test::int": 1,
"test::float": 0.5,
}}},
} }
test.JSONEquals(s.T(), actual, `{"paging": {"limit":5, "since": 122, "size": 5, "next": "http://example.com/message?limit=5&since=122"}, test.JSONEquals(s.T(), actual, `{"paging": {"limit":5, "since": 122, "size": 5, "next": "http://example.com/message?limit=5&since=122"},
"messages": [{"id":55,"appid":2,"message":"hi","title":"hi","priority":4,"date":"2017-01-02T00:00:00Z"}]}`) "messages": [{"id":55,"appid":2,"message":"hi","title":"hi","priority":4,"date":"2017-01-02T00:00:00Z","extras":{"test::string":"string","test::array":[1,2,3],"test::int":1,"test::float":0.5}}]}`)
} }
func (s *MessageSuite) Test_GetMessages() { func (s *MessageSuite) Test_GetMessages() {
user := s.db.User(5) user := s.db.User(5)
first := user.App(1).NewMessage(1) first := user.App(1).NewMessage(1)
second := user.App(2).NewMessage(2) second := user.App(2).NewMessage(2)
firstExternal := toExternalMessage(&first)
secondExternal := toExternalMessage(&second)
test.WithUser(s.ctx, 5) test.WithUser(s.ctx, 5)
s.a.GetMessages(s.ctx) s.a.GetMessages(s.ctx)
expected := &model.PagedMessages{ expected := &model.PagedMessages{
Paging: model.Paging{Limit: 100, Size: 2, Next: ""}, Paging: model.Paging{Limit: 100, Size: 2, Next: ""},
Messages: []*model.Message{&second, &first}, Messages: []*model.MessageExternal{secondExternal, firstExternal},
} }
test.BodyEquals(s.T(), expected, s.recorder) test.BodyEquals(s.T(), expected, s.recorder)
@ -92,7 +99,7 @@ func (s *MessageSuite) Test_GetMessages_WithLimit_ReturnsNext() {
// Since: entries with ids from 100 - 96 will be returned (5 entries) // Since: entries with ids from 100 - 96 will be returned (5 entries)
expected := &model.PagedMessages{ expected := &model.PagedMessages{
Paging: model.Paging{Limit: 5, Size: 5, Since: 96, Next: "http://example.com/messages?limit=5&since=96"}, Paging: model.Paging{Limit: 5, Size: 5, Since: 96, Next: "http://example.com/messages?limit=5&since=96"},
Messages: messages[:5], Messages: toExternalMessages(messages[:5]),
} }
test.BodyEquals(s.T(), expected, s.recorder) test.BodyEquals(s.T(), expected, s.recorder)
@ -116,7 +123,7 @@ func (s *MessageSuite) Test_GetMessages_WithLimit_WithSince_ReturnsNext() {
// Since: entries with ids from 54 - 42 will be returned (13 entries) // Since: entries with ids from 54 - 42 will be returned (13 entries)
expected := &model.PagedMessages{ expected := &model.PagedMessages{
Paging: model.Paging{Limit: 13, Size: 13, Since: 42, Next: "http://example.com/messages?limit=13&since=42"}, Paging: model.Paging{Limit: 13, Size: 13, Since: 42, Next: "http://example.com/messages?limit=13&since=42"},
Messages: messages[46 : 46+13], Messages: toExternalMessages(messages[46 : 46+13]),
} }
test.BodyEquals(s.T(), expected, s.recorder) test.BodyEquals(s.T(), expected, s.recorder)
} }
@ -159,7 +166,7 @@ func (s *MessageSuite) Test_GetMessagesWithToken() {
expected := &model.PagedMessages{ expected := &model.PagedMessages{
Paging: model.Paging{Limit: 100, Size: 1, Next: ""}, Paging: model.Paging{Limit: 100, Size: 1, Next: ""},
Messages: []*model.Message{&msg}, Messages: toExternalMessages([]*model.Message{&msg}),
} }
test.BodyEquals(s.T(), expected, s.recorder) test.BodyEquals(s.T(), expected, s.recorder)
@ -182,7 +189,7 @@ func (s *MessageSuite) Test_GetMessagesWithToken_WithLimit_ReturnsNext() {
// Since: entries with ids from 100 - 92 will be returned (9 entries) // Since: entries with ids from 100 - 92 will be returned (9 entries)
expected := &model.PagedMessages{ expected := &model.PagedMessages{
Paging: model.Paging{Limit: 9, Size: 9, Since: 92, Next: "http://example.com/app/2/message?limit=9&since=92"}, Paging: model.Paging{Limit: 9, Size: 9, Since: 92, Next: "http://example.com/app/2/message?limit=9&since=92"},
Messages: messages[:9], Messages: toExternalMessages(messages[:9]),
} }
test.BodyEquals(s.T(), expected, s.recorder) test.BodyEquals(s.T(), expected, s.recorder)
@ -205,7 +212,7 @@ func (s *MessageSuite) Test_GetMessagesWithToken_WithLimit_WithSince_ReturnsNext
// Since: entries with ids from 54 - 42 will be returned (13 entries) // Since: entries with ids from 54 - 42 will be returned (13 entries)
expected := &model.PagedMessages{ expected := &model.PagedMessages{
Paging: model.Paging{Limit: 13, Size: 13, Since: 42, Next: "http://example.com/app/2/message?limit=13&since=42"}, Paging: model.Paging{Limit: 13, Size: 13, Since: 42, Next: "http://example.com/app/2/message?limit=13&since=42"},
Messages: messages[46 : 46+13], Messages: toExternalMessages(messages[46 : 46+13]),
} }
test.BodyEquals(s.T(), expected, s.recorder) test.BodyEquals(s.T(), expected, s.recorder)
} }
@ -316,17 +323,17 @@ func (s *MessageSuite) Test_CreateMessage_onJson_allParams() {
auth.RegisterAuthentication(s.ctx, nil, 4, "app-token") auth.RegisterAuthentication(s.ctx, nil, 4, "app-token")
s.db.User(4).AppWithToken(7, "app-token") s.db.User(4).AppWithToken(7, "app-token")
s.ctx.Request = httptest.NewRequest("POST", "/token", strings.NewReader(`{"title": "mytitle", "message": "mymessage", "priority": 1}`)) s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`{"title": "mytitle", "message": "mymessage", "priority": 1}`))
s.ctx.Request.Header.Set("Content-Type", "application/json") s.ctx.Request.Header.Set("Content-Type", "application/json")
s.a.CreateMessage(s.ctx) s.a.CreateMessage(s.ctx)
msgs := s.db.GetMessagesByApplication(7) msgs := s.db.GetMessagesByApplication(7)
expected := &model.Message{ID: 1, ApplicationID: 7, Title: "mytitle", Message: "mymessage", Priority: 1, Date: t} expected := &model.MessageExternal{ID: 1, ApplicationID: 7, Title: "mytitle", Message: "mymessage", Priority: 1, Date: t}
assert.Len(s.T(), msgs, 1) assert.Len(s.T(), msgs, 1)
assert.Contains(s.T(), msgs, expected) assert.Equal(s.T(), expected, toExternalMessage(msgs[0]))
assert.Equal(s.T(), 200, s.recorder.Code) assert.Equal(s.T(), 200, s.recorder.Code)
assert.True(s.T(), s.notified) assert.Equal(s.T(), expected, s.notifiedMessage)
} }
func (s *MessageSuite) Test_CreateMessage_WithTitle() { func (s *MessageSuite) Test_CreateMessage_WithTitle() {
@ -336,38 +343,38 @@ func (s *MessageSuite) Test_CreateMessage_WithTitle() {
auth.RegisterAuthentication(s.ctx, nil, 4, "app-token") auth.RegisterAuthentication(s.ctx, nil, 4, "app-token")
s.db.User(4).AppWithToken(5, "app-token") s.db.User(4).AppWithToken(5, "app-token")
s.ctx.Request = httptest.NewRequest("POST", "/token", strings.NewReader(`{"title": "mytitle", "message": "mymessage"}`)) s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`{"title": "mytitle", "message": "mymessage"}`))
s.ctx.Request.Header.Set("Content-Type", "application/json") s.ctx.Request.Header.Set("Content-Type", "application/json")
s.a.CreateMessage(s.ctx) s.a.CreateMessage(s.ctx)
msgs := s.db.GetMessagesByApplication(5) msgs := s.db.GetMessagesByApplication(5)
expected := &model.Message{ID: 1, ApplicationID: 5, Title: "mytitle", Message: "mymessage", Date: t} expected := &model.MessageExternal{ID: 1, ApplicationID: 5, Title: "mytitle", Message: "mymessage", Date: t}
assert.Len(s.T(), msgs, 1) assert.Len(s.T(), msgs, 1)
assert.Contains(s.T(), msgs, expected) assert.Equal(s.T(), expected, toExternalMessage(msgs[0]))
assert.Equal(s.T(), 200, s.recorder.Code) assert.Equal(s.T(), 200, s.recorder.Code)
assert.True(s.T(), s.notified) assert.Equal(s.T(), expected, s.notifiedMessage)
} }
func (s *MessageSuite) Test_CreateMessage_failWhenNoMessage() { func (s *MessageSuite) Test_CreateMessage_failWhenNoMessage() {
auth.RegisterAuthentication(s.ctx, nil, 4, "app-token") auth.RegisterAuthentication(s.ctx, nil, 4, "app-token")
s.db.User(4).AppWithToken(1, "app-token") s.db.User(4).AppWithToken(1, "app-token")
s.ctx.Request = httptest.NewRequest("POST", "/token", strings.NewReader(`{"title": "mytitle"}`)) s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`{"title": "mytitle"}`))
s.ctx.Request.Header.Set("Content-Type", "application/json") s.ctx.Request.Header.Set("Content-Type", "application/json")
s.a.CreateMessage(s.ctx) s.a.CreateMessage(s.ctx)
assert.Empty(s.T(), s.db.GetMessagesByApplication(1)) assert.Empty(s.T(), s.db.GetMessagesByApplication(1))
assert.Equal(s.T(), 400, s.recorder.Code) assert.Equal(s.T(), 400, s.recorder.Code)
assert.False(s.T(), s.notified) assert.Nil(s.T(), s.notifiedMessage)
} }
func (s *MessageSuite) Test_CreateMessage_WithoutTitle() { func (s *MessageSuite) Test_CreateMessage_WithoutTitle() {
auth.RegisterAuthentication(s.ctx, nil, 4, "app-token") auth.RegisterAuthentication(s.ctx, nil, 4, "app-token")
s.db.User(4).AppWithTokenAndName(8, "app-token", "Application name") s.db.User(4).AppWithTokenAndName(8, "app-token", "Application name")
s.ctx.Request = httptest.NewRequest("POST", "/token", strings.NewReader(`{"message": "mymessage"}`)) s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`{"message": "mymessage"}`))
s.ctx.Request.Header.Set("Content-Type", "application/json") s.ctx.Request.Header.Set("Content-Type", "application/json")
s.a.CreateMessage(s.ctx) s.a.CreateMessage(s.ctx)
@ -376,14 +383,14 @@ func (s *MessageSuite) Test_CreateMessage_WithoutTitle() {
assert.Len(s.T(), msgs, 1) assert.Len(s.T(), msgs, 1)
assert.Equal(s.T(), "Application name", msgs[0].Title) assert.Equal(s.T(), "Application name", msgs[0].Title)
assert.Equal(s.T(), 200, s.recorder.Code) assert.Equal(s.T(), 200, s.recorder.Code)
assert.True(s.T(), s.notified) assert.Equal(s.T(), "mymessage", s.notifiedMessage.Message)
} }
func (s *MessageSuite) Test_CreateMessage_WithBlankTitle() { func (s *MessageSuite) Test_CreateMessage_WithBlankTitle() {
auth.RegisterAuthentication(s.ctx, nil, 4, "app-token") auth.RegisterAuthentication(s.ctx, nil, 4, "app-token")
s.db.User(4).AppWithTokenAndName(8, "app-token", "Application name") s.db.User(4).AppWithTokenAndName(8, "app-token", "Application name")
s.ctx.Request = httptest.NewRequest("POST", "/token", strings.NewReader(`{"message": "mymessage", "title": " "}`)) s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`{"message": "mymessage", "title": " "}`))
s.ctx.Request.Header.Set("Content-Type", "application/json") s.ctx.Request.Header.Set("Content-Type", "application/json")
s.a.CreateMessage(s.ctx) s.a.CreateMessage(s.ctx)
@ -392,20 +399,56 @@ func (s *MessageSuite) Test_CreateMessage_WithBlankTitle() {
assert.Len(s.T(), msgs, 1) assert.Len(s.T(), msgs, 1)
assert.Equal(s.T(), "Application name", msgs[0].Title) assert.Equal(s.T(), "Application name", msgs[0].Title)
assert.Equal(s.T(), 200, s.recorder.Code) assert.Equal(s.T(), 200, s.recorder.Code)
assert.True(s.T(), s.notified) assert.Equal(s.T(), "mymessage", msgs[0].Message)
}
func (s *MessageSuite) Test_CreateMessage_WithExtras() {
auth.RegisterAuthentication(s.ctx, nil, 4, "app-token")
s.db.User(4).AppWithTokenAndName(8, "app-token", "Application name")
t, _ := time.Parse("2006/01/02", "2017/01/02")
timeNow = func() time.Time { return t }
defer func() { timeNow = time.Now }()
s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`{"message": "mymessage", "title": "msg with extras", "extras": {"gotify::test":{"int":1,"float":0.5,"string":"test","array":[1,2,3]}}}`))
s.ctx.Request.Header.Set("Content-Type", "application/json")
s.a.CreateMessage(s.ctx)
msgs := s.db.GetMessagesByApplication(8)
expected := &model.MessageExternal{
ID: 1,
ApplicationID: 8,
Message: "mymessage",
Title: "msg with extras",
Date: t,
Extras: map[string]interface{}{
"gotify::test": map[string]interface{}{
"string": "test",
"array": []interface{}{float64(1), float64(2), float64(3)},
"int": float64(1),
"float": float64(0.5),
},
},
}
assert.Len(s.T(), msgs, 1)
assert.Equal(s.T(), expected, toExternalMessage(msgs[0]))
assert.Equal(s.T(), 200, s.recorder.Code)
assert.Equal(s.T(), uint(1), s.notifiedMessage.ID)
} }
func (s *MessageSuite) Test_CreateMessage_failWhenPriorityNotNumber() { func (s *MessageSuite) Test_CreateMessage_failWhenPriorityNotNumber() {
auth.RegisterAuthentication(s.ctx, nil, 4, "app-token") auth.RegisterAuthentication(s.ctx, nil, 4, "app-token")
s.db.User(4).AppWithToken(8, "app-token") s.db.User(4).AppWithToken(8, "app-token")
s.ctx.Request = httptest.NewRequest("POST", "/token", strings.NewReader(`{"title": "mytitle", "message": "mymessage", "priority": "asd"}`)) s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`{"title": "mytitle", "message": "mymessage", "priority": "asd"}`))
s.ctx.Request.Header.Set("Content-Type", "application/json") s.ctx.Request.Header.Set("Content-Type", "application/json")
s.a.CreateMessage(s.ctx) s.a.CreateMessage(s.ctx)
assert.Equal(s.T(), 400, s.recorder.Code) assert.Equal(s.T(), 400, s.recorder.Code)
assert.False(s.T(), s.notified) assert.Nil(s.T(), s.notifiedMessage)
assert.Empty(s.T(), s.db.GetMessagesByApplication(1)) assert.Empty(s.T(), s.db.GetMessagesByApplication(1))
} }
@ -417,20 +460,19 @@ func (s *MessageSuite) Test_CreateMessage_onQueryData() {
timeNow = func() time.Time { return t } timeNow = func() time.Time { return t }
defer func() { timeNow = time.Now }() defer func() { timeNow = time.Now }()
s.ctx.Request = httptest.NewRequest("POST", "/token?title=mytitle&message=mymessage&priority=1", nil) s.ctx.Request = httptest.NewRequest("POST", "/message?title=mytitle&message=mymessage&priority=1", nil)
s.ctx.Request.Header.Set("Content-Type", "application/x-www-form-urlencoded") s.ctx.Request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
s.a.CreateMessage(s.ctx) s.a.CreateMessage(s.ctx)
expected := &model.Message{ID: 1, ApplicationID: 2, Title: "mytitle", Message: "mymessage", Priority: 1, Date: t} expected := &model.MessageExternal{ID: 1, ApplicationID: 2, Title: "mytitle", Message: "mymessage", Priority: 1, Date: t}
msgs := s.db.GetMessagesByApplication(2) msgs := s.db.GetMessagesByApplication(2)
assert.Len(s.T(), msgs, 1) assert.Len(s.T(), msgs, 1)
assert.Contains(s.T(), msgs, expected) assert.Equal(s.T(), expected, toExternalMessage(msgs[0]))
assert.Equal(s.T(), 200, s.recorder.Code) assert.Equal(s.T(), 200, s.recorder.Code)
assert.True(s.T(), s.notified) assert.Equal(s.T(), uint(1), s.notifiedMessage.ID)
} }
func (s *MessageSuite) Test_CreateMessage_onFormData() { func (s *MessageSuite) Test_CreateMessage_onFormData() {
auth.RegisterAuthentication(s.ctx, nil, 4, "app-token") auth.RegisterAuthentication(s.ctx, nil, 4, "app-token")
s.db.User(4).AppWithToken(99, "app-token") s.db.User(4).AppWithToken(99, "app-token")
@ -439,17 +481,17 @@ func (s *MessageSuite) Test_CreateMessage_onFormData() {
timeNow = func() time.Time { return t } timeNow = func() time.Time { return t }
defer func() { timeNow = time.Now }() defer func() { timeNow = time.Now }()
s.ctx.Request = httptest.NewRequest("POST", "/token", strings.NewReader("title=mytitle&message=mymessage&priority=1")) s.ctx.Request = httptest.NewRequest("POST", "/message", strings.NewReader(`title=mytitle&message=mymessage&priority=1`))
s.ctx.Request.Header.Set("Content-Type", "application/x-www-form-urlencoded") s.ctx.Request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
s.a.CreateMessage(s.ctx) s.a.CreateMessage(s.ctx)
expected := &model.Message{ID: 1, ApplicationID: 99, Title: "mytitle", Message: "mymessage", Priority: 1, Date: t} expected := &model.MessageExternal{ID: 1, ApplicationID: 99, Title: "mytitle", Message: "mymessage", Priority: 1, Date: t}
msgs := s.db.GetMessagesByApplication(99) msgs := s.db.GetMessagesByApplication(99)
assert.Len(s.T(), msgs, 1) assert.Len(s.T(), msgs, 1)
assert.Contains(s.T(), msgs, expected) assert.Equal(s.T(), expected, toExternalMessage(msgs[0]))
assert.Equal(s.T(), 200, s.recorder.Code) assert.Equal(s.T(), 200, s.recorder.Code)
assert.True(s.T(), s.notified) assert.Equal(s.T(), uint(1), s.notifiedMessage.ID)
} }
func (s *MessageSuite) withURL(scheme, host, path, query string) { func (s *MessageSuite) withURL(scheme, host, path, query string) {

View File

@ -22,7 +22,7 @@ var writeJSON = func(conn *websocket.Conn, v interface{}) error {
type client struct { type client struct {
conn *websocket.Conn conn *websocket.Conn
onClose func(*client) onClose func(*client)
write chan *model.Message write chan *model.MessageExternal
userID uint userID uint
token string token string
once once once once
@ -31,7 +31,7 @@ type client struct {
func newClient(conn *websocket.Conn, userID uint, token string, onClose func(*client)) *client { func newClient(conn *websocket.Conn, userID uint, token string, onClose func(*client)) *client {
return &client{ return &client{
conn: conn, conn: conn,
write: make(chan *model.Message, 1), write: make(chan *model.MessageExternal, 1),
userID: userID, userID: userID,
token: token, token: token,
onClose: onClose, onClose: onClose,

View File

@ -65,7 +65,7 @@ func (a *API) NotifyDeletedClient(userID uint, token string) {
} }
// Notify notifies the clients with the given userID that a new messages was created. // Notify notifies the clients with the given userID that a new messages was created.
func (a *API) Notify(userID uint, msg *model.Message) { func (a *API) Notify(userID uint, msg *model.MessageExternal) {
a.lock.RLock() a.lock.RLock()
defer a.lock.RUnlock() defer a.lock.RUnlock()
if clients, ok := a.clients[userID]; ok { if clients, ok := a.clients[userID]; ok {

View File

@ -61,7 +61,7 @@ func TestWriteMessageFails(t *testing.T) {
clients := clients(api, 1) clients := clients(api, 1)
assert.NotEmpty(t, clients) assert.NotEmpty(t, clients)
api.Notify(1, &model.Message{Message: "HI"}) api.Notify(1, &model.MessageExternal{Message: "HI"})
user.expectNoMessage() user.expectNoMessage()
} }
@ -94,7 +94,7 @@ func TestWritePingFails(t *testing.T) {
time.Sleep(5 * time.Second) // waiting for ping time.Sleep(5 * time.Second) // waiting for ping
api.Notify(1, &model.Message{Message: "HI"}) api.Notify(1, &model.MessageExternal{Message: "HI"})
user.expectNoMessage() user.expectNoMessage()
} }
@ -130,8 +130,8 @@ func TestPing(t *testing.T) {
} }
expectNoMessage(user) expectNoMessage(user)
api.Notify(1, &model.Message{Message: "HI"}) api.Notify(1, &model.MessageExternal{Message: "HI"})
user.expectMessage(&model.Message{Message: "HI"}) user.expectMessage(&model.MessageExternal{Message: "HI"})
} }
func TestCloseClientOnNotReading(t *testing.T) { func TestCloseClientOnNotReading(t *testing.T) {
@ -169,8 +169,8 @@ func TestMessageDirectlyAfterConnect(t *testing.T) {
defer user.conn.Close() defer user.conn.Close()
// the server may take some time to register the client // the server may take some time to register the client
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
api.Notify(1, &model.Message{Message: "msg"}) api.Notify(1, &model.MessageExternal{Message: "msg"})
user.expectMessage(&model.Message{Message: "msg"}) user.expectMessage(&model.MessageExternal{Message: "msg"})
} }
func TestDeleteClientShouldCloseConnection(t *testing.T) { func TestDeleteClientShouldCloseConnection(t *testing.T) {
@ -186,12 +186,12 @@ func TestDeleteClientShouldCloseConnection(t *testing.T) {
defer user.conn.Close() defer user.conn.Close()
// the server may take some time to register the client // the server may take some time to register the client
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
api.Notify(1, &model.Message{Message: "msg"}) api.Notify(1, &model.MessageExternal{Message: "msg"})
user.expectMessage(&model.Message{Message: "msg"}) user.expectMessage(&model.MessageExternal{Message: "msg"})
api.NotifyDeletedClient(1, "customtoken") api.NotifyDeletedClient(1, "customtoken")
api.Notify(1, &model.Message{Message: "msg"}) api.Notify(1, &model.MessageExternal{Message: "msg"})
user.expectNoMessage() user.expectNoMessage()
} }
@ -233,28 +233,28 @@ func TestDeleteMultipleClients(t *testing.T) {
// the server may take some time to register the client // the server may take some time to register the client
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
api.Notify(1, &model.Message{ID: 4, Message: "there"}) api.Notify(1, &model.MessageExternal{ID: 4, Message: "there"})
expectMessage(&model.Message{ID: 4, Message: "there"}, userOne...) expectMessage(&model.MessageExternal{ID: 4, Message: "there"}, userOne...)
expectNoMessage(userTwo...) expectNoMessage(userTwo...)
expectNoMessage(userThree...) expectNoMessage(userThree...)
api.NotifyDeletedClient(1, "1-2") api.NotifyDeletedClient(1, "1-2")
api.Notify(1, &model.Message{ID: 2, Message: "there"}) api.Notify(1, &model.MessageExternal{ID: 2, Message: "there"})
expectMessage(&model.Message{ID: 2, Message: "there"}, userOneIPhone, userOneOther) expectMessage(&model.MessageExternal{ID: 2, Message: "there"}, userOneIPhone, userOneOther)
expectNoMessage(userOneBrowser, userOneAndroid) expectNoMessage(userOneBrowser, userOneAndroid)
expectNoMessage(userThree...) expectNoMessage(userThree...)
expectNoMessage(userTwo...) expectNoMessage(userTwo...)
api.Notify(2, &model.Message{ID: 2, Message: "there"}) api.Notify(2, &model.MessageExternal{ID: 2, Message: "there"})
expectNoMessage(userOne...) expectNoMessage(userOne...)
expectMessage(&model.Message{ID: 2, Message: "there"}, userTwo...) expectMessage(&model.MessageExternal{ID: 2, Message: "there"}, userTwo...)
expectNoMessage(userThree...) expectNoMessage(userThree...)
api.Notify(3, &model.Message{ID: 5, Message: "there"}) api.Notify(3, &model.MessageExternal{ID: 5, Message: "there"})
expectNoMessage(userOne...) expectNoMessage(userOne...)
expectNoMessage(userTwo...) expectNoMessage(userTwo...)
expectMessage(&model.Message{ID: 5, Message: "there"}, userThree...) expectMessage(&model.MessageExternal{ID: 5, Message: "there"}, userThree...)
api.Close() api.Close()
} }
@ -297,27 +297,27 @@ func TestDeleteUser(t *testing.T) {
// the server may take some time to register the client // the server may take some time to register the client
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
api.Notify(1, &model.Message{ID: 4, Message: "there"}) api.Notify(1, &model.MessageExternal{ID: 4, Message: "there"})
expectMessage(&model.Message{ID: 4, Message: "there"}, userOne...) expectMessage(&model.MessageExternal{ID: 4, Message: "there"}, userOne...)
expectNoMessage(userTwo...) expectNoMessage(userTwo...)
expectNoMessage(userThree...) expectNoMessage(userThree...)
api.NotifyDeletedUser(1) api.NotifyDeletedUser(1)
api.Notify(1, &model.Message{ID: 2, Message: "there"}) api.Notify(1, &model.MessageExternal{ID: 2, Message: "there"})
expectNoMessage(userOne...) expectNoMessage(userOne...)
expectNoMessage(userThree...) expectNoMessage(userThree...)
expectNoMessage(userTwo...) expectNoMessage(userTwo...)
api.Notify(2, &model.Message{ID: 2, Message: "there"}) api.Notify(2, &model.MessageExternal{ID: 2, Message: "there"})
expectNoMessage(userOne...) expectNoMessage(userOne...)
expectMessage(&model.Message{ID: 2, Message: "there"}, userTwo...) expectMessage(&model.MessageExternal{ID: 2, Message: "there"}, userTwo...)
expectNoMessage(userThree...) expectNoMessage(userThree...)
api.Notify(3, &model.Message{ID: 5, Message: "there"}) api.Notify(3, &model.MessageExternal{ID: 5, Message: "there"})
expectNoMessage(userOne...) expectNoMessage(userOne...)
expectNoMessage(userTwo...) expectNoMessage(userTwo...)
expectMessage(&model.Message{ID: 5, Message: "there"}, userThree...) expectMessage(&model.MessageExternal{ID: 5, Message: "there"}, userThree...)
api.Close() api.Close()
} }
@ -362,15 +362,15 @@ func TestMultipleClients(t *testing.T) {
expectNoMessage(userTwo...) expectNoMessage(userTwo...)
expectNoMessage(userThree...) expectNoMessage(userThree...)
api.Notify(1, &model.Message{ID: 1, Message: "hello"}) api.Notify(1, &model.MessageExternal{ID: 1, Message: "hello"})
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
expectMessage(&model.Message{ID: 1, Message: "hello"}, userOne...) expectMessage(&model.MessageExternal{ID: 1, Message: "hello"}, userOne...)
expectNoMessage(userTwo...) expectNoMessage(userTwo...)
expectNoMessage(userThree...) expectNoMessage(userThree...)
api.Notify(2, &model.Message{ID: 2, Message: "there"}) api.Notify(2, &model.MessageExternal{ID: 2, Message: "there"})
expectNoMessage(userOne...) expectNoMessage(userOne...)
expectMessage(&model.Message{ID: 2, Message: "there"}, userTwo...) expectMessage(&model.MessageExternal{ID: 2, Message: "there"}, userTwo...)
expectNoMessage(userThree...) expectNoMessage(userThree...)
userOneIPhone.conn.Close() userOneIPhone.conn.Close()
@ -379,21 +379,21 @@ func TestMultipleClients(t *testing.T) {
expectNoMessage(userTwo...) expectNoMessage(userTwo...)
expectNoMessage(userThree...) expectNoMessage(userThree...)
api.Notify(1, &model.Message{ID: 3, Message: "how"}) api.Notify(1, &model.MessageExternal{ID: 3, Message: "how"})
expectMessage(&model.Message{ID: 3, Message: "how"}, userOneAndroid, userOneBrowser) expectMessage(&model.MessageExternal{ID: 3, Message: "how"}, userOneAndroid, userOneBrowser)
expectNoMessage(userOneIPhone) expectNoMessage(userOneIPhone)
expectNoMessage(userTwo...) expectNoMessage(userTwo...)
expectNoMessage(userThree...) expectNoMessage(userThree...)
api.Notify(2, &model.Message{ID: 4, Message: "are"}) api.Notify(2, &model.MessageExternal{ID: 4, Message: "are"})
expectNoMessage(userOne...) expectNoMessage(userOne...)
expectMessage(&model.Message{ID: 4, Message: "are"}, userTwo...) expectMessage(&model.MessageExternal{ID: 4, Message: "are"}, userTwo...)
expectNoMessage(userThree...) expectNoMessage(userThree...)
api.Close() api.Close()
api.Notify(2, &model.Message{ID: 5, Message: "you"}) api.Notify(2, &model.MessageExternal{ID: 5, Message: "you"})
expectNoMessage(userOne...) expectNoMessage(userOne...)
expectNoMessage(userTwo...) expectNoMessage(userTwo...)
@ -481,7 +481,7 @@ func startReading(client *testingClient) {
return return
} }
actual := &model.Message{} actual := &model.MessageExternal{}
json.NewDecoder(bytes.NewBuffer(payload)).Decode(actual) json.NewDecoder(bytes.NewBuffer(payload)).Decode(actual)
client.readMessage <- *actual client.readMessage <- *actual
} }
@ -492,18 +492,18 @@ func createClient(t *testing.T, url string) *testingClient {
ws, _, err := websocket.DefaultDialer.Dial(url, nil) ws, _, err := websocket.DefaultDialer.Dial(url, nil)
assert.Nil(t, err) assert.Nil(t, err)
readMessages := make(chan model.Message) readMessages := make(chan model.MessageExternal)
return &testingClient{conn: ws, readMessage: readMessages, t: t} return &testingClient{conn: ws, readMessage: readMessages, t: t}
} }
type testingClient struct { type testingClient struct {
conn *websocket.Conn conn *websocket.Conn
readMessage chan model.Message readMessage chan model.MessageExternal
t *testing.T t *testing.T
} }
func (c *testingClient) expectMessage(expected *model.Message) { func (c *testingClient) expectMessage(expected *model.MessageExternal) {
select { select {
case <-time.After(50 * time.Millisecond): case <-time.After(50 * time.Millisecond):
assert.Fail(c.t, "Expected message but none was send :(") assert.Fail(c.t, "Expected message but none was send :(")
@ -512,7 +512,7 @@ func (c *testingClient) expectMessage(expected *model.Message) {
} }
} }
func expectMessage(expected *model.Message, clients ...*testingClient) { func expectMessage(expected *model.MessageExternal, clients ...*testingClient) {
for _, client := range clients { for _, client := range clients {
client.expectMessage(expected) client.expectMessage(expected)
} }

View File

@ -54,7 +54,7 @@ func (a *UserAPI) GetUsers(ctx *gin.Context) {
var resp []*model.UserExternal var resp []*model.UserExternal
for _, user := range users { for _, user := range users {
resp = append(resp, toExternal(user)) resp = append(resp, toExternalUser(user))
} }
ctx.JSON(200, resp) ctx.JSON(200, resp)
@ -83,7 +83,7 @@ func (a *UserAPI) GetUsers(ctx *gin.Context) {
// $ref: "#/definitions/Error" // $ref: "#/definitions/Error"
func (a *UserAPI) GetCurrentUser(ctx *gin.Context) { func (a *UserAPI) GetCurrentUser(ctx *gin.Context) {
user := a.DB.GetUserByID(auth.GetUserID(ctx)) user := a.DB.GetUserByID(auth.GetUserID(ctx))
ctx.JSON(200, toExternal(user)) ctx.JSON(200, toExternalUser(user))
} }
// CreateUser creates a user // CreateUser creates a user
@ -122,10 +122,10 @@ func (a *UserAPI) GetCurrentUser(ctx *gin.Context) {
func (a *UserAPI) CreateUser(ctx *gin.Context) { func (a *UserAPI) CreateUser(ctx *gin.Context) {
user := model.UserExternalWithPass{} user := model.UserExternalWithPass{}
if err := ctx.Bind(&user); err == nil { if err := ctx.Bind(&user); err == nil {
internal := a.toInternal(&user, []byte{}) internal := a.toInternalUser(&user, []byte{})
if a.DB.GetUserByName(internal.Name) == nil { if a.DB.GetUserByName(internal.Name) == nil {
a.DB.CreateUser(internal) a.DB.CreateUser(internal)
ctx.JSON(200, toExternal(internal)) ctx.JSON(200, toExternalUser(internal))
} else { } else {
ctx.AbortWithError(400, errors.New("username already exists")) ctx.AbortWithError(400, errors.New("username already exists"))
} }
@ -171,7 +171,7 @@ func (a *UserAPI) CreateUser(ctx *gin.Context) {
func (a *UserAPI) GetUserByID(ctx *gin.Context) { func (a *UserAPI) GetUserByID(ctx *gin.Context) {
withID(ctx, "id", func(id uint) { withID(ctx, "id", func(id uint) {
if user := a.DB.GetUserByID(uint(id)); user != nil { if user := a.DB.GetUserByID(uint(id)); user != nil {
ctx.JSON(200, toExternal(user)) ctx.JSON(200, toExternalUser(user))
} else { } else {
ctx.AbortWithError(404, errors.New("user does not exist")) ctx.AbortWithError(404, errors.New("user does not exist"))
} }
@ -309,10 +309,10 @@ func (a *UserAPI) UpdateUserByID(ctx *gin.Context) {
var user *model.UserExternalWithPass var user *model.UserExternalWithPass
if err := ctx.Bind(&user); err == nil { if err := ctx.Bind(&user); err == nil {
if oldUser := a.DB.GetUserByID(id); oldUser != nil { if oldUser := a.DB.GetUserByID(id); oldUser != nil {
internal := a.toInternal(user, oldUser.Pass) internal := a.toInternalUser(user, oldUser.Pass)
internal.ID = id internal.ID = id
a.DB.UpdateUser(internal) a.DB.UpdateUser(internal)
ctx.JSON(200, toExternal(internal)) ctx.JSON(200, toExternalUser(internal))
} else { } else {
ctx.AbortWithError(404, errors.New("user does not exist")) ctx.AbortWithError(404, errors.New("user does not exist"))
} }
@ -320,7 +320,7 @@ func (a *UserAPI) UpdateUserByID(ctx *gin.Context) {
}) })
} }
func (a *UserAPI) toInternal(response *model.UserExternalWithPass, pw []byte) *model.User { func (a *UserAPI) toInternalUser(response *model.UserExternalWithPass, pw []byte) *model.User {
user := &model.User{ user := &model.User{
Name: response.Name, Name: response.Name,
Admin: response.Admin, Admin: response.Admin,
@ -333,7 +333,7 @@ func (a *UserAPI) toInternal(response *model.UserExternalWithPass, pw []byte) *m
return user return user
} }
func toExternal(internal *model.User) *model.UserExternal { func toExternalUser(internal *model.User) *model.UserExternal {
return &model.UserExternal{ return &model.UserExternal{
Name: internal.Name, Name: internal.Name,
Admin: internal.Admin, Admin: internal.Admin,

View File

@ -1490,9 +1490,9 @@
"x-go-package": "github.com/gotify/server/model" "x-go-package": "github.com/gotify/server/model"
}, },
"Message": { "Message": {
"description": "The Message holds information about a message which was sent by an Application.", "description": "The MessageExternal holds information about a message which was sent by an Application.",
"type": "object", "type": "object",
"title": "Message Model", "title": "MessageExternal Model",
"required": [ "required": [
"id", "id",
"appid", "appid",
@ -1516,6 +1516,22 @@
"readOnly": true, "readOnly": true,
"example": "2018-02-27T19:36:10.5045044+01:00" "example": "2018-02-27T19:36:10.5045044+01:00"
}, },
"extras": {
"description": "The extra data sent along the message.\n\nThe extra fields are stored in a key-value scheme. Only accepted in CreateMessage requests with application/json content-type.\n\nThe keys should be in the following format: \u0026lt;top-namespace\u0026gt;::[\u0026lt;sub-namespace\u0026gt;::]\u0026lt;action\u0026gt;\n\nThese namespaces are reserved and might be used in the official clients: gotify android ios web server client. Do not use them for other purposes.",
"type": "object",
"additionalProperties": {
"type": "object"
},
"x-go-name": "Extras",
"example": {
"home::appliances::lighting::on": {
"brightness": 15
},
"home::appliances::thermostat::change_temperature": {
"temperature": 23
}
}
},
"id": { "id": {
"description": "The message id.", "description": "The message id.",
"type": "integer", "type": "integer",
@ -1544,6 +1560,7 @@
"example": "Backup" "example": "Backup"
} }
}, },
"x-go-name": "MessageExternal",
"x-go-package": "github.com/gotify/server/model" "x-go-package": "github.com/gotify/server/model"
}, },
"PagedMessages": { "PagedMessages": {

View File

@ -34,6 +34,6 @@ type Application struct {
// read only: true // read only: true
// required: true // required: true
// example: https://example.com/image.jpeg // example: https://example.com/image.jpeg
Image string `json:"image"` Image string `json:"image"`
Messages []Message `json:"-"` Messages []MessageExternal `json:"-"`
} }

View File

@ -1,19 +1,32 @@
package model package model
import "time" import (
"time"
)
// Message Model // Message holds information about a message
type Message struct {
ID uint `gorm:"AUTO_INCREMENT;primary_key;index"`
ApplicationID uint
Message string
Title string
Priority int
Extras []byte
Date time.Time
}
// MessageExternal Model
// //
// The Message holds information about a message which was sent by an Application. // The MessageExternal holds information about a message which was sent by an Application.
// //
// swagger:model Message // swagger:model Message
type Message struct { type MessageExternal struct {
// The message id. // The message id.
// //
// read only: true // read only: true
// required: true // required: true
// example: 25 // example: 25
ID uint `gorm:"AUTO_INCREMENT;primary_key;index" json:"id"` ID uint `json:"id"`
// The application id that send this message. // The application id that send this message.
// //
// read only: true // read only: true
@ -33,6 +46,16 @@ type Message struct {
// //
// example: 2 // example: 2
Priority int `form:"priority" query:"priority" json:"priority"` Priority int `form:"priority" query:"priority" json:"priority"`
// The extra data sent along the message.
//
// The extra fields are stored in a key-value scheme. Only accepted in CreateMessage requests with application/json content-type.
//
// The keys should be in the following format: &lt;top-namespace&gt;::[&lt;sub-namespace&gt;::]&lt;action&gt;
//
// These namespaces are reserved and might be used in the official clients: gotify android ios web server client. Do not use them for other purposes.
//
// example: {"home::appliances::thermostat::change_temperature":{"temperature":23},"home::appliances::lighting::on":{"brightness":15}}
Extras map[string]interface{} `form:"-" query:"-" json:"extras,omitempty"`
// The date the message was created. // The date the message was created.
// //
// read only: true // read only: true

View File

@ -50,5 +50,5 @@ type PagedMessages struct {
// //
// read only: true // read only: true
// required: true // required: true
Messages []*Message `json:"messages"` Messages []*MessageExternal `json:"messages"`
} }

View File

@ -102,7 +102,11 @@ describe('Messages', () => {
} }
return result; return result;
}; };
const m = (title: string, message: string) => ({title, message}); const m = (title: string, message: string, extras?: IMessageExtras) => ({
title,
message,
extras,
});
const windows1 = m('Login', 'User jmattheis logged in.'); const windows1 = m('Login', 'User jmattheis logged in.');
const windows2 = m('Shutdown', 'Windows will be shut down.'); const windows2 = m('Shutdown', 'Windows will be shut down.');

View File

@ -20,6 +20,11 @@ interface IMessage {
priority: number; priority: number;
date: string; date: string;
image?: string; image?: string;
extras?: IMessageExtras;
}
interface IMessageExtras {
[key: string]: any; // tslint:disable-line no-any
} }
interface IPagedMessages { interface IPagedMessages {