From f8d5a31b5216909c518b011645b8293de909a01a Mon Sep 17 00:00:00 2001 From: Vartan Benohanian Date: Sat, 11 Jul 2020 01:37:19 -0400 Subject: [PATCH] Finish MultiService + all tests for it Signed-off-by: Vartan Benohanian --- multi.go | 157 +++++++++++++++--- multi_test.go | 272 ++++++++++++++++++++++++++++++++ testdata/multi/description.json | 7 + testdata/multi/multi.json | 31 ++++ testdata/multi/multis.json | 64 ++++++++ 5 files changed, 512 insertions(+), 19 deletions(-) create mode 100644 multi_test.go create mode 100644 testdata/multi/description.json create mode 100644 testdata/multi/multi.json create mode 100644 testdata/multi/multis.json diff --git a/multi.go b/multi.go index 32fb106..e789b30 100644 --- a/multi.go +++ b/multi.go @@ -24,15 +24,16 @@ type multiRoot struct { // Users can create multis for custom navigation, instead of browsing // one subreddit or all subreddits at a time. type Multi struct { - Name string `json:"name,omitempty"` - DisplayName string `json:"display_name,omitempty"` + Name string `json:"name,omitempty"` + DisplayName string `json:"display_name,omitempty"` + // Format: user/{username}/m/{multiname} Path string `json:"path,omitempty"` Description string `json:"description_md,omitempty"` Subreddits SubredditNames `json:"subreddits"` - CopedFrom *string `json:"copied_from"` + CopiedFrom *string `json:"copied_from"` Owner string `json:"owner,omitempty"` - OwnerID string `json:"ownerID,omitempty"` + OwnerID string `json:"owner_id,omitempty"` Created *Timestamp `json:"created_utc,omitempty"` NumberOfSubscribers int `json:"num_subscribers"` @@ -66,10 +67,23 @@ func (n *SubredditNames) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON implements the json.Marshaler interface. +func (n *SubredditNames) MarshalJSON() ([]byte, error) { + var subreddits []map[string]string + + for _, sr := range *n { + subreddits = append(subreddits, map[string]string{ + "name": sr, + }) + } + + return json.Marshal(subreddits) +} + // MultiCopyRequest represents a request to copy a multireddit. type MultiCopyRequest struct { - From string - To string + FromPath string + ToPath string // Raw markdown text. Description string // No longer than 50 characters. @@ -79,29 +93,38 @@ type MultiCopyRequest struct { // Form parameterizes the fields and returns the form. func (r *MultiCopyRequest) Form() url.Values { form := url.Values{} - form.Set("from", r.From) - form.Set("to", r.To) + form.Set("from", r.FromPath) + form.Set("to", r.ToPath) form.Set("description_md", r.Description) form.Set("display_name", r.DisplayName) return form } -// MultiCreateRequest represents a request to create a multireddit. -type MultiCreateRequest struct { - Description string `json:"description_md,omitempty"` - DisplayName string `json:"display_name,omitempty"` - Subreddits []string `json:"subreddits"` - Visibility string `json:"visibility,omitempty"` +// MultiCreateOrUpdateRequest represents a request to create/update a multireddit. +type MultiCreateOrUpdateRequest struct { + // For updates, this is the display name, i.e. the header of the multi. + // Not part of the path necessarily. + Name string `json:"display_name,omitempty"` + Description string `json:"description_md,omitempty"` + Subreddits SubredditNames `json:"subreddits,omitempty"` + // One of: private, public, hidden + Visibility string `json:"visibility,omitempty"` } // Form parameterizes the fields and returns the form. -func (r *MultiCreateRequest) Form() url.Values { +func (r *MultiCreateOrUpdateRequest) Form() url.Values { byteValue, _ := json.Marshal(r) form := url.Values{} form.Set("model", string(byteValue)) return form } +type rootMultiDescription struct { + Data struct { + Body string `json:"body_md"` + } `json:"data"` +} + // Get gets information about the multireddit from its url path. func (s *MultiService) Get(ctx context.Context, multiPath string) (*Multi, *Response, error) { path := fmt.Sprintf("api/multi/%s", multiPath) @@ -144,6 +167,7 @@ func (s *MultiService) Mine(ctx context.Context) ([]Multi, *Response, error) { } // Of returns the user's public multireddits. +// Or, if the user is you, all of your multireddits. func (s *MultiService) Of(ctx context.Context, username string) ([]Multi, *Response, error) { path := fmt.Sprintf("api/multi/user/%s", username) @@ -172,7 +196,7 @@ func (s *MultiService) Copy(ctx context.Context, copyRequest *MultiCopyRequest) return nil, nil, errors.New("copyRequest cannot be nil") } - path := fmt.Sprintf("api/multi/copy") + path := "api/multi/copy" req, err := s.client.NewPostForm(path, copyRequest.Form()) if err != nil { @@ -189,14 +213,14 @@ func (s *MultiService) Copy(ctx context.Context, copyRequest *MultiCopyRequest) } // Create creates a multireddit. -func (s *MultiService) Create(ctx context.Context, createRequest *MultiCreateRequest) (*Multi, *Response, error) { +func (s *MultiService) Create(ctx context.Context, createRequest *MultiCreateOrUpdateRequest) (*Multi, *Response, error) { if createRequest == nil { return nil, nil, errors.New("createRequest cannot be nil") } - path := fmt.Sprintf("api/multi/copy") + path := "api/multi" - req, err := s.client.NewRequest(http.MethodPost, path, createRequest) + req, err := s.client.NewPostForm(path, createRequest.Form()) if err != nil { return nil, nil, err } @@ -210,6 +234,31 @@ func (s *MultiService) Create(ctx context.Context, createRequest *MultiCreateReq return root.Data, resp, nil } +// Update updates a multireddit. +// If the multireddit does not exist, it will be created. +func (s *MultiService) Update(ctx context.Context, multiPath string, updateRequest *MultiCreateOrUpdateRequest) (*Multi, *Response, error) { + if updateRequest == nil { + return nil, nil, errors.New("updateRequest cannot be nil") + } + + path := fmt.Sprintf("api/multi/%s", multiPath) + + req, err := s.client.NewPostForm(path, updateRequest.Form()) + if err != nil { + return nil, nil, err + } + // todo: fix this + req.Method = http.MethodPut + + root := new(multiRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Data, resp, nil +} + // Delete deletes a multireddit. func (s *MultiService) Delete(ctx context.Context, multiPath string) (*Response, error) { path := fmt.Sprintf("api/multi/%s", multiPath) @@ -221,3 +270,73 @@ func (s *MultiService) Delete(ctx context.Context, multiPath string) (*Response, return s.client.Do(ctx, req, nil) } + +// GetDescription gets a multireddit's description. +func (s *MultiService) GetDescription(ctx context.Context, multiPath string) (string, *Response, error) { + path := fmt.Sprintf("api/multi/%s/description", multiPath) + + req, err := s.client.NewRequest(http.MethodGet, path, nil) + if err != nil { + return "", nil, err + } + + root := new(rootMultiDescription) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return "", resp, err + } + + return root.Data.Body, resp, nil +} + +// UpdateDescription updates a multireddit's description. +func (s *MultiService) UpdateDescription(ctx context.Context, multiPath string, description string) (string, *Response, error) { + path := fmt.Sprintf("api/multi/%s/description", multiPath) + + form := url.Values{} + form.Set("model", fmt.Sprintf(`{"body_md":"%s"}`, description)) + + req, err := s.client.NewPostForm(path, form) + if err != nil { + return "", nil, err + } + // todo: fix this + req.Method = http.MethodPut + + root := new(rootMultiDescription) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return "", resp, err + } + + return root.Data.Body, resp, nil +} + +// AddSubreddit adds a subreddit to a multireddit. +func (s *MultiService) AddSubreddit(ctx context.Context, multiPath string, subreddit string) (*Response, error) { + path := fmt.Sprintf("api/multi/%s/r/%s", multiPath, subreddit) + + form := url.Values{} + form.Set("model", fmt.Sprintf(`{"name":"%s"}`, subreddit)) + + req, err := s.client.NewPostForm(path, form) + if err != nil { + return nil, err + } + // todo: fix this + req.Method = http.MethodPut + + return s.client.Do(ctx, req, nil) +} + +// DeleteSubreddit removes a subreddit from a multireddit. +func (s *MultiService) DeleteSubreddit(ctx context.Context, multiPath string, subreddit string) (*Response, error) { + path := fmt.Sprintf("api/multi/%s/r/%s", multiPath, subreddit) + + req, err := s.client.NewRequest(http.MethodDelete, path, nil) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} diff --git a/multi_test.go b/multi_test.go new file mode 100644 index 0000000..6d11290 --- /dev/null +++ b/multi_test.go @@ -0,0 +1,272 @@ +package geddit + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +var expectedMulti = &Multi{ + Name: "test", + DisplayName: "test", + Path: "/user/v_95/m/test/", + Subreddits: []string{"nba", "golang"}, + CopiedFrom: nil, + + Owner: "v_95", + OwnerID: "t2_164ab8", + Created: &Timestamp{time.Date(2020, 7, 11, 4, 55, 12, 0, time.UTC)}, + + NumberOfSubscribers: 0, + Visibility: "private", + CanEdit: true, +} + +var expectedMulti2 = &Multi{ + Name: "test2", + DisplayName: "test2", + Path: "/user/v_95/m/test2/", + Subreddits: []string{"redditdev", "test"}, + CopiedFrom: nil, + + Owner: "v_95", + OwnerID: "t2_164ab8", + Created: &Timestamp{time.Date(2020, 7, 11, 4, 57, 3, 0, time.UTC)}, + + NumberOfSubscribers: 0, + Visibility: "private", + CanEdit: true, +} + +func TestMultiService_Get(t *testing.T) { + setup() + defer teardown() + + blob := readFileContents(t, "testdata/multi/multi.json") + + mux.HandleFunc("/api/multi/user/testuser/m/testmulti", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + fmt.Fprint(w, blob) + }) + + multi, _, err := client.Multi.Get(ctx, "user/testuser/m/testmulti") + assert.NoError(t, err) + assert.Equal(t, expectedMulti, multi) +} + +func TestMultiService_Mine(t *testing.T) { + setup() + defer teardown() + + blob := readFileContents(t, "testdata/multi/multis.json") + + mux.HandleFunc("/api/multi/mine", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + fmt.Fprint(w, blob) + }) + + multis, _, err := client.Multi.Mine(ctx) + assert.NoError(t, err) + assert.Equal(t, []Multi{*expectedMulti, *expectedMulti2}, multis) +} + +func TestMultiService_Of(t *testing.T) { + setup() + defer teardown() + + blob := readFileContents(t, "testdata/multi/multis.json") + + mux.HandleFunc("/api/multi/user/test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + fmt.Fprint(w, blob) + }) + + multis, _, err := client.Multi.Of(ctx, "test") + assert.NoError(t, err) + assert.Equal(t, []Multi{*expectedMulti, *expectedMulti2}, multis) +} + +func TestMultiService_Copy(t *testing.T) { + setup() + defer teardown() + + blob := readFileContents(t, "testdata/multi/multi.json") + + mux.HandleFunc("/api/multi/copy", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method) + + form := url.Values{} + form.Set("from", "user/testuser/m/testmulti") + form.Set("to", "user/testuser2/m/testmulti2") + form.Set("description_md", "this is a multireddit") + form.Set("display_name", "hello") + + err := r.ParseForm() + assert.NoError(t, err) + assert.Equal(t, form, r.Form) + + fmt.Fprint(w, blob) + }) + + multi, _, err := client.Multi.Copy(ctx, &MultiCopyRequest{ + FromPath: "user/testuser/m/testmulti", + ToPath: "user/testuser2/m/testmulti2", + Description: "this is a multireddit", + DisplayName: "hello", + }) + assert.NoError(t, err) + assert.Equal(t, expectedMulti, multi) +} + +func TestMultiService_Create(t *testing.T) { + setup() + defer teardown() + + blob := readFileContents(t, "testdata/multi/multi.json") + createRequest := &MultiCreateOrUpdateRequest{ + Name: "testmulti", + Description: "this is a multireddit", + Subreddits: []string{"golang"}, + Visibility: "public", + } + + mux.HandleFunc("/api/multi", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method) + + err := r.ParseForm() + assert.NoError(t, err) + + model := r.Form.Get("model") + + expectedCreateRequest := new(MultiCreateOrUpdateRequest) + err = json.Unmarshal([]byte(model), expectedCreateRequest) + assert.NoError(t, err) + assert.Equal(t, expectedCreateRequest, createRequest) + + fmt.Fprint(w, blob) + }) + + multi, _, err := client.Multi.Create(ctx, createRequest) + assert.NoError(t, err) + assert.Equal(t, expectedMulti, multi) +} + +func TestMultiService_Update(t *testing.T) { + setup() + defer teardown() + + blob := readFileContents(t, "testdata/multi/multi.json") + updateRequest := &MultiCreateOrUpdateRequest{ + Name: "testmulti", + Description: "this is a multireddit", + Visibility: "public", + } + + mux.HandleFunc("/api/multi/user/testuser/m/testmulti", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPut, r.Method) + + err := r.ParseForm() + assert.NoError(t, err) + + model := r.Form.Get("model") + + expectedCreateRequest := new(MultiCreateOrUpdateRequest) + err = json.Unmarshal([]byte(model), expectedCreateRequest) + assert.NoError(t, err) + assert.Equal(t, expectedCreateRequest, updateRequest) + + fmt.Fprint(w, blob) + }) + + multi, _, err := client.Multi.Update(ctx, "user/testuser/m/testmulti", updateRequest) + assert.NoError(t, err) + assert.Equal(t, expectedMulti, multi) +} + +func TestMultiService_Delete(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/api/multi/user/testuser/m/testmulti", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodDelete, r.Method) + }) + + _, err := client.Multi.Delete(ctx, "user/testuser/m/testmulti") + assert.NoError(t, err) +} + +func TestMultiService_GetDescription(t *testing.T) { + setup() + defer teardown() + + blob := readFileContents(t, "testdata/multi/description.json") + + mux.HandleFunc("/api/multi/user/testuser/m/testmulti/description", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + fmt.Fprint(w, blob) + }) + + description, _, err := client.Multi.GetDescription(ctx, "user/testuser/m/testmulti") + assert.NoError(t, err) + assert.Equal(t, "hello world", description) +} + +func TestMultiService_UpdateDescription(t *testing.T) { + setup() + defer teardown() + + blob := readFileContents(t, "testdata/multi/description.json") + + mux.HandleFunc("/api/multi/user/testuser/m/testmulti/description", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPut, r.Method) + + form := url.Values{} + form.Set("model", `{"body_md":"hello world"}`) + + err := r.ParseForm() + assert.NoError(t, err) + assert.Equal(t, form, r.Form) + + fmt.Fprint(w, blob) + }) + + description, _, err := client.Multi.UpdateDescription(ctx, "user/testuser/m/testmulti", "hello world") + assert.NoError(t, err) + assert.Equal(t, "hello world", description) +} + +func TestMultiService_AddSubreddit(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/api/multi/user/testuser/m/testmulti/r/golang", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPut, r.Method) + + form := url.Values{} + form.Set("model", `{"name":"golang"}`) + + err := r.ParseForm() + assert.NoError(t, err) + assert.Equal(t, form, r.Form) + }) + + _, err := client.Multi.AddSubreddit(ctx, "user/testuser/m/testmulti", "golang") + assert.NoError(t, err) +} + +func TestMultiService_DeleteSubreddit(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/api/multi/user/testuser/m/testmulti/r/golang", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodDelete, r.Method) + }) + + _, err := client.Multi.DeleteSubreddit(ctx, "user/testuser/m/testmulti", "golang") + assert.NoError(t, err) +} diff --git a/testdata/multi/description.json b/testdata/multi/description.json new file mode 100644 index 0000000..16111f4 --- /dev/null +++ b/testdata/multi/description.json @@ -0,0 +1,7 @@ +{ + "kind": "LabeledMultiDescription", + "data": { + "body_html": "<!-- SC_OFF --><div class=\"md\"><p>hello world</p>\n</div><!-- SC_ON -->", + "body_md": "hello world" + } +} diff --git a/testdata/multi/multi.json b/testdata/multi/multi.json new file mode 100644 index 0000000..1578942 --- /dev/null +++ b/testdata/multi/multi.json @@ -0,0 +1,31 @@ +{ + "kind": "LabeledMulti", + "data": { + "can_edit": true, + "display_name": "test", + "name": "test", + "description_html": "", + "num_subscribers": 0, + "copied_from": null, + "icon_url": "https://www.redditstatic.com/custom_feeds/custom_feed_default_3.png", + "subreddits": [ + { + "name": "nba" + }, + { + "name": "golang" + } + ], + "created_utc": 1594443312.0, + "visibility": "private", + "created": 1594472112.0, + "over_18": false, + "path": "/user/v_95/m/test/", + "owner": "v_95", + "key_color": null, + "is_subscriber": false, + "owner_id": "t2_164ab8", + "description_md": "", + "is_favorited": false + } +} diff --git a/testdata/multi/multis.json b/testdata/multi/multis.json new file mode 100644 index 0000000..234fadb --- /dev/null +++ b/testdata/multi/multis.json @@ -0,0 +1,64 @@ +[ + { + "kind": "LabeledMulti", + "data": { + "can_edit": true, + "display_name": "test", + "name": "test", + "description_html": "", + "num_subscribers": 0, + "copied_from": null, + "icon_url": "https://www.redditstatic.com/custom_feeds/custom_feed_default_3.png", + "subreddits": [ + { + "name": "nba" + }, + { + "name": "golang" + } + ], + "created_utc": 1594443312.0, + "visibility": "private", + "created": 1594472112.0, + "over_18": false, + "path": "/user/v_95/m/test/", + "owner": "v_95", + "key_color": null, + "is_subscriber": false, + "owner_id": "t2_164ab8", + "description_md": "", + "is_favorited": false + } + }, + { + "kind": "LabeledMulti", + "data": { + "can_edit": true, + "display_name": "test2", + "name": "test2", + "description_html": "", + "num_subscribers": 0, + "copied_from": null, + "icon_url": "https://www.redditstatic.com/custom_feeds/custom_feed_default_5.png", + "subreddits": [ + { + "name": "redditdev" + }, + { + "name": "test" + } + ], + "created_utc": 1594443423.0, + "visibility": "private", + "created": 1594472223.0, + "over_18": false, + "path": "/user/v_95/m/test2/", + "owner": "v_95", + "key_color": null, + "is_subscriber": false, + "owner_id": "t2_164ab8", + "description_md": "", + "is_favorited": false + } + } +]