Repository: sivchari/gotwtr
Pull request #18 (2021-10-04 07:35:12)

Impl user timeline
Add user timeline APIs

related #10 

sivchari: Good work, thanks !
sivchari: Please rebase 👍  I fixed workflow.
If you rebase and push, reviewdog annotates your syntax error.
sivchari: please check file changes.

Patch:
diff --git a/client.go b/client.go
index fd37fa1..f05c6be 100644
--- a/client.go
+++ b/client.go
@@ -18,6 +18,12 @@ const (
 type Client interface {
 	LookUpTweets(ctx context.Context, ids []string, opt ...*TweetOption) (*TweetLookUpResponse, error)
 	LookUpTweetByID(ctx context.Context, id string, opt ...*TweetOption) (*TweetLookUpByIDResponse, error)
+	UserTweetTimeline(ctx context.Context, id string, opt ...*UserTweetTimelineOpts) (*UserTweetTimelineResponse, error)
+	UserMentionTimeline(ctx context.Context, id string, opt ...*UserMentionTimelineOpts) (*UserMentionTimelineResponse, error)
+	// User
+	// Media
+	// Poll
+	// Place
 	SearchRecentTweets(ctx context.Context, query string, opt ...*TweetSearchOption) (*TweetSearchResponse, error)
 }
 
@@ -52,6 +58,14 @@ func (c *client) LookUpTweetByID(ctx context.Context, id string, opt ...*TweetOp
 	return lookUpByID(ctx, c, id, opt...)
 }
 
+func (c *client) UserTweetTimeline(ctx context.Context, id string, opt ...*UserTweetTimelineOpts) (*UserTweetTimelineResponse, error) {
+	return userTweetTimeline(ctx, c, id, opt...)
+}
+
+func (c *client) UserMentionTimeline(ctx context.Context, id string, opt ...*UserMentionTimelineOpts) (*UserMentionTimelineResponse, error) {
+	return userMentionTimeline(ctx, c, id, opt...)
+}
+
 func (c *client) SearchRecentTweets(ctx context.Context, query string, opt ...*TweetSearchOption) (*TweetSearchResponse, error) {
 	return searchRecentTweets(ctx, c, query, opt...)
 }
diff --git a/endpoint.go b/endpoint.go
index 14185c0..01feba4 100644
--- a/endpoint.go
+++ b/endpoint.go
@@ -3,5 +3,6 @@ package gotwtr
 // EndpointURL is the base URL for the Twitter V2 API.
 var (
 	tweetLookUp       = "https://api.twitter.com/2/tweets"
+	timeline          = "https://api.twitter.com/2/users"
 	tweetRecentSearch = "https://api.twitter.com/2/tweets/search/recent"
 )
diff --git a/example/tweet/timeline/timeline.go b/example/tweet/timeline/timeline.go
new file mode 100644
index 0000000..d031f14
--- /dev/null
+++ b/example/tweet/timeline/timeline.go
@@ -0,0 +1,30 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"log"
+
+	"github.com/sivchari/gotwtr"
+)
+
+func main() {
+	client := gotwtr.New("key")
+	// user tweet timeline
+	ts, err := client.UserTweetTimeline(context.Background(), "id")
+	if err != nil {
+		log.Fatal(err)
+	}
+	for _, t := range ts.Tweets {
+		fmt.Println(t)
+	}
+
+	// user mention timeline
+	tws, err := client.UserMentionTimeline(context.Background(), "id")
+	if err != nil {
+		log.Fatal(err)
+	}
+	for _, tw := range tws.Tweets {
+		fmt.Println(tw)
+	}
+}
diff --git a/exclude.go b/exclude.go
new file mode 100644
index 0000000..2e8ca4d
--- /dev/null
+++ b/exclude.go
@@ -0,0 +1,17 @@
+package gotwtr
+
+// Exclude used in the timeline parameters
+type Exclude string
+
+const (
+	ExcludeRetweets Exclude = "retweets"
+	ExcludeReplies  Exclude = "replies"
+)
+
+func excludeToString(es []Exclude) []string {
+	slice := make([]string, len(es))
+	for i, e := range es {
+		slice[i] = string(e)
+	}
+	return slice
+}
diff --git a/timeline.go b/timeline.go
new file mode 100644
index 0000000..4ce9406
--- /dev/null
+++ b/timeline.go
@@ -0,0 +1,99 @@
+package gotwtr
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+)
+
+func userTweetTimeline(ctx context.Context, c *client, id string, opt ...*UserTweetTimelineOpts) (*UserTweetTimelineResponse, error) {
+	// check id
+	if len(id) == 0 {
+		return nil, errors.New("user tweet timeline: id parameter is required")
+	}
+	userTweetTimeline := timeline + "?id=" + id + "/tweets"
+
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, userTweetTimeline, nil)
+	if err != nil {
+		return nil, fmt.Errorf("user tweet timeline new request with ctx: %w", err)
+	}
+	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.bearerToken))
+
+	var topt UserTweetTimelineOpts
+	switch len(opt) {
+	case 0:
+		// do nothing
+	case 1:
+		topt = *opt[0]
+	default:
+		return nil, errors.New("user tweet timeline: only one option is allowed")
+	}
+	topt.addQuery(req)
+
+	resp, err := c.client.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("user tweet timeline response: %w", err)
+	}
+	defer resp.Body.Close()
+
+	var timelines UserTweetTimelineResponse
+	if err := json.NewDecoder(resp.Body).Decode(&timelines); err != nil {
+		return nil, fmt.Errorf("user tweet timeline decode: %w", err)
+	}
+	if resp.StatusCode != http.StatusOK {
+		return &timelines, &HTTPError{
+			APIName: "user tweet timeline",
+			Status:  resp.Status,
+			URL:     req.URL.String(),
+		}
+	}
+
+	return &timelines, nil
+}
+
+func userMentionTimeline(ctx context.Context, c *client, id string, opt ...*UserMentionTimelineOpts) (*UserMentionTimelineResponse, error) {
+	// check id
+	if len(id) == 0 {
+		return nil, errors.New("user mention timeline: id parameter is required")
+	}
+	userMentionTimeline := timeline + "?id=" + id + "/mentions"
+
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, userMentionTimeline, nil)
+	if err != nil {
+		return nil, fmt.Errorf("user mention timeline new request with ctx: %w", err)
+	}
+	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.bearerToken))
+
+	var topt UserMentionTimelineOpts
+	switch len(opt) {
+	case 0:
+		// do nothing
+	case 1:
+		topt = *opt[0]
+	default:
+		return nil, errors.New("user mention timeline: only one option is allowed")
+	}
+	topt.addQuery(req)
+
+	resp, err := c.client.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("user mention timeline response: %w", err)
+	}
+	defer resp.Body.Close()
+
+	var timelines UserMentionTimelineResponse
+	if err := json.NewDecoder(resp.Body).Decode(&timelines); err != nil {
+		return nil, fmt.Errorf("user mention timeline decode: %w", err)
+	}
+	if resp.StatusCode != http.StatusOK {
+		return &timelines, &HTTPError{
+			APIName: "user mention timeline",
+			Status:  resp.Status,
+			URL:     req.URL.String(),
+		}
+	}
+
+	return &timelines, nil
+}
diff --git a/tweet.go b/tweet.go
index c9e7fbc..f53db0c 100644
--- a/tweet.go
+++ b/tweet.go
@@ -173,6 +173,27 @@ type TweetIncludes struct {
 	Users  []*User
 }
 
+type UserTweetTimelineResponse struct {
+	Tweets   []*Tweet            `json:"data"`
+	Includes *TweetIncludes      `json:"includes,omitempty"`
+	Errors   []*APIResponseError `json:"errors,omitempty"`
+	Meta     *UserTimelineMeta   `json:"meta"`
+}
+
+type UserMentionTimelineResponse struct {
+	Tweets   []*Tweet            `json:"data"`
+	Includes *TweetIncludes      `json:"includes,omitempty"`
+	Errors   []*APIResponseError `json:"errors,omitempty"`
+	Meta     *UserTimelineMeta   `json:"meta"`
+}
+
+type UserTimelineMeta struct {
+	ResultCount int    `json:"result_count"`
+	NewestID    string `json:"newest_id"`
+	OldestID    string `json:"oldest_id"`
+	NextToken   string `json:"next_token"`
+}
+
 type TweetSearchResponse struct {
 	Tweets   []*Tweet            `json:"data"`
 	Includes *TweetIncludes      `json:"includes,omitempty"`
diff --git a/tweet_option.go b/tweet_option.go
index 44d939c..efc339f 100644
--- a/tweet_option.go
+++ b/tweet_option.go
@@ -108,3 +108,123 @@ func (t TweetSearchOption) addQuery(req *http.Request) {
 		req.URL.RawQuery = q.Encode()
 	}
 }
+
+type UserTweetTimelineOpts struct {
+	Expansions      []Expansion
+	MediaFields     []MediaField
+	PlaceFields     []PlaceField
+	PollFields      []PollField
+	TweetFields     []TweetField
+	UserFields      []UserField
+	Exclude         []Exclude
+	StartTime       time.Time
+	EndTime         time.Time
+	MaxResults      int
+	PaginationToken string
+	SinceID         string
+	UntilID         string
+}
+
+func (t UserTweetTimelineOpts) addQuery(req *http.Request) {
+	q := req.URL.Query()
+	if len(t.Expansions) > 0 {
+		q.Add("expansions", strings.Join(expansionsToString(t.Expansions), ","))
+	}
+	if len(t.MediaFields) > 0 {
+		q.Add("media.fields", strings.Join(mediaFieldsToString(t.MediaFields), ","))
+	}
+	if len(t.PlaceFields) > 0 {
+		q.Add("place.fields", strings.Join(placeFieldsToString(t.PlaceFields), ","))
+	}
+	if len(t.PollFields) > 0 {
+		q.Add("poll.fields", strings.Join(pollFieldsToString(t.PollFields), ","))
+	}
+	if len(t.TweetFields) > 0 {
+		q.Add("tweet.fields", strings.Join(tweetFieldsToString(t.TweetFields), ","))
+	}
+	if len(t.UserFields) > 0 {
+		q.Add("user.fields", strings.Join(userFieldsToString(t.UserFields), ","))
+	}
+	if len(t.Exclude) > 0 {
+		q.Add("exclude", strings.Join(excludeToString(t.Exclude), ","))
+	}
+	if !t.StartTime.IsZero() {
+		q.Add("start_time", t.StartTime.Format(time.RFC3339))
+	}
+	if !t.EndTime.IsZero() {
+		q.Add("end_time", t.EndTime.Format(time.RFC3339))
+	}
+	if t.MaxResults > 0 {
+		q.Add("max_results", strconv.Itoa(t.MaxResults))
+	}
+	if len(t.PaginationToken) > 0 {
+		q.Add("pagination_token", t.PaginationToken)
+	}
+	if len(t.SinceID) > 0 {
+		q.Add("since_id", t.SinceID)
+	}
+	if len(t.UntilID) > 0 {
+		q.Add("until_id", t.UntilID)
+	}
+	if len(q) > 0 {
+		req.URL.RawQuery = q.Encode()
+	}
+}
+
+type UserMentionTimelineOpts struct {
+	Expansions      []Expansion
+	MediaFields     []MediaField
+	PlaceFields     []PlaceField
+	PollFields      []PollField
+	TweetFields     []TweetField
+	UserFields      []UserField
+	StartTime       time.Time
+	EndTime         time.Time
+	MaxResults      int
+	PaginationToken string
+	SinceID         string
+	UntilID         string
+}
+
+func (t UserMentionTimelineOpts) addQuery(req *http.Request) {
+	q := req.URL.Query()
+	if len(t.Expansions) > 0 {
+		q.Add("expansions", strings.Join(expansionsToString(t.Expansions), ","))
+	}
+	if len(t.MediaFields) > 0 {
+		q.Add("media.fields", strings.Join(mediaFieldsToString(t.MediaFields), ","))
+	}
+	if len(t.PlaceFields) > 0 {
+		q.Add("place.fields", strings.Join(placeFieldsToString(t.PlaceFields), ","))
+	}
+	if len(t.PollFields) > 0 {
+		q.Add("poll.fields", strings.Join(pollFieldsToString(t.PollFields), ","))
+	}
+	if len(t.TweetFields) > 0 {
+		q.Add("tweet.fields", strings.Join(tweetFieldsToString(t.TweetFields), ","))
+	}
+	if len(t.UserFields) > 0 {
+		q.Add("user.fields", strings.Join(userFieldsToString(t.UserFields), ","))
+	}
+	if !t.StartTime.IsZero() {
+		q.Add("start_time", t.StartTime.Format(time.RFC3339))
+	}
+	if !t.EndTime.IsZero() {
+		q.Add("end_time", t.EndTime.Format(time.RFC3339))
+	}
+	if t.MaxResults > 0 {
+		q.Add("max_results", strconv.Itoa(t.MaxResults))
+	}
+	if len(t.PaginationToken) > 0 {
+		q.Add("pagination_token", t.PaginationToken)
+	}
+	if len(t.SinceID) > 0 {
+		q.Add("since_id", t.SinceID)
+	}
+	if len(t.UntilID) > 0 {
+		q.Add("until_id", t.UntilID)
+	}
+	if len(q) > 0 {
+		req.URL.RawQuery = q.Encode()
+	}
+}
diff --git a/user.go b/user.go
index b18ebb8..72f970b 100644
--- a/user.go
+++ b/user.go
@@ -75,9 +75,9 @@ type UserHashtag struct {
 }
 
 type UserMention struct {
-	Start int    `json:"start"`
-	End   int    `json:"end"`
-	Tag   string `json:"tag"`
+	Start    int    `json:"start"`
+	End      int    `json:"end"`
+	UserName string `json:"username"`
 }
 
 type UserCashtag struct {


Test patch:
diff --git a/timeline_test.go b/timeline_test.go
new file mode 100644
index 0000000..49fd44b
--- /dev/null
+++ b/timeline_test.go
@@ -0,0 +1,718 @@
+package gotwtr_test
+
+import (
+	"context"
+	"io"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/sivchari/gotwtr"
+)
+
+func Test_userTweetTimeline(t *testing.T) {
+	type args struct {
+		ctx    context.Context
+		client *http.Client
+		id     string
+		opt    []*gotwtr.UserTweetTimelineOpts
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    *gotwtr.UserTweetTimelineResponse
+		wantErr bool
+	}{
+		{
+			name: "200 success single ID default",
+			args: args{
+				ctx: context.Background(),
+				client: mockHTTPClient(func(req *http.Request) *http.Response {
+					body := `{
+						"data": [
+						  {
+							"id": "1338971066773905408",
+							"text": "💡 Using Twitter data for academic research? Join our next livestream this Friday @ 9am PT on https://t.co/GrtBOXh5Y1!\n \n@SuhemParack will show how to get started with recent search &amp; filtered stream endpoints on the #TwitterAPI v2, the new Tweet payload, annotations, &amp; more. https://t.co/IraD2Z7wEg"
+						  },
+						  {
+							"id": "1338923691497959425",
+							"text": "📈 Live now with @jessicagarson and @i_am_daniele! https://t.co/Y1AFzsTTxb"
+						  }
+						],
+						"meta": {
+						  "oldest_id": "1334564488884862976",
+						  "newest_id": "1338971066773905408",
+						  "result_count": 2,
+						  "next_token": "7140dibdnow9c7btw3w29grvxfcgvpb9n9coehpk7xz5i"
+						}
+					  }`
+					return &http.Response{
+						StatusCode: http.StatusOK,
+						Body:       io.NopCloser(strings.NewReader(body)),
+					}
+				}),
+				id: "1338971066773905408",
+			},
+			want: &gotwtr.UserTweetTimelineResponse{
+				Tweets: []*gotwtr.Tweet{
+					{
+						ID:   "1338971066773905408",
+						Text: "💡 Using Twitter data for academic research? Join our next livestream this Friday @ 9am PT on https://t.co/GrtBOXh5Y1!\n \n@SuhemParack will show how to get started with recent search &amp; filtered stream endpoints on the #TwitterAPI v2, the new Tweet payload, annotations, &amp; more. https://t.co/IraD2Z7wEg",
+					},
+					{
+						ID:   "1338923691497959425",
+						Text: "📈 Live now with @jessicagarson and @i_am_daniele! https://t.co/Y1AFzsTTxb",
+					},
+				},
+				Meta: &gotwtr.UserTimelineMeta{
+					ResultCount: 2,
+					OldestID:    "1334564488884862976",
+					NewestID:    "1338971066773905408",
+					NextToken:   "7140dibdnow9c7btw3w29grvxfcgvpb9n9coehpk7xz5i",
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "200 success Optional Fields",
+			args: args{
+				ctx: context.Background(),
+				client: mockHTTPClient(func(req *http.Request) *http.Response {
+					body := `{
+						"data": [
+						  {
+							"author_id": "2244994945",
+							"conversation_id": "1338971066773905408",
+							"text": "💡 Using Twitter data for academic research? Join our next livestream this Friday @ 9am PT on https://t.co/GrtBOXh5Y1!\n \n@SuhemParack will show how to get started with recent search &amp; filtered stream endpoints on the #TwitterAPI v2, the new Tweet payload, annotations, &amp; more. https://t.co/IraD2Z7wEg",
+							"context_annotations": [
+							  {
+								"domain": {
+								  "id": "47",
+								  "name": "Brand",
+								  "description": "Brands and Companies"
+								},
+								"entity": {
+								  "id": "10045225402",
+								  "name": "Twitter"
+								}
+							  }
+							],
+							"public_metrics": {
+							  "retweet_count": 10,
+							  "reply_count": 1,
+							  "like_count": 41,
+							  "quote_count": 4
+							},
+							"id": "1338971066773905408",
+							"created_at": "2020-12-15T22:15:53.000Z"
+						  },
+						  {
+							"author_id": "2244994945",
+							"conversation_id": "1338923691497959425",
+							"text": "📈 Live now with @jessicagarson and @i_am_daniele! https://t.co/Y1AFzsTTxb",
+							"context_annotations": [
+							  {
+								"domain": {
+								  "id": "47",
+								  "name": "Brand",
+								  "description": "Brands and Companies"
+								},
+								"entity": {
+								  "id": "10026378521",
+								  "name": "Google "
+								}
+							  }
+							],
+							"public_metrics": {
+							  "retweet_count": 3,
+							  "reply_count": 0,
+							  "like_count": 12,
+							  "quote_count": 1
+							},
+							"id": "1338923691497959425",
+							"created_at": "2020-12-15T19:07:38.000Z"
+						  }
+						],
+						"includes": {
+						  "users": [
+							{
+							  "id": "2244994945",
+							  "name": "Twitter Dev",
+							  "username": "TwitterDev"
+							}
+						  ]
+						},
+						"meta": {
+						  "oldest_id": "1337122535188652033",
+						  "newest_id": "1338971066773905408",
+						  "result_count": 2,
+						  "next_token": "7140dibdnow9c7btw3w29n4v1mtag9kegr0gr7y26pnw3"
+						}
+					  }`
+					return &http.Response{
+						StatusCode: http.StatusOK,
+						Body:       io.NopCloser(strings.NewReader(body)),
+					}
+				}),
+				id: "2244994945",
+				opt: []*gotwtr.UserTweetTimelineOpts{
+					{
+						TweetFields: []gotwtr.TweetField{
+							gotwtr.TweetFieldAttachments,
+							gotwtr.TweetFieldAuthorID,
+							gotwtr.TweetFieldCreatedAt,
+							gotwtr.TweetFieldEntities,
+							gotwtr.TweetFieldGeo,
+							gotwtr.TweetFieldID,
+							gotwtr.TweetFieldInReplyToUserID,
+							gotwtr.TweetFieldLanguage,
+							gotwtr.TweetFieldPossiblySensitve,
+							gotwtr.TweetFieldReferencedTweets,
+							gotwtr.TweetFieldSource,
+							gotwtr.TweetFieldText,
+							gotwtr.TweetFieldWithHeld,
+						},
+						UserFields: []gotwtr.UserField{
+							gotwtr.UserFieldUserName,
+						},
+						Expansions: []gotwtr.Expansion{
+							gotwtr.ExpansionAuthorID,
+						},
+					},
+				},
+			},
+			want: &gotwtr.UserTweetTimelineResponse{
+				Tweets: []*gotwtr.Tweet{
+					{
+						ID:             "1338971066773905408",
+						Text:           "💡 Using Twitter data for academic research? Join our next livestream this Friday @ 9am PT on https://t.co/GrtBOXh5Y1!\n \n@SuhemParack will show how to get started with recent search &amp; filtered stream endpoints on the #TwitterAPI v2, the new Tweet payload, annotations, &amp; more. https://t.co/IraD2Z7wEg",
+						AuthorID:       "2244994945",
+						ConversationID: "1338971066773905408",
+						CreatedAt:      "2020-12-15T22:15:53.000Z",
+						ContextAnnotations: []*gotwtr.TweetContextAnnotation{
+							{
+								Domain: &gotwtr.TweetContextObj{
+									ID:          "47",
+									Name:        "Brand",
+									Description: "Brands and Companies",
+								},
+								Entity: &gotwtr.TweetContextObj{
+									ID:   "10045225402",
+									Name: "Twitter",
+								},
+							},
+						},
+						PublicMetrics: &gotwtr.TweetMetrics{
+							RetweetCount: 10,
+							ReplyCount:   1,
+							LikeCount:    41,
+							QuoteCount:   4,
+						},
+					},
+					{
+						ID:             "1338923691497959425",
+						Text:           "📈 Live now with @jessicagarson and @i_am_daniele! https://t.co/Y1AFzsTTxb",
+						AuthorID:       "2244994945",
+						ConversationID: "1338923691497959425",
+						CreatedAt:      "2020-12-15T19:07:38.000Z",
+						ContextAnnotations: []*gotwtr.TweetContextAnnotation{
+							{
+								Domain: &gotwtr.TweetContextObj{
+									ID:          "47",
+									Name:        "Brand",
+									Description: "Brands and Companies",
+								},
+								Entity: &gotwtr.TweetContextObj{
+									ID:   "10026378521",
+									Name: "Google ",
+								},
+							},
+						},
+						PublicMetrics: &gotwtr.TweetMetrics{
+							RetweetCount: 3,
+							ReplyCount:   0,
+							LikeCount:    12,
+							QuoteCount:   1,
+						},
+					},
+				},
+				Includes: &gotwtr.TweetIncludes{
+					Users: []*gotwtr.User{
+						{
+							ID:       "2244994945",
+							Name:     "Twitter Dev",
+							UserName: "TwitterDev",
+						},
+					},
+				},
+				Meta: &gotwtr.UserTimelineMeta{
+					ResultCount: 2,
+					OldestID:    "1337122535188652033",
+					NewestID:    "1338971066773905408",
+					NextToken:   "7140dibdnow9c7btw3w29n4v1mtag9kegr0gr7y26pnw3",
+				},
+			},
+			wantErr: false,
+		},
+	}
+	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.UserTweetTimeline(tt.args.ctx, tt.args.id, tt.args.opt...)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("client.UserTweetTimeline() index = %v error = %v, wantErr %v", i, err, tt.wantErr)
+				return
+			}
+			if diff := cmp.Diff(got, tt.want); diff != "" {
+				t.Errorf("client.LookUpTweetByID() index = %v mismatch (-want +got):\n%s", i, diff)
+				return
+			}
+		})
+	}
+}
+
+func Test_userMentionTimeline(t *testing.T) {
+	type args struct {
+		ctx    context.Context
+		client *http.Client
+		id     string
+		opt    []*gotwtr.UserMentionTimelineOpts
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    *gotwtr.UserMentionTimelineResponse
+		wantErr bool
+	}{
+		{
+			name: "200 success single ID default",
+			args: args{
+				ctx: context.Background(),
+				client: mockHTTPClient(func(req *http.Request) *http.Response {
+					body := `{
+						"data": [
+						  {
+								"id": "1375152598945312768",
+								"text": "@LeBraat @TwitterDev @LeGuud There's enough @twitterdev love to go around, @LeBraat"
+							},
+							{
+								"id": "1375152449594523649",
+								"text": "Apparently I'm not the only one (of my test accounts) that loves @TwitterDev nn@LeStaache @LeGuud"
+							},
+							{
+								"id": "1375152043455873027",
+								"text": "Can I join this @twitterdev love party?!"
+							},
+							{
+								"id": "1375151947360174082",
+								"text": "I love me some @twitterdev too!"
+							},
+							{
+								"id": "1375151827189137412",
+								"text": "This is a test, but also a good excuse to express my love for @TwitterDev 😍"
+							}
+						],
+						"meta": {
+						  "oldest_id": "1375151827189137412",
+							"newest_id": "1375152598945312768",
+							"result_count": 5,
+							"next_token": "7140dibdnow9c7btw3w3y5b6jqjnk3k4g5zkmfkvqwa42"
+						}
+					}`
+					return &http.Response{
+						StatusCode: http.StatusOK,
+						Body:       io.NopCloser(strings.NewReader(body)),
+					}
+				}),
+				id: "1338971066773905408",
+			},
+			want: &gotwtr.UserMentionTimelineResponse{
+				Tweets: []*gotwtr.Tweet{
+					{
+						ID:   "1375152598945312768",
+						Text: "@LeBraat @TwitterDev @LeGuud There's enough @twitterdev love to go around, @LeBraat",
+					},
+					{
+						ID:   "1375152449594523649",
+						Text: "Apparently I'm not the only one (of my test accounts) that loves @TwitterDev nn@LeStaache @LeGuud",
+					},
+					{
+						ID:   "1375152043455873027",
+						Text: "Can I join this @twitterdev love party?!",
+					},
+					{
+						ID:   "1375151947360174082",
+						Text: "I love me some @twitterdev too!",
+					},
+					{
+						ID:   "1375151827189137412",
+						Text: "This is a test, but also a good excuse to express my love for @TwitterDev 😍",
+					},
+				},
+				Meta: &gotwtr.UserTimelineMeta{
+					ResultCount: 5,
+					OldestID:    "1375151827189137412",
+					NewestID:    "1375152598945312768",
+					NextToken:   "7140dibdnow9c7btw3w3y5b6jqjnk3k4g5zkmfkvqwa42",
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "200 success Optional Fields",
+			args: args{
+				ctx: context.Background(),
+				client: mockHTTPClient(func(req *http.Request) *http.Response {
+					body := `{
+						"data": [
+							{
+								"author_id": "1034147061711679488",
+								"text": "@LeBraat @TwitterDev @LeGuud There's enough @twitterdev love to go around, @LeBraat",
+								"lang": "en",
+								"conversation_id": "1375152449594523649",
+								"id": "1375152598945312768"
+							},
+							{
+								"author_id": "199566737",
+								"text": "Apparently I'm not the only one (of my test accounts) that loves @TwitterDev nn@LeStaache @LeGuud",
+								"lang": "en",
+								"conversation_id": "1375152449594523649",
+								"id": "1375152449594523649"
+							},
+							{
+								"author_id": "930524282358325248",
+								"text": "Can I join this @twitterdev love party?!",
+								"lang": "en",
+								"conversation_id": "1375152043455873027",
+								"id": "1375152043455873027"
+							},
+							{
+								"author_id": "1034147061711679488",
+								"text": "I love me some @twitterdev too!",
+								"lang": "en",
+								"conversation_id": "1375151947360174082",
+								"id": "1375151947360174082"
+							},
+							{
+								"author_id": "199566737",
+								"text": "This is a test, but also a good excuse to express my love for @TwitterDev 😍",
+								"lang": "en",
+								"conversation_id": "1375151827189137412",
+								"id": "1375151827189137412"
+							}
+						],
+						"includes": {
+							"users": [
+								{
+									"name": "LeStache",
+									"id": "1034147061711679488",
+									"entities": [
+										{
+											"url": {
+												"urls": [
+													{
+														"start": 0,
+														"end": 23,
+														"url": "https://t.co/7IDoW8iFLm",
+														"expanded_url": "https://twitter.com",
+														"display_url": "twitter.com"
+													}
+												]
+											},
+											"description": {
+												"urls": [
+													{
+														"start": 21,
+														"end": 44,
+														"url": "https://t.co/v6nxjDjZR3",
+														"expanded_url": "https://google.com",
+														"display_url": "google.com"
+													}
+												],
+												"hashtags": [
+													{
+														"start": 15,
+														"end": 20,
+														"tag": "test"
+													}
+												],
+												"user_mentions": [
+													{
+														"start": 0,
+														"end": 8,
+														"username": "lebraat"
+													}
+												],
+												"cashtags": [
+													{
+														"start": 9,
+														"end": 14,
+														"tag": "twtr"
+													}
+												]
+											}
+										}
+									],
+									"username": "LeStaache",
+									"created_at": "2018-08-27T18:34:07.000Z"
+								},
+								{
+									"name": "dan dale",
+									"id": "199566737",
+									"entities": [
+										{
+											"description": {
+												"hashtags": [
+													{
+														"start": 30,
+														"end": 37,
+														"tag": "devrel"
+													}
+												],
+												"user_mentions": [
+													{
+														"start": 18,
+														"end": 29,
+														"username": "twitterdev"
+													}
+												]
+											}
+										}
+									],
+									"username": "LeBraat",
+									"created_at": "2010-10-07T05:36:28.000Z"
+								},
+								{
+									"name": "LeGuud",
+									"id": "930524282358325248",
+									"entities": [
+										{
+											"url": {
+												"urls": [
+													{
+														"start": 0,
+														"end": 23,
+														"url": "https://t.co/8IkCzClPCz",
+														"expanded_url": "https://developer.twitter.com",
+														"display_url": "developer.twitter.com"
+													}
+												]
+											}
+										}
+									],
+									"username": "LeGuud",
+									"created_at": "2017-11-14T19:54:12.000Z"
+								}
+							]
+						},
+						"meta": {
+							"oldest_id": "1375151827189137412",
+							"newest_id": "1375152598945312768",
+							"result_count": 5,
+							"next_token": "7140dibdnow9c7btw3w3y5b6jqjnk3k4g5zkmfkvqwa42"
+						}
+					}`
+					return &http.Response{
+						StatusCode: http.StatusOK,
+						Body:       io.NopCloser(strings.NewReader(body)),
+					}
+				}),
+				id: "2244994945",
+				opt: []*gotwtr.UserMentionTimelineOpts{
+					{
+						TweetFields: []gotwtr.TweetField{
+							gotwtr.TweetFieldAttachments,
+							gotwtr.TweetFieldAuthorID,
+							gotwtr.TweetFieldCreatedAt,
+							gotwtr.TweetFieldEntities,
+							gotwtr.TweetFieldGeo,
+							gotwtr.TweetFieldID,
+							gotwtr.TweetFieldInReplyToUserID,
+							gotwtr.TweetFieldLanguage,
+							gotwtr.TweetFieldPossiblySensitve,
+							gotwtr.TweetFieldReferencedTweets,
+							gotwtr.TweetFieldSource,
+							gotwtr.TweetFieldText,
+							gotwtr.TweetFieldWithHeld,
+						},
+						UserFields: []gotwtr.UserField{
+							gotwtr.UserFieldUserName,
+						},
+						Expansions: []gotwtr.Expansion{
+							gotwtr.ExpansionAuthorID,
+						},
+					},
+				},
+			},
+			want: &gotwtr.UserMentionTimelineResponse{
+				Tweets: []*gotwtr.Tweet{
+					{
+						AuthorID:       "1034147061711679488",
+						Text:           "@LeBraat @TwitterDev @LeGuud There's enough @twitterdev love to go around, @LeBraat",
+						Lang:           "en",
+						ConversationID: "1375152449594523649",
+						ID:             "1375152598945312768",
+					},
+					{
+						AuthorID:       "199566737",
+						Text:           "Apparently I'm not the only one (of my test accounts) that loves @TwitterDev nn@LeStaache @LeGuud",
+						Lang:           "en",
+						ConversationID: "1375152449594523649",
+						ID:             "1375152449594523649",
+					},
+					{
+						AuthorID:       "930524282358325248",
+						Text:           "Can I join this @twitterdev love party?!",
+						Lang:           "en",
+						ConversationID: "1375152043455873027",
+						ID:             "1375152043455873027",
+					},
+					{
+						AuthorID:       "1034147061711679488",
+						Text:           "I love me some @twitterdev too!",
+						Lang:           "en",
+						ConversationID: "1375151947360174082",
+						ID:             "1375151947360174082",
+					},
+					{
+						AuthorID:       "199566737",
+						Text:           "This is a test, but also a good excuse to express my love for @TwitterDev 😍",
+						Lang:           "en",
+						ConversationID: "1375151827189137412",
+						ID:             "1375151827189137412",
+					},
+				},
+				Includes: &gotwtr.TweetIncludes{
+					Users: []*gotwtr.User{
+						{
+							Name: "LeStache",
+							ID:   "1034147061711679488",
+							Entities: []*gotwtr.UserEntity{
+								{
+									URL: &gotwtr.UserURL{
+										URLs: []*gotwtr.UserURLs{
+											{
+												Start:       0,
+												End:         23,
+												URL:         "https://t.co/7IDoW8iFLm",
+												ExpandedURL: "https://twitter.com",
+												DisplayURL:  "twitter.com",
+											},
+										},
+									},
+									Description: &gotwtr.UserDescription{
+										URLs: []*gotwtr.UserURLs{
+											{
+												Start:       21,
+												End:         44,
+												URL:         "https://t.co/v6nxjDjZR3",
+												ExpandedURL: "https://google.com",
+												DisplayURL:  "google.com",
+											},
+										},
+										Hashtags: []*gotwtr.UserHashtag{
+											{
+												Start: 15,
+												End:   20,
+												Tag:   "test",
+											},
+										},
+										Mentions: []*gotwtr.UserMention{
+											{
+												Start:    0,
+												End:      8,
+												UserName: "lebraat",
+											},
+										},
+										Cashtags: []*gotwtr.UserCashtag{
+											{
+												Start: 9,
+												End:   14,
+												Tag:   "twtr",
+											},
+										},
+									},
+								},
+							},
+							UserName:  "LeStaache",
+							CreatedAt: "2018-08-27T18:34:07.000Z",
+						},
+						{
+							Name: "dan dale",
+							ID:   "199566737",
+							Entities: []*gotwtr.UserEntity{
+								{
+									Description: &gotwtr.UserDescription{
+										Hashtags: []*gotwtr.UserHashtag{
+											{
+												Start: 30,
+												End:   37,
+												Tag:   "devrel",
+											},
+										},
+										Mentions: []*gotwtr.UserMention{
+											{
+												Start:    18,
+												End:      29,
+												UserName: "twitterdev",
+											},
+										},
+									},
+								},
+							},
+							UserName:  "LeBraat",
+							CreatedAt: "2010-10-07T05:36:28.000Z",
+						},
+						{
+							Name: "LeGuud",
+							ID:   "930524282358325248",
+							Entities: []*gotwtr.UserEntity{
+								{
+									URL: &gotwtr.UserURL{
+										URLs: []*gotwtr.UserURLs{
+											{
+												Start:       0,
+												End:         23,
+												URL:         "https://t.co/8IkCzClPCz",
+												ExpandedURL: "https://developer.twitter.com",
+												DisplayURL:  "developer.twitter.com",
+											},
+										},
+									},
+								},
+							},
+							UserName:  "LeGuud",
+							CreatedAt: "2017-11-14T19:54:12.000Z",
+						},
+					},
+				},
+				Meta: &gotwtr.UserTimelineMeta{
+					OldestID:    "1375151827189137412",
+					NewestID:    "1375152598945312768",
+					ResultCount: 5,
+					NextToken:   "7140dibdnow9c7btw3w3y5b6jqjnk3k4g5zkmfkvqwa42",
+				},
+			},
+			wantErr: false,
+		},
+	}
+	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.UserMentionTimeline(tt.args.ctx, tt.args.id, tt.args.opt...)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("client.UserMentionTimeline() index = %v error = %v, wantErr %v", i, err, tt.wantErr)
+				return
+			}
+			if diff := cmp.Diff(got, tt.want); diff != "" {
+				t.Errorf("client.UserMentionTimeline() index = %v mismatch (-want +got):\n%s", i, diff)
+				return
+			}
+		})
+	}
+}