From 62d7df97106dbd088daed3a3e506d120b41e100d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Wed, 30 Nov 2022 17:58:04 +0100 Subject: [PATCH] Tests[TestSendSimpleMessageJSON] --- server/Makefile | 2 +- server/api/apihighlight/highlights.go | 16 +++ server/api/handler/message.go | 146 ++++++++++----------- server/api/router.go | 2 +- server/common/ginresp/resp.go | 11 +- server/db/channels.go | 22 ++++ server/logic/application.go | 6 +- server/push/testSink.go | 8 +- server/swagger/swagger.json | 127 +++++++++++++++---- server/swagger/swagger.yaml | 174 ++++++++++++++++++-------- server/test/message_test.go | 88 +++++++++++++ server/test/user_test.go | 9 +- server/test/util/common.go | 25 ++++ server/test/util/requests.go | 48 ++++--- 14 files changed, 496 insertions(+), 188 deletions(-) create mode 100644 server/api/apihighlight/highlights.go create mode 100644 server/test/message_test.go diff --git a/server/Makefile b/server/Makefile index 975e424..2623ac9 100644 --- a/server/Makefile +++ b/server/Makefile @@ -31,7 +31,7 @@ docker: build .PHONY: swagger swagger: which swag || go install github.com/swaggo/swag/cmd/swag@latest - swag init -generalInfo api/router.go --output ./swagger/ --outputTypes "json,yaml" + swag init -generalInfo api/router.go --propertyStrategy snakecase --output ./swagger/ --outputTypes "json,yaml" run-docker-local: docker mkdir -p .run-data diff --git a/server/api/apihighlight/highlights.go b/server/api/apihighlight/highlights.go new file mode 100644 index 0000000..3e4fe9b --- /dev/null +++ b/server/api/apihighlight/highlights.go @@ -0,0 +1,16 @@ +package apihighlight + +type ErrHighlight int + +//goland:noinspection GoSnakeCaseUsage +const ( + NONE ErrHighlight = -1 + USER_ID ErrHighlight = 101 + USER_KEY ErrHighlight = 102 + TITLE ErrHighlight = 103 + CONTENT ErrHighlight = 104 + PRIORITY ErrHighlight = 105 + CHANNEL ErrHighlight = 106 + SENDER_NAME ErrHighlight = 107 + USER_MESSAGE_ID ErrHighlight = 108 +) diff --git a/server/api/handler/message.go b/server/api/handler/message.go index f7f4fb9..52f5b8b 100644 --- a/server/api/handler/message.go +++ b/server/api/handler/message.go @@ -2,6 +2,7 @@ package handler import ( "blackforestbytes.com/simplecloudnotifier/api/apierr" + hl "blackforestbytes.com/simplecloudnotifier/api/apihighlight" "blackforestbytes.com/simplecloudnotifier/common/ginresp" "blackforestbytes.com/simplecloudnotifier/db" "blackforestbytes.com/simplecloudnotifier/logic" @@ -38,8 +39,8 @@ func NewMessageHandler(app *logic.Application) MessageHandler { // @Description All parameter can be set via query-parameter or form-data body. Only UserID, UserKey and Title are required // @Tags External // -// @Param query_data query handler.SendMessageCompat.query false " " -// @Param form_data formData handler.SendMessageCompat.form false " " +// @Param query_data query handler.SendMessageCompat.combined false " " +// @Param form_data formData handler.SendMessageCompat.combined false " " // // @Success 200 {object} handler.sendMessageInternal.response // @Failure 400 {object} ginresp.apiError @@ -49,7 +50,7 @@ func NewMessageHandler(app *logic.Application) MessageHandler { // // @Router /send.php [POST] func (h MessageHandler) SendMessageCompat(g *gin.Context) ginresp.HTTPResponse { - type query struct { + type combined struct { UserID *models.UserID `json:"user_id" form:"user_id"` UserKey *string `json:"user_key" form:"user_key"` Title *string `json:"title" form:"title"` @@ -58,18 +59,9 @@ func (h MessageHandler) SendMessageCompat(g *gin.Context) ginresp.HTTPResponse { UserMessageID *string `json:"msg_id" form:"msg_id"` SendTimestamp *float64 `json:"timestamp" form:"timestamp"` } - type form struct { - UserID *models.UserID `form:"user_id"` - UserKey *string `form:"user_key"` - Title *string `form:"title"` - Content *string `form:"content"` - Priority *int `form:"priority"` - UserMessageID *string `form:"msg_id"` - SendTimestamp *float64 `form:"timestamp"` - } - var f form - var q query + var f combined + var q combined ctx, errResp := h.app.StartRequest(g, nil, &q, nil, &f) if errResp != nil { return *errResp @@ -88,9 +80,9 @@ func (h MessageHandler) SendMessageCompat(g *gin.Context) ginresp.HTTPResponse { // @Description All parameter can be set via query-parameter or the json body. Only UserID, UserKey and Title are required // @Tags External // -// @Param query_data query handler.SendMessage.query false " " -// @Param post_body body handler.SendMessage.body false " " -// @Param form_body formData handler.SendMessage.body false " " +// @Param query_data query handler.SendMessage.combined false " " +// @Param post_body body handler.SendMessage.combined false " " +// @Param form_body formData handler.SendMessage.combined false " " // // @Success 200 {object} handler.sendMessageInternal.response // @Failure 400 {object} ginresp.apiError @@ -101,46 +93,22 @@ func (h MessageHandler) SendMessageCompat(g *gin.Context) ginresp.HTTPResponse { // @Router / [POST] // @Router /send [POST] func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse { - type query struct { - UserID *models.UserID `json:"user_id" form:"user_id"` - UserKey *string `json:"user_key" form:"user_key"` - Channel *string `json:"channel" form:"channel"` - ChanKey *string `json:"chan_key" form:"chan_key"` - Title *string `json:"title" form:"title"` - Content *string `json:"content" form:"content"` - Priority *int `json:"priority" form:"priority"` - UserMessageID *string `json:"msg_id" form:"msg_id"` - SendTimestamp *float64 `json:"timestamp" form:"timestamp"` - SenderName *string `json:"sender_name" form:"sender_name"` - } - type body struct { - UserID *models.UserID `json:"user_id"` - UserKey *string `json:"user_key"` - Channel *string `json:"channel"` - ChanKey *string `json:"chan_key"` - Title *string `json:"title"` - Content *string `json:"content"` - Priority *int `json:"priority"` - UserMessageID *string `json:"msg_id"` - SendTimestamp *float64 `json:"timestamp"` - SenderName *string `json:"sender_name"` - } - type form struct { - UserID *models.UserID `form:"user_id"` - UserKey *string `form:"user_key"` - Channel *string `form:"channel"` - ChanKey *string `form:"chan_key"` - Title *string `form:"title"` - Content *string `form:"content"` - Priority *int `form:"priority"` - UserMessageID *string `form:"msg_id"` - SendTimestamp *float64 `form:"timestamp"` - SenderName *string `form:"sender_name"` + type combined struct { + UserID *models.UserID `json:"user_id" form:"user_id" example:"7725" ` + UserKey *string `json:"user_key" form:"user_key" example:"P3TNH8mvv14fm" ` + Channel *string `json:"channel" form:"channel" example:"test" ` + ChanKey *string `json:"chan_key" form:"chan_key" example:"qhnUbKcLgp6tg" ` + Title *string `json:"title" form:"title" example:"Hello World" ` + Content *string `json:"content" form:"content" example:"This is a message" ` + Priority *int `json:"priority" form:"priority" example:"1" enums:"0,1,2" ` + UserMessageID *string `json:"msg_id" form:"msg_id" example:"db8b0e6a-a08c-4646" ` + SendTimestamp *float64 `json:"timestamp" form:"timestamp" example:"1669824037" ` + SenderName *string `json:"sender_name" form:"sender_name" example:"example-server" ` } - var b body - var q query - var f form + var b combined + var q combined + var f combined ctx, errResp := h.app.StartRequest(g, nil, &q, &b, &f) if errResp != nil { return *errResp @@ -175,30 +143,30 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex } if UserID == nil { - return ginresp.SendAPIError(g, 400, apierr.MISSING_UID, 101, "Missing parameter [[user_id]]", nil) + return ginresp.SendAPIError(g, 400, apierr.MISSING_UID, hl.USER_ID, "Missing parameter [[user_id]]", nil) } if UserKey == nil { - return ginresp.SendAPIError(g, 400, apierr.MISSING_TOK, 102, "Missing parameter [[user_token]]", nil) + return ginresp.SendAPIError(g, 400, apierr.MISSING_TOK, hl.USER_KEY, "Missing parameter [[user_token]]", nil) } if Title == nil { - return ginresp.SendAPIError(g, 400, apierr.MISSING_TITLE, 103, "Missing parameter [[title]]", nil) + return ginresp.SendAPIError(g, 400, apierr.MISSING_TITLE, hl.TITLE, "Missing parameter [[title]]", nil) } if SendTimestamp != nil && mathext.Abs(*SendTimestamp-float64(time.Now().Unix())) > (24*time.Hour).Seconds() { - return ginresp.SendAPIError(g, 400, apierr.TIMESTAMP_OUT_OF_RANGE, -1, "The timestamp mus be within 24 hours of now()", nil) + return ginresp.SendAPIError(g, 400, apierr.TIMESTAMP_OUT_OF_RANGE, hl.NONE, "The timestamp mus be within 24 hours of now()", nil) } if Priority != nil && (*Priority != 0 && *Priority != 1 && *Priority != 2) { - return ginresp.SendAPIError(g, 400, apierr.INVALID_PRIO, 105, "Invalid priority", nil) + return ginresp.SendAPIError(g, 400, apierr.INVALID_PRIO, hl.PRIORITY, "Invalid priority", nil) } if len(*Title) == 0 { - return ginresp.SendAPIError(g, 400, apierr.NO_TITLE, 103, "No title specified", nil) + return ginresp.SendAPIError(g, 400, apierr.NO_TITLE, hl.TITLE, "No title specified", nil) } user, err := h.database.GetUser(ctx, *UserID) if err == sql.ErrNoRows { - return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, -1, "User not found", nil) + return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found", nil) } if err != nil { - return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to query user", err) + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query user", err) } channelName := user.DefaultChannel() @@ -207,25 +175,25 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex } if len(*Title) > user.MaxTitleLength() { - return ginresp.SendAPIError(g, 400, apierr.TITLE_TOO_LONG, 103, fmt.Sprintf("Title too long (max %d characters)", user.MaxTitleLength()), nil) + return ginresp.SendAPIError(g, 400, apierr.TITLE_TOO_LONG, hl.TITLE, fmt.Sprintf("Title too long (max %d characters)", user.MaxTitleLength()), nil) } if Content != nil && len(*Content) > user.MaxContentLength() { - return ginresp.SendAPIError(g, 400, apierr.CONTENT_TOO_LONG, 104, fmt.Sprintf("Content too long (%d characters; max := %d characters)", len(*Content), user.MaxContentLength()), nil) + return ginresp.SendAPIError(g, 400, apierr.CONTENT_TOO_LONG, hl.CONTENT, fmt.Sprintf("Content too long (%d characters; max := %d characters)", len(*Content), user.MaxContentLength()), nil) } if len(channelName) > user.MaxChannelNameLength() { - return ginresp.SendAPIError(g, 400, apierr.CONTENT_TOO_LONG, 106, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil) + return ginresp.SendAPIError(g, 400, apierr.CONTENT_TOO_LONG, hl.CHANNEL, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil) } if SenderName != nil && len(*SenderName) > user.MaxSenderName() { - return ginresp.SendAPIError(g, 400, apierr.SENDERNAME_TOO_LONG, 107, fmt.Sprintf("SenderName too long (max %d characters)", user.MaxSenderName()), nil) + return ginresp.SendAPIError(g, 400, apierr.SENDERNAME_TOO_LONG, hl.SENDER_NAME, fmt.Sprintf("SenderName too long (max %d characters)", user.MaxSenderName()), nil) } if UserMessageID != nil && len(*UserMessageID) > user.MaxUserMessageID() { - return ginresp.SendAPIError(g, 400, apierr.USR_MSG_ID_TOO_LONG, -1, fmt.Sprintf("MessageID too long (max %d characters)", user.MaxUserMessageID()), nil) + return ginresp.SendAPIError(g, 400, apierr.USR_MSG_ID_TOO_LONG, hl.USER_MESSAGE_ID, fmt.Sprintf("MessageID too long (max %d characters)", user.MaxUserMessageID()), nil) } if UserMessageID != nil { msg, err := h.database.GetMessageByUserMessageID(ctx, *UserMessageID) if err != nil { - return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to query existing message", err) + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query existing message", err) } if msg != nil { return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{ @@ -244,12 +212,28 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex } if user.QuotaRemainingToday() <= 0 { - return ginresp.SendAPIError(g, 403, apierr.QUOTA_REACHED, -1, fmt.Sprintf("Daily quota reached (%d)", user.QuotaPerDay()), nil) + return ginresp.SendAPIError(g, 403, apierr.QUOTA_REACHED, hl.NONE, fmt.Sprintf("Daily quota reached (%d)", user.QuotaPerDay()), nil) } - channel, err := h.app.GetOrCreateChannel(ctx, *UserID, channelName) - if err != nil { - return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to query/create channel", err) + var channel models.Channel + if ChanKey != nil { + // foreign channel (+ channel send-key) + + foreignChan, err := h.database.GetChannelByNameAndSendKey(ctx, channelName, *ChanKey) + if err != nil { + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query (foreign) channel", err) + } + if foreignChan == nil { + return ginresp.SendAPIError(g, 400, apierr.CHANNEL_NOT_FOUND, hl.CHANNEL, "(Foreign) Channel not found", err) + } + channel = *foreignChan + } else { + // own channel + + channel, err = h.app.GetOrCreateChannel(ctx, *UserID, channelName) + if err != nil { + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query/create (owned) channel", err) + } } selfChanAdmin := *UserID == channel.OwnerUserID && *UserKey == user.AdminKey @@ -257,7 +241,7 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex forgChanSend := *UserID != channel.OwnerUserID && ChanKey != nil && *ChanKey == channel.SendKey if !selfChanAdmin && !selfChanSend && !forgChanSend { - return ginresp.SendAPIError(g, 401, apierr.USER_AUTH_FAILED, 102, fmt.Sprintf("Daily quota reached (%d)", user.QuotaPerDay()), nil) + return ginresp.SendAPIError(g, 401, apierr.USER_AUTH_FAILED, hl.USER_KEY, "You are not authorized for this action", nil) } var sendTimestamp *time.Time = nil @@ -271,28 +255,28 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex msg, err := h.database.CreateMessage(ctx, *UserID, channel, sendTimestamp, *Title, Content, priority, UserMessageID, clientIP, SenderName) if err != nil { - return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to create message in db", err) + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create message in db", err) } subscriptions, err := h.database.ListSubscriptionsByChannel(ctx, channel.ChannelID) if err != nil { - return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to query subscriptions", err) + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query subscriptions", err) } err = h.database.IncUserMessageCounter(ctx, user) if err != nil { - return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to inc user msg-counter", err) + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to inc user msg-counter", err) } err = h.database.IncChannelMessageCounter(ctx, channel) if err != nil { - return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to inc channel msg-counter", err) + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to inc channel msg-counter", err) } for _, sub := range subscriptions { clients, err := h.database.ListClients(ctx, sub.SubscriberUserID) if err != nil { - return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to query clients", err) + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query clients", err) } if !sub.Confirmed { @@ -305,12 +289,12 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex if err != nil { _, err = h.database.CreateRetryDelivery(ctx, client, msg) if err != nil { - return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to create delivery", err) + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create delivery", err) } } else { _, err = h.database.CreateSuccessDelivery(ctx, client, msg, *fcmDelivID) if err != nil { - return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, -1, "Failed to create delivery", err) + return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to create delivery", err) } } diff --git a/server/api/router.go b/server/api/router.go index 55a2bd9..60d0ab6 100644 --- a/server/api/router.go +++ b/server/api/router.go @@ -38,10 +38,10 @@ func NewRouter(app *logic.Application) *Router { // @description API for SCN // @host scn.blackforestbytes.com // -// @tag.name Common // @tag.name External // @tag.name API-v1 // @tag.name API-v2 +// @tag.name Common // // @BasePath / func (r *Router) Init(e *gin.Engine) { diff --git a/server/common/ginresp/resp.go b/server/common/ginresp/resp.go index 59388d5..4f7e121 100644 --- a/server/common/ginresp/resp.go +++ b/server/common/ginresp/resp.go @@ -3,6 +3,7 @@ package ginresp import ( scn "blackforestbytes.com/simplecloudnotifier" "blackforestbytes.com/simplecloudnotifier/api/apierr" + "blackforestbytes.com/simplecloudnotifier/api/apihighlight" "fmt" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" @@ -73,7 +74,7 @@ func APIError(g *gin.Context, status int, errorid apierr.APIError, msg string, e return createApiError(g, "APIError", status, errorid, 0, msg, e) } -func SendAPIError(g *gin.Context, status int, errorid apierr.APIError, highlight int, msg string, e error) HTTPResponse { +func SendAPIError(g *gin.Context, status int, errorid apierr.APIError, highlight apihighlight.ErrHighlight, msg string, e error) HTTPResponse { return createApiError(g, "SendAPIError", status, errorid, highlight, msg, e) } @@ -81,7 +82,7 @@ func NotImplemented(g *gin.Context) HTTPResponse { return createApiError(g, "NotImplemented", 500, apierr.UNDEFINED, 0, "Not Implemented", nil) } -func createApiError(g *gin.Context, ident string, status int, errorid apierr.APIError, highlight int, msg string, e error) HTTPResponse { +func createApiError(g *gin.Context, ident string, status int, errorid apierr.APIError, highlight apihighlight.ErrHighlight, msg string, e error) HTTPResponse { reqUri := "" if g != nil && g.Request != nil { reqUri = g.Request.Method + " :: " + g.Request.RequestURI @@ -89,7 +90,7 @@ func createApiError(g *gin.Context, ident string, status int, errorid apierr.API log.Error(). Int("errorid", int(errorid)). - Int("highlight", highlight). + Int("highlight", int(highlight)). Str("uri", reqUri). AnErr("err", e). Stack(). @@ -101,7 +102,7 @@ func createApiError(g *gin.Context, ident string, status int, errorid apierr.API data: apiError{ Success: false, Error: int(errorid), - ErrorHighlight: highlight, + ErrorHighlight: int(highlight), Message: msg, RawError: fmt.Sprintf("%+v", e), Trace: string(debug.Stack()), @@ -113,7 +114,7 @@ func createApiError(g *gin.Context, ident string, status int, errorid apierr.API data: apiError{ Success: false, Error: int(errorid), - ErrorHighlight: highlight, + ErrorHighlight: int(highlight), Message: msg, }, } diff --git a/server/db/channels.go b/server/db/channels.go index f55936d..afe33fc 100644 --- a/server/db/channels.go +++ b/server/db/channels.go @@ -28,6 +28,28 @@ func (db *Database) GetChannelByName(ctx TxContext, userid models.UserID, chanNa return &channel, nil } +func (db *Database) GetChannelByNameAndSendKey(ctx TxContext, chanName string, sendKey string) (*models.Channel, error) { + tx, err := ctx.GetOrCreateTransaction(db) + if err != nil { + return nil, err + } + + rows, err := tx.QueryContext(ctx, "SELECT * FROM channels WHERE name = ? OR send_key = ? LIMIT 1", chanName, sendKey) + if err != nil { + return nil, err + } + + channel, err := models.DecodeChannel(rows) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + + return &channel, nil +} + func (db *Database) CreateChannel(ctx TxContext, userid models.UserID, name string, subscribeKey string, sendKey string) (models.Channel, error) { tx, err := ctx.GetOrCreateTransaction(db) if err != nil { diff --git a/server/logic/application.go b/server/logic/application.go index 1fd9c02..bb35f1f 100644 --- a/server/logic/application.go +++ b/server/logic/application.go @@ -28,7 +28,7 @@ type Application struct { Config scn.Config Gin *gin.Engine Database *db.Database - Firebase push.NotificationClient + Pusher push.NotificationClient AndroidPublisher google.AndroidPublisherClient Jobs []Job stopChan chan bool @@ -45,7 +45,7 @@ func NewApp(db *db.Database) *Application { func (app *Application) Init(cfg scn.Config, g *gin.Engine, fb push.NotificationClient, apc google.AndroidPublisherClient, jobs []Job) { app.Config = cfg app.Gin = g - app.Firebase = fb + app.Pusher = fb app.AndroidPublisher = apc app.Jobs = jobs } @@ -314,7 +314,7 @@ func (app *Application) NormalizeUsername(v string) string { func (app *Application) DeliverMessage(ctx context.Context, client models.Client, msg models.Message) (*string, error) { if client.FCMToken != nil { - fcmDelivID, err := app.Firebase.SendNotification(ctx, client, msg) + fcmDelivID, err := app.Pusher.SendNotification(ctx, client, msg) if err != nil { log.Warn().Int64("SCNMessageID", msg.SCNMessageID.IntID()).Int64("ClientID", client.ClientID.IntID()).Err(err).Msg("FCM Delivery failed") return nil, err diff --git a/server/push/testSink.go b/server/push/testSink.go index 1786dbe..bfa5006 100644 --- a/server/push/testSink.go +++ b/server/push/testSink.go @@ -13,13 +13,17 @@ type SinkData struct { } type TestSink struct { - data []SinkData + Data []SinkData } func NewTestSink() NotificationClient { return &TestSink{} } +func (d *TestSink) Last() SinkData { + return d.Data[len(d.Data)-1] +} + func (d *TestSink) SendNotification(ctx context.Context, client models.Client, msg models.Message) (string, error) { id, err := langext.NewHexUUID() if err != nil { @@ -28,7 +32,7 @@ func (d *TestSink) SendNotification(ctx context.Context, client models.Client, m key := "TestSink[" + id + "]" - d.data = append(d.data, SinkData{ + d.Data = append(d.Data, SinkData{ Message: msg, Client: client, }) diff --git a/server/swagger/swagger.json b/server/swagger/swagger.json index 9b11389..632c998 100644 --- a/server/swagger/swagger.json +++ b/server/swagger/swagger.json @@ -19,51 +19,66 @@ "parameters": [ { "type": "string", + "example": "qhnUbKcLgp6tg", "name": "chan_key", "in": "query" }, { "type": "string", + "example": "test", "name": "channel", "in": "query" }, { "type": "string", + "example": "This is a message", "name": "content", "in": "query" }, { "type": "string", + "example": "db8b0e6a-a08c-4646", "name": "msg_id", "in": "query" }, { + "enum": [ + 0, + 1, + 2 + ], "type": "integer", + "example": 1, "name": "priority", "in": "query" }, { "type": "string", + "example": "example-server", "name": "sender_name", "in": "query" }, { "type": "number", + "example": 1669824037, "name": "timestamp", "in": "query" }, { "type": "string", + "example": "Hello World", "name": "title", "in": "query" }, { "type": "integer", + "example": 7725, "name": "user_id", "in": "query" }, { "type": "string", + "example": "P3TNH8mvv14fm", "name": "user_key", "in": "query" }, @@ -72,56 +87,71 @@ "name": "post_body", "in": "body", "schema": { - "$ref": "#/definitions/handler.SendMessage.body" + "$ref": "#/definitions/handler.SendMessage.combined" } }, { "type": "string", + "example": "qhnUbKcLgp6tg", "name": "chan_key", "in": "formData" }, { "type": "string", + "example": "test", "name": "channel", "in": "formData" }, { "type": "string", + "example": "This is a message", "name": "content", "in": "formData" }, { "type": "string", + "example": "db8b0e6a-a08c-4646", "name": "msg_id", "in": "formData" }, { + "enum": [ + 0, + 1, + 2 + ], "type": "integer", + "example": 1, "name": "priority", "in": "formData" }, { "type": "string", + "example": "example-server", "name": "sender_name", "in": "formData" }, { "type": "number", + "example": 1669824037, "name": "timestamp", "in": "formData" }, { "type": "string", + "example": "Hello World", "name": "title", "in": "formData" }, { "type": "integer", + "example": 7725, "name": "user_id", "in": "formData" }, { "type": "string", + "example": "P3TNH8mvv14fm", "name": "user_key", "in": "formData" } @@ -1959,51 +1989,66 @@ "parameters": [ { "type": "string", + "example": "qhnUbKcLgp6tg", "name": "chan_key", "in": "query" }, { "type": "string", + "example": "test", "name": "channel", "in": "query" }, { "type": "string", + "example": "This is a message", "name": "content", "in": "query" }, { "type": "string", + "example": "db8b0e6a-a08c-4646", "name": "msg_id", "in": "query" }, { + "enum": [ + 0, + 1, + 2 + ], "type": "integer", + "example": 1, "name": "priority", "in": "query" }, { "type": "string", + "example": "example-server", "name": "sender_name", "in": "query" }, { "type": "number", + "example": 1669824037, "name": "timestamp", "in": "query" }, { "type": "string", + "example": "Hello World", "name": "title", "in": "query" }, { "type": "integer", + "example": 7725, "name": "user_id", "in": "query" }, { "type": "string", + "example": "P3TNH8mvv14fm", "name": "user_key", "in": "query" }, @@ -2012,56 +2057,71 @@ "name": "post_body", "in": "body", "schema": { - "$ref": "#/definitions/handler.SendMessage.body" + "$ref": "#/definitions/handler.SendMessage.combined" } }, { "type": "string", + "example": "qhnUbKcLgp6tg", "name": "chan_key", "in": "formData" }, { "type": "string", + "example": "test", "name": "channel", "in": "formData" }, { "type": "string", + "example": "This is a message", "name": "content", "in": "formData" }, { "type": "string", + "example": "db8b0e6a-a08c-4646", "name": "msg_id", "in": "formData" }, { + "enum": [ + 0, + 1, + 2 + ], "type": "integer", + "example": 1, "name": "priority", "in": "formData" }, { "type": "string", + "example": "example-server", "name": "sender_name", "in": "formData" }, { "type": "number", + "example": 1669824037, "name": "timestamp", "in": "formData" }, { "type": "string", + "example": "Hello World", "name": "title", "in": "formData" }, { "type": "integer", + "example": 7725, "name": "user_id", "in": "formData" }, { "type": "string", + "example": "P3TNH8mvv14fm", "name": "user_key", "in": "formData" } @@ -2149,6 +2209,11 @@ "name": "content", "in": "formData" }, + { + "type": "string", + "name": "msg_id", + "in": "formData" + }, { "type": "integer", "name": "priority", @@ -2156,7 +2221,7 @@ }, { "type": "number", - "name": "sendTimestamp", + "name": "timestamp", "in": "formData" }, { @@ -2166,17 +2231,12 @@ }, { "type": "integer", - "name": "userID", + "name": "user_id", "in": "formData" }, { "type": "string", - "name": "userKey", - "in": "formData" - }, - { - "type": "string", - "name": "userMessageID", + "name": "user_key", "in": "formData" } ], @@ -2297,13 +2357,13 @@ "type": "object", "required": [ "channel", - "channelOwnerUserID" + "channel_owner_user_id" ], "properties": { "channel": { "type": "string" }, - "channelOwnerUserID": { + "channel_owner_user_id": { "type": "integer" } } @@ -2529,38 +2589,53 @@ } } }, - "handler.SendMessage.body": { + "handler.SendMessage.combined": { "type": "object", "properties": { "chan_key": { - "type": "string" + "type": "string", + "example": "qhnUbKcLgp6tg" }, "channel": { - "type": "string" + "type": "string", + "example": "test" }, "content": { - "type": "string" + "type": "string", + "example": "This is a message" }, "msg_id": { - "type": "string" + "type": "string", + "example": "db8b0e6a-a08c-4646" }, "priority": { - "type": "integer" + "type": "integer", + "enum": [ + 0, + 1, + 2 + ], + "example": 1 }, "sender_name": { - "type": "string" + "type": "string", + "example": "example-server" }, "timestamp": { - "type": "number" + "type": "number", + "example": 1669824037 }, "title": { - "type": "string" + "type": "string", + "example": "Hello World" }, "user_id": { - "type": "integer" + "type": "integer", + "example": 7725 }, "user_key": { - "type": "string" + "type": "string", + "example": "P3TNH8mvv14fm" } } }, @@ -2948,9 +3023,6 @@ } }, "tags": [ - { - "name": "Common" - }, { "name": "External" }, @@ -2959,6 +3031,9 @@ }, { "name": "API-v2" + }, + { + "name": "Common" } ] } \ No newline at end of file diff --git a/server/swagger/swagger.yaml b/server/swagger/swagger.yaml index 4add681..d194fce 100644 --- a/server/swagger/swagger.yaml +++ b/server/swagger/swagger.yaml @@ -55,11 +55,11 @@ definitions: properties: channel: type: string - channelOwnerUserID: + channel_owner_user_id: type: integer required: - channel - - channelOwnerUserID + - channel_owner_user_id type: object handler.CreateUser.body: properties: @@ -204,27 +204,41 @@ definitions: success: type: boolean type: object - handler.SendMessage.body: + handler.SendMessage.combined: properties: chan_key: + example: qhnUbKcLgp6tg type: string channel: + example: test type: string content: + example: This is a message type: string msg_id: + example: db8b0e6a-a08c-4646 type: string priority: + enum: + - 0 + - 1 + - 2 + example: 1 type: integer sender_name: + example: example-server type: string timestamp: + example: 1669824037 type: number title: + example: Hello World type: string user_id: + example: 7725 type: integer user_key: + example: P3TNH8mvv14fm type: string type: object handler.Sleep.response: @@ -490,69 +504,97 @@ paths: description: All parameter can be set via query-parameter or the json body. Only UserID, UserKey and Title are required parameters: - - in: query + - example: qhnUbKcLgp6tg + in: query name: chan_key type: string - - in: query + - example: test + in: query name: channel type: string - - in: query + - example: This is a message + in: query name: content type: string - - in: query + - example: db8b0e6a-a08c-4646 + in: query name: msg_id type: string - - in: query + - enum: + - 0 + - 1 + - 2 + example: 1 + in: query name: priority type: integer - - in: query + - example: example-server + in: query name: sender_name type: string - - in: query + - example: 1669824037 + in: query name: timestamp type: number - - in: query + - example: Hello World + in: query name: title type: string - - in: query + - example: 7725 + in: query name: user_id type: integer - - in: query + - example: P3TNH8mvv14fm + in: query name: user_key type: string - description: ' ' in: body name: post_body schema: - $ref: '#/definitions/handler.SendMessage.body' - - in: formData + $ref: '#/definitions/handler.SendMessage.combined' + - example: qhnUbKcLgp6tg + in: formData name: chan_key type: string - - in: formData + - example: test + in: formData name: channel type: string - - in: formData + - example: This is a message + in: formData name: content type: string - - in: formData + - example: db8b0e6a-a08c-4646 + in: formData name: msg_id type: string - - in: formData + - enum: + - 0 + - 1 + - 2 + example: 1 + in: formData name: priority type: integer - - in: formData + - example: example-server + in: formData name: sender_name type: string - - in: formData + - example: 1669824037 + in: formData name: timestamp type: number - - in: formData + - example: Hello World + in: formData name: title type: string - - in: formData + - example: 7725 + in: formData name: user_id type: integer - - in: formData + - example: P3TNH8mvv14fm + in: formData name: user_key type: string responses: @@ -1801,69 +1843,97 @@ paths: description: All parameter can be set via query-parameter or the json body. Only UserID, UserKey and Title are required parameters: - - in: query + - example: qhnUbKcLgp6tg + in: query name: chan_key type: string - - in: query + - example: test + in: query name: channel type: string - - in: query + - example: This is a message + in: query name: content type: string - - in: query + - example: db8b0e6a-a08c-4646 + in: query name: msg_id type: string - - in: query + - enum: + - 0 + - 1 + - 2 + example: 1 + in: query name: priority type: integer - - in: query + - example: example-server + in: query name: sender_name type: string - - in: query + - example: 1669824037 + in: query name: timestamp type: number - - in: query + - example: Hello World + in: query name: title type: string - - in: query + - example: 7725 + in: query name: user_id type: integer - - in: query + - example: P3TNH8mvv14fm + in: query name: user_key type: string - description: ' ' in: body name: post_body schema: - $ref: '#/definitions/handler.SendMessage.body' - - in: formData + $ref: '#/definitions/handler.SendMessage.combined' + - example: qhnUbKcLgp6tg + in: formData name: chan_key type: string - - in: formData + - example: test + in: formData name: channel type: string - - in: formData + - example: This is a message + in: formData name: content type: string - - in: formData + - example: db8b0e6a-a08c-4646 + in: formData name: msg_id type: string - - in: formData + - enum: + - 0 + - 1 + - 2 + example: 1 + in: formData name: priority type: integer - - in: formData + - example: example-server + in: formData name: sender_name type: string - - in: formData + - example: 1669824037 + in: formData name: timestamp type: number - - in: formData + - example: Hello World + in: formData name: title type: string - - in: formData + - example: 7725 + in: formData name: user_id type: integer - - in: formData + - example: P3TNH8mvv14fm + in: formData name: user_key type: string responses: @@ -1921,23 +1991,23 @@ paths: - in: formData name: content type: string + - in: formData + name: msg_id + type: string - in: formData name: priority type: integer - in: formData - name: sendTimestamp + name: timestamp type: number - in: formData name: title type: string - in: formData - name: userID + name: user_id type: integer - in: formData - name: userKey - type: string - - in: formData - name: userMessageID + name: user_key type: string responses: "200": @@ -1965,7 +2035,7 @@ paths: - External swagger: "2.0" tags: -- name: Common - name: External - name: API-v1 - name: API-v2 +- name: Common diff --git a/server/test/message_test.go b/server/test/message_test.go new file mode 100644 index 0000000..c8e386f --- /dev/null +++ b/server/test/message_test.go @@ -0,0 +1,88 @@ +package test + +import ( + "blackforestbytes.com/simplecloudnotifier/push" + tt "blackforestbytes.com/simplecloudnotifier/test/util" + "fmt" + "github.com/gin-gonic/gin" + "testing" +) + +func TestSendSimpleMessageJSON(t *testing.T) { + ws, stop := tt.StartSimpleWebserver(t) + defer stop() + + pusher := ws.Pusher.(*push.TestSink) + + baseUrl := "http://127.0.0.1:" + ws.Port + + r0 := tt.RequestPost[gin.H](t, baseUrl, "/api/users", gin.H{ + "agent_model": "DUMMY_PHONE", + "agent_version": "4X", + "client_type": "ANDROID", + "fcm_token": "DUMMY_FCM", + }) + + uid := int(r0["user_id"].(float64)) + admintok := r0["admin_key"].(string) + readtok := r0["read_key"].(string) + sendtok := r0["send_key"].(string) + + msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ + "user_key": sendtok, + "user_id": uid, + "title": "HelloWorld_001", + }) + + tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{ + "user_key": readtok, + "user_id": uid, + "title": "HelloWorld_001", + }, 401, 1311) + + tt.AssertEqual(t, "messageCount", 1, len(pusher.Data)) + tt.AssertEqual(t, "msg.title", "HelloWorld_001", pusher.Last().Message.Title) + tt.AssertStrRepEqual(t, "msg.scn_msg_id", msg1["scn_msg_id"], pusher.Last().Message.SCNMessageID) + + type mglist struct { + Messages []gin.H `json:"messages"` + } + + msgList1 := tt.RequestAuthGet[mglist](t, admintok, baseUrl, "/api/messages") + tt.AssertEqual(t, "len(messages)", 1, len(msgList1.Messages)) + + msg1Get := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"])) + tt.AssertEqual(t, "msg.title", "HelloWorld_001", msg1Get["title"]) + tt.AssertEqual(t, "msg.channel_name", "main", msg1Get["channel_name"]) + +} + +//TODO message -> query +//TODO message -> form +//TODO overwrite order when all 3 are specified + +//TODO send content +//TODO sendername + +//TODO trim too-long content + +//TODO compat route + +//TODO post to channel +//TODO post to newly-created-channel +//TODO post to foreign channel via send-key + +//TODO usr_msg_id + +//TODO quota exceed (+ quota counter) + +//TODO invalid priority +//TODO chan_naem too long +//TODO chan_name normalization +//TODO custom_timestamp +//TODO invalid time +//TODO title too long +//TODO content too long + +//TODO check message_counter + last_sent in channel +//TODO check message_counter + last_sent in user diff --git a/server/test/user_test.go b/server/test/user_test.go index 9639385..6b23451 100644 --- a/server/test/user_test.go +++ b/server/test/user_test.go @@ -22,8 +22,13 @@ func TestCreateUserNoClient(t *testing.T) { uid := fmt.Sprintf("%v", r0["user_id"]) admintok := r0["admin_key"].(string) + readtok := r0["read_key"].(string) + sendtok := r0["send_key"].(string) - r1 := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/users/"+uid) + tt.RequestAuthGetShouldFail(t, sendtok, baseUrl, "/api/users/"+uid, 401, apierr.USER_AUTH_FAILED) + tt.RequestAuthGetShouldFail(t, "", baseUrl, "/api/users/"+uid, 401, apierr.USER_AUTH_FAILED) + + r1 := tt.RequestAuthGet[gin.H](t, readtok, baseUrl, "/api/users/"+uid) tt.AssertEqual(t, "uid", uid, fmt.Sprintf("%v", r1["user_id"])) tt.AssertEqual(t, "admin_key", admintok, r1["admin_key"]) @@ -214,6 +219,8 @@ func TestDeleteUser(t *testing.T) { tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/users/"+uid) + tt.RequestAuthDeleteShouldFail(t, admintok, baseUrl, "/api/users/"+uid, nil, 401, apierr.USER_AUTH_FAILED) + tt.RequestAuthDelete[tt.Void](t, admintok, baseUrl, "/api/users/"+uid, nil) tt.RequestAuthGetShouldFail(t, admintok, baseUrl, "/api/users/"+uid, 404, apierr.USER_NOT_FOUND) diff --git a/server/test/util/common.go b/server/test/util/common.go index 5fdf3cb..1cc0190 100644 --- a/server/test/util/common.go +++ b/server/test/util/common.go @@ -55,6 +55,31 @@ func AssertEqual(t *testing.T, key string, expected any, actual any) { } } +func AssertStrRepEqual(t *testing.T, key string, expected any, actual any) { + str1 := fmt.Sprintf("%v", expected) + str2 := fmt.Sprintf("%v", actual) + + if str1 != str2 { + t.Errorf("Value [%s] differs (%T <-> %T):\n", key, expected, actual) + + if strings.Contains(str1, "\n") { + t.Errorf("Actual:\n~~~~~~~~~~~~~~~~\n%v\n~~~~~~~~~~~~~~~~\n\n", expected) + } else { + t.Errorf("Actual := \"%v\"\n", expected) + } + + if strings.Contains(str2, "\n") { + t.Errorf("Expected:\n~~~~~~~~~~~~~~~~\n%v\n~~~~~~~~~~~~~~~~\n\n", actual) + } else { + t.Errorf("Expected := \"%v\"\n", actual) + } + + t.Error(string(debug.Stack())) + + t.FailNow() + } +} + func AssertNotEqual(t *testing.T, key string, expected any, actual any) { if expected == actual { t.Errorf("Value [%s] does not differ (%T <-> %T):\n", key, expected, actual) diff --git a/server/test/util/requests.go b/server/test/util/requests.go index dc9633a..f4d728e 100644 --- a/server/test/util/requests.go +++ b/server/test/util/requests.go @@ -52,6 +52,38 @@ func RequestAuthDelete[TResult any](t *testing.T, akey string, baseURL string, u return RequestAny[TResult](t, akey, "DELETE", baseURL, urlSuffix, body) } +func RequestGetShouldFail(t *testing.T, baseURL string, urlSuffix string, statusCode int, errcode apierr.APIError) { + RequestAuthAnyShouldFail(t, "", "GET", baseURL, urlSuffix, nil, statusCode, errcode) +} + +func RequestPostShouldFail(t *testing.T, baseURL string, urlSuffix string, body any, statusCode int, errcode apierr.APIError) { + RequestAuthAnyShouldFail(t, "", "POST", baseURL, urlSuffix, body, statusCode, errcode) +} + +func RequestPatchShouldFail(t *testing.T, baseURL string, urlSuffix string, body any, statusCode int, errcode apierr.APIError) { + RequestAuthAnyShouldFail(t, "", "PATCH", baseURL, urlSuffix, body, statusCode, errcode) +} + +func RequestDeleteShouldFail(t *testing.T, baseURL string, urlSuffix string, body any, statusCode int, errcode apierr.APIError) { + RequestAuthAnyShouldFail(t, "", "DELETE", baseURL, urlSuffix, body, statusCode, errcode) +} + +func RequestAuthGetShouldFail(t *testing.T, akey string, baseURL string, urlSuffix string, statusCode int, errcode apierr.APIError) { + RequestAuthAnyShouldFail(t, akey, "GET", baseURL, urlSuffix, nil, statusCode, errcode) +} + +func RequestAuthPostShouldFail(t *testing.T, akey string, baseURL string, urlSuffix string, body any, statusCode int, errcode apierr.APIError) { + RequestAuthAnyShouldFail(t, akey, "POST", baseURL, urlSuffix, body, statusCode, errcode) +} + +func RequestAuthPatchShouldFail(t *testing.T, akey string, baseURL string, urlSuffix string, body any, statusCode int, errcode apierr.APIError) { + RequestAuthAnyShouldFail(t, akey, "PATCH", baseURL, urlSuffix, body, statusCode, errcode) +} + +func RequestAuthDeleteShouldFail(t *testing.T, akey string, baseURL string, urlSuffix string, body any, statusCode int, errcode apierr.APIError) { + RequestAuthAnyShouldFail(t, akey, "DELETE", baseURL, urlSuffix, body, statusCode, errcode) +} + func RequestAny[TResult any](t *testing.T, akey string, method string, baseURL string, urlSuffix string, body any) TResult { client := http.Client{} @@ -108,22 +140,6 @@ func RequestAny[TResult any](t *testing.T, akey string, method string, baseURL s return data } -func RequestAuthGetShouldFail(t *testing.T, akey string, baseURL string, urlSuffix string, statusCode int, errcode apierr.APIError) { - RequestAuthAnyShouldFail(t, akey, "GET", baseURL, urlSuffix, nil, statusCode, errcode) -} - -func RequestAuthPostShouldFail(t *testing.T, akey string, baseURL string, urlSuffix string, body any, statusCode int, errcode apierr.APIError) { - RequestAuthAnyShouldFail(t, akey, "POST", baseURL, urlSuffix, body, statusCode, errcode) -} - -func RequestAuthPatchShouldFail(t *testing.T, akey string, baseURL string, urlSuffix string, body any, statusCode int, errcode apierr.APIError) { - RequestAuthAnyShouldFail(t, akey, "PATCH", baseURL, urlSuffix, body, statusCode, errcode) -} - -func RequestAuthDeleteShouldFail(t *testing.T, akey string, baseURL string, urlSuffix string, body any, statusCode int, errcode apierr.APIError) { - RequestAuthAnyShouldFail(t, akey, "DELETE", baseURL, urlSuffix, body, statusCode, errcode) -} - func RequestAuthAnyShouldFail(t *testing.T, akey string, method string, baseURL string, urlSuffix string, body any, statusCode int, errcode apierr.APIError) { client := http.Client{}