2365 lines
89 KiB
Go
2365 lines
89 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"telesrv/internal/domain"
|
|
storepkg "telesrv/internal/store"
|
|
)
|
|
|
|
func TestStoryStoreReadMaxHiddenAndPeerMaxIDsPostgres(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
owner, viewer := createStoryTestUsers(t, ctx, pool)
|
|
store := NewStoryStore(pool)
|
|
ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}
|
|
|
|
for _, id := range []int{1, 2} {
|
|
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{
|
|
Owner: ownerPeer,
|
|
ID: id,
|
|
Date: 1700000100 + id,
|
|
ExpireDate: 1700001000,
|
|
Public: true,
|
|
Caption: "pg story",
|
|
}}); err != nil {
|
|
t.Fatalf("upsert story %d: %v", id, err)
|
|
}
|
|
}
|
|
|
|
read, err := store.MarkRead(ctx, viewer.ID, ownerPeer, 2, 1700000200)
|
|
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, viewer.ID, ownerPeer, 1, 1700000201)
|
|
if err != nil {
|
|
t.Fatalf("mark lower read: %v", err)
|
|
}
|
|
if read.Advanced || read.MaxReadID != 2 {
|
|
t.Fatalf("lower read = %+v, want unchanged max 2", read)
|
|
}
|
|
|
|
peerStories, err := store.GetPeerStories(ctx, viewer.ID, ownerPeer, 1700000202)
|
|
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, viewer.ID, []domain.Peer{ownerPeer}, 1700000202)
|
|
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)
|
|
}
|
|
|
|
list, err := store.ListActiveStories(ctx, viewer.ID, false, 1700000202, 100)
|
|
if err != nil {
|
|
t.Fatalf("list active stories: %v", err)
|
|
}
|
|
if list.Count != 1 || len(list.Peers) != 1 || len(list.Stories) != 2 {
|
|
t.Fatalf("active list = %+v, want one peer with two stories", list)
|
|
}
|
|
ownerActive, err := store.ListOwnerActiveStories(ctx, ownerPeer, 1700000202, 100)
|
|
if err != nil {
|
|
t.Fatalf("list owner active stories: %v", err)
|
|
}
|
|
if len(ownerActive.Stories) != 2 {
|
|
t.Fatalf("owner active stories = %+v, want two stories", ownerActive.Stories)
|
|
}
|
|
for _, story := range ownerActive.Stories {
|
|
if story.Out || story.Views.HasViewers || story.SentReaction != nil {
|
|
t.Fatalf("owner active story fanout snapshot = %+v, want no out/views/reaction", story)
|
|
}
|
|
}
|
|
if err := store.SetPeerHidden(ctx, viewer.ID, ownerPeer, true); err != nil {
|
|
t.Fatalf("set hidden: %v", err)
|
|
}
|
|
hiddenStates, err := store.GetPeerHiddenStates(ctx, viewer.ID, []domain.Peer{ownerPeer})
|
|
if err != nil {
|
|
t.Fatalf("get hidden states: %v", err)
|
|
}
|
|
if !hiddenStates[ownerPeer] {
|
|
t.Fatalf("hidden states = %+v, want owner hidden", hiddenStates)
|
|
}
|
|
projections, err := store.GetPeerStoryProjections(ctx, viewer.ID, []domain.Peer{ownerPeer}, 1700000202)
|
|
if err != nil {
|
|
t.Fatalf("get story peer projections: %v", err)
|
|
}
|
|
if len(projections) != 1 || projections[0].Peer != ownerPeer || projections[0].Recent.MaxID != 2 || !projections[0].Hidden {
|
|
t.Fatalf("story peer projections = %+v, want max id 2 hidden owner", projections)
|
|
}
|
|
expirations, err := store.ActiveStoryPeerExpirations(ctx, []domain.Peer{ownerPeer}, 1700000202)
|
|
if err != nil || expirations[ownerPeer] != 1700001000 {
|
|
t.Fatalf("active story peer expirations = %+v, %v; want owner=1700001000", expirations, err)
|
|
}
|
|
hiddenPeers, err := store.ListHiddenStoryPeers(ctx, viewer.ID)
|
|
if err != nil || len(hiddenPeers) != 1 || hiddenPeers[0] != ownerPeer {
|
|
t.Fatalf("hidden story peer snapshot = %+v, %v; want owner", hiddenPeers, err)
|
|
}
|
|
list, err = store.ListActiveStories(ctx, viewer.ID, false, 1700000202, 100)
|
|
if err != nil {
|
|
t.Fatalf("list visible after hidden: %v", err)
|
|
}
|
|
if list.Count != 0 || len(list.Stories) != 0 {
|
|
t.Fatalf("visible list after hidden = %+v, want empty", list)
|
|
}
|
|
list, err = store.ListActiveStories(ctx, viewer.ID, true, 1700000202, 100)
|
|
if err != nil {
|
|
t.Fatalf("list hidden stories: %v", err)
|
|
}
|
|
if list.Count != 1 || len(list.Stories) != 2 {
|
|
t.Fatalf("hidden list = %+v, want hidden peer stories", list)
|
|
}
|
|
if err := store.SetPeerHidden(ctx, viewer.ID, ownerPeer, false); err != nil {
|
|
t.Fatalf("clear hidden: %v", err)
|
|
}
|
|
hiddenStates, err = store.GetPeerHiddenStates(ctx, viewer.ID, []domain.Peer{ownerPeer})
|
|
if err != nil {
|
|
t.Fatalf("get hidden states after clear: %v", err)
|
|
}
|
|
if hiddenStates[ownerPeer] {
|
|
t.Fatalf("hidden states after clear = %+v, want owner visible", hiddenStates)
|
|
}
|
|
hiddenPeers, err = store.ListHiddenStoryPeers(ctx, viewer.ID)
|
|
if err != nil || len(hiddenPeers) != 0 {
|
|
t.Fatalf("hidden story peer snapshot after clear = %+v, %v; want empty", hiddenPeers, err)
|
|
}
|
|
expirations, err = store.ActiveStoryPeerExpirations(ctx, []domain.Peer{ownerPeer}, 1700001000)
|
|
if err != nil || len(expirations) != 0 {
|
|
t.Fatalf("active story peer expirations at boundary = %+v, %v; want empty", expirations, err)
|
|
}
|
|
}
|
|
|
|
func TestStoryStoreListActiveStoriesPaginatesByPeerPostgres(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
users := NewUserStore(pool)
|
|
viewer, err := users.Create(ctx, domain.User{AccessHash: 9100, Phone: "+1881" + suffix + "00", FirstName: "StoryViewer"})
|
|
if err != nil {
|
|
t.Fatalf("create viewer: %v", err)
|
|
}
|
|
owners := make([]domain.User, 0, 3)
|
|
ownerIDs := make([]int64, 0, 3)
|
|
for i := 0; i < 3; i++ {
|
|
owner, err := users.Create(ctx, domain.User{AccessHash: int64(9101 + i), Phone: "+1881" + suffix + "0" + string(rune('1'+i)), FirstName: "StoryOwner"})
|
|
if err != nil {
|
|
t.Fatalf("create owner %d: %v", i, err)
|
|
}
|
|
owners = append(owners, owner)
|
|
ownerIDs = append(ownerIDs, owner.ID)
|
|
}
|
|
t.Cleanup(func() {
|
|
cleanupStoryPagingTestRows(t, context.Background(), pool, viewer.ID, ownerIDs)
|
|
})
|
|
store := NewStoryStore(pool)
|
|
for i, owner := range owners {
|
|
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{
|
|
Owner: domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID},
|
|
ID: 1,
|
|
Date: 1700000300 - i*100,
|
|
ExpireDate: 1700001000,
|
|
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: domain.Peer{Type: domain.PeerTypeUser, ID: owners[0].ID},
|
|
ID: 2,
|
|
Date: 1700000250,
|
|
ExpireDate: 1700001000,
|
|
Public: true,
|
|
}}); err != nil {
|
|
t.Fatalf("upsert second story for first owner: %v", err)
|
|
}
|
|
|
|
first, err := store.ListActiveStoriesPage(ctx, viewer.ID, false, 1700000400, 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.ID != owners[0].ID || len(first.Peers[0].Stories) != 2 {
|
|
t.Fatalf("first peer = %+v stories %d, want first owner with two stories", first.Peers[0].Peer, len(first.Peers[0].Stories))
|
|
}
|
|
|
|
next, err := store.ListActiveStoriesPage(ctx, viewer.ID, false, 1700000400, domain.StoryListCursor{
|
|
Set: true,
|
|
Date: 1700000200,
|
|
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: owners[1].ID},
|
|
}, 2)
|
|
if err != nil {
|
|
t.Fatalf("next page: %v", err)
|
|
}
|
|
if next.Count != 3 || next.HasMore || len(next.Peers) != 1 || next.Peers[0].Peer.ID != owners[2].ID {
|
|
t.Fatalf("next page = %+v, want final page with third owner", next)
|
|
}
|
|
digest, err := store.ActiveStoriesDigest(ctx, viewer.ID, false, 1700000400)
|
|
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, viewer.ID, domain.Peer{Type: domain.PeerTypeUser, ID: owners[0].ID}, 2, 1700000401); err != nil {
|
|
t.Fatalf("mark read for digest: %v", err)
|
|
}
|
|
changed, err := store.ActiveStoriesDigest(ctx, viewer.ID, false, 1700000400)
|
|
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 TestStoryStoreSelfUserViewAndReactionDoNotPolluteOwnerInteractionsPostgres(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
owner, _ := createStoryTestUsers(t, ctx, pool)
|
|
store := NewStoryStore(pool)
|
|
ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}
|
|
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{
|
|
Owner: ownerPeer,
|
|
ID: 1,
|
|
Date: 1700000100,
|
|
ExpireDate: 1700001000,
|
|
Public: true,
|
|
Caption: "pg self view",
|
|
}}); err != nil {
|
|
t.Fatalf("upsert story: %v", err)
|
|
}
|
|
|
|
created, err := store.IncrementViews(ctx, owner.ID, ownerPeer, []int{1}, 1700000200)
|
|
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, ownerPeer, 1, &domain.MessageReaction{
|
|
Type: domain.MessageReactionEmoji,
|
|
Emoticon: "🔥",
|
|
}, 1700000201); err != domain.ErrStoryPeerInvalid {
|
|
t.Fatalf("self reaction err = %v, want ErrStoryPeerInvalid", err)
|
|
}
|
|
list, err := store.ListStoryViews(ctx, domain.StoryViewListRequest{
|
|
ViewerUserID: owner.ID,
|
|
Owner: ownerPeer,
|
|
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 TestStoryStoreBlocklistHidesOwnerStoriesFromViewerPostgres(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
owner, blockedViewer := createStoryTestUsers(t, ctx, pool)
|
|
users := NewUserStore(pool)
|
|
otherViewer, err := users.Create(ctx, domain.User{AccessHash: 8103, Phone: "+1771999903" + randomSuffix(t), FirstName: "StoryOtherViewer"})
|
|
if err != nil {
|
|
t.Fatalf("create other viewer: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if _, err := pool.Exec(context.Background(), `DELETE FROM users WHERE id = $1`, otherViewer.ID); err != nil {
|
|
t.Fatalf("cleanup other viewer: %v", err)
|
|
}
|
|
})
|
|
|
|
store := NewStoryStore(pool)
|
|
ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}
|
|
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{
|
|
Owner: ownerPeer,
|
|
ID: 1,
|
|
Date: 1700000100,
|
|
ExpireDate: 1700001000,
|
|
Public: true,
|
|
Caption: "pg blocked story",
|
|
}}); err != nil {
|
|
t.Fatalf("upsert story: %v", err)
|
|
}
|
|
if created, err := store.IncrementViews(ctx, blockedViewer.ID, ownerPeer, []int{1}, 1700000105); err != nil || created != 1 {
|
|
t.Fatalf("pre-block increment = %d, %v, want 1 nil", created, err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
INSERT INTO contact_blocks (owner_user_id, blocked_user_id, date)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT (owner_user_id, blocked_user_id) DO UPDATE SET date = EXCLUDED.date`,
|
|
owner.ID, blockedViewer.ID, int32(1700000200)); err != nil {
|
|
t.Fatalf("insert contact block: %v", err)
|
|
}
|
|
|
|
peerStories, err := store.GetPeerStories(ctx, blockedViewer.ID, ownerPeer, 1700000201)
|
|
if err != nil {
|
|
t.Fatalf("get blocked peer stories: %v", err)
|
|
}
|
|
if len(peerStories.Stories) != 0 {
|
|
t.Fatalf("blocked peer stories = %+v, want empty", peerStories.Stories)
|
|
}
|
|
exact, err := store.GetStoriesByID(ctx, blockedViewer.ID, ownerPeer, []int{1}, 1700000201)
|
|
if err != nil {
|
|
t.Fatalf("get blocked 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, blockedViewer.ID, []domain.Peer{ownerPeer}, 1700000201)
|
|
if err != nil {
|
|
t.Fatalf("get blocked 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, blockedViewer.ID, false, 1700000201, 100)
|
|
if err != nil {
|
|
t.Fatalf("list blocked 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, blockedViewer.ID, ownerPeer, []int{1}, 1700000210); err != nil || created != 0 {
|
|
t.Fatalf("blocked increment = %d, %v, want 0 nil", created, err)
|
|
}
|
|
if _, err := store.SetReaction(ctx, blockedViewer.ID, ownerPeer, 1, &domain.MessageReaction{
|
|
Type: domain.MessageReactionEmoji,
|
|
Emoticon: "🔥",
|
|
}, 1700000211); err != domain.ErrStoryNotFound {
|
|
t.Fatalf("blocked reaction err = %v, want ErrStoryNotFound", err)
|
|
}
|
|
|
|
ownerStories, err := store.GetPeerStories(ctx, owner.ID, ownerPeer, 1700000201)
|
|
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, otherViewer.ID, ownerPeer, 1700000201)
|
|
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, otherViewer.ID, ownerPeer, []int{1}, 1700000220); 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: ownerPeer,
|
|
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[blockedViewer.ID].BlockedMyStoriesFrom {
|
|
t.Fatalf("blocked viewer row = %+v, want blocked_my_stories_from", got[blockedViewer.ID])
|
|
}
|
|
if got[otherViewer.ID].BlockedMyStoriesFrom {
|
|
t.Fatalf("other viewer row = %+v, want not blocked", got[otherViewer.ID])
|
|
}
|
|
}
|
|
|
|
func TestStoryStoreChannelStoriesRequireActiveMemberPostgres(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
users := NewUserStore(pool)
|
|
creator, err := users.Create(ctx, domain.User{AccessHash: 8111, Phone: "+1773" + suffix + "01", FirstName: "StoryChannelCreator"})
|
|
if err != nil {
|
|
t.Fatalf("create creator: %v", err)
|
|
}
|
|
member, err := users.Create(ctx, domain.User{AccessHash: 8112, Phone: "+1773" + suffix + "02", FirstName: "StoryChannelMember"})
|
|
if err != nil {
|
|
t.Fatalf("create member: %v", err)
|
|
}
|
|
outsider, err := users.Create(ctx, domain.User{AccessHash: 8113, Phone: "+1773" + suffix + "03", FirstName: "StoryChannelOutsider"})
|
|
if err != nil {
|
|
t.Fatalf("create outsider: %v", err)
|
|
}
|
|
|
|
channels := NewChannelStore(pool)
|
|
createdChannel, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: creator.ID,
|
|
Title: "Story Gate " + suffix,
|
|
Broadcast: true,
|
|
MemberUserIDs: []int64{member.ID},
|
|
Date: 1700000300,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel: %v", err)
|
|
}
|
|
channelPeer := domain.Peer{Type: domain.PeerTypeChannel, ID: createdChannel.Channel.ID}
|
|
userIDs := []int64{creator.ID, member.ID, outsider.ID}
|
|
t.Cleanup(func() {
|
|
cleanupChannelStoryTestRows(t, context.Background(), pool, channelPeer.ID, userIDs)
|
|
})
|
|
|
|
store := NewStoryStore(pool)
|
|
createdStory, err := store.CreateStory(ctx, domain.StoryCreateRequest{
|
|
Owner: channelPeer,
|
|
RandomID: 1700000301,
|
|
Date: 1700000301,
|
|
Period: 86400,
|
|
Public: true,
|
|
Caption: "member-only channel story",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create channel story: %v", err)
|
|
}
|
|
|
|
creatorStories, err := store.GetPeerStories(ctx, creator.ID, channelPeer, 1700000310)
|
|
if err != nil {
|
|
t.Fatalf("creator get channel peer stories: %v", err)
|
|
}
|
|
if len(creatorStories.Stories) != 1 {
|
|
t.Fatalf("creator channel stories = %+v, want one story", creatorStories.Stories)
|
|
}
|
|
memberStories, err := store.GetPeerStories(ctx, member.ID, channelPeer, 1700000310)
|
|
if err != nil {
|
|
t.Fatalf("member get channel peer stories: %v", err)
|
|
}
|
|
if len(memberStories.Stories) != 1 {
|
|
t.Fatalf("member channel stories = %+v, want one story", memberStories.Stories)
|
|
}
|
|
memberActive, err := store.ListActiveStories(ctx, member.ID, false, 1700000310, 100)
|
|
if err != nil {
|
|
t.Fatalf("member list active stories: %v", err)
|
|
}
|
|
if !storyListContains(memberActive, channelPeer, createdStory.Story.ID) {
|
|
t.Fatalf("member active stories = %+v, want channel story %d", memberActive.Stories, createdStory.Story.ID)
|
|
}
|
|
memberProjection, err := store.GetPeerStoryProjections(ctx, member.ID, []domain.Peer{channelPeer}, 1700000310)
|
|
if err != nil {
|
|
t.Fatalf("member get projections: %v", err)
|
|
}
|
|
if len(memberProjection) != 1 || memberProjection[0].Recent.MaxID != createdStory.Story.ID {
|
|
t.Fatalf("member projection = %+v, want story max id %d", memberProjection, createdStory.Story.ID)
|
|
}
|
|
|
|
outsiderStories, err := store.GetPeerStories(ctx, outsider.ID, channelPeer, 1700000310)
|
|
if err != nil {
|
|
t.Fatalf("outsider get channel peer stories: %v", err)
|
|
}
|
|
if len(outsiderStories.Stories) != 0 {
|
|
t.Fatalf("outsider channel stories = %+v, want empty", outsiderStories.Stories)
|
|
}
|
|
exact, err := store.GetStoriesByID(ctx, outsider.ID, channelPeer, []int{createdStory.Story.ID}, 1700000310)
|
|
if err != nil {
|
|
t.Fatalf("outsider get stories by id: %v", err)
|
|
}
|
|
if len(exact.Stories) != 0 {
|
|
t.Fatalf("outsider exact channel stories = %+v, want empty", exact.Stories)
|
|
}
|
|
recent, err := store.GetPeerMaxIDs(ctx, outsider.ID, []domain.Peer{channelPeer}, 1700000310)
|
|
if err != nil {
|
|
t.Fatalf("outsider get peer max ids: %v", err)
|
|
}
|
|
if len(recent) != 1 || recent[0].MaxID != 0 {
|
|
t.Fatalf("outsider recent = %+v, want max id 0", recent)
|
|
}
|
|
projections, err := store.GetPeerStoryProjections(ctx, outsider.ID, []domain.Peer{channelPeer}, 1700000310)
|
|
if err != nil {
|
|
t.Fatalf("outsider get projections: %v", err)
|
|
}
|
|
if len(projections) != 1 || projections[0].Recent.MaxID != 0 {
|
|
t.Fatalf("outsider projection = %+v, want max id 0", projections)
|
|
}
|
|
active, err := store.ListActiveStories(ctx, outsider.ID, false, 1700000310, 100)
|
|
if err != nil {
|
|
t.Fatalf("outsider list active stories: %v", err)
|
|
}
|
|
if storyListContains(active, channelPeer, createdStory.Story.ID) {
|
|
t.Fatalf("outsider active stories = %+v, want no channel story %d", active.Stories, createdStory.Story.ID)
|
|
}
|
|
if created, err := store.IncrementViews(ctx, outsider.ID, channelPeer, []int{createdStory.Story.ID}, 1700000311); err != nil || created != 0 {
|
|
t.Fatalf("outsider increment = %d, %v, want 0 nil", created, err)
|
|
}
|
|
if _, err := store.SetReaction(ctx, outsider.ID, channelPeer, createdStory.Story.ID, &domain.MessageReaction{
|
|
Type: domain.MessageReactionEmoji,
|
|
Emoticon: "👍",
|
|
}, 1700000312); !errors.Is(err, domain.ErrStoryNotFound) {
|
|
t.Fatalf("outsider reaction err = %v, want ErrStoryNotFound", err)
|
|
}
|
|
|
|
if _, err := pool.Exec(ctx, `
|
|
UPDATE channel_members
|
|
SET banned_rights = '{"ViewMessages":true}'::jsonb
|
|
WHERE channel_id = $1 AND user_id = $2`, channelPeer.ID, member.ID); err != nil {
|
|
t.Fatalf("ban member view messages: %v", err)
|
|
}
|
|
bannedStories, err := store.GetPeerStories(ctx, member.ID, channelPeer, 1700000313)
|
|
if err != nil {
|
|
t.Fatalf("banned member get channel peer stories: %v", err)
|
|
}
|
|
if len(bannedStories.Stories) != 0 {
|
|
t.Fatalf("banned member stories = %+v, want empty", bannedStories.Stories)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
UPDATE channel_members
|
|
SET banned_rights = '{}'::jsonb,
|
|
status = 'left',
|
|
left_at = $3
|
|
WHERE channel_id = $1 AND user_id = $2`, channelPeer.ID, member.ID, 1700000314); err != nil {
|
|
t.Fatalf("mark member left: %v", err)
|
|
}
|
|
leftStories, err := store.GetPeerStories(ctx, member.ID, channelPeer, 1700000315)
|
|
if err != nil {
|
|
t.Fatalf("left member get channel peer stories: %v", err)
|
|
}
|
|
if len(leftStories.Stories) != 0 {
|
|
t.Fatalf("left member stories = %+v, want empty", leftStories.Stories)
|
|
}
|
|
}
|
|
|
|
func TestStoryStoreMediaAreasRoundTripEditClearPostgres(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
owner, _ := createStoryTestUsers(t, ctx, pool)
|
|
store := NewStoryStore(pool)
|
|
ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}
|
|
|
|
created, err := store.CreateStory(ctx, domain.StoryCreateRequest{
|
|
Owner: ownerPeer,
|
|
RandomID: 1700000251,
|
|
Date: 1700000250,
|
|
Period: 86400,
|
|
Public: true,
|
|
MediaAreas: []domain.StoryMediaArea{testPGStoryMediaArea("🔥", 10)},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create story: %v", err)
|
|
}
|
|
assertPGStoryMediaArea(t, created.Story, "🔥", 10)
|
|
|
|
afterRestart := NewStoryStore(pool)
|
|
list, err := afterRestart.GetStoriesByID(ctx, owner.ID, ownerPeer, []int{created.Story.ID}, 1700000260)
|
|
if err != nil {
|
|
t.Fatalf("get story after restart: %v", err)
|
|
}
|
|
if len(list.Stories) != 1 {
|
|
t.Fatalf("story after restart = %+v, want one story", list.Stories)
|
|
}
|
|
assertPGStoryMediaArea(t, list.Stories[0], "🔥", 10)
|
|
|
|
edited, err := afterRestart.EditStory(ctx, domain.StoryEditRequest{
|
|
Owner: ownerPeer,
|
|
ID: created.Story.ID,
|
|
UpdateMediaAreas: true,
|
|
MediaAreas: []domain.StoryMediaArea{testPGStoryURLMediaArea("https://example.com/story/link", 25)},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("edit story media areas: %v", err)
|
|
}
|
|
assertPGStoryURLMediaArea(t, edited.Story, "https://example.com/story/link", 25)
|
|
|
|
reloaded, err := NewStoryStore(pool).GetStoriesByID(ctx, owner.ID, ownerPeer, []int{created.Story.ID}, 1700000261)
|
|
if err != nil {
|
|
t.Fatalf("reload edited story: %v", err)
|
|
}
|
|
assertPGStoryURLMediaArea(t, reloaded.Stories[0], "https://example.com/story/link", 25)
|
|
|
|
geoEdited, err := NewStoryStore(pool).EditStory(ctx, domain.StoryEditRequest{
|
|
Owner: ownerPeer,
|
|
ID: created.Story.ID,
|
|
UpdateMediaAreas: true,
|
|
MediaAreas: []domain.StoryMediaArea{testPGStoryGeoPointMediaArea(31.2304, 121.4737, 35)},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("edit story geo media area: %v", err)
|
|
}
|
|
assertPGStoryGeoPointMediaArea(t, geoEdited.Story, 31.2304, 121.4737, 35)
|
|
|
|
reloaded, err = NewStoryStore(pool).GetStoriesByID(ctx, owner.ID, ownerPeer, []int{created.Story.ID}, 1700000262)
|
|
if err != nil {
|
|
t.Fatalf("reload geo edited story: %v", err)
|
|
}
|
|
assertPGStoryGeoPointMediaArea(t, reloaded.Stories[0], 31.2304, 121.4737, 35)
|
|
|
|
venueEdited, err := NewStoryStore(pool).EditStory(ctx, domain.StoryEditRequest{
|
|
Owner: ownerPeer,
|
|
ID: created.Story.ID,
|
|
UpdateMediaAreas: true,
|
|
MediaAreas: []domain.StoryMediaArea{testPGStoryVenueMediaArea("Inline Cafe", 31.231, 121.474, 40)},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("edit story venue media area: %v", err)
|
|
}
|
|
assertPGStoryVenueMediaArea(t, venueEdited.Story, "Inline Cafe", 31.231, 121.474, 40)
|
|
|
|
reloaded, err = NewStoryStore(pool).GetStoriesByID(ctx, owner.ID, ownerPeer, []int{created.Story.ID}, 1700000263)
|
|
if err != nil {
|
|
t.Fatalf("reload venue edited story: %v", err)
|
|
}
|
|
assertPGStoryVenueMediaArea(t, reloaded.Stories[0], "Inline Cafe", 31.231, 121.474, 40)
|
|
|
|
weatherEdited, err := NewStoryStore(pool).EditStory(ctx, domain.StoryEditRequest{
|
|
Owner: ownerPeer,
|
|
ID: created.Story.ID,
|
|
UpdateMediaAreas: true,
|
|
MediaAreas: []domain.StoryMediaArea{testPGStoryWeatherMediaArea("☀️", 22.5, 0x00cc6600, 45)},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("edit story weather media area: %v", err)
|
|
}
|
|
assertPGStoryWeatherMediaArea(t, weatherEdited.Story, "☀️", 22.5, 0x00cc6600, 45)
|
|
|
|
reloaded, err = NewStoryStore(pool).GetStoriesByID(ctx, owner.ID, ownerPeer, []int{created.Story.ID}, 1700000264)
|
|
if err != nil {
|
|
t.Fatalf("reload weather edited story: %v", err)
|
|
}
|
|
assertPGStoryWeatherMediaArea(t, reloaded.Stories[0], "☀️", 22.5, 0x00cc6600, 45)
|
|
|
|
channelPostEdited, err := NewStoryStore(pool).EditStory(ctx, domain.StoryEditRequest{
|
|
Owner: ownerPeer,
|
|
ID: created.Story.ID,
|
|
UpdateMediaAreas: true,
|
|
MediaAreas: []domain.StoryMediaArea{testPGStoryChannelPostMediaArea(777001, 42, 50)},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("edit story channel post media area: %v", err)
|
|
}
|
|
assertPGStoryChannelPostMediaArea(t, channelPostEdited.Story, 777001, 42, 50)
|
|
|
|
reloaded, err = NewStoryStore(pool).GetStoriesByID(ctx, owner.ID, ownerPeer, []int{created.Story.ID}, 1700000265)
|
|
if err != nil {
|
|
t.Fatalf("reload channel post edited story: %v", err)
|
|
}
|
|
assertPGStoryChannelPostMediaArea(t, reloaded.Stories[0], 777001, 42, 50)
|
|
|
|
starGiftEdited, err := NewStoryStore(pool).EditStory(ctx, domain.StoryEditRequest{
|
|
Owner: ownerPeer,
|
|
ID: created.Story.ID,
|
|
UpdateMediaAreas: true,
|
|
MediaAreas: []domain.StoryMediaArea{testPGStoryStarGiftMediaArea("Gift.Series_01-42", 55)},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("edit story star gift media area: %v", err)
|
|
}
|
|
assertPGStoryStarGiftMediaArea(t, starGiftEdited.Story, "Gift.Series_01-42", 55)
|
|
|
|
reloaded, err = NewStoryStore(pool).GetStoriesByID(ctx, owner.ID, ownerPeer, []int{created.Story.ID}, 1700000266)
|
|
if err != nil {
|
|
t.Fatalf("reload star gift edited story: %v", err)
|
|
}
|
|
assertPGStoryStarGiftMediaArea(t, reloaded.Stories[0], "Gift.Series_01-42", 55)
|
|
|
|
cleared, err := NewStoryStore(pool).EditStory(ctx, domain.StoryEditRequest{
|
|
Owner: ownerPeer,
|
|
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)
|
|
}
|
|
reloaded, err = NewStoryStore(pool).GetStoriesByID(ctx, owner.ID, ownerPeer, []int{created.Story.ID}, 1700000267)
|
|
if err != nil {
|
|
t.Fatalf("reload cleared story: %v", err)
|
|
}
|
|
if len(reloaded.Stories) != 1 || len(reloaded.Stories[0].MediaAreas) != 0 {
|
|
t.Fatalf("reloaded cleared story = %+v, want no media areas", reloaded.Stories)
|
|
}
|
|
}
|
|
|
|
func TestStoryStoreArchiveCountAndSeekPostgres(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
owner, _ := createStoryTestUsers(t, ctx, pool)
|
|
store := NewStoryStore(pool)
|
|
ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}
|
|
fixtures := []domain.Story{
|
|
{Owner: ownerPeer, ID: 1, Date: 1700000271, ExpireDate: 1700000300, Public: true},
|
|
{Owner: ownerPeer, ID: 2, Date: 1700000272, ExpireDate: 1700000500, Public: true},
|
|
{Owner: ownerPeer, ID: 3, Date: 1700000273, ExpireDate: 1700000300, Public: true, Pinned: true},
|
|
{Owner: ownerPeer, ID: 4, Date: 1700000274, ExpireDate: 1700000300, Public: true, Deleted: true},
|
|
{Owner: ownerPeer, ID: 5, Date: 1700000275, ExpireDate: 1700000300, Public: true},
|
|
}
|
|
for _, story := range fixtures {
|
|
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: story}); err != nil {
|
|
t.Fatalf("upsert story %d: %v", story.ID, err)
|
|
}
|
|
}
|
|
|
|
countOnly, err := store.ListStoriesArchive(ctx, owner.ID, ownerPeer, 0, 0, 1700000400)
|
|
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, ownerPeer, 0, 2, 1700000400)
|
|
if err != nil {
|
|
t.Fatalf("list archive first page: %v", err)
|
|
}
|
|
if first.Count != 3 || len(first.Stories) != 2 || first.Stories[0].ID != 5 || first.Stories[1].ID != 3 || !first.Stories[1].Pinned {
|
|
t.Fatalf("first archive page = %+v, want count 3 ids 5,3 with pinned story", first)
|
|
}
|
|
second, err := store.ListStoriesArchive(ctx, owner.ID, ownerPeer, 3, 2, 1700000400)
|
|
if err != nil {
|
|
t.Fatalf("list archive second page: %v", err)
|
|
}
|
|
if second.Count != 3 || len(second.Stories) != 1 || second.Stories[0].ID != 1 {
|
|
t.Fatalf("second archive page = %+v, want count 3 id 1", second)
|
|
}
|
|
}
|
|
|
|
func TestStoryStoreViewsAndReactionPostgres(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
owner, viewer := createStoryTestUsers(t, ctx, pool)
|
|
store := NewStoryStore(pool)
|
|
ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}
|
|
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{
|
|
Owner: ownerPeer,
|
|
ID: 1,
|
|
Date: 1700000300,
|
|
ExpireDate: 1700001000,
|
|
Public: true,
|
|
}}); err != nil {
|
|
t.Fatalf("upsert story: %v", err)
|
|
}
|
|
|
|
if created, err := store.IncrementViews(ctx, viewer.ID, ownerPeer, []int{1, 1}, 1700000301); err != nil || created != 1 {
|
|
t.Fatalf("increment views first = %d, %v; want 1 nil", created, err)
|
|
}
|
|
if created, err := store.IncrementViews(ctx, viewer.ID, ownerPeer, []int{1}, 1700000302); err != nil || created != 0 {
|
|
t.Fatalf("increment views duplicate = %d, %v; want 0 nil", created, err)
|
|
}
|
|
if _, err := store.IncrementViews(ctx, viewer.ID, ownerPeer, nil, 1700000302); !errors.Is(err, domain.ErrStoryIDInvalid) {
|
|
t.Fatalf("empty increment views err = %v, want ErrStoryIDInvalid", err)
|
|
}
|
|
like := &domain.MessageReaction{Type: domain.MessageReactionEmoji, Emoticon: "like"}
|
|
fire := &domain.MessageReaction{Type: domain.MessageReactionEmoji, Emoticon: "fire"}
|
|
res, err := store.SetReaction(ctx, viewer.ID, ownerPeer, 1, like, 1700000303)
|
|
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, viewer.ID, ownerPeer, 1, fire, 1700000304)
|
|
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 != "fire" {
|
|
t.Fatalf("replace result = %+v, want only fire reaction", res)
|
|
}
|
|
res, err = store.SetReaction(ctx, viewer.ID, ownerPeer, 1, fire, 1700000305)
|
|
if err != nil {
|
|
t.Fatalf("retry same reaction: %v", err)
|
|
}
|
|
if res.Changed || res.Date != 1700000304 || res.Story.Views.ReactionsCount != 1 || len(res.Story.Views.Reactions) != 1 || res.Story.Views.Reactions[0].Reaction.Emoticon != "fire" {
|
|
t.Fatalf("retry same reaction result = %+v, want unchanged fire reaction at original date", res)
|
|
}
|
|
views, err := store.ListStoryViews(ctx, domain.StoryViewListRequest{
|
|
ViewerUserID: owner.ID,
|
|
Owner: ownerPeer,
|
|
StoryID: 1,
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("list story views: %v", err)
|
|
}
|
|
if views.Count != 1 || views.ViewsCount != 1 || views.ReactionsCount != 1 || len(views.Views) != 1 || views.Views[0].ViewerID != viewer.ID {
|
|
t.Fatalf("views list = %+v, want one viewer with reaction", views)
|
|
}
|
|
reactions, err := store.ListStoryReactions(ctx, domain.StoryReactionListRequest{
|
|
ViewerUserID: owner.ID,
|
|
Owner: ownerPeer,
|
|
StoryID: 1,
|
|
Reaction: fire,
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("list story reactions: %v", err)
|
|
}
|
|
if reactions.Count != 1 || len(reactions.Reactions) != 1 || reactions.Reactions[0].ViewerID != viewer.ID {
|
|
t.Fatalf("reactions list = %+v, want one filtered reaction", reactions)
|
|
}
|
|
if reactions.Reactions[0].Date != 1700000304 {
|
|
t.Fatalf("reaction date after retry = %d, want original date", reactions.Reactions[0].Date)
|
|
}
|
|
custom := &domain.MessageReaction{Type: domain.MessageReactionCustomEmoji, DocumentID: 12345}
|
|
res, err = store.SetReaction(ctx, viewer.ID, ownerPeer, 1, custom, 1700000305)
|
|
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, viewer.ID, ownerPeer, 1, custom, 1700000306)
|
|
if err != nil {
|
|
t.Fatalf("retry same custom reaction: %v", err)
|
|
}
|
|
if res.Changed || res.Date != 1700000305 || 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: ownerPeer,
|
|
StoryID: 1,
|
|
Reaction: custom,
|
|
Limit: 10,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("list custom story reactions: %v", err)
|
|
}
|
|
if reactions.Count != 1 || len(reactions.Reactions) != 1 || reactions.Reactions[0].ViewerID != viewer.ID || reactions.Reactions[0].Date != 1700000305 || reactions.Reactions[0].Reaction == nil || reactions.Reactions[0].Reaction.DocumentID != 12345 {
|
|
t.Fatalf("custom story reactions = %+v, want original custom reaction date", reactions)
|
|
}
|
|
if _, err := store.ListStoryViews(ctx, domain.StoryViewListRequest{
|
|
ViewerUserID: owner.ID,
|
|
Owner: ownerPeer,
|
|
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: ownerPeer,
|
|
StoryID: 1,
|
|
Offset: "1:1700000304:2001",
|
|
Limit: 10,
|
|
}); !errors.Is(err, domain.ErrStoryOffsetInvalid) {
|
|
t.Fatalf("bad story reactions offset err = %v, want ErrStoryOffsetInvalid", err)
|
|
}
|
|
res, err = store.SetReaction(ctx, viewer.ID, ownerPeer, 1, nil, 1700000307)
|
|
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)
|
|
}
|
|
|
|
afterRestart := NewStoryStore(pool)
|
|
list, err := afterRestart.GetStoriesByID(ctx, viewer.ID, ownerPeer, []int{1}, 1700000307)
|
|
if err != nil {
|
|
t.Fatalf("get story after restart: %v", err)
|
|
}
|
|
if len(list.Stories) != 1 || list.Stories[0].Views.ViewsCount != 1 || list.Stories[0].Views.ReactionsCount != 0 || list.Stories[0].SentReaction != nil {
|
|
t.Fatalf("story after restart = %+v, want durable view with cleared reaction", list.Stories)
|
|
}
|
|
}
|
|
|
|
func TestStoryStoreExpiredUnpinnedStoriesDoNotAcceptNewInteractionsPostgres(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
owner, viewer := createStoryTestUsers(t, ctx, pool)
|
|
_, otherViewer := createStoryTestUsers(t, ctx, pool)
|
|
store := NewStoryStore(pool)
|
|
ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}
|
|
for _, story := range []domain.Story{
|
|
{Owner: ownerPeer, ID: 1, Date: 1700000100, ExpireDate: 1700000150, Public: true},
|
|
{Owner: ownerPeer, ID: 2, Date: 1700000101, ExpireDate: 1700000150, 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, viewer.ID, ownerPeer, []int{1}, 1700000200); err != nil || created != 0 {
|
|
t.Fatalf("expired unpinned increment = %d, %v; want 0 nil", created, err)
|
|
}
|
|
if _, err := store.SetReaction(ctx, viewer.ID, ownerPeer, 1, &domain.MessageReaction{Type: domain.MessageReactionEmoji, Emoticon: "like"}, 1700000200); !errors.Is(err, domain.ErrStoryNotFound) {
|
|
t.Fatalf("expired unpinned reaction err = %v, want ErrStoryNotFound", err)
|
|
}
|
|
if created, err := store.IncrementViews(ctx, viewer.ID, ownerPeer, []int{2}, 1700000200); err != nil || created != 1 {
|
|
t.Fatalf("expired pinned increment = %d, %v; want 1 nil", created, err)
|
|
}
|
|
res, err := store.SetReaction(ctx, otherViewer.ID, ownerPeer, 2, &domain.MessageReaction{Type: domain.MessageReactionEmoji, Emoticon: "fire"}, 1700000201)
|
|
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 TestStoryStoreForwardRoundTripEditAndClonePostgres(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
owner, sourceUser := createStoryTestUsers(t, ctx, pool)
|
|
store := NewStoryStore(pool)
|
|
ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}
|
|
source := domain.Peer{Type: domain.PeerTypeUser, ID: sourceUser.ID}
|
|
created, err := store.CreateStory(ctx, domain.StoryCreateRequest{
|
|
Owner: ownerPeer,
|
|
RandomID: 202,
|
|
Date: 1700000600,
|
|
Period: 86400,
|
|
Public: true,
|
|
Forward: &domain.StoryForward{
|
|
From: source,
|
|
StoryID: 7,
|
|
Modified: true,
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create story: %v", err)
|
|
}
|
|
assertPGStoryForward(t, created.Story, source, 7, true)
|
|
created.Story.Forward.From.ID = 9999
|
|
created.Story.Forward.StoryID = 99
|
|
|
|
list, err := store.GetStoriesByID(ctx, owner.ID, ownerPeer, []int{created.Story.ID}, 1700000601)
|
|
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)
|
|
}
|
|
assertPGStoryForward(t, list.Stories[0], source, 7, true)
|
|
|
|
edited, err := store.EditStory(ctx, domain.StoryEditRequest{
|
|
Owner: ownerPeer,
|
|
ID: created.Story.ID,
|
|
UpdateCaption: true,
|
|
Caption: "edited repost caption",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("edit story caption: %v", err)
|
|
}
|
|
assertPGStoryForward(t, edited.Story, source, 7, true)
|
|
edited.Story.Forward.From.ID = 9998
|
|
|
|
list, err = store.GetStoriesByID(ctx, owner.ID, ownerPeer, []int{created.Story.ID}, 1700000602)
|
|
if err != nil {
|
|
t.Fatalf("get edited story by id: %v", err)
|
|
}
|
|
assertPGStoryForward(t, list.Stories[0], source, 7, true)
|
|
|
|
hidden, err := store.CreateStory(ctx, domain.StoryCreateRequest{
|
|
Owner: ownerPeer,
|
|
RandomID: 203,
|
|
Date: 1700000603,
|
|
Period: 86400,
|
|
Public: true,
|
|
Forward: &domain.StoryForward{
|
|
FromName: "Alice Hidden",
|
|
StoryID: 8,
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create hidden-author story: %v", err)
|
|
}
|
|
assertPGStoryForwardName(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, ownerPeer, []int{hidden.Story.ID}, 1700000604)
|
|
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)
|
|
}
|
|
assertPGStoryForwardName(t, list.Stories[0], "Alice Hidden", 8, false)
|
|
|
|
editedHidden, err := store.EditStory(ctx, domain.StoryEditRequest{
|
|
Owner: ownerPeer,
|
|
ID: hidden.Story.ID,
|
|
UpdateCaption: true,
|
|
Caption: "edited hidden repost caption",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("edit hidden-author story caption: %v", err)
|
|
}
|
|
assertPGStoryForwardName(t, editedHidden.Story, "Alice Hidden", 8, false)
|
|
}
|
|
|
|
func TestStoryStorePublicRepostForwardCountAndViewsListPostgres(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
sourceUser, repostUser := createStoryTestUsers(t, ctx, pool)
|
|
store := NewStoryStore(pool)
|
|
sourceOwner := domain.Peer{Type: domain.PeerTypeUser, ID: sourceUser.ID}
|
|
repostOwner := domain.Peer{Type: domain.PeerTypeUser, ID: repostUser.ID}
|
|
source, err := store.CreateStory(ctx, domain.StoryCreateRequest{
|
|
Owner: sourceOwner,
|
|
RandomID: 301,
|
|
Date: 1700000700,
|
|
Period: 86400,
|
|
Public: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create source story: %v", err)
|
|
}
|
|
repost, err := store.CreateStory(ctx, domain.StoryCreateRequest{
|
|
Owner: repostOwner,
|
|
RandomID: 302,
|
|
Date: 1700000701,
|
|
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, sourceUser.ID, sourceOwner, []int{source.Story.ID}, 1700000702)
|
|
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: sourceUser.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}, 1700000703); err != nil {
|
|
t.Fatalf("delete repost: %v", err)
|
|
}
|
|
sourceList, err = store.GetStoriesByID(ctx, sourceUser.ID, sourceOwner, []int{source.Story.ID}, 1700000703)
|
|
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 TestStoryStorePublicForwardListReturnsRepostsOnlyPostgres(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
sourceUser, repostUser := createStoryTestUsers(t, ctx, pool)
|
|
store := NewStoryStore(pool)
|
|
sourceOwner := domain.Peer{Type: domain.PeerTypeChannel, ID: sourceUser.ID}
|
|
repostOwner := domain.Peer{Type: domain.PeerTypeChannel, ID: repostUser.ID}
|
|
privateRepostOwner := domain.Peer{Type: domain.PeerTypeChannel, ID: repostUser.ID + 1000}
|
|
source, err := store.CreateStory(ctx, domain.StoryCreateRequest{
|
|
Owner: sourceOwner,
|
|
RandomID: 311,
|
|
Date: 1700000710,
|
|
Period: 86400,
|
|
Public: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create source story: %v", err)
|
|
}
|
|
repost, err := store.CreateStory(ctx, domain.StoryCreateRequest{
|
|
Owner: repostOwner,
|
|
RandomID: 312,
|
|
Date: 1700000711,
|
|
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: 314,
|
|
Date: 1700000713,
|
|
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: 313,
|
|
Date: 1700000712,
|
|
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: sourceUser.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}, 1700000714); err != nil {
|
|
t.Fatalf("delete repost: %v", err)
|
|
}
|
|
afterVisibleDelete, err := store.ListStoryPublicForwards(ctx, domain.StoryPublicForwardListRequest{
|
|
ViewerUserID: sourceUser.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}, 1700000715); err != nil {
|
|
t.Fatalf("delete hidden repost: %v", err)
|
|
}
|
|
empty, err := store.ListStoryPublicForwards(ctx, domain.StoryPublicForwardListRequest{
|
|
ViewerUserID: sourceUser.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 TestStoryStorePublicRepostStoryReactionsListPostgres(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
sourceUser, repostUser := createStoryTestUsers(t, ctx, pool)
|
|
store := NewStoryStore(pool)
|
|
channels := NewChannelStore(pool)
|
|
createdChannel, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
|
CreatorUserID: sourceUser.ID,
|
|
Title: "Story Reactions " + randomSuffix(t),
|
|
Broadcast: true,
|
|
MemberUserIDs: []int64{repostUser.ID},
|
|
Date: 1700000790,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create source channel: %v", err)
|
|
}
|
|
sourceOwner := domain.Peer{Type: domain.PeerTypeChannel, ID: createdChannel.Channel.ID}
|
|
t.Cleanup(func() {
|
|
cleanupChannelStoryTestRows(t, context.Background(), pool, sourceOwner.ID, []int64{sourceUser.ID, repostUser.ID})
|
|
})
|
|
repostOwner := domain.Peer{Type: domain.PeerTypeUser, ID: repostUser.ID}
|
|
source, err := store.CreateStory(ctx, domain.StoryCreateRequest{
|
|
Owner: sourceOwner,
|
|
RandomID: 401,
|
|
Date: 1700000800,
|
|
Period: 86400,
|
|
Public: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create source story: %v", err)
|
|
}
|
|
if _, err := store.SetReaction(ctx, repostUser.ID, sourceOwner, source.Story.ID, &domain.MessageReaction{
|
|
Type: domain.MessageReactionEmoji,
|
|
Emoticon: "🔥",
|
|
}, 1700000801); err != nil {
|
|
t.Fatalf("set reaction: %v", err)
|
|
}
|
|
repost, err := store.CreateStory(ctx, domain.StoryCreateRequest{
|
|
Owner: repostOwner,
|
|
RandomID: 402,
|
|
Date: 1700000802,
|
|
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: sourceUser.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: sourceUser.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 != repostUser.ID {
|
|
t.Fatalf("filtered reaction list = %+v, want only emoji reactor", filtered)
|
|
}
|
|
if _, err := store.DeleteStories(ctx, repostOwner, []int{repost.Story.ID}, 1700000803); err != nil {
|
|
t.Fatalf("delete repost: %v", err)
|
|
}
|
|
list, err = store.ListStoryReactions(ctx, domain.StoryReactionListRequest{
|
|
ViewerUserID: sourceUser.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 testPGStoryMediaArea(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 assertPGStoryMediaArea(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 testPGStoryURLMediaArea(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 assertPGStoryURLMediaArea(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 testPGStoryGeoPointMediaArea(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 assertPGStoryGeoPointMediaArea(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 testPGStoryVenueMediaArea(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 assertPGStoryVenueMediaArea(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 testPGStoryWeatherMediaArea(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 assertPGStoryWeatherMediaArea(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 testPGStoryChannelPostMediaArea(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 assertPGStoryChannelPostMediaArea(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 testPGStoryStarGiftMediaArea(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 assertPGStoryStarGiftMediaArea(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 assertPGStoryForward(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 assertPGStoryForwardName(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 TestStoryStoreViewListFiltersPostgres(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
owner, contactViewer := createStoryTestUsers(t, ctx, pool)
|
|
users := NewUserStore(pool)
|
|
stranger, err := users.Create(ctx, domain.User{AccessHash: 8103, Phone: "+1771" + randomSuffix(t) + "03", FirstName: "Bob", LastName: "Stranger"})
|
|
if err != nil {
|
|
t.Fatalf("create stranger: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if _, err := pool.Exec(context.Background(), `DELETE FROM users WHERE id = $1`, stranger.ID); err != nil {
|
|
t.Fatalf("cleanup stranger: %v", err)
|
|
}
|
|
})
|
|
store := NewStoryStore(pool)
|
|
ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}
|
|
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{
|
|
Owner: ownerPeer,
|
|
ID: 1,
|
|
Date: 1700000400,
|
|
ExpireDate: 1700001000,
|
|
Public: true,
|
|
}}); err != nil {
|
|
t.Fatalf("upsert story: %v", err)
|
|
}
|
|
if _, err := store.IncrementViews(ctx, contactViewer.ID, ownerPeer, []int{1}, 1700000401); err != nil {
|
|
t.Fatalf("increment contact view: %v", err)
|
|
}
|
|
if _, err := store.IncrementViews(ctx, stranger.ID, ownerPeer, []int{1}, 1700000402); err != nil {
|
|
t.Fatalf("increment stranger view: %v", err)
|
|
}
|
|
viewerIDs, err := store.ListStoryViewerIDs(ctx, ownerPeer, 1, 10)
|
|
if err != nil {
|
|
t.Fatalf("list story viewer ids: %v", err)
|
|
}
|
|
if len(viewerIDs) != 2 || viewerIDs[0] != contactViewer.ID || viewerIDs[1] != stranger.ID {
|
|
t.Fatalf("viewer ids = %v, want contact and stranger ascending", viewerIDs)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
INSERT INTO contacts (user_id, contact_user_id, mutual, contact_first_name, contact_last_name, contact_phone)
|
|
VALUES ($1, $2, false, 'Close', 'Friend', '7001')
|
|
ON CONFLICT (user_id, contact_user_id) DO UPDATE SET
|
|
contact_first_name = EXCLUDED.contact_first_name,
|
|
contact_last_name = EXCLUDED.contact_last_name,
|
|
contact_phone = EXCLUDED.contact_phone`, owner.ID, contactViewer.ID); err != nil {
|
|
t.Fatalf("insert contact: %v", err)
|
|
}
|
|
|
|
contactsOnly, err := store.ListStoryViews(ctx, domain.StoryViewListRequest{
|
|
ViewerUserID: owner.ID,
|
|
Owner: ownerPeer,
|
|
StoryID: 1,
|
|
Limit: 10,
|
|
JustContacts: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("list contacts-only: %v", err)
|
|
}
|
|
if contactsOnly.Count != 1 || contactsOnly.ViewsCount != 2 || len(contactsOnly.Views) != 1 || contactsOnly.Views[0].ViewerID != contactViewer.ID {
|
|
t.Fatalf("contacts-only = %+v, want one contact viewer and total views 2", contactsOnly)
|
|
}
|
|
|
|
remark, err := store.ListStoryViews(ctx, domain.StoryViewListRequest{
|
|
ViewerUserID: owner.ID,
|
|
Owner: ownerPeer,
|
|
StoryID: 1,
|
|
Limit: 10,
|
|
Query: "close",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("list remark query: %v", err)
|
|
}
|
|
if remark.Count != 1 || len(remark.Views) != 1 || remark.Views[0].ViewerID != contactViewer.ID {
|
|
t.Fatalf("remark query = %+v, want contact viewer", remark)
|
|
}
|
|
|
|
strangerQuery, err := store.ListStoryViews(ctx, domain.StoryViewListRequest{
|
|
ViewerUserID: owner.ID,
|
|
Owner: ownerPeer,
|
|
StoryID: 1,
|
|
Limit: 10,
|
|
Query: "bob",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("list stranger query: %v", err)
|
|
}
|
|
if strangerQuery.Count != 1 || len(strangerQuery.Views) != 1 || strangerQuery.Views[0].ViewerID != stranger.ID {
|
|
t.Fatalf("stranger query = %+v, want stranger viewer", strangerQuery)
|
|
}
|
|
|
|
intersection, err := store.ListStoryViews(ctx, domain.StoryViewListRequest{
|
|
ViewerUserID: owner.ID,
|
|
Owner: ownerPeer,
|
|
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 empty contact-filtered bob result", intersection)
|
|
}
|
|
if _, err := store.DeleteStories(ctx, ownerPeer, []int{1}, 1700000403); err != nil {
|
|
t.Fatalf("delete story: %v", err)
|
|
}
|
|
deletedViewerIDs, err := store.ListStoryViewerIDs(ctx, ownerPeer, 1, 10)
|
|
if err != nil {
|
|
t.Fatalf("list deleted story viewer ids: %v", err)
|
|
}
|
|
if len(deletedViewerIDs) != 2 || deletedViewerIDs[0] != contactViewer.ID || deletedViewerIDs[1] != stranger.ID {
|
|
t.Fatalf("deleted story viewer ids = %v, want contact and stranger ascending", deletedViewerIDs)
|
|
}
|
|
}
|
|
|
|
func TestStoryStoreViewerIDsIncludeCachedStoryExposurePostgres(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
owner, _ := createStoryTestUsers(t, ctx, pool)
|
|
store := NewStoryStore(pool)
|
|
ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}
|
|
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{
|
|
Owner: ownerPeer,
|
|
ID: 1,
|
|
Date: 1700000900,
|
|
ExpireDate: 1700001900,
|
|
Public: true,
|
|
}}); err != nil {
|
|
t.Fatalf("upsert story: %v", err)
|
|
}
|
|
|
|
viewerOne := owner.ID + 100001
|
|
viewerTwo := owner.ID + 100002
|
|
viewerThree := owner.ID + 100003
|
|
list, err := store.ListActiveStories(ctx, viewerOne, false, 1700000910, 10)
|
|
if err != nil {
|
|
t.Fatalf("list active stories: %v", err)
|
|
}
|
|
if len(list.Stories) != 1 {
|
|
t.Fatalf("active stories = %d, want 1", len(list.Stories))
|
|
}
|
|
peerStories, err := store.GetPeerStories(ctx, viewerTwo, ownerPeer, 1700000911)
|
|
if err != nil {
|
|
t.Fatalf("get peer stories: %v", err)
|
|
}
|
|
if len(peerStories.Stories) != 1 {
|
|
t.Fatalf("peer stories = %d, want 1", len(peerStories.Stories))
|
|
}
|
|
exact, err := store.GetStoriesByID(ctx, viewerThree, ownerPeer, []int{1}, 1700000912)
|
|
if err != nil {
|
|
t.Fatalf("get stories by id: %v", err)
|
|
}
|
|
if len(exact.Stories) != 1 {
|
|
t.Fatalf("exact stories = %d, want 1", len(exact.Stories))
|
|
}
|
|
|
|
viewerIDs, err := store.ListStoryViewerIDs(ctx, ownerPeer, 1, 10)
|
|
if err != nil {
|
|
t.Fatalf("list story viewer ids: %v", err)
|
|
}
|
|
want := []int64{viewerOne, viewerTwo, viewerThree}
|
|
if len(viewerIDs) != len(want) {
|
|
t.Fatalf("viewer ids = %v, want %v", viewerIDs, want)
|
|
}
|
|
for i := range want {
|
|
if viewerIDs[i] != want[i] {
|
|
t.Fatalf("viewer ids = %v, want %v", viewerIDs, want)
|
|
}
|
|
}
|
|
views, err := store.ListStoryViews(ctx, domain.StoryViewListRequest{
|
|
ViewerUserID: owner.ID,
|
|
Owner: ownerPeer,
|
|
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("story views = %+v, want exposure without view counters", views)
|
|
}
|
|
}
|
|
|
|
func TestStoryStorePrivacyVisibilityPostgres(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
owner, selectedViewer := createStoryTestUsers(t, ctx, pool)
|
|
users := NewUserStore(pool)
|
|
suffix := randomSuffix(t)
|
|
stranger, err := users.Create(ctx, domain.User{AccessHash: 8203, Phone: "+1772" + suffix + "03", FirstName: "Stranger"})
|
|
if err != nil {
|
|
t.Fatalf("create stranger: %v", err)
|
|
}
|
|
contactViewer, err := users.Create(ctx, domain.User{AccessHash: 8204, Phone: "+1772" + suffix + "04", FirstName: "Contact"})
|
|
if err != nil {
|
|
t.Fatalf("create contact viewer: %v", err)
|
|
}
|
|
excludedContact, err := users.Create(ctx, domain.User{AccessHash: 8205, Phone: "+1772" + suffix + "05", FirstName: "Excluded"})
|
|
if err != nil {
|
|
t.Fatalf("create excluded contact: %v", err)
|
|
}
|
|
closeFriendViewer, err := users.Create(ctx, domain.User{AccessHash: 8206, Phone: "+1772" + suffix + "06", FirstName: "Close"})
|
|
if err != nil {
|
|
t.Fatalf("create close friend viewer: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if _, err := pool.Exec(context.Background(), `
|
|
DELETE FROM contacts
|
|
WHERE user_id = $1
|
|
OR contact_user_id = ANY($2::bigint[])`, owner.ID, []int64{stranger.ID, contactViewer.ID, excludedContact.ID, closeFriendViewer.ID}); err != nil {
|
|
t.Fatalf("cleanup contacts: %v", err)
|
|
}
|
|
if _, err := pool.Exec(context.Background(), `
|
|
DELETE FROM users
|
|
WHERE id = ANY($1::bigint[])`, []int64{stranger.ID, contactViewer.ID, excludedContact.ID, closeFriendViewer.ID}); err != nil {
|
|
t.Fatalf("cleanup extra users: %v", err)
|
|
}
|
|
})
|
|
if _, err := pool.Exec(ctx, `
|
|
INSERT INTO contacts (user_id, contact_user_id, mutual, close_friend, contact_first_name, contact_last_name, contact_phone)
|
|
VALUES ($1, $2, false, false, 'Contact', 'Viewer', '7004'),
|
|
($1, $3, false, false, 'Excluded', 'Contact', '7005'),
|
|
($1, $4, false, true, 'Close', 'Friend', '7006')
|
|
ON CONFLICT (user_id, contact_user_id) DO UPDATE SET
|
|
close_friend = EXCLUDED.close_friend,
|
|
contact_first_name = EXCLUDED.contact_first_name,
|
|
contact_last_name = EXCLUDED.contact_last_name,
|
|
contact_phone = EXCLUDED.contact_phone`, owner.ID, contactViewer.ID, excludedContact.ID, closeFriendViewer.ID); err != nil {
|
|
t.Fatalf("insert contacts: %v", err)
|
|
}
|
|
|
|
store := NewStoryStore(pool)
|
|
ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}
|
|
stories := []domain.Story{
|
|
{
|
|
Owner: ownerPeer,
|
|
ID: 1,
|
|
Date: 1700000701,
|
|
ExpireDate: 1700009000,
|
|
Public: true,
|
|
DisallowUserIDs: []int64{stranger.ID},
|
|
PrivacyRules: []domain.PrivacyRule{
|
|
{Kind: domain.PrivacyRuleAllowAll},
|
|
{Kind: domain.PrivacyRuleDisallowUsers, UserIDs: []int64{stranger.ID}},
|
|
},
|
|
},
|
|
{
|
|
Owner: ownerPeer,
|
|
ID: 2,
|
|
Date: 1700000702,
|
|
ExpireDate: 1700009000,
|
|
SelectedContacts: true,
|
|
AllowUserIDs: []int64{selectedViewer.ID},
|
|
PrivacyRules: []domain.PrivacyRule{
|
|
{Kind: domain.PrivacyRuleAllowUsers, UserIDs: []int64{selectedViewer.ID}},
|
|
},
|
|
},
|
|
{
|
|
Owner: ownerPeer,
|
|
ID: 3,
|
|
Date: 1700000703,
|
|
ExpireDate: 1700009000,
|
|
Contacts: true,
|
|
DisallowUserIDs: []int64{excludedContact.ID},
|
|
PrivacyRules: []domain.PrivacyRule{
|
|
{Kind: domain.PrivacyRuleAllowContacts},
|
|
{Kind: domain.PrivacyRuleDisallowUsers, UserIDs: []int64{excludedContact.ID}},
|
|
},
|
|
},
|
|
{
|
|
Owner: ownerPeer,
|
|
ID: 4,
|
|
Date: 1700000704,
|
|
ExpireDate: 1700009000,
|
|
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)
|
|
}
|
|
}
|
|
|
|
afterRestart := NewStoryStore(pool)
|
|
selected, err := afterRestart.GetStoriesByID(ctx, selectedViewer.ID, ownerPeer, []int{1, 2, 3, 4}, 1700000800)
|
|
if err != nil {
|
|
t.Fatalf("selected viewer get stories: %v", err)
|
|
}
|
|
if gotIDs := pgStoryIDs(selected.Stories); !samePGStoryIDs(gotIDs, []int{1, 2}) {
|
|
t.Fatalf("selected viewer story ids = %v, want [1 2]", gotIDs)
|
|
}
|
|
if len(selected.Stories[1].PrivacyRules) != 1 || selected.Stories[1].AllowUserIDs[0] != selectedViewer.ID {
|
|
t.Fatalf("selected story privacy = %+v allow=%v, want persisted allow user", selected.Stories[1].PrivacyRules, selected.Stories[1].AllowUserIDs)
|
|
}
|
|
hidden, err := afterRestart.GetStoriesByID(ctx, stranger.ID, ownerPeer, []int{1, 2, 3, 4}, 1700000800)
|
|
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 := afterRestart.GetStoriesByID(ctx, contactViewer.ID, ownerPeer, []int{1, 2, 3, 4}, 1700000800)
|
|
if err != nil {
|
|
t.Fatalf("contact get stories: %v", err)
|
|
}
|
|
if gotIDs := pgStoryIDs(contacts.Stories); !samePGStoryIDs(gotIDs, []int{1, 3}) {
|
|
t.Fatalf("contact story ids = %v, want [1 3]", gotIDs)
|
|
}
|
|
excluded, err := afterRestart.GetStoriesByID(ctx, excludedContact.ID, ownerPeer, []int{3}, 1700000800)
|
|
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 := afterRestart.GetStoriesByID(ctx, closeFriendViewer.ID, ownerPeer, []int{1, 2, 3, 4}, 1700000800)
|
|
if err != nil {
|
|
t.Fatalf("close friend get stories: %v", err)
|
|
}
|
|
if gotIDs := pgStoryIDs(closeFriend.Stories); !samePGStoryIDs(gotIDs, []int{1, 3, 4}) {
|
|
t.Fatalf("close friend story ids = %v, want [1 3 4]", gotIDs)
|
|
}
|
|
if created, err := afterRestart.IncrementViews(ctx, stranger.ID, ownerPeer, []int{2}, 1700000801); err != nil || created != 0 {
|
|
t.Fatalf("hidden increment views = %d, %v; want 0 nil", created, err)
|
|
}
|
|
if _, err := afterRestart.SetReaction(ctx, stranger.ID, ownerPeer, 2, &domain.MessageReaction{Type: domain.MessageReactionEmoji, Emoticon: "like"}, 1700000802); err != domain.ErrStoryNotFound {
|
|
t.Fatalf("hidden set reaction err = %v, want ErrStoryNotFound", err)
|
|
}
|
|
recent, err := afterRestart.GetPeerMaxIDs(ctx, contactViewer.ID, []domain.Peer{ownerPeer}, 1700000803)
|
|
if err != nil {
|
|
t.Fatalf("contact peer max ids: %v", err)
|
|
}
|
|
if len(recent) != 1 || recent[0].MaxID != 3 {
|
|
t.Fatalf("contact recent = %+v, want max id 3", recent)
|
|
}
|
|
closeFriendRecent, err := afterRestart.GetPeerMaxIDs(ctx, closeFriendViewer.ID, []domain.Peer{ownerPeer}, 1700000803)
|
|
if err != nil {
|
|
t.Fatalf("close friend peer max ids: %v", err)
|
|
}
|
|
if len(closeFriendRecent) != 1 || closeFriendRecent[0].MaxID != 4 {
|
|
t.Fatalf("close friend recent = %+v, want max id 4", closeFriendRecent)
|
|
}
|
|
}
|
|
|
|
func TestStoryStoreCreateEditDeleteAndPinnedPostgres(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
owner, _ := createStoryTestUsers(t, ctx, pool)
|
|
store := NewStoryStore(pool)
|
|
ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}
|
|
media := &domain.MessageMedia{Kind: domain.MessageMediaKindPhoto, Photo: &domain.Photo{ID: 9701, AccessHash: 97, DCID: 2}}
|
|
|
|
created, err := store.CreateStory(ctx, domain.StoryCreateRequest{
|
|
Owner: ownerPeer,
|
|
RandomID: 99001,
|
|
Date: 1700000600,
|
|
Period: 86400,
|
|
Public: true,
|
|
Pinned: true,
|
|
Caption: "pg first",
|
|
Media: media,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create story: %v", err)
|
|
}
|
|
if created.Duplicate || created.Story.ID != 1 || created.Story.ExpireDate != 1700087000 {
|
|
t.Fatalf("created = %+v, want new story id 1", created)
|
|
}
|
|
dup, err := store.CreateStory(ctx, domain.StoryCreateRequest{
|
|
Owner: ownerPeer,
|
|
RandomID: 99001,
|
|
Date: 1700000601,
|
|
Period: 86400,
|
|
Public: true,
|
|
Caption: "duplicate",
|
|
Media: media,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create duplicate: %v", err)
|
|
}
|
|
if !dup.Duplicate || dup.Story.ID != created.Story.ID || dup.Story.Caption != "pg first" {
|
|
t.Fatalf("duplicate = %+v, want original story", dup)
|
|
}
|
|
selfStories, err := store.GetPeerStories(ctx, owner.ID, ownerPeer, 1700000602)
|
|
if err != nil {
|
|
t.Fatalf("get self stories: %v", err)
|
|
}
|
|
if selfStories.MaxReadID != created.Story.ID {
|
|
t.Fatalf("self max read = %d, want story id", selfStories.MaxReadID)
|
|
}
|
|
exact, err := store.GetStoriesByID(ctx, owner.ID, ownerPeer, []int{created.Story.ID, created.Story.ID}, 1700000602)
|
|
if err != nil {
|
|
t.Fatalf("get stories by duplicate ids: %v", err)
|
|
}
|
|
if len(exact.Stories) != 1 || exact.Stories[0].ID != created.Story.ID {
|
|
t.Fatalf("exact stories = %+v, want one deduped story", exact.Stories)
|
|
}
|
|
if _, err := store.GetStoriesByID(ctx, owner.ID, ownerPeer, nil, 1700000602); !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: ownerPeer,
|
|
ID: created.Story.ID,
|
|
Caption: "pg edited",
|
|
UpdateCaption: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("edit story: %v", err)
|
|
}
|
|
if edited.Previous.Caption != "pg first" || edited.Previous.Edited {
|
|
t.Fatalf("previous = %+v, want pre-edit story", edited.Previous)
|
|
}
|
|
if !edited.Story.Edited || edited.Story.Caption != "pg edited" {
|
|
t.Fatalf("edited = %+v, want edited caption", edited)
|
|
}
|
|
pinned, err := store.ListPinnedStories(ctx, owner.ID, ownerPeer, 0, 10, 1700000603)
|
|
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)
|
|
}
|
|
toggled, err := store.TogglePinned(ctx, ownerPeer, []int{created.Story.ID, created.Story.ID}, false, 1700000604)
|
|
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 unpinned story", toggled)
|
|
}
|
|
if len(toggled.Previous) != 1 || toggled.Previous[0].Deleted || !toggled.Previous[0].Pinned {
|
|
t.Fatalf("toggled previous = %+v, want pre-toggle pinned snapshot", toggled.Previous)
|
|
}
|
|
retryToggled, err := store.TogglePinned(ctx, ownerPeer, []int{created.Story.ID}, false, 1700000605)
|
|
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)
|
|
}
|
|
emptyToggle, err := store.TogglePinned(ctx, ownerPeer, nil, true, 1700000604)
|
|
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, ownerPeer, []int{created.Story.ID, created.Story.ID}, 1700000605)
|
|
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 deleted story", deleted)
|
|
}
|
|
retryDeleted, err := store.DeleteStories(ctx, ownerPeer, []int{created.Story.ID}, 1700000606)
|
|
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)
|
|
}
|
|
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{
|
|
Owner: ownerPeer,
|
|
ID: 77,
|
|
Date: 1700000000,
|
|
ExpireDate: 1700000100,
|
|
Pinned: true,
|
|
Public: true,
|
|
Media: media,
|
|
}}); err != nil {
|
|
t.Fatalf("upsert expired pinned story: %v", err)
|
|
}
|
|
expiredPinnedDeleted, err := store.DeleteStories(ctx, ownerPeer, []int{77}, 1700000607)
|
|
if err != nil {
|
|
t.Fatalf("delete expired pinned story: %v", err)
|
|
}
|
|
if len(expiredPinnedDeleted.Stories) != 1 || !expiredPinnedDeleted.Stories[0].Deleted || expiredPinnedDeleted.Stories[0].Pinned {
|
|
t.Fatalf("expired pinned deleted = %+v, want deleted unpinned snapshot", expiredPinnedDeleted)
|
|
}
|
|
if len(expiredPinnedDeleted.Previous) != 1 || expiredPinnedDeleted.Previous[0].Deleted || !expiredPinnedDeleted.Previous[0].Pinned {
|
|
t.Fatalf("expired pinned previous = %+v, want pre-delete pinned snapshot", expiredPinnedDeleted.Previous)
|
|
}
|
|
active, err := store.GetPeerStories(ctx, owner.ID, ownerPeer, 1700000606)
|
|
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 TestStoryStorePinnedToTopOrderPostgres(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
owner, _ := createStoryTestUsers(t, ctx, pool)
|
|
store := NewStoryStore(pool)
|
|
ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}
|
|
for _, id := range []int{1, 2, 3, 4} {
|
|
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{
|
|
Owner: ownerPeer,
|
|
ID: id,
|
|
Date: 1700000700 + id,
|
|
ExpireDate: 1700009000,
|
|
Pinned: true,
|
|
Public: true,
|
|
}}); err != nil {
|
|
t.Fatalf("upsert story %d: %v", id, err)
|
|
}
|
|
}
|
|
if err := store.TogglePinnedToTop(ctx, ownerPeer, []int{3, 1, 3}); err != nil {
|
|
t.Fatalf("toggle pinned to top: %v", err)
|
|
}
|
|
pinned, err := store.ListPinnedStories(ctx, owner.ID, ownerPeer, 0, 2, 1700000800)
|
|
if err != nil {
|
|
t.Fatalf("list pinned stories: %v", err)
|
|
}
|
|
if pinned.Count != 4 || !samePGStoryIDs(pgStoryIDs(pinned.Stories), []int{4, 3}) || !samePGStoryIDs(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, pgStoryIDs(pinned.Stories), pinned.PinnedToTop)
|
|
}
|
|
androidInitial, err := store.ListPinnedStories(ctx, owner.ID, ownerPeer, -1, 2, 1700000800)
|
|
if err != nil {
|
|
t.Fatalf("list pinned stories with android sentinel: %v", err)
|
|
}
|
|
if androidInitial.Count != pinned.Count || !samePGStoryIDs(pgStoryIDs(androidInitial.Stories), []int{4, 3}) || !samePGStoryIDs(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, pgStoryIDs(androidInitial.Stories), androidInitial.PinnedToTop)
|
|
}
|
|
if err := store.TogglePinnedToTop(ctx, ownerPeer, []int{2, 1, 4}); err != nil {
|
|
t.Fatalf("replace pinned to top: %v", err)
|
|
}
|
|
pinned, err = store.ListPinnedStories(ctx, owner.ID, ownerPeer, 0, 10, 1700000801)
|
|
if err != nil {
|
|
t.Fatalf("list replaced pinned stories: %v", err)
|
|
}
|
|
if !samePGStoryIDs(pinned.PinnedToTop, []int{2, 1, 4}) {
|
|
t.Fatalf("replaced top = %v, want 2,1,4", pinned.PinnedToTop)
|
|
}
|
|
pageAfterOffset, err := store.ListPinnedStories(ctx, owner.ID, ownerPeer, 3, 10, 1700000801)
|
|
if err != nil {
|
|
t.Fatalf("list pinned page after offset: %v", err)
|
|
}
|
|
if !samePGStoryIDs(pgStoryIDs(pageAfterOffset.Stories), []int{2, 1}) || !samePGStoryIDs(pageAfterOffset.PinnedToTop, []int{2, 1, 4}) {
|
|
t.Fatalf("offset page stories %v top %v, want stories 2,1 and full top 2,1,4", pgStoryIDs(pageAfterOffset.Stories), pageAfterOffset.PinnedToTop)
|
|
}
|
|
pageAfterTail, err := store.ListPinnedStories(ctx, owner.ID, ownerPeer, 1, 10, 1700000801)
|
|
if err != nil {
|
|
t.Fatalf("list pinned page after tail: %v", err)
|
|
}
|
|
if pageAfterTail.Count != 4 || len(pageAfterTail.Stories) != 0 || !samePGStoryIDs(pageAfterTail.PinnedToTop, []int{2, 1, 4}) {
|
|
t.Fatalf("tail page = count %d stories %v top %v, want count 4 empty stories top 2,1,4", pageAfterTail.Count, pgStoryIDs(pageAfterTail.Stories), pageAfterTail.PinnedToTop)
|
|
}
|
|
if _, err := store.TogglePinned(ctx, ownerPeer, []int{2}, false, 1700000802); err != nil {
|
|
t.Fatalf("unpin story: %v", err)
|
|
}
|
|
pinned, err = store.ListPinnedStories(ctx, owner.ID, ownerPeer, 0, 10, 1700000803)
|
|
if err != nil {
|
|
t.Fatalf("list after unpin: %v", err)
|
|
}
|
|
if !samePGStoryIDs(pinned.PinnedToTop, []int{1, 4}) {
|
|
t.Fatalf("top after unpin = %v, want 1,4", pinned.PinnedToTop)
|
|
}
|
|
if err := store.TogglePinnedToTop(ctx, ownerPeer, []int{2}); !errors.Is(err, domain.ErrStoryIDInvalid) {
|
|
t.Fatalf("unpinned top err = %v, want ErrStoryIDInvalid", err)
|
|
}
|
|
if _, err := store.DeleteStories(ctx, ownerPeer, []int{1}, 1700000804); err != nil {
|
|
t.Fatalf("delete story: %v", err)
|
|
}
|
|
pinned, err = store.ListPinnedStories(ctx, owner.ID, ownerPeer, 0, 10, 1700000805)
|
|
if err != nil {
|
|
t.Fatalf("list after delete: %v", err)
|
|
}
|
|
if !samePGStoryIDs(pinned.PinnedToTop, []int{4}) {
|
|
t.Fatalf("top after delete = %v, want 4", pinned.PinnedToTop)
|
|
}
|
|
if err := store.TogglePinnedToTop(ctx, ownerPeer, nil); err != nil {
|
|
t.Fatalf("clear pinned to top: %v", err)
|
|
}
|
|
pinned, err = store.ListPinnedStories(ctx, owner.ID, ownerPeer, 0, 10, 1700000806)
|
|
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 pgStoryIDs(stories []domain.Story) []int {
|
|
out := make([]int, 0, len(stories))
|
|
for _, story := range stories {
|
|
out = append(out, story.ID)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func samePGStoryIDs(got, want []int) bool {
|
|
if len(got) != len(want) {
|
|
return false
|
|
}
|
|
for i := range got {
|
|
if got[i] != want[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func TestStoryUpdateEventPayloadPostgres(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
owner, _ := createStoryTestUsers(t, ctx, pool)
|
|
events := NewUpdateEventStore(pool)
|
|
ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}
|
|
story := domain.Story{
|
|
Owner: ownerPeer,
|
|
ID: 7,
|
|
Date: 1700000400,
|
|
ExpireDate: 1700004000,
|
|
Public: true,
|
|
Caption: "durable payload",
|
|
}
|
|
reaction := &domain.MessageReaction{Type: domain.MessageReactionEmoji, Emoticon: "spark"}
|
|
|
|
if err := events.Append(ctx, owner.ID, domain.UpdateEvent{
|
|
Type: domain.UpdateEventStory,
|
|
Pts: 1,
|
|
PtsCount: 1,
|
|
Date: story.Date,
|
|
Peer: ownerPeer,
|
|
Story: story,
|
|
}); err != nil {
|
|
t.Fatalf("append story event: %v", err)
|
|
}
|
|
if err := events.Append(ctx, owner.ID, domain.UpdateEvent{
|
|
Type: domain.UpdateEventSentStoryReaction,
|
|
Pts: 2,
|
|
PtsCount: 1,
|
|
Date: 1700000401,
|
|
Peer: ownerPeer,
|
|
MaxID: story.ID,
|
|
Story: story,
|
|
Reaction: reaction,
|
|
}); err != nil {
|
|
t.Fatalf("append reaction event: %v", err)
|
|
}
|
|
|
|
listed, err := events.ListAfter(ctx, owner.ID, 0, 10)
|
|
if err != nil {
|
|
t.Fatalf("list events: %v", err)
|
|
}
|
|
if len(listed) != 2 || listed[0].Story.ID != story.ID || listed[0].Story.Caption != story.Caption {
|
|
t.Fatalf("listed story events = %+v, want story payload", listed)
|
|
}
|
|
if listed[1].Reaction == nil || *listed[1].Reaction != *reaction || listed[1].Story.ID != story.ID {
|
|
t.Fatalf("listed reaction event = %+v, want reaction and story payload", listed[1])
|
|
}
|
|
|
|
batched, err := events.BatchByCursor(ctx, []storepkg.EventCursor{{UserID: owner.ID, Pts: 2}})
|
|
if err != nil {
|
|
t.Fatalf("batch events: %v", err)
|
|
}
|
|
if len(batched) != 1 || batched[0].Reaction == nil || *batched[0].Reaction != *reaction || batched[0].Story.ID != story.ID {
|
|
t.Fatalf("batched event = %+v, want durable reaction payload", batched)
|
|
}
|
|
}
|
|
|
|
func createStoryTestUsers(t *testing.T, ctx context.Context, pool *pgxpool.Pool) (domain.User, domain.User) {
|
|
t.Helper()
|
|
suffix := randomSuffix(t)
|
|
users := NewUserStore(pool)
|
|
owner, err := users.Create(ctx, domain.User{AccessHash: 8101, Phone: "+1771" + suffix + "01", FirstName: "StoryOwner"})
|
|
if err != nil {
|
|
t.Fatalf("create owner: %v", err)
|
|
}
|
|
viewer, err := users.Create(ctx, domain.User{AccessHash: 8102, Phone: "+1771" + suffix + "02", FirstName: "StoryViewer"})
|
|
if err != nil {
|
|
t.Fatalf("create viewer: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
cleanupStoryTestRows(t, context.Background(), pool, owner.ID, viewer.ID)
|
|
})
|
|
return owner, viewer
|
|
}
|
|
|
|
func cleanupStoryTestRows(t *testing.T, ctx context.Context, pool *pgxpool.Pool, ownerID, viewerID int64) {
|
|
t.Helper()
|
|
if _, err := pool.Exec(ctx, `
|
|
DELETE FROM story_hidden_peers
|
|
WHERE viewer_user_id = $1
|
|
OR owner_peer_id = $2`, viewerID, ownerID); err != nil {
|
|
t.Fatalf("cleanup story hidden peers: %v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
DELETE FROM story_read_states
|
|
WHERE viewer_user_id = $1
|
|
OR owner_peer_id = $2`, viewerID, ownerID); err != nil {
|
|
t.Fatalf("cleanup story read states: %v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
DELETE FROM story_views
|
|
WHERE viewer_user_id = $1
|
|
OR owner_peer_id = $2`, viewerID, ownerID); err != nil {
|
|
t.Fatalf("cleanup story views: %v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
DELETE FROM story_exposures
|
|
WHERE viewer_user_id = $1
|
|
OR owner_peer_id = $2`, viewerID, ownerID); err != nil {
|
|
t.Fatalf("cleanup story exposures: %v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
DELETE FROM stories
|
|
WHERE owner_peer_id = ANY($1::bigint[])`, []int64{ownerID, viewerID}); err != nil {
|
|
t.Fatalf("cleanup stories: %v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
DELETE FROM users
|
|
WHERE id = ANY($1::bigint[])`, []int64{ownerID, viewerID}); err != nil {
|
|
t.Fatalf("cleanup story users: %v", err)
|
|
}
|
|
}
|
|
|
|
func cleanupChannelStoryTestRows(t *testing.T, ctx context.Context, pool *pgxpool.Pool, channelID int64, userIDs []int64) {
|
|
t.Helper()
|
|
if _, err := pool.Exec(ctx, `
|
|
DELETE FROM story_hidden_peers
|
|
WHERE (owner_peer_type = 'channel' AND owner_peer_id = $1)
|
|
OR viewer_user_id = ANY($2::bigint[])`, channelID, userIDs); err != nil {
|
|
t.Fatalf("cleanup channel story hidden peers: %v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
DELETE FROM story_read_states
|
|
WHERE (owner_peer_type = 'channel' AND owner_peer_id = $1)
|
|
OR viewer_user_id = ANY($2::bigint[])`, channelID, userIDs); err != nil {
|
|
t.Fatalf("cleanup channel story read states: %v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
DELETE FROM story_views
|
|
WHERE (owner_peer_type = 'channel' AND owner_peer_id = $1)
|
|
OR viewer_user_id = ANY($2::bigint[])`, channelID, userIDs); err != nil {
|
|
t.Fatalf("cleanup channel story views: %v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
DELETE FROM story_exposures
|
|
WHERE (owner_peer_type = 'channel' AND owner_peer_id = $1)
|
|
OR viewer_user_id = ANY($2::bigint[])`, channelID, userIDs); err != nil {
|
|
t.Fatalf("cleanup channel story exposures: %v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
DELETE FROM stories
|
|
WHERE owner_peer_type = 'channel'
|
|
AND owner_peer_id = $1`, channelID); err != nil {
|
|
t.Fatalf("cleanup channel stories: %v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `DELETE FROM channels WHERE id = $1`, channelID); err != nil {
|
|
t.Fatalf("cleanup channel: %v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `DELETE FROM users WHERE id = ANY($1::bigint[])`, userIDs); err != nil {
|
|
t.Fatalf("cleanup channel story users: %v", err)
|
|
}
|
|
}
|
|
|
|
func storyListContains(list domain.StoryList, peer domain.Peer, storyID int) bool {
|
|
for _, story := range list.Stories {
|
|
if story.Owner == peer && story.ID == storyID {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func cleanupStoryPagingTestRows(t *testing.T, ctx context.Context, pool *pgxpool.Pool, viewerID int64, ownerIDs []int64) {
|
|
t.Helper()
|
|
if _, err := pool.Exec(ctx, `
|
|
DELETE FROM story_hidden_peers
|
|
WHERE viewer_user_id = $1
|
|
OR owner_peer_id = ANY($2::bigint[])`, viewerID, ownerIDs); err != nil {
|
|
t.Fatalf("cleanup story paging hidden peers: %v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
DELETE FROM story_read_states
|
|
WHERE viewer_user_id = $1
|
|
OR owner_peer_id = ANY($2::bigint[])`, viewerID, ownerIDs); err != nil {
|
|
t.Fatalf("cleanup story paging read states: %v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
DELETE FROM story_views
|
|
WHERE viewer_user_id = $1
|
|
OR owner_peer_id = ANY($2::bigint[])`, viewerID, ownerIDs); err != nil {
|
|
t.Fatalf("cleanup story paging views: %v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
DELETE FROM story_exposures
|
|
WHERE viewer_user_id = $1
|
|
OR owner_peer_id = ANY($2::bigint[])`, viewerID, ownerIDs); err != nil {
|
|
t.Fatalf("cleanup story paging exposures: %v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
DELETE FROM stories
|
|
WHERE owner_peer_id = ANY($1::bigint[])`, ownerIDs); err != nil {
|
|
t.Fatalf("cleanup story paging stories: %v", err)
|
|
}
|
|
userIDs := append([]int64{viewerID}, ownerIDs...)
|
|
if _, err := pool.Exec(ctx, `
|
|
DELETE FROM users
|
|
WHERE id = ANY($1::bigint[])`, userIDs); err != nil {
|
|
t.Fatalf("cleanup story paging users: %v", err)
|
|
}
|
|
}
|