diff --git a/server/api/handler/common.go b/server/api/handler/common.go index 9ae0bf6..bfad530 100644 --- a/server/api/handler/common.go +++ b/server/api/handler/common.go @@ -184,7 +184,7 @@ func (h CommonHandler) Sleep(g *gin.Context) ginresp.HTTPResponse { return ginresp.APIError(g, 400, apierr.BINDFAIL_URI_PARAM, "Failed to read uri", err) } - time.Sleep(timeext.FromSecondsFloat64(u.Seconds)) + time.Sleep(timeext.FromSeconds(u.Seconds)) t1 := time.Now().Format(time.RFC3339Nano) diff --git a/server/api/handler/message.go b/server/api/handler/message.go index b198e6e..2e720ef 100644 --- a/server/api/handler/message.go +++ b/server/api/handler/message.go @@ -152,9 +152,6 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex if 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, 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, hl.PRIORITY, "Invalid priority", nil) } @@ -190,6 +187,9 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex if UserMessageID != nil && len(*UserMessageID) > user.MaxUserMessageID() { 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 SendTimestamp != nil && mathext.Abs(*SendTimestamp-float64(time.Now().Unix())) > timeext.FromHours(user.MaxTimestampDiffHours()).Seconds() { + return ginresp.SendAPIError(g, 400, apierr.TIMESTAMP_OUT_OF_RANGE, hl.NONE, fmt.Sprintf("The timestamp mus be within %d hours of now()", user.MaxTimestampDiffHours()), nil) + } if UserMessageID != nil { msg, err := h.database.GetMessageByUserMessageID(ctx, *UserMessageID) diff --git a/server/go.mod b/server/go.mod index e500e88..546d152 100644 --- a/server/go.mod +++ b/server/go.mod @@ -8,7 +8,7 @@ require ( github.com/mattn/go-sqlite3 v1.14.16 github.com/rs/zerolog v1.28.0 github.com/swaggo/swag v1.8.7 - gogs.mikescher.com/BlackForestBytes/goext v0.0.21 + gogs.mikescher.com/BlackForestBytes/goext v0.0.22 ) require ( diff --git a/server/go.sum b/server/go.sum index 384c598..ee6848b 100644 --- a/server/go.sum +++ b/server/go.sum @@ -96,6 +96,8 @@ github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0 github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= gogs.mikescher.com/BlackForestBytes/goext v0.0.21 h1:OibsssmorZsTdFYRiQFlkXtjUYweQg9SBkWO40ONe0Y= gogs.mikescher.com/BlackForestBytes/goext v0.0.21/go.mod h1:TMBOjo3FRFh/GiTT0z3nwLmgcFJB87oSF2VMs4XUCTQ= +gogs.mikescher.com/BlackForestBytes/goext v0.0.22 h1:D+49BDPz+2BbRwUePilIDqUsWNZIfXJKqX7yGL2b6+Q= +gogs.mikescher.com/BlackForestBytes/goext v0.0.22/go.mod h1:TMBOjo3FRFh/GiTT0z3nwLmgcFJB87oSF2VMs4XUCTQ= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= diff --git a/server/models/user.go b/server/models/user.go index 1c385ca..ab909e0 100644 --- a/server/models/user.go +++ b/server/models/user.go @@ -102,6 +102,10 @@ func (u User) MaxUserMessageID() int { return 64 } +func (u User) MaxTimestampDiffHours() int { + return 24 +} + type UserJSON struct { UserID UserID `json:"user_id"` Username *string `json:"username"` diff --git a/server/test/message_test.go b/server/test/message_test.go index 5d375b6..cf86d31 100644 --- a/server/test/message_test.go +++ b/server/test/message_test.go @@ -8,6 +8,7 @@ import ( "github.com/gin-gonic/gin" "net/url" "testing" + "time" ) func TestSendSimpleMessageJSON(t *testing.T) { @@ -234,6 +235,9 @@ func TestSendContentMessage(t *testing.T) { msgList1 := tt.RequestAuthGet[mglist](t, admintok, baseUrl, "/api/messages") tt.AssertEqual(t, "len(messages)", 1, len(msgList1.Messages)) + tt.AssertStrRepEqual(t, "msg.title", "HelloWorld_042", msgList1.Messages[0]["title"]) + tt.AssertStrRepEqual(t, "msg.content", "I am Content\nasdf", msgList1.Messages[0]["content"]) + tt.AssertStrRepEqual(t, "msg.channel_name", "main", msgList1.Messages[0]["channel_name"]) msg1Get := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"])) tt.AssertStrRepEqual(t, "msg.title", "HelloWorld_042", msg1Get["title"]) @@ -258,6 +262,7 @@ func TestSendWithSendername(t *testing.T) { uid := int(r0["user_id"].(float64)) sendtok := r0["send_key"].(string) + admintok := r0["admin_key"].(string) msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ "user_key": sendtok, @@ -272,6 +277,23 @@ func TestSendWithSendername(t *testing.T) { tt.AssertStrRepEqual(t, "msg.content", "Unicode: 日本 - yäy\000\n\t\x00...", pusher.Last().Message.Content) tt.AssertStrRepEqual(t, "msg.SenderName", "localhorst", pusher.Last().Message.SenderName) 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)) + tt.AssertStrRepEqual(t, "msg.title", "HelloWorld_xyz", msgList1.Messages[0]["title"]) + tt.AssertStrRepEqual(t, "msg.content", "Unicode: 日本 - yäy\000\n\t\x00...", msgList1.Messages[0]["content"]) + tt.AssertStrRepEqual(t, "msg.sender_name", "localhorst", msgList1.Messages[0]["sender_name"]) + tt.AssertStrRepEqual(t, "msg.channel_name", "main", msgList1.Messages[0]["channel_name"]) + + msg1Get := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"])) + tt.AssertStrRepEqual(t, "msg.title", "HelloWorld_xyz", msg1Get["title"]) + tt.AssertStrRepEqual(t, "msg.content", "Unicode: 日本 - yäy\000\n\t\x00...", msg1Get["content"]) + tt.AssertStrRepEqual(t, "msg.sender_name", "localhorst", msg1Get["sender_name"]) + tt.AssertStrRepEqual(t, "msg.channel_name", "main", msg1Get["channel_name"]) } func TestSendLongContent(t *testing.T) { @@ -682,6 +704,170 @@ func TestSendInvalidPriority(t *testing.T) { tt.AssertEqual(t, "messageCount", 0, len(pusher.Data)) } +func TestSendWithTimestamp(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)) + sendtok := r0["send_key"].(string) + admintok := r0["admin_key"].(string) + + ts := time.Now().Unix() - int64(time.Hour.Seconds()) + + msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", tt.FormData{ + "user_key": sendtok, + "user_id": fmt.Sprintf("%d", uid), + "title": "TTT", + "timestamp": fmt.Sprintf("%d", ts), + }) + + tt.AssertEqual(t, "messageCount", 1, len(pusher.Data)) + tt.AssertStrRepEqual(t, "msg.title", "TTT", pusher.Last().Message.Title) + tt.AssertStrRepEqual(t, "msg.TimestampClient", ts, pusher.Last().Message.TimestampClient.Unix()) + tt.AssertStrRepEqual(t, "msg.Timestamp", ts, pusher.Last().Message.Timestamp().Unix()) + tt.AssertNotStrRepEqual(t, "msg.ts", pusher.Last().Message.TimestampClient, pusher.Last().Message.TimestampReal) + 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)) + tt.AssertStrRepEqual(t, "msg.title", "TTT", msgList1.Messages[0]["title"]) + tt.AssertStrRepEqual(t, "msg.content", nil, msgList1.Messages[0]["sender_name"]) + tt.AssertStrRepEqual(t, "msg.channel_name", "main", msgList1.Messages[0]["channel_name"]) + + tm1, err := time.Parse(time.RFC3339Nano, msgList1.Messages[0]["timestamp"].(string)) + tt.TestFailIfErr(t, err) + tt.AssertStrRepEqual(t, "msg.timestamp", ts, tm1.Unix()) + + msg1Get := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"])) + tt.AssertStrRepEqual(t, "msg.title", "TTT", msg1Get["title"]) + tt.AssertStrRepEqual(t, "msg.content", nil, msg1Get["sender_name"]) + tt.AssertStrRepEqual(t, "msg.channel_name", "main", msg1Get["channel_name"]) + + tmg1, err := time.Parse(time.RFC3339Nano, msg1Get["timestamp"].(string)) + tt.TestFailIfErr(t, err) + tt.AssertStrRepEqual(t, "msg.timestamp", ts, tmg1.Unix()) +} + +func TestSendInvalidTimestamp(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)) + sendtok := r0["send_key"].(string) + + tt.RequestPostShouldFail(t, baseUrl, "/", tt.FormData{ + "user_key": sendtok, + "user_id": fmt.Sprintf("%d", uid), + "title": "TTT", + "timestamp": "-10000", + }, 400, apierr.TIMESTAMP_OUT_OF_RANGE) + + tt.RequestPostShouldFail(t, baseUrl, "/", tt.FormData{ + "user_key": sendtok, + "user_id": fmt.Sprintf("%d", uid), + "title": "TTT", + "timestamp": "0", + }, 400, apierr.TIMESTAMP_OUT_OF_RANGE) + + tt.RequestPostShouldFail(t, baseUrl, "/", tt.FormData{ + "user_key": sendtok, + "user_id": fmt.Sprintf("%d", uid), + "title": "TTT", + "timestamp": fmt.Sprintf("%d", time.Now().Unix()-int64(25*time.Hour.Seconds())), + }, 400, apierr.TIMESTAMP_OUT_OF_RANGE) + + tt.RequestPostShouldFail(t, baseUrl, "/", tt.FormData{ + "user_key": sendtok, + "user_id": fmt.Sprintf("%d", uid), + "title": "TTT", + "timestamp": fmt.Sprintf("%d", time.Now().Unix()+int64(25*time.Hour.Seconds())), + }, 400, apierr.TIMESTAMP_OUT_OF_RANGE) + + tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{ + "user_key": sendtok, + "user_id": uid, + "title": "TTT", + "timestamp": -10000, + }, 400, apierr.TIMESTAMP_OUT_OF_RANGE) + + tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{ + "user_key": sendtok, + "user_id": uid, + "title": "TTT", + "timestamp": 0, + }, 400, apierr.TIMESTAMP_OUT_OF_RANGE) + + tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{ + "user_key": sendtok, + "user_id": uid, + "title": "TTT", + "timestamp": time.Now().Unix() - int64(25*time.Hour.Seconds()), + }, 400, apierr.TIMESTAMP_OUT_OF_RANGE) + + tt.RequestPostShouldFail(t, baseUrl, "/", gin.H{ + "user_key": sendtok, + "user_id": uid, + "title": "TTT", + "timestamp": time.Now().Unix() + int64(25*time.Hour.Seconds()), + }, 400, apierr.TIMESTAMP_OUT_OF_RANGE) + + tt.RequestPostShouldFail(t, baseUrl, fmt.Sprintf("/?user_key=%s&user_id=%d&title=%s×tamp=%d", + sendtok, + uid, + "TTT", + -10000, + ), nil, 400, apierr.TIMESTAMP_OUT_OF_RANGE) + + tt.RequestPostShouldFail(t, baseUrl, fmt.Sprintf("/?user_key=%s&user_id=%d&title=%s×tamp=%d", + sendtok, + uid, + "TTT", + 0, + ), nil, 400, apierr.TIMESTAMP_OUT_OF_RANGE) + + tt.RequestPostShouldFail(t, baseUrl, fmt.Sprintf("/?user_key=%s&user_id=%d&title=%s×tamp=%d", + sendtok, + uid, + "TTT", + time.Now().Unix()-int64(25*time.Hour.Seconds()), + ), nil, 400, apierr.TIMESTAMP_OUT_OF_RANGE) + + tt.RequestPostShouldFail(t, baseUrl, fmt.Sprintf("/?user_key=%s&user_id=%d&title=%s×tamp=%d", + sendtok, + uid, + "TTT", + time.Now().Unix()+int64(25*time.Hour.Seconds()), + ), nil, 400, apierr.TIMESTAMP_OUT_OF_RANGE) + + tt.AssertEqual(t, "messageCount", 0, len(pusher.Data)) +} + //TODO compat route //TODO post to channel @@ -696,10 +882,6 @@ func TestSendInvalidPriority(t *testing.T) { //TODO chan_name normalization -//TODO custom_timestamp - -//TODO invalid time - //TODO check message_counter + last_sent in channel //TODO check message_counter + last_sent in user diff --git a/server/test/util/common.go b/server/test/util/common.go index 7661891..296d2f8 100644 --- a/server/test/util/common.go +++ b/server/test/util/common.go @@ -133,19 +133,28 @@ func AssertNotStrRepEqual(t *testing.T, key string, expected any, actual any) { func TestFail(t *testing.T, msg string) { t.Error(msg) + t.Error(string(debug.Stack())) t.FailNow() } func TestFailFmt(t *testing.T, format string, args ...any) { t.Errorf(format, args...) + t.Error(string(debug.Stack())) t.FailNow() } func TestFailErr(t *testing.T, e error) { t.Error(fmt.Sprintf("Failed with error:\n%s\n\nError:\n%+v\n\nTrace:\n%s", e.Error(), e, string(debug.Stack()))) + t.Error(string(debug.Stack())) t.FailNow() } +func TestFailIfErr(t *testing.T, e error) { + if e != nil { + TestFailErr(t, e) + } +} + func unpointer(v any) any { if v == nil { return v diff --git a/server/test/util/requests.go b/server/test/util/requests.go index be0a601..7e4738c 100644 --- a/server/test/util/requests.go +++ b/server/test/util/requests.go @@ -224,7 +224,9 @@ func RequestAuthAnyShouldFail(t *testing.T, akey string, method string, baseURL fmt.Println("") fmt.Printf("---------------- RESPONSE (%d) ----------------\n", resp.StatusCode) fmt.Println(langext.TryPrettyPrintJson(string(respBodyBin))) - //TryPrintTraceObj("---------------- -------- ----------------", respBodyBin, "") + if resp.StatusCode != statusCode { + TryPrintTraceObj("---------------- -------- ----------------", respBodyBin, "") + } fmt.Println("---------------- -------- ----------------") fmt.Println("")