owpengram-server/internal/store/memory/story_test.go
2026-07-24 11:56:58 +08:00

2047 lines
74 KiB
Go

package memory
import (
"context"
"errors"
"testing"
"telesrv/internal/domain"
)
func TestStoryStoreReadMaxAndPeerMaxIDs(t *testing.T) {
ctx := context.Background()
store := NewStoryStore()
owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1001}
for _, id := range []int{1, 2} {
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{
Owner: owner,
ID: id,
Date: 100 + id,
ExpireDate: 1000,
Public: true,
}}); err != nil {
t.Fatalf("upsert story %d: %v", id, err)
}
}
read, err := store.MarkRead(ctx, 2001, owner, 2, 200)
if err != nil {
t.Fatalf("mark read: %v", err)
}
if !read.Advanced || read.MaxReadID != 2 {
t.Fatalf("read = %+v, want advanced max 2", read)
}
read, err = store.MarkRead(ctx, 2001, owner, 1, 201)
if err != nil {
t.Fatalf("mark read lower: %v", err)
}
if read.Advanced || read.MaxReadID != 2 {
t.Fatalf("lower read = %+v, want unchanged max 2", read)
}
peerStories, err := store.GetPeerStories(ctx, 2001, owner, 300)
if err != nil {
t.Fatalf("get peer stories: %v", err)
}
if peerStories.MaxReadID != 2 || len(peerStories.Stories) != 2 {
t.Fatalf("peer stories = %+v, want max read 2 and two stories", peerStories)
}
recent, err := store.GetPeerMaxIDs(ctx, 2001, []domain.Peer{owner}, 300)
if err != nil {
t.Fatalf("peer max ids: %v", err)
}
if len(recent) != 1 || recent[0].MaxID != 2 {
t.Fatalf("recent = %+v, want max id 2", recent)
}
projections, err := store.GetPeerStoryProjections(ctx, 2001, []domain.Peer{owner}, 300)
if err != nil {
t.Fatalf("peer projections: %v", err)
}
if len(projections) != 1 || projections[0].Peer != owner || projections[0].Recent.MaxID != 2 || projections[0].Hidden {
t.Fatalf("projections = %+v, want max id 2 and visible owner", projections)
}
}
func TestStoryStorePeerHiddenStates(t *testing.T) {
ctx := context.Background()
store := NewStoryStore()
viewerID := int64(2001)
owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1001}
channel := domain.Peer{Type: domain.PeerTypeChannel, ID: 3001}
states, err := store.GetPeerHiddenStates(ctx, viewerID, []domain.Peer{owner, channel})
if err != nil {
t.Fatalf("initial hidden states: %v", err)
}
if states[owner] || states[channel] {
t.Fatalf("initial hidden states = %+v, want both false", states)
}
if err := store.SetPeerHidden(ctx, viewerID, owner, true); err != nil {
t.Fatalf("set owner hidden: %v", err)
}
states, err = store.GetPeerHiddenStates(ctx, viewerID, []domain.Peer{owner, channel})
if err != nil {
t.Fatalf("hidden states after set: %v", err)
}
if !states[owner] || states[channel] {
t.Fatalf("hidden states after set = %+v, want owner true channel false", states)
}
projections, err := store.GetPeerStoryProjections(ctx, viewerID, []domain.Peer{owner, channel}, 300)
if err != nil {
t.Fatalf("story projections after set: %v", err)
}
if len(projections) != 2 || !projections[0].Hidden || projections[1].Hidden {
t.Fatalf("story projections after set = %+v, want owner hidden channel visible", projections)
}
if err := store.SetPeerHidden(ctx, viewerID, owner, false); err != nil {
t.Fatalf("clear owner hidden: %v", err)
}
states, err = store.GetPeerHiddenStates(ctx, viewerID, []domain.Peer{owner})
if err != nil {
t.Fatalf("hidden states after clear: %v", err)
}
if states[owner] {
t.Fatalf("hidden states after clear = %+v, want owner false", states)
}
}
func TestStoryStoreListActiveStoriesPaginatesByPeer(t *testing.T) {
ctx := context.Background()
store := NewStoryStore()
owners := []domain.Peer{
{Type: domain.PeerTypeUser, ID: 1001},
{Type: domain.PeerTypeUser, ID: 1002},
{Type: domain.PeerTypeUser, ID: 1003},
}
for i, owner := range owners {
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{
Owner: owner,
ID: 1,
Date: 300 - i*100,
ExpireDate: 1000,
Public: true,
}}); err != nil {
t.Fatalf("upsert owner %d story: %v", owner.ID, err)
}
}
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{
Owner: owners[0],
ID: 2,
Date: 250,
ExpireDate: 1000,
Public: true,
}}); err != nil {
t.Fatalf("upsert second story for first owner: %v", err)
}
first, err := store.ListActiveStoriesPage(ctx, 2001, false, 400, domain.StoryListCursor{}, 2)
if err != nil {
t.Fatalf("first page: %v", err)
}
if first.Count != 3 || !first.HasMore || len(first.Peers) != 2 {
t.Fatalf("first page = count %d more %v peers %d, want count 3 more true peers 2", first.Count, first.HasMore, len(first.Peers))
}
if first.Peers[0].Peer != owners[0] || len(first.Peers[0].Stories) != 2 {
t.Fatalf("first peer = %+v stories %d, want owner 1001 with two stories", first.Peers[0].Peer, len(first.Peers[0].Stories))
}
next, err := store.ListActiveStoriesPage(ctx, 2001, false, 400, domain.StoryListCursor{
Set: true,
Date: 200,
Peer: owners[1],
}, 2)
if err != nil {
t.Fatalf("next page: %v", err)
}
if next.Count != 3 || next.HasMore || len(next.Peers) != 1 || next.Peers[0].Peer != owners[2] {
t.Fatalf("next page = %+v, want final page with owner 1003", next)
}
digest, err := store.ActiveStoriesDigest(ctx, 2001, false, 400)
if err != nil {
t.Fatalf("digest active stories: %v", err)
}
if digest.Count != 3 {
t.Fatalf("digest count = %d, want 3", digest.Count)
}
if _, err := store.MarkRead(ctx, 2001, owners[0], 2, 401); err != nil {
t.Fatalf("mark read for digest: %v", err)
}
changed, err := store.ActiveStoriesDigest(ctx, 2001, false, 400)
if err != nil {
t.Fatalf("changed digest active stories: %v", err)
}
if changed.Hash == digest.Hash {
t.Fatalf("digest hash unchanged after read boundary: %#x", changed.Hash)
}
}
func TestStoryStoreSelfUserViewAndReactionDoNotPolluteOwnerInteractions(t *testing.T) {
ctx := context.Background()
store := NewStoryStore()
owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1001}
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{
Owner: owner,
ID: 1,
Date: 100,
ExpireDate: 1000,
Public: true,
}}); err != nil {
t.Fatalf("upsert story: %v", err)
}
created, err := store.IncrementViews(ctx, owner.ID, owner, []int{1}, 200)
if err != nil {
t.Fatalf("increment self view: %v", err)
}
if created != 0 {
t.Fatalf("created self views = %d, want 0", created)
}
if _, err := store.SetReaction(ctx, owner.ID, owner, 1, &domain.MessageReaction{
Type: domain.MessageReactionEmoji,
Emoticon: "🔥",
}, 201); err != domain.ErrStoryPeerInvalid {
t.Fatalf("self reaction err = %v, want ErrStoryPeerInvalid", err)
}
list, err := store.ListStoryViews(ctx, domain.StoryViewListRequest{
ViewerUserID: owner.ID,
Owner: owner,
StoryID: 1,
Limit: 10,
})
if err != nil {
t.Fatalf("list story views: %v", err)
}
if list.Count != 0 || list.ViewsCount != 0 || list.ReactionsCount != 0 || len(list.Views) != 0 {
t.Fatalf("self view list = %+v, want empty counters", list)
}
}
func TestStoryStoreBlocklistHidesOwnerStoriesFromViewer(t *testing.T) {
ctx := context.Background()
store := NewStoryStore()
owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1001}
blockedViewerID := int64(2001)
otherViewerID := int64(2002)
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{
Owner: owner,
ID: 1,
Date: 100,
ExpireDate: 1000,
Public: true,
}}); err != nil {
t.Fatalf("upsert story: %v", err)
}
if created, err := store.IncrementViews(ctx, blockedViewerID, owner, []int{1}, 105); err != nil || created != 1 {
t.Fatalf("pre-block increment = %d, %v, want 1 nil", created, err)
}
store.SetStoryBlockedUsers(owner.ID, blockedViewerID)
peerStories, err := store.GetPeerStories(ctx, blockedViewerID, owner, 200)
if err != nil {
t.Fatalf("get peer stories: %v", err)
}
if len(peerStories.Stories) != 0 {
t.Fatalf("blocked peer stories = %+v, want empty", peerStories.Stories)
}
exact, err := store.GetStoriesByID(ctx, blockedViewerID, owner, []int{1}, 200)
if err != nil {
t.Fatalf("get stories by id: %v", err)
}
if len(exact.Stories) != 0 {
t.Fatalf("blocked exact stories = %+v, want empty", exact.Stories)
}
recent, err := store.GetPeerMaxIDs(ctx, blockedViewerID, []domain.Peer{owner}, 200)
if err != nil {
t.Fatalf("get peer max ids: %v", err)
}
if len(recent) != 1 || recent[0].MaxID != 0 {
t.Fatalf("blocked recent = %+v, want max id 0", recent)
}
active, err := store.ListActiveStories(ctx, blockedViewerID, false, 200, 100)
if err != nil {
t.Fatalf("list active stories: %v", err)
}
if active.Count != 0 || len(active.Stories) != 0 {
t.Fatalf("blocked active stories = %+v, want empty", active)
}
if created, err := store.IncrementViews(ctx, blockedViewerID, owner, []int{1}, 210); err != nil || created != 0 {
t.Fatalf("blocked increment = %d, %v, want 0 nil", created, err)
}
if _, err := store.SetReaction(ctx, blockedViewerID, owner, 1, &domain.MessageReaction{
Type: domain.MessageReactionEmoji,
Emoticon: "🔥",
}, 211); err != domain.ErrStoryNotFound {
t.Fatalf("blocked reaction err = %v, want ErrStoryNotFound", err)
}
ownerStories, err := store.GetPeerStories(ctx, owner.ID, owner, 200)
if err != nil {
t.Fatalf("owner get peer stories: %v", err)
}
if len(ownerStories.Stories) != 1 {
t.Fatalf("owner peer stories = %+v, want one story", ownerStories.Stories)
}
otherStories, err := store.GetPeerStories(ctx, otherViewerID, owner, 200)
if err != nil {
t.Fatalf("other get peer stories: %v", err)
}
if len(otherStories.Stories) != 1 {
t.Fatalf("other peer stories = %+v, want one story", otherStories.Stories)
}
if created, err := store.IncrementViews(ctx, otherViewerID, owner, []int{1}, 220); err != nil || created != 1 {
t.Fatalf("other increment = %d, %v, want 1 nil", created, err)
}
list, err := store.ListStoryViews(ctx, domain.StoryViewListRequest{
ViewerUserID: owner.ID,
Owner: owner,
StoryID: 1,
Limit: 10,
})
if err != nil {
t.Fatalf("list story views: %v", err)
}
got := map[int64]domain.StoryView{}
for _, view := range list.Views {
got[view.ViewerID] = view
}
if list.Count != 2 || len(list.Views) != 2 {
t.Fatalf("view list = %+v, want historical blocked viewer and other viewer", list)
}
if !got[blockedViewerID].BlockedMyStoriesFrom {
t.Fatalf("blocked viewer row = %+v, want blocked_my_stories_from", got[blockedViewerID])
}
if got[otherViewerID].BlockedMyStoriesFrom {
t.Fatalf("other viewer row = %+v, want not blocked", got[otherViewerID])
}
}
func TestStoryStoreChannelStoriesRequireActiveMember(t *testing.T) {
ctx := context.Background()
store := NewStoryStore()
channel := domain.Peer{Type: domain.PeerTypeChannel, ID: 3001}
memberID := int64(2001)
outsiderID := int64(2002)
store.SetStoryChannelMembers(channel.ID, memberID)
if _, err := store.CreateStory(ctx, domain.StoryCreateRequest{
Owner: channel,
RandomID: 300101,
Date: 100,
Period: 86400,
Public: true,
}); err != nil {
t.Fatalf("create channel story: %v", err)
}
memberStories, err := store.GetPeerStories(ctx, memberID, channel, 200)
if err != nil {
t.Fatalf("member get peer stories: %v", err)
}
if len(memberStories.Stories) != 1 || memberStories.Stories[0].Owner != channel {
t.Fatalf("member stories = %+v, want channel story", memberStories.Stories)
}
outsiderStories, err := store.GetPeerStories(ctx, outsiderID, channel, 200)
if err != nil {
t.Fatalf("outsider get peer stories: %v", err)
}
if len(outsiderStories.Stories) != 0 {
t.Fatalf("outsider stories = %+v, want empty", outsiderStories.Stories)
}
exact, err := store.GetStoriesByID(ctx, outsiderID, channel, []int{1}, 200)
if err != nil {
t.Fatalf("outsider exact story: %v", err)
}
if len(exact.Stories) != 0 {
t.Fatalf("outsider exact = %+v, want empty", exact.Stories)
}
recent, err := store.GetPeerMaxIDs(ctx, outsiderID, []domain.Peer{channel}, 200)
if err != nil {
t.Fatalf("outsider peer max ids: %v", err)
}
if len(recent) != 1 || recent[0].MaxID != 0 {
t.Fatalf("outsider recent = %+v, want max id 0", recent)
}
active, err := store.ListActiveStories(ctx, outsiderID, false, 200, 100)
if err != nil {
t.Fatalf("outsider active stories: %v", err)
}
if active.Count != 0 || len(active.Stories) != 0 {
t.Fatalf("outsider active stories = %+v, want empty", active)
}
if created, err := store.IncrementViews(ctx, outsiderID, channel, []int{1}, 201); err != nil || created != 0 {
t.Fatalf("outsider increment = %d, %v, want 0 nil", created, err)
}
if _, err := store.SetReaction(ctx, outsiderID, channel, 1, &domain.MessageReaction{
Type: domain.MessageReactionEmoji,
Emoticon: "like",
}, 202); !errors.Is(err, domain.ErrStoryNotFound) {
t.Fatalf("outsider reaction err = %v, want ErrStoryNotFound", err)
}
}
func TestStoryStoreMediaAreasRoundTripEditClearAndClone(t *testing.T) {
ctx := context.Background()
store := NewStoryStore()
owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1001}
created, err := store.CreateStory(ctx, domain.StoryCreateRequest{
Owner: owner,
RandomID: 101,
Date: 100,
Period: 900,
Public: true,
MediaAreas: []domain.StoryMediaArea{testDomainStoryMediaArea("🔥", 10)},
})
if err != nil {
t.Fatalf("create story: %v", err)
}
assertDomainStoryMediaArea(t, created.Story, "🔥", 10)
created.Story.MediaAreas[0].Reaction.Emoticon = "mutated"
list, err := store.GetStoriesByID(ctx, owner.ID, owner, []int{created.Story.ID, created.Story.ID}, 200)
if err != nil {
t.Fatalf("get stories by id: %v", err)
}
if len(list.Stories) != 1 {
t.Fatalf("stories by duplicate ids = %+v, want one story", list.Stories)
}
assertDomainStoryMediaArea(t, list.Stories[0], "🔥", 10)
if _, err := store.GetStoriesByID(ctx, owner.ID, owner, nil, 200); !errors.Is(err, domain.ErrStoryIDInvalid) {
t.Fatalf("empty get stories by id err = %v, want ErrStoryIDInvalid", err)
}
edited, err := store.EditStory(ctx, domain.StoryEditRequest{
Owner: owner,
ID: created.Story.ID,
UpdateMediaAreas: true,
MediaAreas: []domain.StoryMediaArea{testDomainStoryURLMediaArea("https://example.com/story/link", 25)},
})
if err != nil {
t.Fatalf("edit story media areas: %v", err)
}
assertDomainStoryURLMediaArea(t, edited.Story, "https://example.com/story/link", 25)
edited.Story.MediaAreas[0].URL = "https://mutated.invalid"
list, err = store.GetStoriesByID(ctx, owner.ID, owner, []int{created.Story.ID}, 201)
if err != nil {
t.Fatalf("get edited story by id: %v", err)
}
assertDomainStoryURLMediaArea(t, list.Stories[0], "https://example.com/story/link", 25)
geoEdited, err := store.EditStory(ctx, domain.StoryEditRequest{
Owner: owner,
ID: created.Story.ID,
UpdateMediaAreas: true,
MediaAreas: []domain.StoryMediaArea{testDomainStoryGeoPointMediaArea(31.2304, 121.4737, 35)},
})
if err != nil {
t.Fatalf("edit story geo media area: %v", err)
}
assertDomainStoryGeoPointMediaArea(t, geoEdited.Story, 31.2304, 121.4737, 35)
geoEdited.Story.MediaAreas[0].Geo.Lat = 0
geoEdited.Story.MediaAreas[0].GeoAddress.City = "Mutated"
list, err = store.GetStoriesByID(ctx, owner.ID, owner, []int{created.Story.ID}, 202)
if err != nil {
t.Fatalf("get geo edited story by id: %v", err)
}
assertDomainStoryGeoPointMediaArea(t, list.Stories[0], 31.2304, 121.4737, 35)
venueEdited, err := store.EditStory(ctx, domain.StoryEditRequest{
Owner: owner,
ID: created.Story.ID,
UpdateMediaAreas: true,
MediaAreas: []domain.StoryMediaArea{testDomainStoryVenueMediaArea("Inline Cafe", 31.231, 121.474, 40)},
})
if err != nil {
t.Fatalf("edit story venue media area: %v", err)
}
assertDomainStoryVenueMediaArea(t, venueEdited.Story, "Inline Cafe", 31.231, 121.474, 40)
venueEdited.Story.MediaAreas[0].Venue.Title = "Mutated"
list, err = store.GetStoriesByID(ctx, owner.ID, owner, []int{created.Story.ID}, 203)
if err != nil {
t.Fatalf("get venue edited story by id: %v", err)
}
assertDomainStoryVenueMediaArea(t, list.Stories[0], "Inline Cafe", 31.231, 121.474, 40)
weatherEdited, err := store.EditStory(ctx, domain.StoryEditRequest{
Owner: owner,
ID: created.Story.ID,
UpdateMediaAreas: true,
MediaAreas: []domain.StoryMediaArea{testDomainStoryWeatherMediaArea("☀️", 22.5, 0x00cc6600, 45)},
})
if err != nil {
t.Fatalf("edit story weather media area: %v", err)
}
assertDomainStoryWeatherMediaArea(t, weatherEdited.Story, "☀️", 22.5, 0x00cc6600, 45)
weatherEdited.Story.MediaAreas[0].WeatherEmoji = "mutated"
weatherEdited.Story.MediaAreas[0].TemperatureC = -100
weatherEdited.Story.MediaAreas[0].Color = 0
list, err = store.GetStoriesByID(ctx, owner.ID, owner, []int{created.Story.ID}, 204)
if err != nil {
t.Fatalf("get weather edited story by id: %v", err)
}
assertDomainStoryWeatherMediaArea(t, list.Stories[0], "☀️", 22.5, 0x00cc6600, 45)
channelPostEdited, err := store.EditStory(ctx, domain.StoryEditRequest{
Owner: owner,
ID: created.Story.ID,
UpdateMediaAreas: true,
MediaAreas: []domain.StoryMediaArea{testDomainStoryChannelPostMediaArea(777001, 42, 50)},
})
if err != nil {
t.Fatalf("edit story channel post media area: %v", err)
}
assertDomainStoryChannelPostMediaArea(t, channelPostEdited.Story, 777001, 42, 50)
channelPostEdited.Story.MediaAreas[0].ChannelID = 0
channelPostEdited.Story.MediaAreas[0].MsgID = 0
list, err = store.GetStoriesByID(ctx, owner.ID, owner, []int{created.Story.ID}, 205)
if err != nil {
t.Fatalf("get channel post edited story by id: %v", err)
}
assertDomainStoryChannelPostMediaArea(t, list.Stories[0], 777001, 42, 50)
starGiftEdited, err := store.EditStory(ctx, domain.StoryEditRequest{
Owner: owner,
ID: created.Story.ID,
UpdateMediaAreas: true,
MediaAreas: []domain.StoryMediaArea{testDomainStoryStarGiftMediaArea("Gift.Series_01-42", 55)},
})
if err != nil {
t.Fatalf("edit story star gift media area: %v", err)
}
assertDomainStoryStarGiftMediaArea(t, starGiftEdited.Story, "Gift.Series_01-42", 55)
starGiftEdited.Story.MediaAreas[0].StarGiftSlug = "mutated"
list, err = store.GetStoriesByID(ctx, owner.ID, owner, []int{created.Story.ID}, 206)
if err != nil {
t.Fatalf("get star gift edited story by id: %v", err)
}
assertDomainStoryStarGiftMediaArea(t, list.Stories[0], "Gift.Series_01-42", 55)
cleared, err := store.EditStory(ctx, domain.StoryEditRequest{
Owner: owner,
ID: created.Story.ID,
UpdateMediaAreas: true,
})
if err != nil {
t.Fatalf("clear story media areas: %v", err)
}
if len(cleared.Story.MediaAreas) != 0 {
t.Fatalf("cleared story media areas = %+v, want none", cleared.Story.MediaAreas)
}
}
func TestStoryStoreForwardRoundTripEditAndClone(t *testing.T) {
ctx := context.Background()
store := NewStoryStore()
owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1002}
source := domain.Peer{Type: domain.PeerTypeUser, ID: 2002}
created, err := store.CreateStory(ctx, domain.StoryCreateRequest{
Owner: owner,
RandomID: 202,
Date: 200,
Period: 900,
Public: true,
Forward: &domain.StoryForward{
From: source,
StoryID: 7,
Modified: true,
},
})
if err != nil {
t.Fatalf("create story: %v", err)
}
assertDomainStoryForward(t, created.Story, source, 7, true)
created.Story.Forward.From.ID = 9999
created.Story.Forward.StoryID = 99
list, err := store.GetStoriesByID(ctx, owner.ID, owner, []int{created.Story.ID}, 201)
if err != nil {
t.Fatalf("get story by id: %v", err)
}
if len(list.Stories) != 1 {
t.Fatalf("stories by id = %+v, want one story", list.Stories)
}
assertDomainStoryForward(t, list.Stories[0], source, 7, true)
edited, err := store.EditStory(ctx, domain.StoryEditRequest{
Owner: owner,
ID: created.Story.ID,
UpdateCaption: true,
Caption: "edited repost caption",
})
if err != nil {
t.Fatalf("edit story caption: %v", err)
}
assertDomainStoryForward(t, edited.Story, source, 7, true)
edited.Story.Forward.From.ID = 9998
list, err = store.GetStoriesByID(ctx, owner.ID, owner, []int{created.Story.ID}, 202)
if err != nil {
t.Fatalf("get edited story by id: %v", err)
}
assertDomainStoryForward(t, list.Stories[0], source, 7, true)
hidden, err := store.CreateStory(ctx, domain.StoryCreateRequest{
Owner: owner,
RandomID: 203,
Date: 203,
Period: 900,
Public: true,
Forward: &domain.StoryForward{
FromName: "Alice Hidden",
StoryID: 8,
},
})
if err != nil {
t.Fatalf("create hidden-author story: %v", err)
}
assertDomainStoryForwardName(t, hidden.Story, "Alice Hidden", 8, false)
hidden.Story.Forward.From = source
hidden.Story.Forward.FromName = "mutated"
hidden.Story.Forward.StoryID = 88
list, err = store.GetStoriesByID(ctx, owner.ID, owner, []int{hidden.Story.ID}, 204)
if err != nil {
t.Fatalf("get hidden-author story by id: %v", err)
}
if len(list.Stories) != 1 {
t.Fatalf("hidden-author stories by id = %+v, want one story", list.Stories)
}
assertDomainStoryForwardName(t, list.Stories[0], "Alice Hidden", 8, false)
editedHidden, err := store.EditStory(ctx, domain.StoryEditRequest{
Owner: owner,
ID: hidden.Story.ID,
UpdateCaption: true,
Caption: "edited hidden repost caption",
})
if err != nil {
t.Fatalf("edit hidden-author story caption: %v", err)
}
assertDomainStoryForwardName(t, editedHidden.Story, "Alice Hidden", 8, false)
}
func TestStoryStorePublicRepostForwardCountAndViewsList(t *testing.T) {
ctx := context.Background()
store := NewStoryStore()
sourceOwner := domain.Peer{Type: domain.PeerTypeUser, ID: 1101}
repostOwner := domain.Peer{Type: domain.PeerTypeUser, ID: 2202}
source, err := store.CreateStory(ctx, domain.StoryCreateRequest{
Owner: sourceOwner,
RandomID: 1101,
Date: 100,
Period: 86400,
Public: true,
})
if err != nil {
t.Fatalf("create source story: %v", err)
}
repost, err := store.CreateStory(ctx, domain.StoryCreateRequest{
Owner: repostOwner,
RandomID: 2202,
Date: 200,
Period: 86400,
Public: true,
Forward: &domain.StoryForward{
From: sourceOwner,
StoryID: source.Story.ID,
},
})
if err != nil {
t.Fatalf("create repost story: %v", err)
}
sourceList, err := store.GetStoriesByID(ctx, sourceOwner.ID, sourceOwner, []int{source.Story.ID}, 201)
if err != nil {
t.Fatalf("get source story: %v", err)
}
if len(sourceList.Stories) != 1 || sourceList.Stories[0].Views.ForwardsCount != 1 {
t.Fatalf("source story = %+v, want forwards_count=1", sourceList.Stories)
}
views, err := store.ListStoryViews(ctx, domain.StoryViewListRequest{
ViewerUserID: sourceOwner.ID,
Owner: sourceOwner,
StoryID: source.Story.ID,
Limit: 20,
ForwardsFirst: true,
})
if err != nil {
t.Fatalf("list story views: %v", err)
}
if views.Count != 1 || views.ForwardsCount != 1 || len(views.Views) != 1 || views.Views[0].Repost == nil {
t.Fatalf("views list = %+v, want one repost interaction", views)
}
if views.Views[0].Repost.Owner != repostOwner || views.Views[0].Repost.ID != repost.Story.ID {
t.Fatalf("repost interaction = %+v, want owner %+v story %d", views.Views[0].Repost, repostOwner, repost.Story.ID)
}
if _, err := store.DeleteStories(ctx, repostOwner, []int{repost.Story.ID}, 202); err != nil {
t.Fatalf("delete repost: %v", err)
}
sourceList, err = store.GetStoriesByID(ctx, sourceOwner.ID, sourceOwner, []int{source.Story.ID}, 202)
if err != nil {
t.Fatalf("get source story after delete: %v", err)
}
if len(sourceList.Stories) != 1 || sourceList.Stories[0].Views.ForwardsCount != 0 {
t.Fatalf("source story after delete = %+v, want forwards_count=0", sourceList.Stories)
}
}
func TestStoryStorePublicForwardListReturnsRepostsOnly(t *testing.T) {
ctx := context.Background()
store := NewStoryStore()
sourceOwner := domain.Peer{Type: domain.PeerTypeChannel, ID: 3311}
repostOwner := domain.Peer{Type: domain.PeerTypeChannel, ID: 4412}
privateRepostOwner := domain.Peer{Type: domain.PeerTypeChannel, ID: 4413}
source, err := store.CreateStory(ctx, domain.StoryCreateRequest{
Owner: sourceOwner,
RandomID: 331101,
Date: 100,
Period: 86400,
Public: true,
})
if err != nil {
t.Fatalf("create source story: %v", err)
}
repost, err := store.CreateStory(ctx, domain.StoryCreateRequest{
Owner: repostOwner,
RandomID: 441201,
Date: 200,
Period: 86400,
Public: true,
Forward: &domain.StoryForward{
From: sourceOwner,
StoryID: source.Story.ID,
},
})
if err != nil {
t.Fatalf("create public repost: %v", err)
}
hiddenRepost, err := store.CreateStory(ctx, domain.StoryCreateRequest{
Owner: repostOwner,
RandomID: 441202,
Date: 202,
Period: 86400,
Public: true,
Forward: &domain.StoryForward{
Source: sourceOwner,
FromName: "Hidden Source",
StoryID: source.Story.ID,
},
})
if err != nil {
t.Fatalf("create hidden-source public repost: %v", err)
}
if _, err := store.CreateStory(ctx, domain.StoryCreateRequest{
Owner: privateRepostOwner,
RandomID: 441301,
Date: 201,
Period: 86400,
Public: false,
Forward: &domain.StoryForward{
From: sourceOwner,
StoryID: source.Story.ID,
},
}); err != nil {
t.Fatalf("create private repost: %v", err)
}
list, err := store.ListStoryPublicForwards(ctx, domain.StoryPublicForwardListRequest{
ViewerUserID: sourceOwner.ID,
Owner: sourceOwner,
StoryID: source.Story.ID,
Limit: 20,
})
if err != nil {
t.Fatalf("list story public forwards: %v", err)
}
if list.Count != 2 || len(list.Forwards) != 2 || list.Forwards[0].Repost == nil || list.Forwards[1].Repost == nil {
t.Fatalf("public forwards = %+v, want two public reposts", list)
}
if list.Forwards[0].Repost.ID != hiddenRepost.Story.ID ||
list.Forwards[0].Repost.Forward == nil ||
list.Forwards[0].Repost.Forward.From != (domain.Peer{}) ||
list.Forwards[0].Repost.Forward.Source != sourceOwner ||
list.Forwards[0].Repost.Forward.FromName != "Hidden Source" {
t.Fatalf("hidden public forward repost = %+v, want from_name-only header with source %+v", list.Forwards[0].Repost, sourceOwner)
}
if list.Forwards[1].Repost.Owner != repostOwner || list.Forwards[1].Repost.ID != repost.Story.ID {
t.Fatalf("public forward repost = %+v, want owner %+v story %d", list.Forwards[1].Repost, repostOwner, repost.Story.ID)
}
if _, err := store.DeleteStories(ctx, repostOwner, []int{repost.Story.ID}, 202); err != nil {
t.Fatalf("delete repost: %v", err)
}
afterVisibleDelete, err := store.ListStoryPublicForwards(ctx, domain.StoryPublicForwardListRequest{
ViewerUserID: sourceOwner.ID,
Owner: sourceOwner,
StoryID: source.Story.ID,
Limit: 20,
})
if err != nil {
t.Fatalf("list story public forwards after delete: %v", err)
}
if afterVisibleDelete.Count != 1 || len(afterVisibleDelete.Forwards) != 1 || afterVisibleDelete.Forwards[0].Repost == nil ||
afterVisibleDelete.Forwards[0].Repost.ID != hiddenRepost.Story.ID {
t.Fatalf("public forwards after visible delete = %+v, want hidden repost only", afterVisibleDelete)
}
if _, err := store.DeleteStories(ctx, repostOwner, []int{hiddenRepost.Story.ID}, 203); err != nil {
t.Fatalf("delete hidden repost: %v", err)
}
empty, err := store.ListStoryPublicForwards(ctx, domain.StoryPublicForwardListRequest{
ViewerUserID: sourceOwner.ID,
Owner: sourceOwner,
StoryID: source.Story.ID,
Limit: 20,
})
if err != nil {
t.Fatalf("list story public forwards after hidden delete: %v", err)
}
if empty.Count != 0 || len(empty.Forwards) != 0 {
t.Fatalf("public forwards after delete = %+v, want empty", empty)
}
}
func TestStoryStorePublicRepostStoryReactionsList(t *testing.T) {
ctx := context.Background()
store := NewStoryStore()
sourceOwner := domain.Peer{Type: domain.PeerTypeChannel, ID: 3301}
repostOwner := domain.Peer{Type: domain.PeerTypeUser, ID: 4402}
source, err := store.CreateStory(ctx, domain.StoryCreateRequest{
Owner: sourceOwner,
RandomID: 3301,
Date: 100,
Period: 86400,
Public: true,
})
if err != nil {
t.Fatalf("create source story: %v", err)
}
if _, err := store.SetReaction(ctx, 5503, sourceOwner, source.Story.ID, &domain.MessageReaction{
Type: domain.MessageReactionEmoji,
Emoticon: "🔥",
}, 150); err != nil {
t.Fatalf("set reaction: %v", err)
}
repost, err := store.CreateStory(ctx, domain.StoryCreateRequest{
Owner: repostOwner,
RandomID: 4402,
Date: 200,
Period: 86400,
Public: true,
Forward: &domain.StoryForward{
From: sourceOwner,
StoryID: source.Story.ID,
},
})
if err != nil {
t.Fatalf("create repost story: %v", err)
}
list, err := store.ListStoryReactions(ctx, domain.StoryReactionListRequest{
ViewerUserID: sourceOwner.ID,
Owner: sourceOwner,
StoryID: source.Story.ID,
Limit: 20,
ForwardsFirst: true,
})
if err != nil {
t.Fatalf("list story reactions: %v", err)
}
if list.Count != 2 || len(list.Reactions) != 2 || list.Reactions[0].Repost == nil {
t.Fatalf("reaction list = %+v, want repost first plus reaction", list)
}
if list.Reactions[0].Repost.Owner != repostOwner || list.Reactions[0].Repost.ID != repost.Story.ID {
t.Fatalf("repost reaction = %+v, want owner %+v story %d", list.Reactions[0].Repost, repostOwner, repost.Story.ID)
}
filtered, err := store.ListStoryReactions(ctx, domain.StoryReactionListRequest{
ViewerUserID: sourceOwner.ID,
Owner: sourceOwner,
StoryID: source.Story.ID,
Limit: 20,
Reaction: &domain.MessageReaction{
Type: domain.MessageReactionEmoji,
Emoticon: "🔥",
},
})
if err != nil {
t.Fatalf("list filtered story reactions: %v", err)
}
if filtered.Count != 1 || len(filtered.Reactions) != 1 || filtered.Reactions[0].Repost != nil || filtered.Reactions[0].ViewerID != 5503 {
t.Fatalf("filtered reaction list = %+v, want only emoji reactor", filtered)
}
if _, err := store.DeleteStories(ctx, repostOwner, []int{repost.Story.ID}, 202); err != nil {
t.Fatalf("delete repost: %v", err)
}
list, err = store.ListStoryReactions(ctx, domain.StoryReactionListRequest{
ViewerUserID: sourceOwner.ID,
Owner: sourceOwner,
StoryID: source.Story.ID,
Limit: 20,
ForwardsFirst: true,
})
if err != nil {
t.Fatalf("list story reactions after delete: %v", err)
}
if list.Count != 1 || len(list.Reactions) != 1 || list.Reactions[0].Repost != nil {
t.Fatalf("reaction list after delete = %+v, want only durable reaction", list)
}
}
func TestStoryStoreListStoriesArchiveCountAndSeek(t *testing.T) {
ctx := context.Background()
store := NewStoryStore()
owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1001}
fixtures := []domain.Story{
{Owner: owner, ID: 1, Date: 101, ExpireDate: 900, Public: true},
{Owner: owner, ID: 2, Date: 102, ExpireDate: 1200, Public: true},
{Owner: owner, ID: 3, Date: 103, ExpireDate: 900, Public: true, Pinned: true},
{Owner: owner, ID: 4, Date: 104, ExpireDate: 900, Public: true, Deleted: true},
{Owner: owner, ID: 5, Date: 105, ExpireDate: 900, Public: true},
{Owner: domain.Peer{Type: domain.PeerTypeUser, ID: 1002}, ID: 1, Date: 106, ExpireDate: 900, Public: true},
}
for _, story := range fixtures {
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: story}); err != nil {
t.Fatalf("upsert story %d/%d: %v", story.Owner.ID, story.ID, err)
}
}
countOnly, err := store.ListStoriesArchive(ctx, owner.ID, owner, 0, 0, 1000)
if err != nil {
t.Fatalf("count archive stories: %v", err)
}
if countOnly.Count != 3 || len(countOnly.Stories) != 0 {
t.Fatalf("count-only archive = %+v, want count 3 and no page", countOnly)
}
first, err := store.ListStoriesArchive(ctx, owner.ID, owner, 0, 2, 1000)
if err != nil {
t.Fatalf("list archive first page: %v", err)
}
if first.Count != 3 || !sameStoryIDs(storyIDs(first.Stories), []int{5, 3}) {
t.Fatalf("first archive page = count %d ids %v, want count 3 ids 5,3", first.Count, storyIDs(first.Stories))
}
if !first.Stories[1].Pinned || !first.Stories[0].Out || !first.Stories[1].Out {
t.Fatalf("first archive stories = %+v, want pinned expired retained and owner out=true", first.Stories)
}
second, err := store.ListStoriesArchive(ctx, owner.ID, owner, 3, 2, 1000)
if err != nil {
t.Fatalf("list archive second page: %v", err)
}
if second.Count != 3 || !sameStoryIDs(storyIDs(second.Stories), []int{1}) {
t.Fatalf("second archive page = count %d ids %v, want count 3 ids 1", second.Count, storyIDs(second.Stories))
}
}
func TestStoryStoreListOwnerActiveStoriesReturnsFanoutSnapshots(t *testing.T) {
ctx := context.Background()
store := NewStoryStore()
owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1001}
other := domain.Peer{Type: domain.PeerTypeUser, ID: 1002}
reaction := &domain.MessageReaction{Type: domain.MessageReactionEmoji, Emoticon: "👍"}
for _, story := range []domain.Story{
{
Owner: owner,
ID: 1,
Date: 100,
ExpireDate: 1000,
CloseFriends: true,
Out: true,
Views: domain.StoryViews{ViewsCount: 1, HasViewers: true, RecentViewers: []int64{2001}},
SentReaction: reaction,
},
{Owner: owner, ID: 2, Date: 101, ExpireDate: 90, CloseFriends: true},
{Owner: owner, ID: 3, Date: 102, ExpireDate: 1000, CloseFriends: true, Deleted: true},
{Owner: other, ID: 1, Date: 103, ExpireDate: 1000, CloseFriends: true},
} {
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: story}); err != nil {
t.Fatalf("upsert story %d/%d: %v", story.Owner.ID, story.ID, err)
}
}
list, err := store.ListOwnerActiveStories(ctx, owner, 200, 100)
if err != nil {
t.Fatalf("list owner active stories: %v", err)
}
if len(list.Stories) != 1 || list.Stories[0].ID != 1 {
t.Fatalf("owner active stories = %+v, want only story 1", list.Stories)
}
story := list.Stories[0]
if story.Out || story.Views.HasViewers || story.Views.ViewsCount != 0 || story.SentReaction != nil {
t.Fatalf("fanout snapshot = %+v, want no out/views/reaction", story)
}
}
func testDomainStoryMediaArea(emoticon string, x float64) domain.StoryMediaArea {
return domain.StoryMediaArea{
Kind: domain.StoryMediaAreaSuggestedReaction,
Coordinates: domain.StoryMediaAreaCoordinates{
X: x,
Y: 10,
W: 10,
H: 10,
Rotation: 15,
Radius: 8,
HasRadius: true,
},
Dark: true,
Flipped: true,
Reaction: &domain.MessageReaction{Type: domain.MessageReactionEmoji, Emoticon: emoticon},
}
}
func assertDomainStoryMediaArea(t *testing.T, story domain.Story, wantEmoticon string, wantX float64) {
t.Helper()
if len(story.MediaAreas) != 1 {
t.Fatalf("story media areas = %+v, want one area", story.MediaAreas)
}
area := story.MediaAreas[0]
if area.Kind != domain.StoryMediaAreaSuggestedReaction || area.Reaction == nil {
t.Fatalf("story media area = %+v, want suggested reaction", area)
}
if area.Reaction.Type != domain.MessageReactionEmoji || area.Reaction.Emoticon != wantEmoticon {
t.Fatalf("story media area reaction = %+v, want emoji %q", area.Reaction, wantEmoticon)
}
if area.Coordinates.X != wantX || area.Coordinates.Y != 10 || area.Coordinates.W != 10 || area.Coordinates.H != 10 ||
area.Coordinates.Rotation != 15 || !area.Coordinates.HasRadius || area.Coordinates.Radius != 8 {
t.Fatalf("story media area coordinates = %+v, want x %v y/w/h 10 rotation 15 radius 8", area.Coordinates, wantX)
}
if !area.Dark || !area.Flipped {
t.Fatalf("story media area flags = dark %v flipped %v, want true/true", area.Dark, area.Flipped)
}
}
func testDomainStoryURLMediaArea(url string, x float64) domain.StoryMediaArea {
return domain.StoryMediaArea{
Kind: domain.StoryMediaAreaURL,
Coordinates: domain.StoryMediaAreaCoordinates{
X: x,
Y: 10,
W: 20,
H: 10,
Rotation: 15,
},
URL: url,
}
}
func assertDomainStoryURLMediaArea(t *testing.T, story domain.Story, wantURL string, wantX float64) {
t.Helper()
if len(story.MediaAreas) != 1 {
t.Fatalf("story media areas = %+v, want one url area", story.MediaAreas)
}
area := story.MediaAreas[0]
if area.Kind != domain.StoryMediaAreaURL || area.URL != wantURL {
t.Fatalf("story media area = %+v, want url %q", area, wantURL)
}
if area.Coordinates.X != wantX || area.Coordinates.Y != 10 || area.Coordinates.W != 20 || area.Coordinates.H != 10 ||
area.Coordinates.Rotation != 15 {
t.Fatalf("story media area coordinates = %+v, want x %v y 10 w 20 h 10 rotation 15", area.Coordinates, wantX)
}
}
func testDomainStoryGeoPointMediaArea(lat, long, x float64) domain.StoryMediaArea {
return domain.StoryMediaArea{
Kind: domain.StoryMediaAreaGeoPoint,
Coordinates: domain.StoryMediaAreaCoordinates{
X: x,
Y: 12,
W: 22,
H: 11,
Rotation: 20,
},
Geo: &domain.MessageGeoPoint{
Lat: lat,
Long: long,
AccessHash: 123456,
},
GeoAddress: &domain.StoryGeoPointAddress{
CountryISO2: "CN",
State: "Shanghai",
City: "Shanghai",
Street: "People Square",
},
}
}
func assertDomainStoryGeoPointMediaArea(t *testing.T, story domain.Story, wantLat, wantLong, wantX float64) {
t.Helper()
if len(story.MediaAreas) != 1 {
t.Fatalf("story media areas = %+v, want one geo area", story.MediaAreas)
}
area := story.MediaAreas[0]
if area.Kind != domain.StoryMediaAreaGeoPoint || area.Geo == nil {
t.Fatalf("story media area = %+v, want geo point", area)
}
if area.Geo.Lat != wantLat || area.Geo.Long != wantLong || area.Geo.AccessHash != 123456 {
t.Fatalf("story media area geo = %+v, want lat %v long %v access_hash 123456", area.Geo, wantLat, wantLong)
}
if area.GeoAddress == nil || area.GeoAddress.CountryISO2 != "CN" || area.GeoAddress.State != "Shanghai" || area.GeoAddress.City != "Shanghai" || area.GeoAddress.Street != "People Square" {
t.Fatalf("story media area address = %+v, want CN/Shanghai/Shanghai/People Square", area.GeoAddress)
}
if area.Coordinates.X != wantX || area.Coordinates.Y != 12 || area.Coordinates.W != 22 || area.Coordinates.H != 11 ||
area.Coordinates.Rotation != 20 {
t.Fatalf("story media area coordinates = %+v, want x %v y 12 w 22 h 11 rotation 20", area.Coordinates, wantX)
}
}
func testDomainStoryVenueMediaArea(title string, lat, long, x float64) domain.StoryMediaArea {
venue := &domain.MessageVenue{
Geo: domain.MessageGeoPoint{Lat: lat, Long: long, AccessHash: 123456},
Title: title,
Address: "Inline Street",
Provider: "gplaces",
VenueID: "venue-id",
VenueType: "cafe",
}
return domain.StoryMediaArea{
Kind: domain.StoryMediaAreaVenue,
Coordinates: domain.StoryMediaAreaCoordinates{
X: x,
Y: 13,
W: 23,
H: 12,
Rotation: 22,
},
Geo: &venue.Geo,
Venue: venue,
}
}
func assertDomainStoryVenueMediaArea(t *testing.T, story domain.Story, wantTitle string, wantLat, wantLong, wantX float64) {
t.Helper()
if len(story.MediaAreas) != 1 {
t.Fatalf("story media areas = %+v, want one venue area", story.MediaAreas)
}
area := story.MediaAreas[0]
if area.Kind != domain.StoryMediaAreaVenue || area.Venue == nil || area.Geo == nil {
t.Fatalf("story media area = %+v, want venue", area)
}
if area.Venue.Title != wantTitle || area.Venue.Address != "Inline Street" || area.Venue.Provider != "gplaces" || area.Venue.VenueID != "venue-id" || area.Venue.VenueType != "cafe" {
t.Fatalf("story venue = %+v, want %q/Inline Street/gplaces/venue-id/cafe", area.Venue, wantTitle)
}
if area.Venue.Geo.Lat != wantLat || area.Venue.Geo.Long != wantLong || area.Venue.Geo.AccessHash != 123456 ||
area.Geo.Lat != wantLat || area.Geo.Long != wantLong || area.Geo.AccessHash != 123456 {
t.Fatalf("story venue geo = venue %+v area %+v, want lat %v long %v access_hash 123456", area.Venue.Geo, area.Geo, wantLat, wantLong)
}
if area.Coordinates.X != wantX || area.Coordinates.Y != 13 || area.Coordinates.W != 23 || area.Coordinates.H != 12 ||
area.Coordinates.Rotation != 22 {
t.Fatalf("story media area coordinates = %+v, want x %v y 13 w 23 h 12 rotation 22", area.Coordinates, wantX)
}
}
func testDomainStoryWeatherMediaArea(emoji string, temperatureC float64, color int, x float64) domain.StoryMediaArea {
return domain.StoryMediaArea{
Kind: domain.StoryMediaAreaWeather,
Coordinates: domain.StoryMediaAreaCoordinates{
X: x,
Y: 14,
W: 24,
H: 12,
Rotation: 25,
},
WeatherEmoji: emoji,
TemperatureC: temperatureC,
Color: color,
}
}
func assertDomainStoryWeatherMediaArea(t *testing.T, story domain.Story, wantEmoji string, wantTemperatureC float64, wantColor int, wantX float64) {
t.Helper()
if len(story.MediaAreas) != 1 {
t.Fatalf("story media areas = %+v, want one weather area", story.MediaAreas)
}
area := story.MediaAreas[0]
if area.Kind != domain.StoryMediaAreaWeather {
t.Fatalf("story media area = %+v, want weather", area)
}
if area.WeatherEmoji != wantEmoji || area.TemperatureC != wantTemperatureC || area.Color != wantColor {
t.Fatalf("story weather area = emoji %q temp %v color %d, want %q/%v/%d", area.WeatherEmoji, area.TemperatureC, area.Color, wantEmoji, wantTemperatureC, wantColor)
}
if area.Coordinates.X != wantX || area.Coordinates.Y != 14 || area.Coordinates.W != 24 || area.Coordinates.H != 12 ||
area.Coordinates.Rotation != 25 {
t.Fatalf("story media area coordinates = %+v, want x %v y 14 w 24 h 12 rotation 25", area.Coordinates, wantX)
}
}
func testDomainStoryChannelPostMediaArea(channelID int64, msgID int, x float64) domain.StoryMediaArea {
return domain.StoryMediaArea{
Kind: domain.StoryMediaAreaChannelPost,
Coordinates: domain.StoryMediaAreaCoordinates{
X: x,
Y: 15,
W: 25,
H: 12,
Rotation: 27,
},
ChannelID: channelID,
MsgID: msgID,
}
}
func assertDomainStoryChannelPostMediaArea(t *testing.T, story domain.Story, wantChannelID int64, wantMsgID int, wantX float64) {
t.Helper()
if len(story.MediaAreas) != 1 {
t.Fatalf("story media areas = %+v, want one channel post area", story.MediaAreas)
}
area := story.MediaAreas[0]
if area.Kind != domain.StoryMediaAreaChannelPost || area.ChannelID != wantChannelID || area.MsgID != wantMsgID {
t.Fatalf("story channel post area = %+v, want channel %d msg %d", area, wantChannelID, wantMsgID)
}
if area.Coordinates.X != wantX || area.Coordinates.Y != 15 || area.Coordinates.W != 25 || area.Coordinates.H != 12 ||
area.Coordinates.Rotation != 27 {
t.Fatalf("story media area coordinates = %+v, want x %v y 15 w 25 h 12 rotation 27", area.Coordinates, wantX)
}
}
func testDomainStoryStarGiftMediaArea(slug string, x float64) domain.StoryMediaArea {
return domain.StoryMediaArea{
Kind: domain.StoryMediaAreaStarGift,
Coordinates: domain.StoryMediaAreaCoordinates{
X: x,
Y: 16,
W: 26,
H: 12,
Rotation: 29,
},
StarGiftSlug: slug,
}
}
func assertDomainStoryStarGiftMediaArea(t *testing.T, story domain.Story, wantSlug string, wantX float64) {
t.Helper()
if len(story.MediaAreas) != 1 {
t.Fatalf("story media areas = %+v, want one star gift area", story.MediaAreas)
}
area := story.MediaAreas[0]
if area.Kind != domain.StoryMediaAreaStarGift || area.StarGiftSlug != wantSlug {
t.Fatalf("story star gift area = %+v, want slug %q", area, wantSlug)
}
if area.Coordinates.X != wantX || area.Coordinates.Y != 16 || area.Coordinates.W != 26 || area.Coordinates.H != 12 ||
area.Coordinates.Rotation != 29 {
t.Fatalf("story media area coordinates = %+v, want x %v y 16 w 26 h 12 rotation 29", area.Coordinates, wantX)
}
}
func assertDomainStoryForward(t *testing.T, story domain.Story, wantSource domain.Peer, wantStoryID int, wantModified bool) {
t.Helper()
if story.Forward == nil {
t.Fatalf("story forward is nil, want source %+v story %d", wantSource, wantStoryID)
}
if story.Forward.From != wantSource || story.Forward.StoryID != wantStoryID || story.Forward.Modified != wantModified {
t.Fatalf("story forward = %+v, want source %+v story %d modified %v", story.Forward, wantSource, wantStoryID, wantModified)
}
}
func assertDomainStoryForwardName(t *testing.T, story domain.Story, wantName string, wantStoryID int, wantModified bool) {
t.Helper()
if story.Forward == nil {
t.Fatalf("story forward is nil, want from_name %q story %d", wantName, wantStoryID)
}
if story.Forward.From != (domain.Peer{}) || story.Forward.FromName != wantName ||
story.Forward.StoryID != wantStoryID || story.Forward.Modified != wantModified {
t.Fatalf("story forward = %+v, want from_name %q story %d modified %v", story.Forward, wantName, wantStoryID, wantModified)
}
}
func TestStoryStoreViewsIdempotentAndReactionReplace(t *testing.T) {
ctx := context.Background()
store := NewStoryStore()
owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1001}
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{
Owner: owner,
ID: 1,
Date: 100,
ExpireDate: 1000,
Public: true,
}}); err != nil {
t.Fatalf("upsert story: %v", err)
}
if created, err := store.IncrementViews(ctx, 2001, owner, []int{1, 1}, 200); err != nil || created != 1 {
t.Fatalf("increment views first = %d, %v; want 1 nil", created, err)
}
if created, err := store.IncrementViews(ctx, 2001, owner, []int{1}, 201); err != nil || created != 0 {
t.Fatalf("increment views duplicate = %d, %v; want 0 nil", created, err)
}
if _, err := store.IncrementViews(ctx, 2001, owner, nil, 201); !errors.Is(err, domain.ErrStoryIDInvalid) {
t.Fatalf("empty increment views err = %v, want ErrStoryIDInvalid", err)
}
like := &domain.MessageReaction{Type: domain.MessageReactionEmoji, Emoticon: "👍"}
fire := &domain.MessageReaction{Type: domain.MessageReactionEmoji, Emoticon: "🔥"}
res, err := store.SetReaction(ctx, 2001, owner, 1, like, 202)
if err != nil {
t.Fatalf("set reaction: %v", err)
}
if !res.Changed || res.Story.Views.ViewsCount != 1 || res.Story.Views.ReactionsCount != 1 {
t.Fatalf("reaction result = %+v, want one view and one reaction", res)
}
res, err = store.SetReaction(ctx, 2001, owner, 1, fire, 203)
if err != nil {
t.Fatalf("replace reaction: %v", err)
}
if !res.Changed || res.Story.Views.ReactionsCount != 1 || len(res.Story.Views.Reactions) != 1 || res.Story.Views.Reactions[0].Reaction.Emoticon != "🔥" {
t.Fatalf("replace result = %+v, want only fire reaction", res)
}
res, err = store.SetReaction(ctx, 2001, owner, 1, fire, 204)
if err != nil {
t.Fatalf("retry same reaction: %v", err)
}
if res.Changed || res.Date != 203 || res.Story.Views.ReactionsCount != 1 || len(res.Story.Views.Reactions) != 1 || res.Story.Views.Reactions[0].Reaction.Emoticon != "🔥" {
t.Fatalf("retry same reaction result = %+v, want unchanged fire reaction at original date", res)
}
reactions, err := store.ListStoryReactions(ctx, domain.StoryReactionListRequest{
ViewerUserID: owner.ID,
Owner: owner,
StoryID: 1,
Limit: 10,
})
if err != nil {
t.Fatalf("list reactions after retry: %v", err)
}
if reactions.Count != 1 || len(reactions.Reactions) != 1 || reactions.Reactions[0].ViewerID != 2001 || reactions.Reactions[0].Date != 203 {
t.Fatalf("reactions after retry = %+v, want original reaction date", reactions)
}
custom := &domain.MessageReaction{Type: domain.MessageReactionCustomEmoji, DocumentID: 12345}
res, err = store.SetReaction(ctx, 2001, owner, 1, custom, 204)
if err != nil {
t.Fatalf("replace with custom reaction: %v", err)
}
if !res.Changed || res.Story.Views.ReactionsCount != 1 || len(res.Story.Views.Reactions) != 1 || res.Story.Views.Reactions[0].Reaction.DocumentID != 12345 {
t.Fatalf("custom replace result = %+v, want one custom reaction", res)
}
res, err = store.SetReaction(ctx, 2001, owner, 1, custom, 205)
if err != nil {
t.Fatalf("retry same custom reaction: %v", err)
}
if res.Changed || res.Date != 204 || res.Story.Views.ReactionsCount != 1 || len(res.Story.Views.Reactions) != 1 || res.Story.Views.Reactions[0].Reaction.DocumentID != 12345 {
t.Fatalf("retry same custom reaction result = %+v, want unchanged custom reaction at original date", res)
}
reactions, err = store.ListStoryReactions(ctx, domain.StoryReactionListRequest{
ViewerUserID: owner.ID,
Owner: owner,
StoryID: 1,
Reaction: custom,
Limit: 10,
})
if err != nil {
t.Fatalf("list custom reactions: %v", err)
}
if reactions.Count != 1 || len(reactions.Reactions) != 1 || reactions.Reactions[0].ViewerID != 2001 || reactions.Reactions[0].Date != 204 || reactions.Reactions[0].Reaction == nil || reactions.Reactions[0].Reaction.DocumentID != 12345 {
t.Fatalf("custom reactions = %+v, want original custom reaction date", reactions)
}
if _, err := store.ListStoryViews(ctx, domain.StoryViewListRequest{
ViewerUserID: owner.ID,
Owner: owner,
StoryID: 1,
Offset: "bad",
Limit: 10,
}); !errors.Is(err, domain.ErrStoryOffsetInvalid) {
t.Fatalf("bad story views offset err = %v, want ErrStoryOffsetInvalid", err)
}
if _, err := store.ListStoryReactions(ctx, domain.StoryReactionListRequest{
ViewerUserID: owner.ID,
Owner: owner,
StoryID: 1,
Offset: "1:203:2001",
Limit: 10,
}); !errors.Is(err, domain.ErrStoryOffsetInvalid) {
t.Fatalf("bad story reactions offset err = %v, want ErrStoryOffsetInvalid", err)
}
res, err = store.SetReaction(ctx, 2001, owner, 1, nil, 206)
if err != nil {
t.Fatalf("clear reaction: %v", err)
}
if !res.Changed || res.Story.Views.ReactionsCount != 0 || len(res.Story.Views.Reactions) != 0 {
t.Fatalf("clear result = %+v, want no reactions", res)
}
}
func TestStoryStoreExpiredUnpinnedStoriesDoNotAcceptNewInteractions(t *testing.T) {
ctx := context.Background()
store := NewStoryStore()
owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1001}
for _, story := range []domain.Story{
{Owner: owner, ID: 1, Date: 100, ExpireDate: 150, Public: true},
{Owner: owner, ID: 2, Date: 101, ExpireDate: 150, Public: true, Pinned: true},
} {
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: story}); err != nil {
t.Fatalf("upsert story %d: %v", story.ID, err)
}
}
if created, err := store.IncrementViews(ctx, 2001, owner, []int{1}, 200); err != nil || created != 0 {
t.Fatalf("expired unpinned increment = %d, %v; want 0 nil", created, err)
}
if _, err := store.SetReaction(ctx, 2001, owner, 1, &domain.MessageReaction{Type: domain.MessageReactionEmoji, Emoticon: "like"}, 200); !errors.Is(err, domain.ErrStoryNotFound) {
t.Fatalf("expired unpinned reaction err = %v, want ErrStoryNotFound", err)
}
if created, err := store.IncrementViews(ctx, 2001, owner, []int{2}, 200); err != nil || created != 1 {
t.Fatalf("expired pinned increment = %d, %v; want 1 nil", created, err)
}
res, err := store.SetReaction(ctx, 2002, owner, 2, &domain.MessageReaction{Type: domain.MessageReactionEmoji, Emoticon: "fire"}, 201)
if err != nil {
t.Fatalf("expired pinned reaction: %v", err)
}
if !res.Changed || res.Story.Views.ViewsCount != 2 || res.Story.Views.ReactionsCount != 1 {
t.Fatalf("expired pinned reaction result = %+v, want accepted profile interaction", res)
}
}
func TestStoryStoreListsViewsAndReactions(t *testing.T) {
ctx := context.Background()
store := NewStoryStore()
owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1001}
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{
Owner: owner,
ID: 1,
Date: 100,
ExpireDate: 1000,
Public: true,
}}); err != nil {
t.Fatalf("upsert story: %v", err)
}
if _, err := store.IncrementViews(ctx, 2001, owner, []int{1}, 201); err != nil {
t.Fatalf("increment viewer 2001: %v", err)
}
if _, err := store.IncrementViews(ctx, 2004, owner, []int{1}, 204); err != nil {
t.Fatalf("increment viewer 2004: %v", err)
}
like := &domain.MessageReaction{Type: domain.MessageReactionEmoji, Emoticon: "like"}
fire := &domain.MessageReaction{Type: domain.MessageReactionEmoji, Emoticon: "fire"}
if _, err := store.SetReaction(ctx, 2002, owner, 1, like, 202); err != nil {
t.Fatalf("set like reaction: %v", err)
}
if _, err := store.SetReaction(ctx, 2003, owner, 1, fire, 203); err != nil {
t.Fatalf("set fire reaction: %v", err)
}
viewerIDs, err := store.ListStoryViewerIDs(ctx, owner, 1, 3)
if err != nil {
t.Fatalf("list story viewer ids: %v", err)
}
if len(viewerIDs) != 3 || viewerIDs[0] != 2001 || viewerIDs[1] != 2002 || viewerIDs[2] != 2003 {
t.Fatalf("viewer ids = %v, want first three ascending ids", viewerIDs)
}
first, err := store.ListStoryViews(ctx, domain.StoryViewListRequest{
ViewerUserID: owner.ID,
Owner: owner,
StoryID: 1,
Limit: 2,
ReactionsFirst: true,
})
if err != nil {
t.Fatalf("list views first page: %v", err)
}
if first.Count != 4 || first.ViewsCount != 4 || first.ReactionsCount != 2 || len(first.Views) != 2 || first.NextOffset == "" {
t.Fatalf("first page = %+v, want 4 total, 2 reactions, 2 rows and next offset", first)
}
if first.Views[0].ViewerID != 2003 || first.Views[1].ViewerID != 2002 {
t.Fatalf("first page viewers = %+v, want reactions newest first", first.Views)
}
second, err := store.ListStoryViews(ctx, domain.StoryViewListRequest{
ViewerUserID: owner.ID,
Owner: owner,
StoryID: 1,
Limit: 2,
ReactionsFirst: true,
Offset: first.NextOffset,
})
if err != nil {
t.Fatalf("list views second page: %v", err)
}
if len(second.Views) != 2 || second.NextOffset != "" || second.Views[0].ViewerID != 2004 || second.Views[1].ViewerID != 2001 {
t.Fatalf("second page = %+v, want remaining non-reaction viewers", second)
}
reactions, err := store.ListStoryReactions(ctx, domain.StoryReactionListRequest{
ViewerUserID: owner.ID,
Owner: owner,
StoryID: 1,
Limit: 10,
})
if err != nil {
t.Fatalf("list reactions: %v", err)
}
if reactions.Count != 2 || len(reactions.Reactions) != 2 || reactions.Reactions[0].ViewerID != 2003 {
t.Fatalf("reactions = %+v, want two newest reactions", reactions)
}
filtered, err := store.ListStoryReactions(ctx, domain.StoryReactionListRequest{
ViewerUserID: owner.ID,
Owner: owner,
StoryID: 1,
Reaction: fire,
Limit: 10,
})
if err != nil {
t.Fatalf("list filtered reactions: %v", err)
}
if filtered.Count != 1 || len(filtered.Reactions) != 1 || filtered.Reactions[0].ViewerID != 2003 {
t.Fatalf("filtered reactions = %+v, want viewer 2003 only", filtered)
}
if _, err := store.SetReaction(ctx, 2003, owner, 1, nil, 205); err != nil {
t.Fatalf("clear reaction: %v", err)
}
reactions, err = store.ListStoryReactions(ctx, domain.StoryReactionListRequest{
ViewerUserID: owner.ID,
Owner: owner,
StoryID: 1,
Limit: 10,
})
if err != nil {
t.Fatalf("list reactions after clear: %v", err)
}
if reactions.Count != 1 || len(reactions.Reactions) != 1 || reactions.Reactions[0].ViewerID != 2002 {
t.Fatalf("reactions after clear = %+v, want only viewer 2002", reactions)
}
if _, err := store.DeleteStories(ctx, owner, []int{1}, 206); err != nil {
t.Fatalf("delete story: %v", err)
}
deletedViewerIDs, err := store.ListStoryViewerIDs(ctx, owner, 1, 10)
if err != nil {
t.Fatalf("list deleted story viewer ids: %v", err)
}
if len(deletedViewerIDs) != 4 || deletedViewerIDs[0] != 2001 || deletedViewerIDs[1] != 2002 || deletedViewerIDs[2] != 2003 || deletedViewerIDs[3] != 2004 {
t.Fatalf("deleted story viewer ids = %v, want all durable viewers ascending", deletedViewerIDs)
}
}
func TestStoryStoreViewerIDsIncludeCachedStoryExposure(t *testing.T) {
ctx := context.Background()
store := NewStoryStore()
owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1001}
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{
Owner: owner,
ID: 1,
Date: 100,
ExpireDate: 1000,
Public: true,
}}); err != nil {
t.Fatalf("upsert story: %v", err)
}
if list, err := store.ListActiveStories(ctx, 2005, false, 200, 10); err != nil || len(list.Stories) != 1 {
t.Fatalf("list active exposure = %+v err=%v, want one story", list, err)
}
if peerStories, err := store.GetPeerStories(ctx, 2006, owner, 200); err != nil || len(peerStories.Stories) != 1 {
t.Fatalf("peer stories exposure = %+v err=%v, want one story", peerStories, err)
}
if exact, err := store.GetStoriesByID(ctx, 2007, owner, []int{1}, 200); err != nil || len(exact.Stories) != 1 {
t.Fatalf("exact exposure = %+v err=%v, want one story", exact, err)
}
viewerIDs, err := store.ListStoryViewerIDs(ctx, owner, 1, 10)
if err != nil {
t.Fatalf("list story viewer ids: %v", err)
}
if len(viewerIDs) != 3 || viewerIDs[0] != 2005 || viewerIDs[1] != 2006 || viewerIDs[2] != 2007 {
t.Fatalf("viewer ids = %v, want exposure viewers ascending", viewerIDs)
}
views, err := store.ListStoryViews(ctx, domain.StoryViewListRequest{
ViewerUserID: owner.ID,
Owner: owner,
StoryID: 1,
Limit: 10,
})
if err != nil {
t.Fatalf("list story views: %v", err)
}
if views.Count != 0 || views.ViewsCount != 0 || len(views.Views) != 0 {
t.Fatalf("views = %+v, exposure must not count as a real view", views)
}
}
func TestStoryStoreListStoryViewsFiltersByContactsAndQuery(t *testing.T) {
ctx := context.Background()
store := NewStoryStore()
owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1001}
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{
Owner: owner,
ID: 1,
Date: 100,
ExpireDate: 1000,
Public: true,
}}); err != nil {
t.Fatalf("upsert story: %v", err)
}
store.SetStoryViewerProfiles(
domain.User{ID: 2001, FirstName: "Alice", LastName: "Viewer", Username: "alicev", Phone: "155501"},
domain.User{ID: 2002, FirstName: "Bob", LastName: "Stranger", Username: "bobstory", Phone: "155502"},
domain.User{ID: 2003, FirstName: "Carol", LastName: "Contact", Username: "carolstory", Phone: "155503"},
)
store.SetStoryViewerContacts(owner.ID,
domain.Contact{User: domain.User{ID: 2001, FirstName: "Alice", Username: "alicev"}, FirstName: "Close", LastName: "Friend", Phone: "7001"},
domain.Contact{User: domain.User{ID: 2003, FirstName: "Carol", Username: "carolstory"}, FirstName: "Work", LastName: "Carol", Phone: "7003"},
)
for _, item := range []struct {
viewerID int64
date int
}{
{viewerID: 2001, date: 201},
{viewerID: 2002, date: 202},
{viewerID: 2003, date: 203},
} {
if _, err := store.IncrementViews(ctx, item.viewerID, owner, []int{1}, item.date); err != nil {
t.Fatalf("increment viewer %d: %v", item.viewerID, err)
}
}
contactsOnly, err := store.ListStoryViews(ctx, domain.StoryViewListRequest{
ViewerUserID: owner.ID,
Owner: owner,
StoryID: 1,
Limit: 10,
JustContacts: true,
})
if err != nil {
t.Fatalf("list contacts-only: %v", err)
}
if contactsOnly.Count != 2 || contactsOnly.ViewsCount != 3 || len(contactsOnly.Views) != 2 || contactsOnly.Views[0].ViewerID != 2003 || contactsOnly.Views[1].ViewerID != 2001 {
t.Fatalf("contacts-only = %+v, want two contacts and total views 3", contactsOnly)
}
remark, err := store.ListStoryViews(ctx, domain.StoryViewListRequest{
ViewerUserID: owner.ID,
Owner: owner,
StoryID: 1,
Limit: 10,
Query: "close",
})
if err != nil {
t.Fatalf("list query remark: %v", err)
}
if remark.Count != 1 || len(remark.Views) != 1 || remark.Views[0].ViewerID != 2001 {
t.Fatalf("remark query = %+v, want viewer 2001", remark)
}
stranger, err := store.ListStoryViews(ctx, domain.StoryViewListRequest{
ViewerUserID: owner.ID,
Owner: owner,
StoryID: 1,
Limit: 10,
Query: "@bobstory",
})
if err != nil {
t.Fatalf("list query username: %v", err)
}
if stranger.Count != 1 || len(stranger.Views) != 1 || stranger.Views[0].ViewerID != 2002 {
t.Fatalf("username query = %+v, want viewer 2002", stranger)
}
hiddenAccountPhone, err := store.ListStoryViews(ctx, domain.StoryViewListRequest{
ViewerUserID: owner.ID,
Owner: owner,
StoryID: 1,
Limit: 10,
Query: "155502",
})
if err != nil {
t.Fatalf("list query hidden account phone: %v", err)
}
if hiddenAccountPhone.Count != 0 || len(hiddenAccountPhone.Views) != 0 {
t.Fatalf("hidden account phone query = %+v, want no match", hiddenAccountPhone)
}
knownContactPhone, err := store.ListStoryViews(ctx, domain.StoryViewListRequest{
ViewerUserID: owner.ID,
Owner: owner,
StoryID: 1,
Limit: 10,
Query: "7001",
})
if err != nil {
t.Fatalf("list query known contact phone: %v", err)
}
if knownContactPhone.Count != 1 || len(knownContactPhone.Views) != 1 || knownContactPhone.Views[0].ViewerID != 2001 {
t.Fatalf("known contact phone query = %+v, want viewer 2001", knownContactPhone)
}
intersection, err := store.ListStoryViews(ctx, domain.StoryViewListRequest{
ViewerUserID: owner.ID,
Owner: owner,
StoryID: 1,
Limit: 10,
Query: "bob",
JustContacts: true,
})
if err != nil {
t.Fatalf("list query contacts intersection: %v", err)
}
if intersection.Count != 0 || len(intersection.Views) != 0 {
t.Fatalf("intersection = %+v, want no contact match for bob", intersection)
}
}
func TestStoryStorePrivacyVisibility(t *testing.T) {
ctx := context.Background()
store := NewStoryStore()
owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1001}
selectedViewer := int64(2001)
stranger := int64(2002)
contactViewer := int64(2003)
excludedContact := int64(2004)
closeFriendViewer := int64(2005)
store.SetStoryViewerContacts(owner.ID,
domain.Contact{User: domain.User{ID: contactViewer, FirstName: "Contact"}},
domain.Contact{User: domain.User{ID: excludedContact, FirstName: "Excluded"}},
domain.Contact{User: domain.User{ID: closeFriendViewer, FirstName: "Close"}, CloseFriend: true},
)
stories := []domain.Story{
{
Owner: owner,
ID: 1,
Date: 100,
ExpireDate: 1000,
Public: true,
DisallowUserIDs: []int64{stranger},
PrivacyRules: []domain.PrivacyRule{
{Kind: domain.PrivacyRuleAllowAll},
{Kind: domain.PrivacyRuleDisallowUsers, UserIDs: []int64{stranger}},
},
},
{
Owner: owner,
ID: 2,
Date: 101,
ExpireDate: 1000,
SelectedContacts: true,
AllowUserIDs: []int64{selectedViewer},
PrivacyRules: []domain.PrivacyRule{
{Kind: domain.PrivacyRuleAllowUsers, UserIDs: []int64{selectedViewer}},
},
},
{
Owner: owner,
ID: 3,
Date: 102,
ExpireDate: 1000,
Contacts: true,
DisallowUserIDs: []int64{excludedContact},
PrivacyRules: []domain.PrivacyRule{
{Kind: domain.PrivacyRuleAllowContacts},
{Kind: domain.PrivacyRuleDisallowUsers, UserIDs: []int64{excludedContact}},
},
},
{
Owner: owner,
ID: 4,
Date: 103,
ExpireDate: 1000,
CloseFriends: true,
PrivacyRules: []domain.PrivacyRule{
{Kind: domain.PrivacyRuleAllowCloseFriends},
},
},
}
for _, story := range stories {
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: story}); err != nil {
t.Fatalf("upsert story %d: %v", story.ID, err)
}
}
selected, err := store.GetStoriesByID(ctx, selectedViewer, owner, []int{1, 2, 3, 4}, 200)
if err != nil {
t.Fatalf("selected viewer get stories: %v", err)
}
if gotIDs := storyIDs(selected.Stories); !sameStoryIDs(gotIDs, []int{1, 2}) {
t.Fatalf("selected viewer story ids = %v, want [1 2]", gotIDs)
}
hidden, err := store.GetStoriesByID(ctx, stranger, owner, []int{1, 2, 3, 4}, 200)
if err != nil {
t.Fatalf("stranger get stories: %v", err)
}
if len(hidden.Stories) != 0 {
t.Fatalf("stranger stories = %+v, want none", hidden.Stories)
}
contacts, err := store.GetStoriesByID(ctx, contactViewer, owner, []int{1, 2, 3, 4}, 200)
if err != nil {
t.Fatalf("contact get stories: %v", err)
}
if gotIDs := storyIDs(contacts.Stories); !sameStoryIDs(gotIDs, []int{1, 3}) {
t.Fatalf("contact story ids = %v, want [1 3]", gotIDs)
}
excluded, err := store.GetStoriesByID(ctx, excludedContact, owner, []int{3}, 200)
if err != nil {
t.Fatalf("excluded contact get stories: %v", err)
}
if len(excluded.Stories) != 0 {
t.Fatalf("excluded contact stories = %+v, want none", excluded.Stories)
}
closeFriend, err := store.GetStoriesByID(ctx, closeFriendViewer, owner, []int{1, 2, 3, 4}, 200)
if err != nil {
t.Fatalf("close friend get stories: %v", err)
}
if gotIDs := storyIDs(closeFriend.Stories); !sameStoryIDs(gotIDs, []int{1, 3, 4}) {
t.Fatalf("close friend story ids = %v, want [1 3 4]", gotIDs)
}
if created, err := store.IncrementViews(ctx, stranger, owner, []int{2}, 210); err != nil || created != 0 {
t.Fatalf("hidden increment views = %d, %v; want 0 nil", created, err)
}
if _, err := store.SetReaction(ctx, stranger, owner, 2, &domain.MessageReaction{Type: domain.MessageReactionEmoji, Emoticon: "like"}, 211); err != domain.ErrStoryNotFound {
t.Fatalf("hidden set reaction err = %v, want ErrStoryNotFound", err)
}
}
func TestStoryStoreCreateEditDeleteAndPinned(t *testing.T) {
ctx := context.Background()
store := NewStoryStore()
owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1001}
media := &domain.MessageMedia{Kind: domain.MessageMediaKindPhoto, Photo: &domain.Photo{ID: 77, AccessHash: 7, DCID: 2}}
created, err := store.CreateStory(ctx, domain.StoryCreateRequest{
Owner: owner,
RandomID: 9001,
Date: 100,
Period: 86400,
Public: true,
Pinned: true,
Caption: "first",
Media: media,
NoForwards: true,
})
if err != nil {
t.Fatalf("create story: %v", err)
}
if created.Duplicate || created.Story.ID != 1 || created.Story.ExpireDate != 86500 || !created.Story.Pinned {
t.Fatalf("created = %+v, want new pinned story id 1", created)
}
dup, err := store.CreateStory(ctx, domain.StoryCreateRequest{
Owner: owner,
RandomID: 9001,
Date: 101,
Period: 86400,
Public: true,
Caption: "retry must not win",
Media: media,
})
if err != nil {
t.Fatalf("create duplicate: %v", err)
}
if !dup.Duplicate || dup.Story.ID != created.Story.ID || dup.Story.Caption != "first" {
t.Fatalf("duplicate = %+v, want original story", dup)
}
read, err := store.GetPeerStories(ctx, owner.ID, owner, 102)
if err != nil {
t.Fatalf("get self stories: %v", err)
}
if read.MaxReadID != 1 {
t.Fatalf("self max read = %d, want created story id", read.MaxReadID)
}
edited, err := store.EditStory(ctx, domain.StoryEditRequest{
Owner: owner,
ID: created.Story.ID,
Caption: "edited",
UpdateCaption: true,
})
if err != nil {
t.Fatalf("edit story: %v", err)
}
if edited.Previous.Caption != "first" || edited.Previous.Edited {
t.Fatalf("previous = %+v, want pre-edit story", edited.Previous)
}
if !edited.Story.Edited || edited.Story.Caption != "edited" || !edited.Story.Pinned {
t.Fatalf("edited = %+v, want edited caption preserving pin", edited)
}
pinned, err := store.ListPinnedStories(ctx, owner.ID, owner, 0, 10, 103)
if err != nil {
t.Fatalf("list pinned: %v", err)
}
if len(pinned.Stories) != 1 || pinned.Stories[0].ID != created.Story.ID {
t.Fatalf("pinned = %+v, want created story", pinned)
}
deleted, err := store.DeleteStories(ctx, owner, []int{created.Story.ID}, 104)
if err != nil {
t.Fatalf("delete story: %v", err)
}
if len(deleted.Stories) != 1 || !deleted.Stories[0].Deleted || deleted.Stories[0].Pinned {
t.Fatalf("deleted = %+v, want deleted unpinned snapshot", deleted)
}
if len(deleted.Previous) != 1 || deleted.Previous[0].Deleted || !deleted.Previous[0].Pinned {
t.Fatalf("deleted previous = %+v, want pre-delete pinned snapshot", deleted.Previous)
}
active, err := store.GetPeerStories(ctx, owner.ID, owner, 105)
if err != nil {
t.Fatalf("get active after delete: %v", err)
}
if len(active.Stories) != 0 {
t.Fatalf("active after delete = %+v, want empty", active)
}
}
func TestStoryMutationIDsAreDeduped(t *testing.T) {
ctx := context.Background()
store := NewStoryStore()
owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1001}
media := &domain.MessageMedia{Kind: domain.MessageMediaKindPhoto, Photo: &domain.Photo{ID: 1}}
created, err := store.CreateStory(ctx, domain.StoryCreateRequest{
Owner: owner,
RandomID: 9101,
Date: 100,
Period: 86400,
Public: true,
Media: media,
})
if err != nil {
t.Fatalf("create story: %v", err)
}
toggled, err := store.TogglePinned(ctx, owner, []int{created.Story.ID, created.Story.ID}, true, 101)
if err != nil {
t.Fatalf("toggle pinned: %v", err)
}
if len(toggled.IDs) != 1 || toggled.IDs[0] != created.Story.ID || len(toggled.Stories) != 1 || !toggled.Stories[0].Pinned {
t.Fatalf("toggled = %+v, want one pinned story", toggled)
}
if len(toggled.Previous) != 1 || toggled.Previous[0].Pinned {
t.Fatalf("toggled previous = %+v, want pre-toggle unpinned snapshot", toggled.Previous)
}
retryToggled, err := store.TogglePinned(ctx, owner, []int{created.Story.ID}, true, 102)
if err != nil {
t.Fatalf("retry toggle pinned: %v", err)
}
if len(retryToggled.IDs) != 1 || retryToggled.IDs[0] != created.Story.ID || len(retryToggled.Stories) != 0 || len(retryToggled.Previous) != 0 {
t.Fatalf("retry toggled = %+v, want id echo without mutation snapshot", retryToggled)
}
unpinned, err := store.TogglePinned(ctx, owner, []int{created.Story.ID}, false, 103)
if err != nil {
t.Fatalf("toggle unpinned: %v", err)
}
if len(unpinned.Stories) != 1 || unpinned.Stories[0].Pinned || len(unpinned.Previous) != 1 || !unpinned.Previous[0].Pinned {
t.Fatalf("unpinned = %+v, want unpinned story with pre-toggle pinned snapshot", unpinned)
}
retryUnpinned, err := store.TogglePinned(ctx, owner, []int{created.Story.ID}, false, 104)
if err != nil {
t.Fatalf("retry toggle unpinned: %v", err)
}
if len(retryUnpinned.Stories) != 0 || len(retryUnpinned.Previous) != 0 {
t.Fatalf("retry unpinned = %+v, want no mutation snapshots", retryUnpinned)
}
emptyToggle, err := store.TogglePinned(ctx, owner, nil, false, 101)
if err != nil {
t.Fatalf("empty toggle pinned: %v", err)
}
if len(emptyToggle.IDs) != 0 || len(emptyToggle.Stories) != 0 {
t.Fatalf("empty toggle = %+v, want no-op", emptyToggle)
}
deleted, err := store.DeleteStories(ctx, owner, []int{created.Story.ID, created.Story.ID}, 102)
if err != nil {
t.Fatalf("delete story: %v", err)
}
if len(deleted.IDs) != 1 || deleted.IDs[0] != created.Story.ID || len(deleted.Stories) != 1 || !deleted.Stories[0].Deleted {
t.Fatalf("deleted = %+v, want one deleted story", deleted)
}
retryDeleted, err := store.DeleteStories(ctx, owner, []int{created.Story.ID}, 103)
if err != nil {
t.Fatalf("retry delete story: %v", err)
}
if len(retryDeleted.IDs) != 1 || retryDeleted.IDs[0] != created.Story.ID || len(retryDeleted.Stories) != 0 {
t.Fatalf("retry deleted = %+v, want id echo without mutation snapshot", retryDeleted)
}
}
func TestStoryStorePinnedToTopOrder(t *testing.T) {
ctx := context.Background()
store := NewStoryStore()
owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1001}
for _, id := range []int{1, 2, 3, 4} {
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{
Owner: owner,
ID: id,
Date: 100 + id,
ExpireDate: 1000,
Pinned: true,
Public: true,
}}); err != nil {
t.Fatalf("upsert story %d: %v", id, err)
}
}
if err := store.TogglePinnedToTop(ctx, owner, []int{3, 1, 3}); err != nil {
t.Fatalf("toggle pinned to top: %v", err)
}
pinned, err := store.ListPinnedStories(ctx, owner.ID, owner, 0, 2, 200)
if err != nil {
t.Fatalf("list pinned stories: %v", err)
}
if pinned.Count != 4 || !sameStoryIDs(storyIDs(pinned.Stories), []int{4, 3}) || !sameStoryIDs(pinned.PinnedToTop, []int{3, 1}) {
t.Fatalf("pinned = count %d stories %v top %v, want count 4 stories 4,3 top 3,1", pinned.Count, storyIDs(pinned.Stories), pinned.PinnedToTop)
}
androidInitial, err := store.ListPinnedStories(ctx, owner.ID, owner, -1, 2, 200)
if err != nil {
t.Fatalf("list pinned stories with android sentinel: %v", err)
}
if androidInitial.Count != pinned.Count || !sameStoryIDs(storyIDs(androidInitial.Stories), []int{4, 3}) || !sameStoryIDs(androidInitial.PinnedToTop, []int{3, 1}) {
t.Fatalf("android sentinel pinned = count %d stories %v top %v, want same first page 4,3 top 3,1", androidInitial.Count, storyIDs(androidInitial.Stories), androidInitial.PinnedToTop)
}
if err := store.TogglePinnedToTop(ctx, owner, []int{2, 1, 4}); err != nil {
t.Fatalf("replace pinned to top: %v", err)
}
pinned, err = store.ListPinnedStories(ctx, owner.ID, owner, 0, 10, 201)
if err != nil {
t.Fatalf("list replaced pinned stories: %v", err)
}
if !sameStoryIDs(pinned.PinnedToTop, []int{2, 1, 4}) {
t.Fatalf("replaced top = %v, want 2,1,4", pinned.PinnedToTop)
}
pageAfterOffset, err := store.ListPinnedStories(ctx, owner.ID, owner, 3, 10, 201)
if err != nil {
t.Fatalf("list pinned page after offset: %v", err)
}
if !sameStoryIDs(storyIDs(pageAfterOffset.Stories), []int{2, 1}) || !sameStoryIDs(pageAfterOffset.PinnedToTop, []int{2, 1, 4}) {
t.Fatalf("offset page stories %v top %v, want stories 2,1 and full top 2,1,4", storyIDs(pageAfterOffset.Stories), pageAfterOffset.PinnedToTop)
}
if _, err := store.TogglePinned(ctx, owner, []int{2}, false, 202); err != nil {
t.Fatalf("unpin story: %v", err)
}
pinned, err = store.ListPinnedStories(ctx, owner.ID, owner, 0, 10, 203)
if err != nil {
t.Fatalf("list after unpin: %v", err)
}
if !sameStoryIDs(pinned.PinnedToTop, []int{1, 4}) {
t.Fatalf("top after unpin = %v, want 1,4", pinned.PinnedToTop)
}
if err := store.TogglePinnedToTop(ctx, owner, []int{2}); !errors.Is(err, domain.ErrStoryIDInvalid) {
t.Fatalf("unpinned top err = %v, want ErrStoryIDInvalid", err)
}
if _, err := store.DeleteStories(ctx, owner, []int{1}, 204); err != nil {
t.Fatalf("delete story: %v", err)
}
pinned, err = store.ListPinnedStories(ctx, owner.ID, owner, 0, 10, 205)
if err != nil {
t.Fatalf("list after delete: %v", err)
}
if !sameStoryIDs(pinned.PinnedToTop, []int{4}) {
t.Fatalf("top after delete = %v, want 4", pinned.PinnedToTop)
}
if err := store.TogglePinnedToTop(ctx, owner, nil); err != nil {
t.Fatalf("clear pinned to top: %v", err)
}
pinned, err = store.ListPinnedStories(ctx, owner.ID, owner, 0, 10, 206)
if err != nil {
t.Fatalf("list after clear: %v", err)
}
if len(pinned.PinnedToTop) != 0 {
t.Fatalf("top after clear = %v, want empty", pinned.PinnedToTop)
}
}
func storyIDs(stories []domain.Story) []int {
out := make([]int, 0, len(stories))
for _, story := range stories {
out = append(out, story.ID)
}
return out
}
func sameStoryIDs(got, want []int) bool {
if len(got) != len(want) {
return false
}
for i := range got {
if got[i] != want[i] {
return false
}
}
return true
}