[#34] Adjust message api to be paged

This commit is contained in:
Jannis Mattheis 2018-04-08 14:32:47 +02:00 committed by Jannis Mattheis
parent 53aec56ca5
commit 39a3d46607
3 changed files with 255 additions and 15 deletions

View File

@ -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,23 +37,62 @@ 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) {
withPaging(ctx, func(params *pagingParams) {
if app := a.DB.GetApplicationByID(id); app != nil && app.UserID == auth.GetUserID(ctx) {
messages := a.DB.GetMessagesByApplication(id)
ctx.JSON(200, messages)
// 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"))
}
})
})
}
// DeleteMessages delete all messages from a user.

View File

@ -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})
}

54
model/paging.go Normal file
View File

@ -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"`
}