mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
The server module named github.com/hanzoai/team/server/v8 — a path that resolves to the retired workspace's repository, not this one — while server/public still published github.com/mattermost/mattermost/server/public. Both are github.com/hanzoteam/server/... now, which is where this code actually lives. That second half is what made the binary unbootable. Upstream's public path stayed claimed by upstream, mattermost-plugin-ai imported it, and the build linked public/model twice; public/plugin's gob.Register then ran twice under one name and panicked at init, before main. Reverting the rename traded the brand for a working binary. Rebranding the plugin alongside it needs no such trade: the agents fork is github.com/hanzoteam/agents/v2 against our public module, so no module named mattermost/mattermost is in the graph at all. Measured: `go list -deps ./cmd/mattermost` names exactly one public/model, and it is ours.
518 lines
15 KiB
Go
518 lines
15 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package storetest
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/hanzoteam/server/server/public/model"
|
|
"github.com/hanzoteam/server/server/public/shared/request"
|
|
"github.com/hanzoteam/server/server/v8/channels/store"
|
|
)
|
|
|
|
func TestViewStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
|
|
t.Run("SaveView", func(t *testing.T) { testSaveView(t, ss) })
|
|
t.Run("GetView", func(t *testing.T) { testGetView(t, ss) })
|
|
t.Run("GetViewsForChannel", func(t *testing.T) { testGetViewsForChannel(t, ss) })
|
|
t.Run("UpdateView", func(t *testing.T) { testUpdateView(t, ss) })
|
|
t.Run("DeleteView", func(t *testing.T) { testDeleteView(t, ss) })
|
|
t.Run("UpdateSortOrder", func(t *testing.T) { testUpdateViewSortOrder(t, ss) })
|
|
}
|
|
|
|
func makeView(channelID, creatorID string) *model.View {
|
|
kanban := &model.KanbanProps{
|
|
GroupBy: model.KanbanGroupBy{
|
|
FieldID: model.NewId(),
|
|
Columns: []model.KanbanColumn{
|
|
{ID: model.NewId(), Name: "Todo", OptionIDs: []string{model.NewId()}},
|
|
{ID: model.NewId(), Name: "Done", OptionIDs: []string{model.NewId()}},
|
|
},
|
|
},
|
|
}
|
|
props, _ := kanban.ToProps()
|
|
return &model.View{
|
|
ChannelId: channelID,
|
|
CreatorId: creatorID,
|
|
Type: model.ViewTypeKanban,
|
|
Title: "Test Kanban",
|
|
Props: props,
|
|
}
|
|
}
|
|
|
|
func testSaveView(t *testing.T, ss store.Store) {
|
|
channelID := model.NewId()
|
|
creatorID := model.NewId()
|
|
|
|
t.Run("saves a valid view and returns it with generated fields", func(t *testing.T) {
|
|
v := makeView(channelID, creatorID)
|
|
saved, err := ss.View().Save(v)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, saved)
|
|
|
|
assert.True(t, model.IsValidId(saved.Id))
|
|
assert.NotZero(t, saved.CreateAt)
|
|
assert.NotZero(t, saved.UpdateAt)
|
|
assert.Equal(t, channelID, saved.ChannelId)
|
|
assert.Equal(t, creatorID, saved.CreatorId)
|
|
assert.Equal(t, model.ViewTypeKanban, saved.Type)
|
|
assert.Equal(t, "Test Kanban", saved.Title)
|
|
})
|
|
|
|
t.Run("persists and round-trips props correctly", func(t *testing.T) {
|
|
v := makeView(channelID, creatorID)
|
|
v.Title = "Props Kanban"
|
|
saved, err := ss.View().Save(v)
|
|
require.NoError(t, err)
|
|
|
|
fetched, err := ss.View().Get(saved.Id)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, fetched.Props)
|
|
|
|
kanban, kErr := model.KanbanPropsFromProps(fetched.Props)
|
|
require.NoError(t, kErr)
|
|
assert.NotEmpty(t, kanban.GroupBy.FieldID)
|
|
require.Len(t, kanban.GroupBy.Columns, 2)
|
|
})
|
|
|
|
t.Run("rejects view with nil props", func(t *testing.T) {
|
|
v := makeView(channelID, creatorID)
|
|
v.Title = "Nil Props Kanban"
|
|
v.Props = nil
|
|
_, err := ss.View().Save(v)
|
|
require.Error(t, err, "kanban views require valid props")
|
|
})
|
|
}
|
|
|
|
func testGetView(t *testing.T, ss store.Store) {
|
|
channelID := model.NewId()
|
|
creatorID := model.NewId()
|
|
|
|
saved, err := ss.View().Save(makeView(channelID, creatorID))
|
|
require.NoError(t, err)
|
|
|
|
t.Run("returns the view by ID", func(t *testing.T) {
|
|
v, err := ss.View().Get(saved.Id)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, saved.Id, v.Id)
|
|
assert.Equal(t, saved.ChannelId, v.ChannelId)
|
|
assert.Equal(t, saved.Title, v.Title)
|
|
})
|
|
|
|
t.Run("returns not found for unknown ID", func(t *testing.T) {
|
|
_, err := ss.View().Get(model.NewId())
|
|
require.Error(t, err)
|
|
var nfErr *store.ErrNotFound
|
|
assert.ErrorAs(t, err, &nfErr)
|
|
})
|
|
|
|
t.Run("returns not found for deleted view", func(t *testing.T) {
|
|
err := ss.View().Delete(saved.Id, model.GetMillis())
|
|
require.NoError(t, err)
|
|
|
|
_, err = ss.View().Get(saved.Id)
|
|
require.Error(t, err)
|
|
var nfErr *store.ErrNotFound
|
|
assert.ErrorAs(t, err, &nfErr)
|
|
})
|
|
}
|
|
|
|
func testGetViewsForChannel(t *testing.T, ss store.Store) {
|
|
channelID := model.NewId()
|
|
otherChannelID := model.NewId()
|
|
creatorID := model.NewId()
|
|
|
|
v1 := makeView(channelID, creatorID)
|
|
v1.Title = "Kanban A"
|
|
v1.SortOrder = 1
|
|
saved1, err := ss.View().Save(v1)
|
|
require.NoError(t, err)
|
|
|
|
v2 := makeView(channelID, creatorID)
|
|
v2.Title = "Kanban B"
|
|
v2.SortOrder = 0
|
|
saved2, err := ss.View().Save(v2)
|
|
require.NoError(t, err)
|
|
|
|
v3 := makeView(otherChannelID, creatorID)
|
|
v3.Title = "Other Channel Kanban"
|
|
_, err = ss.View().Save(v3)
|
|
require.NoError(t, err)
|
|
|
|
t.Run("returns only views for the given channel", func(t *testing.T) {
|
|
views, err := ss.View().GetForChannel(channelID, model.ViewQueryOpts{})
|
|
require.NoError(t, err)
|
|
assert.Len(t, views, 2)
|
|
ids := []string{views[0].Id, views[1].Id}
|
|
assert.Contains(t, ids, saved1.Id)
|
|
assert.Contains(t, ids, saved2.Id)
|
|
})
|
|
|
|
t.Run("returns views ordered by sort_order ascending", func(t *testing.T) {
|
|
views, err := ss.View().GetForChannel(channelID, model.ViewQueryOpts{})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, saved2.Id, views[0].Id) // SortOrder 0
|
|
assert.Equal(t, saved1.Id, views[1].Id) // SortOrder 1
|
|
})
|
|
|
|
t.Run("excludes deleted views", func(t *testing.T) {
|
|
err := ss.View().Delete(saved1.Id, model.GetMillis())
|
|
require.NoError(t, err)
|
|
|
|
views, err := ss.View().GetForChannel(channelID, model.ViewQueryOpts{})
|
|
require.NoError(t, err)
|
|
assert.Len(t, views, 1)
|
|
assert.Equal(t, saved2.Id, views[0].Id)
|
|
})
|
|
|
|
t.Run("returns empty slice for channel with no views", func(t *testing.T) {
|
|
views, err := ss.View().GetForChannel(model.NewId(), model.ViewQueryOpts{})
|
|
require.NoError(t, err)
|
|
assert.Empty(t, views)
|
|
})
|
|
|
|
t.Run("paginates with page/offset", func(t *testing.T) {
|
|
ch := model.NewId()
|
|
creator := model.NewId()
|
|
var saved []*model.View
|
|
for i := range 3 {
|
|
v := makeView(ch, creator)
|
|
v.Title = "Paginate Kanban"
|
|
v.SortOrder = i
|
|
s, err := ss.View().Save(v)
|
|
require.NoError(t, err)
|
|
saved = append(saved, s)
|
|
}
|
|
|
|
page1, err := ss.View().GetForChannel(ch, model.ViewQueryOpts{PerPage: 2, Page: 0})
|
|
require.NoError(t, err)
|
|
assert.Len(t, page1, 2)
|
|
assert.Equal(t, saved[0].Id, page1[0].Id)
|
|
assert.Equal(t, saved[1].Id, page1[1].Id)
|
|
|
|
page2, err := ss.View().GetForChannel(ch, model.ViewQueryOpts{PerPage: 2, Page: 1})
|
|
require.NoError(t, err)
|
|
require.Len(t, page2, 1)
|
|
assert.Equal(t, saved[2].Id, page2[0].Id)
|
|
})
|
|
|
|
t.Run("respects SortOrder ordering across pages", func(t *testing.T) {
|
|
ch := model.NewId()
|
|
creator := model.NewId()
|
|
orders := []int{0, 5, 10}
|
|
var created []*model.View
|
|
for _, so := range orders {
|
|
v := makeView(ch, creator)
|
|
v.Title = "SortOrder Kanban"
|
|
v.SortOrder = so
|
|
s, err := ss.View().Save(v)
|
|
require.NoError(t, err)
|
|
created = append(created, s)
|
|
}
|
|
|
|
page1, err := ss.View().GetForChannel(ch, model.ViewQueryOpts{PerPage: 2, Page: 0})
|
|
require.NoError(t, err)
|
|
require.Len(t, page1, 2)
|
|
assert.Equal(t, created[0].Id, page1[0].Id)
|
|
assert.Equal(t, created[1].Id, page1[1].Id)
|
|
|
|
page2, err := ss.View().GetForChannel(ch, model.ViewQueryOpts{PerPage: 2, Page: 1})
|
|
require.NoError(t, err)
|
|
assert.Len(t, page2, 1)
|
|
assert.Equal(t, created[2].Id, page2[0].Id)
|
|
})
|
|
|
|
t.Run("defaults PerPage to 20 when 0", func(t *testing.T) {
|
|
ch := model.NewId()
|
|
creator := model.NewId()
|
|
for i := range model.ViewQueryDefaultPerPage + 1 {
|
|
v := makeView(ch, creator)
|
|
v.Title = "Default PerPage Kanban"
|
|
v.SortOrder = i
|
|
_, err := ss.View().Save(v)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
views, err := ss.View().GetForChannel(ch, model.ViewQueryOpts{PerPage: 0})
|
|
require.NoError(t, err)
|
|
assert.Len(t, views, model.ViewQueryDefaultPerPage)
|
|
})
|
|
|
|
t.Run("rejects empty channelID", func(t *testing.T) {
|
|
_, err := ss.View().GetForChannel("", model.ViewQueryOpts{})
|
|
require.Error(t, err)
|
|
var invErr *store.ErrInvalidInput
|
|
assert.ErrorAs(t, err, &invErr)
|
|
})
|
|
|
|
t.Run("clamps PerPage above max to max", func(t *testing.T) {
|
|
ch := model.NewId()
|
|
creator := model.NewId()
|
|
for i := range model.ViewQueryMaxPerPage + 1 {
|
|
v := makeView(ch, creator)
|
|
v.Title = "Clamp Kanban"
|
|
v.SortOrder = i
|
|
_, err := ss.View().Save(v)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
views, err := ss.View().GetForChannel(ch, model.ViewQueryOpts{PerPage: 9999})
|
|
require.NoError(t, err)
|
|
assert.Len(t, views, model.ViewQueryMaxPerPage)
|
|
})
|
|
|
|
t.Run("negative page defaults to 0", func(t *testing.T) {
|
|
ch := model.NewId()
|
|
creator := model.NewId()
|
|
v := makeView(ch, creator)
|
|
saved, err := ss.View().Save(v)
|
|
require.NoError(t, err)
|
|
|
|
views, err := ss.View().GetForChannel(ch, model.ViewQueryOpts{Page: -1})
|
|
require.NoError(t, err)
|
|
require.Len(t, views, 1)
|
|
assert.Equal(t, saved.Id, views[0].Id)
|
|
})
|
|
|
|
t.Run("out-of-bounds page returns empty slice", func(t *testing.T) {
|
|
ch := model.NewId()
|
|
creator := model.NewId()
|
|
for range 3 {
|
|
_, err := ss.View().Save(makeView(ch, creator))
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
views, err := ss.View().GetForChannel(ch, model.ViewQueryOpts{PerPage: 2, Page: 999})
|
|
require.NoError(t, err)
|
|
assert.Empty(t, views)
|
|
})
|
|
}
|
|
|
|
func testUpdateView(t *testing.T, ss store.Store) {
|
|
channelID := model.NewId()
|
|
creatorID := model.NewId()
|
|
|
|
saved, err := ss.View().Save(makeView(channelID, creatorID))
|
|
require.NoError(t, err)
|
|
|
|
t.Run("updates mutable fields", func(t *testing.T) {
|
|
updated := saved.Clone()
|
|
updated.Title = "Updated Title"
|
|
updated.Description = "A description"
|
|
updated.SortOrder = 5
|
|
|
|
result, err := ss.View().Update(updated)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Updated Title", result.Title)
|
|
assert.Equal(t, "A description", result.Description)
|
|
assert.Equal(t, 5, result.SortOrder)
|
|
assert.GreaterOrEqual(t, result.UpdateAt, saved.UpdateAt)
|
|
})
|
|
|
|
t.Run("persists updated props", func(t *testing.T) {
|
|
fetched, err := ss.View().Get(saved.Id)
|
|
require.NoError(t, err)
|
|
|
|
newKanban := &model.KanbanProps{
|
|
GroupBy: model.KanbanGroupBy{
|
|
FieldID: model.NewId(),
|
|
Columns: []model.KanbanColumn{
|
|
{ID: model.NewId(), Name: "Updated Col", OptionIDs: []string{model.NewId()}},
|
|
},
|
|
},
|
|
}
|
|
fetched.Props, _ = newKanban.ToProps()
|
|
_, err = ss.View().Update(fetched)
|
|
require.NoError(t, err)
|
|
|
|
refetched, err := ss.View().Get(saved.Id)
|
|
require.NoError(t, err)
|
|
kanban, kErr := model.KanbanPropsFromProps(refetched.Props)
|
|
require.NoError(t, kErr)
|
|
require.Len(t, kanban.GroupBy.Columns, 1)
|
|
assert.Equal(t, "Updated Col", kanban.GroupBy.Columns[0].Name)
|
|
})
|
|
|
|
t.Run("returns not found for unknown ID", func(t *testing.T) {
|
|
ghost := makeView(channelID, creatorID)
|
|
ghost.Id = model.NewId()
|
|
ghost.PreSave()
|
|
|
|
_, err := ss.View().Update(ghost)
|
|
require.Error(t, err)
|
|
var nfErr *store.ErrNotFound
|
|
assert.ErrorAs(t, err, &nfErr)
|
|
})
|
|
}
|
|
|
|
func testDeleteView(t *testing.T, ss store.Store) {
|
|
channelID := model.NewId()
|
|
creatorID := model.NewId()
|
|
|
|
t.Run("soft deletes the view", func(t *testing.T) {
|
|
saved, err := ss.View().Save(makeView(channelID, creatorID))
|
|
require.NoError(t, err)
|
|
|
|
deleteAt := model.GetMillis()
|
|
err = ss.View().Delete(saved.Id, deleteAt)
|
|
require.NoError(t, err)
|
|
|
|
_, err = ss.View().Get(saved.Id)
|
|
require.Error(t, err)
|
|
var nfErr *store.ErrNotFound
|
|
assert.ErrorAs(t, err, &nfErr)
|
|
})
|
|
|
|
t.Run("returns not found for unknown ID", func(t *testing.T) {
|
|
err := ss.View().Delete(model.NewId(), model.GetMillis())
|
|
require.Error(t, err)
|
|
var nfErr *store.ErrNotFound
|
|
assert.ErrorAs(t, err, &nfErr)
|
|
})
|
|
|
|
t.Run("returns not found when deleting an already deleted view", func(t *testing.T) {
|
|
saved, err := ss.View().Save(makeView(channelID, creatorID))
|
|
require.NoError(t, err)
|
|
|
|
deleteAt := model.GetMillis()
|
|
err = ss.View().Delete(saved.Id, deleteAt)
|
|
require.NoError(t, err)
|
|
|
|
err = ss.View().Delete(saved.Id, model.GetMillis())
|
|
require.Error(t, err)
|
|
var nfErr *store.ErrNotFound
|
|
assert.ErrorAs(t, err, &nfErr)
|
|
})
|
|
}
|
|
|
|
func testUpdateViewSortOrder(t *testing.T, ss store.Store) {
|
|
channelID := model.NewId()
|
|
creatorID := model.NewId()
|
|
|
|
// Create 3 views with different sort orders
|
|
var views []*model.View
|
|
for i := range 3 {
|
|
v := makeView(channelID, creatorID)
|
|
v.Title = fmt.Sprintf("View %d", i)
|
|
v.SortOrder = i
|
|
saved, err := ss.View().Save(v)
|
|
require.NoError(t, err)
|
|
views = append(views, saved)
|
|
}
|
|
|
|
// Extract IDs for clarity across stateful subtests
|
|
idA, idB, idC := views[0].Id, views[1].Id, views[2].Id
|
|
|
|
t.Run("moves last to first", func(t *testing.T) {
|
|
// BEFORE: A, B, C — AFTER: C, A, B
|
|
result, err := ss.View().UpdateSortOrder(idC, channelID, 0)
|
|
require.NoError(t, err)
|
|
require.Len(t, result, 3)
|
|
assert.Equal(t, idC, result[0].Id)
|
|
assert.Equal(t, idA, result[1].Id)
|
|
assert.Equal(t, idB, result[2].Id)
|
|
assert.Equal(t, 0, result[0].SortOrder)
|
|
assert.Equal(t, 1, result[1].SortOrder)
|
|
assert.Equal(t, 2, result[2].SortOrder)
|
|
})
|
|
|
|
t.Run("moves first to last", func(t *testing.T) {
|
|
// BEFORE: C, A, B — AFTER: A, B, C
|
|
result, err := ss.View().UpdateSortOrder(idC, channelID, 2)
|
|
require.NoError(t, err)
|
|
require.Len(t, result, 3)
|
|
assert.Equal(t, idA, result[0].Id)
|
|
assert.Equal(t, idB, result[1].Id)
|
|
assert.Equal(t, idC, result[2].Id)
|
|
})
|
|
|
|
t.Run("moves to middle", func(t *testing.T) {
|
|
// BEFORE: A, B, C — AFTER: A, C, B
|
|
result, err := ss.View().UpdateSortOrder(idC, channelID, 1)
|
|
require.NoError(t, err)
|
|
require.Len(t, result, 3)
|
|
assert.Equal(t, idA, result[0].Id)
|
|
assert.Equal(t, idC, result[1].Id)
|
|
assert.Equal(t, idB, result[2].Id)
|
|
})
|
|
|
|
t.Run("negative index returns error", func(t *testing.T) {
|
|
_, err := ss.View().UpdateSortOrder(idA, channelID, -1)
|
|
require.Error(t, err)
|
|
var iiErr *store.ErrInvalidInput
|
|
assert.ErrorAs(t, err, &iiErr)
|
|
})
|
|
|
|
t.Run("out of bounds index returns error", func(t *testing.T) {
|
|
_, err := ss.View().UpdateSortOrder(idA, channelID, 99)
|
|
require.Error(t, err)
|
|
var iiErr *store.ErrInvalidInput
|
|
assert.ErrorAs(t, err, &iiErr)
|
|
})
|
|
|
|
t.Run("non-existent view returns not found", func(t *testing.T) {
|
|
_, err := ss.View().UpdateSortOrder(model.NewId(), channelID, 0)
|
|
require.Error(t, err)
|
|
var nfErr *store.ErrNotFound
|
|
assert.ErrorAs(t, err, &nfErr)
|
|
})
|
|
|
|
t.Run("empty channel returns error", func(t *testing.T) {
|
|
_, err := ss.View().UpdateSortOrder(idA, model.NewId(), 0)
|
|
require.Error(t, err)
|
|
var iiErr *store.ErrInvalidInput
|
|
assert.ErrorAs(t, err, &iiErr)
|
|
})
|
|
|
|
t.Run("does not include deleted views", func(t *testing.T) {
|
|
ch := model.NewId()
|
|
creator := model.NewId()
|
|
var created []*model.View
|
|
for i := range 3 {
|
|
v := makeView(ch, creator)
|
|
v.Title = fmt.Sprintf("Del View %d", i)
|
|
v.SortOrder = i
|
|
s, err := ss.View().Save(v)
|
|
require.NoError(t, err)
|
|
created = append(created, s)
|
|
}
|
|
|
|
// Delete the middle one
|
|
err := ss.View().Delete(created[1].Id, model.GetMillis())
|
|
require.NoError(t, err)
|
|
|
|
result, err := ss.View().UpdateSortOrder(created[2].Id, ch, 0)
|
|
require.NoError(t, err)
|
|
require.Len(t, result, 2)
|
|
assert.Equal(t, created[2].Id, result[0].Id)
|
|
assert.Equal(t, created[0].Id, result[1].Id)
|
|
})
|
|
|
|
t.Run("updates timestamps on all views", func(t *testing.T) {
|
|
ch := model.NewId()
|
|
creator := model.NewId()
|
|
var created []*model.View
|
|
for i := range 2 {
|
|
v := makeView(ch, creator)
|
|
v.Title = fmt.Sprintf("TS View %d", i)
|
|
v.SortOrder = i
|
|
s, err := ss.View().Save(v)
|
|
require.NoError(t, err)
|
|
created = append(created, s)
|
|
}
|
|
|
|
time.Sleep(2 * time.Millisecond)
|
|
|
|
result, err := ss.View().UpdateSortOrder(created[1].Id, ch, 0)
|
|
require.NoError(t, err)
|
|
for _, v := range result {
|
|
assert.Greater(t, v.UpdateAt, created[0].UpdateAt)
|
|
}
|
|
})
|
|
}
|