Repository: sivchari/gotwtr
Pull request #49 (2021-10-15 02:11:53)

impl deleteRetweet and postRetweet
relate to #39 

sivchari: looks good, thanks!

Patch:
diff --git a/client.go b/client.go
index 2d659fc..1d8d22c 100644
--- a/client.go
+++ b/client.go
@@ -19,10 +19,12 @@ const (
 type Client interface {
 	// CountsFullArchiveTweet(ctx context.Context, query string, opt ...*TweetCountsOption) (*TweetCountsResponse, error)
 	CountsRecentTweet(ctx context.Context, query string, opt ...*TweetCountsOption) (*TweetCountsResponse, error)
+	DeleteRetweet(ctx context.Context, id string, stid string) (*DeleteRetweetResponse, error)
 	LookUpSpaces(ctx context.Context, ids []string, opt ...*SpaceLookUpOption) (*SpaceLookUpResponse, error)
 	LookUpSpaceByID(ctx context.Context, id string, opt ...*SpaceLookUpOption) (*SpaceLookUpByIDResponse, error)
 	LookUpTweets(ctx context.Context, ids []string, opt ...*TweetLookUpOption) (*TweetLookUpResponse, error)
 	LookUpTweetByID(ctx context.Context, id string, opt ...*TweetLookUpOption) (*TweetLookUpByIDResponse, error)
+	PostRetweet(ctx context.Context, uid string, tid string) (*PostRetweetResponse, error)
 	RetweetsLookup(ctx context.Context, id string, opt ...*RetweetsLookupOpts) (*RetweetsLookupResponse, error)
 	SampledStream(ctx context.Context, opt ...*SampledStreamOpts) (*SampledStreamResponse, error)
 	// SearchFullArchiveTweets(ctx context.Context, query string, opt ...*TweetSearchOption) (*TweetSearchResponse, error)
@@ -63,6 +65,10 @@ func (c *client) CountsRecentTweet(ctx context.Context, query string, opt ...*Tw
 	return countsRecentTweet(ctx, c, query, opt...)
 }
 
+func (c *client) DeleteRetweet(ctx context.Context, id string, stid string) (*DeleteRetweetResponse, error) {
+	return deleteRetweet(ctx, c, id, stid)
+}
+
 func (c *client) LookUpSpaces(ctx context.Context, ids []string, opt ...*SpaceLookUpOption) (*SpaceLookUpResponse, error) {
 	return lookUpSpaces(ctx, c, ids, opt...)
 }
@@ -79,6 +85,10 @@ func (c *client) LookUpTweetByID(ctx context.Context, id string, opt ...*TweetLo
 	return lookUpTweetByID(ctx, c, id, opt...)
 }
 
+func (c *client) PostRetweet(ctx context.Context, uid string, tid string) (*PostRetweetResponse, error) {
+	return postRetweet(ctx, c, uid, tid)
+}
+
 func (c *client) RetweetsLookup(ctx context.Context, id string, opt ...*RetweetsLookupOpts) (*RetweetsLookupResponse, error) {
 	return retweetsLookup(ctx, c, id, opt...)
 }
diff --git a/endpoint.go b/endpoint.go
index fd9a283..bd7e246 100644
--- a/endpoint.go
+++ b/endpoint.go
@@ -2,8 +2,9 @@ package gotwtr
 
 // EndpointURL is the base URL for the Twitter V2 API.
 var (
+	baseUserPath      = "https://api.twitter.com/2/users"
+	baseTweetPath     = "https://api.twitter.com/2/tweets"
 	recentTweetCounts = "https://api.twitter.com/2/tweets/counts/recent"
-	retweetLookUp     = "https://api.twitter.com/2/tweets"
 	sampleStream      = "https://api.twitter.com/2/tweets/sample/stream"
 	spaceLookUp       = "https://api.twitter.com/2/spaces"
 	spacesSearch      = "https://api.twitter.com/2/spaces/search"
diff --git a/example/tweet/retweets/main.go b/example/tweet/retweets/main.go
index 4a13c9f..6ab5cde 100644
--- a/example/tweet/retweets/main.go
+++ b/example/tweet/retweets/main.go
@@ -15,4 +15,18 @@ func main() {
 		panic(err)
 	}
 	fmt.Println(t)
+
+	// post retweet
+	p, err := client.PostRetweet(context.Background(), "user_id", "tweet_id")
+	if err != nil {
+		panic(err)
+	}
+	fmt.Println(p)
+
+	// delete retweet
+	d, err := client.DeleteRetweet(context.Background(), "id", "source_tweet_id")
+	if err != nil {
+		panic(err)
+	}
+	fmt.Println(d)
 }
diff --git a/retweets.go b/retweets.go
index 9480dba..ed61739 100644
--- a/retweets.go
+++ b/retweets.go
@@ -1,6 +1,7 @@
 package gotwtr
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"errors"
@@ -12,7 +13,7 @@ func retweetsLookup(ctx context.Context, c *client, id string, opt ...*RetweetsL
 	if id == "" {
 		return nil, errors.New("retweets lookup by id: id parameter is required")
 	}
-	retweetsLookupPath := retweetLookUp + "/" + id + "/retweeted_by"
+	retweetsLookupPath := baseTweetPath + "/" + id + "/retweeted_by"
 
 	req, err := http.NewRequestWithContext(ctx, http.MethodGet, retweetsLookupPath, nil)
 	if err != nil {
@@ -51,3 +52,86 @@ func retweetsLookup(ctx context.Context, c *client, id string, opt ...*RetweetsL
 
 	return &retweetsLookup, nil
 }
+
+// uid = "user_id" tid = "tweet_id"
+func postRetweet(ctx context.Context, c *client, uid string, tid string) (*PostRetweetResponse, error) {
+	if uid == "" {
+		return nil, errors.New("post retweet by uid: uid parameter is required")
+	}
+	postRetweetPath := baseUserPath + "/" + uid + "/retweets"
+
+	if tid == "" {
+		return nil, errors.New("post retweet by tid: tid parameter is required")
+	}
+	body := &TweetBody{
+		TweetID: tid,
+	}
+	jsonStr, err := json.Marshal(body)
+	if err != nil {
+		return nil, errors.New("post retweet: can not marshal")
+	}
+
+	req, err := http.NewRequestWithContext(ctx, http.MethodPost, postRetweetPath, bytes.NewBuffer(jsonStr))
+	if err != nil {
+		return nil, fmt.Errorf("post retweet new request with ctx: %w", err)
+	}
+	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.bearerToken))
+	req.Header.Set("Content-Type", "application/json")
+
+	resp, err := c.client.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("post retweet response: %w", err)
+	}
+	defer resp.Body.Close()
+
+	var postRetweet PostRetweetResponse
+	if err := json.NewDecoder(resp.Body).Decode(&postRetweet); err != nil {
+		return nil, fmt.Errorf("post retweet decode: %w", err)
+	}
+	if resp.StatusCode != http.StatusOK {
+		return &postRetweet, &HTTPError{
+			APIName: "post retweet",
+			Status:  resp.Status,
+			URL:     req.URL.String(),
+		}
+	}
+
+	return &postRetweet, nil
+}
+
+// stid = "source_tweet_id"
+func deleteRetweet(ctx context.Context, c *client, id string, stid string) (*DeleteRetweetResponse, error) {
+	if id == "" {
+		return nil, errors.New("delete retweet by id: id parameter is required")
+	}
+	if stid == "" {
+		return nil, errors.New("delete retweet by source_tweet_id: source_tweet_id parameter is required")
+	}
+	deleteRetweetPath := baseUserPath + "/" + id + "/retweets/" + stid
+
+	req, err := http.NewRequestWithContext(ctx, http.MethodDelete, deleteRetweetPath, nil)
+	if err != nil {
+		return nil, fmt.Errorf("delete retweet new request with ctx: %w", err)
+	}
+	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.bearerToken))
+
+	resp, err := c.client.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("delete retweet response: %w", err)
+	}
+	defer resp.Body.Close()
+
+	var deleteRetweet DeleteRetweetResponse
+	if err := json.NewDecoder(resp.Body).Decode(&deleteRetweet); err != nil {
+		return nil, fmt.Errorf("delete retweet decode: %w", err)
+	}
+	if resp.StatusCode != http.StatusOK {
+		return &deleteRetweet, &HTTPError{
+			APIName: "delete retweet",
+			Status:  resp.Status,
+			URL:     req.URL.String(),
+		}
+	}
+
+	return &deleteRetweet, nil
+}
diff --git a/tweet.go b/tweet.go
index 5f5b072..734ca4d 100644
--- a/tweet.go
+++ b/tweet.go
@@ -239,3 +239,21 @@ type RetweetsLookupResponse struct {
 type RetweetsLookupMeta struct {
 	ResultCount int `json:"result_count"`
 }
+
+type PostRetweetResponse struct {
+	Retweeted *Retweeted          `json:"data,omitempty"`
+	Errors    []*APIResponseError `json:"errors,omitempty"`
+}
+
+type DeleteRetweetResponse struct {
+	Retweeted *Retweeted          `json:"data,omitempty"`
+	Errors    []*APIResponseError `json:"errors,omitempty"`
+}
+
+type Retweeted struct {
+	Retweeted bool `json:"retweeted"`
+}
+
+type TweetBody struct {
+	TweetID string `json:"tweet_id"`
+}


Test patch:
diff --git a/retweets_test.go b/retweets_test.go
index e4f9e6d..68b017a 100644
--- a/retweets_test.go
+++ b/retweets_test.go
@@ -160,6 +160,47 @@ func Test_retweetsLookup(t *testing.T) {
 			},
 			wantErr: false,
 		},
+		{
+			name: "404 not found",
+			args: args{
+				ctx: context.Background(),
+				client: mockHTTPClient(func(req *http.Request) *http.Response {
+					body := `{
+						"errors":[
+							{
+								"value":"11111111111111111",
+								"detail":"Could not find tweet with id: [11111111111111111].",
+								"title":"Not Found Error",
+								"resource_type":"tweet",
+								"parameter":"id",
+								"resource_id":"11111111111111111",
+								"type":"https://api.twitter.com/2/problems/resource-not-found"
+							}
+						]
+					}`
+					return &http.Response{
+						StatusCode: http.StatusNotFound,
+						Body:       io.NopCloser(strings.NewReader(body)),
+					}
+				}),
+				id: "11111111111111111",
+			},
+			want: &gotwtr.RetweetsLookupResponse{
+				Users: nil,
+				Errors: []*gotwtr.APIResponseError{
+					{
+						Value:        "11111111111111111",
+						Detail:       "Could not find tweet with id: [11111111111111111].",
+						Title:        "Not Found Error",
+						ResourceType: "tweet",
+						Parameter:    "id",
+						ResourceID:   "11111111111111111",
+						Type:         "https://api.twitter.com/2/problems/resource-not-found",
+					},
+				},
+			},
+			wantErr: true,
+		},
 	}
 	for i, tt := range tests {
 		tt := tt
@@ -178,3 +219,181 @@ func Test_retweetsLookup(t *testing.T) {
 		})
 	}
 }
+
+func Test_postRetweet(t *testing.T) {
+	type args struct {
+		ctx    context.Context
+		client *http.Client
+		uid    string
+		tid    string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    *gotwtr.PostRetweetResponse
+		wantErr bool
+	}{
+		{
+			name: "200 success",
+			args: args{
+				ctx: context.Background(),
+				client: mockHTTPClient(func(req *http.Request) *http.Response {
+					if req.Method != http.MethodPost {
+						t.Fatalf("the method is not correct got %s want %s", req.Method, http.MethodPost)
+					}
+					body := `{
+						"data": {
+							"retweeted": true
+						}
+					}`
+					return &http.Response{
+						StatusCode: http.StatusOK,
+						Body:       io.NopCloser(strings.NewReader(body)),
+					}
+				}),
+				uid: "2244994945",
+				tid: "1228393702244134912",
+			},
+			want: &gotwtr.PostRetweetResponse{
+				Retweeted: &gotwtr.Retweeted{
+					Retweeted: true,
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "400 request failed",
+			args: args{
+				ctx: context.Background(),
+				client: mockHTTPClient(func(req *http.Request) *http.Response {
+					body := `{
+						"errors": [
+							{
+								"message":"Sorry, that page does not exist, code:34"
+							}
+						]
+					}`
+					return &http.Response{
+						StatusCode: http.StatusBadRequest,
+						Body:       io.NopCloser(strings.NewReader(body)),
+					}
+				}),
+				uid: "2244994945",
+				tid: "1228393702244134912",
+			},
+			want: &gotwtr.PostRetweetResponse{
+				Errors: []*gotwtr.APIResponseError{
+					{
+						Message: "Sorry, that page does not exist, code:34",
+					},
+				},
+			},
+			wantErr: true,
+		},
+	}
+	for i, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			c := gotwtr.New("test-key", gotwtr.WithHTTPClient(tt.args.client))
+			got, err := c.PostRetweet(tt.args.ctx, tt.args.uid, tt.args.tid)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("PostRetweet() index = %v error = %v, wantErr %v", i, err, tt.wantErr)
+				return
+			}
+			if diff := cmp.Diff(got, tt.want); diff != "" {
+				t.Errorf("PostRetweet() index = %v mismatch (-want +got):\n%s", i, diff)
+				return
+			}
+		})
+	}
+}
+
+func Test_deleteRetweet(t *testing.T) {
+	type args struct {
+		ctx    context.Context
+		client *http.Client
+		id     string
+		stid   string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    *gotwtr.DeleteRetweetResponse
+		wantErr bool
+	}{
+		{
+			name: "200 success",
+			args: args{
+				ctx: context.Background(),
+				client: mockHTTPClient(func(req *http.Request) *http.Response {
+					if req.Method != http.MethodDelete {
+						t.Fatalf("the method is not correct got %s want %s", req.Method, http.MethodDelete)
+					}
+					body := `{
+						"data": {
+							"retweeted": false
+						}
+					}`
+					return &http.Response{
+						StatusCode: http.StatusOK,
+						Body:       io.NopCloser(strings.NewReader(body)),
+					}
+				}),
+				id:   "2244994945",
+				stid: "1228393702244134912",
+			},
+			want: &gotwtr.DeleteRetweetResponse{
+				Retweeted: &gotwtr.Retweeted{
+					Retweeted: false,
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "400 request failed",
+			args: args{
+				ctx: context.Background(),
+				client: mockHTTPClient(func(req *http.Request) *http.Response {
+					body := `{
+						"errors": [
+							{
+								"message":"Sorry, that page does not exist, code:34"
+							}
+						]
+					}`
+					return &http.Response{
+						StatusCode: http.StatusBadRequest,
+						Body:       io.NopCloser(strings.NewReader(body)),
+					}
+				}),
+				id:   "2244994945",
+				stid: "1228393702244134912",
+			},
+			want: &gotwtr.DeleteRetweetResponse{
+				Errors: []*gotwtr.APIResponseError{
+					{
+						Message: "Sorry, that page does not exist, code:34",
+					},
+				},
+			},
+			wantErr: true,
+		},
+	}
+	for i, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			c := gotwtr.New("test-key", gotwtr.WithHTTPClient(tt.args.client))
+			got, err := c.DeleteRetweet(tt.args.ctx, tt.args.id, tt.args.stid)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("DeleteRetweet() index = %v error = %v, wantErr %v", i, err, tt.wantErr)
+				return
+			}
+			if diff := cmp.Diff(got, tt.want); diff != "" {
+				t.Errorf("DeleteRetweet() index = %v mismatch (-want +got):\n%s", i, diff)
+				return
+			}
+		})
+	}
+}