owpengram-server/internal/app/stories/service_test.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
}