From 0988f477d1080042be41cdb0c0931a3118364885 Mon Sep 17 00:00:00 2001 From: Mercier Mateo Date: Sun, 6 Oct 2024 20:22:36 +0200 Subject: [PATCH 1/7] Feature add tags on post creation first version --- app/actions/post.go | 1 + app/handlers/apiv1/post.go | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/app/actions/post.go b/app/actions/post.go index fa7ba65a7..37068efd2 100644 --- a/app/actions/post.go +++ b/app/actions/post.go @@ -21,6 +21,7 @@ import ( type CreateNewPost struct { Title string `json:"title"` Description string `json:"description"` + Tags []string `json:"tags"` Attachments []*dto.ImageUpload `json:"attachments"` } diff --git a/app/handlers/apiv1/post.go b/app/handlers/apiv1/post.go index bf33153ef..bed83646e 100644 --- a/app/handlers/apiv1/post.go +++ b/app/handlers/apiv1/post.go @@ -59,6 +59,18 @@ func CreatePost() web.HandlerFunc { if err = bus.Dispatch(c, setAttachments, addVote); err != nil { return c.Failure(err) } + + for _, tag := range action.Tags { + getTag := &query.GetTagBySlug{Slug: tag} + if err := bus.Dispatch(c, getTag); err != nil { + return c.Failure(err) + } + + assignTag := &cmd.AssignTag{Tag: getTag.Result, Post: newPost.Result} + if err := bus.Dispatch(c, assignTag); err != nil { + return c.Failure(err) + } + } c.Enqueue(tasks.NotifyAboutNewPost(newPost.Result)) From dddd043f461146b9a236d6cf377f7f983cb19467 Mon Sep 17 00:00:00 2001 From: Mercier Mateo Date: Mon, 18 Nov 2024 00:06:44 +0100 Subject: [PATCH 2/7] Add authorization on post creation with tags --- app/actions/post.go | 30 ++++++++++++++++++++++++++++-- app/handlers/apiv1/post.go | 7 +------ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/app/actions/post.go b/app/actions/post.go index e34d97323..0750f494e 100644 --- a/app/actions/post.go +++ b/app/actions/post.go @@ -21,13 +21,39 @@ import ( type CreateNewPost struct { Title string `json:"title"` Description string `json:"description"` - Tags []string `json:"tags"` + TagSlugs []string `json:"tags"` Attachments []*dto.ImageUpload `json:"attachments"` + + Tags []*entity.Tag +} + +// OnPreExecute prefetches Post for later use +func (input *CreateNewPost) OnPreExecute(ctx context.Context) error { + input.Tags = make([]*entity.Tag, len(input.TagSlugs)) + for i, slug := range input.TagSlugs { + getTag := &query.GetTagBySlug{Slug: slug} + if err := bus.Dispatch(ctx, getTag); err != nil { + return err + } + + input.Tags[i] = getTag.Result + } + + return nil } // IsAuthorized returns true if current user is authorized to perform this action func (action *CreateNewPost) IsAuthorized(ctx context.Context, user *entity.User) bool { - return user != nil + if user == nil { + return false + } else if !user.IsCollaborator() { + for _, tag := range action.Tags { + if !tag.IsPublic { + return false + } + } + } + return true } // Validate if current model is valid diff --git a/app/handlers/apiv1/post.go b/app/handlers/apiv1/post.go index 8ea0c5277..2c57cbeb8 100644 --- a/app/handlers/apiv1/post.go +++ b/app/handlers/apiv1/post.go @@ -61,12 +61,7 @@ func CreatePost() web.HandlerFunc { } for _, tag := range action.Tags { - getTag := &query.GetTagBySlug{Slug: tag} - if err := bus.Dispatch(c, getTag); err != nil { - return c.Failure(err) - } - - assignTag := &cmd.AssignTag{Tag: getTag.Result, Post: newPost.Result} + assignTag := &cmd.AssignTag{Tag: tag, Post: newPost.Result} if err := bus.Dispatch(c, assignTag); err != nil { return c.Failure(err) } From 782dcafb85a72c20015ffa02f61df0875279d756 Mon Sep 17 00:00:00 2001 From: Mercier Mateo Date: Tue, 26 Nov 2024 11:03:14 +0100 Subject: [PATCH 3/7] Fix comment on CreateNewPost action --- app/actions/post.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/actions/post.go b/app/actions/post.go index 0750f494e..b2865050b 100644 --- a/app/actions/post.go +++ b/app/actions/post.go @@ -27,7 +27,7 @@ type CreateNewPost struct { Tags []*entity.Tag } -// OnPreExecute prefetches Post for later use +// OnPreExecute prefetches Tags for later use func (input *CreateNewPost) OnPreExecute(ctx context.Context) error { input.Tags = make([]*entity.Tag, len(input.TagSlugs)) for i, slug := range input.TagSlugs { From 3b44094be9ce304f048a152ce3bc5a4805559a08 Mon Sep 17 00:00:00 2001 From: Mercier Mateo Date: Tue, 26 Nov 2024 12:01:12 +0100 Subject: [PATCH 4/7] Add feature flag on post creation with tags --- app/actions/post.go | 21 +++++++++++++-------- app/handlers/apiv1/post.go | 11 +++++++---- app/pkg/env/env.go | 13 +++++++------ 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/app/actions/post.go b/app/actions/post.go index b2865050b..18255a655 100644 --- a/app/actions/post.go +++ b/app/actions/post.go @@ -3,12 +3,14 @@ package actions import ( "context" "time" + "fmt" "github.com/getfider/fider/app/models/dto" "github.com/getfider/fider/app/models/entity" "github.com/getfider/fider/app/models/enum" "github.com/getfider/fider/app/models/query" "github.com/getfider/fider/app/pkg/bus" + "github.com/getfider/fider/app/pkg/env" "github.com/getfider/fider/app/pkg/i18n" "github.com/gosimple/slug" @@ -29,14 +31,17 @@ type CreateNewPost struct { // OnPreExecute prefetches Tags for later use func (input *CreateNewPost) OnPreExecute(ctx context.Context) error { - input.Tags = make([]*entity.Tag, len(input.TagSlugs)) - for i, slug := range input.TagSlugs { - getTag := &query.GetTagBySlug{Slug: slug} - if err := bus.Dispatch(ctx, getTag); err != nil { - return err + fmt.Println(env.Config.PostCreationWithTagsEnabled) + if env.Config.PostCreationWithTagsEnabled { + input.Tags = make([]*entity.Tag, len(input.TagSlugs)) + for i, slug := range input.TagSlugs { + getTag := &query.GetTagBySlug{Slug: slug} + if err := bus.Dispatch(ctx, getTag); err != nil { + return err + } + + input.Tags[i] = getTag.Result } - - input.Tags[i] = getTag.Result } return nil @@ -46,7 +51,7 @@ func (input *CreateNewPost) OnPreExecute(ctx context.Context) error { func (action *CreateNewPost) IsAuthorized(ctx context.Context, user *entity.User) bool { if user == nil { return false - } else if !user.IsCollaborator() { + } else if env.Config.PostCreationWithTagsEnabled && !user.IsCollaborator() { for _, tag := range action.Tags { if !tag.IsPublic { return false diff --git a/app/handlers/apiv1/post.go b/app/handlers/apiv1/post.go index 2c57cbeb8..19f2631cd 100644 --- a/app/handlers/apiv1/post.go +++ b/app/handlers/apiv1/post.go @@ -8,6 +8,7 @@ import ( "github.com/getfider/fider/app/models/enum" "github.com/getfider/fider/app/models/query" "github.com/getfider/fider/app/pkg/bus" + "github.com/getfider/fider/app/pkg/env" "github.com/getfider/fider/app/pkg/web" "github.com/getfider/fider/app/tasks" ) @@ -60,10 +61,12 @@ func CreatePost() web.HandlerFunc { return c.Failure(err) } - for _, tag := range action.Tags { - assignTag := &cmd.AssignTag{Tag: tag, Post: newPost.Result} - if err := bus.Dispatch(c, assignTag); err != nil { - return c.Failure(err) + if env.Config.PostCreationWithTagsEnabled { + for _, tag := range action.Tags { + assignTag := &cmd.AssignTag{Tag: tag, Post: newPost.Result} + if err := bus.Dispatch(c, assignTag); err != nil { + return c.Failure(err) + } } } diff --git a/app/pkg/env/env.go b/app/pkg/env/env.go index 027036338..cb1327846 100644 --- a/app/pkg/env/env.go +++ b/app/pkg/env/env.go @@ -42,12 +42,13 @@ type config struct { WriteTimeout time.Duration `env:"HTTP_WRITE_TIMEOUT,default=10s,strict"` IdleTimeout time.Duration `env:"HTTP_IDLE_TIMEOUT,default=120s,strict"` } - Port string `env:"PORT,default=3000"` - HostMode string `env:"HOST_MODE,default=single"` - HostDomain string `env:"HOST_DOMAIN"` - BaseURL string `env:"BASE_URL"` - Locale string `env:"LOCALE,default=en"` - JWTSecret string `env:"JWT_SECRET,required"` + Port string `env:"PORT,default=3000"` + HostMode string `env:"HOST_MODE,default=single"` + HostDomain string `env:"HOST_DOMAIN"` + BaseURL string `env:"BASE_URL"` + Locale string `env:"LOCALE,default=en"` + JWTSecret string `env:"JWT_SECRET,required"` + PostCreationWithTagsEnabled bool `env:"POST_CREATION_WITH_TAGS_ENABLED,default=false"` Paddle struct { IsSandbox bool `env:"PADDLE_SANDBOX,default=false"` VendorID string `env:"PADDLE_VENDOR_ID"` From 7d4c70abadf98db732f877840b095e5efcc94587 Mon Sep 17 00:00:00 2001 From: Mercier Mateo Date: Wed, 11 Dec 2024 00:37:55 +0100 Subject: [PATCH 5/7] Move tag not found error handling to validate in order to get proper http 400 status code --- app/actions/post.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/actions/post.go b/app/actions/post.go index 18255a655..061b0d27f 100644 --- a/app/actions/post.go +++ b/app/actions/post.go @@ -3,7 +3,6 @@ package actions import ( "context" "time" - "fmt" "github.com/getfider/fider/app/models/dto" "github.com/getfider/fider/app/models/entity" @@ -31,16 +30,15 @@ type CreateNewPost struct { // OnPreExecute prefetches Tags for later use func (input *CreateNewPost) OnPreExecute(ctx context.Context) error { - fmt.Println(env.Config.PostCreationWithTagsEnabled) if env.Config.PostCreationWithTagsEnabled { - input.Tags = make([]*entity.Tag, len(input.TagSlugs)) - for i, slug := range input.TagSlugs { + input.Tags = make([]*entity.Tag, 0, len(input.TagSlugs)) + for _, slug := range input.TagSlugs { getTag := &query.GetTagBySlug{Slug: slug} if err := bus.Dispatch(ctx, getTag); err != nil { - return err + break } - input.Tags[i] = getTag.Result + input.Tags = append(input.Tags, getTag.Result) } } @@ -71,6 +69,8 @@ func (action *CreateNewPost) Validate(ctx context.Context, user *entity.User) *v result.AddFieldFailure("title", i18n.T(ctx, "validation.custom.descriptivetitle")) } else if len(action.Title) > 100 { result.AddFieldFailure("title", propertyMaxStringLen(ctx, "title", 100)) + } else if env.Config.PostCreationWithTagsEnabled && len(action.TagSlugs) != len(action.Tags) { + result.AddFieldFailure("tags", propertyIsInvalid(ctx, "tags")) } else { err := bus.Dispatch(ctx, &query.GetPostBySlug{Slug: slug.Make(action.Title)}) if err != nil && errors.Cause(err) != app.ErrNotFound { From 1a4712c2bc80b8cbb11e58ab8cd5a71fa1dbbd5b Mon Sep 17 00:00:00 2001 From: Mercier Mateo Date: Wed, 11 Dec 2024 00:38:10 +0100 Subject: [PATCH 6/7] Add tests for post creation with tags --- .test.env | 4 +- app/handlers/apiv1/post_test.go | 180 ++++++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+), 1 deletion(-) diff --git a/.test.env b/.test.env index f1e0a7123..f5d61a25f 100644 --- a/.test.env +++ b/.test.env @@ -8,6 +8,8 @@ JWT_SECRET=SOME_RANDOM_TOKEN_JUST_FOR_TESTING METRICS_ENABLED=true +POST_CREATION_WITH_TAGS_ENABLED=true + BLOB_STORAGE=s3 BLOB_STORAGE_S3_ENDPOINT_URL=http://localhost:9000 BLOB_STORAGE_S3_REGION=us-east-1 @@ -37,4 +39,4 @@ EMAIL_MAILGUN_API=mys3cr3tk3y EMAIL_MAILGUN_DOMAIN=mydomain.com USER_LIST_ENABLED=true -USER_LIST_APIKEY=abcdefg \ No newline at end of file +USER_LIST_APIKEY=abcdefg diff --git a/app/handlers/apiv1/post_test.go b/app/handlers/apiv1/post_test.go index 1bb77621e..614cf7476 100644 --- a/app/handlers/apiv1/post_test.go +++ b/app/handlers/apiv1/post_test.go @@ -17,6 +17,7 @@ import ( "github.com/getfider/fider/app/handlers/apiv1" . "github.com/getfider/fider/app/pkg/assert" "github.com/getfider/fider/app/pkg/bus" + "github.com/getfider/fider/app/pkg/env" "github.com/getfider/fider/app/pkg/mock" ) @@ -63,6 +64,185 @@ func TestCreatePostHandler_WithoutTitle(t *testing.T) { Expect(code).Equals(http.StatusBadRequest) } +func TestCreatePostHandler_WithInexistentTag(t *testing.T) { + if env.Config.PostCreationWithTagsEnabled { + RegisterT(t) + + bus.AddHandler(func(ctx context.Context, q *query.GetTagBySlug) error { + return app.ErrNotFound + }) + bus.AddHandler(func(ctx context.Context, q *query.GetPostBySlug) error { + return app.ErrNotFound + }) + + code, _ := mock.NewServer(). + OnTenant(mock.DemoTenant). + AsUser(mock.JonSnow). + ExecutePost(apiv1.CreatePost(), `{ "title": "My newest post :)", "tags": ["inexistent_tag"]}`) + + Expect(code).Equals(http.StatusBadRequest) + } +} + +func TestCreatePostHandler_WithPrivateTagAsVisitor(t *testing.T) { + if env.Config.PostCreationWithTagsEnabled { + RegisterT(t) + + privateTag := &entity.Tag{ + ID: 1, + Name: "private_tag", + Slug: "private_tag", + Color: "blue", + IsPublic: false, + } + bus.AddHandler(func(ctx context.Context, q *query.GetTagBySlug) error { + if q.Slug == "private_tag" { + q.Result = privateTag + return nil + } + return app.ErrNotFound + }) + + bus.AddHandler(func(ctx context.Context, q *query.GetPostBySlug) error { + return app.ErrNotFound + }) + + code, _ := mock.NewServer(). + OnTenant(mock.DemoTenant). + AsUser(mock.AryaStark). + ExecutePost(apiv1.CreatePost(), `{ "title": "My newest post :)", "tags": ["private_tag"]}`) + + Expect(code).Equals(http.StatusForbidden) + } +} + +func TestCreatePostHandler_WithPublicTagAsVisitor(t *testing.T) { + if env.Config.PostCreationWithTagsEnabled { + RegisterT(t) + + var newPost *cmd.AddNewPost + bus.AddHandler(func(ctx context.Context, c *cmd.AddNewPost) error { + newPost = c + c.Result = &entity.Post{ + ID: 1, + Title: c.Title, + Description: c.Description, + } + return nil + }) + + publicTag := &entity.Tag{ + ID: 1, + Name: "public_tag", + Slug: "public_tag", + Color: "red", + IsPublic: true, + } + bus.AddHandler(func(ctx context.Context, q *query.GetTagBySlug) error { + if q.Slug == "public_tag" { + q.Result = publicTag + return nil + } + return app.ErrNotFound + }) + + var tagAssignment *cmd.AssignTag + bus.AddHandler(func(ctx context.Context, c *cmd.AssignTag) error { + tagAssignment = c + return nil + }) + + bus.AddHandler(func(ctx context.Context, q *query.GetPostBySlug) error { + return app.ErrNotFound + }) + + bus.AddHandler(func(ctx context.Context, c *cmd.SetAttachments) error { return nil }) + bus.AddHandler(func(ctx context.Context, c *cmd.AddVote) error { return nil }) + bus.AddHandler(func(ctx context.Context, c *cmd.UploadImages) error { return nil }) + + code, _ := mock.NewServer(). + OnTenant(mock.DemoTenant). + AsUser(mock.AryaStark). + ExecutePost(apiv1.CreatePost(), `{ "title": "My newest post :)", "tags": ["public_tag"]}`) + + Expect(code).Equals(http.StatusOK) + Expect(tagAssignment.Tag).Equals(publicTag) + Expect(tagAssignment.Post).Equals(newPost.Result) + } +} + +func TestCreatePostHandler_WithPublicTagAndPrivateTagAsCollaborator(t *testing.T) { + if env.Config.PostCreationWithTagsEnabled { + RegisterT(t) + + var newPost *cmd.AddNewPost + bus.AddHandler(func(ctx context.Context, c *cmd.AddNewPost) error { + newPost = c + c.Result = &entity.Post{ + ID: 1, + Title: c.Title, + Description: c.Description, + } + return nil + }) + + publicTag := &entity.Tag{ + ID: 1, + Name: "public_tag", + Slug: "public_tag", + Color: "red", + IsPublic: true, + } + privateTag := &entity.Tag{ + ID: 1, + Name: "private_tag", + Slug: "private_tag", + Color: "blue", + IsPublic: false, + } + bus.AddHandler(func(ctx context.Context, q *query.GetTagBySlug) error { + if q.Slug == "public_tag" { + q.Result = publicTag + return nil + } + if q.Slug == "private_tag" { + q.Result = privateTag + return nil + } + return app.ErrNotFound + }) + + tagAssignments := make([]*cmd.AssignTag, 2) + bus.AddHandler(func(ctx context.Context, c *cmd.AssignTag) error { + if c.Tag.Slug == "public_tag" { + tagAssignments[0] = c + } else if c.Tag.Slug == "private_tag" { + tagAssignments[1] = c + } + return nil + }) + + bus.AddHandler(func(ctx context.Context, q *query.GetPostBySlug) error { + return app.ErrNotFound + }) + + bus.AddHandler(func(ctx context.Context, c *cmd.SetAttachments) error { return nil }) + bus.AddHandler(func(ctx context.Context, c *cmd.AddVote) error { return nil }) + bus.AddHandler(func(ctx context.Context, c *cmd.UploadImages) error { return nil }) + + code, _ := mock.NewServer(). + OnTenant(mock.DemoTenant). + AsUser(mock.JonSnow). + ExecutePost(apiv1.CreatePost(), `{ "title": "My newest post :)", "tags": ["public_tag", "private_tag"]}`) + + Expect(code).Equals(http.StatusOK) + Expect(tagAssignments[0].Tag).Equals(publicTag) + Expect(tagAssignments[1].Tag).Equals(privateTag) + Expect(tagAssignments[0].Post).Equals(newPost.Result) + Expect(tagAssignments[1].Post).Equals(newPost.Result) + } +} + func TestGetPostHandler(t *testing.T) { RegisterT(t) From 513d517d138b1ccdf5958b86d16a1143666b46b8 Mon Sep 17 00:00:00 2001 From: MercierMateo Date: Tue, 17 Dec 2024 00:04:29 +0100 Subject: [PATCH 7/7] Fix grammar on test Co-authored-by: Matt Roberts --- app/handlers/apiv1/post_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/handlers/apiv1/post_test.go b/app/handlers/apiv1/post_test.go index 614cf7476..27839eb67 100644 --- a/app/handlers/apiv1/post_test.go +++ b/app/handlers/apiv1/post_test.go @@ -64,7 +64,7 @@ func TestCreatePostHandler_WithoutTitle(t *testing.T) { Expect(code).Equals(http.StatusBadRequest) } -func TestCreatePostHandler_WithInexistentTag(t *testing.T) { +func TestCreatePostHandler_WithNonExistentTag(t *testing.T) { if env.Config.PostCreationWithTagsEnabled { RegisterT(t)