Files
zeekay 8feb24ea1f One name for this fork, and it is ours
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.
2026-08-28 23:35:36 -07:00

614 lines
20 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"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 find_bookmark(slice []*model.ChannelBookmarkWithFileInfo, id string) *model.ChannelBookmarkWithFileInfo {
for _, element := range slice {
if element.Id == id {
return element
}
}
return nil
}
func TestChannelBookmarkStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
t.Run("SaveChannelBookmark", func(t *testing.T) { testSaveChannelBookmark(t, rctx, ss) })
t.Run("UpdateChannelBookmark", func(t *testing.T) { testUpdateChannelBookmark(t, rctx, ss) })
t.Run("UpdateSortOrderChannelBookmark", func(t *testing.T) { testUpdateSortOrderChannelBookmark(t, rctx, ss) })
t.Run("DeleteChannelBookmark", func(t *testing.T) { testDeleteChannelBookmark(t, rctx, ss) })
t.Run("GetChannelBookmark", func(t *testing.T) { testGetChannelBookmark(t, rctx, ss) })
t.Run("BoardBookmarkSaveAndGet", func(t *testing.T) { testBoardBookmarkSaveAndGet(t, rctx, ss, s) })
}
func testSaveChannelBookmark(t *testing.T, rctx request.CTX, ss store.Store) {
channelID := model.NewId()
otherChannelID := model.NewId()
userID := model.NewId()
createAt := time.Now().Add(-1 * time.Minute)
deleteAt := createAt.Add(1 * time.Second)
bookmark1 := &model.ChannelBookmark{
ChannelId: channelID,
OwnerId: userID,
DisplayName: "Link bookmark test",
LinkUrl: "https://hanzo.ai",
Type: model.ChannelBookmarkLink,
Emoji: ":smile:",
}
file := &model.FileInfo{
Id: model.NewId(),
ChannelId: channelID,
CreatorId: model.BookmarkFileOwner,
Path: "somepath",
ThumbnailPath: "thumbpath",
PreviewPath: "prevPath",
Name: "test file",
Extension: "png",
MimeType: "images/png",
Size: 873182,
Width: 3076,
Height: 2200,
HasPreviewImage: true,
}
bookmark2 := &model.ChannelBookmark{
ChannelId: channelID,
OwnerId: userID,
DisplayName: "file bookmark test",
FileId: file.Id,
Type: model.ChannelBookmarkFile,
Emoji: ":smile:",
}
bookmark3 := &model.ChannelBookmark{
ChannelId: channelID,
OwnerId: userID,
DisplayName: "file already attached",
FileId: file.Id,
Type: model.ChannelBookmarkFile,
Emoji: ":smile:",
}
file2 := &model.FileInfo{
Id: model.NewId(),
ChannelId: channelID,
CreatorId: userID,
Path: "somepath",
ThumbnailPath: "thumbpath",
PreviewPath: "prevPath",
Name: "test file",
Extension: "png",
MimeType: "images/png",
Size: 873182,
Width: 3076,
Height: 2200,
HasPreviewImage: true,
}
bookmark4 := &model.ChannelBookmark{
ChannelId: channelID,
OwnerId: userID,
DisplayName: "file already attached to a post",
FileId: file2.Id,
Type: model.ChannelBookmarkFile,
Emoji: ":smile:",
}
deletedFile := &model.FileInfo{
Id: model.NewId(),
ChannelId: channelID,
CreatorId: model.BookmarkFileOwner,
Path: "somepath",
ThumbnailPath: "thumbpath",
PreviewPath: "prevPath",
Name: "test file",
Extension: "png",
MimeType: "images/png",
Size: 873182,
Width: 3076,
Height: 2200,
HasPreviewImage: true,
CreateAt: createAt.UnixMilli(),
UpdateAt: createAt.UnixMilli(),
DeleteAt: deleteAt.UnixMilli(),
}
bookmarkFileDeleted := &model.ChannelBookmark{
ChannelId: channelID,
OwnerId: userID,
DisplayName: "file deleted",
FileId: deletedFile.Id,
Type: model.ChannelBookmarkFile,
Emoji: ":smile:",
}
// another channel
anotherChannelFile := &model.FileInfo{
Id: model.NewId(),
ChannelId: otherChannelID,
CreatorId: model.BookmarkFileOwner,
Path: "somepath",
ThumbnailPath: "thumbpath",
PreviewPath: "prevPath",
Name: "test file",
Extension: "png",
MimeType: "images/png",
Size: 873182,
Width: 3076,
Height: 2200,
HasPreviewImage: true,
}
bookmarkFileAnotherChannel := &model.ChannelBookmark{
ChannelId: channelID,
OwnerId: userID,
DisplayName: "file another channel",
FileId: anotherChannelFile.Id,
Type: model.ChannelBookmarkFile,
Emoji: ":smile:",
}
_, err := ss.FileInfo().Save(rctx, file)
require.NoError(t, err)
defer ss.FileInfo().PermanentDelete(rctx, file.Id)
_, err = ss.FileInfo().Save(rctx, file2)
require.NoError(t, err)
defer ss.FileInfo().PermanentDelete(rctx, file2.Id)
err = ss.FileInfo().AttachToPost(rctx, file2.Id, model.NewId(), channelID, userID)
require.NoError(t, err)
_, err = ss.FileInfo().Save(rctx, deletedFile)
require.NoError(t, err)
defer ss.FileInfo().PermanentDelete(rctx, deletedFile.Id)
_, err = ss.FileInfo().Save(rctx, anotherChannelFile)
require.NoError(t, err)
defer ss.FileInfo().PermanentDelete(rctx, anotherChannelFile.Id)
t.Run("save bookmarks", func(t *testing.T) {
bookmarkResp, err := ss.ChannelBookmark().Save(bookmark1.Clone(), true)
assert.NoError(t, err)
assert.NotEmpty(t, bookmarkResp.Id)
assert.Equal(t, bookmark1.ChannelId, bookmarkResp.ChannelId)
assert.Nil(t, bookmarkResp.FileInfo)
bookmarkResp, err = ss.ChannelBookmark().Save(bookmark2.Clone(), true)
assert.NoError(t, err)
assert.NotEmpty(t, bookmarkResp.Id)
assert.Equal(t, bookmark2.ChannelId, bookmarkResp.ChannelId)
assert.NotNil(t, bookmarkResp.FileInfo)
bookmarks, err := ss.ChannelBookmark().GetBookmarksForChannelSince(channelID, 0)
assert.NoError(t, err)
assert.Len(t, bookmarks, 2)
_, err = ss.ChannelBookmark().Save(bookmark3.Clone(), true)
assert.Error(t, err) // Error as the file is attached to a bookmark
_, err = ss.ChannelBookmark().Save(bookmark4.Clone(), true)
assert.Error(t, err) // Error as the file is attached to a post
_, err = ss.ChannelBookmark().Save(bookmarkFileDeleted.Clone(), true)
assert.Error(t, err) // Error as the file is deleted
_, err = ss.ChannelBookmark().Save(bookmarkFileAnotherChannel.Clone(), true)
assert.Error(t, err) // Error as the file is from another channel
})
}
func testUpdateChannelBookmark(t *testing.T, rctx request.CTX, ss store.Store) {
channelID := model.NewId()
userID := model.NewId()
bookmark1 := &model.ChannelBookmark{
ChannelId: channelID,
OwnerId: userID,
DisplayName: "Link bookmark test",
LinkUrl: "https://hanzo.ai",
Type: model.ChannelBookmarkLink,
}
t.Run("update bookmark", func(t *testing.T) {
bookmarkResp, err := ss.ChannelBookmark().Save(bookmark1.Clone(), true)
assert.NoError(t, err)
now := model.GetMillis()
bookmark2 := bookmarkResp.ChannelBookmark.Clone()
bookmark2.DisplayName = "Updated display name"
bookmark2.Emoji = ":smile:"
bookmark2.LinkUrl = "https://hanzo.ai/about"
time.Sleep(time.Millisecond * 250)
err = ss.ChannelBookmark().Update(bookmark2.Clone())
assert.NoError(t, err)
bookmarks, err := ss.ChannelBookmark().GetBookmarksForChannelSince(channelID, now)
assert.NoError(t, err)
assert.Len(t, bookmarks, 1)
b := find_bookmark(bookmarks, bookmark2.Id)
assert.NotNil(t, b)
assert.Equal(t, b.DisplayName, bookmark2.DisplayName)
assert.Equal(t, b.Type, model.ChannelBookmarkLink)
assert.NotEmpty(t, b.Emoji)
assert.Equal(t, b.CreateAt, bookmark2.CreateAt)
assert.Greater(t, b.UpdateAt, bookmark2.UpdateAt)
err = ss.ChannelBookmark().Update(bookmark1.Clone())
assert.Error(t, err)
bookmark3 := bookmark2.Clone()
bookmark3.Type = model.ChannelBookmarkFile
err = ss.ChannelBookmark().Update(bookmark3)
assert.Error(t, err)
})
}
func testUpdateSortOrderChannelBookmark(t *testing.T, rctx request.CTX, ss store.Store) {
channelID := model.NewId()
userID := model.NewId()
bookmark0 := &model.ChannelBookmark{
ChannelId: channelID,
OwnerId: userID,
DisplayName: "Bookmark 0",
LinkUrl: "https://hanzo.ai",
Type: model.ChannelBookmarkLink,
Emoji: ":smile:",
}
file := &model.FileInfo{
Id: model.NewId(),
ChannelId: channelID,
CreatorId: model.BookmarkFileOwner,
Path: "somepath",
ThumbnailPath: "thumbpath",
PreviewPath: "prevPath",
Name: "test file",
Extension: "png",
MimeType: "images/png",
Size: 873182,
Width: 3076,
Height: 2200,
HasPreviewImage: true,
}
bookmark1 := &model.ChannelBookmark{
ChannelId: channelID,
OwnerId: userID,
DisplayName: "Bookmark 1",
FileId: file.Id,
Type: model.ChannelBookmarkFile,
Emoji: ":smile:",
}
_, err := ss.FileInfo().Save(rctx, file)
require.NoError(t, err)
defer ss.FileInfo().PermanentDelete(rctx, file.Id)
bookmark2 := &model.ChannelBookmark{
ChannelId: channelID,
OwnerId: userID,
DisplayName: "Bookmark 2",
LinkUrl: "https://hanzo.ai",
Type: model.ChannelBookmarkLink,
}
bookmark3 := &model.ChannelBookmark{
ChannelId: channelID,
OwnerId: userID,
DisplayName: "Bookmark 3",
LinkUrl: "https://hanzo.ai",
Type: model.ChannelBookmarkLink,
}
bookmark4 := &model.ChannelBookmark{
ChannelId: channelID,
OwnerId: userID,
DisplayName: "Bookmark 4",
LinkUrl: "https://hanzo.ai",
Type: model.ChannelBookmarkLink,
}
bookmarkResp, err := ss.ChannelBookmark().Save(bookmark0.Clone(), true)
assert.NoError(t, err)
bookmark0 = bookmarkResp.ChannelBookmark.Clone()
assert.NotEmpty(t, bookmarkResp.Id)
assert.Equal(t, bookmark0.ChannelId, bookmarkResp.ChannelId)
assert.Nil(t, bookmarkResp.FileInfo)
bookmarkResp, err = ss.ChannelBookmark().Save(bookmark1.Clone(), true)
assert.NoError(t, err)
bookmark1 = bookmarkResp.ChannelBookmark.Clone()
bookmarkResp, err = ss.ChannelBookmark().Save(bookmark2.Clone(), true)
assert.NoError(t, err)
bookmark2 = bookmarkResp.ChannelBookmark.Clone()
bookmarkResp, err = ss.ChannelBookmark().Save(bookmark3.Clone(), true)
assert.NoError(t, err)
bookmark3 = bookmarkResp.ChannelBookmark.Clone()
bookmarkResp, err = ss.ChannelBookmark().Save(bookmark4.Clone(), true)
assert.NoError(t, err)
bookmark4 = bookmarkResp.ChannelBookmark.Clone()
t.Run("change order of bookmarks first to last", func(t *testing.T) {
bookmarks, sortError := ss.ChannelBookmark().UpdateSortOrder(bookmark0.Id, channelID, 4)
assert.NoError(t, sortError)
assert.Equal(t, find_bookmark(bookmarks, bookmark1.Id).SortOrder, int64(0))
assert.Equal(t, find_bookmark(bookmarks, bookmark2.Id).SortOrder, int64(1))
assert.Equal(t, find_bookmark(bookmarks, bookmark3.Id).SortOrder, int64(2))
assert.Equal(t, find_bookmark(bookmarks, bookmark4.Id).SortOrder, int64(3))
assert.Equal(t, find_bookmark(bookmarks, bookmark0.Id).SortOrder, int64(4))
})
t.Run("change order of bookmarks last to first", func(t *testing.T) {
bookmarks, sortError := ss.ChannelBookmark().UpdateSortOrder(bookmark0.Id, channelID, 0)
assert.NoError(t, sortError)
assert.Equal(t, find_bookmark(bookmarks, bookmark0.Id).SortOrder, int64(0))
assert.Equal(t, find_bookmark(bookmarks, bookmark1.Id).SortOrder, int64(1))
assert.Equal(t, find_bookmark(bookmarks, bookmark2.Id).SortOrder, int64(2))
assert.Equal(t, find_bookmark(bookmarks, bookmark3.Id).SortOrder, int64(3))
assert.Equal(t, find_bookmark(bookmarks, bookmark4.Id).SortOrder, int64(4))
})
t.Run("change order of bookmarks first to third", func(t *testing.T) {
bookmarks, sortError := ss.ChannelBookmark().UpdateSortOrder(bookmark0.Id, channelID, 2)
assert.NoError(t, sortError)
assert.Equal(t, find_bookmark(bookmarks, bookmark1.Id).SortOrder, int64(0))
assert.Equal(t, find_bookmark(bookmarks, bookmark2.Id).SortOrder, int64(1))
assert.Equal(t, find_bookmark(bookmarks, bookmark0.Id).SortOrder, int64(2))
assert.Equal(t, find_bookmark(bookmarks, bookmark3.Id).SortOrder, int64(3))
assert.Equal(t, find_bookmark(bookmarks, bookmark4.Id).SortOrder, int64(4))
// now reset order
ss.ChannelBookmark().UpdateSortOrder(bookmark0.Id, channelID, 0)
})
t.Run("change order of bookmarks second to third", func(t *testing.T) {
bookmarks, sortError := ss.ChannelBookmark().UpdateSortOrder(bookmark1.Id, channelID, 2)
assert.NoError(t, sortError)
assert.Equal(t, find_bookmark(bookmarks, bookmark0.Id).SortOrder, int64(0))
assert.Equal(t, find_bookmark(bookmarks, bookmark2.Id).SortOrder, int64(1))
assert.Equal(t, find_bookmark(bookmarks, bookmark1.Id).SortOrder, int64(2))
assert.Equal(t, find_bookmark(bookmarks, bookmark3.Id).SortOrder, int64(3))
assert.Equal(t, find_bookmark(bookmarks, bookmark4.Id).SortOrder, int64(4))
})
t.Run("change order of bookmarks third to second", func(t *testing.T) {
bookmarks, sortError := ss.ChannelBookmark().UpdateSortOrder(bookmark1.Id, channelID, 1)
assert.NoError(t, sortError)
assert.Equal(t, find_bookmark(bookmarks, bookmark0.Id).SortOrder, int64(0))
assert.Equal(t, find_bookmark(bookmarks, bookmark1.Id).SortOrder, int64(1))
assert.Equal(t, find_bookmark(bookmarks, bookmark2.Id).SortOrder, int64(2))
assert.Equal(t, find_bookmark(bookmarks, bookmark3.Id).SortOrder, int64(3))
assert.Equal(t, find_bookmark(bookmarks, bookmark4.Id).SortOrder, int64(4))
})
t.Run("change order of bookmarks last to previous last", func(t *testing.T) {
bookmarks, sortError := ss.ChannelBookmark().UpdateSortOrder(bookmark4.Id, channelID, 3)
assert.NoError(t, sortError)
assert.Equal(t, find_bookmark(bookmarks, bookmark0.Id).SortOrder, int64(0))
assert.Equal(t, find_bookmark(bookmarks, bookmark1.Id).SortOrder, int64(1))
assert.Equal(t, find_bookmark(bookmarks, bookmark2.Id).SortOrder, int64(2))
assert.Equal(t, find_bookmark(bookmarks, bookmark4.Id).SortOrder, int64(3))
assert.Equal(t, find_bookmark(bookmarks, bookmark3.Id).SortOrder, int64(4))
})
t.Run("change order of bookmarks last to second", func(t *testing.T) {
bookmarks, sortError := ss.ChannelBookmark().UpdateSortOrder(bookmark3.Id, channelID, 1)
assert.NoError(t, sortError)
assert.Equal(t, find_bookmark(bookmarks, bookmark0.Id).SortOrder, int64(0))
assert.Equal(t, find_bookmark(bookmarks, bookmark3.Id).SortOrder, int64(1))
assert.Equal(t, find_bookmark(bookmarks, bookmark1.Id).SortOrder, int64(2))
assert.Equal(t, find_bookmark(bookmarks, bookmark2.Id).SortOrder, int64(3))
assert.Equal(t, find_bookmark(bookmarks, bookmark4.Id).SortOrder, int64(4))
})
t.Run("change order of bookmarks error when new index is out of bounds", func(t *testing.T) {
var iiErr *store.ErrInvalidInput
_, err = ss.ChannelBookmark().UpdateSortOrder(bookmark3.Id, channelID, -1)
assert.Error(t, err)
assert.ErrorAs(t, err, &iiErr)
_, err = ss.ChannelBookmark().UpdateSortOrder(bookmark3.Id, channelID, 5)
assert.Error(t, err)
assert.ErrorAs(t, err, &iiErr)
})
t.Run("change order of bookmarks error when bookmark not found", func(t *testing.T) {
_, err = ss.ChannelBookmark().UpdateSortOrder(model.NewId(), channelID, 0)
assert.Error(t, err)
var nfErr *store.ErrNotFound
assert.ErrorAs(t, err, &nfErr)
})
}
func testDeleteChannelBookmark(t *testing.T, rctx request.CTX, ss store.Store) {
channelID := model.NewId()
userID := model.NewId()
bookmark1 := &model.ChannelBookmark{
ChannelId: channelID,
OwnerId: userID,
DisplayName: "Link bookmark test",
LinkUrl: "https://hanzo.ai",
Type: model.ChannelBookmarkLink,
Emoji: ":smile:",
}
file := &model.FileInfo{
Id: model.NewId(),
ChannelId: channelID,
CreatorId: model.BookmarkFileOwner,
Path: "somepath",
ThumbnailPath: "thumbpath",
PreviewPath: "prevPath",
Name: "test file",
Extension: "png",
MimeType: "images/png",
Size: 873182,
Width: 3076,
Height: 2200,
HasPreviewImage: true,
}
bookmark2 := &model.ChannelBookmark{
ChannelId: channelID,
OwnerId: userID,
DisplayName: "file bookmark test",
FileId: file.Id,
Type: model.ChannelBookmarkFile,
Emoji: ":smile:",
}
_, err := ss.FileInfo().Save(rctx, file)
require.NoError(t, err)
defer ss.FileInfo().PermanentDelete(rctx, file.Id)
t.Run("delete bookmark", func(t *testing.T) {
now := model.GetMillis()
bookmarkResp, err := ss.ChannelBookmark().Save(bookmark1.Clone(), true)
assert.NoError(t, err)
bookmark1 = bookmarkResp.ChannelBookmark.Clone()
assert.NotEmpty(t, bookmarkResp.Id)
assert.Equal(t, bookmark1.ChannelId, bookmarkResp.ChannelId)
assert.Nil(t, bookmarkResp.FileInfo)
bookmarkResp, err = ss.ChannelBookmark().Save(bookmark2.Clone(), true)
assert.NoError(t, err)
bookmark2 = bookmarkResp.ChannelBookmark.Clone()
err = ss.ChannelBookmark().Delete(bookmark2.Id, true)
assert.NoError(t, err)
_, err = ss.FileInfo().Get(file.Id)
assert.Error(t, err)
var nfErr *store.ErrNotFound
assert.ErrorAs(t, err, &nfErr)
bookmarks, err := ss.ChannelBookmark().GetBookmarksForChannelSince(channelID, now)
assert.NoError(t, err)
assert.Len(t, bookmarks, 2) // we have two as the deleted record also gets returned for sync'ing purposes
b := find_bookmark(bookmarks, bookmark2.Id)
assert.NotNil(t, b)
assert.Equal(t, bookmarks[0].Type, model.ChannelBookmarkLink)
})
}
func testGetChannelBookmark(t *testing.T, rctx request.CTX, ss store.Store) {
channelID := model.NewId()
userID := model.NewId()
bookmark1 := &model.ChannelBookmark{
ChannelId: channelID,
OwnerId: userID,
DisplayName: "Link bookmark test",
LinkUrl: "https://hanzo.ai",
Type: model.ChannelBookmarkLink,
Emoji: ":smile:",
}
t.Run("get bookmark", func(t *testing.T) {
bookmarkResp, err := ss.ChannelBookmark().Save(bookmark1.Clone(), true)
assert.NoError(t, err)
bookmark1 = bookmarkResp.ChannelBookmark.Clone()
bookmarkResp, err = ss.ChannelBookmark().Get(bookmark1.Id, false)
assert.NoError(t, err)
assert.NotEmpty(t, bookmarkResp.Id)
assert.Equal(t, bookmark1.ChannelId, bookmarkResp.ChannelId)
assert.Nil(t, bookmarkResp.FileInfo)
err = ss.ChannelBookmark().Delete(bookmark1.Id, true)
assert.NoError(t, err)
bookmarkResp, err = ss.ChannelBookmark().Get(bookmark1.Id, false)
assert.Error(t, err)
assert.Nil(t, bookmarkResp)
bookmarkResp, err = ss.ChannelBookmark().Get(bookmark1.Id, true)
assert.NoError(t, err)
assert.NotNil(t, bookmarkResp)
})
}
func testBoardBookmarkSaveAndGet(t *testing.T, _ request.CTX, ss store.Store, s SqlStore) {
t.Helper()
channelID := model.NewId()
userID := model.NewId()
boardChannelID := model.NewId()
linkPath := "/team/boards/" + boardChannelID
bookmark := &model.ChannelBookmark{
ChannelId: channelID,
OwnerId: userID,
DisplayName: "Engineering Roadmap",
LinkUrl: linkPath,
Type: model.ChannelBookmarkBoard,
TargetId: boardChannelID,
}
saved, err := ss.ChannelBookmark().Save(bookmark, true)
require.NoError(t, err)
require.NotNil(t, saved)
assert.Equal(t, model.ChannelBookmarkBoard, saved.Type)
assert.Equal(t, boardChannelID, saved.TargetId)
assert.Equal(t, linkPath, saved.LinkUrl)
loaded, err := ss.ChannelBookmark().Get(saved.Id, false)
require.NoError(t, err)
assert.Equal(t, model.ChannelBookmarkBoard, loaded.Type)
assert.Equal(t, boardChannelID, loaded.TargetId)
assert.Equal(t, linkPath, loaded.LinkUrl)
linkBookmark := &model.ChannelBookmark{
ChannelId: channelID,
OwnerId: userID,
DisplayName: "Plain link",
LinkUrl: "https://example.com",
Type: model.ChannelBookmarkLink,
}
linkSaved, err := ss.ChannelBookmark().Save(linkBookmark, true)
require.NoError(t, err)
var count int
err = s.GetMaster().Get(&count,
"SELECT COUNT(*) FROM channelbookmarks WHERE id = ? AND targetid IS NULL",
linkSaved.Id)
require.NoError(t, err)
assert.Equal(t, 1, count, "link bookmarks must persist SQL NULL for targetid")
}