diff --git a/.golangci.yml b/.golangci.yml index bb853b6230..9d790040db 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,3 +1,7 @@ +# run: +# # timeout for analysis, e.g. 30s, 5m, default is 1m +# timeout: 5m + linters: enable: - bodyclose @@ -49,4 +53,4 @@ linters-settings: max-blank-identifiers: 3 maligned: # print struct with more effective memory layout or not, false by default - suggest-new: true \ No newline at end of file + suggest-new: true diff --git a/cmd/desmosd/main.go b/cmd/desmosd/main.go index 86c2f32c57..1de8c83a03 100644 --- a/cmd/desmosd/main.go +++ b/cmd/desmosd/main.go @@ -2,9 +2,10 @@ package main import ( "encoding/json" - "github.com/cosmos/cosmos-sdk/client" "io" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/server" "github.com/cosmos/cosmos-sdk/x/genaccounts" genaccscli "github.com/cosmos/cosmos-sdk/x/genaccounts/client/cli" diff --git a/cmd/desmosd/testnet.go b/cmd/desmosd/testnet.go index 6f338e4b78..709ca48fe2 100644 --- a/cmd/desmosd/testnet.go +++ b/cmd/desmosd/testnet.go @@ -6,11 +6,12 @@ import ( "bufio" "encoding/json" "fmt" - "github.com/cosmos/cosmos-sdk/x/genaccounts" "net" "os" "path/filepath" + "github.com/cosmos/cosmos-sdk/x/genaccounts" + "github.com/spf13/cobra" "github.com/spf13/viper" tmconfig "github.com/tendermint/tendermint/config" diff --git a/go.mod b/go.mod index f48f32f012..6f031af623 100644 --- a/go.mod +++ b/go.mod @@ -12,4 +12,5 @@ require ( github.com/tendermint/go-amino v0.15.1 github.com/tendermint/tendermint v0.32.7 github.com/tendermint/tm-db v0.2.0 + golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135 ) diff --git a/go.sum b/go.sum index 7df303a070..f27d6bd2ef 100644 --- a/go.sum +++ b/go.sum @@ -282,6 +282,7 @@ golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135 h1:5Beo0mZN8dRzgrMMkDp0jc8YXQKx9DiJ2k1dkvGsn5A= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= diff --git a/x/posts/alias.go b/x/posts/alias.go index 9c9a449c46..ff3060b8ae 100644 --- a/x/posts/alias.go +++ b/x/posts/alias.go @@ -37,10 +37,11 @@ type ( Keeper = keeper.Keeper // Types - PostID = types.PostID - Post = types.Post - Like = types.Like - Likes = types.Likes + PostID = types.PostID + PostIDs = types.PostIDs + Post = types.Post + Like = types.Like + Likes = types.Likes // Msgs MsgCreatePost = types.MsgCreatePost diff --git a/x/posts/client/cli/tx.go b/x/posts/client/cli/tx.go index 30045abf5e..d2cec0a048 100644 --- a/x/posts/client/cli/tx.go +++ b/x/posts/client/cli/tx.go @@ -1,6 +1,11 @@ package cli import ( + "strconv" + + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/spf13/viper" + "github.com/spf13/cobra" "github.com/cosmos/cosmos-sdk/client" @@ -32,12 +37,17 @@ func GetTxCmd(_ string, cdc *codec.Codec) *cobra.Command { return postsTxCmd } +var ( + flagParentID = "parent-id" + flagExternalReference = "external-reference" +) + // GetCmdCreatePost is the CLI command for creating a post func GetCmdCreatePost(cdc *codec.Codec) *cobra.Command { - return &cobra.Command{ - Use: "create [message] [parent-post-id]", + cmd := &cobra.Command{ + Use: "create [message] [allows-comments]", Short: "Create a new post", - Args: cobra.ExactArgs(4), + Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { cliCtx := context.NewCLIContext().WithCodec(cdc) @@ -49,12 +59,19 @@ func GetCmdCreatePost(cdc *codec.Codec) *cobra.Command { return err } - parentID, err := types.ParsePostID(args[1]) + allowsComments, err := strconv.ParseBool(args[1]) if err != nil { return err } - msg := types.NewMsgCreatePost(args[0], parentID, from) + parentID, err := types.ParsePostID(viper.GetString(flagParentID)) + if err != nil { + return err + } + + externalReference := viper.GetString(flagExternalReference) + + msg := types.NewMsgCreatePost(args[0], parentID, allowsComments, externalReference, from) if err = msg.ValidateBasic(); err != nil { return err } @@ -62,6 +79,11 @@ func GetCmdCreatePost(cdc *codec.Codec) *cobra.Command { return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) }, } + + cmd.Flags().String(flagParentID, "0", "Id of the post to which this one should be an answer to") + cmd.Flags().String(flagExternalReference, "", "External reference to this post") + + return flags.GetCommands(cmd)[0] } // GetCmdEditPost is the CLI command for editing a post diff --git a/x/posts/client/rest/rest.go b/x/posts/client/rest/rest.go index b1ae512fc0..365ac5a306 100644 --- a/x/posts/client/rest/rest.go +++ b/x/posts/client/rest/rest.go @@ -25,9 +25,11 @@ func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, storeName string) // --- Tx Handler type createPostReq struct { - BaseReq rest.BaseReq `json:"base_req"` - Message string `json:"message"` - ParentID string `json:"parent_id"` + BaseReq rest.BaseReq `json:"base_req"` + Message string `json:"message"` + ParentID string `json:"parent_id"` + AllowsComments bool `json:"allows_comments"` + ExternalReference string `json:"external_reference"` } func createPostHandler(cliCtx context.CLIContext) http.HandlerFunc { @@ -57,7 +59,7 @@ func createPostHandler(cliCtx context.CLIContext) http.HandlerFunc { return } - msg := types.NewMsgCreatePost(req.Message, parentID, addr) + msg := types.NewMsgCreatePost(req.Message, parentID, req.AllowsComments, req.ExternalReference, addr) err = msg.ValidateBasic() if err != nil { rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) diff --git a/x/posts/genesis.go b/x/posts/genesis.go index f2d9461829..6b8b0ca547 100644 --- a/x/posts/genesis.go +++ b/x/posts/genesis.go @@ -27,9 +27,7 @@ func ExportGenesis(ctx sdk.Context, k keeper.Keeper) GenesisState { // InitGenesis initializes the chain state based on the given GenesisState func InitGenesis(ctx sdk.Context, keeper keeper.Keeper, data GenesisState) []abci.ValidatorUpdate { for _, post := range data.Posts { - if err := keeper.SavePost(ctx, post); err != nil { - panic(err) - } + keeper.SavePost(ctx, post) } for postID, likes := range data.Likes { diff --git a/x/posts/internal/keeper/common_test.go b/x/posts/internal/keeper/common_test.go index 4389ccb0ce..e9910ed8c0 100644 --- a/x/posts/internal/keeper/common_test.go +++ b/x/posts/internal/keeper/common_test.go @@ -42,11 +42,12 @@ func testCodec() *codec.Codec { } var testPostOwner, _ = sdk.AccAddressFromBech32("cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47") -var testPost = types.Post{ - PostID: types.PostID(3257), - ParentID: types.PostID(502), - Message: "Post message", - Created: 10, - LastEdited: 50, - Owner: testPostOwner, -} +var testPost = types.NewPost( + types.PostID(3257), + types.PostID(0), + "Post message", + false, + "", + 10, + testPostOwner, +) diff --git a/x/posts/internal/keeper/handler.go b/x/posts/internal/keeper/handler.go index d3f1a97eef..20b02a36be 100644 --- a/x/posts/internal/keeper/handler.go +++ b/x/posts/internal/keeper/handler.go @@ -2,7 +2,6 @@ package keeper import ( "fmt" - "strconv" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/desmos-labs/desmos/x/posts/internal/types" @@ -37,29 +36,41 @@ func handleMsgCreatePost(ctx sdk.Context, keeper Keeper, msg types.MsgCreatePost ), ) - post := types.Post{ - PostID: keeper.GetLastPostID(ctx).Next(), - ParentID: msg.ParentID, - Message: msg.Message, - Created: ctx.BlockHeight(), - Owner: msg.Creator, - } + post := types.NewPost( + keeper.GetLastPostID(ctx).Next(), + msg.ParentID, + msg.Message, + msg.AllowsComments, + msg.ExternalReference, + ctx.BlockHeight(), + msg.Creator, + ) + // Check for double posting if _, found := keeper.GetPost(ctx, post.PostID); found { - msg := fmt.Sprintf("Post with id %s already exists", post.PostID.String()) - return sdk.ErrUnknownRequest(msg).Result() + return sdk.ErrUnknownRequest(fmt.Sprintf("Post with id %s already exists", post.PostID)).Result() } - if err := keeper.SavePost(ctx, post); err != nil { - return err.Result() + // If valid, check the parent post + if post.ParentID.Valid() { + parentPost, found := keeper.GetPost(ctx, post.ParentID) + if !found { + return sdk.ErrUnknownRequest(fmt.Sprintf("Parent post with id %s not found", post.ParentID)).Result() + } + + if !parentPost.AllowsComments { + return sdk.ErrUnknownRequest(fmt.Sprintf("Post with id %s does not allow comments", parentPost.PostID)).Result() + } } + keeper.SavePost(ctx, post) + ctx.EventManager().EmitEvent( sdk.NewEvent( types.EventTypeCreatePost, sdk.NewAttribute(types.AttributeKeyPostID, post.PostID.String()), sdk.NewAttribute(types.AttributeKeyPostParentID, post.ParentID.String()), - sdk.NewAttribute(types.AttributeKeyCreationTime, strconv.FormatInt(post.Created, 10)), + sdk.NewAttribute(types.AttributeKeyCreationTime, post.Created.String()), sdk.NewAttribute(types.AttributeKeyPostOwner, post.Owner.String()), ), ) @@ -94,22 +105,20 @@ func handleMsgEditPost(ctx sdk.Context, keeper Keeper, msg types.MsgEditPost) sd } // Check the validity of the current block height respect to the creation date of the post - if ctx.BlockHeight() < existing.Created { + if existing.Created.GT(sdk.NewInt(ctx.BlockHeight())) { return sdk.ErrUnknownRequest("Edit date cannot be before creation date").Result() } // Edit the post existing.Message = msg.Message - existing.LastEdited = ctx.BlockHeight() - if err := keeper.SavePost(ctx, existing); err != nil { - return err.Result() - } + existing.LastEdited = sdk.NewInt(ctx.BlockHeight()) + keeper.SavePost(ctx, existing) ctx.EventManager().EmitEvent( sdk.NewEvent( types.EventTypeEditPost, sdk.NewAttribute(types.AttributeKeyPostID, existing.PostID.String()), - sdk.NewAttribute(types.AttributeKeyPostEditTime, strconv.FormatInt(existing.LastEdited, 10)), + sdk.NewAttribute(types.AttributeKeyPostEditTime, existing.LastEdited.String()), ), ) @@ -137,7 +146,7 @@ func handleMsgLike(ctx sdk.Context, keeper Keeper, msg types.MsgLikePost) sdk.Re } // Check the like date to make sure it's before the post creation date. - if ctx.BlockHeight() < post.Created { + if post.Created.GT(sdk.NewInt(ctx.BlockHeight())) { return sdk.ErrUnknownRequest("Like cannot have a creation time before the post itself").Result() } diff --git a/x/posts/internal/keeper/handler_test.go b/x/posts/internal/keeper/handler_test.go index ae9f1f5f62..bd7c35f00a 100644 --- a/x/posts/internal/keeper/handler_test.go +++ b/x/posts/internal/keeper/handler_test.go @@ -14,78 +14,97 @@ import ( // --- handleMsgCreatePost // --------------------------- -func Test_handleMsgCreatePost_returns_error_with_existing_post_id(t *testing.T) { - ctx, k := SetupTestInput() - - msg := types.MsgCreatePost{ - ParentID: testPost.ParentID, - Message: testPost.Message, - Creator: testPost.Owner, +func Test_handleMsgCreatePost(t *testing.T) { + tests := []struct { + name string + storedPosts types.Posts + lastPostID types.PostID + msg types.MsgCreatePost + expPost types.Post + expError string + }{ + { + name: "Trying to store post with same id returns error", + storedPosts: types.Posts{ + types.NewPost(types.PostID(1), testPost.ParentID, testPost.Message, testPost.AllowsComments, "", testPost.Created.Int64(), testPost.Owner), + }, + lastPostID: types.PostID(0), + msg: types.NewMsgCreatePost(testPost.Message, testPost.ParentID, testPost.AllowsComments, "", testPost.Owner), + expError: "Post with id 1 already exists", + }, + { + name: "Post with new id is stored properly", + msg: types.NewMsgCreatePost(testPost.Message, testPost.ParentID, false, "", testPost.Owner), + expPost: types.NewPost(types.PostID(1), testPost.ParentID, testPost.Message, testPost.AllowsComments, "", 0, testPost.Owner), + }, + { + name: "Storing a valid post with missing parent id returns error", + msg: types.NewMsgCreatePost(testPost.Message, types.PostID(50), false, "", testPost.Owner), + expError: "Parent post with id 50 not found", + }, + { + name: "Storing a valid post with parent stored but not accepting comments returns error", + storedPosts: types.Posts{ + types.NewPost(types.PostID(50), types.PostID(50), "Parent post", false, "", 0, testPost.Owner), + }, + msg: types.NewMsgCreatePost(testPost.Message, types.PostID(50), false, "", testPost.Owner), + expError: "Post with id 50 does not allow comments", + }, } - existing := testPost - existing.PostID = types.PostID(1) - - store := ctx.KVStore(k.StoreKey) - store.Set([]byte(types.LastPostIDStoreKey), k.Cdc.MustMarshalBinaryBare(types.PostID(0))) - store.Set([]byte(types.PostStorePrefix+existing.PostID.String()), k.Cdc.MustMarshalBinaryBare(&existing)) + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + ctx, k := SetupTestInput() + store := ctx.KVStore(k.StoreKey) - handler := keeper.NewHandler(k) - res := handler(ctx, msg) + for _, p := range test.storedPosts { + store.Set([]byte(types.PostStorePrefix+p.PostID.String()), k.Cdc.MustMarshalBinaryBare(p)) + } - // Check the response - assert.False(t, res.IsOK()) - assert.Contains(t, res.Log, "Post with id 1 already exists") - assert.Empty(t, res.Events, 0) + if test.lastPostID.Valid() { + store.Set([]byte(types.LastPostIDStoreKey), k.Cdc.MustMarshalBinaryBare(&test.lastPostID)) + } - // Check the events - assert.Len(t, ctx.EventManager().Events(), 1) - expected := sdk.NewEvent( - sdk.EventTypeMessage, - sdk.NewAttribute(sdk.AttributeKeyAction, types.ActionCreatePost), - sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), - sdk.NewAttribute(sdk.AttributeKeySender, msg.Creator.String()), - ) - assert.Contains(t, ctx.EventManager().Events(), expected) -} + handler := keeper.NewHandler(k) + res := handler(ctx, test.msg) -func Test_handleMsgCreatePost_valid_request(t *testing.T) { - ctx, k := SetupTestInput() + if len(test.expError) != 0 { + assert.False(t, res.IsOK()) + assert.Contains(t, res.Log, test.expError) + } - expectedPostID := types.PostID(1) - msg := types.NewMsgCreatePost(testPost.Message, testPost.ParentID, testPost.Owner) - handler := keeper.NewHandler(k) - res := handler(ctx, msg) + if len(test.expError) == 0 { + assert.True(t, res.IsOK()) - // Check the response - assert.True(t, res.IsOK()) - assert.Equal(t, k.Cdc.MustMarshalBinaryLengthPrefixed(expectedPostID), res.Data) + // Check the post + var stored types.Post + k.Cdc.MustUnmarshalBinaryBare(store.Get([]byte(types.PostStorePrefix+test.expPost.PostID.String())), &stored) + assert.Equal(t, test.expPost, stored) - // Check the events - creationEvent := sdk.NewEvent( - types.EventTypeCreatePost, - sdk.NewAttribute(types.AttributeKeyPostID, expectedPostID.String()), - sdk.NewAttribute(types.AttributeKeyPostParentID, msg.ParentID.String()), - sdk.NewAttribute(types.AttributeKeyCreationTime, strconv.FormatInt(ctx.BlockHeight(), 10)), - sdk.NewAttribute(types.AttributeKeyPostOwner, msg.Creator.String()), - ) - assert.Len(t, ctx.EventManager().Events(), 2) - assert.Equal(t, ctx.EventManager().Events(), res.Events) - assert.Contains(t, ctx.EventManager().Events(), creationEvent) + // Check the data + assert.Equal(t, k.Cdc.MustMarshalBinaryLengthPrefixed(test.expPost.PostID), res.Data) - // Check the stored post - expected := types.Post{ - PostID: expectedPostID, - ParentID: msg.ParentID, - Message: msg.Message, - LastEdited: 0, - Owner: msg.Creator, + // Check the events + msgEvent := sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyAction, types.ActionCreatePost), + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(sdk.AttributeKeySender, test.msg.Creator.String()), + ) + creationEvent := sdk.NewEvent( + types.EventTypeCreatePost, + sdk.NewAttribute(types.AttributeKeyPostID, test.expPost.PostID.String()), + sdk.NewAttribute(types.AttributeKeyPostParentID, test.expPost.ParentID.String()), + sdk.NewAttribute(types.AttributeKeyCreationTime, test.expPost.Created.String()), + sdk.NewAttribute(types.AttributeKeyPostOwner, test.expPost.Owner.String()), + ) + assert.Contains(t, ctx.EventManager().Events(), msgEvent) + assert.Contains(t, ctx.EventManager().Events(), creationEvent) + } + }) } - var stored types.Post - store := ctx.KVStore(k.StoreKey) - k.Cdc.MustUnmarshalBinaryBare(store.Get([]byte(types.PostStorePrefix+expectedPostID.String())), &stored) - assert.Equal(t, expected, stored) } // -------------------------- @@ -123,7 +142,7 @@ func Test_handleMSgEditPost_invalid_requests(t *testing.T) { }, { name: "Edit date before creation date", storedPost: &testPost, - blockHeight: testPost.Created - 1, + blockHeight: testPost.Created.Int64() - 1, msg: types.MsgEditPost{ PostID: testPost.PostID, Message: "Edited message", @@ -174,7 +193,7 @@ func Test_handleMSgEditPost_invalid_requests(t *testing.T) { func Test_handleMsgEditPost_valid_request(t *testing.T) { ctx, k := SetupTestInput() - ctx = ctx.WithBlockHeight(testPost.Created + 1) + ctx = ctx.WithBlockHeight(testPost.Created.Int64() + 1) // Insert the post store := ctx.KVStore(k.StoreKey) @@ -206,7 +225,7 @@ func Test_handleMsgEditPost_valid_request(t *testing.T) { Message: msg.Message, Owner: testPost.Owner, Created: testPost.Created, - LastEdited: ctx.BlockHeight(), + LastEdited: sdk.NewInt(ctx.BlockHeight()), } var stored types.Post @@ -239,7 +258,7 @@ func Test_handleMsgLikePost_invalid_requests(t *testing.T) { { name: "Like date before post date", existingPost: &testPost, - blockHeight: testPost.Created - 1, + blockHeight: testPost.Created.Int64() - 1, msg: types.MsgLikePost{ PostID: testPost.PostID, Liker: liker, @@ -286,7 +305,7 @@ func Test_handleMsgLikePost_invalid_requests(t *testing.T) { func Test_handleMsgLikePost_valid_request(t *testing.T) { ctx, k := SetupTestInput() - ctx = ctx.WithBlockHeight(testPost.Created) + ctx = ctx.WithBlockHeight(testPost.Created.Int64()) // Insert the post store := ctx.KVStore(k.StoreKey) diff --git a/x/posts/internal/keeper/keeper.go b/x/posts/internal/keeper/keeper.go index b263d01c53..bf8ce7a2d2 100644 --- a/x/posts/internal/keeper/keeper.go +++ b/x/posts/internal/keeper/keeper.go @@ -46,13 +46,26 @@ func (k Keeper) GetLastPostID(ctx sdk.Context) types.PostID { // SavePost allows to save the given post inside the current context. // It assumes that the given post has already been validated. // If another post has the same ID of the given post, the old post will be overridden -func (k Keeper) SavePost(ctx sdk.Context, post types.Post) sdk.Error { +func (k Keeper) SavePost(ctx sdk.Context, post types.Post) { store := ctx.KVStore(k.StoreKey) - // Save the post and set the last post id + // Save the post store.Set([]byte(types.PostStorePrefix+post.PostID.String()), k.Cdc.MustMarshalBinaryBare(&post)) + + // Set the last post id store.Set([]byte(types.LastPostIDStoreKey), k.Cdc.MustMarshalBinaryBare(&post.PostID)) - return nil + + // Save the comments to the parent post, if it is valid + if post.ParentID.Valid() { + parentCommentsKey := []byte(types.PostCommentsStorePrefix + post.ParentID.String()) + + var commentsIDs []types.PostID + k.Cdc.MustUnmarshalBinaryBare(store.Get(parentCommentsKey), &commentsIDs) + + commentsIDs = append(commentsIDs, post.PostID) + + store.Set(parentCommentsKey, k.Cdc.MustMarshalBinaryBare(&commentsIDs)) + } } // GetPost returns the post having the given id inside the current context. @@ -69,6 +82,17 @@ func (k Keeper) GetPost(ctx sdk.Context, id types.PostID) (post types.Post, foun return post, true } +// GetPostChildrenIDs returns the IDs of all the children posts associated to the post +// having the given postID +// nolint: interfacer +func (k Keeper) GetPostChildrenIDs(ctx sdk.Context, postID types.PostID) []types.PostID { + store := ctx.KVStore(k.StoreKey) + + var postIDs types.PostIDs + k.Cdc.MustUnmarshalBinaryBare(store.Get([]byte(types.PostCommentsStorePrefix+postID.String())), &postIDs) + return postIDs +} + // GetPosts returns the list of all the posts that are stored into the current state. func (k Keeper) GetPosts(ctx sdk.Context) []types.Post { store := ctx.KVStore(k.StoreKey) @@ -113,6 +137,16 @@ func (k Keeper) SaveLike(ctx sdk.Context, postID types.PostID, like types.Like) return nil } +// GetPostLikes returns the list of likes that has been associated to the post having the given id +// nolint: interfacer +func (k Keeper) GetPostLikes(ctx sdk.Context, postID types.PostID) types.Likes { + store := ctx.KVStore(k.StoreKey) + + var likes types.Likes + k.Cdc.MustUnmarshalBinaryBare(store.Get([]byte(types.LikesStorePrefix+postID.String())), &likes) + return likes +} + // GetLikes allows to returns the list of likes that have been stored inside the given context func (k Keeper) GetLikes(ctx sdk.Context) map[types.PostID]types.Likes { store := ctx.KVStore(k.StoreKey) diff --git a/x/posts/internal/keeper/keeper_test.go b/x/posts/internal/keeper/keeper_test.go index 18752be958..fbfec14531 100644 --- a/x/posts/internal/keeper/keeper_test.go +++ b/x/posts/internal/keeper/keeper_test.go @@ -47,21 +47,34 @@ func TestKeeper_GetLastPostId(t *testing.T) { func TestKeeper_SavePost(t *testing.T) { tests := []struct { - name string - existingPost types.Post - newPost types.Post - error sdk.Error + name string + existingPosts types.Posts + newPost types.Post + expParentCommentsIDs types.PostIDs }{ { - name: "Duplicate ID is overridden", - existingPost: types.NewPost(types.PostID(0), types.PostID(0), "Post", 0, testPostOwner), - newPost: types.NewPost(types.PostID(0), types.PostID(10), "New post", 0, testPostOwner), - error: nil, + name: "Post with ID already present", + existingPosts: types.Posts{ + types.NewPost(types.PostID(1), types.PostID(0), "Post", false, "", 0, testPostOwner), + }, + newPost: types.NewPost(types.PostID(1), types.PostID(0), "New post", false, "", 0, testPostOwner), + expParentCommentsIDs: []types.PostID{}, }, { - name: "Not duplicate ID saved correctly", - existingPost: types.NewPost(types.PostID(0), types.PostID(0), "Post", 0, testPostOwner), - newPost: types.NewPost(types.PostID(15), types.PostID(10), "New post", 0, testPostOwner), + name: "Post which ID is not already present", + existingPosts: types.Posts{ + types.NewPost(types.PostID(1), types.PostID(0), "Post", false, "", 0, testPostOwner), + }, + newPost: types.NewPost(types.PostID(15), types.PostID(0), "New post", false, "", 0, testPostOwner), + expParentCommentsIDs: []types.PostID{}, + }, + { + name: "Post with valid parent ID", + existingPosts: []types.Post{ + types.NewPost(types.PostID(1), types.PostID(0), "Parent", false, "", 0, testPostOwner), + }, + newPost: types.NewPost(types.PostID(15), types.PostID(1), "Comment", false, "", 0, testPostOwner), + expParentCommentsIDs: []types.PostID{types.PostID(15)}, }, } @@ -71,50 +84,48 @@ func TestKeeper_SavePost(t *testing.T) { ctx, k := SetupTestInput() store := ctx.KVStore(k.StoreKey) - store.Set( - []byte(types.PostStorePrefix+test.existingPost.PostID.String()), - k.Cdc.MustMarshalBinaryBare(&test.existingPost), - ) + for _, p := range test.existingPosts { + store.Set([]byte(types.PostStorePrefix+p.PostID.String()), k.Cdc.MustMarshalBinaryBare(p)) + } - err := k.SavePost(ctx, test.newPost) - assert.Equal(t, test.error, err) + // Save the post + k.SavePost(ctx, test.newPost) - if test.error == nil { - var expected types.Post - k.Cdc.MustUnmarshalBinaryBare( - store.Get([]byte(types.PostStorePrefix+test.newPost.PostID.String())), - &expected, - ) - assert.Equal(t, test.newPost, expected) - - var lastPostID types.PostID - k.Cdc.MustUnmarshalBinaryBare(store.Get([]byte(types.LastPostIDStoreKey)), &lastPostID) - assert.Equal(t, test.newPost.PostID, lastPostID) - } + // Check the stored post + var expected types.Post + k.Cdc.MustUnmarshalBinaryBare(store.Get([]byte(types.PostStorePrefix+test.newPost.PostID.String())), &expected) + assert.Equal(t, test.newPost, expected) + + // Check the latest post id + var lastPostID types.PostID + k.Cdc.MustUnmarshalBinaryBare(store.Get([]byte(types.LastPostIDStoreKey)), &lastPostID) + assert.Equal(t, test.newPost.PostID, lastPostID) + + // Check the parent comments + var parentCommentsIDs []types.PostID + k.Cdc.MustUnmarshalBinaryBare(store.Get([]byte(types.PostCommentsStorePrefix+test.newPost.ParentID.String())), &parentCommentsIDs) + assert.True(t, test.expParentCommentsIDs.Equals(parentCommentsIDs)) }) } } func TestKeeper_GetPost(t *testing.T) { tests := []struct { - name string - existingPost types.Post - ID types.PostID - expectedFound bool - expected types.Post + name string + postExists bool + ID types.PostID + expected types.Post }{ { - name: "Non existent post is not found", - ID: types.PostID(123), - expectedFound: false, - expected: types.Post{}, + name: "Non existent post is not found", + ID: types.PostID(123), + expected: types.Post{}, }, { - name: "Existing post is found properly", - existingPost: types.NewPost(types.PostID(45), types.PostID(0), "Post", 0, testPostOwner), - ID: types.PostID(45), - expectedFound: true, - expected: types.NewPost(types.PostID(45), types.PostID(0), "Post", 0, testPostOwner), + name: "Existing post is found properly", + ID: types.PostID(45), + postExists: true, + expected: types.NewPost(types.PostID(45), types.PostID(0), "Post", false, "", 0, testPostOwner), }, } @@ -124,16 +135,60 @@ func TestKeeper_GetPost(t *testing.T) { ctx, k := SetupTestInput() store := ctx.KVStore(k.StoreKey) - if !(types.Post{}).Equals(test.existingPost) { - store.Set( - []byte(types.PostStorePrefix+test.existingPost.PostID.String()), - k.Cdc.MustMarshalBinaryBare(&test.existingPost), - ) + if test.postExists { + store.Set([]byte(types.PostStorePrefix+test.expected.PostID.String()), k.Cdc.MustMarshalBinaryBare(&test.expected)) } expected, found := k.GetPost(ctx, test.ID) - assert.Equal(t, test.expected, expected) - assert.Equal(t, test.expectedFound, found) + assert.Equal(t, test.postExists, found) + if test.postExists { + assert.Equal(t, test.expected, expected) + } + }) + } +} + +func TestKeeper_GetPostChildrenIDs(t *testing.T) { + tests := []struct { + name string + storedPosts types.Posts + postID types.PostID + expChildrenIDs types.PostIDs + }{ + { + name: "Empty children list is returned properly", + postID: types.PostID(76), + expChildrenIDs: types.PostIDs{}, + }, + { + name: "Non empty children list is returned properly", + storedPosts: types.Posts{ + types.NewPost(types.PostID(10), types.PostID(0), "Original post", false, "", 10, testPost.Owner), + types.NewPost(types.PostID(55), types.PostID(10), "First commit", false, "", 10, testPost.Owner), + types.NewPost(types.PostID(78), types.PostID(10), "Other commit", false, "", 10, testPost.Owner), + types.NewPost(types.PostID(11), types.PostID(0), "Second post", false, "", 10, testPost.Owner), + types.NewPost(types.PostID(104), types.PostID(11), "Comment to second post", false, "", 10, testPost.Owner), + }, + postID: types.PostID(10), + expChildrenIDs: types.PostIDs{types.PostID(55), types.PostID(78)}, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + ctx, k := SetupTestInput() + + for _, p := range test.storedPosts { + k.SavePost(ctx, p) + } + + storedChildrenIDs := k.GetPostChildrenIDs(ctx, test.postID) + assert.Len(t, storedChildrenIDs, len(test.expChildrenIDs)) + + for _, id := range test.expChildrenIDs { + assert.Contains(t, storedChildrenIDs, id) + } }) } } @@ -150,8 +205,8 @@ func TestKeeper_GetPosts(t *testing.T) { { name: "Existing list is returned properly", posts: types.Posts{ - types.Post{PostID: types.PostID(13)}, - types.Post{PostID: types.PostID(76)}, + types.NewPost(types.PostID(13), types.PostID(0), "", false, "", 0, testPostOwner), + types.NewPost(types.PostID(76), types.PostID(0), "", false, "", 0, testPostOwner), }, }, } @@ -167,7 +222,11 @@ func TestKeeper_GetPosts(t *testing.T) { } posts := k.GetPosts(ctx) - assert.True(t, test.posts.Equals(posts)) + assert.Len(t, posts, len(test.posts)) + + for _, p := range test.posts { + assert.Contains(t, posts, p) + } }) } } @@ -237,6 +296,48 @@ func TestKeeper_SaveLike(t *testing.T) { } } +func TestKeeper_GetPostLikes(t *testing.T) { + liker, _ := sdk.AccAddressFromBech32("cosmos1s3nh6tafl4amaxkke9kdejhp09lk93g9ev39r4") + otherLiker, _ := sdk.AccAddressFromBech32("cosmos15lt0mflt6j9a9auj7yl3p20xec4xvljge0zhae") + + tests := []struct { + name string + likes types.Likes + postID types.PostID + }{ + { + name: "Empty list are returned properly", + likes: types.Likes{}, + postID: types.PostID(10), + }, + { + name: "Valid list of likes is returned properly", + likes: types.Likes{ + types.NewLike(11, otherLiker), + types.NewLike(10, liker), + }, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + ctx, k := SetupTestInput() + + for _, l := range test.likes { + _ = k.SaveLike(ctx, test.postID, l) + } + + stored := k.GetPostLikes(ctx, test.postID) + + assert.Len(t, stored, len(test.likes)) + for _, l := range test.likes { + assert.Contains(t, stored, l) + } + }) + } +} + func TestKeeper_GetLikes(t *testing.T) { liker1, _ := sdk.AccAddressFromBech32("cosmos1s3nh6tafl4amaxkke9kdejhp09lk93g9ev39r4") liker2, _ := sdk.AccAddressFromBech32("cosmos15lt0mflt6j9a9auj7yl3p20xec4xvljge0zhae") diff --git a/x/posts/internal/keeper/post_response.go b/x/posts/internal/keeper/post_response.go new file mode 100644 index 0000000000..cfd69c7694 --- /dev/null +++ b/x/posts/internal/keeper/post_response.go @@ -0,0 +1,30 @@ +package keeper + +import ( + "encoding/json" + + "github.com/desmos-labs/desmos/x/posts/internal/types" +) + +// PostQueryResponse represents the data of a post +// that is returned to user upon a query +type PostQueryResponse struct { + types.Post + Likes types.Likes `json:"likes"` + Children types.Posts `json:"children"` +} + +func NewPostResponse(post types.Post, likes types.Likes, children types.Posts) PostQueryResponse { + return PostQueryResponse{ + Post: post, + Likes: likes, + Children: children, + } +} + +// MarshalJSON implements json.Marshaler as Amino does +// not respect default json composition +func (response PostQueryResponse) MarshalJSON() ([]byte, error) { + type temp PostQueryResponse + return json.Marshal(temp(response)) +} diff --git a/x/posts/internal/keeper/post_response_test.go b/x/posts/internal/keeper/post_response_test.go new file mode 100644 index 0000000000..2e27bfa39b --- /dev/null +++ b/x/posts/internal/keeper/post_response_test.go @@ -0,0 +1,35 @@ +package keeper_test + +import ( + "encoding/json" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/desmos-labs/desmos/x/posts/internal/keeper" + "github.com/desmos-labs/desmos/x/posts/internal/types" + "github.com/stretchr/testify/assert" +) + +func TestPostQueryResponse_MarshalJSON(t *testing.T) { + postOwner, _ := sdk.AccAddressFromBech32("cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47") + liker, _ := sdk.AccAddressFromBech32("cosmos1s3nh6tafl4amaxkke9kdejhp09lk93g9ev39r4") + otherLiker, _ := sdk.AccAddressFromBech32("cosmos15lt0mflt6j9a9auj7yl3p20xec4xvljge0zhae") + + post := types.NewPost(types.PostID(10), types.PostID(0), "Post", true, "", 10, postOwner) + likes := types.Likes{ + types.NewLike(11, liker), + types.NewLike(12, otherLiker), + } + children := types.Posts{ + types.NewPost(types.PostID(98), types.PostID(10), "First comment!", true, "", 10, liker), + types.NewPost(types.PostID(100), types.PostID(11), "Second :(", true, "", 10, otherLiker), + } + + response := keeper.NewPostResponse(post, likes, children) + jsonData, err := json.Marshal(&response) + assert.NoError(t, err) + assert.Equal(t, + `{"id":"10","parent_id":"0","message":"Post","created":"10","last_edited":"0","allows_comments":true,"external_reference":"","owner":"cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47","likes":[{"created":"11","owner":"cosmos1s3nh6tafl4amaxkke9kdejhp09lk93g9ev39r4"},{"created":"12","owner":"cosmos15lt0mflt6j9a9auj7yl3p20xec4xvljge0zhae"}],"children":[{"id":"98","parent_id":"10","message":"First comment!","created":"10","last_edited":"0","allows_comments":true,"external_reference":"","owner":"cosmos1s3nh6tafl4amaxkke9kdejhp09lk93g9ev39r4"},{"id":"100","parent_id":"11","message":"Second :(","created":"10","last_edited":"0","allows_comments":true,"external_reference":"","owner":"cosmos15lt0mflt6j9a9auj7yl3p20xec4xvljge0zhae"}]}`, + string(jsonData), + ) +} diff --git a/x/posts/internal/keeper/querier.go b/x/posts/internal/keeper/querier.go index cd43c4342a..93da0a0e8e 100644 --- a/x/posts/internal/keeper/querier.go +++ b/x/posts/internal/keeper/querier.go @@ -40,7 +40,21 @@ func queryPost(ctx sdk.Context, path []string, _ abci.RequestQuery, keeper Keepe return nil, sdk.ErrUnknownRequest("could not get post") } - bz, err2 := codec.MarshalJSONIndent(keeper.Cdc, &post) + // Get the likes + postLikes := keeper.GetPostLikes(ctx, post.PostID) + + // Get the children + childrenIDs := keeper.GetPostChildrenIDs(ctx, post.PostID) + postChildren := make(types.Posts, len(childrenIDs)) + for index, childrenID := range childrenIDs { + children, _ := keeper.GetPost(ctx, childrenID) + postChildren[index] = children + } + + // Crete the response object + postResponse := NewPostResponse(post, postLikes, postChildren) + + bz, err2 := codec.MarshalJSONIndent(keeper.Cdc, &postResponse) if err2 != nil { panic("could not marshal result to JSON") } diff --git a/x/posts/internal/types/keys.go b/x/posts/internal/types/keys.go index 4abf702491..0803b99215 100644 --- a/x/posts/internal/types/keys.go +++ b/x/posts/internal/types/keys.go @@ -5,10 +5,11 @@ const ( RouterKey = ModuleName StoreKey = ModuleName - PostStorePrefix = "post:" - LastPostIDStoreKey = "last_post_id" - LikesStorePrefix = "likes:" - LastLikeIDStoreKey = "last_like_id" + PostStorePrefix = "post:" + LastPostIDStoreKey = "last_post_id" + PostCommentsStorePrefix = "comments:" + + LikesStorePrefix = "likes:" ActionCreatePost = "create_post" ActionEditPost = "edit_post" diff --git a/x/posts/internal/types/msgs.go b/x/posts/internal/types/msgs.go index 1c37c43bbd..828bf62f88 100644 --- a/x/posts/internal/types/msgs.go +++ b/x/posts/internal/types/msgs.go @@ -12,17 +12,21 @@ import ( // MsgCreatePost defines a CreatePost message type MsgCreatePost struct { - ParentID PostID `json:"parent_id"` - Message string `json:"message"` - Creator sdk.AccAddress `json:"creator"` + ParentID PostID `json:"parent_id"` + Message string `json:"message"` + AllowsComments bool `json:"allows_comments"` + ExternalReference string `json:"external_reference"` + Creator sdk.AccAddress `json:"creator"` } // NewMsgCreatePost is a constructor function for MsgSetName -func NewMsgCreatePost(message string, parentID PostID, owner sdk.AccAddress) MsgCreatePost { +func NewMsgCreatePost(message string, parentID PostID, allowsComments bool, externalReference string, owner sdk.AccAddress) MsgCreatePost { return MsgCreatePost{ - Message: message, - ParentID: parentID, - Creator: owner, + Message: message, + ParentID: parentID, + AllowsComments: allowsComments, + ExternalReference: externalReference, + Creator: owner, } } diff --git a/x/posts/internal/types/msgs_test.go b/x/posts/internal/types/msgs_test.go index 77cf52a0df..bba54f92a3 100644 --- a/x/posts/internal/types/msgs_test.go +++ b/x/posts/internal/types/msgs_test.go @@ -13,7 +13,7 @@ import ( // ---------------------- var testOwner, _ = sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") -var msgCreatePost = types.NewMsgCreatePost("My new post", types.PostID(53), testOwner) +var msgCreatePost = types.NewMsgCreatePost("My new post", types.PostID(53), false, "Ref#123", testOwner) func TestMsgCreatePost_Route(t *testing.T) { actual := msgCreatePost.Route() @@ -34,17 +34,17 @@ func TestMsgCreatePost_ValidateBasic(t *testing.T) { }{ { name: "Empty owner returns error", - msg: types.NewMsgCreatePost("Message", types.PostID(0), nil), + msg: types.NewMsgCreatePost("Message", types.PostID(0), false, "", nil), error: sdk.ErrInvalidAddress("Invalid creator address: "), }, { name: "Empty post message returns error", - msg: types.NewMsgCreatePost("", types.PostID(0), creator), + msg: types.NewMsgCreatePost("", types.PostID(0), false, "", creator), error: sdk.ErrUnknownRequest("Post message cannot be empty"), }, { name: "Valid message does not return any error", - msg: types.NewMsgCreatePost("Message", types.PostID(0), creator), + msg: types.NewMsgCreatePost("Message", types.PostID(0), false, "", creator), error: nil, }, } @@ -61,9 +61,29 @@ func TestMsgCreatePost_ValidateBasic(t *testing.T) { } func TestMsgCreatePost_GetSignBytes(t *testing.T) { - actual := msgCreatePost.GetSignBytes() - expected := `{"type":"desmos/MsgCreatePost","value":{"creator":"cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns","message":"My new post","parent_id":"53"}}` - assert.Equal(t, expected, string(actual)) + tests := []struct { + name string + msg types.MsgCreatePost + expSignJSON string + }{ + { + name: "Message with non-empty external reference", + msg: types.NewMsgCreatePost("My new post", types.PostID(53), false, "Ref#123", testOwner), + expSignJSON: `{"type":"desmos/MsgCreatePost","value":{"allows_comments":false,"creator":"cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns","external_reference":"Ref#123","message":"My new post","parent_id":"53"}}`, + }, + { + name: "Message with non-empty external reference", + msg: types.NewMsgCreatePost("My post", types.PostID(15), false, "", testOwner), + expSignJSON: `{"type":"desmos/MsgCreatePost","value":{"allows_comments":false,"creator":"cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns","external_reference":"","message":"My post","parent_id":"15"}}`, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expSignJSON, string(test.msg.GetSignBytes())) + }) + } } func TestMsgCreatePost_GetSigners(t *testing.T) { diff --git a/x/posts/internal/types/post.go b/x/posts/internal/types/post.go index 62341cac76..6e6e663652 100644 --- a/x/posts/internal/types/post.go +++ b/x/posts/internal/types/post.go @@ -15,18 +15,28 @@ import ( // PostID represents a unique post id type PostID uint64 +// Valid tells if the id can be used safely func (id PostID) Valid() bool { return id != 0 } +// Next returns the subsequent id to this one func (id PostID) Next() PostID { return id + 1 } +// String implements fmt.Stringer func (id PostID) String() string { return strconv.FormatUint(uint64(id), 10) } +// Equals compares two PostID instances +func (id PostID) Equals(other PostID) bool { + return id == other +} + +// ParsePostID returns the PostID represented inside the provided +// value, or an error if no id could be parsed properly func ParsePostID(value string) (PostID, error) { intVal, err := strconv.ParseUint(value, 10, 64) if err != nil { @@ -36,28 +46,55 @@ func ParsePostID(value string) (PostID, error) { return PostID(intVal), err } +// ---------------- +// --- Post IDs +// ---------------- + +// PostIDs represents a slice of PostID objects +type PostIDs []PostID + +// Equals returns true iff the ids slice and the other +// one contain the same data in the same order +func (ids PostIDs) Equals(other PostIDs) bool { + if len(ids) != len(other) { + return false + } + + for index, id := range ids { + if id != other[index] { + return false + } + } + + return true +} + // --------------- // --- Post // --------------- // Post is a struct of a Magpie post type Post struct { - PostID PostID `json:"id"` - ParentID PostID `json:"parent_id"` - Message string `json:"message"` - Created int64 `json:"created"` // Block height at which the post has been created - LastEdited int64 `json:"last_edited"` // Block height at which the post has been edited the last time - Owner sdk.AccAddress `json:"owner"` + PostID PostID `json:"id,string"` // Unique id + ParentID PostID `json:"parent_id,string"` // Post of which this one is a comment + Message string `json:"message"` // Message contained inside the post + Created sdk.Int `json:"created"` // Block height at which the post has been created + LastEdited sdk.Int `json:"last_edited"` // Block height at which the post has been edited the last time + AllowsComments bool `json:"allows_comments"` // Tells if users can reference this PostID as the parent + ExternalReference string `json:"external_reference"` // Used to know when to display this post + Owner sdk.AccAddress `json:"owner"` // Creator of the Post } -func NewPost(id, parentID PostID, message string, created int64, owner sdk.AccAddress) Post { +func NewPost(id, parentID PostID, message string, allowsComments bool, externalReference string, created int64, owner sdk.AccAddress) Post { return Post{ - PostID: id, - ParentID: parentID, - Message: message, - Created: created, - LastEdited: 0, - Owner: owner, + PostID: id, + ParentID: parentID, + Message: message, + Created: sdk.NewInt(created), + LastEdited: sdk.ZeroInt(), + AllowsComments: allowsComments, + ExternalReference: externalReference, + Owner: owner, } } @@ -85,23 +122,24 @@ func (p Post) Validate() error { return fmt.Errorf("invalid post message: %s", p.Message) } - if p.Created == 0 { - return fmt.Errorf("invalid post creation block heigth: %d", p.Created) + if sdk.ZeroInt().Equal(p.Created) { + return fmt.Errorf("invalid post creation block height: %s", p.Created) } - if p.LastEdited == 0 || p.LastEdited < p.Created { - return fmt.Errorf("invalid Post edit time %d", p.LastEdited) + if p.Created.GT(p.LastEdited) { + return fmt.Errorf("invalid post last edit block height: %s", p.LastEdited) } return nil } func (p Post) Equals(other Post) bool { - return p.PostID == other.PostID && - p.ParentID == other.ParentID && + return p.PostID.Equals(other.PostID) && + p.ParentID.Equals(other.ParentID) && p.Message == other.Message && - p.Created == other.Created && - p.LastEdited == other.LastEdited && + p.Created.Equal(other.Created) && + p.LastEdited.Equal(other.LastEdited) && + p.AllowsComments == other.AllowsComments && p.Owner.Equals(other.Owner) } @@ -109,8 +147,11 @@ func (p Post) Equals(other Post) bool { // --- Posts // ------------- +// Posts represents a slice of Post objects type Posts []Post +// Equals returns true iff the p slice contains the same +// data in the same order of the other slice func (p Posts) Equals(other Posts) bool { if len(p) != len(other) { return false diff --git a/x/posts/internal/types/post_test.go b/x/posts/internal/types/post_test.go new file mode 100644 index 0000000000..b55b1b90b3 --- /dev/null +++ b/x/posts/internal/types/post_test.go @@ -0,0 +1,342 @@ +package types_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/desmos-labs/desmos/x/posts/internal/types" + "github.com/stretchr/testify/assert" +) + +// ----------- +// --- Post +// ----------- + +func TestPost_String(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + post := types.Post{ + PostID: types.PostID(19), + ParentID: types.PostID(1), + Message: "My post message", + Created: sdk.NewInt(98), + LastEdited: sdk.NewInt(105), + AllowsComments: true, + ExternalReference: "My reference", + Owner: owner, + } + + assert.Equal(t, + `{"id":"19","parent_id":"1","message":"My post message","created":"98","last_edited":"105","allows_comments":true,"external_reference":"My reference","owner":"cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns"}`, + post.String(), + ) +} + +func TestPost_Validate(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + tests := []struct { + post types.Post + expError string + }{ + { + post: types.Post{PostID: types.PostID(0)}, + expError: "invalid post id: 0", + }, + { + post: types.Post{PostID: types.PostID(19), Owner: nil}, + expError: "invalid post owner: ", + }, + { + post: types.Post{PostID: types.PostID(19), Owner: owner, Message: ""}, + expError: "invalid post message: ", + }, + { + post: types.Post{PostID: types.PostID(19), Owner: owner, Message: "Message", Created: sdk.NewInt(0)}, + expError: "invalid post creation block height: 0", + }, + { + post: types.Post{PostID: types.PostID(19), Owner: owner, Message: "Message", Created: sdk.NewInt(10), LastEdited: sdk.NewInt(9)}, + expError: "invalid post last edit block height: 9", + }, + } + + for _, test := range tests { + test := test + t.Run(test.expError, func(t *testing.T) { + assert.Equal(t, test.expError, test.post.Validate().Error()) + }) + } +} + +func TestPost_Equals(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + otherOwner, _ := sdk.AccAddressFromBech32("cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47") + + tests := []struct { + name string + first types.Post + second types.Post + expEquals bool + }{ + { + name: "Different post ID", + first: types.Post{ + PostID: types.PostID(19), + ParentID: types.PostID(1), + Message: "My post message", + Created: sdk.NewInt(98), + LastEdited: sdk.NewInt(105), + AllowsComments: true, + ExternalReference: "My reference", + Owner: owner, + }, + second: types.Post{ + PostID: types.PostID(10), + ParentID: types.PostID(1), + Message: "My post message", + Created: sdk.NewInt(98), + LastEdited: sdk.NewInt(105), + AllowsComments: true, + ExternalReference: "My reference", + Owner: owner, + }, + expEquals: false, + }, + { + name: "Different parent ID", + first: types.Post{ + PostID: types.PostID(19), + ParentID: types.PostID(1), + Message: "My post message", + Created: sdk.NewInt(98), + LastEdited: sdk.NewInt(105), + AllowsComments: true, + ExternalReference: "My reference", + Owner: owner, + }, + second: types.Post{ + PostID: types.PostID(19), + ParentID: types.PostID(10), + Message: "My post message", + Created: sdk.NewInt(98), + LastEdited: sdk.NewInt(105), + AllowsComments: true, + ExternalReference: "My reference", + Owner: owner, + }, + expEquals: false, + }, + { + name: "Different message", + first: types.Post{ + PostID: types.PostID(19), + ParentID: types.PostID(1), + Message: "My post message", + Created: sdk.NewInt(98), + LastEdited: sdk.NewInt(105), + AllowsComments: true, + ExternalReference: "My reference", + Owner: owner, + }, + second: types.Post{ + PostID: types.PostID(19), + ParentID: types.PostID(1), + Message: "Another post message", + Created: sdk.NewInt(98), + LastEdited: sdk.NewInt(105), + AllowsComments: true, + ExternalReference: "My reference", + Owner: owner, + }, + expEquals: false, + }, + { + name: "Different creation time", + first: types.Post{ + PostID: types.PostID(19), + ParentID: types.PostID(1), + Message: "My post message", + Created: sdk.NewInt(98), + LastEdited: sdk.NewInt(105), + AllowsComments: true, + ExternalReference: "My reference", + Owner: owner, + }, + second: types.Post{ + PostID: types.PostID(19), + ParentID: types.PostID(1), + Message: "My post message", + Created: sdk.NewInt(15), + LastEdited: sdk.NewInt(105), + AllowsComments: true, + ExternalReference: "My reference", + Owner: owner, + }, + expEquals: false, + }, + { + name: "Different last edited", + first: types.Post{ + PostID: types.PostID(19), + ParentID: types.PostID(1), + Message: "My post message", + Created: sdk.NewInt(98), + LastEdited: sdk.NewInt(105), + AllowsComments: true, + ExternalReference: "My reference", + Owner: owner, + }, + second: types.Post{ + PostID: types.PostID(19), + ParentID: types.PostID(1), + Message: "My post message", + Created: sdk.NewInt(98), + LastEdited: sdk.NewInt(13), + AllowsComments: true, + ExternalReference: "My reference", + Owner: owner, + }, + expEquals: false, + }, + { + name: "Different allows comments", + first: types.Post{ + PostID: types.PostID(19), + ParentID: types.PostID(1), + Message: "My post message", + Created: sdk.NewInt(98), + LastEdited: sdk.NewInt(105), + AllowsComments: true, + ExternalReference: "My reference", + Owner: owner, + }, + second: types.Post{ + PostID: types.PostID(19), + ParentID: types.PostID(1), + Message: "My post message", + Created: sdk.NewInt(98), + LastEdited: sdk.NewInt(105), + AllowsComments: false, + ExternalReference: "My reference", + Owner: owner, + }, + expEquals: false, + }, + { + name: "Different owner", + first: types.Post{ + PostID: types.PostID(19), + ParentID: types.PostID(1), + Message: "My post message", + Created: sdk.NewInt(98), + LastEdited: sdk.NewInt(105), + AllowsComments: true, + ExternalReference: "My reference", + Owner: owner, + }, + second: types.Post{ + PostID: types.PostID(19), + ParentID: types.PostID(1), + Message: "My post message", + Created: sdk.NewInt(98), + LastEdited: sdk.NewInt(105), + AllowsComments: true, + ExternalReference: "My reference", + Owner: otherOwner, + }, + expEquals: false, + }, + { + name: "Same data", + first: types.Post{ + PostID: types.PostID(19), + ParentID: types.PostID(1), + Message: "My post message", + Created: sdk.NewInt(98), + LastEdited: sdk.NewInt(105), + AllowsComments: true, + ExternalReference: "My reference", + Owner: owner, + }, + second: types.Post{ + PostID: types.PostID(19), + ParentID: types.PostID(1), + Message: "My post message", + Created: sdk.NewInt(98), + LastEdited: sdk.NewInt(105), + AllowsComments: true, + ExternalReference: "My reference", + Owner: owner, + }, + expEquals: true, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expEquals, test.first.Equals(test.second)) + }) + } +} + +// ----------- +// --- Posts +// ----------- + +func TestPosts_Equals(t *testing.T) { + tests := []struct { + name string + first types.Posts + second types.Posts + expEquals bool + }{ + { + name: "Empty lists are equals", + first: types.Posts{}, + second: types.Posts{}, + expEquals: true, + }, + { + name: "List of different lengths are not equals", + first: types.Posts{ + types.Post{PostID: types.PostID(0), Created: sdk.ZeroInt(), LastEdited: sdk.ZeroInt()}, + }, + second: types.Posts{ + types.Post{PostID: types.PostID(0), Created: sdk.ZeroInt(), LastEdited: sdk.ZeroInt()}, + types.Post{PostID: types.PostID(1), Created: sdk.ZeroInt(), LastEdited: sdk.ZeroInt()}, + }, + expEquals: false, + }, + { + name: "Same lists but in different orders", + first: types.Posts{ + types.Post{PostID: types.PostID(0), Created: sdk.ZeroInt(), LastEdited: sdk.ZeroInt()}, + types.Post{PostID: types.PostID(1), Created: sdk.ZeroInt(), LastEdited: sdk.ZeroInt()}, + }, + second: types.Posts{ + types.Post{PostID: types.PostID(1), Created: sdk.ZeroInt(), LastEdited: sdk.ZeroInt()}, + types.Post{PostID: types.PostID(0), Created: sdk.ZeroInt(), LastEdited: sdk.ZeroInt()}, + }, + expEquals: false, + }, + { + name: "Same lists are equals", + first: types.Posts{ + types.Post{PostID: types.PostID(0), Created: sdk.ZeroInt(), LastEdited: sdk.ZeroInt()}, + types.Post{PostID: types.PostID(1), Created: sdk.ZeroInt(), LastEdited: sdk.ZeroInt()}, + }, + second: types.Posts{ + types.Post{PostID: types.PostID(0), Created: sdk.ZeroInt(), LastEdited: sdk.ZeroInt()}, + types.Post{PostID: types.PostID(1), Created: sdk.ZeroInt(), LastEdited: sdk.ZeroInt()}, + }, + expEquals: true, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expEquals, test.first.Equals(test.second)) + }) + } +}