[#34] Adjust message api to be paged
This commit is contained in:
parent
53aec56ca5
commit
39a3d46607
|
|
@ -4,16 +4,20 @@ import (
|
|||
"errors"
|
||||
"time"
|
||||
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/gotify/location"
|
||||
"github.com/gotify/server/auth"
|
||||
"github.com/gotify/server/model"
|
||||
)
|
||||
|
||||
// The MessageDatabase interface for encapsulating database access.
|
||||
type MessageDatabase interface {
|
||||
GetMessagesByApplication(id uint) []*model.Message
|
||||
GetMessagesByApplicationSince(appID uint, limit int, since uint) []*model.Message
|
||||
GetApplicationByID(id uint) *model.Application
|
||||
GetMessagesByUser(userID uint) []*model.Message
|
||||
GetMessagesByUserSince(userID uint, limit int, since uint) []*model.Message
|
||||
DeleteMessageByID(id uint) error
|
||||
GetMessageByID(id uint) *model.Message
|
||||
DeleteMessagesByUser(userID uint) error
|
||||
|
|
@ -33,22 +37,61 @@ type MessageAPI struct {
|
|||
Notifier Notifier
|
||||
}
|
||||
|
||||
type pagingParams struct {
|
||||
Limit int `form:"limit" binding:"min=1,max=200"`
|
||||
Since uint `form:"since" binding:"min=0"`
|
||||
}
|
||||
|
||||
// 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)
|
||||
withPaging(ctx, func(params *pagingParams) {
|
||||
// the +1 is used to check if there are more messages and will be removed on buildWithPaging
|
||||
messages := a.DB.GetMessagesByUserSince(userID, params.Limit+1, params.Since)
|
||||
ctx.JSON(200, buildWithPaging(ctx, params, messages))
|
||||
})
|
||||
}
|
||||
|
||||
func buildWithPaging(ctx *gin.Context, paging *pagingParams, messages []*model.Message) *model.PagedMessages {
|
||||
next := ""
|
||||
since := uint(0)
|
||||
useMessages := messages
|
||||
if len(messages) > paging.Limit {
|
||||
useMessages = messages[:len(messages)-1]
|
||||
since = useMessages[len(useMessages)-1].ID
|
||||
url := location.Get(ctx)
|
||||
url.Path = ctx.Request.URL.Path
|
||||
query := url.Query()
|
||||
query.Add("limit", strconv.Itoa(paging.Limit))
|
||||
query.Add("since", strconv.FormatUint(uint64(since), 10))
|
||||
url.RawQuery = query.Encode()
|
||||
next = url.String()
|
||||
}
|
||||
return &model.PagedMessages{
|
||||
Paging: model.Paging{Size: len(useMessages), Limit: paging.Limit, Next: next, Since: since},
|
||||
Messages: useMessages,
|
||||
}
|
||||
}
|
||||
|
||||
func withPaging(ctx *gin.Context, f func(pagingParams *pagingParams)) {
|
||||
params := &pagingParams{Limit: 100}
|
||||
if err := ctx.MustBindWith(params, binding.Query); err == nil {
|
||||
f(params)
|
||||
}
|
||||
}
|
||||
|
||||
// GetMessagesWithApplication returns all messages from a specific application.
|
||||
func (a *MessageAPI) GetMessagesWithApplication(ctx *gin.Context) {
|
||||
withID(ctx, "id", func(id uint) {
|
||||
if app := a.DB.GetApplicationByID(id); app != nil && app.UserID == auth.GetUserID(ctx) {
|
||||
messages := a.DB.GetMessagesByApplication(id)
|
||||
ctx.JSON(200, messages)
|
||||
} else {
|
||||
ctx.AbortWithError(404, errors.New("application does not exist"))
|
||||
}
|
||||
withPaging(ctx, func(params *pagingParams) {
|
||||
if app := a.DB.GetApplicationByID(id); app != nil && app.UserID == auth.GetUserID(ctx) {
|
||||
// the +1 is used to check if there are more messages and will be removed on buildWithPaging
|
||||
messages := a.DB.GetMessagesByApplicationSince(id, params.Limit+1, params.Since)
|
||||
ctx.JSON(200, buildWithPaging(ctx, params, messages))
|
||||
} else {
|
||||
ctx.AbortWithError(404, errors.New("application does not exist"))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import (
|
|||
|
||||
"strings"
|
||||
|
||||
"net/url"
|
||||
|
||||
"github.com/bouk/monkey"
|
||||
"github.com/gotify/server/auth"
|
||||
)
|
||||
|
|
@ -35,6 +37,7 @@ func (s *MessageSuite) BeforeTest(suiteName, testName string) {
|
|||
mode.Set(mode.TestDev)
|
||||
s.recorder = httptest.NewRecorder()
|
||||
s.ctx, _ = gin.CreateTestContext(s.recorder)
|
||||
s.ctx.Request = httptest.NewRequest("GET", "/irrelevant", nil)
|
||||
s.db = test.NewDB(s.T())
|
||||
s.notified = false
|
||||
s.a = &MessageAPI{DB: s.db, Notifier: s}
|
||||
|
|
@ -51,8 +54,12 @@ func (s *MessageSuite) Notify(userID uint, msg *model.Message) {
|
|||
func (s *MessageSuite) Test_ensureCorrectJsonRepresentation() {
|
||||
t, _ := time.Parse("2006/01/02", "2017/01/02")
|
||||
|
||||
actual := model.Message{ID: 55, ApplicationID: 2, Message: "hi", Title: "hi", Date: t, Priority: 4}
|
||||
test.JSONEquals(s.T(), actual, `{"id":55,"appid":2,"message":"hi","title":"hi","priority":4,"date":"2017-01-02T00:00:00Z"}`)
|
||||
actual := &model.PagedMessages{
|
||||
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}},
|
||||
}
|
||||
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"}]}`)
|
||||
}
|
||||
|
||||
func (s *MessageSuite) Test_GetMessages() {
|
||||
|
|
@ -63,17 +70,148 @@ func (s *MessageSuite) Test_GetMessages() {
|
|||
test.WithUser(s.ctx, 5)
|
||||
s.a.GetMessages(s.ctx)
|
||||
|
||||
test.BodyEquals(s.T(), &[]model.Message{first, second}, s.recorder)
|
||||
expected := &model.PagedMessages{
|
||||
Paging: model.Paging{Limit: 100, Size: 2, Next: ""},
|
||||
Messages: []*model.Message{&second, &first},
|
||||
}
|
||||
|
||||
test.BodyEquals(s.T(), expected, s.recorder)
|
||||
}
|
||||
|
||||
func (s *MessageSuite) Test_GetMessages_WithLimit_ReturnsNext() {
|
||||
user := s.db.User(5)
|
||||
app1 := user.App(1)
|
||||
app2 := user.App(2)
|
||||
var messages []*model.Message
|
||||
for i := 100; i >= 1; i -= 2 {
|
||||
one := app2.NewMessage(uint(i))
|
||||
two := app1.NewMessage(uint(i - 1))
|
||||
messages = append(messages, &one, &two)
|
||||
}
|
||||
|
||||
s.withURL("http", "example.com", "/messages", "limit=5")
|
||||
test.WithUser(s.ctx, 5)
|
||||
s.a.GetMessages(s.ctx)
|
||||
|
||||
// Since: entries with ids from 100 - 96 will be returned (5 entries)
|
||||
expected := &model.PagedMessages{
|
||||
Paging: model.Paging{Limit: 5, Size: 5, Since: 96, Next: "http://example.com/messages?limit=5&since=96"},
|
||||
Messages: messages[:5],
|
||||
}
|
||||
|
||||
test.BodyEquals(s.T(), expected, s.recorder)
|
||||
}
|
||||
|
||||
func (s *MessageSuite) Test_GetMessages_WithLimit_WithSince_ReturnsNext() {
|
||||
user := s.db.User(5)
|
||||
app1 := user.App(1)
|
||||
app2 := user.App(2)
|
||||
var messages []*model.Message
|
||||
for i := 100; i >= 1; i -= 2 {
|
||||
one := app2.NewMessage(uint(i))
|
||||
two := app1.NewMessage(uint(i - 1))
|
||||
messages = append(messages, &one, &two)
|
||||
}
|
||||
|
||||
s.withURL("http", "example.com", "/messages", "limit=13&since=55")
|
||||
test.WithUser(s.ctx, 5)
|
||||
s.a.GetMessages(s.ctx)
|
||||
|
||||
// Since: entries with ids from 54 - 42 will be returned (13 entries)
|
||||
expected := &model.PagedMessages{
|
||||
Paging: model.Paging{Limit: 13, Size: 13, Since: 42, Next: "http://example.com/messages?limit=13&since=42"},
|
||||
Messages: messages[46 : 46+13],
|
||||
}
|
||||
test.BodyEquals(s.T(), expected, s.recorder)
|
||||
}
|
||||
|
||||
func (s *MessageSuite) Test_GetMessages_BadRequestOnInvalidLimit() {
|
||||
s.db.User(5)
|
||||
test.WithUser(s.ctx, 5)
|
||||
s.withURL("http", "example.com", "/messages", "limit=555")
|
||||
s.a.GetMessages(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 400, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *MessageSuite) Test_GetMessages_BadRequestOnInvalidLimit_Negative() {
|
||||
s.db.User(5)
|
||||
test.WithUser(s.ctx, 5)
|
||||
s.withURL("http", "example.com", "/messages", "limit=-5")
|
||||
s.a.GetMessages(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 400, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *MessageSuite) Test_GetMessagesWithToken_InvalidLimit_BadRequest() {
|
||||
s.db.User(4).App(2).NewMessage(1)
|
||||
|
||||
test.WithUser(s.ctx, 4)
|
||||
s.ctx.Params = gin.Params{{Key: "id", Value: "2"}}
|
||||
s.withURL("http", "example.com", "/messages", "limit=555")
|
||||
s.a.GetMessagesWithApplication(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 400, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *MessageSuite) Test_GetMessagesWithToken() {
|
||||
expected := s.db.User(4).App(2).NewMessage(1)
|
||||
msg := s.db.User(4).App(2).NewMessage(1)
|
||||
|
||||
test.WithUser(s.ctx, 4)
|
||||
s.ctx.Params = gin.Params{{Key: "id", Value: "2"}}
|
||||
s.a.GetMessagesWithApplication(s.ctx)
|
||||
|
||||
test.BodyEquals(s.T(), []model.Message{expected}, s.recorder)
|
||||
expected := &model.PagedMessages{
|
||||
Paging: model.Paging{Limit: 100, Size: 1, Next: ""},
|
||||
Messages: []*model.Message{&msg},
|
||||
}
|
||||
|
||||
test.BodyEquals(s.T(), expected, s.recorder)
|
||||
}
|
||||
|
||||
func (s *MessageSuite) Test_GetMessagesWithToken_WithLimit_ReturnsNext() {
|
||||
user := s.db.User(5)
|
||||
app1 := user.App(2)
|
||||
var messages []*model.Message
|
||||
for i := 100; i >= 1; i-- {
|
||||
msg := app1.NewMessage(uint(i))
|
||||
messages = append(messages, &msg)
|
||||
}
|
||||
|
||||
s.withURL("http", "example.com", "/app/2/message", "limit=9")
|
||||
test.WithUser(s.ctx, 5)
|
||||
s.ctx.Params = gin.Params{{Key: "id", Value: "2"}}
|
||||
s.a.GetMessagesWithApplication(s.ctx)
|
||||
|
||||
// Since: entries with ids from 100 - 92 will be returned (9 entries)
|
||||
expected := &model.PagedMessages{
|
||||
Paging: model.Paging{Limit: 9, Size: 9, Since: 92, Next: "http://example.com/app/2/message?limit=9&since=92"},
|
||||
Messages: messages[:9],
|
||||
}
|
||||
|
||||
test.BodyEquals(s.T(), expected, s.recorder)
|
||||
}
|
||||
|
||||
func (s *MessageSuite) Test_GetMessagesWithToken_WithLimit_WithSince_ReturnsNext() {
|
||||
user := s.db.User(5)
|
||||
app1 := user.App(2)
|
||||
var messages []*model.Message
|
||||
for i := 100; i >= 1; i-- {
|
||||
msg := app1.NewMessage(uint(i))
|
||||
messages = append(messages, &msg)
|
||||
}
|
||||
|
||||
s.withURL("http", "example.com", "/app/2/message", "limit=13&since=55")
|
||||
test.WithUser(s.ctx, 5)
|
||||
s.ctx.Params = gin.Params{{Key: "id", Value: "2"}}
|
||||
s.a.GetMessagesWithApplication(s.ctx)
|
||||
|
||||
// Since: entries with ids from 54 - 42 will be returned (13 entries)
|
||||
expected := &model.PagedMessages{
|
||||
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],
|
||||
}
|
||||
test.BodyEquals(s.T(), expected, s.recorder)
|
||||
}
|
||||
|
||||
func (s *MessageSuite) Test_GetMessagesWithToken_withWrongUser_expectNotFound() {
|
||||
|
|
@ -298,3 +436,8 @@ func (s *MessageSuite) Test_CreateMessage_onFormData() {
|
|||
assert.Equal(s.T(), 200, s.recorder.Code)
|
||||
assert.True(s.T(), s.notified)
|
||||
}
|
||||
|
||||
func (s *MessageSuite) withURL(scheme, host, path, query string) {
|
||||
s.ctx.Request.URL = &url.URL{Path: path, RawQuery: query}
|
||||
s.ctx.Set("location", &url.URL{Scheme: scheme, Host: host})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
package model
|
||||
|
||||
// Paging Model
|
||||
//
|
||||
// The Paging holds holds information about the limit and making requests to the next page.
|
||||
//
|
||||
// swagger:model Paging
|
||||
type Paging struct {
|
||||
// The request url for the next page. Empty/Null when no next page is available.
|
||||
//
|
||||
// read only: true
|
||||
// required: false
|
||||
// example: http://example.com/message?limit=50&since=123456
|
||||
Next string `json:"next,omitempty"`
|
||||
// The amount of messages that got returned in the current request.
|
||||
//
|
||||
// read only: true
|
||||
// required: true
|
||||
// example: 5
|
||||
Size int `json:"size"`
|
||||
// The ID of the last message returned in the current request. Use this as alternative to the next link.
|
||||
//
|
||||
// read only: true
|
||||
// required: true
|
||||
// example: 5
|
||||
// min: 0
|
||||
Since uint `json:"since"`
|
||||
// The limit of the messages for the current request.
|
||||
//
|
||||
// read only: true
|
||||
// required: true
|
||||
// min: 1
|
||||
// max: 200
|
||||
// example: 123
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
// PagedMessages Model
|
||||
//
|
||||
// Wrapper for the paging and the messages
|
||||
//
|
||||
// swagger:model PagedMessages
|
||||
type PagedMessages struct {
|
||||
// The paging of the messages.
|
||||
//
|
||||
// read only: true
|
||||
// required: true
|
||||
Paging Paging `json:"paging"`
|
||||
// The messages.
|
||||
//
|
||||
// read only: true
|
||||
// required: true
|
||||
Messages []*Message `json:"messages"`
|
||||
}
|
||||
Loading…
Reference in New Issue