From 9a26d92131761c0b8f01a243ff30ea6cf3b65594 Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Tue, 19 Mar 2024 11:08:06 +0100 Subject: [PATCH] feat: Add Ownable, proper sort by timestamp (#8) * add ownable, proper time sort * make tests better * add remove post test * add handler for panic in RemovePost * formatting * remove comments, move types around * fix nit * Revert "fix nit" This reverts commit cca7ff0ab9d037fd95fb62db3a83a6e09bef7912. * Revert "Revert "fix nit"" This reverts commit 7a5df33bbce0bcde71e3eb3c601ea33dd6af9099. * fix sorter, add godoc comments * order imports --- api/p/memeland/memeland.gno | 80 ++++++++++++---- api/p/memeland/memeland_test.gno | 153 +++++++++++++++---------------- api/r/memeland/memeland.gno | 3 +- 3 files changed, 135 insertions(+), 101 deletions(-) diff --git a/api/p/memeland/memeland.gno b/api/p/memeland/memeland.gno index 011a3cc..bd25ae5 100644 --- a/api/p/memeland/memeland.gno +++ b/api/p/memeland/memeland.gno @@ -1,16 +1,17 @@ package memeland import ( - "gno.land/p/demo/avl" - "gno.land/p/demo/seqid" "sort" "std" "strconv" "strings" "time" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ownable" + "gno.land/p/demo/seqid" ) -// Post - as previously defined type Post struct { ID string Data string @@ -20,15 +21,15 @@ type Post struct { } type Memeland struct { + *ownable.Ownable Posts []*Post MemeCounter seqid.ID } -type UpvoteSorter []*Post - func NewMemeland() *Memeland { return &Memeland{ - Posts: make([]*Post, 0), + Ownable: ownable.New(), + Posts: make([]*Post, 0), } } @@ -70,7 +71,7 @@ func (m *Memeland) Upvote(id string) string { return "upvote successful" } -// GetPostsInRange returns a JSON string of posts within the given timestamp range, supporting pagination. +// GetPostsInRange returns a JSON string of posts within the given timestamp range, supporting pagination func (m *Memeland) GetPostsInRange(startTimestamp, endTimestamp int64, page, pageSize int, sortBy string) string { if len(m.Posts) == 0 { return "[]" @@ -80,10 +81,16 @@ func (m *Memeland) GetPostsInRange(startTimestamp, endTimestamp int64, page, pag panic("page count cannot be less than 1") } + // No empty pages if pageSize < 1 { panic("page size cannot be less than 1") } + // No pages larger than 10 + if pageSize > 10 { + panic("page size cannot be larger than 10") + } + var filteredPosts []*Post start := time.Unix(startTimestamp, 0) @@ -99,10 +106,22 @@ func (m *Memeland) GetPostsInRange(startTimestamp, endTimestamp int64, page, pag switch sortBy { // Sort by upvote descending case "UPVOTES": - sort.Sort(UpvoteSorter(filteredPosts)) + dateSorter := PostSorter{ + Posts: filteredPosts, + LessF: func(i, j int) bool { + return filteredPosts[i].UpvoteTracker.Size() > filteredPosts[j].UpvoteTracker.Size() + }, + } + sort.Sort(dateSorter) default: // Sort by timestamp, beginning with newest - filteredPosts = reversePosts(filteredPosts) + dateSorter := PostSorter{ + Posts: filteredPosts, + LessF: func(i, j int) bool { + return filteredPosts[i].Timestamp.Before(filteredPosts[j].Timestamp) + }, + } + sort.Sort(dateSorter) } // Pagination @@ -123,7 +142,24 @@ func (m *Memeland) GetPostsInRange(startTimestamp, endTimestamp int64, page, pag return PostsToJSONString(filteredPosts[startIndex:endIndex]) } -// PostsToJSONString converts a slice of Post structs into a JSON string representation. +// RemovePost allows the owner to remove a post with a specific ID +func (m *Memeland) RemovePost(id string) string { + if err := m.CallerIsOwner(); err != nil { + panic(err) + } + + posts := m.Posts + for i, post := range posts { + if post.ID == id { + posts = append(posts[:i], posts[i+1:]...) + return id + } + } + + panic("post with specified id does not exist") +} + +// PostsToJSONString converts a slice of Post structs into a JSON string func PostsToJSONString(posts []*Post) string { var sb strings.Builder sb.WriteString("[") @@ -140,6 +176,7 @@ func PostsToJSONString(posts []*Post) string { return sb.String() } +// PostToJSONString returns a Post formatted as a JSON string func PostToJSONString(post *Post) string { var sb strings.Builder @@ -169,15 +206,20 @@ func (m *Memeland) getPost(id string) *Post { return nil } -func reversePosts(posts []*Post) []*Post { - for i, j := 0, len(posts)-1; i < j; i, j = i+1, j-1 { - posts[i], posts[j] = posts[j], posts[i] - } - return posts +// PostSorter is a flexible sorter for the *Post slice +type PostSorter struct { + Posts []*Post + LessF func(i, j int) bool +} + +func (p PostSorter) Len() int { + return len(p.Posts) +} + +func (p PostSorter) Swap(i, j int) { + p.Posts[i], p.Posts[j] = p.Posts[j], p.Posts[i] } -func (a UpvoteSorter) Len() int { return len(a) } -func (a UpvoteSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a UpvoteSorter) Less(i, j int) bool { - return a[i].UpvoteTracker.Size() > a[j].UpvoteTracker.Size() +func (p PostSorter) Less(i, j int) bool { + return p.LessF(i, j) } diff --git a/api/p/memeland/memeland_test.gno b/api/p/memeland/memeland_test.gno index 92bb8da..275657a 100644 --- a/api/p/memeland/memeland_test.gno +++ b/api/p/memeland/memeland_test.gno @@ -22,11 +22,11 @@ func TestGetPostsInRangePagination(t *testing.T) { m := NewMemeland() now := time.Now() - numOfPosts := 10 + numOfPosts := 5 var memeData []string for i := 1; i <= numOfPosts; i++ { // Prepare meme data - nextTime := now.Add(time.Duration(-i) * time.Minute) + nextTime := now.Add(time.Duration(i) * time.Minute) data := ufmt.Sprintf("Meme #%d", i) memeData = append(memeData, data) @@ -34,8 +34,8 @@ func TestGetPostsInRangePagination(t *testing.T) { } // Get timestamps - beforeEarliest := now.Add(-time.Minute * time.Duration(numOfPosts+1)) // subtract 3 + 1 min from now - afterLatest := now.Add(time.Minute) // add 1 min to now + beforeEarliest := now.Add(-1 * time.Minute) + afterLatest := now.Add(time.Duration(numOfPosts)*time.Minute + time.Minute) testCases := []struct { page int @@ -47,7 +47,7 @@ func TestGetPostsInRangePagination(t *testing.T) { {page: 1, pageSize: numOfPosts, expectedNumOfPosts: numOfPosts}, // all posts on single page {page: 12, pageSize: 1, expectedNumOfPosts: 0}, // empty page {page: 1, pageSize: numOfPosts + 1, expectedNumOfPosts: numOfPosts}, // page with fewer posts than its size - {page: 5, pageSize: numOfPosts / 5, expectedNumOfPosts: 2}, // evenly distribute posts per page + {page: 5, pageSize: numOfPosts / 5, expectedNumOfPosts: 1}, // evenly distribute posts per page } for _, tc := range testCases { @@ -70,8 +70,8 @@ func TestGetPostsInRangeByTimestamp(t *testing.T) { numOfPosts := 5 var memeData []string for i := 1; i <= numOfPosts; i++ { - // Prep meme data - nextTime := now.Add(time.Duration(-i) * time.Minute) + // Prepare meme data + nextTime := now.Add(time.Duration(i) * time.Minute) data := ufmt.Sprintf("Meme #%d", i) memeData = append(memeData, data) @@ -79,8 +79,8 @@ func TestGetPostsInRangeByTimestamp(t *testing.T) { } // Get timestamps - beforeEarliest := now.Add(-time.Minute * time.Duration(numOfPosts+1)) // subtract 3 + 1 min from now - afterLatest := now.Add(time.Minute) // add 1 min to now + beforeEarliest := now.Add(-1 * time.Minute) + afterLatest := now.Add(time.Duration(numOfPosts)*time.Minute + time.Minute) // Default sort is by addition order/timestamp jsonStr := m.GetPostsInRange( @@ -88,7 +88,7 @@ func TestGetPostsInRangeByTimestamp(t *testing.T) { afterLatest.Unix(), // end at latest post 1, // first page numOfPosts, // all memes on the page - "DATE_CREATED", // do not sort by upvote + "DATE_CREATED", // sort by newest first ) if jsonStr == "" { @@ -101,101 +101,69 @@ func TestGetPostsInRangeByTimestamp(t *testing.T) { t.Errorf("Expected %d posts in the JSON string, but found %d", m.MemeCounter, postCount) } + // Check if data is there for _, expData := range memeData { if !strings.Contains(jsonStr, expData) { t.Errorf("Expected %s in the JSON string, but counld't find it", expData) } } - // todo add test to check order + // Check if ordering is correct, sort by created date + for i := 0; i < len(memeData)-2; i++ { + if strings.Index(jsonStr, memeData[i]) > strings.Index(jsonStr, memeData[i+1]) { + t.Errorf("Expected %s to be before %s, but was at %d, and %d", memeData[i], memeData[i+1], i, i+1) + } + } } func TestGetPostsInRangeByUpvote(t *testing.T) { m := NewMemeland() - - // Create posts at specific times for testing now := time.Now() - oneHourAgo := now.Add(-time.Hour) - twoHoursAgo := now.Add(-2 * time.Hour) - threeHoursAgo := now.Add(-3 * time.Hour) - // Add posts at specific timestamps - id1 := m.PostMeme("Meme #1", oneHourAgo.Unix()) - id2 := m.PostMeme("Meme #2", twoHoursAgo.Unix()) + memeData1 := "Meme #1" + memeData2 := "Meme #2" + + // Create posts at specific times for testing + id1 := m.PostMeme(memeData1, now.Unix()) + id2 := m.PostMeme(memeData2, now.Add(time.Minute).Unix()) m.Upvote(id1) m.Upvote(id2) // Change caller so avoid double upvote panic std.TestSetOrigCaller(testutils.TestAddress("alice")) - m.Upvote(id2) + m.Upvote(id1) // Final upvote count: - // Meme #1 - 1 upvote - // Meme #2 - 2 upvotes + // Meme #1 - 2 upvote + // Meme #2 - 1 upvotes - // Define test cases for different time ranges and pagination scenarios - testCases := []struct { - name string - startTimestamp int64 - endTimestamp int64 - page int - pageSize int - expectedCount int - expectedData []string - }{ - { - name: "Upvote Sorted All Posts", - startTimestamp: threeHoursAgo.Add(-1 * time.Minute).Unix(), // Slightly before threeHoursAgo - endTimestamp: now.Unix(), - page: 1, - pageSize: 10, - expectedCount: 2, - expectedData: []string{"Meme #1", "Meme #2"}, - }, - { - name: "Upvote Sorted Pagination Test", - startTimestamp: twoHoursAgo.Add(-1 * time.Minute).Unix(), - endTimestamp: now.Unix(), - page: 1, - pageSize: 1, // Only request one post per page - expectedCount: 1, - expectedData: []string{"Meme #2"}, - }, - { - name: "Upvote Sorted Pagination Test: Second Post", - startTimestamp: threeHoursAgo.Unix(), - endTimestamp: now.Unix(), - page: 2, - pageSize: 1, - expectedCount: 1, - expectedData: []string{"Meme #1"}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - // Sort by Upvote descending - jsonStr := m.GetPostsInRange(tc.startTimestamp, tc.endTimestamp, tc.page, tc.pageSize, "UPVOTES") + // Get timestamps + beforeEarliest := now.Add(-time.Minute) + afterLatest := now.Add(time.Hour) - if jsonStr == "" { - t.Error("Expected non-empty JSON string, got empty string") - } + // Default sort is by addition order/timestamp + jsonStr := m.GetPostsInRange( + beforeEarliest.Unix(), // start at earliest post + afterLatest.Unix(), // end at latest post + 1, // first page + 2, // all memes on the page + "UPVOTE", // sort by upvote + ) - // Count the number of posts returned in the JSON string as a rudimentary check for correct pagination/filtering - postCount := strings.Count(jsonStr, `"id":"`) - if postCount != tc.expectedCount { - t.Errorf("Expected %d posts in the JSON string, but found %d", tc.expectedCount, postCount) - } + if jsonStr == "" { + t.Error("Expected non-empty JSON string, got empty string") + } - for _, expData := range tc.expectedData { - if !strings.Contains(jsonStr, expData) { - t.Errorf("Expected %s in the JSON string, but counld't find it", expData) - } - } + // Count the number of posts returned in the JSON string as a rudimentary check for correct pagination/filtering + postCount := strings.Count(jsonStr, `"id":"`) + if postCount != m.MemeCounter { + t.Errorf("Expected %d posts in the JSON string, but found %d", m.MemeCounter, postCount) + } - // todo add test to check order - }) + // Check if ordering is correct + if strings.Index(jsonStr, "Meme #1") > strings.Index(jsonStr, "Meme #2") { + t.Errorf("Expected %s to be before %s", memeData1, memeData2) } } @@ -210,7 +178,6 @@ func TestNoPosts(t *testing.T) { if jsonStr != "[]" { t.Errorf("Expected 0 posts to return [], got %s", jsonStr) } - } func TestUpvote(t *testing.T) { @@ -240,3 +207,27 @@ func TestUpvote(t *testing.T) { t.Errorf("Expected upvotes to be 1 after upvoting, got %d", post.UpvoteTracker.Size()) } } + +func TestDelete(t *testing.T) { + alice := testutils.TestAddress("alice") + std.TestSetOrigCaller(alice) + + m := NewMemeland() + + // Add a post to Memeland + now := time.Now() + postID := m.PostMeme("Meme #1", now.Unix()) + + // Bob will try to delete meme posted by Alice + bob := testutils.TestAddress("bob") + std.TestSetOrigCaller(bob) + + defer func() { + if r := recover(); r == nil { + t.Errorf("code did not panic when in should have") + } + }() + + // Should panic - caught by defer + m.RemovePost(postID) +} diff --git a/api/r/memeland/memeland.gno b/api/r/memeland/memeland.gno index 543f849..5a7a604 100644 --- a/api/r/memeland/memeland.gno +++ b/api/r/memeland/memeland.gno @@ -1,8 +1,9 @@ package memeland import ( - "gno.land/demo/p/memeland" "time" + + "gno.land/demo/p/memeland" ) var m = memeland.NewMemeland()