Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Ownable, proper sort by timestamp #8

Merged
merged 11 commits into from
Mar 19, 2024
Merged
45 changes: 33 additions & 12 deletions api/p/memeland/memeland.gno
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -19,16 +20,19 @@ type Post struct {
UpvoteTracker *avl.Tree // address > struct{}{}
}

type UpvoteSorter []*Post
type DateSorter []*Post
zivkovicmilos marked this conversation as resolved.
Show resolved Hide resolved

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),
}
}

Expand Down Expand Up @@ -102,7 +106,7 @@ func (m *Memeland) GetPostsInRange(startTimestamp, endTimestamp int64, page, pag
sort.Sort(UpvoteSorter(filteredPosts))
default:
// Sort by timestamp, beginning with newest
filteredPosts = reversePosts(filteredPosts)
sort.Sort(DateSorter(filteredPosts))
}

// Pagination
Expand All @@ -123,6 +127,22 @@ func (m *Memeland) GetPostsInRange(startTimestamp, endTimestamp int64, page, pag
return PostsToJSONString(filteredPosts[startIndex:endIndex])
}

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 representation.
func PostsToJSONString(posts []*Post) string {
var sb strings.Builder
Expand Down Expand Up @@ -169,13 +189,14 @@ 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
// Sort by newest first
func (a DateSorter) Len() int { return len(a) }
func (a DateSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a DateSorter) Less(i, j int) bool {
return a[i].Timestamp.Before(a[j].Timestamp)
}

// Sort by upvote count
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 {
Expand Down
151 changes: 71 additions & 80 deletions api/p/memeland/memeland_test.gno
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,16 @@ func TestGetPostsInRangePagination(t *testing.T) {
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)

m.PostMeme(data, nextTime.Unix())
}

// 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
Expand Down Expand Up @@ -67,28 +67,28 @@ func TestGetPostsInRangeByTimestamp(t *testing.T) {
m := NewMemeland()
now := time.Now()

numOfPosts := 5
numOfPosts := 10
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)

m.PostMeme(data, nextTime.Unix())
}

// 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(
beforeEarliest.Unix(), // start at earliest post
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 == "" {
Expand All @@ -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)
}
}

Expand All @@ -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) {
Expand Down Expand Up @@ -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)
}