313 lines
9.8 KiB
Go
313 lines
9.8 KiB
Go
package stories
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"telesrv/internal/domain"
|
|
"telesrv/internal/store/memory"
|
|
)
|
|
|
|
func TestReadStoriesClampsFutureMaxIDToVisibleActiveStory(t *testing.T) {
|
|
ctx := context.Background()
|
|
store := memory.NewStoryStore()
|
|
service := NewService(store)
|
|
owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1001}
|
|
viewerID := int64(2001)
|
|
for _, id := range []int{1, 2} {
|
|
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{
|
|
Owner: owner,
|
|
ID: id,
|
|
Date: 100 + id,
|
|
ExpireDate: 1000,
|
|
Public: true,
|
|
}}); err != nil {
|
|
t.Fatalf("upsert story %d: %v", id, err)
|
|
}
|
|
}
|
|
|
|
read, err := service.ReadStories(ctx, viewerID, owner, 99, 200)
|
|
if err != nil {
|
|
t.Fatalf("read stories: %v", err)
|
|
}
|
|
if !read.Advanced || read.MaxReadID != 2 {
|
|
t.Fatalf("read = %+v, want advanced max 2", read)
|
|
}
|
|
|
|
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{
|
|
Owner: owner,
|
|
ID: 3,
|
|
Date: 300,
|
|
ExpireDate: 1000,
|
|
Public: true,
|
|
}}); err != nil {
|
|
t.Fatalf("upsert future story: %v", err)
|
|
}
|
|
peerStories, err := store.GetPeerStories(ctx, viewerID, owner, 400)
|
|
if err != nil {
|
|
t.Fatalf("get peer stories: %v", err)
|
|
}
|
|
if peerStories.MaxReadID != 2 || len(peerStories.Stories) != 3 {
|
|
t.Fatalf("peer stories = %+v, want max read 2 and three active stories", peerStories)
|
|
}
|
|
}
|
|
|
|
func TestReadStoriesWithoutVisibleActiveStoryDoesNotWriteFutureBoundary(t *testing.T) {
|
|
ctx := context.Background()
|
|
store := memory.NewStoryStore()
|
|
service := NewService(store)
|
|
owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1001}
|
|
viewerID := int64(2001)
|
|
|
|
read, err := service.ReadStories(ctx, viewerID, owner, 99, 200)
|
|
if err != nil {
|
|
t.Fatalf("read stories: %v", err)
|
|
}
|
|
if read.Advanced || read.MaxReadID != 0 {
|
|
t.Fatalf("read = %+v, want no-op zero boundary", read)
|
|
}
|
|
states, err := store.ListReadStates(ctx, viewerID)
|
|
if err != nil {
|
|
t.Fatalf("list read states: %v", err)
|
|
}
|
|
if len(states) != 0 {
|
|
t.Fatalf("read states = %+v, want none", states)
|
|
}
|
|
|
|
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{
|
|
Owner: owner,
|
|
ID: 1,
|
|
Date: 300,
|
|
ExpireDate: 1000,
|
|
Public: true,
|
|
}}); err != nil {
|
|
t.Fatalf("upsert story after no-op read: %v", err)
|
|
}
|
|
peerStories, err := store.GetPeerStories(ctx, viewerID, owner, 400)
|
|
if err != nil {
|
|
t.Fatalf("get peer stories: %v", err)
|
|
}
|
|
if peerStories.MaxReadID != 0 || len(peerStories.Stories) != 1 {
|
|
t.Fatalf("peer stories = %+v, want unread first story", peerStories)
|
|
}
|
|
}
|
|
|
|
func TestReadStoriesWithoutStoreIsNoop(t *testing.T) {
|
|
ctx := context.Background()
|
|
service := NewService(nil)
|
|
owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1001}
|
|
|
|
read, err := service.ReadStories(ctx, 2001, owner, 99, 200)
|
|
if err != nil {
|
|
t.Fatalf("read stories without store: %v", err)
|
|
}
|
|
if read.Advanced || read.MaxReadID != 0 || read.Peer != owner || read.ViewerID != 2001 {
|
|
t.Fatalf("read without store = %+v, want no-op zero boundary for viewer/peer", read)
|
|
}
|
|
}
|
|
|
|
func TestGetStoryReactionsListRequiresChannelInteractionGrant(t *testing.T) {
|
|
ctx := context.Background()
|
|
store := memory.NewStoryStore()
|
|
service := NewService(store)
|
|
owner := domain.Peer{Type: domain.PeerTypeChannel, ID: 1001}
|
|
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{
|
|
Owner: owner,
|
|
ID: 1,
|
|
Date: 100,
|
|
ExpireDate: 1000,
|
|
Public: true,
|
|
}}); err != nil {
|
|
t.Fatalf("upsert story: %v", err)
|
|
}
|
|
if _, err := store.SetReaction(ctx, 2001, owner, 1, &domain.MessageReaction{
|
|
Type: domain.MessageReactionEmoji,
|
|
Emoticon: "🔥",
|
|
}, 110); err != nil {
|
|
t.Fatalf("set reaction: %v", err)
|
|
}
|
|
|
|
_, err := service.GetStoryReactionsList(ctx, 3001, domain.StoryReactionListRequest{
|
|
Owner: owner,
|
|
StoryID: 1,
|
|
Limit: 10,
|
|
})
|
|
if !errors.Is(err, domain.ErrStoryPeerInvalid) {
|
|
t.Fatalf("ungranted channel reaction list err = %v, want ErrStoryPeerInvalid", err)
|
|
}
|
|
|
|
list, err := service.GetStoryReactionsList(ctx, 3001, domain.StoryReactionListRequest{
|
|
Owner: owner,
|
|
StoryID: 1,
|
|
Limit: 10,
|
|
CanViewOwnerInteractions: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("granted channel reaction list: %v", err)
|
|
}
|
|
if list.Count != 1 || len(list.Reactions) != 1 || list.Reactions[0].ViewerID != 2001 {
|
|
t.Fatalf("reaction list = %+v, want viewer 2001", list)
|
|
}
|
|
}
|
|
|
|
func TestGetStoryReactionsListRejectsUserPeer(t *testing.T) {
|
|
ctx := context.Background()
|
|
service := NewService(memory.NewStoryStore())
|
|
_, err := service.GetStoryReactionsList(ctx, 1001, domain.StoryReactionListRequest{
|
|
Owner: domain.Peer{Type: domain.PeerTypeUser, ID: 1001},
|
|
StoryID: 1,
|
|
Limit: 10,
|
|
CanViewOwnerInteractions: true,
|
|
})
|
|
if !errors.Is(err, domain.ErrStoryPeerInvalid) {
|
|
t.Fatalf("user story reaction list err = %v, want ErrStoryPeerInvalid", err)
|
|
}
|
|
}
|
|
|
|
func TestStoryInteractionListsRejectMalformedOffsets(t *testing.T) {
|
|
ctx := context.Background()
|
|
service := NewService(nil)
|
|
owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1001}
|
|
if _, err := service.GetStoryViewsList(ctx, owner.ID, domain.StoryViewListRequest{
|
|
Owner: owner,
|
|
StoryID: 1,
|
|
Offset: "bad",
|
|
Limit: 10,
|
|
}); !errors.Is(err, domain.ErrStoryOffsetInvalid) {
|
|
t.Fatalf("bad views offset err = %v, want ErrStoryOffsetInvalid", err)
|
|
}
|
|
if _, err := service.GetStoryViewsList(ctx, owner.ID, domain.StoryViewListRequest{
|
|
Owner: owner,
|
|
StoryID: 1,
|
|
Offset: strings.Repeat("9", domain.MaxStoryInteractionOffsetLength+1),
|
|
Limit: 10,
|
|
}); !errors.Is(err, domain.ErrStoryOffsetInvalid) {
|
|
t.Fatalf("oversized views offset err = %v, want ErrStoryOffsetInvalid", err)
|
|
}
|
|
if _, err := service.GetStoryReactionsList(ctx, 2001, domain.StoryReactionListRequest{
|
|
Owner: domain.Peer{Type: domain.PeerTypeChannel, ID: 3001},
|
|
StoryID: 1,
|
|
Offset: "1:1700000000:2001",
|
|
Limit: 10,
|
|
CanViewOwnerInteractions: true,
|
|
}); !errors.Is(err, domain.ErrStoryOffsetInvalid) {
|
|
t.Fatalf("reaction group offset err = %v, want ErrStoryOffsetInvalid", err)
|
|
}
|
|
if _, err := service.GetStoryReactionsList(ctx, 2001, domain.StoryReactionListRequest{
|
|
Owner: domain.Peer{Type: domain.PeerTypeChannel, ID: 3001},
|
|
StoryID: 1,
|
|
Offset: "1:1700000000:2001",
|
|
Limit: 10,
|
|
ForwardsFirst: true,
|
|
CanViewOwnerInteractions: true,
|
|
}); err != nil {
|
|
t.Fatalf("reaction forwards-first group offset err = %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
func TestGetStoriesByIDDedupesAndRejectsEmptyIDs(t *testing.T) {
|
|
ctx := context.Background()
|
|
store := memory.NewStoryStore()
|
|
service := NewService(store)
|
|
owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1001}
|
|
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{
|
|
Owner: owner,
|
|
ID: 3,
|
|
Date: 100,
|
|
ExpireDate: 1000,
|
|
Public: true,
|
|
}}); err != nil {
|
|
t.Fatalf("upsert story: %v", err)
|
|
}
|
|
|
|
list, err := service.GetStoriesByID(ctx, owner.ID, owner, []int{3, 3}, 200)
|
|
if err != nil {
|
|
t.Fatalf("get stories by duplicate ids: %v", err)
|
|
}
|
|
if len(list.Stories) != 1 || list.Stories[0].ID != 3 {
|
|
t.Fatalf("stories = %+v, want one deduped story", list.Stories)
|
|
}
|
|
|
|
if _, err := service.GetStoriesByID(ctx, owner.ID, owner, nil, 200); !errors.Is(err, domain.ErrStoryIDInvalid) {
|
|
t.Fatalf("empty get stories by id err = %v, want ErrStoryIDInvalid", err)
|
|
}
|
|
}
|
|
|
|
func TestIncrementViewsDedupeAndRejectsEmptyIDs(t *testing.T) {
|
|
ctx := context.Background()
|
|
store := memory.NewStoryStore()
|
|
service := NewService(store)
|
|
owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1001}
|
|
if _, err := store.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{
|
|
Owner: owner,
|
|
ID: 7,
|
|
Date: 100,
|
|
ExpireDate: 1000,
|
|
Public: true,
|
|
}}); err != nil {
|
|
t.Fatalf("upsert story: %v", err)
|
|
}
|
|
|
|
created, err := service.IncrementViews(ctx, 2001, owner, []int{7, 7}, 200)
|
|
if err != nil {
|
|
t.Fatalf("increment views duplicate ids: %v", err)
|
|
}
|
|
if created != 1 {
|
|
t.Fatalf("created views = %d, want one deduped view", created)
|
|
}
|
|
|
|
if _, err := service.IncrementViews(ctx, 2001, owner, nil, 200); !errors.Is(err, domain.ErrStoryIDInvalid) {
|
|
t.Fatalf("empty increment views err = %v, want ErrStoryIDInvalid", err)
|
|
}
|
|
}
|
|
|
|
func TestStoryMutationsDedupeIDsBeforeStore(t *testing.T) {
|
|
ctx := context.Background()
|
|
service := NewService(nil)
|
|
owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1001}
|
|
|
|
pinned, err := service.TogglePinned(ctx, owner.ID, owner, []int{3, 3, 4}, true, 100)
|
|
if err != nil {
|
|
t.Fatalf("toggle pinned: %v", err)
|
|
}
|
|
if got, want := pinned.IDs, []int{3, 4}; !intSlicesEqual(got, want) {
|
|
t.Fatalf("pinned ids = %v, want %v", got, want)
|
|
}
|
|
|
|
emptyPinned, err := service.TogglePinned(ctx, owner.ID, owner, nil, true, 100)
|
|
if err != nil {
|
|
t.Fatalf("empty toggle pinned: %v", err)
|
|
}
|
|
if len(emptyPinned.IDs) != 0 || len(emptyPinned.Stories) != 0 {
|
|
t.Fatalf("empty pinned = %+v, want no-op", emptyPinned)
|
|
}
|
|
|
|
deleted, err := service.DeleteStories(ctx, owner.ID, owner, []int{4, 3, 4}, 101)
|
|
if err != nil {
|
|
t.Fatalf("delete stories: %v", err)
|
|
}
|
|
if got, want := deleted.IDs, []int{4, 3}; !intSlicesEqual(got, want) {
|
|
t.Fatalf("deleted ids = %v, want %v", got, want)
|
|
}
|
|
|
|
if err := service.TogglePinnedToTop(ctx, owner.ID, owner, []int{4, 3, 4}); err != nil {
|
|
t.Fatalf("toggle pinned to top deduped ids: %v", err)
|
|
}
|
|
if err := service.TogglePinnedToTop(ctx, owner.ID, owner, []int{1, 2, 3, 4}); !errors.Is(err, domain.ErrStoryIDInvalid) {
|
|
t.Fatalf("oversized pinned-to-top err = %v, want ErrStoryIDInvalid", err)
|
|
}
|
|
}
|
|
|
|
func intSlicesEqual(a, b []int) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|