Save internal_name and display_name in channel

This commit is contained in:
Mike Schwörer 2022-12-22 11:22:36 +01:00
parent f65c231ba0
commit 0cb2a977a0
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
20 changed files with 414 additions and 296 deletions

View File

@ -23,6 +23,8 @@
------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------
- (?) default-priority for channels
- (?) ack/read deliveries && return ack-count (? or not, how to query?) - (?) ack/read deliveries && return ack-count (? or not, how to query?)
- (?) "login" on website and list/search/filter messages - (?) "login" on website and list/search/filter messages

View File

@ -20,13 +20,14 @@ const (
BINDFAIL_URI_PARAM APIError = 1153 BINDFAIL_URI_PARAM APIError = 1153
INVALID_ENUM_VALUE APIError = 1171 INVALID_ENUM_VALUE APIError = 1171
NO_TITLE APIError = 1201 NO_TITLE APIError = 1201
TITLE_TOO_LONG APIError = 1202 TITLE_TOO_LONG APIError = 1202
CONTENT_TOO_LONG APIError = 1203 CONTENT_TOO_LONG APIError = 1203
USR_MSG_ID_TOO_LONG APIError = 1204 USR_MSG_ID_TOO_LONG APIError = 1204
TIMESTAMP_OUT_OF_RANGE APIError = 1205 TIMESTAMP_OUT_OF_RANGE APIError = 1205
SENDERNAME_TOO_LONG APIError = 1206 SENDERNAME_TOO_LONG APIError = 1206
CHANNEL_TOO_LONG APIError = 1207 CHANNEL_TOO_LONG APIError = 1207
CHANNEL_NAME_WOULD_CHANGE APIError = 1207
USER_NOT_FOUND APIError = 1301 USER_NOT_FOUND APIError = 1301
CLIENT_NOT_FOUND APIError = 1302 CLIENT_NOT_FOUND APIError = 1302

View File

@ -667,9 +667,10 @@ func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
return *permResp return *permResp
} }
channelName := h.app.NormalizeChannelName(b.Name) channelDisplayName := h.app.NormalizeChannelDisplayName(b.Name)
channelInternalName := h.app.NormalizeChannelInternalName(b.Name)
channelExisting, err := h.database.GetChannelByName(ctx, u.UserID, channelName) channelExisting, err := h.database.GetChannelByName(ctx, u.UserID, channelInternalName)
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
} }
@ -682,7 +683,10 @@ func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query user", err) return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query user", err)
} }
if len(channelName) > user.MaxChannelNameLength() { if len(channelDisplayName) > user.MaxChannelNameLength() {
return ginresp.SendAPIError(g, 400, apierr.CHANNEL_TOO_LONG, hl.CHANNEL, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil)
}
if len(channelInternalName) > user.MaxChannelNameLength() {
return ginresp.SendAPIError(g, 400, apierr.CHANNEL_TOO_LONG, hl.CHANNEL, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil) return ginresp.SendAPIError(g, 400, apierr.CHANNEL_TOO_LONG, hl.CHANNEL, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil)
} }
@ -693,7 +697,7 @@ func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
subscribeKey := h.app.GenerateRandomAuthKey() subscribeKey := h.app.GenerateRandomAuthKey()
sendKey := h.app.GenerateRandomAuthKey() sendKey := h.app.GenerateRandomAuthKey()
channel, err := h.database.CreateChannel(ctx, u.UserID, channelName, subscribeKey, sendKey) channel, err := h.database.CreateChannel(ctx, u.UserID, channelDisplayName, channelInternalName, subscribeKey, sendKey)
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create channel", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create channel", err)
} }
@ -726,6 +730,7 @@ func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
// //
// @Param subscribe_key body string false "Send `true` to create a new subscribe_key" // @Param subscribe_key body string false "Send `true` to create a new subscribe_key"
// @Param send_key body string false "Send `true` to create a new send_key" // @Param send_key body string false "Send `true` to create a new send_key"
// @Param display_name body string false "Change the cahnnel display-name (only chnages to lowercase/uppercase are allowed - internal_name must stay the same)"
// //
// @Success 200 {object} models.ChannelWithSubscriptionJSON // @Success 200 {object} models.ChannelWithSubscriptionJSON
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid" // @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
@ -740,8 +745,9 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse {
ChannelID models.ChannelID `uri:"cid"` ChannelID models.ChannelID `uri:"cid"`
} }
type body struct { type body struct {
RefreshSubscribeKey *bool `json:"subscribe_key"` RefreshSubscribeKey *bool `json:"subscribe_key"`
RefreshSendKey *bool `json:"send_key"` RefreshSendKey *bool `json:"send_key"`
DisplayName *string `json:"display_name"`
} }
var u uri var u uri
@ -756,7 +762,7 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse {
return *permResp return *permResp
} }
_, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID) oldChannel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
} }
@ -769,7 +775,7 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse {
err := h.database.UpdateChannelSendKey(ctx, u.ChannelID, newkey) err := h.database.UpdateChannelSendKey(ctx, u.ChannelID, newkey)
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update user", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update channel", err)
} }
} }
@ -778,13 +784,29 @@ func (h APIHandler) UpdateChannel(g *gin.Context) ginresp.HTTPResponse {
err := h.database.UpdateChannelSubscribeKey(ctx, u.ChannelID, newkey) err := h.database.UpdateChannelSubscribeKey(ctx, u.ChannelID, newkey)
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update user", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update channel", err)
} }
} }
if b.DisplayName != nil {
newDisplayName := h.app.NormalizeChannelDisplayName(*b.DisplayName)
newInternalName := h.app.NormalizeChannelInternalName(*b.DisplayName)
if newInternalName != oldChannel.InternalName {
return ginresp.APIError(g, 400, apierr.CHANNEL_NAME_WOULD_CHANGE, "Cannot substantially change the channel name", err)
}
err := h.database.UpdateChannelDisplayName(ctx, u.ChannelID, newDisplayName)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update channel", err)
}
}
channel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID) channel, err := h.database.GetChannel(ctx, u.UserID, u.ChannelID)
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query (updated) user", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query (updated) channel", err)
} }
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, channel.JSON(true))) return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, channel.JSON(true)))
@ -1191,7 +1213,9 @@ func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse {
return *permResp return *permResp
} }
channel, err := h.database.GetChannelByName(ctx, b.ChannelOwnerUserID, h.app.NormalizeChannelName(b.Channel)) channelInternalName := h.app.NormalizeChannelInternalName(b.Channel)
channel, err := h.database.GetChannelByName(ctx, b.ChannelOwnerUserID, channelInternalName)
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
} }

View File

@ -166,9 +166,11 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex
return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query user", err) return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query user", err)
} }
channelName := user.DefaultChannel() channelDisplayName := user.DefaultChannel()
channelInternalName := user.DefaultChannel()
if Channel != nil { if Channel != nil {
channelName = h.app.NormalizeChannelName(*Channel) channelDisplayName = h.app.NormalizeChannelDisplayName(*Channel)
channelInternalName = h.app.NormalizeChannelInternalName(*Channel)
} }
if len(*Title) > user.MaxTitleLength() { if len(*Title) > user.MaxTitleLength() {
@ -177,7 +179,10 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex
if Content != nil && len(*Content) > user.MaxContentLength() { if Content != nil && len(*Content) > user.MaxContentLength() {
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) 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() { if len(channelDisplayName) > user.MaxChannelNameLength() {
return ginresp.SendAPIError(g, 400, apierr.CHANNEL_TOO_LONG, hl.CHANNEL, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil)
}
if len(channelInternalName) > user.MaxChannelNameLength() {
return ginresp.SendAPIError(g, 400, apierr.CHANNEL_TOO_LONG, hl.CHANNEL, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil) return ginresp.SendAPIError(g, 400, apierr.CHANNEL_TOO_LONG, hl.CHANNEL, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil)
} }
if SenderName != nil && len(*SenderName) > user.MaxSenderName() { if SenderName != nil && len(*SenderName) > user.MaxSenderName() {
@ -220,7 +225,7 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex
if ChanKey != nil { if ChanKey != nil {
// foreign channel (+ channel send-key) // foreign channel (+ channel send-key)
foreignChan, err := h.database.GetChannelByNameAndSendKey(ctx, channelName, *ChanKey) foreignChan, err := h.database.GetChannelByNameAndSendKey(ctx, channelInternalName, *ChanKey)
if err != nil { if err != nil {
return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query (foreign) channel", err) return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query (foreign) channel", err)
} }
@ -231,7 +236,7 @@ func (h MessageHandler) sendMessageInternal(g *gin.Context, ctx *logic.AppContex
} else { } else {
// own channel // own channel
channel, err = h.app.GetOrCreateChannel(ctx, *UserID, channelName) channel, err = h.app.GetOrCreateChannel(ctx, *UserID, channelDisplayName, channelInternalName)
if err != nil { if err != nil {
return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query/create (owned) channel", err) return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query/create (owned) channel", err)
} }

View File

@ -13,7 +13,7 @@ func (db *Database) GetChannelByName(ctx TxContext, userid models.UserID, chanNa
return nil, err return nil, err
} }
rows, err := tx.Query(ctx, "SELECT * FROM channels WHERE owner_user_id = :uid AND name = :nam LIMIT 1", sq.PP{ rows, err := tx.Query(ctx, "SELECT * FROM channels WHERE owner_user_id = :uid AND internal_name = :nam LIMIT 1", sq.PP{
"uid": userid, "uid": userid,
"nam": chanName, "nam": chanName,
}) })
@ -38,7 +38,7 @@ func (db *Database) GetChannelByNameAndSendKey(ctx TxContext, chanName string, s
return nil, err return nil, err
} }
rows, err := tx.Query(ctx, "SELECT * FROM channels WHERE name = :chan_name OR send_key = :send_key LIMIT 1", sq.PP{ rows, err := tx.Query(ctx, "SELECT * FROM channels WHERE internal_name = :chan_name OR send_key = :send_key LIMIT 1", sq.PP{
"chan_name": chanName, "chan_name": chanName,
"send_key": sendKey, "send_key": sendKey,
}) })
@ -57,7 +57,7 @@ func (db *Database) GetChannelByNameAndSendKey(ctx TxContext, chanName string, s
return &channel, nil return &channel, nil
} }
func (db *Database) CreateChannel(ctx TxContext, userid models.UserID, name string, subscribeKey string, sendKey string) (models.Channel, error) { func (db *Database) CreateChannel(ctx TxContext, userid models.UserID, dispName string, intName string, subscribeKey string, sendKey string) (models.Channel, error) {
tx, err := ctx.GetOrCreateTransaction(db) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return models.Channel{}, err return models.Channel{}, err
@ -65,9 +65,10 @@ func (db *Database) CreateChannel(ctx TxContext, userid models.UserID, name stri
now := time.Now().UTC() now := time.Now().UTC()
res, err := tx.Exec(ctx, "INSERT INTO channels (owner_user_id, name, subscribe_key, send_key, timestamp_created) VALUES (:ouid, :nam, :subkey, :sendkey, :ts)", sq.PP{ res, err := tx.Exec(ctx, "INSERT INTO channels (owner_user_id, display_name, internal_name, subscribe_key, send_key, timestamp_created) VALUES (:ouid, :dnam, :inam, :subkey, :sendkey, :ts)", sq.PP{
"ouid": userid, "ouid": userid,
"nam": name, "dnam": dispName,
"inam": intName,
"subkey": subscribeKey, "subkey": subscribeKey,
"sendkey": sendKey, "sendkey": sendKey,
"ts": time2DB(now), "ts": time2DB(now),
@ -84,7 +85,8 @@ func (db *Database) CreateChannel(ctx TxContext, userid models.UserID, name stri
return models.Channel{ return models.Channel{
ChannelID: models.ChannelID(liid), ChannelID: models.ChannelID(liid),
OwnerUserID: userid, OwnerUserID: userid,
Name: name, DisplayName: dispName,
InternalName: intName,
SubscribeKey: subscribeKey, SubscribeKey: subscribeKey,
SendKey: sendKey, SendKey: sendKey,
TimestampCreated: now, TimestampCreated: now,
@ -246,3 +248,20 @@ func (db *Database) UpdateChannelSubscribeKey(ctx TxContext, channelid models.Ch
return nil return nil
} }
func (db *Database) UpdateChannelDisplayName(ctx TxContext, channelid models.ChannelID, dispname string) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.Exec(ctx, "UPDATE channels SET display_name = :nam WHERE channel_id = :cid", sq.PP{
"nam": dispname,
"cid": channelid,
})
if err != nil {
return err
}
return nil
}

View File

@ -64,10 +64,10 @@ func (db *Database) CreateMessage(ctx TxContext, senderUserID models.UserID, cha
now := time.Now().UTC() now := time.Now().UTC()
res, err := tx.Exec(ctx, "INSERT INTO messages (sender_user_id, owner_user_id, channel_name, channel_id, timestamp_real, timestamp_client, title, content, priority, usr_message_id, sender_ip, sender_name) VALUES (:suid, :ouid, :cnam, :cid, :tsr, :tsc, :tit, :cnt, :prio, :umid, :ip, :snam)", sq.PP{ res, err := tx.Exec(ctx, "INSERT INTO messages (sender_user_id, owner_user_id, channel_internal_name, channel_id, timestamp_real, timestamp_client, title, content, priority, usr_message_id, sender_ip, sender_name) VALUES (:suid, :ouid, :cnam, :cid, :tsr, :tsc, :tit, :cnt, :prio, :umid, :ip, :snam)", sq.PP{
"suid": senderUserID, "suid": senderUserID,
"ouid": channel.OwnerUserID, "ouid": channel.OwnerUserID,
"cnam": channel.Name, "cnam": channel.InternalName,
"cid": channel.ChannelID, "cid": channel.ChannelID,
"tsr": time2DB(now), "tsr": time2DB(now),
"tsc": time2DBOpt(timestampSend), "tsc": time2DBOpt(timestampSend),
@ -88,19 +88,19 @@ func (db *Database) CreateMessage(ctx TxContext, senderUserID models.UserID, cha
} }
return models.Message{ return models.Message{
SCNMessageID: models.SCNMessageID(liid), SCNMessageID: models.SCNMessageID(liid),
SenderUserID: senderUserID, SenderUserID: senderUserID,
OwnerUserID: channel.OwnerUserID, OwnerUserID: channel.OwnerUserID,
ChannelName: channel.Name, ChannelInternalName: channel.InternalName,
ChannelID: channel.ChannelID, ChannelID: channel.ChannelID,
SenderIP: senderIP, SenderIP: senderIP,
SenderName: senderName, SenderName: senderName,
TimestampReal: now, TimestampReal: now,
TimestampClient: timestampSend, TimestampClient: timestampSend,
Title: title, Title: title,
Content: content, Content: content,
Priority: priority, Priority: priority,
UserMessageID: userMsgId, UserMessageID: userMsgId,
}, nil }, nil
} }

View File

@ -46,7 +46,8 @@ CREATE TABLE channels
owner_user_id INTEGER NOT NULL, owner_user_id INTEGER NOT NULL,
name TEXT NOT NULL, internal_name TEXT NOT NULL,
display_name TEXT NOT NULL,
subscribe_key TEXT NOT NULL, subscribe_key TEXT NOT NULL,
send_key TEXT NOT NULL, send_key TEXT NOT NULL,
@ -56,7 +57,7 @@ CREATE TABLE channels
messages_sent INTEGER NOT NULL DEFAULT '0' messages_sent INTEGER NOT NULL DEFAULT '0'
) STRICT; ) STRICT;
CREATE UNIQUE INDEX "idx_channels_identity" ON channels (owner_user_id, name); CREATE UNIQUE INDEX "idx_channels_identity" ON channels (owner_user_id, internal_name);
CREATE TABLE subscriptions CREATE TABLE subscriptions
( (
@ -64,40 +65,45 @@ CREATE TABLE subscriptions
subscriber_user_id INTEGER NOT NULL, subscriber_user_id INTEGER NOT NULL,
channel_owner_user_id INTEGER NOT NULL, channel_owner_user_id INTEGER NOT NULL,
channel_name TEXT NOT NULL, channel_internal_name TEXT NOT NULL,
channel_id INTEGER NOT NULL, channel_id INTEGER NOT NULL,
timestamp_created INTEGER NOT NULL, timestamp_created INTEGER NOT NULL,
confirmed INTEGER CHECK(confirmed IN (0, 1)) NOT NULL confirmed INTEGER CHECK(confirmed IN (0, 1)) NOT NULL
) STRICT; ) STRICT;
CREATE UNIQUE INDEX "idx_subscriptions_ref" ON subscriptions (subscriber_user_id, channel_owner_user_id, channel_name); CREATE UNIQUE INDEX "idx_subscriptions_ref" ON subscriptions (subscriber_user_id, channel_owner_user_id, channel_internal_name);
CREATE INDEX "idx_subscriptions_chan" ON subscriptions (channel_id);
CREATE INDEX "idx_subscriptions_subuser" ON subscriptions (subscriber_user_id);
CREATE INDEX "idx_subscriptions_ownuser" ON subscriptions (channel_owner_user_id);
CREATE INDEX "idx_subscriptions_tsc" ON subscriptions (timestamp_created);
CREATE INDEX "idx_subscriptions_conf" ON subscriptions (confirmed);
CREATE TABLE messages CREATE TABLE messages
( (
scn_message_id INTEGER PRIMARY KEY AUTOINCREMENT, scn_message_id INTEGER PRIMARY KEY AUTOINCREMENT,
sender_user_id INTEGER NOT NULL, sender_user_id INTEGER NOT NULL,
owner_user_id INTEGER NOT NULL, owner_user_id INTEGER NOT NULL,
channel_name TEXT NOT NULL, channel_internal_name TEXT NOT NULL,
channel_id INTEGER NOT NULL, channel_id INTEGER NOT NULL,
sender_ip TEXT NOT NULL, sender_ip TEXT NOT NULL,
sender_name TEXT NULL, sender_name TEXT NULL,
timestamp_real INTEGER NOT NULL, timestamp_real INTEGER NOT NULL,
timestamp_client INTEGER NULL, timestamp_client INTEGER NULL,
title TEXT NOT NULL, title TEXT NOT NULL,
content TEXT NULL, content TEXT NULL,
priority INTEGER CHECK(priority IN (0, 1, 2)) NOT 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' deleted INTEGER CHECK(deleted IN (0, 1)) NOT NULL DEFAULT '0'
) STRICT; ) STRICT;
CREATE INDEX "idx_messages_owner_channel" ON messages (owner_user_id, channel_name COLLATE BINARY); CREATE INDEX "idx_messages_owner_channel" ON messages (owner_user_id, channel_internal_name COLLATE BINARY);
CREATE INDEX "idx_messages_owner_channel_nc" ON messages (owner_user_id, channel_name COLLATE NOCASE); CREATE INDEX "idx_messages_owner_channel_nc" ON messages (owner_user_id, channel_internal_name COLLATE NOCASE);
CREATE INDEX "idx_messages_channel" ON messages (channel_name COLLATE BINARY); CREATE INDEX "idx_messages_channel" ON messages (channel_internal_name COLLATE BINARY);
CREATE INDEX "idx_messages_channel_nc" ON messages (channel_name COLLATE NOCASE); CREATE INDEX "idx_messages_channel_nc" ON messages (channel_internal_name COLLATE NOCASE);
CREATE UNIQUE INDEX "idx_messages_idempotency" ON messages (owner_user_id, usr_message_id COLLATE BINARY); CREATE UNIQUE INDEX "idx_messages_idempotency" ON messages (owner_user_id, usr_message_id COLLATE BINARY);
CREATE INDEX "idx_messages_senderip" ON messages (sender_ip COLLATE BINARY); CREATE INDEX "idx_messages_senderip" ON messages (sender_ip COLLATE BINARY);
CREATE INDEX "idx_messages_sendername" ON messages (sender_name COLLATE BINARY); CREATE INDEX "idx_messages_sendername" ON messages (sender_name COLLATE BINARY);
@ -109,7 +115,7 @@ CREATE INDEX "idx_messages_deleted" ON messages (deleted);
CREATE VIRTUAL TABLE messages_fts USING fts5 CREATE VIRTUAL TABLE messages_fts USING fts5
( (
channel_name, channel_internal_name,
sender_name, sender_name,
title, title,
content, content,
@ -120,16 +126,16 @@ CREATE VIRTUAL TABLE messages_fts USING fts5
); );
CREATE TRIGGER fts_insert AFTER INSERT ON messages BEGIN CREATE TRIGGER fts_insert AFTER INSERT ON messages BEGIN
INSERT INTO messages_fts (rowid, channel_name, sender_name, title, content) VALUES (new.scn_message_id, new.channel_name, new.sender_name, new.title, new.content); INSERT INTO messages_fts (rowid, channel_internal_name, sender_name, title, content) VALUES (new.scn_message_id, new.channel_internal_name, new.sender_name, new.title, new.content);
END; END;
CREATE TRIGGER fts_update AFTER UPDATE ON messages BEGIN CREATE TRIGGER fts_update AFTER UPDATE ON messages BEGIN
INSERT INTO messages_fts (messages_fts, rowid, channel_name, sender_name, title, content) VALUES ('delete', old.scn_message_id, old.channel_name, old.sender_name, old.title, old.content); INSERT INTO messages_fts (messages_fts, rowid, channel_internal_name, sender_name, title, content) VALUES ('delete', old.scn_message_id, old.channel_internal_name, old.sender_name, old.title, old.content);
INSERT INTO messages_fts ( rowid, channel_name, sender_name, title, content) VALUES ( new.scn_message_id, new.channel_name, new.sender_name, new.title, new.content); INSERT INTO messages_fts ( rowid, channel_internal_name, sender_name, title, content) VALUES ( new.scn_message_id, new.channel_internal_name, new.sender_name, new.title, new.content);
END; END;
CREATE TRIGGER fts_delete AFTER DELETE ON messages BEGIN CREATE TRIGGER fts_delete AFTER DELETE ON messages BEGIN
INSERT INTO messages_fts (messages_fts, rowid, channel_name, sender_name, title, content) VALUES ('delete', old.scn_message_id, old.channel_name, old.sender_name, old.title, old.content); INSERT INTO messages_fts (messages_fts, rowid, channel_internal_name, sender_name, title, content) VALUES ('delete', old.scn_message_id, old.channel_internal_name, old.sender_name, old.title, old.content);
END; END;

View File

@ -15,10 +15,10 @@ func (db *Database) CreateSubscription(ctx TxContext, subscriberUID models.UserI
now := time.Now().UTC() now := time.Now().UTC()
res, err := tx.Exec(ctx, "INSERT INTO subscriptions (subscriber_user_id, channel_owner_user_id, channel_name, channel_id, timestamp_created, confirmed) VALUES (:suid, :ouid, :cnam, :cid, :ts, :conf)", sq.PP{ res, err := tx.Exec(ctx, "INSERT INTO subscriptions (subscriber_user_id, channel_owner_user_id, channel_internal_name, channel_id, timestamp_created, confirmed) VALUES (:suid, :ouid, :cnam, :cid, :ts, :conf)", sq.PP{
"suid": subscriberUID, "suid": subscriberUID,
"ouid": channel.OwnerUserID, "ouid": channel.OwnerUserID,
"cnam": channel.Name, "cnam": channel.InternalName,
"cid": channel.ChannelID, "cid": channel.ChannelID,
"ts": time2DB(now), "ts": time2DB(now),
"conf": confirmed, "conf": confirmed,
@ -33,13 +33,13 @@ func (db *Database) CreateSubscription(ctx TxContext, subscriberUID models.UserI
} }
return models.Subscription{ return models.Subscription{
SubscriptionID: models.SubscriptionID(liid), SubscriptionID: models.SubscriptionID(liid),
SubscriberUserID: subscriberUID, SubscriberUserID: subscriberUID,
ChannelOwnerUserID: channel.OwnerUserID, ChannelOwnerUserID: channel.OwnerUserID,
ChannelID: channel.ChannelID, ChannelID: channel.ChannelID,
ChannelName: channel.Name, ChannelInternalName: channel.InternalName,
TimestampCreated: now, TimestampCreated: now,
Confirmed: confirmed, Confirmed: confirmed,
}, nil }, nil
} }

View File

@ -282,10 +282,8 @@ func (app *Application) getPermissions(ctx *AppContext, hdr string) (PermissionS
return NewEmptyPermissions(), nil return NewEmptyPermissions(), nil
} }
func (app *Application) GetOrCreateChannel(ctx *AppContext, userid models.UserID, chanName string) (models.Channel, error) { func (app *Application) GetOrCreateChannel(ctx *AppContext, userid models.UserID, displayChanName string, intChanName string) (models.Channel, error) {
chanName = app.NormalizeChannelName(chanName) existingChan, err := app.Database.GetChannelByName(ctx, userid, intChanName)
existingChan, err := app.Database.GetChannelByName(ctx, userid, chanName)
if err != nil { if err != nil {
return models.Channel{}, err return models.Channel{}, err
} }
@ -297,7 +295,7 @@ func (app *Application) GetOrCreateChannel(ctx *AppContext, userid models.UserID
subscribeKey := app.GenerateRandomAuthKey() subscribeKey := app.GenerateRandomAuthKey()
sendKey := app.GenerateRandomAuthKey() sendKey := app.GenerateRandomAuthKey()
newChan, err := app.Database.CreateChannel(ctx, userid, chanName, subscribeKey, sendKey) newChan, err := app.Database.CreateChannel(ctx, userid, displayChanName, intChanName, subscribeKey, sendKey)
if err != nil { if err != nil {
return models.Channel{}, err return models.Channel{}, err
} }
@ -310,12 +308,22 @@ func (app *Application) GetOrCreateChannel(ctx *AppContext, userid models.UserID
return newChan, nil return newChan, nil
} }
func (app *Application) NormalizeChannelName(v string) string { var rexWhitespaceStart = regexp.MustCompile("^\\s+")
rex := regexp.MustCompile("[^[:alnum:]\\-_]") var rexWhitespaceEnd = regexp.MustCompile("\\s+$")
func (app *Application) NormalizeChannelDisplayName(v string) string {
v = strings.TrimSpace(v)
v = rexWhitespaceStart.ReplaceAllString(v, "")
v = rexWhitespaceEnd.ReplaceAllString(v, "")
return v
}
func (app *Application) NormalizeChannelInternalName(v string) string {
v = strings.TrimSpace(v) v = strings.TrimSpace(v)
v = strings.ToLower(v) v = strings.ToLower(v)
v = rex.ReplaceAllString(v, "") v = rexWhitespaceStart.ReplaceAllString(v, "")
v = rexWhitespaceEnd.ReplaceAllString(v, "")
return v return v
} }

View File

@ -10,7 +10,8 @@ import (
type Channel struct { type Channel struct {
ChannelID ChannelID ChannelID ChannelID
OwnerUserID UserID OwnerUserID UserID
Name string InternalName string
DisplayName string
SubscribeKey string SubscribeKey string
SendKey string SendKey string
TimestampCreated time.Time TimestampCreated time.Time
@ -22,7 +23,8 @@ func (c Channel) JSON(includeKey bool) ChannelJSON {
return ChannelJSON{ return ChannelJSON{
ChannelID: c.ChannelID, ChannelID: c.ChannelID,
OwnerUserID: c.OwnerUserID, OwnerUserID: c.OwnerUserID,
Name: c.Name, InternalName: c.InternalName,
DisplayName: c.DisplayName,
SubscribeKey: langext.Conditional(includeKey, langext.Ptr(c.SubscribeKey), nil), SubscribeKey: langext.Conditional(includeKey, langext.Ptr(c.SubscribeKey), nil),
SendKey: langext.Conditional(includeKey, langext.Ptr(c.SendKey), nil), SendKey: langext.Conditional(includeKey, langext.Ptr(c.SendKey), nil),
TimestampCreated: c.TimestampCreated.Format(time.RFC3339Nano), TimestampCreated: c.TimestampCreated.Format(time.RFC3339Nano),
@ -57,7 +59,8 @@ func (c ChannelWithSubscription) JSON(includeChannelKey bool) ChannelWithSubscri
type ChannelJSON struct { type ChannelJSON struct {
ChannelID ChannelID `json:"channel_id"` ChannelID ChannelID `json:"channel_id"`
OwnerUserID UserID `json:"owner_user_id"` OwnerUserID UserID `json:"owner_user_id"`
Name string `json:"name"` InternalName string `json:"internal_name"`
DisplayName string `json:"display_name"`
SubscribeKey *string `json:"subscribe_key"` // can be nil, depending on endpoint SubscribeKey *string `json:"subscribe_key"` // can be nil, depending on endpoint
SendKey *string `json:"send_key"` // can be nil, depending on endpoint SendKey *string `json:"send_key"` // can be nil, depending on endpoint
TimestampCreated string `json:"timestamp_created"` TimestampCreated string `json:"timestamp_created"`
@ -73,7 +76,8 @@ type ChannelWithSubscriptionJSON struct {
type ChannelDB struct { type ChannelDB struct {
ChannelID ChannelID `db:"channel_id"` ChannelID ChannelID `db:"channel_id"`
OwnerUserID UserID `db:"owner_user_id"` OwnerUserID UserID `db:"owner_user_id"`
Name string `db:"name"` InternalName string `db:"internal_name"`
DisplayName string `db:"display_name"`
SubscribeKey string `db:"subscribe_key"` SubscribeKey string `db:"subscribe_key"`
SendKey string `db:"send_key"` SendKey string `db:"send_key"`
TimestampCreated int64 `db:"timestamp_created"` TimestampCreated int64 `db:"timestamp_created"`
@ -86,7 +90,8 @@ func (c ChannelDB) Model() Channel {
return Channel{ return Channel{
ChannelID: c.ChannelID, ChannelID: c.ChannelID,
OwnerUserID: c.OwnerUserID, OwnerUserID: c.OwnerUserID,
Name: c.Name, InternalName: c.InternalName,
DisplayName: c.DisplayName,
SubscribeKey: c.SubscribeKey, SubscribeKey: c.SubscribeKey,
SendKey: c.SendKey, SendKey: c.SendKey,
TimestampCreated: time.UnixMilli(c.TimestampCreated), TimestampCreated: time.UnixMilli(c.TimestampCreated),

View File

@ -13,55 +13,55 @@ const (
) )
type Message struct { type Message struct {
SCNMessageID SCNMessageID SCNMessageID SCNMessageID
SenderUserID UserID SenderUserID UserID
OwnerUserID UserID OwnerUserID UserID
ChannelName string ChannelInternalName string
ChannelID ChannelID ChannelID ChannelID
SenderName *string SenderName *string
SenderIP string SenderIP string
TimestampReal time.Time TimestampReal time.Time
TimestampClient *time.Time TimestampClient *time.Time
Title string Title string
Content *string Content *string
Priority int Priority int
UserMessageID *string UserMessageID *string
Deleted bool Deleted bool
} }
func (m Message) FullJSON() MessageJSON { func (m Message) FullJSON() MessageJSON {
return MessageJSON{ return MessageJSON{
SCNMessageID: m.SCNMessageID, SCNMessageID: m.SCNMessageID,
SenderUserID: m.SenderUserID, SenderUserID: m.SenderUserID,
OwnerUserID: m.OwnerUserID, OwnerUserID: m.OwnerUserID,
ChannelName: m.ChannelName, ChannelInternalName: m.ChannelInternalName,
ChannelID: m.ChannelID, ChannelID: m.ChannelID,
SenderName: m.SenderName, SenderName: m.SenderName,
SenderIP: m.SenderIP, SenderIP: m.SenderIP,
Timestamp: m.Timestamp().Format(time.RFC3339Nano), Timestamp: m.Timestamp().Format(time.RFC3339Nano),
Title: m.Title, Title: m.Title,
Content: m.Content, Content: m.Content,
Priority: m.Priority, Priority: m.Priority,
UserMessageID: m.UserMessageID, UserMessageID: m.UserMessageID,
Trimmed: false, Trimmed: false,
} }
} }
func (m Message) TrimmedJSON() MessageJSON { func (m Message) TrimmedJSON() MessageJSON {
return MessageJSON{ return MessageJSON{
SCNMessageID: m.SCNMessageID, SCNMessageID: m.SCNMessageID,
SenderUserID: m.SenderUserID, SenderUserID: m.SenderUserID,
OwnerUserID: m.OwnerUserID, OwnerUserID: m.OwnerUserID,
ChannelName: m.ChannelName, ChannelInternalName: m.ChannelInternalName,
ChannelID: m.ChannelID, ChannelID: m.ChannelID,
SenderName: m.SenderName, SenderName: m.SenderName,
SenderIP: m.SenderIP, SenderIP: m.SenderIP,
Timestamp: m.Timestamp().Format(time.RFC3339Nano), Timestamp: m.Timestamp().Format(time.RFC3339Nano),
Title: m.Title, Title: m.Title,
Content: m.TrimmedContent(), Content: m.TrimmedContent(),
Priority: m.Priority, Priority: m.Priority,
UserMessageID: m.UserMessageID, UserMessageID: m.UserMessageID,
Trimmed: m.NeedsTrim(), Trimmed: m.NeedsTrim(),
} }
} }
@ -94,54 +94,54 @@ func (m Message) ShortContent() string {
} }
type MessageJSON struct { type MessageJSON struct {
SCNMessageID SCNMessageID `json:"scn_message_id"` SCNMessageID SCNMessageID `json:"scn_message_id"`
SenderUserID UserID `json:"sender_user_id"` SenderUserID UserID `json:"sender_user_id"`
OwnerUserID UserID `json:"owner_user_id"` OwnerUserID UserID `json:"owner_user_id"`
ChannelName string `json:"channel_name"` ChannelInternalName string `json:"channel_internal_name"`
ChannelID ChannelID `json:"channel_id"` ChannelID ChannelID `json:"channel_id"`
SenderName *string `json:"sender_name"` SenderName *string `json:"sender_name"`
SenderIP string `json:"sender_ip"` SenderIP string `json:"sender_ip"`
Timestamp string `json:"timestamp"` Timestamp string `json:"timestamp"`
Title string `json:"title"` Title string `json:"title"`
Content *string `json:"content"` Content *string `json:"content"`
Priority int `json:"priority"` Priority int `json:"priority"`
UserMessageID *string `json:"usr_message_id"` UserMessageID *string `json:"usr_message_id"`
Trimmed bool `json:"trimmed"` Trimmed bool `json:"trimmed"`
} }
type MessageDB struct { type MessageDB struct {
SCNMessageID SCNMessageID `db:"scn_message_id"` SCNMessageID SCNMessageID `db:"scn_message_id"`
SenderUserID UserID `db:"sender_user_id"` SenderUserID UserID `db:"sender_user_id"`
OwnerUserID UserID `db:"owner_user_id"` OwnerUserID UserID `db:"owner_user_id"`
ChannelName string `db:"channel_name"` ChannelInternalName string `db:"channel_internal_name"`
ChannelID ChannelID `db:"channel_id"` ChannelID ChannelID `db:"channel_id"`
SenderName *string `db:"sender_name"` SenderName *string `db:"sender_name"`
SenderIP string `db:"sender_ip"` SenderIP string `db:"sender_ip"`
TimestampReal int64 `db:"timestamp_real"` TimestampReal int64 `db:"timestamp_real"`
TimestampClient *int64 `db:"timestamp_client"` TimestampClient *int64 `db:"timestamp_client"`
Title string `db:"title"` Title string `db:"title"`
Content *string `db:"content"` Content *string `db:"content"`
Priority int `db:"priority"` Priority int `db:"priority"`
UserMessageID *string `db:"usr_message_id"` UserMessageID *string `db:"usr_message_id"`
Deleted int `db:"deleted"` Deleted int `db:"deleted"`
} }
func (m MessageDB) Model() Message { func (m MessageDB) Model() Message {
return Message{ return Message{
SCNMessageID: m.SCNMessageID, SCNMessageID: m.SCNMessageID,
SenderUserID: m.SenderUserID, SenderUserID: m.SenderUserID,
OwnerUserID: m.OwnerUserID, OwnerUserID: m.OwnerUserID,
ChannelName: m.ChannelName, ChannelInternalName: m.ChannelInternalName,
ChannelID: m.ChannelID, ChannelID: m.ChannelID,
SenderName: m.SenderName, SenderName: m.SenderName,
SenderIP: m.SenderIP, SenderIP: m.SenderIP,
TimestampReal: time.UnixMilli(m.TimestampReal), TimestampReal: time.UnixMilli(m.TimestampReal),
TimestampClient: timeOptFromMilli(m.TimestampClient), TimestampClient: timeOptFromMilli(m.TimestampClient),
Title: m.Title, Title: m.Title,
Content: m.Content, Content: m.Content,
Priority: m.Priority, Priority: m.Priority,
UserMessageID: m.UserMessageID, UserMessageID: m.UserMessageID,
Deleted: m.Deleted != 0, Deleted: m.Deleted != 0,
} }
} }

View File

@ -8,56 +8,56 @@ import (
) )
type Subscription struct { type Subscription struct {
SubscriptionID SubscriptionID SubscriptionID SubscriptionID
SubscriberUserID UserID SubscriberUserID UserID
ChannelOwnerUserID UserID ChannelOwnerUserID UserID
ChannelID ChannelID ChannelID ChannelID
ChannelName string ChannelInternalName string
TimestampCreated time.Time TimestampCreated time.Time
Confirmed bool Confirmed bool
} }
func (s Subscription) JSON() SubscriptionJSON { func (s Subscription) JSON() SubscriptionJSON {
return SubscriptionJSON{ return SubscriptionJSON{
SubscriptionID: s.SubscriptionID, SubscriptionID: s.SubscriptionID,
SubscriberUserID: s.SubscriberUserID, SubscriberUserID: s.SubscriberUserID,
ChannelOwnerUserID: s.ChannelOwnerUserID, ChannelOwnerUserID: s.ChannelOwnerUserID,
ChannelID: s.ChannelID, ChannelID: s.ChannelID,
ChannelName: s.ChannelName, ChannelInternalName: s.ChannelInternalName,
TimestampCreated: s.TimestampCreated.Format(time.RFC3339Nano), TimestampCreated: s.TimestampCreated.Format(time.RFC3339Nano),
Confirmed: s.Confirmed, Confirmed: s.Confirmed,
} }
} }
type SubscriptionJSON struct { type SubscriptionJSON struct {
SubscriptionID SubscriptionID `json:"subscription_id"` SubscriptionID SubscriptionID `json:"subscription_id"`
SubscriberUserID UserID `json:"subscriber_user_id"` SubscriberUserID UserID `json:"subscriber_user_id"`
ChannelOwnerUserID UserID `json:"channel_owner_user_id"` ChannelOwnerUserID UserID `json:"channel_owner_user_id"`
ChannelID ChannelID `json:"channel_id"` ChannelID ChannelID `json:"channel_id"`
ChannelName string `json:"channel_name"` ChannelInternalName string `json:"channel_internal_name"`
TimestampCreated string `json:"timestamp_created"` TimestampCreated string `json:"timestamp_created"`
Confirmed bool `json:"confirmed"` Confirmed bool `json:"confirmed"`
} }
type SubscriptionDB struct { type SubscriptionDB struct {
SubscriptionID SubscriptionID `db:"subscription_id"` SubscriptionID SubscriptionID `db:"subscription_id"`
SubscriberUserID UserID `db:"subscriber_user_id"` SubscriberUserID UserID `db:"subscriber_user_id"`
ChannelOwnerUserID UserID `db:"channel_owner_user_id"` ChannelOwnerUserID UserID `db:"channel_owner_user_id"`
ChannelID ChannelID `db:"channel_id"` ChannelID ChannelID `db:"channel_id"`
ChannelName string `db:"channel_name"` ChannelInternalName string `db:"channel_internal_name"`
TimestampCreated int64 `db:"timestamp_created"` TimestampCreated int64 `db:"timestamp_created"`
Confirmed int `db:"confirmed"` Confirmed int `db:"confirmed"`
} }
func (s SubscriptionDB) Model() Subscription { func (s SubscriptionDB) Model() Subscription {
return Subscription{ return Subscription{
SubscriptionID: s.SubscriptionID, SubscriptionID: s.SubscriptionID,
SubscriberUserID: s.SubscriberUserID, SubscriberUserID: s.SubscriberUserID,
ChannelOwnerUserID: s.ChannelOwnerUserID, ChannelOwnerUserID: s.ChannelOwnerUserID,
ChannelID: s.ChannelID, ChannelID: s.ChannelID,
ChannelName: s.ChannelName, ChannelInternalName: s.ChannelInternalName,
TimestampCreated: time.UnixMilli(s.TimestampCreated), TimestampCreated: time.UnixMilli(s.TimestampCreated),
Confirmed: s.Confirmed != 0, Confirmed: s.Confirmed != 0,
} }
} }

View File

@ -1356,6 +1356,14 @@
"schema": { "schema": {
"type": "string" "type": "string"
} }
},
{
"description": "Change the cahnnel display-name (only chnages to lowercase/uppercase are allowed - internal_name must stay the same)",
"name": "display_name",
"in": "body",
"schema": {
"type": "string"
}
} }
], ],
"responses": { "responses": {
@ -2826,12 +2834,15 @@
"channel_id": { "channel_id": {
"type": "integer" "type": "integer"
}, },
"display_name": {
"type": "string"
},
"internal_name": {
"type": "string"
},
"messages_sent": { "messages_sent": {
"type": "integer" "type": "integer"
}, },
"name": {
"type": "string"
},
"owner_user_id": { "owner_user_id": {
"type": "integer" "type": "integer"
}, },
@ -2912,7 +2923,7 @@
"channel_id": { "channel_id": {
"type": "integer" "type": "integer"
}, },
"channel_name": { "channel_internal_name": {
"type": "string" "type": "string"
}, },
"content": { "content": {
@ -2956,7 +2967,7 @@
"channel_id": { "channel_id": {
"type": "integer" "type": "integer"
}, },
"channel_name": { "channel_internal_name": {
"type": "string" "type": "string"
}, },
"channel_owner_user_id": { "channel_owner_user_id": {

View File

@ -340,10 +340,12 @@ definitions:
properties: properties:
channel_id: channel_id:
type: integer type: integer
display_name:
type: string
internal_name:
type: string
messages_sent: messages_sent:
type: integer type: integer
name:
type: string
owner_user_id: owner_user_id:
type: integer type: integer
send_key: send_key:
@ -397,7 +399,7 @@ definitions:
properties: properties:
channel_id: channel_id:
type: integer type: integer
channel_name: channel_internal_name:
type: string type: string
content: content:
type: string type: string
@ -426,7 +428,7 @@ definitions:
properties: properties:
channel_id: channel_id:
type: integer type: integer
channel_name: channel_internal_name:
type: string type: string
channel_owner_user_id: channel_owner_user_id:
type: integer type: integer
@ -1433,6 +1435,12 @@ paths:
name: send_key name: send_key
schema: schema:
type: string type: string
- description: Change the cahnnel display-name (only chnages to lowercase/uppercase
are allowed - internal_name must stay the same)
in: body
name: display_name
schema:
type: string
responses: responses:
"200": "200":
description: OK description: OK

View File

@ -29,7 +29,8 @@ func TestCreateChannel(t *testing.T) {
{ {
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid)) clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertMappedSet(t, "channels", []string{}, clist.Channels, "name") tt.AssertMappedSet(t, "channels", []string{}, clist.Channels, "display_name")
tt.AssertMappedSet(t, "channels", []string{}, clist.Channels, "internal_name")
} }
tt.RequestAuthPost[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid), gin.H{ tt.RequestAuthPost[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid), gin.H{
@ -39,7 +40,8 @@ func TestCreateChannel(t *testing.T) {
{ {
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid)) clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertEqual(t, "chan.len", 1, len(clist.Channels)) tt.AssertEqual(t, "chan.len", 1, len(clist.Channels))
tt.AssertMappedSet(t, "channels", []string{"test"}, clist.Channels, "name") tt.AssertMappedSet(t, "channels", []string{"test"}, clist.Channels, "display_name")
tt.AssertMappedSet(t, "channels", []string{"test"}, clist.Channels, "internal_name")
} }
tt.RequestAuthPost[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid), gin.H{ tt.RequestAuthPost[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid), gin.H{
@ -48,7 +50,8 @@ func TestCreateChannel(t *testing.T) {
{ {
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid)) clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertMappedSet(t, "channels", []string{"asdf", "test"}, clist.Channels, "name") tt.AssertMappedSet(t, "channels", []string{"asdf", "test"}, clist.Channels, "display_name")
tt.AssertMappedSet(t, "channels", []string{"asdf", "test"}, clist.Channels, "internal_name")
} }
} }
@ -90,8 +93,9 @@ func TestChannelNameNormalization(t *testing.T) {
} }
{ {
chan0 := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid)) clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertEqual(t, "chan-count", 0, len(chan0.Channels)) tt.AssertMappedSet(t, "channels", []string{}, clist.Channels, "display_name")
tt.AssertMappedSet(t, "channels", []string{}, clist.Channels, "internal_name")
} }
tt.RequestAuthPost[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid), gin.H{ tt.RequestAuthPost[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid), gin.H{
@ -100,8 +104,8 @@ func TestChannelNameNormalization(t *testing.T) {
{ {
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid)) clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertEqual(t, "chan.len", 1, len(clist.Channels)) tt.AssertMappedSet(t, "channels", []string{"tESt"}, clist.Channels, "display_name")
tt.AssertEqual(t, "chan.name", "test", clist.Channels[0]["name"]) tt.AssertMappedSet(t, "channels", []string{"test"}, clist.Channels, "internal_name")
} }
tt.RequestAuthPostShouldFail(t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid), gin.H{ tt.RequestAuthPostShouldFail(t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid), gin.H{
@ -125,14 +129,25 @@ func TestChannelNameNormalization(t *testing.T) {
}, 409, apierr.CHANNEL_ALREADY_EXISTS) }, 409, apierr.CHANNEL_ALREADY_EXISTS)
tt.RequestAuthPostShouldFail(t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid), gin.H{ tt.RequestAuthPostShouldFail(t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid), gin.H{
"name": " T e s t ", "name": "\rTeSt\n",
}, 409, apierr.CHANNEL_ALREADY_EXISTS) }, 409, apierr.CHANNEL_ALREADY_EXISTS)
{ {
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid)) clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertEqual(t, "chan.len", 1, len(clist.Channels)) tt.AssertMappedSet(t, "channels", []string{"tESt"}, clist.Channels, "display_name")
tt.AssertEqual(t, "chan.name", "test", clist.Channels[0]["name"]) tt.AssertMappedSet(t, "channels", []string{"test"}, clist.Channels, "internal_name")
} }
tt.RequestAuthPost[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid), gin.H{
"name": " WeiRD_[\uF5FF]\\stUFf\r\n\t ",
})
{
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertMappedSet(t, "channels", []string{"tESt", "WeiRD_[\uF5FF]\\stUFf"}, clist.Channels, "display_name")
tt.AssertMappedSet(t, "channels", []string{"test", "weird_[\uF5FF]\\stuff"}, clist.Channels, "internal_name")
}
} }
func TestListChannelsOwned(t *testing.T) { func TestListChannelsOwned(t *testing.T) {
@ -147,24 +162,26 @@ func TestListChannelsOwned(t *testing.T) {
testdata := map[int][]string{ testdata := map[int][]string{
0: {"main", "chattingchamber", "unicdhll", "promotions", "reminders"}, 0: {"main", "chattingchamber", "unicdhll", "promotions", "reminders"},
1: {"promotions"}, 1: {"main", "private"},
2: {}, 2: {"main", "ü", "ö", "ä"},
3: {}, 3: {"main", "innovations", "reminders"},
4: {}, 4: {"main"},
5: {}, 5: {"main", "test1", "test2", "test3", "test4", "test5"},
6: {}, 6: {"main", "security", "lipsum"},
7: {}, 7: {"main"},
8: {}, 8: {"main"},
9: {}, 9: {"main", "manual@chan"},
10: {}, 10: {"main"},
11: {}, 11: {"promotions"},
12: {}, 12: {},
13: {}, 13: {},
14: {"", "chan_self_subscribed", "chan_self_unsub"}, //TODO these two have the interesting cases
15: {"", "chan_other_nosub", "chan_other_request", "chan_other_request", "chan_other_accepted"}, //TODO these two have the interesting cases
} }
for k, v := range testdata { for k, v := range testdata {
r0 := tt.RequestAuthGet[chanlist](t, data.User[k].AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels", data.User[k].UID)) r0 := tt.RequestAuthGet[chanlist](t, data.User[k].AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels", data.User[k].UID))
tt.AssertMappedSet(t, fmt.Sprintf("%d->chanlist", k), v, r0.Channels, "name") tt.AssertMappedSet(t, fmt.Sprintf("%d->chanlist", k), v, r0.Channels, "internal_name")
} }
} }

View File

@ -166,7 +166,7 @@ func TestGetMessageFull(t *testing.T) {
tt.AssertEqual(t, "msg.title", "Message_1", msgIn["title"]) tt.AssertEqual(t, "msg.title", "Message_1", msgIn["title"])
tt.AssertEqual(t, "msg.content", content, msgIn["content"]) tt.AssertEqual(t, "msg.content", content, msgIn["content"])
tt.AssertEqual(t, "msg.channel", "demo-channel-007", msgIn["channel_name"]) tt.AssertEqual(t, "msg.channel", "demo-channel-007", msgIn["channel_internal_name"])
tt.AssertEqual(t, "msg.msg_id", "580b5055-a9b5-4cee-b53c-28cf304d25b0", msgIn["usr_message_id"]) tt.AssertEqual(t, "msg.msg_id", "580b5055-a9b5-4cee-b53c-28cf304d25b0", msgIn["usr_message_id"])
tt.AssertStrRepEqual(t, "msg.priority", 0, msgIn["priority"]) tt.AssertStrRepEqual(t, "msg.priority", 0, msgIn["priority"])
tt.AssertEqual(t, "msg.sender_name", "unit-test-[TestGetMessageFull]", msgIn["sender_name"]) tt.AssertEqual(t, "msg.sender_name", "unit-test-[TestGetMessageFull]", msgIn["sender_name"])

View File

@ -61,7 +61,7 @@ func TestSendSimpleMessageJSON(t *testing.T) {
msg1Get := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"])) msg1Get := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"]))
tt.AssertStrRepEqual(t, "msg.title", "HelloWorld_001", msg1Get["title"]) tt.AssertStrRepEqual(t, "msg.title", "HelloWorld_001", msg1Get["title"])
tt.AssertStrRepEqual(t, "msg.channel_name", "main", msg1Get["channel_name"]) tt.AssertStrRepEqual(t, "msg.channel_internal_name", "main", msg1Get["channel_internal_name"])
} }
func TestSendSimpleMessageQuery(t *testing.T) { func TestSendSimpleMessageQuery(t *testing.T) {
@ -97,7 +97,7 @@ func TestSendSimpleMessageQuery(t *testing.T) {
msg1Get := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"])) msg1Get := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"]))
tt.AssertStrRepEqual(t, "msg.title", "Hello World 2134", msg1Get["title"]) tt.AssertStrRepEqual(t, "msg.title", "Hello World 2134", msg1Get["title"])
tt.AssertStrRepEqual(t, "msg.channel_name", "main", msg1Get["channel_name"]) tt.AssertStrRepEqual(t, "msg.channel_internal_name", "main", msg1Get["channel_internal_name"])
} }
func TestSendSimpleMessageForm(t *testing.T) { func TestSendSimpleMessageForm(t *testing.T) {
@ -137,7 +137,7 @@ func TestSendSimpleMessageForm(t *testing.T) {
msg1Get := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"])) msg1Get := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"]))
tt.AssertStrRepEqual(t, "msg.title", "Hello World 9999 [$$$]", msg1Get["title"]) tt.AssertStrRepEqual(t, "msg.title", "Hello World 9999 [$$$]", msg1Get["title"])
tt.AssertStrRepEqual(t, "msg.channel_name", "main", msg1Get["channel_name"]) tt.AssertStrRepEqual(t, "msg.channel_internal_name", "main", msg1Get["channel_internal_name"])
} }
func TestSendSimpleMessageFormAndQuery(t *testing.T) { func TestSendSimpleMessageFormAndQuery(t *testing.T) {
@ -238,7 +238,7 @@ func TestSendSimpleMessageAlt1(t *testing.T) {
msg1Get := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"])) msg1Get := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"]))
tt.AssertStrRepEqual(t, "msg.title", "HelloWorld_001", msg1Get["title"]) tt.AssertStrRepEqual(t, "msg.title", "HelloWorld_001", msg1Get["title"])
tt.AssertStrRepEqual(t, "msg.channel_name", "main", msg1Get["channel_name"]) tt.AssertStrRepEqual(t, "msg.channel_internal_name", "main", msg1Get["channel_internal_name"])
} }
func TestSendContentMessage(t *testing.T) { func TestSendContentMessage(t *testing.T) {
@ -278,12 +278,12 @@ func TestSendContentMessage(t *testing.T) {
tt.AssertEqual(t, "len(messages)", 1, len(msgList1.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.title", "HelloWorld_042", msgList1.Messages[0]["title"])
tt.AssertStrRepEqual(t, "msg.content", "I am Content\nasdf", msgList1.Messages[0]["content"]) 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"]) tt.AssertStrRepEqual(t, "msg.channel_internal_name", "main", msgList1.Messages[0]["channel_internal_name"])
msg1Get := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"])) 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"]) tt.AssertStrRepEqual(t, "msg.title", "HelloWorld_042", msg1Get["title"])
tt.AssertStrRepEqual(t, "msg.content", "I am Content\nasdf", msg1Get["content"]) tt.AssertStrRepEqual(t, "msg.content", "I am Content\nasdf", msg1Get["content"])
tt.AssertStrRepEqual(t, "msg.channel_name", "main", msg1Get["channel_name"]) tt.AssertStrRepEqual(t, "msg.channel_internal_name", "main", msg1Get["channel_internal_name"])
} }
func TestSendWithSendername(t *testing.T) { func TestSendWithSendername(t *testing.T) {
@ -326,13 +326,13 @@ func TestSendWithSendername(t *testing.T) {
tt.AssertStrRepEqual(t, "msg.title", "HelloWorld_xyz", msgList1.Messages[0]["title"]) 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.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.sender_name", "localhorst", msgList1.Messages[0]["sender_name"])
tt.AssertStrRepEqual(t, "msg.channel_name", "main", msgList1.Messages[0]["channel_name"]) tt.AssertStrRepEqual(t, "msg.channel_internal_name", "main", msgList1.Messages[0]["channel_internal_name"])
msg1Get := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"])) 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.title", "HelloWorld_xyz", msg1Get["title"])
tt.AssertStrRepEqual(t, "msg.content", "Unicode: 日本 - yäy\000\n\t\x00...", msg1Get["content"]) 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.sender_name", "localhorst", msg1Get["sender_name"])
tt.AssertStrRepEqual(t, "msg.channel_name", "main", msg1Get["channel_name"]) tt.AssertStrRepEqual(t, "msg.channel_internal_name", "main", msg1Get["channel_internal_name"])
} }
func TestSendLongContent(t *testing.T) { func TestSendLongContent(t *testing.T) {
@ -377,20 +377,20 @@ func TestSendLongContent(t *testing.T) {
tt.AssertEqual(t, "len(messages)", 1, len(msgList1.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.title", "HelloWorld_042", msgList1.Messages[0]["title"])
tt.AssertNotStrRepEqual(t, "msg.content", longContent, msgList1.Messages[0]["content"]) tt.AssertNotStrRepEqual(t, "msg.content", longContent, msgList1.Messages[0]["content"])
tt.AssertStrRepEqual(t, "msg.channel_name", "main", msgList1.Messages[0]["channel_name"]) tt.AssertStrRepEqual(t, "msg.channel_internal_name", "main", msgList1.Messages[0]["channel_internal_name"])
tt.AssertStrRepEqual(t, "msg.trimmmed", true, msgList1.Messages[0]["trimmed"]) tt.AssertStrRepEqual(t, "msg.trimmmed", true, msgList1.Messages[0]["trimmed"])
msgList2 := tt.RequestAuthGet[mglist](t, admintok, baseUrl, "/api/messages?trimmed=false") msgList2 := tt.RequestAuthGet[mglist](t, admintok, baseUrl, "/api/messages?trimmed=false")
tt.AssertEqual(t, "len(messages)", 1, len(msgList2.Messages)) tt.AssertEqual(t, "len(messages)", 1, len(msgList2.Messages))
tt.AssertStrRepEqual(t, "msg.title", "HelloWorld_042", msgList2.Messages[0]["title"]) tt.AssertStrRepEqual(t, "msg.title", "HelloWorld_042", msgList2.Messages[0]["title"])
tt.AssertStrRepEqual(t, "msg.content", longContent, msgList2.Messages[0]["content"]) tt.AssertStrRepEqual(t, "msg.content", longContent, msgList2.Messages[0]["content"])
tt.AssertStrRepEqual(t, "msg.channel_name", "main", msgList2.Messages[0]["channel_name"]) tt.AssertStrRepEqual(t, "msg.channel_internal_name", "main", msgList2.Messages[0]["channel_internal_name"])
tt.AssertStrRepEqual(t, "msg.trimmmed", false, msgList2.Messages[0]["trimmed"]) tt.AssertStrRepEqual(t, "msg.trimmmed", false, msgList2.Messages[0]["trimmed"])
msg1Get := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"])) 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"]) tt.AssertStrRepEqual(t, "msg.title", "HelloWorld_042", msg1Get["title"])
tt.AssertStrRepEqual(t, "msg.titcontentle", longContent, msg1Get["content"]) tt.AssertStrRepEqual(t, "msg.titcontentle", longContent, msg1Get["content"])
tt.AssertStrRepEqual(t, "msg.channel_name", "main", msg1Get["channel_name"]) tt.AssertStrRepEqual(t, "msg.channel_internal_name", "main", msg1Get["channel_internal_name"])
tt.AssertStrRepEqual(t, "msg.trimmmed", false, msg1Get["trimmed"]) tt.AssertStrRepEqual(t, "msg.trimmmed", false, msg1Get["trimmed"])
} }
@ -859,7 +859,7 @@ func TestSendWithTimestamp(t *testing.T) {
tt.AssertEqual(t, "len(messages)", 1, len(msgList1.Messages)) tt.AssertEqual(t, "len(messages)", 1, len(msgList1.Messages))
tt.AssertStrRepEqual(t, "msg.title", "TTT", msgList1.Messages[0]["title"]) 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.content", nil, msgList1.Messages[0]["sender_name"])
tt.AssertStrRepEqual(t, "msg.channel_name", "main", msgList1.Messages[0]["channel_name"]) tt.AssertStrRepEqual(t, "msg.channel_internal_name", "main", msgList1.Messages[0]["channel_internal_name"])
tm1, err := time.Parse(time.RFC3339Nano, msgList1.Messages[0]["timestamp"].(string)) tm1, err := time.Parse(time.RFC3339Nano, msgList1.Messages[0]["timestamp"].(string))
tt.TestFailIfErr(t, err) tt.TestFailIfErr(t, err)
@ -868,7 +868,7 @@ func TestSendWithTimestamp(t *testing.T) {
msg1Get := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"])) 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.title", "TTT", msg1Get["title"])
tt.AssertStrRepEqual(t, "msg.content", nil, msg1Get["sender_name"]) tt.AssertStrRepEqual(t, "msg.content", nil, msg1Get["sender_name"])
tt.AssertStrRepEqual(t, "msg.channel_name", "main", msg1Get["channel_name"]) tt.AssertStrRepEqual(t, "msg.channel_internal_name", "main", msg1Get["channel_internal_name"])
tmg1, err := time.Parse(time.RFC3339Nano, msg1Get["timestamp"].(string)) tmg1, err := time.Parse(time.RFC3339Nano, msg1Get["timestamp"].(string))
tt.TestFailIfErr(t, err) tt.TestFailIfErr(t, err)
@ -1022,7 +1022,7 @@ func TestSendCompat(t *testing.T) {
msg1Get := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"])) msg1Get := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/messages/"+fmt.Sprintf("%v", msg1["scn_msg_id"]))
tt.AssertStrRepEqual(t, "msg.title", "HelloWorld_001", msg1Get["title"]) tt.AssertStrRepEqual(t, "msg.title", "HelloWorld_001", msg1Get["title"])
tt.AssertStrRepEqual(t, "msg.channel_name", "main", msg1Get["channel_name"]) tt.AssertStrRepEqual(t, "msg.channel_internal_name", "main", msg1Get["channel_internal_name"])
msg2 := tt.RequestPost[gin.H](t, baseUrl, fmt.Sprintf("/send.php?user_key=%s&user_id=%d&title=%s", sendtok, uid, "HelloWorld_002"), nil) msg2 := tt.RequestPost[gin.H](t, baseUrl, fmt.Sprintf("/send.php?user_key=%s&user_id=%d&title=%s", sendtok, uid, "HelloWorld_002"), nil)
@ -1098,8 +1098,8 @@ func TestSendToNewChannel(t *testing.T) {
{ {
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid)) clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertEqual(t, "chan.len", 1, len(clist.Channels)) tt.AssertMappedSet(t, "channels", []string{"main"}, clist.Channels, "display_name")
tt.AssertEqual(t, "chan.name", "main", clist.Channels[0]["name"]) tt.AssertMappedSet(t, "channels", []string{"main"}, clist.Channels, "internal_name")
} }
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
@ -1112,8 +1112,8 @@ func TestSendToNewChannel(t *testing.T) {
{ {
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid)) clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertEqual(t, "chan.len", 1, len(clist.Channels)) tt.AssertMappedSet(t, "channels", []string{"main"}, clist.Channels, "display_name")
tt.AssertEqual(t, "chan.name", "main", clist.Channels[0]["name"]) tt.AssertMappedSet(t, "channels", []string{"main"}, clist.Channels, "internal_name")
} }
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
@ -1126,9 +1126,8 @@ func TestSendToNewChannel(t *testing.T) {
{ {
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid)) clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertEqual(t, "chan-count", 2, len(clist.Channels)) tt.AssertMappedSet(t, "channels", []string{"main", "test"}, clist.Channels, "display_name")
tt.AssertArrAny(t, "chan.has('main')", clist.Channels, func(msg gin.H) bool { return msg["name"].(string) == "main" }) tt.AssertMappedSet(t, "channels", []string{"main", "test"}, clist.Channels, "internal_name")
tt.AssertArrAny(t, "chan.has('test')", clist.Channels, func(msg gin.H) bool { return msg["name"].(string) == "test" })
} }
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
@ -1140,9 +1139,8 @@ func TestSendToNewChannel(t *testing.T) {
{ {
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid)) clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertEqual(t, "chan-count", 2, len(clist.Channels)) tt.AssertMappedSet(t, "channels", []string{"main", "test"}, clist.Channels, "display_name")
tt.AssertArrAny(t, "chan.has('main')", clist.Channels, func(msg gin.H) bool { return msg["name"].(string) == "main" }) tt.AssertMappedSet(t, "channels", []string{"main", "test"}, clist.Channels, "internal_name")
tt.AssertArrAny(t, "chan.has('test')", clist.Channels, func(msg gin.H) bool { return msg["name"].(string) == "test" })
} }
} }
@ -1166,8 +1164,9 @@ func TestSendToManualChannel(t *testing.T) {
} }
{ {
chan0 := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid)) clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertEqual(t, "chan-count", 0, len(chan0.Channels)) tt.AssertMappedSet(t, "channels", []string{}, clist.Channels, "display_name")
tt.AssertMappedSet(t, "channels", []string{}, clist.Channels, "internal_name")
} }
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
@ -1178,8 +1177,8 @@ func TestSendToManualChannel(t *testing.T) {
{ {
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid)) clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertEqual(t, "chan.len", 1, len(clist.Channels)) tt.AssertMappedSet(t, "channels", []string{"main"}, clist.Channels, "display_name")
tt.AssertEqual(t, "chan.name", "main", clist.Channels[0]["name"]) tt.AssertMappedSet(t, "channels", []string{"main"}, clist.Channels, "internal_name")
} }
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
@ -1193,7 +1192,8 @@ func TestSendToManualChannel(t *testing.T) {
{ {
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid)) clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertEqual(t, "chan.len", 1, len(clist.Channels)) tt.AssertEqual(t, "chan.len", 1, len(clist.Channels))
tt.AssertEqual(t, "chan.name", "main", clist.Channels[0]["name"]) tt.AssertEqual(t, "chan.internal_name", "main", clist.Channels[0]["internal_name"])
tt.AssertEqual(t, "chan.display_name", "main", clist.Channels[0]["display_name"])
} }
tt.RequestAuthPost[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid), gin.H{ tt.RequestAuthPost[gin.H](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid), gin.H{
@ -1202,9 +1202,8 @@ func TestSendToManualChannel(t *testing.T) {
{ {
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid)) clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertEqual(t, "chan-count", 2, len(clist.Channels)) tt.AssertMappedSet(t, "channels", []string{"main", "test"}, clist.Channels, "display_name")
tt.AssertArrAny(t, "chan.has('main')", clist.Channels, func(msg gin.H) bool { return msg["name"].(string) == "main" }) tt.AssertMappedSet(t, "channels", []string{"main", "test"}, clist.Channels, "internal_name")
tt.AssertArrAny(t, "chan.has('test')", clist.Channels, func(msg gin.H) bool { return msg["name"].(string) == "test" })
} }
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
@ -1217,9 +1216,8 @@ func TestSendToManualChannel(t *testing.T) {
{ {
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid)) clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertEqual(t, "chan-count", 2, len(clist.Channels)) tt.AssertMappedSet(t, "channels", []string{"main", "test"}, clist.Channels, "display_name")
tt.AssertArrAny(t, "chan.has('main')", clist.Channels, func(msg gin.H) bool { return msg["name"].(string) == "main" }) tt.AssertMappedSet(t, "channels", []string{"main", "test"}, clist.Channels, "internal_name")
tt.AssertArrAny(t, "chan.has('test')", clist.Channels, func(msg gin.H) bool { return msg["name"].(string) == "test" })
} }
tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{ tt.RequestPost[gin.H](t, baseUrl, "/", gin.H{
@ -1231,9 +1229,8 @@ func TestSendToManualChannel(t *testing.T) {
{ {
clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid)) clist := tt.RequestAuthGet[chanlist](t, admintok, baseUrl, fmt.Sprintf("/api/users/%d/channels", uid))
tt.AssertEqual(t, "chan-count", 2, len(clist.Channels)) tt.AssertMappedSet(t, "channels", []string{"main", "test"}, clist.Channels, "display_name")
tt.AssertArrAny(t, "chan.has('main')", clist.Channels, func(msg gin.H) bool { return msg["name"].(string) == "main" }) tt.AssertMappedSet(t, "channels", []string{"main", "test"}, clist.Channels, "internal_name")
tt.AssertArrAny(t, "chan.has('test')", clist.Channels, func(msg gin.H) bool { return msg["name"].(string) == "test" })
} }
} }

View File

@ -203,7 +203,14 @@ func AssertMultiNonEmpty(t *testing.T, key string, args ...any) {
func AssertMappedSet[T langext.OrderedConstraint](t *testing.T, key string, expected []T, values []gin.H, objkey string) { func AssertMappedSet[T langext.OrderedConstraint](t *testing.T, key string, expected []T, values []gin.H, objkey string) {
actual := langext.ArrMap(values, func(v gin.H) T { return v[objkey].(T) }) actual := make([]T, 0)
for idx, vv := range values {
if tv, ok := vv[objkey].(T); ok {
actual = append(actual, tv)
} else {
TestFailFmt(t, "[%s]->[%d] is wrong type (expected: %T, actual: %T)", key, idx, *new(T), vv)
}
}
langext.Sort(actual) langext.Sort(actual)
langext.Sort(expected) langext.Sort(expected)

View File

@ -86,8 +86,10 @@ var userExamples = []userex{
{9, true, "UniqueUnicorn", "Galaxy Quest", "2023.1", "ANDROID", "FCM_TOK_EX_010", ""}, {9, true, "UniqueUnicorn", "Galaxy Quest", "2023.1", "ANDROID", "FCM_TOK_EX_010", ""},
{10, false, "", "", "", "", "", ""}, {10, false, "", "", "", "", "", ""},
{11, false, "", "", "", "", "", "ANDROID|v2|PURCHASED:PRO_TOK_002"}, {11, false, "", "", "", "", "", "ANDROID|v2|PURCHASED:PRO_TOK_002"},
{12, true, "ChanTester1", "StarfireXX", "1.x", "IOS", "FCM_TOK_EX_012", ""}, {12, true, "NoMessageUser", "Ocean Explorer", "737edc01", "IOS", "FCM_TOK_EX_014", ""},
{13, true, "ChanTester2", "StarfireXX", "1.x", "IOS", "FCM_TOK_EX_013", ""}, {13, false, "EmptyUser", "", "", "", "", ""},
{14, true, "ChanTester1", "StarfireXX", "1.x", "IOS", "FCM_TOK_EX_012", ""},
{15, true, "ChanTester2", "StarfireXX", "1.x", "IOS", "FCM_TOK_EX_013", ""},
} }
var clientExamples = []clientex{ var clientExamples = []clientex{
@ -267,14 +269,14 @@ var messageExamples = []msgex{
{11, "Promotions", "", PX, AKEY, "Join Our VIP Club and Enjoy Exclusive Benefits", "Sign up for our VIP club and enjoy exclusive benefits like early access to sales, special offers, and personalized service. Don't miss out on this exclusive opportunity.", timeext.FromHours(2.32)}, {11, "Promotions", "", PX, AKEY, "Join Our VIP Club and Enjoy Exclusive Benefits", "Sign up for our VIP club and enjoy exclusive benefits like early access to sales, special offers, and personalized service. Don't miss out on this exclusive opportunity.", timeext.FromHours(2.32)},
{11, "Promotions", "", P2, SKEY, "Summer Clearance: Save Up to 75% on Your Favorite Products", "It's time for our annual summer clearance sale! Save up to 75% on your favorite products, from clothing and accessories to home decor and more.", timeext.FromHours(1.87)}, {11, "Promotions", "", P2, SKEY, "Summer Clearance: Save Up to 75% on Your Favorite Products", "It's time for our annual summer clearance sale! Save up to 75% on your favorite products, from clothing and accessories to home decor and more.", timeext.FromHours(1.87)},
{12, "", "", P0, SKEY, "New Product Launch", "We are excited to announce the launch of our new product, the XYZ widget", 0}, {14, "", "", P0, SKEY, "New Product Launch", "We are excited to announce the launch of our new product, the XYZ widget", 0},
{12, "chan_self_subscribed", "", P0, SKEY, "Important Update", "We have released a critical update", 0}, {14, "chan_self_subscribed", "", P0, SKEY, "Important Update", "We have released a critical update", 0},
{12, "chan_self_unsub", "", P0, SKEY, "Reminder: Upcoming Maintenance", "", 0}, {14, "chan_self_unsub", "", P0, SKEY, "Reminder: Upcoming Maintenance", "", 0},
{13, "", "", P0, SKEY, "New Feature Available", "ability to schedule appointments", 0}, {15, "", "", P0, SKEY, "New Feature Available", "ability to schedule appointments", 0},
{13, "chan_other_nosub", "", P0, SKEY, "Account Suspended", "Please contact us", 0}, {15, "chan_other_nosub", "", P0, SKEY, "Account Suspended", "Please contact us", 0},
{13, "chan_other_request", "", P0, SKEY, "Invitation to Beta Test", "", 0}, {15, "chan_other_request", "", P0, SKEY, "Invitation to Beta Test", "", 0},
{13, "chan_other_accepted", "", P0, SKEY, "New Blog Post", "Congratulations on your promotion! We are proud", 0}, {15, "chan_other_accepted", "", P0, SKEY, "New Blog Post", "Congratulations on your promotion! We are proud", 0},
} }
type DefData struct { type DefData struct {
@ -373,6 +375,12 @@ func InitDefaultData(t *testing.T, ws *logic.Application) DefData {
RequestPost[gin.H](t, baseUrl, "/", body) RequestPost[gin.H](t, baseUrl, "/", body)
} }
// create manual channels
{
RequestAuthPost[Void](t, users[9].AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels", users[9].UID), gin.H{"name": "manual@chan"})
}
// Sub/Unsub for Users 12+13 // Sub/Unsub for Users 12+13
{ {

View File

@ -163,10 +163,10 @@ func RequestAny[TResult any](t *testing.T, akey string, method string, baseURL s
return data return data
} }
func RequestAuthAnyShouldFail(t *testing.T, akey string, method string, baseURL string, urlSuffix string, body any, statusCode int, errcode apierr.APIError) { func RequestAuthAnyShouldFail(t *testing.T, akey string, method string, baseURL string, urlSuffix string, body any, expectedStatusCode int, errcode apierr.APIError) {
client := http.Client{} client := http.Client{}
TPrintf("[-> REQUEST] (%s) %s%s [%s] (should-fail with %d/%d)\n", method, baseURL, urlSuffix, langext.Conditional(akey == "", "NO AUTH", "AUTH"), statusCode, errcode) TPrintf("[-> REQUEST] (%s) %s%s [%s] (should-fail with %d/%d)\n", method, baseURL, urlSuffix, langext.Conditional(akey == "", "NO AUTH", "AUTH"), expectedStatusCode, errcode)
bytesbody := make([]byte, 0) bytesbody := make([]byte, 0)
contentType := "" contentType := ""
@ -224,17 +224,17 @@ func RequestAuthAnyShouldFail(t *testing.T, akey string, method string, baseURL
TPrintln("") TPrintln("")
TPrintf("---------------- RESPONSE (%d) ----------------\n", resp.StatusCode) TPrintf("---------------- RESPONSE (%d) ----------------\n", resp.StatusCode)
TPrintln(langext.TryPrettyPrintJson(string(respBodyBin))) TPrintln(langext.TryPrettyPrintJson(string(respBodyBin)))
if (statusCode != 0 && resp.StatusCode != statusCode) || (statusCode == 0 && resp.StatusCode == 200) { if (expectedStatusCode != 0 && resp.StatusCode != expectedStatusCode) || (expectedStatusCode == 0 && resp.StatusCode == 200) {
TryPrintTraceObj("---------------- -------- ----------------", respBodyBin, "") TryPrintTraceObj("---------------- -------- ----------------", respBodyBin, "")
} }
TPrintln("---------------- -------- ----------------") TPrintln("---------------- -------- ----------------")
TPrintln("") TPrintln("")
if statusCode != 0 && resp.StatusCode != statusCode { if expectedStatusCode != 0 && resp.StatusCode != expectedStatusCode {
TestFailFmt(t, "Statuscode != %d (expected failure)", statusCode) TestFailFmt(t, "Statuscode != %d (expected failure, but got %d)", expectedStatusCode, resp.StatusCode)
} }
if statusCode == 0 && resp.StatusCode == 200 { if expectedStatusCode == 0 && resp.StatusCode == 200 {
TestFailFmt(t, "Statuscode == %d (expected failure)", resp.StatusCode) TestFailFmt(t, "Statuscode == %d (expected any failure, but got %d)", resp.StatusCode, resp.StatusCode)
} }
var data gin.H var data gin.H