From a0cc913b85dea5023b705697afa5cd8749a6e5de Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Tue, 14 Jun 2016 09:38:19 -0400 Subject: PLT-3143 Added serverside code for custom Emoji (#3311) * Added model objects for emoji * Added database tables for emoji * Added settings for custom emoji * Added serverside APIs and unit tests for custom emoji * Added additional validation to catch duplicate emoji names earlier on * Added additional validation to prevent users from adding emoji as another user --- store/sql_emoji_store.go | 169 ++++++++++++++++++++++++++++++++++++++++++ store/sql_emoji_store_test.go | 162 ++++++++++++++++++++++++++++++++++++++++ store/sql_store.go | 8 ++ store/store.go | 9 +++ 4 files changed, 348 insertions(+) create mode 100644 store/sql_emoji_store.go create mode 100644 store/sql_emoji_store_test.go (limited to 'store') diff --git a/store/sql_emoji_store.go b/store/sql_emoji_store.go new file mode 100644 index 000000000..99434ee64 --- /dev/null +++ b/store/sql_emoji_store.go @@ -0,0 +1,169 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + "github.com/mattermost/platform/model" +) + +type SqlEmojiStore struct { + *SqlStore +} + +func NewSqlEmojiStore(sqlStore *SqlStore) EmojiStore { + s := &SqlEmojiStore{sqlStore} + + for _, db := range sqlStore.GetAllConns() { + table := db.AddTableWithName(model.Emoji{}, "Emoji").SetKeys(false, "Id") + table.ColMap("Id").SetMaxSize(26) + table.ColMap("CreatorId").SetMaxSize(26) + table.ColMap("Name").SetMaxSize(64) + + table.SetUniqueTogether("Name", "DeleteAt") + } + + return s +} + +func (es SqlEmojiStore) UpgradeSchemaIfNeeded() { +} + +func (es SqlEmojiStore) CreateIndexesIfNotExists() { +} + +func (es SqlEmojiStore) Save(emoji *model.Emoji) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + emoji.PreSave() + if result.Err = emoji.IsValid(); result.Err != nil { + storeChannel <- result + close(storeChannel) + return + } + + if err := es.GetMaster().Insert(emoji); err != nil { + result.Err = model.NewLocAppError("SqlEmojiStore.Save", "store.sql_emoji.save.app_error", nil, "id="+emoji.Id+", "+err.Error()) + } else { + result.Data = emoji + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (es SqlEmojiStore) Get(id string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var emoji *model.Emoji + + if err := es.GetReplica().SelectOne(&emoji, + `SELECT + * + FROM + Emoji + WHERE + Id = :Id + AND DeleteAt = 0`, map[string]interface{}{"Id": id}); err != nil { + result.Err = model.NewLocAppError("SqlEmojiStore.Get", "store.sql_emoji.get.app_error", nil, "id="+id+", "+err.Error()) + } else { + result.Data = emoji + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (es SqlEmojiStore) GetByName(name string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var emoji *model.Emoji + + if err := es.GetReplica().SelectOne(&emoji, + `SELECT + * + FROM + Emoji + WHERE + Name = :Name + AND DeleteAt = 0`, map[string]interface{}{"Name": name}); err != nil { + result.Err = model.NewLocAppError("SqlEmojiStore.GetByName", "store.sql_emoji.get_by_name.app_error", nil, "name="+name+", "+err.Error()) + } else { + result.Data = emoji + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (es SqlEmojiStore) GetAll() StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var emoji []*model.Emoji + + if _, err := es.GetReplica().Select(&emoji, + `SELECT + * + FROM + Emoji + WHERE + DeleteAt = 0`); err != nil { + result.Err = model.NewLocAppError("SqlEmojiStore.Get", "store.sql_emoji.get_all.app_error", nil, err.Error()) + } else { + result.Data = emoji + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (es SqlEmojiStore) Delete(id string, time int64) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + if sqlResult, err := es.GetMaster().Exec( + `Update + Emoji + SET + DeleteAt = :DeleteAt, + UpdateAt = :UpdateAt + WHERE + Id = :Id + AND DeleteAt = 0`, map[string]interface{}{"DeleteAt": time, "UpdateAt": time, "Id": id}); err != nil { + result.Err = model.NewLocAppError("SqlEmojiStore.Delete", "store.sql_emoji.delete.app_error", nil, "id="+id+", err="+err.Error()) + } else if rows, _ := sqlResult.RowsAffected(); rows == 0 { + result.Err = model.NewLocAppError("SqlEmojiStore.Delete", "store.sql_emoji.delete.no_results", nil, "id="+id+", err="+err.Error()) + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} diff --git a/store/sql_emoji_store_test.go b/store/sql_emoji_store_test.go new file mode 100644 index 000000000..f9c42c906 --- /dev/null +++ b/store/sql_emoji_store_test.go @@ -0,0 +1,162 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + "github.com/mattermost/platform/model" + "testing" + "time" +) + +func TestEmojiSaveDelete(t *testing.T) { + Setup() + + emoji1 := &model.Emoji{ + CreatorId: model.NewId(), + Name: model.NewId(), + } + + if result := <-store.Emoji().Save(emoji1); result.Err != nil { + t.Fatal(result.Err) + } + + if len(emoji1.Id) != 26 { + t.Fatal("should've set id for emoji") + } + + emoji2 := model.Emoji{ + CreatorId: model.NewId(), + Name: emoji1.Name, + } + if result := <-store.Emoji().Save(&emoji2); result.Err == nil { + t.Fatal("shouldn't be able to save emoji with duplicate name") + } + + if result := <-store.Emoji().Delete(emoji1.Id, time.Now().Unix()); result.Err != nil { + t.Fatal(result.Err) + } + + if result := <-store.Emoji().Save(&emoji2); result.Err != nil { + t.Fatal("should be able to save emoji with duplicate name now that original has been deleted", result.Err) + } + + if result := <-store.Emoji().Delete(emoji2.Id, time.Now().Unix()+1); result.Err != nil { + t.Fatal(result.Err) + } +} + +func TestEmojiGet(t *testing.T) { + Setup() + + emojis := []model.Emoji{ + { + CreatorId: model.NewId(), + Name: model.NewId(), + }, + { + CreatorId: model.NewId(), + Name: model.NewId(), + }, + { + CreatorId: model.NewId(), + Name: model.NewId(), + }, + } + + for i, emoji := range emojis { + emojis[i] = *Must(store.Emoji().Save(&emoji)).(*model.Emoji) + } + defer func() { + for _, emoji := range emojis { + Must(store.Emoji().Delete(emoji.Id, time.Now().Unix())) + } + }() + + for _, emoji := range emojis { + if result := <-store.Emoji().Get(emoji.Id); result.Err != nil { + t.Fatalf("failed to get emoji with id %v: %v", emoji.Id, result.Err) + } + } +} + +func TestEmojiGetByName(t *testing.T) { + Setup() + + emojis := []model.Emoji{ + { + CreatorId: model.NewId(), + Name: model.NewId(), + }, + { + CreatorId: model.NewId(), + Name: model.NewId(), + }, + { + CreatorId: model.NewId(), + Name: model.NewId(), + }, + } + + for i, emoji := range emojis { + emojis[i] = *Must(store.Emoji().Save(&emoji)).(*model.Emoji) + } + defer func() { + for _, emoji := range emojis { + Must(store.Emoji().Delete(emoji.Id, time.Now().Unix())) + } + }() + + for _, emoji := range emojis { + if result := <-store.Emoji().GetByName(emoji.Name); result.Err != nil { + t.Fatalf("failed to get emoji with name %v: %v", emoji.Name, result.Err) + } + } +} + +func TestEmojiGetAll(t *testing.T) { + Setup() + + emojis := []model.Emoji{ + { + CreatorId: model.NewId(), + Name: model.NewId(), + }, + { + CreatorId: model.NewId(), + Name: model.NewId(), + }, + { + CreatorId: model.NewId(), + Name: model.NewId(), + }, + } + + for i, emoji := range emojis { + emojis[i] = *Must(store.Emoji().Save(&emoji)).(*model.Emoji) + } + defer func() { + for _, emoji := range emojis { + Must(store.Emoji().Delete(emoji.Id, time.Now().Unix())) + } + }() + + if result := <-store.Emoji().GetAll(); result.Err != nil { + t.Fatal(result.Err) + } else { + for _, emoji := range emojis { + found := false + + for _, savedEmoji := range result.Data.([]*model.Emoji) { + if emoji.Id == savedEmoji.Id { + found = true + break + } + } + + if !found { + t.Fatalf("failed to get emoji with id %v", emoji.Id) + } + } + } +} diff --git a/store/sql_store.go b/store/sql_store.go index e45d1ef94..c33da62cc 100644 --- a/store/sql_store.go +++ b/store/sql_store.go @@ -52,6 +52,7 @@ type SqlStore struct { preference PreferenceStore license LicenseStore recovery PasswordRecoveryStore + emoji EmojiStore SchemaVersion string } @@ -127,6 +128,7 @@ func NewSqlStore() Store { sqlStore.preference = NewSqlPreferenceStore(sqlStore) sqlStore.license = NewSqlLicenseStore(sqlStore) sqlStore.recovery = NewSqlPasswordRecoveryStore(sqlStore) + sqlStore.emoji = NewSqlEmojiStore(sqlStore) err := sqlStore.master.CreateTablesIfNotExists() if err != nil { @@ -149,6 +151,7 @@ func NewSqlStore() Store { sqlStore.preference.(*SqlPreferenceStore).UpgradeSchemaIfNeeded() sqlStore.license.(*SqlLicenseStore).UpgradeSchemaIfNeeded() sqlStore.recovery.(*SqlPasswordRecoveryStore).UpgradeSchemaIfNeeded() + sqlStore.emoji.(*SqlEmojiStore).UpgradeSchemaIfNeeded() sqlStore.team.(*SqlTeamStore).CreateIndexesIfNotExists() sqlStore.channel.(*SqlChannelStore).CreateIndexesIfNotExists() @@ -164,6 +167,7 @@ func NewSqlStore() Store { sqlStore.preference.(*SqlPreferenceStore).CreateIndexesIfNotExists() sqlStore.license.(*SqlLicenseStore).CreateIndexesIfNotExists() sqlStore.recovery.(*SqlPasswordRecoveryStore).CreateIndexesIfNotExists() + sqlStore.emoji.(*SqlEmojiStore).CreateIndexesIfNotExists() sqlStore.preference.(*SqlPreferenceStore).DeleteUnusedFeatures() @@ -688,6 +692,10 @@ func (ss SqlStore) PasswordRecovery() PasswordRecoveryStore { return ss.recovery } +func (ss SqlStore) Emoji() EmojiStore { + return ss.emoji +} + func (ss SqlStore) DropAllTables() { ss.master.TruncateTables() } diff --git a/store/store.go b/store/store.go index 7f4db396c..29a7e8d82 100644 --- a/store/store.go +++ b/store/store.go @@ -42,6 +42,7 @@ type Store interface { Preference() PreferenceStore License() LicenseStore PasswordRecovery() PasswordRecoveryStore + Emoji() EmojiStore MarkSystemRanUnitTests() Close() DropAllTables() @@ -254,3 +255,11 @@ type PasswordRecoveryStore interface { Get(userId string) StoreChannel GetByCode(code string) StoreChannel } + +type EmojiStore interface { + Save(emoji *model.Emoji) StoreChannel + Get(id string) StoreChannel + GetByName(name string) StoreChannel + GetAll() StoreChannel + Delete(id string, time int64) StoreChannel +} -- cgit v1.2.3-1-g7c22