diff --git a/server/api/handler/api.go b/server/api/handler/api.go index dad3c1f..98fee2c 100644 --- a/server/api/handler/api.go +++ b/server/api/handler/api.go @@ -1237,7 +1237,7 @@ func (h APIHandler) GetMessage(g *gin.Context) ginresp.HTTPResponse { return *permResp } - msg, err := h.database.GetMessage(ctx, u.MessageID) + msg, err := h.database.GetMessage(ctx, u.MessageID, false) if err == sql.ErrNoRows { return ginresp.APIError(g, 404, apierr.MESSAGE_NOT_FOUND, "message not found", err) } @@ -1307,7 +1307,7 @@ func (h APIHandler) DeleteMessage(g *gin.Context) ginresp.HTTPResponse { return *permResp } - msg, err := h.database.GetMessage(ctx, u.MessageID) + msg, err := h.database.GetMessage(ctx, u.MessageID, false) if err == sql.ErrNoRows { return ginresp.APIError(g, 404, apierr.MESSAGE_NOT_FOUND, "message not found", err) } diff --git a/server/api/handler/compat.go b/server/api/handler/compat.go index a5bf462..16dd08f 100644 --- a/server/api/handler/compat.go +++ b/server/api/handler/compat.go @@ -533,7 +533,7 @@ func (h CompatHandler) Expand(g *gin.Context) ginresp.HTTPResponse { return ginresp.CompatAPIError(204, "Authentification failed") } - msg, err := h.database.GetMessage(ctx, models.SCNMessageID(*data.MessageID)) + msg, err := h.database.GetMessage(ctx, models.SCNMessageID(*data.MessageID), false) if err == sql.ErrNoRows { return ginresp.CompatAPIError(301, "Message not found") } diff --git a/server/api/handler/message.go b/server/api/handler/message.go index 355e9e5..68195a0 100644 --- a/server/api/handler/message.go +++ b/server/api/handler/message.go @@ -196,6 +196,7 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query existing message", err) } if msg != nil { + //the found message can be deleted (!), but we still return NO_ERROR here... return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{ Success: true, ErrorID: apierr.NO_ERROR, diff --git a/server/db/messages.go b/server/db/messages.go index 4f59719..f639642 100644 --- a/server/db/messages.go +++ b/server/db/messages.go @@ -30,13 +30,20 @@ func (db *Database) GetMessageByUserMessageID(ctx TxContext, usrMsgId string) (* return &msg, nil } -func (db *Database) GetMessage(ctx TxContext, scnMessageID models.SCNMessageID) (models.Message, error) { +func (db *Database) GetMessage(ctx TxContext, scnMessageID models.SCNMessageID, allowDeleted bool) (models.Message, error) { tx, err := ctx.GetOrCreateTransaction(db) if err != nil { return models.Message{}, err } - rows, err := tx.Query(ctx, "SELECT * FROM messages WHERE scn_message_id = :mid LIMIT 1", sq.PP{"mid": scnMessageID}) + var sqlcmd string + if allowDeleted { + sqlcmd = "SELECT * FROM messages WHERE scn_message_id = :mid LIMIT 1" + } else { + sqlcmd = "SELECT * FROM messages WHERE scn_message_id = :mid AND deleted=0 LIMIT 1" + } + + rows, err := tx.Query(ctx, sqlcmd, sq.PP{"mid": scnMessageID}) if err != nil { return models.Message{}, err } @@ -103,7 +110,7 @@ func (db *Database) DeleteMessage(ctx TxContext, scnMessageID models.SCNMessageI return err } - _, err = tx.Exec(ctx, "DELETE FROM messages WHERE scn_message_id = :mid", sq.PP{"mid": scnMessageID}) + _, err = tx.Exec(ctx, "UPDATE messages SET deleted=1 WHERE scn_message_id = :mid AND deleted=0", sq.PP{"mid": scnMessageID}) if err != nil { return err } diff --git a/server/db/schema/schema_3.ddl b/server/db/schema/schema_3.ddl index 8f7af68..273f83a 100644 --- a/server/db/schema/schema_3.ddl +++ b/server/db/schema/schema_3.ddl @@ -90,7 +90,9 @@ CREATE TABLE messages title TEXT NOT NULL, content TEXT NULL, priority INTEGER CHECK(priority IN (0, 1, 2)) NOT NULL, - usr_message_id TEXT NULL + usr_message_id TEXT NULL, + + deleted INTEGER CHECK(deleted IN (0, 1)) NOT NULL DEFAULT '0' ) STRICT; CREATE INDEX "idx_messages_owner_channel" ON messages (owner_user_id, channel_name COLLATE BINARY); CREATE INDEX "idx_messages_owner_channel_nc" ON messages (owner_user_id, channel_name COLLATE NOCASE); @@ -102,6 +104,7 @@ CREATE INDEX "idx_messages_sendername" ON messages (sender_name COL CREATE INDEX "idx_messages_sendername_nc" ON messages (sender_name COLLATE NOCASE); CREATE INDEX "idx_messages_title" ON messages (title COLLATE BINARY); CREATE INDEX "idx_messages_title_nc" ON messages (title COLLATE NOCASE); +CREATE INDEX "idx_messages_deleted" ON messages (deleted); CREATE VIRTUAL TABLE messages_fts USING fts5 diff --git a/server/jobs/DeliveryRetryJob.go b/server/jobs/DeliveryRetryJob.go index e702fe6..d544f85 100644 --- a/server/jobs/DeliveryRetryJob.go +++ b/server/jobs/DeliveryRetryJob.go @@ -93,36 +93,47 @@ func (j *DeliveryRetryJob) redeliver(ctx *logic.SimpleContext, delivery models.D return } - msg, err := j.app.Database.GetMessage(ctx, delivery.SCNMessageID) + msg, err := j.app.Database.GetMessage(ctx, delivery.SCNMessageID, true) if err != nil { log.Err(err).Int64("SCNMessageID", delivery.SCNMessageID.IntID()).Msg("Failed to get message") ctx.RollbackTransaction() return } - fcmDelivID, err := j.app.DeliverMessage(ctx, client, msg) - if err == nil { - err = j.app.Database.SetDeliverySuccess(ctx, delivery, *fcmDelivID) - if err != nil { - log.Err(err).Int64("SCNMessageID", delivery.SCNMessageID.IntID()).Int64("DeliveryID", delivery.DeliveryID.IntID()).Msg("Failed to update delivery") - ctx.RollbackTransaction() - return - } - } else if delivery.RetryCount+1 > delivery.MaxRetryCount() { + if msg.Deleted { err = j.app.Database.SetDeliveryFailed(ctx, delivery) if err != nil { log.Err(err).Int64("SCNMessageID", delivery.SCNMessageID.IntID()).Int64("DeliveryID", delivery.DeliveryID.IntID()).Msg("Failed to update delivery") ctx.RollbackTransaction() return } - log.Warn().Int64("SCNMessageID", delivery.SCNMessageID.IntID()).Int64("DeliveryID", delivery.DeliveryID.IntID()).Msg("Delivery failed after retries (set to FAILURE)") } else { - err = j.app.Database.SetDeliveryRetry(ctx, delivery) - if err != nil { - log.Err(err).Int64("SCNMessageID", delivery.SCNMessageID.IntID()).Int64("DeliveryID", delivery.DeliveryID.IntID()).Msg("Failed to update delivery") - ctx.RollbackTransaction() - return + + fcmDelivID, err := j.app.DeliverMessage(ctx, client, msg) + if err == nil { + err = j.app.Database.SetDeliverySuccess(ctx, delivery, *fcmDelivID) + if err != nil { + log.Err(err).Int64("SCNMessageID", delivery.SCNMessageID.IntID()).Int64("DeliveryID", delivery.DeliveryID.IntID()).Msg("Failed to update delivery") + ctx.RollbackTransaction() + return + } + } else if delivery.RetryCount+1 > delivery.MaxRetryCount() { + err = j.app.Database.SetDeliveryFailed(ctx, delivery) + if err != nil { + log.Err(err).Int64("SCNMessageID", delivery.SCNMessageID.IntID()).Int64("DeliveryID", delivery.DeliveryID.IntID()).Msg("Failed to update delivery") + ctx.RollbackTransaction() + return + } + log.Warn().Int64("SCNMessageID", delivery.SCNMessageID.IntID()).Int64("DeliveryID", delivery.DeliveryID.IntID()).Msg("Delivery failed after retries (set to FAILURE)") + } else { + err = j.app.Database.SetDeliveryRetry(ctx, delivery) + if err != nil { + log.Err(err).Int64("SCNMessageID", delivery.SCNMessageID.IntID()).Int64("DeliveryID", delivery.DeliveryID.IntID()).Msg("Failed to update delivery") + ctx.RollbackTransaction() + return + } } + } err = ctx.CommitTransaction() diff --git a/server/models/message.go b/server/models/message.go index 0b133bc..71cd238 100644 --- a/server/models/message.go +++ b/server/models/message.go @@ -26,6 +26,7 @@ type Message struct { Content *string Priority int UserMessageID *string + Deleted bool } func (m Message) FullJSON() MessageJSON { @@ -122,6 +123,7 @@ type MessageDB struct { Content *string `db:"content"` Priority int `db:"priority"` UserMessageID *string `db:"usr_message_id"` + Deleted int `db:"deleted"` } func (m MessageDB) Model() Message { @@ -139,6 +141,7 @@ func (m MessageDB) Model() Message { Content: m.Content, Priority: m.Priority, UserMessageID: m.UserMessageID, + Deleted: m.Deleted != 0, } } diff --git a/server/models/messagefilter.go b/server/models/messagefilter.go index 73be803..863e714 100644 --- a/server/models/messagefilter.go +++ b/server/models/messagefilter.go @@ -37,6 +37,8 @@ type MessageFilter struct { TitleCI *string // case-insensitive Priority *[]int UserMessageID *[]string + OnlyDeleted bool + IncludeDeleted bool } func (f MessageFilter) SQL() (string, string, sq.PP, error) { @@ -53,6 +55,14 @@ func (f MessageFilter) SQL() (string, string, sq.PP, error) { params := sq.PP{} + if f.OnlyDeleted { + sqlClauses = append(sqlClauses, "(deleted=1)") + } else if f.IncludeDeleted { + // nothing, return all + } else { + sqlClauses = append(sqlClauses, "(deleted=0)") // default + } + if f.ConfirmedSubscriptionBy != nil { sqlClauses = append(sqlClauses, "(subs.subscriber_user_id = :sub_uid AND subs.confirmed = 1)") params["sub_uid"] = *f.ConfirmedSubscriptionBy diff --git a/server/test/message_test.go b/server/test/message_test.go index a558aca..8929870 100644 --- a/server/test/message_test.go +++ b/server/test/message_test.go @@ -1,12 +1,15 @@ package test import ( + "blackforestbytes.com/simplecloudnotifier/api/apierr" tt "blackforestbytes.com/simplecloudnotifier/test/util" "fmt" "github.com/gin-gonic/gin" "gogs.mikescher.com/BlackForestBytes/goext/langext" + "gogs.mikescher.com/BlackForestBytes/goext/timeext" "net/url" "testing" + "time" ) func TestSearchMessageFTSSimple(t *testing.T) { @@ -31,4 +34,154 @@ func TestSearchMessageFTSMulti(t *testing.T) { //TODO search for messages by FTS } -//TODO test missing message-xx methods +//TODO more search/list/filter message tests + +//TODO list messages by chan_key + +//TODO list messages from channel that you cannot see + +func TestDeleteMessage(t *testing.T) { + ws, stop := tt.StartSimpleWebserver(t) + defer stop() + + 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) + + msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ + "user_key": sendtok, + "user_id": uid, + "title": "Message_1", + }) + + tt.RequestAuthGet[tt.Void](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"])) + + tt.RequestAuthDelete[tt.Void](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"]), gin.H{}) + + tt.RequestAuthGetShouldFail(t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"]), 404, apierr.MESSAGE_NOT_FOUND) +} + +func TestDeleteMessageAndResendUsrMsgId(t *testing.T) { + ws, stop := tt.StartSimpleWebserver(t) + defer stop() + + 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) + + msg1 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ + "user_key": sendtok, + "user_id": uid, + "title": "Message_1", + "msg_id": "bef8dd3d-078e-4f89-abf4-5258ad22a2e4", + }) + + tt.AssertEqual(t, "suppress_send", false, msg1["suppress_send"]) + + tt.RequestAuthGet[tt.Void](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"])) + + msg2 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ + "user_key": sendtok, + "user_id": uid, + "title": "Message_1", + "msg_id": "bef8dd3d-078e-4f89-abf4-5258ad22a2e4", + }) + + tt.AssertEqual(t, "suppress_send", true, msg2["suppress_send"]) + + tt.RequestAuthDelete[tt.Void](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"]), gin.H{}) + + // even though message is deleted, we still get a `suppress_send` on send_message + + msg3 := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ + "user_key": sendtok, + "user_id": uid, + "title": "Message_1", + "msg_id": "bef8dd3d-078e-4f89-abf4-5258ad22a2e4", + }) + + tt.AssertEqual(t, "suppress_send", true, msg3["suppress_send"]) + +} + +func TestGetMessageSimple(t *testing.T) { + ws, stop := tt.StartSimpleWebserver(t) + defer stop() + + baseUrl := "http://127.0.0.1:" + ws.Port + + data := tt.InitDefaultData(t, ws) + + msgOut := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ + "user_key": data.User[0].SendKey, + "user_id": data.User[0].UID, + "title": "Message_1", + }) + + msgIn := tt.RequestAuthGet[gin.H](t, data.User[0].AdminKey, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msgOut["scn_msg_id"])) + + tt.AssertEqual(t, "msg.title", "Message_1", msgIn["title"]) +} + +func TestGetMessageNotFound(t *testing.T) { + ws, stop := tt.StartSimpleWebserver(t) + defer stop() + + baseUrl := "http://127.0.0.1:" + ws.Port + + data := tt.InitDefaultData(t, ws) + + tt.RequestAuthGetShouldFail(t, data.User[0].AdminKey, baseUrl, "/api/messages/8963586", 404, apierr.MESSAGE_NOT_FOUND) +} + +func TestGetMessageFull(t *testing.T) { + ws, stop := tt.StartSimpleWebserver(t) + defer stop() + + baseUrl := "http://127.0.0.1:" + ws.Port + + data := tt.InitDefaultData(t, ws) + + ts := time.Now().Unix() - 735 + content := tt.Lipsum0(2) + + msgOut := tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ + "user_key": data.User[0].SendKey, + "user_id": data.User[0].UID, + "title": "Message_1", + "content": content, + "channel": "demo-channel-007", + "msg_id": "580b5055-a9b5-4cee-b53c-28cf304d25b0", + "priority": 0, + "sender_name": "unit-test-[TestGetMessageFull]", + "timestamp": ts, + }) + + msgIn := tt.RequestAuthGet[gin.H](t, data.User[0].AdminKey, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msgOut["scn_msg_id"])) + + tt.AssertEqual(t, "msg.title", "Message_1", msgIn["title"]) + tt.AssertEqual(t, "msg.content", content, msgIn["content"]) + tt.AssertEqual(t, "msg.channel", "demo-channel-007", msgIn["channel_name"]) + tt.AssertEqual(t, "msg.msg_id", "580b5055-a9b5-4cee-b53c-28cf304d25b0", msgIn["usr_message_id"]) + tt.AssertStrRepEqual(t, "msg.priority", 0, msgIn["priority"]) + tt.AssertEqual(t, "msg.sender_name", "unit-test-[TestGetMessageFull]", msgIn["sender_name"]) + tt.AssertEqual(t, "msg.timestamp", time.Unix(ts, 0).In(timeext.TimezoneBerlin).Format(time.RFC3339Nano), msgIn["timestamp"]) +} diff --git a/server/test/util/factory.go b/server/test/util/factory.go index 67179c6..09b2486 100644 --- a/server/test/util/factory.go +++ b/server/test/util/factory.go @@ -112,7 +112,7 @@ var messageExamples = []msgex{ {0, "", "", P0, SKEY, "Congratulations", "You have been selected as Employee of the Month. Please come to the front desk to pick up your prize", 0}, {0, "", "", PX, AKEY, "Attention", "The water cooler is empty. Could someone please refill it?", timeext.FromHours(-11.29)}, {0, "Chatting Chamber", "Mobile Mate", P2, SKEY, "Important", "All employees are required to complete a safety training course by the end of the month", 0}, - {0, "", "", P1, AKEY, "FAQ Update", lipsum(10001, 1), 0}, + {0, "", "", P1, AKEY, "FAQ Update", Lipsum(10001, 1), 0}, {0, "", "", PX, AKEY, "Notice", "There will be a fire drill at 10:00am tomorrow. Please follow the instructions of the fire marshal", 0}, {0, "", "Cellular Confidant", P2, SKEY, "Invitation", "You are invited to a celebration in honor of our 10-year anniversary. The party will be held on Friday at 7:00pm", 0}, {0, "", "", P0, SKEY, "Deadline reminder", "Please remember to submit your project proposal by the end of the day \U0001f638", 0}, @@ -156,9 +156,9 @@ var messageExamples = []msgex{ {3, "", "", PX, AKEY, "Payment confirmation", "Your payment of $100 has been successfully processed. Thank you for your business.", 0}, {3, "", "", P2, SKEY, "Task completed", "Your task \"Update website content\" has been completed and is ready for review.", 0}, {3, "Innovations", "", PX, AKEY, "Invitation to join a group", "You have been invited to join the \"Marketing Team\" group on our collaboration platform.", 0}, - {3, "", "", P2, SKEY, "Password reset", lipsum(10002, 1), 0}, - {3, "", "", P2, SKEY, "Low battery alert", lipsum(10003, 2), 0}, - {3, "Innovations", "", P2, SKEY, "System update available", lipsum(10004, 5), 0}, + {3, "", "", P2, SKEY, "Password reset", Lipsum(10002, 1), 0}, + {3, "", "", P2, SKEY, "Low battery alert", Lipsum(10003, 2), 0}, + {3, "Innovations", "", P2, SKEY, "System update available", Lipsum(10004, 5), 0}, {3, "", "", P2, SKEY, "Appointment confirmation", "Your appointment for a physical exam on Monday, March 15th at 10 AM has been confirmed.", 0}, {3, "\U0001f5ff", "", P2, SKEY, "Order shipped", "Your order #123456 has been shipped and is on its way to your address.", 0}, {3, "", "", P2, SKEY, "Order cancelled", "Your order #123456 has been cancelled. We apologize for any inconvenience this may have caused.", 0}, @@ -166,7 +166,7 @@ var messageExamples = []msgex{ {3, "Reminders", "", PX, AKEY, "Account verification", "", timeext.FromHours(1.15)}, {3, "Reminders", "", PX, AKEY, "Overdue payment", "", 0}, {3, "Reminders", "", P2, SKEY, "Security alert", "We have detected suspicious activity on your account. Please take the necessary steps to secure your account.", timeext.FromHours(0.80)}, - {3, "Reminders", "", PX, AKEY, "Product back in stock", lipsum(10001, 6), 0}, + {3, "Reminders", "", PX, AKEY, "Product back in stock", Lipsum(10001, 6), 0}, {3, "", "", PX, AKEY, "Connection lost", "Your device has lost its connection to the internet. Please check your network settings and try again.", 0}, {3, "", "", P2, SKEY, "Subscription renewal", "Your subscription is set to renew in one week. Please update your payment information to avoid any interruption in service.", 0}, {3, "", "", PX, AKEY, "Work order assigned", "You have been assigned a new work order #123456. Please review the details and complete the task as soon as possible.", 0}, @@ -202,22 +202,22 @@ var messageExamples = []msgex{ {6, "", "server1", P2, SKEY, "Server performance improvement", "Thanks to recent upgrades, the server is now performing better than ever", 0}, {6, "", "server1", PX, AKEY, "Server security update", "The server has been updated with the latest security patches and enhancements", 0}, {6, "", "server1", P1, AKEY, "Server downtime schedule change", "The server downtime schedule has been changed to every other Friday at 8am EST", 0}, - {6, "Lipsum", "", P2, SKEY, "Lorem Ipsum", lipsum(20001, 1), 0}, - {6, "Lipsum", "", P0, SKEY, "Lorem Ipsum", lipsum(20002, 1), 0}, - {6, "Lipsum", "", P2, SKEY, "Lorem Ipsum", lipsum(20003, 1), 0}, - {6, "Lipsum", "", P0, SKEY, "Lorem Ipsum", lipsum(20004, 1), 0}, - {6, "Lipsum", "", P2, SKEY, "Lorem Ipsum", lipsum(20005, 1), 0}, - {6, "Lipsum", "", P1, AKEY, "Lorem Ipsum", lipsum(20006, 1), 0}, - {6, "Lipsum", "", P1, AKEY, "Lorem Ipsum", lipsum(20007, 1), timeext.FromHours(-3.39)}, - {6, "Lipsum", "", P0, SKEY, "Lorem Ipsum", lipsum(20008, 1), 0}, - {6, "Lipsum", "", PX, AKEY, "Lorem Ipsum", lipsum(20009, 1), 0}, - {6, "Lipsum", "", P0, SKEY, "Lorem Ipsum", lipsum(20010, 1), 0}, - {6, "Lipsum", "", P2, SKEY, "Lorem Ipsum", lipsum(20011, 1), 0}, - {6, "Lipsum", "", PX, AKEY, "Lorem Ipsum", lipsum(20012, 1), 0}, - {6, "Lipsum", "", P2, SKEY, "Lorem Ipsum", lipsum(20013, 1), 0}, - {6, "Lipsum", "", P2, SKEY, "Lorem Ipsum", lipsum(20014, 1), timeext.FromHours(-2.33)}, - {6, "Lipsum", "", P0, SKEY, "Lorem Ipsum", lipsum(20015, 1), 0}, - {6, "Lipsum", "", P0, SKEY, "Lorem Ipsum", lipsum(20016, 1), 0}, + {6, "Lipsum", "", P2, SKEY, "Lorem Ipsum", Lipsum(20001, 1), 0}, + {6, "Lipsum", "", P0, SKEY, "Lorem Ipsum", Lipsum(20002, 1), 0}, + {6, "Lipsum", "", P2, SKEY, "Lorem Ipsum", Lipsum(20003, 1), 0}, + {6, "Lipsum", "", P0, SKEY, "Lorem Ipsum", Lipsum(20004, 1), 0}, + {6, "Lipsum", "", P2, SKEY, "Lorem Ipsum", Lipsum(20005, 1), 0}, + {6, "Lipsum", "", P1, AKEY, "Lorem Ipsum", Lipsum(20006, 1), 0}, + {6, "Lipsum", "", P1, AKEY, "Lorem Ipsum", Lipsum(20007, 1), timeext.FromHours(-3.39)}, + {6, "Lipsum", "", P0, SKEY, "Lorem Ipsum", Lipsum(20008, 1), 0}, + {6, "Lipsum", "", PX, AKEY, "Lorem Ipsum", Lipsum(20009, 1), 0}, + {6, "Lipsum", "", P0, SKEY, "Lorem Ipsum", Lipsum(20010, 1), 0}, + {6, "Lipsum", "", P2, SKEY, "Lorem Ipsum", Lipsum(20011, 1), 0}, + {6, "Lipsum", "", PX, AKEY, "Lorem Ipsum", Lipsum(20012, 1), 0}, + {6, "Lipsum", "", P2, SKEY, "Lorem Ipsum", Lipsum(20013, 1), 0}, + {6, "Lipsum", "", P2, SKEY, "Lorem Ipsum", Lipsum(20014, 1), timeext.FromHours(-2.33)}, + {6, "Lipsum", "", P0, SKEY, "Lorem Ipsum", Lipsum(20015, 1), 0}, + {6, "Lipsum", "", P0, SKEY, "Lorem Ipsum", Lipsum(20016, 1), 0}, {7, "", "localhost", P2, SKEY, "Server outage resolution update", "We are still working on resolving the server outage and will provide updates as soon as possible", 0}, {7, "", "localhost", P0, SKEY, "New server release update", "A new update for the server has been released. Please update to the latest version for optimal performance", 0}, @@ -369,6 +369,10 @@ func InitDefaultData(t *testing.T, ws *logic.Application) DefData { return DefData{User: users} } -func lipsum(seed int64, paracount int) string { +func Lipsum(seed int64, paracount int) string { return loremipsum.NewWithSeed(seed).Paragraphs(paracount) } + +func Lipsum0(paracount int) string { + return loremipsum.NewWithSeed(0).Paragraphs(paracount) +}