Tests[TestSendSimpleMessageJSON]

This commit is contained in:
Mike Schwörer 2022-11-30 17:58:04 +01:00
parent 0ff1188c3d
commit 62d7df9710
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
14 changed files with 496 additions and 188 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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