diff --git a/CHANGELOG.md b/CHANGELOG.md index 48a12dea6c..936f28fa49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# Version 0.3.0 +## Changes +- Implemented the support for media in posts (#36) + # Version 0.2.0 ## Changes - Implemented the support for arbitrary data inside a post (#52, #66) diff --git a/x/posts/alias.go b/x/posts/alias.go index c8020b221b..6ce73e5785 100644 --- a/x/posts/alias.go +++ b/x/posts/alias.go @@ -24,13 +24,15 @@ var ( // Types NewReaction = types.NewReaction - NewPost = types.NewPost + NewTextPost = types.NewTextPost + NewMediaPost = types.NewMediaPost ParsePostID = types.ParsePostID DefaultGenesisState = types.DefaultGenesisState ValidateGenesis = types.ValidateGenesis // Msgs - NewMsgCreatePost = types.NewMsgCreatePost + NewMsgCreateTextPost = types.NewMsgCreateTextPost + NewMsgCreateMediaPost = types.NewMsgCreateMediaPost NewMsgEditPost = types.NewMsgEditPost NewMsgAddPostReaction = types.NewMsgAddPostReaction NewMsgRemovePostReaction = types.NewMsgRemovePostReaction @@ -48,12 +50,16 @@ type ( PostIDs = types.PostIDs Post = types.Post Posts = types.Posts + PostMedia = types.PostMedia + PostMedias = types.PostMedias Reaction = types.Reaction Reactions = types.Reactions GenesisState = types.GenesisState // Msgs MsgCreatePost = types.MsgCreatePost + MsgCreateTextPost = types.MsgCreateTextPost + MsgCreateMediaPost = types.MsgCreateMediaPost MsgEditPost = types.MsgEditPost MsgAddPostReaction = types.MsgAddPostReaction MsgRemovePostReaction = types.MsgRemovePostReaction diff --git a/x/posts/client/cli/tx.go b/x/posts/client/cli/tx.go index db6e2f2727..111768bce8 100644 --- a/x/posts/client/cli/tx.go +++ b/x/posts/client/cli/tx.go @@ -3,6 +3,7 @@ package cli import ( "fmt" "strconv" + "strings" "time" "github.com/cosmos/cosmos-sdk/version" @@ -43,9 +44,20 @@ func GetTxCmd(_ string, cdc *codec.Codec) *cobra.Command { // GetCmdCreatePost is the CLI command for creating a post func GetCmdCreatePost(cdc *codec.Codec) *cobra.Command { cmd := &cobra.Command{ - Use: "create [subspace] [message] [allows-comments]", + Use: "create [subspace] [message] [allows-comments] [[[uri],[provider],[mime-type]]...]", Short: "Create a new post", - Args: cobra.ExactArgs(3), + Long: fmt.Sprintf(` + Create a new post, specifying the subspace, message and whether or not it will allow for comments. + Optional media attachments are also supported. + If you with to add one or more media attachment, you have to specify a uri, a provider and a mime type for each. + You can do so by concatenating them together separated by a comma (,). + Usage examples: + + - tx posts create "desmos" "Hello world!" true + - tx posts create "demos" "A post with media" true "https://example.com,text/plain" + - tx posts create "desmos" "A post with multiple medias" false "https://example.com/media1,text/plain" "https://example.com/media2,application/json" + `), + Args: cobra.MinimumNArgs(3), RunE: func(cmd *cobra.Command, args []string) error { cliCtx := context.NewCLIContext().WithCodec(cdc) @@ -67,11 +79,42 @@ func GetCmdCreatePost(cdc *codec.Codec) *cobra.Command { return err } - msg := types.NewMsgCreatePost(args[1], parentID, allowsComments, args[0], map[string]string{}, from, time.Now().UTC()) + var msg types.MsgCreatePost + msgTextPost := types.NewMsgCreateTextPost( + args[1], + parentID, + allowsComments, + args[0], + map[string]string{}, + from, + time.Now().UTC(), + ) + + // If there are some medias + if len(args) > 3 { + var medias types.PostMedias + var appended bool + // Read each media and add it to the medias if valid + for i := 3; i < len(args); i++ { + arg := strings.Split(args[i], ",") + if len(arg) == 3 { + media := types.NewPostMedia(arg[0], arg[1], arg[2]) + medias, appended = medias.AppendIfMissing(media) + if !appended { + return sdk.ErrUnknownRequest("You can't send the same media two times") + } + } else { + return sdk.ErrUnknownRequest( + "If medias are present, you should specify uri, provider and mime type, " + + "if you are confused, please use the --help flag") + } + } + msg = types.NewMsgCreateMediaPost(msgTextPost, medias) + } + if err = msg.ValidateBasic(); err != nil { return err } - return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) }, } diff --git a/x/posts/client/rest/rest.go b/x/posts/client/rest/rest.go index feb6e3e3ae..a3c6e2c98a 100644 --- a/x/posts/client/rest/rest.go +++ b/x/posts/client/rest/rest.go @@ -5,6 +5,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/cosmos-sdk/types/rest" + "github.com/desmos-labs/desmos/x/posts/internal/types" "github.com/gorilla/mux" ) @@ -23,6 +24,7 @@ type CreatePostReq struct { Subspace string `json:"subspace"` OptionalData map[string]string `json:"optional_data"` CreationTime time.Time `json:"creation_time"` + Medias types.PostMedias `json:"medias,omitempty"` } // AddReactionReq defines the properties of a reaction adding request's body. diff --git a/x/posts/client/rest/tx.go b/x/posts/client/rest/tx.go index fe823f91db..c5673e14a7 100644 --- a/x/posts/client/rest/tx.go +++ b/x/posts/client/rest/tx.go @@ -44,7 +44,14 @@ func createPostHandler(cliCtx context.CLIContext) http.HandlerFunc { return } - msg := types.NewMsgCreatePost(req.Message, parentID, req.AllowsComments, req.Subspace, req.OptionalData, addr, req.CreationTime) + var msg types.MsgCreatePost + msgTextPost := types.NewMsgCreateTextPost(req.Message, parentID, req.AllowsComments, req.Subspace, req.OptionalData, + addr, req.CreationTime) + + if len(req.Medias) != 0 { + msg = types.NewMsgCreateMediaPost(msgTextPost, req.Medias) + } + err = msg.ValidateBasic() if err != nil { rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) diff --git a/x/posts/internal/keeper/common_test.go b/x/posts/internal/keeper/common_test.go index fb284c143d..b89570981d 100644 --- a/x/posts/internal/keeper/common_test.go +++ b/x/posts/internal/keeper/common_test.go @@ -46,7 +46,7 @@ func testCodec() *codec.Codec { var testPostOwner, _ = sdk.AccAddressFromBech32("cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47") var timeZone, _ = time.LoadLocation("UTC") var testPostCreationDate = time.Date(2020, 1, 1, 15, 15, 00, 000, timeZone) -var testPost = types.NewPost( +var testPost = types.NewTextPost( types.PostID(3257), types.PostID(0), "Post message", diff --git a/x/posts/internal/keeper/handler.go b/x/posts/internal/keeper/handler.go index 15cc4be39a..78a0703ff7 100644 --- a/x/posts/internal/keeper/handler.go +++ b/x/posts/internal/keeper/handler.go @@ -11,14 +11,14 @@ import ( func NewHandler(keeper Keeper) sdk.Handler { return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { switch msg := msg.(type) { - case types.MsgCreatePost: - return handleMsgCreatePost(ctx, keeper, msg) case types.MsgEditPost: return handleMsgEditPost(ctx, keeper, msg) case types.MsgAddPostReaction: return handleMsgAddPostReaction(ctx, keeper, msg) case types.MsgRemovePostReaction: return handleMsgRemovePostReaction(ctx, keeper, msg) + case types.MsgCreatePost: + return handleMsgCreatePost(ctx, keeper, msg) default: errMsg := fmt.Sprintf("Unrecognized Posts message type: %v", msg.Type()) return sdk.ErrUnknownRequest(errMsg).Result() @@ -28,31 +28,53 @@ func NewHandler(keeper Keeper) sdk.Handler { // handleMsgCreatePost handles the creation of a new post func handleMsgCreatePost(ctx sdk.Context, keeper Keeper, msg types.MsgCreatePost) sdk.Result { - post := types.NewPost( - keeper.GetLastPostID(ctx).Next(), - msg.ParentID, - msg.Message, - msg.AllowsComments, - msg.Subspace, - msg.OptionalData, - msg.CreationDate, - msg.Creator, - ) + + var post types.Post + + if textMsg, ok := msg.(types.MsgCreateTextPost); ok { + post = types.NewTextPost( + keeper.GetLastPostID(ctx).Next(), + textMsg.ParentID, + textMsg.Message, + textMsg.AllowsComments, + textMsg.Subspace, + textMsg.OptionalData, + textMsg.CreationDate, + textMsg.Creator, + ) + } + + if mediaMsg, ok := msg.(types.MsgCreateMediaPost); ok { + textPost := types.NewTextPost( + keeper.GetLastPostID(ctx).Next(), + mediaMsg.MsgCreatePost.ParentID, + mediaMsg.MsgCreatePost.Message, + mediaMsg.MsgCreatePost.AllowsComments, + mediaMsg.MsgCreatePost.Subspace, + mediaMsg.MsgCreatePost.OptionalData, + mediaMsg.MsgCreatePost.CreationDate, + mediaMsg.MsgCreatePost.Creator, + ) + + post = types.NewMediaPost(textPost, mediaMsg.Medias) + } // Check for double posting - if _, found := keeper.GetPost(ctx, post.PostID); found { - return sdk.ErrUnknownRequest(fmt.Sprintf("Post with id %s already exists", post.PostID)).Result() + if _, found := keeper.GetPost(ctx, post.GetID()); found { + return sdk.ErrUnknownRequest(fmt.Sprintf("Post with id %s already exists", post.GetID())).Result() } // If valid, check the parent post - if post.ParentID.Valid() { - parentPost, found := keeper.GetPost(ctx, post.ParentID) + if post.GetParentID().Valid() { + parentPost, found := keeper.GetPost(ctx, post.GetParentID()) if !found { - return sdk.ErrUnknownRequest(fmt.Sprintf("Parent post with id %s not found", post.ParentID)).Result() + return sdk.ErrUnknownRequest(fmt.Sprintf("Parent post with id %s not found", + post.GetParentID())).Result() } - if !parentPost.AllowsComments { - return sdk.ErrUnknownRequest(fmt.Sprintf("Post with id %s does not allow comments", parentPost.PostID)).Result() + if !parentPost.CanComment() { + return sdk.ErrUnknownRequest(fmt.Sprintf("Post with id %s does not allow comments", + parentPost.GetID())).Result() } } @@ -60,15 +82,15 @@ func handleMsgCreatePost(ctx sdk.Context, keeper Keeper, msg types.MsgCreatePost createEvent := sdk.NewEvent( types.EventTypePostCreated, - sdk.NewAttribute(types.AttributeKeyPostID, post.PostID.String()), - sdk.NewAttribute(types.AttributeKeyPostParentID, post.ParentID.String()), - sdk.NewAttribute(types.AttributeKeyCreationTime, post.Created.String()), - sdk.NewAttribute(types.AttributeKeyPostOwner, post.Creator.String()), + sdk.NewAttribute(types.AttributeKeyPostID, post.GetID().String()), + sdk.NewAttribute(types.AttributeKeyPostParentID, post.GetParentID().String()), + sdk.NewAttribute(types.AttributeKeyCreationTime, post.CreationTime().String()), + sdk.NewAttribute(types.AttributeKeyPostOwner, post.Owner().String()), ) ctx.EventManager().EmitEvent(createEvent) return sdk.Result{ - Data: keeper.Cdc.MustMarshalBinaryLengthPrefixed(post.PostID), + Data: keeper.Cdc.MustMarshalBinaryLengthPrefixed(post.GetID()), Events: sdk.Events{createEvent}, } } @@ -83,29 +105,29 @@ func handleMsgEditPost(ctx sdk.Context, keeper Keeper, msg types.MsgEditPost) sd } // Checks if the the msg sender is the same as the current owner - if !msg.Editor.Equals(existing.Creator) { + if !msg.Editor.Equals(existing.Owner()) { return sdk.ErrUnauthorized("Incorrect owner").Result() } // Check the validity of the current block height respect to the creation date of the post - if existing.Created.After(msg.EditDate) { + if existing.CreationTime().After(msg.EditDate) { return sdk.ErrUnknownRequest("Edit date cannot be before creation date").Result() } // Edit the post - existing.Message = msg.Message - existing.LastEdited = msg.EditDate + existing = existing.SetMessage(msg.Message) + existing = existing.SetEditTime(msg.EditDate) keeper.SavePost(ctx, existing) editEvent := sdk.NewEvent( types.EventTypePostEdited, - sdk.NewAttribute(types.AttributeKeyPostID, existing.PostID.String()), - sdk.NewAttribute(types.AttributeKeyPostEditTime, existing.LastEdited.String()), + sdk.NewAttribute(types.AttributeKeyPostID, existing.GetID().String()), + sdk.NewAttribute(types.AttributeKeyPostEditTime, existing.GetEditTime().String()), ) ctx.EventManager().EmitEvent(editEvent) return sdk.Result{ - Data: keeper.Cdc.MustMarshalBinaryLengthPrefixed(existing.PostID), + Data: keeper.Cdc.MustMarshalBinaryLengthPrefixed(existing.GetID()), Events: sdk.Events{editEvent}, } } @@ -120,7 +142,7 @@ func handleMsgAddPostReaction(ctx sdk.Context, keeper Keeper, msg types.MsgAddPo // Create and store the reaction reaction := types.NewReaction(msg.Value, msg.User) - if err := keeper.SaveReaction(ctx, post.PostID, reaction); err != nil { + if err := keeper.SaveReaction(ctx, post.GetID(), reaction); err != nil { return err.Result() } @@ -148,7 +170,7 @@ func handleMsgRemovePostReaction(ctx sdk.Context, keeper Keeper, msg types.MsgRe } // Remove the reaction - if err := keeper.RemoveReaction(ctx, post.PostID, msg.User, msg.Reaction); err != nil { + if err := keeper.RemoveReaction(ctx, post.GetID(), msg.User, msg.Reaction); err != nil { return err.Result() } diff --git a/x/posts/internal/keeper/handler_test.go b/x/posts/internal/keeper/handler_test.go index dbb62d524b..6d955daf39 100644 --- a/x/posts/internal/keeper/handler_test.go +++ b/x/posts/internal/keeper/handler_test.go @@ -26,30 +26,48 @@ func Test_handleMsgCreatePost(t *testing.T) { { name: "Trying to store post with same id returns expError", storedPosts: types.Posts{ - types.NewPost(types.PostID(1), testPost.ParentID, testPost.Message, testPost.AllowsComments, "desmos", map[string]string{}, testPost.Created, testPost.Creator), + types.NewTextPost(types.PostID(1), testPost.GetParentID(), testPost.GetMessage(), testPost.CanComment(), "desmos", map[string]string{}, testPost.GetEditTime(), testPost.Owner()), }, lastPostID: types.PostID(0), - msg: types.NewMsgCreatePost(testPost.Message, testPost.ParentID, testPost.AllowsComments, "desmos", map[string]string{}, testPost.Creator, testPost.Created), + msg: types.NewMsgCreateTextPost(testPost.GetMessage(), testPost.GetParentID(), testPost.CanComment(), "desmos", map[string]string{}, testPost.Owner(), testPost.GetEditTime()), expError: "Post with id 1 already exists", }, { - name: "Post with new id is stored properly", - msg: types.NewMsgCreatePost(testPost.Message, testPost.ParentID, testPost.AllowsComments, testPost.Subspace, testPost.OptionalData, testPost.Creator, testPost.Created), - expPost: types.NewPost(types.PostID(1), testPost.ParentID, testPost.Message, testPost.AllowsComments, testPost.Subspace, testPost.OptionalData, testPost.Created, testPost.Creator), + name: "Text Post with new id is stored properly", + msg: types.NewMsgCreateTextPost(testPost.GetMessage(), testPost.GetParentID(), testPost.CanComment(), testPost.GetSubspace(), testPost.GetOptionalData(), testPost.Owner(), testPost.GetEditTime()), + expPost: types.NewTextPost(types.PostID(1), testPost.GetParentID(), testPost.GetMessage(), testPost.CanComment(), testPost.GetSubspace(), testPost.GetOptionalData(), testPost.GetEditTime(), testPost.Owner()), }, { name: "Storing a valid post with missing parent id returns expError", - msg: types.NewMsgCreatePost(testPost.Message, types.PostID(50), false, "desmos", map[string]string{}, testPost.Creator, testPost.Created), + msg: types.NewMsgCreateTextPost(testPost.GetMessage(), types.PostID(50), false, "desmos", map[string]string{}, testPost.Owner(), testPost.GetEditTime()), expError: "Parent post with id 50 not found", }, { name: "Storing a valid post with parent stored but not accepting comments returns expError", storedPosts: types.Posts{ - types.NewPost(types.PostID(50), types.PostID(50), "Parent post", false, "desmos", map[string]string{}, testPost.Created, testPost.Creator), + types.NewTextPost(types.PostID(50), types.PostID(50), "Parent post", false, "desmos", map[string]string{}, testPost.GetEditTime(), testPost.Owner()), }, - msg: types.NewMsgCreatePost(testPost.Message, types.PostID(50), false, "desmos", map[string]string{}, testPost.Creator, testPost.Created), + msg: types.NewMsgCreateTextPost(testPost.GetMessage(), types.PostID(50), false, "desmos", map[string]string{}, testPost.Owner(), testPost.GetEditTime()), expError: "Post with id 50 does not allow comments", }, + { + name: "Media Post with new id is stored properly", + msg: types.NewMsgCreateMediaPost(types.NewMsgCreateTextPost(testPost.GetMessage(), testPost.GetParentID(), false, "desmos", map[string]string{}, testPost.Owner(), testPost.CreationTime()), types.PostMedias{types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }}), + expPost: types.NewMediaPost( + types.NewTextPost(types.PostID(1), types.PostID(0), "Post message", false, "desmos", map[string]string{}, testPost.CreationTime(), testPost.Owner()), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }, + }, + ), + }, } for _, test := range tests { @@ -59,7 +77,7 @@ func Test_handleMsgCreatePost(t *testing.T) { store := ctx.KVStore(k.StoreKey) for _, p := range test.storedPosts { - store.Set([]byte(types.PostStorePrefix+p.PostID.String()), k.Cdc.MustMarshalBinaryBare(p)) + store.Set([]byte(types.PostStorePrefix+p.GetID().String()), k.Cdc.MustMarshalBinaryBare(p)) } if test.lastPostID.Valid() { @@ -75,19 +93,19 @@ func Test_handleMsgCreatePost(t *testing.T) { // Check the post var stored types.Post - k.Cdc.MustUnmarshalBinaryBare(store.Get([]byte(types.PostStorePrefix+test.expPost.PostID.String())), &stored) + k.Cdc.MustUnmarshalBinaryBare(store.Get([]byte(types.PostStorePrefix+test.expPost.GetID().String())), &stored) assert.True(t, stored.Equals(test.expPost), "Expected: %s, actual: %s", test.expPost, stored) // Check the data - assert.Equal(t, k.Cdc.MustMarshalBinaryLengthPrefixed(test.expPost.PostID), res.Data) + assert.Equal(t, k.Cdc.MustMarshalBinaryLengthPrefixed(test.expPost.GetID()), res.Data) // Check the events creationEvent := sdk.NewEvent( types.EventTypePostCreated, - 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.Creator.String()), + sdk.NewAttribute(types.AttributeKeyPostID, test.expPost.GetID().String()), + sdk.NewAttribute(types.AttributeKeyPostParentID, test.expPost.GetParentID().String()), + sdk.NewAttribute(types.AttributeKeyCreationTime, test.expPost.CreationTime().String()), + sdk.NewAttribute(types.AttributeKeyPostOwner, test.expPost.Owner().String()), ) assert.Len(t, ctx.EventManager().Events(), 1) assert.Contains(t, ctx.EventManager().Events(), creationEvent) @@ -108,43 +126,43 @@ func Test_handleMsgEditPost(t *testing.T) { testData := []struct { name string - storedPost *types.Post + storedPost types.Post msg types.MsgEditPost expError string - expPost types.Post + expPost types.TextPost }{ { - name: "Post not found", + name: "Text Post not found", storedPost: nil, - msg: types.NewMsgEditPost(types.PostID(0), "Edited message", testPostOwner, testPost.Created), + msg: types.NewMsgEditPost(types.PostID(0), "Edited message", testPostOwner, testPost.GetEditTime()), expError: "Post with id 0 not found", }, { name: "Invalid editor", storedPost: &testPost, - msg: types.NewMsgEditPost(testPost.PostID, "Edited message", editor, testPost.Created), + msg: types.NewMsgEditPost(testPost.GetID(), "Edited message", editor, testPost.GetEditTime()), expError: "Incorrect owner", }, { name: "Edit date before creation date", storedPost: &testPost, - msg: types.NewMsgEditPost(testPost.PostID, "Edited message", testPost.Creator, testPost.Created.AddDate(0, 0, -1)), + msg: types.NewMsgEditPost(testPost.GetID(), "Edited message", testPost.Owner(), testPost.GetEditTime().AddDate(0, 0, -1)), expError: "Edit date cannot be before creation date", }, { name: "Valid request is handled properly", storedPost: &testPost, - msg: types.NewMsgEditPost(testPost.PostID, "Edited message", testPost.Creator, testPost.Created.AddDate(0, 0, 1)), - expPost: types.Post{ - PostID: testPost.PostID, - ParentID: testPost.ParentID, + msg: types.NewMsgEditPost(testPost.GetID(), "Edited message", testPost.Owner(), testPost.Created.AddDate(0, 0, 1)), + expPost: types.TextPost{ + PostID: testPost.GetID(), + ParentID: testPost.GetParentID(), Message: "Edited message", - Created: testPost.Created, + Created: testPost.CreationTime(), LastEdited: testPost.Created.AddDate(0, 0, 1), - AllowsComments: testPost.AllowsComments, - Subspace: testPost.Subspace, - OptionalData: testPost.OptionalData, - Creator: testPost.Creator, + AllowsComments: testPost.CanComment(), + Subspace: testPost.GetSubspace(), + OptionalData: testPost.GetOptionalData(), + Creator: testPost.Owner(), }, }, } @@ -157,7 +175,7 @@ func Test_handleMsgEditPost(t *testing.T) { store := ctx.KVStore(k.StoreKey) if test.storedPost != nil { store.Set( - []byte(types.PostStorePrefix+test.storedPost.PostID.String()), + []byte(types.PostStorePrefix+test.storedPost.GetID().String()), k.Cdc.MustMarshalBinaryBare(&test.storedPost), ) } @@ -174,8 +192,8 @@ func Test_handleMsgEditPost(t *testing.T) { sdk.NewAttribute(types.AttributeKeyPostEditTime, test.msg.EditDate.String()), )) - var stored types.Post - k.Cdc.MustUnmarshalBinaryBare(store.Get([]byte(types.PostStorePrefix+testPost.PostID.String())), &stored) + var stored types.TextPost + k.Cdc.MustUnmarshalBinaryBare(store.Get([]byte(types.PostStorePrefix+testPost.GetID().String())), &stored) assert.True(t, test.expPost.Equals(stored)) } @@ -194,19 +212,19 @@ func Test_handleMsgAddPostReaction(t *testing.T) { user, _ := sdk.AccAddressFromBech32("cosmos1q4hx350dh0843wr3csctxr87at3zcvd9qehqvg") tests := []struct { name string - existingPost *types.Post + existingPost types.Post msg types.MsgAddPostReaction error string }{ { - name: "Post not found", + name: "Text Post not found", msg: types.NewMsgAddPostReaction(types.PostID(0), "like", user), error: "Post with id 0 not found", }, { name: "Valid message works properly", existingPost: &testPost, - msg: types.NewMsgAddPostReaction(testPost.PostID, "like", user), + msg: types.NewMsgAddPostReaction(testPost.GetID(), "like", user), error: "", }, } @@ -219,7 +237,7 @@ func Test_handleMsgAddPostReaction(t *testing.T) { store := ctx.KVStore(k.StoreKey) if test.existingPost != nil { store.Set( - []byte(types.PostStorePrefix+test.existingPost.PostID.String()), + []byte(types.PostStorePrefix+test.existingPost.GetID().String()), k.Cdc.MustMarshalBinaryBare(&test.existingPost), ) } @@ -237,8 +255,8 @@ func Test_handleMsgAddPostReaction(t *testing.T) { sdk.NewAttribute(types.AttributeKeyReactionValue, test.msg.Value), )) - var storedPost types.Post - k.Cdc.MustUnmarshalBinaryBare(store.Get([]byte(types.PostStorePrefix+testPost.PostID.String())), &storedPost) + var storedPost types.TextPost + k.Cdc.MustUnmarshalBinaryBare(store.Get([]byte(types.PostStorePrefix+testPost.GetID().String())), &storedPost) assert.True(t, test.existingPost.Equals(storedPost)) var storedReactions types.Reactions @@ -260,27 +278,27 @@ func Test_handleMsgRemovePostReaction(t *testing.T) { reaction := types.NewReaction("like", user) tests := []struct { name string - existingPost *types.Post + existingPost types.Post existingReaction *types.Reaction msg types.MsgRemovePostReaction error string }{ { - name: "Post not found", + name: "Text Post not found", msg: types.NewMsgRemovePostReaction(types.PostID(0), user, "like"), error: "Post with id 0 not found", }, { name: "Reaction not found", existingPost: &testPost, - msg: types.NewMsgRemovePostReaction(testPost.PostID, user, "like"), + msg: types.NewMsgRemovePostReaction(testPost.GetID(), user, "like"), error: fmt.Sprintf("Cannot remove the reaction with value like from user %s as it does not exist", user), }, { name: "Valid message works properly", existingPost: &testPost, existingReaction: &reaction, - msg: types.NewMsgRemovePostReaction(testPost.PostID, user, reaction.Value), + msg: types.NewMsgRemovePostReaction(testPost.GetID(), user, reaction.Value), error: "", }, } @@ -293,14 +311,14 @@ func Test_handleMsgRemovePostReaction(t *testing.T) { store := ctx.KVStore(k.StoreKey) if test.existingPost != nil { store.Set( - []byte(types.PostStorePrefix+test.existingPost.PostID.String()), + []byte(types.PostStorePrefix+test.existingPost.GetID().String()), k.Cdc.MustMarshalBinaryBare(&test.existingPost), ) } if test.existingReaction != nil { store.Set( - []byte(types.PostReactionsStorePrefix+test.existingPost.PostID.String()), + []byte(types.PostReactionsStorePrefix+test.existingPost.GetID().String()), k.Cdc.MustMarshalBinaryBare(&types.Reactions{*test.existingReaction}), ) } @@ -318,8 +336,8 @@ func Test_handleMsgRemovePostReaction(t *testing.T) { sdk.NewAttribute(types.AttributeKeyReactionValue, test.msg.Reaction), )) - var storedPost types.Post - k.Cdc.MustUnmarshalBinaryBare(store.Get([]byte(types.PostStorePrefix+testPost.PostID.String())), &storedPost) + var storedPost types.TextPost + k.Cdc.MustUnmarshalBinaryBare(store.Get([]byte(types.PostStorePrefix+testPost.GetID().String())), &storedPost) assert.True(t, test.existingPost.Equals(storedPost)) var storedReactions types.Reactions diff --git a/x/posts/internal/keeper/keeper.go b/x/posts/internal/keeper/keeper.go index 4aa315afc5..912422e15f 100644 --- a/x/posts/internal/keeper/keeper.go +++ b/x/posts/internal/keeper/keeper.go @@ -26,7 +26,7 @@ func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey) Keeper { } // ------------- -// --- Posts +// --- Post // ------------- func (k Keeper) getPostStoreKey(postID types.PostID) []byte { @@ -52,21 +52,21 @@ func (k Keeper) SavePost(ctx sdk.Context, post types.Post) { store := ctx.KVStore(k.StoreKey) // Save the post - store.Set([]byte(types.PostStorePrefix+post.PostID.String()), k.Cdc.MustMarshalBinaryBare(&post)) + store.Set([]byte(types.PostStorePrefix+post.GetID().String()), k.Cdc.MustMarshalBinaryBare(&post)) // Set the last post id only if the current post has a greater one than the last one stored - if post.PostID > k.GetLastPostID(ctx) { - store.Set([]byte(types.LastPostIDStoreKey), k.Cdc.MustMarshalBinaryBare(&post.PostID)) + if id := post.GetID(); id > k.GetLastPostID(ctx) { + store.Set([]byte(types.LastPostIDStoreKey), k.Cdc.MustMarshalBinaryBare(&id)) } // Save the comments to the parent post, if it is valid - if post.ParentID.Valid() { - parentCommentsKey := []byte(types.PostCommentsStorePrefix + post.ParentID.String()) + if post.GetParentID().Valid() { + parentCommentsKey := []byte(types.PostCommentsStorePrefix + post.GetParentID().String()) var commentsIDs types.PostIDs k.Cdc.MustUnmarshalBinaryBare(store.Get(parentCommentsKey), &commentsIDs) - if editedIDs, appended := commentsIDs.AppendIfMissing(post.PostID); appended { + if editedIDs, appended := commentsIDs.AppendIfMissing(post.GetID()); appended { store.Set(parentCommentsKey, k.Cdc.MustMarshalBinaryBare(&editedIDs)) } } @@ -79,7 +79,7 @@ func (k Keeper) GetPost(ctx sdk.Context, id types.PostID) (post types.Post, foun key := k.getPostStoreKey(id) if !store.Has(key) { - return types.Post{}, false + return types.TextPost{}, false } k.Cdc.MustUnmarshalBinaryBare(store.Get(key), &post) @@ -126,27 +126,27 @@ func (k Keeper) GetPostsFiltered(ctx sdk.Context, params types.QueryPostsParams) // match parent id if valid if params.ParentID != nil { - matchParentID = params.ParentID.Equals(p.ParentID) + matchParentID = params.ParentID.Equals(p.GetParentID()) } // match creation time if valid height if params.CreationTime != nil { - matchCreationTime = params.CreationTime.Equal(p.Created) + matchCreationTime = params.CreationTime.Equal(p.CreationTime()) } // match allows comments if params.AllowsComments != nil { - matchAllowsComments = *params.AllowsComments == p.AllowsComments + matchAllowsComments = *params.AllowsComments == p.CanComment() } // match subspace if provided if len(params.Subspace) > 0 { - matchSubspace = params.Subspace == p.Subspace + matchSubspace = params.Subspace == p.GetSubspace() } // match creator address (if supplied) if len(params.Creator) > 0 { - matchCreator = params.Creator.Equals(p.Creator) + matchCreator = params.Creator.Equals(p.Owner()) } if matchParentID && matchCreationTime && matchAllowsComments && matchSubspace && matchCreator { diff --git a/x/posts/internal/keeper/keeper_test.go b/x/posts/internal/keeper/keeper_test.go index 7251cc1045..419dcf876c 100644 --- a/x/posts/internal/keeper/keeper_test.go +++ b/x/posts/internal/keeper/keeper_test.go @@ -10,7 +10,7 @@ import ( ) // ------------- -// --- Posts +// --- TextPosts // ------------- func TestKeeper_GetLastPostId(t *testing.T) { @@ -56,9 +56,9 @@ func TestKeeper_SavePost(t *testing.T) { expLastID types.PostID }{ { - name: "Post with ID already present", + name: "TextPost with ID already present", existingPosts: types.Posts{ - types.NewPost(types.PostID(1), + types.NewTextPost(types.PostID(1), types.PostID(0), "Post", false, @@ -69,7 +69,7 @@ func TestKeeper_SavePost(t *testing.T) { ), }, lastPostID: types.PostID(1), - newPost: types.NewPost(types.PostID(1), + newPost: types.NewTextPost(types.PostID(1), types.PostID(0), "New post", false, @@ -82,9 +82,9 @@ func TestKeeper_SavePost(t *testing.T) { expLastID: types.PostID(1), }, { - name: "Post which ID is not already present", + name: "TextPost which ID is not already present", existingPosts: types.Posts{ - types.NewPost(types.PostID(1), + types.NewTextPost(types.PostID(1), types.PostID(0), "Post", false, @@ -95,7 +95,7 @@ func TestKeeper_SavePost(t *testing.T) { ), }, lastPostID: types.PostID(1), - newPost: types.NewPost(types.PostID(15), + newPost: types.NewTextPost(types.PostID(15), types.PostID(0), "New post", false, @@ -108,9 +108,9 @@ func TestKeeper_SavePost(t *testing.T) { expLastID: types.PostID(15), }, { - name: "Post with valid parent ID", + name: "TextPost with valid parent ID", existingPosts: []types.Post{ - types.NewPost(types.PostID(1), + types.NewTextPost(types.PostID(1), types.PostID(0), "Parent", false, @@ -121,7 +121,7 @@ func TestKeeper_SavePost(t *testing.T) { ), }, lastPostID: types.PostID(1), - newPost: types.NewPost(types.PostID(15), + newPost: types.NewTextPost(types.PostID(15), types.PostID(1), "Comment", false, @@ -134,9 +134,9 @@ func TestKeeper_SavePost(t *testing.T) { expLastID: types.PostID(15), }, { - name: "Post with ID greater ID than Last ID stored", + name: "TextPost with ID greater ID than Last ID stored", existingPosts: types.Posts{ - types.NewPost(types.PostID(4), + types.NewTextPost(types.PostID(4), types.PostID(0), "Post lesser", false, @@ -147,7 +147,7 @@ func TestKeeper_SavePost(t *testing.T) { ), }, lastPostID: types.PostID(4), - newPost: types.NewPost(types.PostID(5), + newPost: types.NewTextPost(types.PostID(5), types.PostID(0), "New post greater", false, @@ -160,9 +160,9 @@ func TestKeeper_SavePost(t *testing.T) { expLastID: types.PostID(5), }, { - name: "Post with ID lesser ID than Last ID stored", + name: "TextPost with ID lesser ID than Last ID stored", existingPosts: types.Posts{ - types.NewPost(types.PostID(4), + types.NewTextPost(types.PostID(4), types.PostID(0), "Post ID greater", false, @@ -173,7 +173,7 @@ func TestKeeper_SavePost(t *testing.T) { ), }, lastPostID: types.PostID(4), - newPost: types.NewPost(types.PostID(3), + newPost: types.NewTextPost(types.PostID(3), types.PostID(0), "New post ID lesser", false, @@ -185,6 +185,24 @@ func TestKeeper_SavePost(t *testing.T) { expParentCommentsIDs: []types.PostID{}, expLastID: types.PostID(4), }, + { + name: "MediaPost stored, existing text posts", + existingPosts: types.Posts{ + types.NewTextPost(types.PostID(1), types.PostID(0), "a text post", false, "desmos", map[string]string{}, testPost.Created, testPostOwner), + }, + lastPostID: types.PostID(0), + newPost: types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPost.Created, testPostOwner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }, + }), + expParentCommentsIDs: []types.PostID{}, + expLastID: types.PostID(2), + }, } for _, test := range tests { @@ -194,7 +212,7 @@ func TestKeeper_SavePost(t *testing.T) { store := ctx.KVStore(k.StoreKey) for _, p := range test.existingPosts { - store.Set([]byte(types.PostStorePrefix+p.PostID.String()), k.Cdc.MustMarshalBinaryBare(p)) + store.Set([]byte(types.PostStorePrefix+p.GetID().String()), k.Cdc.MustMarshalBinaryBare(p)) store.Set([]byte(types.LastPostIDStoreKey), k.Cdc.MustMarshalBinaryBare(test.lastPostID)) } @@ -203,7 +221,7 @@ func TestKeeper_SavePost(t *testing.T) { // Check the stored post var expected types.Post - k.Cdc.MustUnmarshalBinaryBare(store.Get([]byte(types.PostStorePrefix+test.newPost.PostID.String())), &expected) + k.Cdc.MustUnmarshalBinaryBare(store.Get([]byte(types.PostStorePrefix+test.newPost.GetID().String())), &expected) assert.True(t, expected.Equals(test.newPost)) // Check the latest post id @@ -213,7 +231,7 @@ func TestKeeper_SavePost(t *testing.T) { // Check the parent comments var parentCommentsIDs []types.PostID - k.Cdc.MustUnmarshalBinaryBare(store.Get([]byte(types.PostCommentsStorePrefix+test.newPost.ParentID.String())), &parentCommentsIDs) + k.Cdc.MustUnmarshalBinaryBare(store.Get([]byte(types.PostCommentsStorePrefix+test.newPost.GetParentID().String())), &parentCommentsIDs) assert.True(t, test.expParentCommentsIDs.Equals(parentCommentsIDs)) }) } @@ -227,15 +245,34 @@ func TestKeeper_GetPost(t *testing.T) { expected types.Post }{ { - name: "Non existent post is not found", + name: "Non existent text post is not found", ID: types.PostID(123), - expected: types.Post{}, + expected: types.TextPost{}, }, { - name: "Existing post is found properly", + name: "Existing text post is found properly", ID: types.PostID(45), postExists: true, - expected: types.NewPost(types.PostID(45), types.PostID(0), "Post", false, "desmos", map[string]string{}, testPost.Created, testPost.Creator), + expected: types.NewTextPost(types.PostID(45), types.PostID(0), "TextPost", false, "desmos", map[string]string{}, testPost.Created, testPostOwner), + }, + { + name: "Non Existent media post is not found", + ID: types.PostID(15), + expected: types.MediaPost{}, + }, + { + name: "Existing media post is found properly", + ID: types.PostID(15), + postExists: true, + expected: types.NewMediaPost( + types.NewTextPost(types.PostID(15), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPost.Created, testPostOwner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }, + }), }, } @@ -246,7 +283,7 @@ func TestKeeper_GetPost(t *testing.T) { store := ctx.KVStore(k.StoreKey) if test.postExists { - store.Set([]byte(types.PostStorePrefix+test.expected.PostID.String()), k.Cdc.MustMarshalBinaryBare(&test.expected)) + store.Set([]byte(types.PostStorePrefix+test.expected.GetID().String()), k.Cdc.MustMarshalBinaryBare(&test.expected)) } expected, found := k.GetPost(ctx, test.ID) @@ -273,11 +310,20 @@ func TestKeeper_GetPostChildrenIDs(t *testing.T) { { name: "Non empty children list is returned properly", storedPosts: types.Posts{ - types.NewPost(types.PostID(10), types.PostID(0), "Original post", false, "desmos", map[string]string{}, testPost.Created, testPost.Creator), - types.NewPost(types.PostID(55), types.PostID(10), "First commit", false, "desmos", map[string]string{}, testPost.Created, testPost.Creator), - types.NewPost(types.PostID(78), types.PostID(10), "Other commit", false, "desmos", map[string]string{}, testPost.Created, testPost.Creator), - types.NewPost(types.PostID(11), types.PostID(0), "Second post", false, "desmos", map[string]string{}, testPost.Created, testPost.Creator), - types.NewPost(types.PostID(104), types.PostID(11), "Comment to second post", false, "desmos", map[string]string{}, testPost.Created, testPost.Creator), + types.NewTextPost(types.PostID(10), types.PostID(0), "Original post", false, "desmos", map[string]string{}, testPost.Created, testPost.Creator), + types.NewTextPost(types.PostID(55), types.PostID(10), "First commit", false, "desmos", map[string]string{}, testPost.Created, testPost.Creator), + types.NewMediaPost( + types.NewTextPost(types.PostID(78), types.PostID(10), "Second Commit", false, "desmos", map[string]string{}, testPost.Created, testPostOwner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }, + }, + ), + types.NewTextPost(types.PostID(11), types.PostID(0), "Second post", false, "desmos", map[string]string{}, testPost.Created, testPost.Creator), + types.NewTextPost(types.PostID(104), types.PostID(11), "Comment to second post", false, "desmos", map[string]string{}, testPost.Created, testPost.Creator), }, postID: types.PostID(10), expChildrenIDs: types.PostIDs{types.PostID(55), types.PostID(78)}, @@ -315,8 +361,17 @@ func TestKeeper_GetPosts(t *testing.T) { { name: "Existing list is returned properly", posts: types.Posts{ - types.NewPost(types.PostID(13), types.PostID(0), "", false, "desmos", map[string]string{}, testPost.Created, testPost.Creator), - types.NewPost(types.PostID(76), types.PostID(0), "", false, "desmos", map[string]string{}, testPost.Created, testPost.Creator), + types.NewTextPost(types.PostID(13), types.PostID(0), "", false, "desmos", map[string]string{}, testPost.Created, testPostOwner), + types.NewMediaPost( + types.NewTextPost(types.PostID(78), types.PostID(10), "Second Commit", false, "desmos", map[string]string{}, testPost.Created, testPostOwner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }, + }, + ), }, }, } @@ -328,11 +383,13 @@ func TestKeeper_GetPosts(t *testing.T) { store := ctx.KVStore(k.StoreKey) for _, p := range test.posts { - store.Set([]byte(types.PostStorePrefix+p.PostID.String()), k.Cdc.MustMarshalBinaryBare(p)) + store.Set([]byte(types.PostStorePrefix+p.GetID().String()), k.Cdc.MustMarshalBinaryBare(p)) } posts := k.GetPosts(ctx) - assert.True(t, test.posts.Equals(posts)) + for index, post := range test.posts { + assert.True(t, post.Equals(posts[index])) + } }) } } @@ -346,8 +403,8 @@ func TestKeeper_GetPostsFiltered(t *testing.T) { timeZone, _ := time.LoadLocation("UTC") date := time.Date(2020, 1, 1, 1, 1, 0, 0, timeZone) - posts := types.Posts{ - types.NewPost( + posts := types.TextPosts{ + types.NewTextPost( types.PostID(10), types.PostID(1), "Post 1", @@ -357,7 +414,7 @@ func TestKeeper_GetPostsFiltered(t *testing.T) { date, creator1, ), - types.NewPost( + types.NewTextPost( types.PostID(11), types.PostID(1), "Post 2", @@ -367,7 +424,7 @@ func TestKeeper_GetPostsFiltered(t *testing.T) { time.Date(2020, 2, 1, 1, 1, 0, 0, timeZone), creator2, ), - types.NewPost( + types.NewTextPost( types.PostID(12), types.PostID(2), "Post 3", @@ -382,47 +439,47 @@ func TestKeeper_GetPostsFiltered(t *testing.T) { tests := []struct { name string filter types.QueryPostsParams - expected types.Posts + expected types.TextPosts }{ { name: "Valid pagination works properly", filter: types.DefaultQueryPostsParams(1, 2), - expected: types.Posts{posts[0], posts[1]}, + expected: types.TextPosts{posts[0], posts[1]}, }, { name: "Non existing page returns empty list", filter: types.DefaultQueryPostsParams(10, 1), - expected: types.Posts{}, + expected: types.TextPosts{}, }, { name: "Invalid pagination returns all data", filter: types.DefaultQueryPostsParams(1, 15), - expected: types.Posts{posts[0], posts[1], posts[2]}, + expected: types.TextPosts{posts[0], posts[1], posts[2]}, }, { name: "Parent ID matcher works properly", filter: types.QueryPostsParams{Page: 1, Limit: 5, ParentID: &posts[0].ParentID}, - expected: types.Posts{posts[0], posts[1]}, + expected: types.TextPosts{posts[0], posts[1]}, }, { name: "Creation time matcher works properly", filter: types.QueryPostsParams{Page: 1, Limit: 5, CreationTime: &date}, - expected: types.Posts{posts[0], posts[2]}, + expected: types.TextPosts{posts[0], posts[2]}, }, { name: "Allows comments matcher works properly", filter: types.QueryPostsParams{Page: 1, Limit: 5, AllowsComments: &boolTrue}, - expected: types.Posts{posts[1]}, + expected: types.TextPosts{posts[1]}, }, { name: "Subspace mather works properly", filter: types.QueryPostsParams{Page: 1, Limit: 5, Subspace: "desmos"}, - expected: types.Posts{posts[1], posts[2]}, + expected: types.TextPosts{posts[1], posts[2]}, }, { name: "Creator mather works properly", filter: types.QueryPostsParams{Page: 1, Limit: 5, Creator: creator2}, - expected: types.Posts{posts[1], posts[2]}, + expected: types.TextPosts{posts[1], posts[2]}, }, } diff --git a/x/posts/internal/keeper/querier.go b/x/posts/internal/keeper/querier.go index a36342fb56..8a09f75426 100644 --- a/x/posts/internal/keeper/querier.go +++ b/x/posts/internal/keeper/querier.go @@ -30,13 +30,13 @@ func NewQuerier(keeper Keeper) sdk.Querier { // using the given Context and Keeper. func getPostResponse(ctx sdk.Context, keeper Keeper, post types.Post) types.PostQueryResponse { // Get the likes - postLikes := keeper.GetPostReactions(ctx, post.PostID) + postLikes := keeper.GetPostReactions(ctx, post.GetID()) if postLikes == nil { postLikes = types.Reactions{} } // Get the children - childrenIDs := keeper.GetPostChildrenIDs(ctx, post.PostID) + childrenIDs := keeper.GetPostChildrenIDs(ctx, post.GetID()) if childrenIDs == nil { childrenIDs = types.PostIDs{} } diff --git a/x/posts/internal/keeper/querier_test.go b/x/posts/internal/keeper/querier_test.go index b30dc3844c..45dbc32e6c 100644 --- a/x/posts/internal/keeper/querier_test.go +++ b/x/posts/internal/keeper/querier_test.go @@ -35,33 +35,74 @@ func Test_queryPost(t *testing.T) { { name: "Post without likes is returned properly", storedPosts: types.Posts{ - types.NewPost(types.PostID(1), types.PostID(0), "Parent", false, "", map[string]string{}, testPost.Created, creator), - types.NewPost(types.PostID(2), types.PostID(1), "Child", false, "", map[string]string{}, testPost.Created, creator), + types.NewTextPost(types.PostID(1), types.PostID(0), "Parent", false, "", map[string]string{}, testPost.Created, creator), + types.NewTextPost(types.PostID(2), types.PostID(1), "Child", false, "", map[string]string{}, testPost.Created, creator), + types.NewMediaPost( + types.NewTextPost(types.PostID(3), types.PostID(1), "Second Child", false, "", map[string]string{}, testPost.Created, creator), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }, + }, + ), }, path: []string{types.QueryPost, "1"}, expResult: types.NewPostResponse( - types.NewPost(types.PostID(1), types.PostID(0), "Parent", false, "", map[string]string{}, testPost.Created, creator), + types.NewTextPost(types.PostID(1), types.PostID(0), "Parent", false, "", map[string]string{}, testPost.Created, creator), types.Reactions{}, - types.PostIDs{types.PostID(2)}, + types.PostIDs{types.PostID(2), types.PostID(3)}, ), }, { - name: "Post without children is returned properly", + name: "Text Post without children is returned properly", storedPosts: types.Posts{ - types.NewPost(types.PostID(1), types.PostID(0), "Parent", false, "", map[string]string{}, testPost.Created, creator), + types.NewTextPost(types.PostID(1), types.PostID(0), "Parent", false, "", map[string]string{}, testPost.Created, creator), }, path: []string{types.QueryPost, "1"}, expResult: types.NewPostResponse( - types.NewPost(types.PostID(1), types.PostID(0), "Parent", false, "", map[string]string{}, testPost.Created, creator), + types.NewTextPost(types.PostID(1), types.PostID(0), "Parent", false, "", map[string]string{}, testPost.Created, creator), types.Reactions{}, types.PostIDs{}, ), }, { - name: "Post with all data is returned properly", + name: "Text Post with all data is returned properly", + storedPosts: types.Posts{ + types.NewTextPost(types.PostID(1), types.PostID(0), "Parent", false, "", map[string]string{}, testPost.Created, creator), + types.NewTextPost(types.PostID(2), types.PostID(1), "Child", false, "", map[string]string{}, testPost.Created, creator), + }, + storedReactions: map[types.PostID]types.Reactions{ + types.PostID(1): { + types.NewReaction("Like", creator), + types.NewReaction("Like", otherCreator), + }, + }, + path: []string{types.QueryPost, "1"}, + expResult: types.NewPostResponse( + types.NewTextPost(types.PostID(1), types.PostID(0), "Parent", false, "", map[string]string{}, testPost.Created, creator), + types.Reactions{ + types.NewReaction("Like", creator), + types.NewReaction("Like", otherCreator), + }, + types.PostIDs{types.PostID(2)}, + ), + }, + { + name: "Media Post with all data is returned properly", storedPosts: types.Posts{ - types.NewPost(types.PostID(1), types.PostID(0), "Parent", false, "", map[string]string{}, testPost.Created, creator), - types.NewPost(types.PostID(2), types.PostID(1), "Child", false, "", map[string]string{}, testPost.Created, creator), + types.NewMediaPost( + types.NewTextPost(types.PostID(1), types.PostID(0), "Parent", false, "desmos", map[string]string{}, testPost.Created, creator), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }, + }, + ), + types.NewTextPost(types.PostID(2), types.PostID(1), "Child", false, "", map[string]string{}, testPost.Created, creator), }, storedReactions: map[types.PostID]types.Reactions{ types.PostID(1): { @@ -71,7 +112,16 @@ func Test_queryPost(t *testing.T) { }, path: []string{types.QueryPost, "1"}, expResult: types.NewPostResponse( - types.NewPost(types.PostID(1), types.PostID(0), "Parent", false, "", map[string]string{}, testPost.Created, creator), + types.NewMediaPost( + types.NewTextPost(types.PostID(1), types.PostID(0), "Parent", false, "desmos", map[string]string{}, testPost.Created, creator), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }, + }, + ), types.Reactions{ types.NewReaction("Like", creator), types.NewReaction("Like", otherCreator), @@ -122,18 +172,56 @@ func Test_queryPosts(t *testing.T) { { name: "Empty params returns all", storedPosts: types.Posts{ - types.NewPost(types.PostID(1), types.PostID(0), "Parent", false, "", map[string]string{}, testPost.Created, creator), - types.NewPost(types.PostID(2), types.PostID(1), "Child", false, "", map[string]string{}, testPost.Created, creator), + types.NewTextPost(types.PostID(1), types.PostID(0), "Parent", false, "", map[string]string{}, testPost.Created, creator), + types.NewTextPost(types.PostID(2), types.PostID(1), "Child", false, "", map[string]string{}, testPost.Created, creator), + }, + params: types.QueryPostsParams{}, + expResponse: []types.PostQueryResponse{ + types.NewPostResponse( + types.NewTextPost(types.PostID(1), types.PostID(0), "Parent", false, "", map[string]string{}, testPost.Created, creator), + types.Reactions{}, + types.PostIDs{types.PostID(2)}, + ), + types.NewPostResponse( + types.NewTextPost(types.PostID(2), types.PostID(1), "Child", false, "", map[string]string{}, testPost.Created, creator), + types.Reactions{}, + types.PostIDs{}, + ), + }, + }, + { + name: "Empty params returns all posts", + storedPosts: types.Posts{ + types.NewMediaPost( + types.NewTextPost(types.PostID(1), types.PostID(0), "Parent", false, "desmos", map[string]string{}, testPost.Created, creator), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }, + }, + ), + types.NewTextPost(types.PostID(2), types.PostID(1), "Child", false, "", map[string]string{}, testPost.Created, creator), }, params: types.QueryPostsParams{}, expResponse: []types.PostQueryResponse{ types.NewPostResponse( - types.NewPost(types.PostID(1), types.PostID(0), "Parent", false, "", map[string]string{}, testPost.Created, creator), + types.NewMediaPost( + types.NewTextPost(types.PostID(1), types.PostID(0), "Parent", false, "desmos", map[string]string{}, testPost.Created, creator), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }, + }, + ), types.Reactions{}, types.PostIDs{types.PostID(2)}, ), types.NewPostResponse( - types.NewPost(types.PostID(2), types.PostID(1), "Child", false, "", map[string]string{}, testPost.Created, creator), + types.NewTextPost(types.PostID(2), types.PostID(1), "Child", false, "", map[string]string{}, testPost.Created, creator), types.Reactions{}, types.PostIDs{}, ), @@ -142,13 +230,13 @@ func Test_queryPosts(t *testing.T) { { name: "Non empty params return proper posts", storedPosts: types.Posts{ - types.NewPost(types.PostID(1), types.PostID(0), "Parent", false, "", map[string]string{}, testPost.Created, creator), - types.NewPost(types.PostID(2), types.PostID(1), "Child", false, "", map[string]string{}, testPost.Created, creator), + types.NewTextPost(types.PostID(1), types.PostID(0), "Parent", false, "", map[string]string{}, testPost.Created, creator), + types.NewTextPost(types.PostID(2), types.PostID(1), "Child", false, "", map[string]string{}, testPost.Created, creator), }, params: types.DefaultQueryPostsParams(1, 1), expResponse: []types.PostQueryResponse{ types.NewPostResponse( - types.NewPost(types.PostID(1), types.PostID(0), "Parent", false, "", map[string]string{}, testPost.Created, creator), + types.NewTextPost(types.PostID(1), types.PostID(0), "Parent", false, "", map[string]string{}, testPost.Created, creator), types.Reactions{}, types.PostIDs{types.PostID(2)}, ), diff --git a/x/posts/internal/types/codec.go b/x/posts/internal/types/codec.go index 7e06be6d21..f14ef694a5 100644 --- a/x/posts/internal/types/codec.go +++ b/x/posts/internal/types/codec.go @@ -13,8 +13,12 @@ func init() { // RegisterCodec registers concrete types on the Amino codec func RegisterCodec(cdc *codec.Codec) { - cdc.RegisterConcrete(MsgCreatePost{}, "desmos/MsgCreatePost", nil) + cdc.RegisterConcrete(MsgCreateTextPost{}, "desmos/MsgCreateTextPost", nil) + cdc.RegisterConcrete(MsgCreateMediaPost{}, "desmos/MsgCreateMediaPost", nil) cdc.RegisterConcrete(MsgEditPost{}, "desmos/MsgEditPost", nil) cdc.RegisterConcrete(MsgAddPostReaction{}, "desmos/MsgAddPostReaction", nil) cdc.RegisterConcrete(MsgRemovePostReaction{}, "desmos/MsgRemovePostReaction", nil) + cdc.RegisterInterface((*Post)(nil), nil) + cdc.RegisterConcrete(TextPost{}, "desmos/TextPost", nil) + cdc.RegisterConcrete(MediaPost{}, "desmos/MediaPost", nil) } diff --git a/x/posts/internal/types/genesis_test.go b/x/posts/internal/types/genesis_test.go index 52b1880d94..61ea722290 100644 --- a/x/posts/internal/types/genesis_test.go +++ b/x/posts/internal/types/genesis_test.go @@ -21,7 +21,19 @@ func TestValidateGenesis(t *testing.T) { { name: "Genesis with invalid post errors", genesis: types.GenesisState{ - Posts: types.Posts{types.Post{PostID: types.PostID(0)}}, + Posts: types.Posts{ + types.TextPost{PostID: types.PostID(0)}, + types.NewMediaPost( + types.TextPost{PostID: types.PostID(1)}, + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "https://example.com", + MimeType: "text/plain", + }, + }, + ), + }, Reactions: map[string]types.Reactions{}, }, shouldError: true, diff --git a/x/posts/internal/types/keys.go b/x/posts/internal/types/keys.go index 3e0353442f..f4dacaab9e 100644 --- a/x/posts/internal/types/keys.go +++ b/x/posts/internal/types/keys.go @@ -15,6 +15,7 @@ const ( MaxOptionalDataFieldValueLength = 200 ActionCreatePost = "create_post" + ActionCreateMediaPost = "create_media_post" ActionEditPost = "edit_post" ActionAddPostReaction = "add_post_reaction" ActionRemovePostReaction = "remove_post_reaction" diff --git a/x/posts/internal/types/media_post.go b/x/posts/internal/types/media_post.go new file mode 100644 index 0000000000..18332bf05b --- /dev/null +++ b/x/posts/internal/types/media_post.go @@ -0,0 +1,270 @@ +package types + +import ( + "encoding/json" + "fmt" + "regexp" + "strings" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type MediaPost struct { + TextPost + Medias PostMedias `json:"medias"` +} + +func NewMediaPost(post TextPost, medias PostMedias) MediaPost { + return MediaPost{ + TextPost: post, + Medias: medias, + } +} + +// String implements fmt.Stringer +func (mp MediaPost) String() string { + bytes, err := json.Marshal(&mp) + if err != nil { + panic(err) + } + + return string(bytes) +} + +// GetID implements Post +func (mp MediaPost) GetID() PostID { + return mp.PostID +} + +// GetParentID implements Post +func (mp MediaPost) GetParentID() PostID { + return mp.ParentID +} + +// SetMessage implements Post +func (mp MediaPost) SetMessage(message string) Post { + mp.Message = message + return mp +} + +// GetMessage implements Post +func (mp MediaPost) GetMessage() string { + return mp.Message +} + +// CreationTime implements Post +func (mp MediaPost) CreationTime() time.Time { + return mp.Created +} + +// SetEditTime implements Post +func (mp MediaPost) SetEditTime(time time.Time) Post { + mp.LastEdited = time + return mp +} + +// GetEditTime implements Post +func (mp MediaPost) GetEditTime() time.Time { + return mp.LastEdited +} + +// CanComment implements Post +func (mp MediaPost) CanComment() bool { + return mp.AllowsComments +} + +// GetSubspace implements Post +func (mp MediaPost) GetSubspace() string { + return mp.Subspace +} + +// GetOptionalData implements Post +func (mp MediaPost) GetOptionalData() map[string]string { + return mp.OptionalData +} + +// Owner implements Post +func (mp MediaPost) Owner() sdk.AccAddress { + return mp.Creator +} + +// Validate implements Post Validate +func (mp MediaPost) Validate() error { + if err := mp.TextPost.Validate(); err != nil { + return err + } + + for _, media := range mp.Medias { + if err := media.Validate(); err != nil { + return err + } + } + + return nil +} + +// Equal implements Post +func (mp MediaPost) Equals(other Post) bool { + // Cast and delegate + if otherMp, ok := other.(MediaPost); ok { + return checkMediaPostEquals(mp, otherMp) + } + + return false +} + +// checkMediaPostEquals checks if two MediaPost are equal +func checkMediaPostEquals(first MediaPost, second MediaPost) bool { + return first.TextPost.Equals(second.TextPost) && first.Medias.Equals(second.Medias) +} + +// MarshalJSON implements Marshaler +func (mp MediaPost) MarshalJSON() ([]byte, error) { + type temp MediaPost + return json.Marshal(temp(mp)) +} + +// UnmarshalJSON implements Unmarshaler +func (mp *MediaPost) UnmarshalJSON(data []byte) error { + type mediaPostJSON MediaPost + var temp mediaPostJSON + if err := json.Unmarshal(data, &temp); err != nil { + return err + } + *mp = MediaPost(temp) + return nil +} + +// ------------- +// --- MediaPosts +// ------------- + +// TextPosts represents a slice of TextPost objects +type MediaPosts []MediaPost + +// checkPostsEqual returns true iff the p slice contains the same +// data in the same order of the other slice +func (mps MediaPosts) Equals(other MediaPosts) bool { + if len(mps) != len(other) { + return false + } + + for index, post := range mps { + if !post.Equals(other[index]) { + return false + } + } + + return true +} + +// String implements stringer interface +func (mps MediaPosts) String() string { + bytes, err := json.Marshal(&mps) + if err != nil { + panic(err) + } + + return string(bytes) +} + +// --------------- +// --- PostMedias +// --------------- + +type PostMedias []PostMedia + +func (pms PostMedias) String() string { + bytes, err := json.Marshal(&pms) + if err != nil { + panic(err) + } + + return string(bytes) +} +func (pms PostMedias) Equals(other PostMedias) bool { + if len(pms) != len(other) { + return false + } + + for index, postMedia := range pms { + if !postMedia.Equals(other[index]) { + return false + } + } + + return true +} + +func (pms PostMedias) AppendIfMissing(otherMedia PostMedia) (PostMedias, bool) { + for _, media := range pms { + if media.Equals(otherMedia) { + return pms, false + } + } + return append(pms, otherMedia), true +} + +// --------------- +// --- PostMedia +// --------------- + +type PostMedia struct { + URI string `json:"uri"` + Provider string `json:"provider"` + MimeType string `json:"mime_Type"` +} + +func NewPostMedia(uri, provider, mimeType string) PostMedia { + return PostMedia{ + URI: uri, + Provider: provider, + MimeType: mimeType, + } +} + +// String implements fmt.Stringer +func (pm PostMedia) String() string { + bytes, err := json.Marshal(&pm) + if err != nil { + panic(err) + } + + return string(bytes) +} + +func (pm PostMedia) Validate() error { + if len(strings.TrimSpace(pm.Provider)) == 0 { + return fmt.Errorf("media provider must be specified and cannot be empty") + } + + if len(strings.TrimSpace(pm.URI)) == 0 { + return fmt.Errorf("uri must be specified and cannot be empty") + } + + if err := ParseURI(pm.URI); err != nil { + return err + } + + if len(strings.TrimSpace(pm.MimeType)) == 0 { + return fmt.Errorf("mime type must be specified and cannot be empty") + } + + return nil +} + +func (pm PostMedia) Equals(other PostMedia) bool { + return pm.URI == other.URI && pm.MimeType == other.MimeType && pm.Provider == other.Provider +} + +func ParseURI(uri string) error { + rEx := regexp.MustCompile( + `^(?:https:\/\/)[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:\/?#[\]@!\$&'\(\)\*\+,;=.]+$`) + + if !rEx.MatchString(uri) { + return fmt.Errorf("invalid uri provided") + } + + return nil +} diff --git a/x/posts/internal/types/media_post_test.go b/x/posts/internal/types/media_post_test.go new file mode 100644 index 0000000000..908e263e2e --- /dev/null +++ b/x/posts/internal/types/media_post_test.go @@ -0,0 +1,1067 @@ +package types_test + +import ( + "fmt" + "testing" + "time" + + "github.com/desmos-labs/desmos/x/posts/internal/types" + "github.com/stretchr/testify/assert" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// ----------- +// --- MediaPost +// ----------- + +func TestMediaPost_String(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + testPostCreationDate := time.Date(2020, 1, 1, 15, 15, 00, 000, timeZone) + + var mp = types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }, + }, + ) + assert.Equal(t, + `{"id":"2","parent_id":"0","message":"media Post","created":"2020-01-01T15:15:00Z","last_edited":"0001-01-01T00:00:00Z","allows_comments":false,"subspace":"desmos","creator":"cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns","medias":[{"uri":"uri","provider":"provider","mime_Type":"text/plain"}]}`, + mp.String(), + ) +} + +func TestMediaPost_GetID(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + testPostCreationDate := time.Date(2020, 1, 1, 15, 15, 00, 000, timeZone) + var mp = types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }, + }, + ) + + actual := mp.GetID() + + assert.Equal(t, types.PostID(2), actual) +} + +func TestMediaPost_GetParentID(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + testPostCreationDate := time.Date(2020, 1, 1, 15, 15, 00, 000, timeZone) + var mp = types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }, + }, + ) + + actual := mp.GetParentID() + + assert.Equal(t, types.PostID(0), actual) +} + +func TestMediaPost_SetMessage(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + testPostCreationDate := time.Date(2020, 1, 1, 15, 15, 00, 000, timeZone) + var mp = types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }, + }, + ) + + actual := mp.SetMessage("edited media post") + + assert.Equal(t, "edited media post", actual.GetMessage()) +} + +func TestMediaPost_GetMessage(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + testPostCreationDate := time.Date(2020, 1, 1, 15, 15, 00, 000, timeZone) + var mp = types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }, + }, + ) + + actual := mp.GetMessage() + + assert.Equal(t, "media Post", actual) + +} + +func TestMediaPost_CreationTime(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + testPostCreationDate := time.Date(2020, 1, 1, 15, 15, 00, 000, timeZone) + var mp = types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }, + }, + ) + + actual := mp.CreationTime() + + assert.Equal(t, testPostCreationDate, actual) +} + +func TestMediaPost_SetEditTime(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + testPostCreationDate := time.Date(2020, 1, 1, 15, 15, 00, 000, timeZone) + var mp = types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }, + }, + ) + + actual := mp.SetEditTime(time.Date(2020, 1, 2, 15, 15, 00, 000, timeZone)) + + assert.Equal(t, time.Date(2020, 1, 2, 15, 15, 00, 000, timeZone), actual.GetEditTime()) +} + +func TestMediaPost_GetEditTime(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + testPostCreationDate := time.Date(2020, 1, 1, 15, 15, 00, 000, timeZone) + var mp = types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }, + }, + ) + + actual := mp.GetEditTime() + + assert.Equal(t, time.Time{}, actual) +} + +func TestMediaPost_CanComment(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + testPostCreationDate := time.Date(2020, 1, 1, 15, 15, 00, 000, timeZone) + var mp = types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }, + }, + ) + + actual := mp.CanComment() + + assert.Equal(t, false, actual) +} + +func TestMediaPost_GetSubspace(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + testPostCreationDate := time.Date(2020, 1, 1, 15, 15, 00, 000, timeZone) + var mp = types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }, + }, + ) + + actual := mp.GetSubspace() + + assert.Equal(t, "desmos", actual) +} + +func TestMediaPost_GetOptionalData(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + testPostCreationDate := time.Date(2020, 1, 1, 15, 15, 00, 000, timeZone) + var mp = types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{"key1": "value1"}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }, + }, + ) + + actual := mp.GetOptionalData() + + assert.Equal(t, map[string]string{"key1": "value1"}, actual) +} + +func TestMediaPost_Owner(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + testPostCreationDate := time.Date(2020, 1, 1, 15, 15, 00, 000, timeZone) + var mp = types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{"key1": "value1"}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }, + }, + ) + + actual := mp.Owner() + + assert.Equal(t, owner, actual) +} + +func TestMediaPost_Validate(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + testPostCreationDate := time.Date(2020, 1, 1, 15, 15, 00, 000, timeZone) + tests := []struct { + post types.MediaPost + expErr string + }{ + { + post: types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "", + URI: "https://example.com", + MimeType: "text/plain", + }, + }, + ), + expErr: "media provider must be specified and cannot be empty", + }, + { + post: types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "", + MimeType: "text/plain", + }, + }, + ), + expErr: "uri must be specified and cannot be empty", + }, + { + post: types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "htt://example.com", + MimeType: "text/plain", + }, + }, + ), + expErr: "invalid uri provided", + }, + { + post: types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "https://example.com", + MimeType: "", + }, + }, + ), + expErr: "mime type must be specified and cannot be empty", + }, + } + + for _, test := range tests { + test := test + t.Run(test.expErr, func(t *testing.T) { + if len(test.expErr) != 0 { + assert.Equal(t, test.expErr, test.post.Validate().Error()) + } else { + assert.Nil(t, test.post.Validate()) + } + }) + } +} + +func TestMediaPost_Equals(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + otherOwner, _ := sdk.AccAddressFromBech32("cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47") + testPostCreationDate := time.Date(2020, 1, 1, 15, 15, 00, 000, timeZone) + + tests := []struct { + name string + first types.MediaPost + second types.MediaPost + expEquals bool + }{ + { + name: "Same data returns true", + first: types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "https://example.com", + MimeType: "text/plain", + }, + }, + ), + second: types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "https://example.com", + MimeType: "text/plain", + }, + }, + ), + expEquals: true, + }, + { + name: "Different owner returns false", + first: types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "https://example.com", + MimeType: "text/plain", + }, + }, + ), + second: types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, otherOwner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "https://example.com", + MimeType: "text/plain", + }, + }, + ), + expEquals: false, + }, + { + name: "Different provider returns false", + first: types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "https://example.com", + MimeType: "text/plain", + }, + }, + ), + second: types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider2", + URI: "https://example.com", + MimeType: "text/plain", + }, + }, + ), + expEquals: false, + }, + { + name: "Different URI returns false", + first: types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "https://example.com", + MimeType: "text/plain", + }, + }, + ), + second: types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, otherOwner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "https://another.com", + MimeType: "text/plain", + }, + }, + ), + expEquals: false, + }, + { + name: "Different mime type returns false", + first: types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "https://example.com", + MimeType: "text/plain", + }, + }, + ), + second: types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, otherOwner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "https://example.com", + MimeType: "application/json", + }, + }, + ), + expEquals: false, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expEquals, test.first.Equals(test.second)) + }) + } +} + +func TestMediaPost_MarshalJSON(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + testPostCreationDate := time.Date(2020, 1, 1, 15, 15, 00, 000, timeZone) + var mp = types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }, + }, + ) + json := types.ModuleCdc.MustMarshalJSON(mp) + assert.Equal(t, `{"type":"desmos/MediaPost","value":{"id":"2","parent_id":"0","message":"media Post","created":"2020-01-01T15:15:00Z","last_edited":"0001-01-01T00:00:00Z","allows_comments":false,"subspace":"desmos","creator":"cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns","medias":[{"uri":"uri","provider":"provider","mime_Type":"text/plain"}]}}`, string(json)) +} + +func TestMediaPost_UnmarshalJSON(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + testPostCreationDate := time.Date(2020, 1, 1, 15, 15, 00, 000, timeZone) + var mp = types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", nil, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }, + }, + ) + tests := []struct { + name string + value string + expMediaPost types.MediaPost + expError string + }{ + { + name: "Valid media post is ready properly", + value: `{"type":"desmos/MediaPost","value":{"id":"2","parent_id":"0","message":"media Post","created":"2020-01-01T15:15:00Z","last_edited":"0001-01-01T00:00:00Z","allows_comments":false,"subspace":"desmos","creator":"cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns","medias":[{"uri":"uri","provider":"provider","mime_Type":"text/plain"}]}}`, + expMediaPost: mp, + expError: "", + }, + { + name: "Invalid media post returns error", + value: `{"post":{"id":"2","parent_id":"0","message":"media Post","created":"0","last_edited":"0","allows_comments":false,"subspace":"desmos","creator":"cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns"},"medias":[{"provider":"ipfs","uri":"uri","mime_Type":"text/plain"}]}`, + expMediaPost: mp, + expError: "JSON encoding of interfaces require non-empty type field.", + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + var mediaPost types.MediaPost + err := types.ModuleCdc.UnmarshalJSON([]byte(test.value), &mediaPost) + + if err == nil { + assert.Equal(t, test.expMediaPost, mediaPost) + } else { + assert.Equal(t, test.expError, err.Error()) + } + }) + } +} + +// ----------- +// --- MediaPosts +// ----------- + +func TestMediaPosts_Equals(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + otherOwner, _ := sdk.AccAddressFromBech32("cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47") + testPostCreationDate := time.Date(2020, 1, 1, 15, 15, 00, 000, timeZone) + + tests := []struct { + name string + first types.MediaPosts + second types.MediaPosts + expEquals bool + }{ + { + name: "Same data returns true", + first: types.MediaPosts{types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "https://example.com", + MimeType: "text/plain", + }, + }, + )}, + second: types.MediaPosts{types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "https://example.com", + MimeType: "text/plain", + }, + }, + )}, + expEquals: true, + }, + { + name: "Different owner returns false", + first: types.MediaPosts{types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "https://example.com", + MimeType: "text/plain", + }, + }, + )}, + second: types.MediaPosts{types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, otherOwner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "https://example.com", + MimeType: "text/plain", + }, + }, + )}, + expEquals: false, + }, + { + name: "Different provider returns false", + first: types.MediaPosts{types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "https://example.com", + MimeType: "text/plain", + }, + }, + )}, + second: types.MediaPosts{types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider2", + URI: "https://example.com", + MimeType: "text/plain", + }, + }, + )}, + expEquals: false, + }, + { + name: "Different URI returns false", + first: types.MediaPosts{types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "https://example.com", + MimeType: "text/plain", + }, + }, + )}, + second: types.MediaPosts{types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "https://different.com", + MimeType: "text/plain", + }, + }, + )}, + expEquals: false, + }, + { + name: "Different mime type returns false", + first: types.MediaPosts{types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "https://example.com", + MimeType: "text/plain", + }, + }, + )}, + second: types.MediaPosts{types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "provider", + URI: "https://example.com", + MimeType: "application/json", + }, + }, + )}, + expEquals: false, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expEquals, test.first.Equals(test.second)) + }) + } +} + +func TestMediaPosts_String(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + testPostCreationDate := time.Date(2020, 1, 1, 15, 15, 00, 000, timeZone) + var mps = types.MediaPosts{types.NewMediaPost( + types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner), + types.PostMedias{ + types.PostMedia{ + Provider: "ipfs", + URI: "uri", + MimeType: "text/plain", + }, + }, + )} + + assert.Equal(t, + `[{"id":"2","parent_id":"0","message":"media Post","created":"2020-01-01T15:15:00Z","last_edited":"0001-01-01T00:00:00Z","allows_comments":false,"subspace":"desmos","creator":"cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns","medias":[{"uri":"uri","provider":"ipfs","mime_Type":"text/plain"}]}]`, + mps.String(), + ) +} + +// ----------- +// --- PostMedias +// ----------- + +func TestPostMedias_String(t *testing.T) { + postMedias := types.PostMedias{ + types.PostMedia{ + Provider: "ipfs", + URI: "uri", + MimeType: "text/plain", + }, + types.PostMedia{ + Provider: "dropbox", + URI: "uri", + MimeType: "application/json", + }, + } + + actual := postMedias.String() + + assert.Equal(t, `[{"uri":"uri","provider":"ipfs","mime_Type":"text/plain"},{"uri":"uri","provider":"dropbox","mime_Type":"application/json"}]`, actual) +} + +func TestPostMedias_Equals(t *testing.T) { + tests := []struct { + name string + first types.PostMedias + second types.PostMedias + expEquals bool + }{ + { + name: "Same data returns true", + first: types.PostMedias{ + types.PostMedia{ + Provider: "ipfs", + URI: "uri", + MimeType: "text/plain", + }, + types.PostMedia{ + Provider: "dropbox", + URI: "uri", + MimeType: "application/json", + }, + }, + second: types.PostMedias{ + types.PostMedia{ + Provider: "ipfs", + URI: "uri", + MimeType: "text/plain", + }, + types.PostMedia{ + Provider: "dropbox", + URI: "uri", + MimeType: "application/json", + }, + }, + expEquals: true, + }, + { + name: "different data returns false", + first: types.PostMedias{ + types.PostMedia{ + Provider: "ipfs", + URI: "uri", + MimeType: "text/plain", + }, + types.PostMedia{ + Provider: "dropbox", + URI: "uri", + MimeType: "application/json", + }, + }, + second: types.PostMedias{ + types.PostMedia{ + Provider: "dropbox", + URI: "uri", + MimeType: "text/plain", + }, + types.PostMedia{ + Provider: "ipfs", + URI: "uri", + MimeType: "application/json", + }, + }, + expEquals: false, + }, + { + name: "different length returns false", + first: types.PostMedias{ + types.PostMedia{ + Provider: "ipfs", + URI: "uri", + MimeType: "text/plain", + }, + types.PostMedia{ + Provider: "dropbox", + URI: "uri", + MimeType: "application/json", + }, + }, + second: types.PostMedias{ + types.PostMedia{ + Provider: "dropbox", + URI: "uri", + MimeType: "text/plain", + }, + }, + expEquals: false, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expEquals, test.first.Equals(test.second)) + }) + } +} + +func TestPostMedias_AppendIfMissing(t *testing.T) { + tests := []struct { + name string + medias types.PostMedias + newMedia types.PostMedia + expMedias types.PostMedias + expAppended bool + }{ + { + name: "append a new media and returns true", + medias: types.PostMedias{ + types.PostMedia{ + Provider: "ipfs", + URI: "uri", + MimeType: "text/plain", + }, + }, + newMedia: types.PostMedia{ + Provider: "dropbox", + URI: "uri", + MimeType: "application/json", + }, + expMedias: types.PostMedias{ + types.PostMedia{ + Provider: "ipfs", + URI: "uri", + MimeType: "text/plain", + }, + types.PostMedia{ + Provider: "dropbox", + URI: "uri", + MimeType: "application/json", + }, + }, + expAppended: true, + }, + { + name: "not append an existing media and returns false", + medias: types.PostMedias{ + types.PostMedia{ + Provider: "ipfs", + URI: "uri", + MimeType: "text/plain", + }, + }, + newMedia: types.PostMedia{ + Provider: "ipfs", + URI: "uri", + MimeType: "text/plain", + }, + expMedias: types.PostMedias{ + types.PostMedia{ + Provider: "ipfs", + URI: "uri", + MimeType: "text/plain", + }, + }, + expAppended: false, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + medias, found := test.medias.AppendIfMissing(test.newMedia) + assert.Equal(t, test.expMedias, medias) + assert.Equal(t, test.expAppended, found) + }) + } +} + +// ----------- +// --- PostMedia +// ----------- + +func TestPostMedia_String(t *testing.T) { + pm := types.PostMedia{ + Provider: "provider", + URI: "http://example.com", + MimeType: "text/plain", + } + + actual := pm.String() + + assert.Equal(t, `{"uri":"http://example.com","provider":"provider","mime_Type":"text/plain"}`, actual) +} + +func TestPostMedia_Validate(t *testing.T) { + tests := []struct { + postMedia types.PostMedia + expErr string + }{ + { + postMedia: types.PostMedia{ + Provider: "", + URI: "https://example.com", + MimeType: "text/plain", + }, + expErr: "media provider must be specified and cannot be empty", + }, + { + postMedia: types.PostMedia{ + Provider: "provider", + URI: "", + MimeType: "text/plain", + }, + expErr: "uri must be specified and cannot be empty", + }, + { + postMedia: types.PostMedia{ + Provider: "provider", + URI: "htt://example.com", + MimeType: "text/plain", + }, + expErr: "invalid uri provided", + }, + { + postMedia: types.PostMedia{ + Provider: "provider", + URI: "https://example.com", + MimeType: "", + }, + expErr: "mime type must be specified and cannot be empty", + }, + } + + for _, test := range tests { + test := test + t.Run(test.expErr, func(t *testing.T) { + if len(test.expErr) != 0 { + assert.Equal(t, test.expErr, test.postMedia.Validate().Error()) + } else { + assert.Nil(t, test.postMedia.Validate()) + } + }) + } +} + +func TestPostMedia_Equals(t *testing.T) { + tests := []struct { + name string + first types.PostMedia + second types.PostMedia + expEquals bool + }{ + { + name: "Same data returns true", + first: types.PostMedia{ + Provider: "provider", + URI: "https://example.com", + MimeType: "text/plain", + }, + second: types.PostMedia{ + Provider: "provider", + URI: "https://example.com", + MimeType: "text/plain", + }, + expEquals: true, + }, + { + name: "Different provider returns false", + first: types.PostMedia{ + Provider: "provider", + URI: "https://example.com", + MimeType: "text/plain", + }, + second: types.PostMedia{ + Provider: "provider2", + URI: "https://example.com", + MimeType: "text/plain", + }, + expEquals: false, + }, + { + name: "Different URI returns false", + first: types.PostMedia{ + Provider: "provider", + URI: "https://example.com", + MimeType: "text/plain", + }, + second: types.PostMedia{ + Provider: "provider", + URI: "https://another.com", + MimeType: "text/plain", + }, + expEquals: false, + }, + { + name: "Different mime type returns false", + first: types.PostMedia{ + Provider: "provider", + URI: "https://example.com", + MimeType: "text/plain", + }, + second: types.PostMedia{ + Provider: "provider", + URI: "https://example.com", + MimeType: "application/json", + }, + expEquals: false, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expEquals, test.first.Equals(test.second)) + }) + } +} + +func TestPostMedia_ParseURI(t *testing.T) { + tests := []struct { + uri string + expErr error + }{ + { + uri: "http://error.com", + expErr: fmt.Errorf("invalid uri provided"), + }, + { + uri: "http://", + expErr: fmt.Errorf("invalid uri provided"), + }, + { + uri: "error.com", + expErr: fmt.Errorf("invalid uri provided"), + }, + { + uri: ".com", + expErr: fmt.Errorf("invalid uri provided"), + }, + { + uri: "ttps://", + expErr: fmt.Errorf("invalid uri provided"), + }, + { + uri: "ps://site.com", + expErr: fmt.Errorf("invalid uri provided"), + }, + { + uri: "https://", + expErr: fmt.Errorf("invalid uri provided"), + }, + { + uri: "https://example.com", + expErr: nil, + }, + } + + for _, test := range tests { + test := test + t.Run(test.uri, func(t *testing.T) { + assert.Equal(t, test.expErr, types.ParseURI(test.uri)) + }) + } +} diff --git a/x/posts/internal/types/msgs.go b/x/posts/internal/types/msgs.go index f0641c17f9..4a276d1696 100644 --- a/x/posts/internal/types/msgs.go +++ b/x/posts/internal/types/msgs.go @@ -9,12 +9,34 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) +type MsgCreatePost interface { + // Return the message type. + // Must be alphanumeric or empty. + Route() string + + // Returns a human-readable string for the message, intended for utilization + // within tags + Type() string + + // ValidateBasic does a simple validation check that + // doesn't require access to any other information. + ValidateBasic() sdk.Error + + // Get the canonical byte representation of the Msg. + GetSignBytes() []byte + + // Signers returns the addrs of signers that must sign. + // CONTRACT: All signatures must be present to be valid. + // CONTRACT: Returns addrs in some deterministic order. + GetSigners() []sdk.AccAddress +} + // ---------------------- -// --- MsgCreatePost +// --- MsgCreateTextPost // ---------------------- -// MsgCreatePost defines a CreatePost message -type MsgCreatePost struct { +// MsgCreateTextPost defines a CreatePost message +type MsgCreateTextPost struct { ParentID PostID `json:"parent_id"` Message string `json:"message"` AllowsComments bool `json:"allows_comments"` @@ -25,9 +47,9 @@ type MsgCreatePost struct { } // NewMsgCreatePost is a constructor function for MsgSetName -func NewMsgCreatePost(message string, parentID PostID, allowsComments bool, subspace string, - optionalData map[string]string, owner sdk.AccAddress, creationDate time.Time) MsgCreatePost { - return MsgCreatePost{ +func NewMsgCreateTextPost(message string, parentID PostID, allowsComments bool, subspace string, + optionalData map[string]string, owner sdk.AccAddress, creationDate time.Time) MsgCreateTextPost { + return MsgCreateTextPost{ Message: message, ParentID: parentID, AllowsComments: allowsComments, @@ -40,19 +62,19 @@ func NewMsgCreatePost(message string, parentID PostID, allowsComments bool, subs // MarshalJSON implements the custom marshaling as Amino does not support // the JSON signature omitempty -func (msg MsgCreatePost) MarshalJSON() ([]byte, error) { - type msgCreatePost MsgCreatePost +func (msg MsgCreateTextPost) MarshalJSON() ([]byte, error) { + type msgCreatePost MsgCreateTextPost return json.Marshal(msgCreatePost(msg)) } // Route should return the name of the module -func (msg MsgCreatePost) Route() string { return RouterKey } +func (msg MsgCreateTextPost) Route() string { return RouterKey } // Type should return the action -func (msg MsgCreatePost) Type() string { return ActionCreatePost } +func (msg MsgCreateTextPost) Type() string { return ActionCreatePost } // ValidateBasic runs stateless checks on the message -func (msg MsgCreatePost) ValidateBasic() sdk.Error { +func (msg MsgCreateTextPost) ValidateBasic() sdk.Error { if msg.Creator.Empty() { return sdk.ErrInvalidAddress(fmt.Sprintf("Invalid creator address: %s", msg.Creator)) } @@ -94,15 +116,64 @@ func (msg MsgCreatePost) ValidateBasic() sdk.Error { } // GetSignBytes encodes the message for signing -func (msg MsgCreatePost) GetSignBytes() []byte { +func (msg MsgCreateTextPost) GetSignBytes() []byte { return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) } // GetSigners defines whose signature is required -func (msg MsgCreatePost) GetSigners() []sdk.AccAddress { +func (msg MsgCreateTextPost) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.Creator} } +// ---------------------- +// --- MsgCreateMediaPost +// ---------------------- + +// MsgCreateMediaPost defines a CreateMediaPost message +type MsgCreateMediaPost struct { + MsgCreatePost MsgCreateTextPost `json:"msg_create_post"` + Medias PostMedias `json:"post_medias"` +} + +// NewMsgCreateMediaPost is a constructor function for MsgCreateMediaPost +func NewMsgCreateMediaPost(msg MsgCreateTextPost, medias PostMedias) MsgCreateMediaPost { + return MsgCreateMediaPost{ + MsgCreatePost: msg, + Medias: medias, + } +} + +// Route should return the name of the module +func (msg MsgCreateMediaPost) Route() string { return RouterKey } + +// Type should return the action +func (msg MsgCreateMediaPost) Type() string { return ActionCreateMediaPost } + +// ValidateBasic runs stateless checks on the message +func (msg MsgCreateMediaPost) ValidateBasic() sdk.Error { + if err := msg.MsgCreatePost.ValidateBasic(); err != nil { + return err + } + + for _, media := range msg.Medias { + if err := media.Validate(); err != nil { + return sdk.ErrUnknownRequest(err.Error()) + } + } + + return nil +} + +// GetSignBytes encodes the message for signing +func (msg MsgCreateMediaPost) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +// GetSigners defines whose signature is required +func (msg MsgCreateMediaPost) GetSigners() []sdk.AccAddress { + return msg.MsgCreatePost.GetSigners() +} + // ---------------------- // --- MsgEditPost // ---------------------- diff --git a/x/posts/internal/types/msgs_test.go b/x/posts/internal/types/msgs_test.go index 79f1ec6a13..8928ec2fe5 100644 --- a/x/posts/internal/types/msgs_test.go +++ b/x/posts/internal/types/msgs_test.go @@ -10,13 +10,13 @@ import ( ) // ---------------------- -// --- MsgCreatePost +// --- MsgCreateTextPost // ---------------------- var testOwner, _ = sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") var timeZone, _ = time.LoadLocation("UTC") var date = time.Date(2020, 1, 1, 12, 0, 0, 0, timeZone) -var msgCreatePost = types.NewMsgCreatePost( +var msgCreatePost = types.NewMsgCreateTextPost( "My new post", types.PostID(53), false, @@ -40,22 +40,22 @@ func TestMsgCreatePost_ValidateBasic(t *testing.T) { creator, _ := sdk.AccAddressFromBech32("cosmos16vphdl9nhm26murvfrrp8gdsknvfrxctl6y29h") tests := []struct { name string - msg types.MsgCreatePost + msg types.MsgCreateTextPost error sdk.Error }{ { name: "Empty owner returns error", - msg: types.NewMsgCreatePost("Message", types.PostID(0), false, "desmos", map[string]string{}, nil, date), + msg: types.NewMsgCreateTextPost("Message", types.PostID(0), false, "desmos", map[string]string{}, nil, date), error: sdk.ErrInvalidAddress("Invalid creator address: "), }, { name: "Empty message returns error", - msg: types.NewMsgCreatePost("", types.PostID(0), false, "desmos", map[string]string{}, creator, date), + msg: types.NewMsgCreateTextPost("", types.PostID(0), false, "desmos", map[string]string{}, creator, date), error: sdk.ErrUnknownRequest("Post message cannot be empty nor blank"), }, { name: "Very long message returns error", - msg: types.NewMsgCreatePost( + msg: types.NewMsgCreateTextPost( ` Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque massa felis, aliquam sed ipsum at, mollis pharetra quam. Vestibulum nec nulla ante. Praesent sed dignissim turpis. Curabitur aliquam nunc @@ -74,12 +74,12 @@ func TestMsgCreatePost_ValidateBasic(t *testing.T) { }, { name: "Empty subspace returns error", - msg: types.NewMsgCreatePost("My message", types.PostID(0), false, "", map[string]string{}, creator, date), + msg: types.NewMsgCreateTextPost("My message", types.PostID(0), false, "", map[string]string{}, creator, date), error: sdk.ErrUnknownRequest("Post subspace cannot be empty nor blank"), }, { name: "More than 10 optional data returns error", - msg: types.NewMsgCreatePost( + msg: types.NewMsgCreateTextPost( "My message", types.PostID(0), false, @@ -104,7 +104,7 @@ func TestMsgCreatePost_ValidateBasic(t *testing.T) { }, { name: "Optional data longer than 200 characters returns error", - msg: types.NewMsgCreatePost( + msg: types.NewMsgCreateTextPost( "My message", types.PostID(0), false, @@ -119,12 +119,12 @@ func TestMsgCreatePost_ValidateBasic(t *testing.T) { }, { name: "Future creation date returns error", - msg: types.NewMsgCreatePost("future post", types.PostID(0), false, "desmos", map[string]string{}, creator, time.Now().UTC().Add(time.Hour)), + msg: types.NewMsgCreateTextPost("future post", types.PostID(0), false, "desmos", map[string]string{}, creator, time.Now().UTC().Add(time.Hour)), error: sdk.ErrUnknownRequest("Creation date cannot be in the future"), }, { name: "Valid message does not return any error", - msg: types.NewMsgCreatePost( + msg: types.NewMsgCreateTextPost( "Message", types.PostID(0), false, @@ -159,18 +159,18 @@ func TestMsgCreatePost_ValidateBasic(t *testing.T) { func TestMsgCreatePost_GetSignBytes(t *testing.T) { tests := []struct { name string - msg types.MsgCreatePost + msg types.MsgCreateTextPost expSignJSON string }{ { name: "Message with non-empty external reference", - msg: types.NewMsgCreatePost("My new post", types.PostID(53), false, "desmos", map[string]string{"field": "value"}, testOwner, date), - expSignJSON: `{"type":"desmos/MsgCreatePost","value":{"allows_comments":false,"creation_date":"2020-01-01T12:00:00Z","creator":"cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns","message":"My new post","optional_data":{"field":"value"},"parent_id":"53","subspace":"desmos"}}`, + msg: types.NewMsgCreateTextPost("My new post", types.PostID(53), false, "desmos", map[string]string{"field": "value"}, testOwner, date), + expSignJSON: `{"type":"desmos/MsgCreateTextPost","value":{"allows_comments":false,"creation_date":"2020-01-01T12:00:00Z","creator":"cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns","message":"My new post","optional_data":{"field":"value"},"parent_id":"53","subspace":"desmos"}}`, }, { name: "Message with non-empty external reference", - msg: types.NewMsgCreatePost("My post", types.PostID(15), false, "desmos", map[string]string{}, testOwner, date), - expSignJSON: `{"type":"desmos/MsgCreatePost","value":{"allows_comments":false,"creation_date":"2020-01-01T12:00:00Z","creator":"cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns","message":"My post","parent_id":"15","subspace":"desmos"}}`, + msg: types.NewMsgCreateTextPost("My post", types.PostID(15), false, "desmos", map[string]string{}, testOwner, date), + expSignJSON: `{"type":"desmos/MsgCreateTextPost","value":{"allows_comments":false,"creation_date":"2020-01-01T12:00:00Z","creator":"cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns","message":"My post","parent_id":"15","subspace":"desmos"}}`, }, } @@ -188,6 +188,178 @@ func TestMsgCreatePost_GetSigners(t *testing.T) { assert.Equal(t, msgCreatePost.Creator, actual[0]) } +// ---------------------- +// --- MsgCreateMediaPost +// ---------------------- +var testPostCreationDate = time.Date(2020, 1, 1, 15, 15, 00, 000, timeZone) +var msgCreateTextPost = types.NewMsgCreateTextPost("My new post", types.PostID(53), false, "desmos", map[string]string{}, testOwner, testPostCreationDate) +var msgCreateMediaPost = types.NewMsgCreateMediaPost(msgCreateTextPost, + types.PostMedias{types.PostMedia{Provider: "provider", URI: "https://uri.com", MimeType: "text/plain"}}, +) + +func TestMsgCreateMediaPost_Route(t *testing.T) { + actual := msgCreateMediaPost.Route() + assert.Equal(t, "posts", actual) +} + +func TestMsgCreateMediaPost_Type(t *testing.T) { + actual := msgCreateMediaPost.Type() + assert.Equal(t, "create_media_post", actual) +} + +func TestMsgCreateMediaPost_ValidateBasic(t *testing.T) { + creator, _ := sdk.AccAddressFromBech32("cosmos16vphdl9nhm26murvfrrp8gdsknvfrxctl6y29h") + testPostCreationDate := time.Date(2020, 1, 1, 15, 15, 00, 000, timeZone) + tests := []struct { + name string + msg types.MsgCreateMediaPost + error sdk.Error + }{ + { + name: "Empty owner returns error", + msg: types.NewMsgCreateMediaPost(types.NewMsgCreateTextPost("My new post", types.PostID(53), false, "desmos", map[string]string{}, nil, testPostCreationDate), + types.PostMedias{types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }}), + error: sdk.ErrInvalidAddress("Invalid creator address: "), + }, + { + name: "Empty message returns error", + msg: types.NewMsgCreateMediaPost(types.NewMsgCreateTextPost("", types.PostID(0), false, "desmos", map[string]string{}, creator, testPostCreationDate), + types.PostMedias{types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }}), + error: sdk.ErrUnknownRequest("Post message cannot be empty nor blank"), + }, + { + name: "Empty subspace returns error", + msg: types.NewMsgCreateMediaPost(types.NewMsgCreateTextPost("My message", types.PostID(0), false, "", map[string]string{}, creator, testPostCreationDate), types.PostMedias{types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }}), + error: sdk.ErrUnknownRequest("Post subspace cannot be empty nor blank"), + }, + { + name: "More than 10 optional data returns error", + msg: types.NewMsgCreateMediaPost(types.NewMsgCreateTextPost( + "My message", types.PostID(0), + false, + "desmos", map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + "key4": "value4", + "key5": "value5", + "key6": "value6", + "key7": "value7", + "key8": "value8", + "key9": "value9", + "key10": "value10", + "key11": "value11", + }, + creator, + testPostCreationDate), + types.PostMedias{types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }}), + error: sdk.ErrUnknownRequest("Post optional data cannot be longer than 10 fields"), + }, + { + name: "Optional data longer than 200 characters returns error", + msg: types.NewMsgCreateMediaPost( + types.NewMsgCreateTextPost( + "My message", + types.PostID(0), + false, + "desmos", map[string]string{ + "key1": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi ac ullamcorper dui, a mattis sapien. Vivamus sed massa eget felis hendrerit ultrices. Morbi pretium hendrerit nisi quis faucibus volutpat.", + }, + creator, + testPostCreationDate), + types.PostMedias{types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + }}), + error: sdk.ErrUnknownRequest("Post optional data value lengths cannot be longer than 200. key1 exceeds the limit"), + }, + { + name: "Valid message does not return any error", + msg: msgCreateMediaPost, + error: nil, + }, + { + name: "Empty provider in message returns error", + msg: types.NewMsgCreateMediaPost(types.NewMsgCreateTextPost("My message", types.PostID(0), false, "desmos", map[string]string{}, creator, testPostCreationDate), + types.PostMedias{types.PostMedia{ + Provider: "", + URI: "https://example.com", + MimeType: "text/plain", + }}), + error: sdk.ErrUnknownRequest("media provider must be specified and cannot be empty"), + }, + { + name: "Empty uri in message returns error", + msg: types.NewMsgCreateMediaPost(types.NewMsgCreateTextPost("My message", types.PostID(0), false, "desmos", map[string]string{}, creator, testPostCreationDate), + types.PostMedias{types.PostMedia{ + Provider: "provider", + URI: "", + MimeType: "text/plain", + }}), + error: sdk.ErrUnknownRequest("uri must be specified and cannot be empty"), + }, + { + name: "Invalid URI in message returns error", + msg: types.NewMsgCreateMediaPost(types.NewMsgCreateTextPost("My message", types.PostID(0), false, "desmos", map[string]string{}, creator, testPostCreationDate), + types.PostMedias{types.PostMedia{ + Provider: "provider", + URI: "invalid-uri", + MimeType: "text/plain", + }}), + error: sdk.ErrUnknownRequest("invalid uri provided"), + }, + { + name: "Empty mime type in message returns error", + msg: types.NewMsgCreateMediaPost(types.NewMsgCreateTextPost("My message", types.PostID(0), false, "desmos", map[string]string{}, creator, testPostCreationDate), + types.PostMedias{types.PostMedia{ + Provider: "provider", + URI: "https://example.com", + MimeType: "", + }}), + error: sdk.ErrUnknownRequest("mime type must be specified and cannot be empty"), + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.error, test.msg.ValidateBasic()) + }) + } + + err := msgCreatePost.ValidateBasic() + assert.Nil(t, err) +} + +func TestMsgCreateMediaPost_GetSignBytes(t *testing.T) { + actual := msgCreateMediaPost.GetSignBytes() + expected := `{"type":"desmos/MsgCreateMediaPost","value":{"msg_create_post":{"allows_comments":false,"creation_date":"2020-01-01T15:15:00Z","creator":"cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns","message":"My new post","parent_id":"53","subspace":"desmos"},"post_medias":[{"mime_Type":"text/plain","provider":"provider","uri":"https://uri.com"}]}}` + assert.Equal(t, expected, string(actual)) +} + +func TestMsgCreateMediaPost_GetSigners(t *testing.T) { + actual := msgCreateMediaPost.GetSigners() + assert.Equal(t, 1, len(actual)) + assert.Equal(t, msgCreateMediaPost.MsgCreatePost.Creator, actual[0]) +} + // ---------------------- // --- MsgEditPost // ---------------------- diff --git a/x/posts/internal/types/post.go b/x/posts/internal/types/post.go index f9d3f12774..f679d144f2 100644 --- a/x/posts/internal/types/post.go +++ b/x/posts/internal/types/post.go @@ -2,268 +2,38 @@ package types import ( "encoding/json" - "fmt" - "strconv" - "strings" - "time" sdk "github.com/cosmos/cosmos-sdk/types" -) - -// --------------- -// --- Post id -// --------------- - -// 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 -} - -// MarshalJSON implements Marshaler -func (id PostID) MarshalJSON() ([]byte, error) { - return json.Marshal(id.String()) -} - -// UnmarshalJSON implements Unmarshaler -func (id *PostID) UnmarshalJSON(data []byte) error { - var s string - if err := json.Unmarshal(data, &s); err != nil { - return err - } - - postID, err := ParsePostID(s) - if err != nil { - return err - } - - *id = postID - return nil -} -// 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 { - return PostID(0), err - } - - 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 -} - -// AppendIfMissing appends the given postID to the ids slice if it does not exist inside it yet. -// It returns a new slice of PostIDs containing such ID and a boolean indicating whether or not the original -// slice has been modified. -func (ids PostIDs) AppendIfMissing(id PostID) (PostIDs, bool) { - for _, ele := range ids { - if ele.Equals(id) { - return ids, false - } - } - return append(ids, id), true -} - -// --------------- -// --- Post -// --------------- - -// Post is a struct of a post -type Post struct { - PostID PostID `json:"id"` // Unique id - ParentID PostID `json:"parent_id"` // Post of which this one is a comment - Message string `json:"message"` // Message contained inside the post - Created time.Time `json:"created"` // RFC3339 date at which the post has been created - LastEdited time.Time `json:"last_edited"` // RFC3339 date 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 - Subspace string `json:"subspace"` // Identifies the application that has posted the message - OptionalData OptionalData `json:"optional_data,omitempty"` // Arbitrary data that can be used from the developers - Creator sdk.AccAddress `json:"creator"` // Creator of the Post -} + "time" +) -func NewPost(id, parentID PostID, message string, allowsComments bool, subspace string, optionalData map[string]string, - created time.Time, creator sdk.AccAddress) Post { - return Post{ - PostID: id, - ParentID: parentID, - Message: message, - Created: created, - LastEdited: time.Time{}, - AllowsComments: allowsComments, - Subspace: subspace, - OptionalData: optionalData, - Creator: creator, - } +// Represents the interface that each post should implement +type Post interface { + String() string // Returns the string representation of the post + GetID() PostID // Returns the ID of the post + GetParentID() PostID // Returns the ParentID of the post + SetMessage(message string) Post // Sets the post message and return the post + GetMessage() string // Returns the post message + CreationTime() time.Time // Returns the creation time of the post + GetEditTime() time.Time // Returns the last modified time of the post + SetEditTime(time time.Time) Post // Sets the last modified time of the post and return the post + CanComment() bool // Tells if the post can be commented + GetSubspace() string // Returns the subspace of the post + GetOptionalData() map[string]string //Returns the optional data of the post + Owner() sdk.AccAddress // Tells which user owns the post + Validate() error // Tells if the post is valid or not + Equals(Post) bool // Tells if this post has the same contents of the one given } -func NewPostComplete(id, parentID PostID, message string, created, lastEdited time.Time, allowsComments bool, - subspace string, optionalData map[string]string, creator sdk.AccAddress) Post { - return Post{ - PostID: id, - ParentID: parentID, - Message: message, - Created: created, - LastEdited: lastEdited, - AllowsComments: allowsComments, - Subspace: subspace, - OptionalData: optionalData, - Creator: creator, - } -} +type Posts []Post // String implements fmt.Stringer -func (p Post) String() string { - bytes, err := json.Marshal(&p) +func (posts Posts) String() string { + bytes, err := json.Marshal(&posts) if err != nil { panic(err) } return string(bytes) } - -// Validate implements validator -func (p Post) Validate() error { - if !p.PostID.Valid() { - return fmt.Errorf("invalid post id: %s", p.PostID) - } - - if p.Creator == nil { - return fmt.Errorf("invalid post owner: %s", p.Creator) - } - - if len(strings.TrimSpace(p.Message)) == 0 { - return fmt.Errorf("post message must be non empty and non blank") - } - - if len(p.Message) > MaxPostMessageLength { - return fmt.Errorf("post message cannot be longer than %d characters", MaxPostMessageLength) - } - - if len(strings.TrimSpace(p.Subspace)) == 0 { - return fmt.Errorf("post subspace must be non empty and non blank") - } - - if p.Created.IsZero() { - return fmt.Errorf("invalid post creation time: %s", p.Created) - } - - if p.Created.After(time.Now().UTC()) { - return fmt.Errorf("post creation date cannot be in the future") - } - - if !p.LastEdited.IsZero() && p.LastEdited.Before(p.Created) { - return fmt.Errorf("invalid post last edit time: %s", p.LastEdited) - } - - if !p.LastEdited.IsZero() && p.LastEdited.After(time.Now().UTC()) { - return fmt.Errorf("post last edit date cannot be in the future") - } - - if len(p.OptionalData) > MaxOptionalDataFieldsNumber { - return fmt.Errorf("post optional data cannot contain more than %d key-value pairs", MaxOptionalDataFieldsNumber) - } - - for key, value := range p.OptionalData { - if len(value) > MaxOptionalDataFieldValueLength { - return fmt.Errorf( - "post optional data values cannot exceed %d characters. %s of post with id %s is longer than this", - MaxOptionalDataFieldValueLength, key, p.PostID, - ) - } - } - - return nil -} - -// Equals allows to check whether the contents of p are the same of other -func (p Post) Equals(other Post) bool { - equalsOptionalData := len(p.OptionalData) == len(other.OptionalData) - if equalsOptionalData { - for key := range p.OptionalData { - equalsOptionalData = equalsOptionalData && p.OptionalData[key] == other.OptionalData[key] - } - } - - return p.PostID.Equals(other.PostID) && - p.ParentID.Equals(other.ParentID) && - p.Message == other.Message && - p.Created.Equal(other.Created) && - p.LastEdited.Equal(other.LastEdited) && - p.AllowsComments == other.AllowsComments && - p.Subspace == other.Subspace && - equalsOptionalData && - p.Creator.Equals(other.Creator) -} - -// ------------- -// --- 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 - } - - for index, post := range p { - if !post.Equals(other[index]) { - return false - } - } - - return true -} - -// String implements stringer interface -func (p Posts) String() string { - out := "ID - [Creator] Message\n" - for _, post := range p { - out += fmt.Sprintf("%d - [%s] %s\n", - post.PostID, post.Creator, post.Message) - } - return strings.TrimSpace(out) -} diff --git a/x/posts/internal/types/post_response.go b/x/posts/internal/types/post_response.go index 6036d81a35..363152f620 100644 --- a/x/posts/internal/types/post_response.go +++ b/x/posts/internal/types/post_response.go @@ -7,13 +7,34 @@ import ( // PostQueryResponse represents the data of a post // that is returned to user upon a query type PostQueryResponse struct { - Post + Type string `json:"type"` + Post Post `json:"post"` Reactions Reactions `json:"reactions"` Children PostIDs `json:"children"` } +func (response PostQueryResponse) String() string { + bytes, err := json.Marshal(&response) + if err != nil { + panic(err) + } + + return string(bytes) +} + func NewPostResponse(post Post, reactions Reactions, children PostIDs) PostQueryResponse { + var msgType string + + if _, ok := post.(TextPost); ok { + msgType = "TextPost" + } + + if _, ok := post.(MediaPost); ok { + msgType = "MediaPost" + } + return PostQueryResponse{ + Type: msgType, Post: post, Reactions: reactions, Children: children, diff --git a/x/posts/internal/types/post_response_test.go b/x/posts/internal/types/post_response_test.go index cf00158eb3..f0e3fcccff 100644 --- a/x/posts/internal/types/post_response_test.go +++ b/x/posts/internal/types/post_response_test.go @@ -19,18 +19,51 @@ func TestPostQueryResponse_MarshalJSON(t *testing.T) { timeZone, _ := time.LoadLocation("UTC") date := time.Date(2020, 2, 2, 15, 0, 0, 0, timeZone) - post := types.NewPost(types.PostID(10), types.PostID(0), "Post", true, "desmos", map[string]string{}, date, postOwner) + post := types.NewTextPost(types.PostID(10), types.PostID(0), "Post", true, "desmos", map[string]string{}, date, postOwner) likes := types.Reactions{ types.NewReaction("like", liker), types.NewReaction("like", otherLiker), } children := types.PostIDs{types.PostID(98), types.PostID(100)} - response := types.NewPostResponse(post, likes, children) - jsonData, err := json.Marshal(&response) - assert.NoError(t, err) - assert.Equal(t, - `{"id":"10","parent_id":"0","message":"Post","created":"2020-02-02T15:00:00Z","last_edited":"0001-01-01T00:00:00Z","allows_comments":true,"subspace":"desmos","creator":"cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47","reactions":[{"owner":"cosmos1s3nh6tafl4amaxkke9kdejhp09lk93g9ev39r4","value":"like"},{"owner":"cosmos15lt0mflt6j9a9auj7yl3p20xec4xvljge0zhae","value":"like"}],"children":["98","100"]}`, - string(jsonData), - ) + textPostResponse := types.NewPostResponse(post, likes, children) + + media := types.PostMedia{ + Provider: "provider", + URI: "uri", + MimeType: "text/plain", + } + + mediaPost := types.NewMediaPost(post, []types.PostMedia{media}) + + mediaPostResponse := types.NewPostResponse(mediaPost, likes, children) + + tests := []struct { + name string + response types.PostQueryResponse + expResponse string + }{ + { + name: "Post Query Response with TextPost", + response: textPostResponse, + expResponse: `{"type":"TextPost","post":{"id":"10","parent_id":"0","message":"Post","created":"2020-02-02T15:00:00Z","last_edited":"0001-01-01T00:00:00Z","allows_comments":true,"subspace":"desmos","creator":"cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47"},"reactions":[{"owner":"cosmos1s3nh6tafl4amaxkke9kdejhp09lk93g9ev39r4","value":"like"},{"owner":"cosmos15lt0mflt6j9a9auj7yl3p20xec4xvljge0zhae","value":"like"}],"children":["98","100"]}`, + }, + { + name: "Post Query Response with MediaPost", + response: mediaPostResponse, + expResponse: `{"type":"MediaPost","post":{"id":"10","parent_id":"0","message":"Post","created":"2020-02-02T15:00:00Z","last_edited":"0001-01-01T00:00:00Z","allows_comments":true,"subspace":"desmos","creator":"cosmos1y54exmx84cqtasvjnskf9f63djuuj68p7hqf47","medias":[{"uri":"uri","provider":"provider","mime_Type":"text/plain"}]},"reactions":[{"owner":"cosmos1s3nh6tafl4amaxkke9kdejhp09lk93g9ev39r4","value":"like"},{"owner":"cosmos15lt0mflt6j9a9auj7yl3p20xec4xvljge0zhae","value":"like"}],"children":["98","100"]}`, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + jsonData, err := json.Marshal(&test.response) + assert.NoError(t, err) + assert.Equal(t, + test.expResponse, + string(jsonData), + ) + }) + } } diff --git a/x/posts/internal/types/text_post.go b/x/posts/internal/types/text_post.go new file mode 100644 index 0000000000..0e4c20d1ce --- /dev/null +++ b/x/posts/internal/types/text_post.go @@ -0,0 +1,326 @@ +package types + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// --------------- +// --- TextPost id +// --------------- + +// 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) +} + +// checkPostsEqual compares two PostID instances +func (id PostID) Equals(other PostID) bool { + return id == other +} + +// MarshalJSON implements Marshaler +func (id PostID) MarshalJSON() ([]byte, error) { + return json.Marshal(id.String()) +} + +// UnmarshalJSON implements Unmarshaler +func (id *PostID) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + + postID, err := ParsePostID(s) + if err != nil { + return err + } + + *id = postID + return nil +} + +// 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 { + return PostID(0), err + } + + return PostID(intVal), err +} + +// ---------------- +// --- TextPost IDs +// ---------------- + +// PostIDs represents a slice of PostID objects +type PostIDs []PostID + +// checkPostsEqual 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 +} + +// AppendIfMissing appends the given postID to the ids slice if it does not exist inside it yet. +// It returns a new slice of PostIDs containing such ID and a boolean indicating whether or not the original +// slice has been modified. +func (ids PostIDs) AppendIfMissing(id PostID) (PostIDs, bool) { + for _, ele := range ids { + if ele.Equals(id) { + return ids, false + } + } + return append(ids, id), true +} + +// --------------- +// --- TextPost +// --------------- + +// TextPost is a struct of a post +type TextPost struct { + PostID PostID `json:"id"` // Unique id + ParentID PostID `json:"parent_id"` // Post of which this one is a comment + Message string `json:"message"` // Message contained inside the post + Created time.Time `json:"created"` // RFC3339 date at which the post has been created + LastEdited time.Time `json:"last_edited"` // RFC3339 date 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 + Subspace string `json:"subspace"` // Identifies the application that has posted the message + OptionalData OptionalData `json:"optional_data,omitempty"` // Arbitrary data that can be used from the developers + Creator sdk.AccAddress `json:"creator"` // Creator of the Post +} + +func NewTextPost(id, parentID PostID, message string, allowsComments bool, subspace string, optionalData map[string]string, + created time.Time, creator sdk.AccAddress) TextPost { + return TextPost{ + PostID: id, + ParentID: parentID, + Message: message, + Created: created, + LastEdited: time.Time{}, + AllowsComments: allowsComments, + Subspace: subspace, + OptionalData: optionalData, + Creator: creator, + } +} + +func NewTextPostComplete(id, parentID PostID, message string, created, lastEdited time.Time, allowsComments bool, + subspace string, optionalData map[string]string, creator sdk.AccAddress) TextPost { + return TextPost{ + PostID: id, + ParentID: parentID, + Message: message, + Created: created, + LastEdited: lastEdited, + AllowsComments: allowsComments, + Subspace: subspace, + OptionalData: optionalData, + Creator: creator, + } +} + +// String implements fmt.Stringer +func (p TextPost) String() string { + bytes, err := json.Marshal(&p) + if err != nil { + panic(err) + } + + return string(bytes) +} + +// GetID implements Post GetID +func (p TextPost) GetID() PostID { + return p.PostID +} + +// GetParentID implements Post GetParentID +func (p TextPost) GetParentID() PostID { + return p.ParentID +} + +func (p TextPost) SetMessage(message string) Post { + p.Message = message + return p +} + +func (p TextPost) GetMessage() string { + return p.Message +} + +func (p TextPost) CreationTime() time.Time { + return p.Created +} + +func (p TextPost) SetEditTime(time time.Time) Post { + p.LastEdited = time + return p +} + +func (p TextPost) GetEditTime() time.Time { + return p.LastEdited +} + +func (p TextPost) CanComment() bool { + return p.AllowsComments +} + +func (p TextPost) GetSubspace() string { + return p.Subspace +} + +func (p TextPost) GetOptionalData() map[string]string { + return p.OptionalData +} + +func (p TextPost) Owner() sdk.AccAddress { + return p.Creator +} + +// Validate implements Post Validate +func (p TextPost) Validate() error { + if !p.PostID.Valid() { + return fmt.Errorf("invalid post id: %s", p.PostID) + } + + if p.Creator == nil { + return fmt.Errorf("invalid post owner: %s", p.Creator) + } + + if len(strings.TrimSpace(p.Message)) == 0 { + return fmt.Errorf("post message must be non empty and non blank") + } + + if len(p.Message) > MaxPostMessageLength { + return fmt.Errorf("post message cannot be longer than %d characters", MaxPostMessageLength) + } + + if len(strings.TrimSpace(p.Subspace)) == 0 { + return fmt.Errorf("post subspace must be non empty and non blank") + } + + if p.Created.IsZero() { + return fmt.Errorf("invalid post creation time: %s", p.Created) + } + + if p.Created.After(time.Now().UTC()) { + return fmt.Errorf("post creation date cannot be in the future") + } + + if !p.LastEdited.IsZero() && p.LastEdited.Before(p.Created) { + return fmt.Errorf("invalid post last edit time: %s", p.LastEdited) + } + + if !p.LastEdited.IsZero() && p.LastEdited.After(time.Now().UTC()) { + return fmt.Errorf("post last edit date cannot be in the future") + } + + if len(p.OptionalData) > MaxOptionalDataFieldsNumber { + return fmt.Errorf("post optional data cannot contain more than %d key-value pairs", MaxOptionalDataFieldsNumber) + } + + for key, value := range p.OptionalData { + if len(value) > MaxOptionalDataFieldValueLength { + return fmt.Errorf( + "post optional data values cannot exceed %d characters. %s of post with id %s is longer than this", + MaxOptionalDataFieldValueLength, key, p.PostID, + ) + } + } + + return nil +} + +// Equals implements Post Equals +func (p TextPost) Equals(other Post) bool { + // Cast and delegate + if otherPost, ok := other.(TextPost); ok { + return checkPostsEqual(p, otherPost) + } + + return false +} + +func checkPostsEqual(first TextPost, second TextPost) bool { + equalsOptionalData := len(first.OptionalData) == len(second.OptionalData) + if equalsOptionalData { + for key := range first.OptionalData { + equalsOptionalData = equalsOptionalData && first.OptionalData[key] == second.OptionalData[key] + } + } + + return first.PostID.Equals(second.PostID) && + first.ParentID.Equals(second.ParentID) && + first.Message == second.Message && + first.Created.Equal(second.Created) && + first.LastEdited.Equal(second.LastEdited) && + first.AllowsComments == second.AllowsComments && + first.Subspace == second.Subspace && + equalsOptionalData && + first.Creator.Equals(second.Creator) +} + +// ------------- +// --- TextPosts +// ------------- + +// TextPosts represents a slice of TextPost objects +type TextPosts []TextPost + +// checkPostsEqual returns true iff the p slice contains the same +// data in the same order of the other slice +func (p TextPosts) Equals(other TextPosts) bool { + if len(p) != len(other) { + return false + } + + for index, post := range p { + if !post.Equals(other[index]) { + return false + } + } + + return true +} + +// String implements stringer interface +func (p TextPosts) String() string { + out := "ID - [Creator] Message\n" + for _, post := range p { + out += fmt.Sprintf("%d - [%s] %s\n", + post.PostID, post.Creator, post.Message) + } + return strings.TrimSpace(out) +} diff --git a/x/posts/internal/types/post_test.go b/x/posts/internal/types/text_post_test.go similarity index 69% rename from x/posts/internal/types/post_test.go rename to x/posts/internal/types/text_post_test.go index 60747e2a05..8860ecb8e4 100644 --- a/x/posts/internal/types/post_test.go +++ b/x/posts/internal/types/text_post_test.go @@ -186,15 +186,117 @@ func TestPostIDs_AppendIfMissing(t *testing.T) { } } +func TestPost_GetID(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + testPostCreationDate := time.Date(2020, 1, 1, 15, 15, 00, 000, timeZone) + var post = types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner) + + actual := post.GetID() + + assert.Equal(t, types.PostID(2), actual) +} + +func TestPost_GetParentID(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + var post = types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner) + + actual := post.GetParentID() + + assert.Equal(t, types.PostID(0), actual) +} + +func TestPost_SetMessage(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + var post = types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner) + + actual := post.SetMessage("edited media post") + + assert.Equal(t, "edited media post", actual.GetMessage()) +} + +func TestPost_GetMessage(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + var post = types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner) + + actual := post.GetMessage() + + assert.Equal(t, "media Post", actual) + +} + +func TestPost_CreationTime(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + var post = types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner) + + actual := post.CreationTime() + + assert.Equal(t, testPostCreationDate, actual) +} + +func TestPost_SetEditTime(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + var post = types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner) + testPostNewDate := time.Date(2020, 1, 1, 15, 15, 00, 000, timeZone) + + actual := post.SetEditTime(testPostNewDate) + + assert.Equal(t, testPostNewDate, actual.GetEditTime()) +} + +func TestPost_GetEditTime(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + var post = types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner) + + actual := post.GetEditTime() + + assert.Equal(t, time.Time{}, actual) +} + +func TestPost_CanComment(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + var post = types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner) + + actual := post.CanComment() + + assert.Equal(t, false, actual) +} + +func TestPost_GetSubspace(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + var post = types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner) + + actual := post.GetSubspace() + + assert.Equal(t, "desmos", actual) +} + +func TestPost_GetOptionalData(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + var post = types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{"key1": "value1"}, testPostCreationDate, owner) + + actual := post.GetOptionalData() + + assert.Equal(t, map[string]string{"key1": "value1"}, actual) +} + +func TestPost_Owner(t *testing.T) { + owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") + var post = types.NewTextPost(types.PostID(2), types.PostID(0), "media Post", false, "desmos", map[string]string{}, testPostCreationDate, owner) + + actual := post.Owner() + + assert.Equal(t, owner, actual) +} + // ----------- -// --- Post +// --- TextPost // ----------- func TestPost_String(t *testing.T) { owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") timeZone, _ := time.LoadLocation("UTC") date := time.Date(2020, 1, 1, 12, 00, 00, 000, timeZone) - post := types.Post{ + post := types.TextPost{ PostID: types.PostID(19), ParentID: types.PostID(1), Message: "My post message", @@ -214,48 +316,47 @@ func TestPost_String(t *testing.T) { func TestPost_Validate(t *testing.T) { owner, _ := sdk.AccAddressFromBech32("cosmos1cjf97gpzwmaf30pzvaargfgr884mpp5ak8f7ns") - timeZone, _ := time.LoadLocation("UTC") date := time.Date(2020, 1, 1, 12, 00, 00, 000, timeZone) tests := []struct { - post types.Post + post types.TextPost expError string }{ { - post: types.NewPost(types.PostID(0), types.PostID(0), "Message", true, "Desmos", map[string]string{}, date, owner), + post: types.NewTextPost(types.PostID(0), types.PostID(0), "Message", true, "Desmos", map[string]string{}, date, owner), expError: "invalid post id: 0", }, { - post: types.NewPost(types.PostID(1), types.PostID(0), "", true, "Desmos", map[string]string{}, date, nil), + post: types.NewTextPost(types.PostID(1), types.PostID(0), "", true, "Desmos", map[string]string{}, date, nil), expError: "invalid post owner: ", }, { - post: types.NewPost(types.PostID(1), types.PostID(0), "", true, "Desmos", map[string]string{}, date, owner), + post: types.NewTextPost(types.PostID(1), types.PostID(0), "", true, "Desmos", map[string]string{}, date, owner), expError: "post message must be non empty and non blank", }, { - post: types.NewPost(types.PostID(1), types.PostID(0), " ", true, "Desmos", map[string]string{}, date, owner), + post: types.NewTextPost(types.PostID(1), types.PostID(0), " ", true, "Desmos", map[string]string{}, date, owner), expError: "post message must be non empty and non blank", }, { - post: types.NewPost(types.PostID(1), types.PostID(0), "Message", true, "Desmos", map[string]string{}, time.Time{}, owner), + post: types.NewTextPost(types.PostID(1), types.PostID(0), "Message", true, "Desmos", map[string]string{}, time.Time{}, owner), expError: "invalid post creation time: 0001-01-01 00:00:00 +0000 UTC", }, { - post: types.Post{PostID: types.PostID(19), Creator: owner, Message: "Message", Subspace: "desmos", Created: date, LastEdited: date.AddDate(0, 0, -1)}, + post: types.TextPost{PostID: types.PostID(19), Creator: owner, Message: "Message", Subspace: "desmos", Created: date, LastEdited: date.AddDate(0, 0, -1)}, expError: "invalid post last edit time: 2019-12-31 12:00:00 +0000 UTC", }, { - post: types.NewPost(types.PostID(1), types.PostID(0), "Message", true, "", map[string]string{}, date, owner), + post: types.NewTextPost(types.PostID(1), types.PostID(0), "Message", true, "", map[string]string{}, date, owner), expError: "post subspace must be non empty and non blank", }, { - post: types.NewPost(types.PostID(1), types.PostID(0), "Message", true, " ", map[string]string{}, date, owner), + post: types.NewTextPost(types.PostID(1), types.PostID(0), "Message", true, " ", map[string]string{}, date, owner), expError: "post subspace must be non empty and non blank", }, { - post: types.Post{ + post: types.TextPost{ PostID: types.PostID(1), ParentID: types.PostID(0), Message: "Message", @@ -268,7 +369,7 @@ func TestPost_Validate(t *testing.T) { expError: "post creation date cannot be in the future", }, { - post: types.Post{ + post: types.TextPost{ PostID: types.PostID(1), ParentID: types.PostID(0), Message: "Message", @@ -282,7 +383,7 @@ func TestPost_Validate(t *testing.T) { expError: "post last edit date cannot be in the future", }, { - post: types.NewPost( + post: types.NewTextPost( types.PostID(1), types.PostID(0), ` @@ -301,7 +402,7 @@ func TestPost_Validate(t *testing.T) { expError: "post message cannot be longer than 500 characters", }, { - post: types.NewPost( + post: types.NewTextPost( types.PostID(1), types.PostID(0), "Message", @@ -326,7 +427,7 @@ func TestPost_Validate(t *testing.T) { expError: "post optional data cannot contain more than 10 key-value pairs", }, { - post: types.NewPost( + post: types.NewTextPost( types.PostID(1), types.PostID(0), "Message", @@ -343,7 +444,7 @@ func TestPost_Validate(t *testing.T) { expError: "post optional data values cannot exceed 200 characters. key1 of post with id 1 is longer than this", }, { - post: types.NewPost(types.PostID(1), types.PostID(0), "Message", true, "Desmos", map[string]string{}, date, owner), + post: types.NewTextPost(types.PostID(1), types.PostID(0), "Message", true, "Desmos", map[string]string{}, date, owner), expError: "", }, } @@ -369,13 +470,13 @@ func TestPost_Equals(t *testing.T) { tests := []struct { name string - first types.Post - second types.Post + first types.TextPost + second types.TextPost expEquals bool }{ { name: "Different post ID", - first: types.Post{ + first: types.TextPost{ PostID: types.PostID(19), ParentID: types.PostID(1), Message: "My post message", @@ -386,7 +487,7 @@ func TestPost_Equals(t *testing.T) { OptionalData: map[string]string{}, Creator: owner, }, - second: types.Post{ + second: types.TextPost{ PostID: types.PostID(10), ParentID: types.PostID(1), Message: "My post message", @@ -401,7 +502,7 @@ func TestPost_Equals(t *testing.T) { }, { name: "Different parent ID", - first: types.Post{ + first: types.TextPost{ PostID: types.PostID(19), ParentID: types.PostID(1), Message: "My post message", @@ -412,7 +513,7 @@ func TestPost_Equals(t *testing.T) { OptionalData: map[string]string{}, Creator: owner, }, - second: types.Post{ + second: types.TextPost{ PostID: types.PostID(19), ParentID: types.PostID(10), Message: "My post message", @@ -427,7 +528,7 @@ func TestPost_Equals(t *testing.T) { }, { name: "Different message", - first: types.Post{ + first: types.TextPost{ PostID: types.PostID(19), ParentID: types.PostID(1), Message: "My post message", @@ -438,7 +539,7 @@ func TestPost_Equals(t *testing.T) { OptionalData: map[string]string{}, Creator: owner, }, - second: types.Post{ + second: types.TextPost{ PostID: types.PostID(19), ParentID: types.PostID(1), Message: "Another post message", @@ -453,7 +554,7 @@ func TestPost_Equals(t *testing.T) { }, { name: "Different creation time", - first: types.Post{ + first: types.TextPost{ PostID: types.PostID(19), ParentID: types.PostID(1), Message: "My post message", @@ -464,7 +565,7 @@ func TestPost_Equals(t *testing.T) { OptionalData: map[string]string{}, Creator: owner, }, - second: types.Post{ + second: types.TextPost{ PostID: types.PostID(19), ParentID: types.PostID(1), Message: "My post message", @@ -479,7 +580,7 @@ func TestPost_Equals(t *testing.T) { }, { name: "Different last edited", - first: types.Post{ + first: types.TextPost{ PostID: types.PostID(19), ParentID: types.PostID(1), Message: "My post message", @@ -490,7 +591,7 @@ func TestPost_Equals(t *testing.T) { OptionalData: map[string]string{}, Creator: owner, }, - second: types.Post{ + second: types.TextPost{ PostID: types.PostID(19), ParentID: types.PostID(1), Message: "My post message", @@ -505,7 +606,7 @@ func TestPost_Equals(t *testing.T) { }, { name: "Different allows comments", - first: types.Post{ + first: types.TextPost{ PostID: types.PostID(19), ParentID: types.PostID(1), Message: "My post message", @@ -516,7 +617,7 @@ func TestPost_Equals(t *testing.T) { OptionalData: map[string]string{}, Creator: owner, }, - second: types.Post{ + second: types.TextPost{ PostID: types.PostID(19), ParentID: types.PostID(1), Message: "My post message", @@ -531,7 +632,7 @@ func TestPost_Equals(t *testing.T) { }, { name: "Different subspace", - first: types.Post{ + first: types.TextPost{ PostID: types.PostID(19), ParentID: types.PostID(1), Message: "My post message", @@ -542,7 +643,7 @@ func TestPost_Equals(t *testing.T) { OptionalData: map[string]string{}, Creator: owner, }, - second: types.Post{ + second: types.TextPost{ PostID: types.PostID(19), ParentID: types.PostID(1), Message: "My post message", @@ -557,7 +658,7 @@ func TestPost_Equals(t *testing.T) { }, { name: "Different optional data", - first: types.Post{ + first: types.TextPost{ PostID: types.PostID(19), ParentID: types.PostID(1), Message: "My post message", @@ -570,7 +671,7 @@ func TestPost_Equals(t *testing.T) { }, Creator: owner, }, - second: types.Post{ + second: types.TextPost{ PostID: types.PostID(19), ParentID: types.PostID(1), Message: "My post message", @@ -587,7 +688,7 @@ func TestPost_Equals(t *testing.T) { }, { name: "Different owner", - first: types.Post{ + first: types.TextPost{ PostID: types.PostID(19), ParentID: types.PostID(1), Message: "My post message", @@ -598,7 +699,7 @@ func TestPost_Equals(t *testing.T) { OptionalData: map[string]string{}, Creator: owner, }, - second: types.Post{ + second: types.TextPost{ PostID: types.PostID(19), ParentID: types.PostID(1), Message: "My post message", @@ -613,7 +714,7 @@ func TestPost_Equals(t *testing.T) { }, { name: "Same data", - first: types.Post{ + first: types.TextPost{ PostID: types.PostID(19), ParentID: types.PostID(1), Message: "My post message", @@ -624,7 +725,7 @@ func TestPost_Equals(t *testing.T) { OptionalData: map[string]string{}, Creator: owner, }, - second: types.Post{ + second: types.TextPost{ PostID: types.PostID(19), ParentID: types.PostID(1), Message: "My post message", @@ -648,7 +749,7 @@ func TestPost_Equals(t *testing.T) { } // ----------- -// --- Posts +// --- TextPosts // ----------- func TestPosts_Equals(t *testing.T) { timeZone, _ := time.LoadLocation("UTC") @@ -656,48 +757,48 @@ func TestPosts_Equals(t *testing.T) { tests := []struct { name string - first types.Posts - second types.Posts + first types.TextPosts + second types.TextPosts expEquals bool }{ { name: "Empty lists are equals", - first: types.Posts{}, - second: types.Posts{}, + first: types.TextPosts{}, + second: types.TextPosts{}, expEquals: true, }, { name: "List of different lengths are not equals", - first: types.Posts{ - types.Post{PostID: types.PostID(0), Created: date, LastEdited: date.AddDate(0, 0, 1)}, + first: types.TextPosts{ + types.TextPost{PostID: types.PostID(0), Created: date, LastEdited: date.AddDate(0, 0, 1)}, }, - second: types.Posts{ - types.Post{PostID: types.PostID(0), Created: date, LastEdited: date.AddDate(0, 0, 1)}, - types.Post{PostID: types.PostID(1), Created: date, LastEdited: date.AddDate(0, 0, 1)}, + second: types.TextPosts{ + types.TextPost{PostID: types.PostID(0), Created: date, LastEdited: date.AddDate(0, 0, 1)}, + types.TextPost{PostID: types.PostID(1), Created: date, LastEdited: date.AddDate(0, 0, 1)}, }, expEquals: false, }, { name: "Same lists but in different orders", - first: types.Posts{ - types.Post{PostID: types.PostID(0), Created: date, LastEdited: date.AddDate(0, 0, 1)}, - types.Post{PostID: types.PostID(1), Created: date, LastEdited: date.AddDate(0, 0, 1)}, + first: types.TextPosts{ + types.TextPost{PostID: types.PostID(0), Created: date, LastEdited: date.AddDate(0, 0, 1)}, + types.TextPost{PostID: types.PostID(1), Created: date, LastEdited: date.AddDate(0, 0, 1)}, }, - second: types.Posts{ - types.Post{PostID: types.PostID(1), Created: date, LastEdited: date.AddDate(0, 0, 1)}, - types.Post{PostID: types.PostID(0), Created: date, LastEdited: date.AddDate(0, 0, 1)}, + second: types.TextPosts{ + types.TextPost{PostID: types.PostID(1), Created: date, LastEdited: date.AddDate(0, 0, 1)}, + types.TextPost{PostID: types.PostID(0), Created: date, LastEdited: date.AddDate(0, 0, 1)}, }, expEquals: false, }, { name: "Same lists are equals", - first: types.Posts{ - types.Post{PostID: types.PostID(0), Created: date, LastEdited: date.AddDate(0, 0, 1)}, - types.Post{PostID: types.PostID(1), Created: date, LastEdited: date.AddDate(0, 0, 1)}, + first: types.TextPosts{ + types.TextPost{PostID: types.PostID(0), Created: date, LastEdited: date.AddDate(0, 0, 1)}, + types.TextPost{PostID: types.PostID(1), Created: date, LastEdited: date.AddDate(0, 0, 1)}, }, - second: types.Posts{ - types.Post{PostID: types.PostID(0), Created: date, LastEdited: date.AddDate(0, 0, 1)}, - types.Post{PostID: types.PostID(1), Created: date, LastEdited: date.AddDate(0, 0, 1)}, + second: types.TextPosts{ + types.TextPost{PostID: types.PostID(0), Created: date, LastEdited: date.AddDate(0, 0, 1)}, + types.TextPost{PostID: types.PostID(1), Created: date, LastEdited: date.AddDate(0, 0, 1)}, }, expEquals: true, }, @@ -718,9 +819,9 @@ func TestPosts_String(t *testing.T) { timeZone, _ := time.LoadLocation("UTC") date := time.Date(2020, 1, 1, 12, 0, 00, 000, timeZone) - posts := types.Posts{ - types.NewPost(types.PostID(1), types.PostID(10), "Post 1", false, "external-ref-1", map[string]string{}, date, owner1), - types.NewPost(types.PostID(2), types.PostID(10), "Post 2", false, "external-ref-1", map[string]string{}, date, owner2), + posts := types.TextPosts{ + types.NewTextPost(types.PostID(1), types.PostID(10), "Post 1", false, "external-ref-1", map[string]string{}, date, owner1), + types.NewTextPost(types.PostID(2), types.PostID(10), "Post 2", false, "external-ref-1", map[string]string{}, date, owner2), } expected := `ID - [Creator] Message diff --git a/x/posts/legacy/v0.1.0/types.go b/x/posts/legacy/v0.1.0/types.go index fd65f40e9d..07d1ed40ec 100644 --- a/x/posts/legacy/v0.1.0/types.go +++ b/x/posts/legacy/v0.1.0/types.go @@ -24,7 +24,7 @@ type Post struct { 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 + Owner sdk.AccAddress `json:"owner"` // Creator of the post } // Reaction is a struct of a user like