diff --git a/VERSION b/VERSION index a2268e2..9fc80f9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.1 \ No newline at end of file +0.3.2 \ No newline at end of file diff --git a/example/go.mod b/example/go.mod index dbf8fd3..08c71df 100644 --- a/example/go.mod +++ b/example/go.mod @@ -2,6 +2,6 @@ module github.com/fallenstedt/twitter-stream/example replace github.com/fallenstedt/twitter-stream => /Users/alex/Projects/twitter-stream/ -go 1.15 +go 1.16 require github.com/fallenstedt/twitter-stream v0.2.1 diff --git a/go.mod b/go.mod index 0fe0b64..aaf5a28 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/fallenstedt/twitter-stream -go 1.15 +go 1.16 diff --git a/http_client.go b/http_client.go deleted file mode 100644 index e8a355a..0000000 --- a/http_client.go +++ /dev/null @@ -1,159 +0,0 @@ -package twitterstream - -import ( - "bytes" - "errors" - "io/ioutil" - "log" - "net/http" - "strings" -) - -type twitterEndpoints map[string]string -var endpoints = make(twitterEndpoints) - -type ( - // IHttpClient is the interface the httpClient struct implements. - IHttpClient interface { - - newHttpRequest(opts *requestOpts) (*http.Response, error) - getRules() (*http.Response, error) - getSearchStream(queryParams string) (*http.Response, error) - addRules(queryParams string, body string) (*http.Response, error) - generateUrl(name string, queryParams string) (string, error) - } - - httpClient struct { - token string - } - - requestOpts struct { - Method string - Url string - Body string - Headers []struct { - key string - value string - } - } -) - -func newHttpClient(token string) *httpClient { - endpoints["rules"] = "https://api.twitter.com/2/tweets/search/stream/rules" - endpoints["stream"] = "https://api.twitter.com/2/tweets/search/stream" - endpoints["token"] = "https://api.twitter.com/oauth2/token" - return &httpClient{token} -} - -func (t *httpClient) getRules() (*http.Response, error) { - res, err := t.newHttpRequest(&requestOpts{ - Method: "GET", - Url: endpoints["rules"], - Body: "", - }) - - return res, err -} - -func (t *httpClient) addRules(queryParams string, body string) (*http.Response, error) { - url, err := t.generateUrl("rules", queryParams) - - if err != nil { - return nil, err - } - - res, err := t.newHttpRequest(&requestOpts{ - Method: "POST", - Url: url, - Body: body, - }) - - if err != nil { - return nil, err - } - - return res, nil -} - -func (t *httpClient) getSearchStream(queryParams string) (*http.Response, error) { - // Make an HTTP GET request to GET /2/tweets/search/stream - url, err := t.generateUrl("stream", queryParams) - - if err != nil { - return nil, err - } - - res, err := t.newHttpRequest(&requestOpts{ - Method: "GET", - Url: url, - }) - - if err != nil { - return nil, err - } - - return res, nil -} - -func (t *httpClient) generateUrl(name string, queryParams string) (string, error) { - var url string - if len(queryParams) > 0 { - url = endpoints[name] + queryParams - } else { - url = endpoints[name] - } - - if len(url) == 0 || !strings.HasPrefix(url, "https://api.twitter.com") { - return url, errors.New("Could not find endpoint with name " + name) - } else { - return url, nil - } -} - -func (t *httpClient) newHttpRequest(opts *requestOpts) (*http.Response, error) { - client := &http.Client{} - - var req *http.Request - var err error - if opts.Method == "GET" { - req, err = http.NewRequest(opts.Method, opts.Url, nil) - } else { - bufferBody := bytes.NewBuffer([]byte(opts.Body)) - req, err = http.NewRequest(opts.Method, opts.Url, bufferBody) - } - - if err != nil { - log.Printf("Failed to construct http request for %s: %v", opts.Url, err) - return nil, err - } - - // Set Headers - req.Header.Set("Content-Type", "application/json") - if len(opts.Headers) > 0 { - for _, header := range opts.Headers { - req.Header.Set(header.key, header.value) - } - } - - // Set token if this client has a token set - if len(t.token) > 0 { - req.Header.Set("Authorization", "Bearer "+t.token) - } - - // Perform network request - resp, err := client.Do(req) - if err != nil { - log.Printf("Failed to perform request for %s: %v", opts.Url, err) - return nil, err - } - - // Reject if 400 or greater - if resp.StatusCode >= 400 { - log.Printf("Network Request failed: %v", resp.StatusCode) - body, _ := ioutil.ReadAll(resp.Body) - msg := "Network request failed: " + string(body) - return nil, errors.New(msg) - } - - return resp, nil -} diff --git a/http_client_mock.go b/http_client_mock.go deleted file mode 100644 index 0c8dab5..0000000 --- a/http_client_mock.go +++ /dev/null @@ -1,36 +0,0 @@ -package twitterstream - -import "net/http" - -type mockHttpClient struct { - token string - MockNewHttpRequest func(opts *requestOpts) (*http.Response, error) - MockGetSearchStream func(queryParams string) (*http.Response, error) - MockGetRules func() (*http.Response, error) - MockAddRules func(queryParams string, body string) (*http.Response, error) - MockGenerateUrl func (name string, queryParams string) (string, error) -} - -func newHttpClientMock(token string) *mockHttpClient { - return &mockHttpClient{token: token} -} - -func (t *mockHttpClient) generateUrl(name string, queryParams string) (string, error) { - return t.MockGenerateUrl(name, queryParams) -} - -func (t *mockHttpClient) getRules() (*http.Response, error) { - return t.MockGetRules() -} - -func (t *mockHttpClient) addRules(queryParams string, body string) (*http.Response, error) { - return t.MockAddRules(queryParams, body) -} - -func (t *mockHttpClient) getSearchStream(queryParams string) (*http.Response, error) { - return t.MockGetSearchStream(queryParams) -} - -func (t *mockHttpClient) newHttpRequest(opts *requestOpts) (*http.Response, error) { - return t.MockNewHttpRequest(opts) -} diff --git a/httpclient/http_client_mock.go b/httpclient/http_client_mock.go new file mode 100644 index 0000000..6250708 --- /dev/null +++ b/httpclient/http_client_mock.go @@ -0,0 +1,36 @@ +package httpclient + +import "net/http" + +type mockHttpClient struct { + token string + MockNewHttpRequest func(opts *RequestOpts) (*http.Response, error) + MockGetSearchStream func(queryParams string) (*http.Response, error) + MockGetRules func() (*http.Response, error) + MockAddRules func(queryParams string, body string) (*http.Response, error) + MockGenerateUrl func(name string, queryParams string) (string, error) +} + +func NewHttpClientMock(token string) *mockHttpClient { + return &mockHttpClient{token: token} +} + +func (t *mockHttpClient) GenerateUrl(name string, queryParams string) (string, error) { + return t.MockGenerateUrl(name, queryParams) +} + +func (t *mockHttpClient) GetRules() (*http.Response, error) { + return t.MockGetRules() +} + +func (t *mockHttpClient) AddRules(queryParams string, body string) (*http.Response, error) { + return t.MockAddRules(queryParams, body) +} + +func (t *mockHttpClient) GetSearchStream(queryParams string) (*http.Response, error) { + return t.MockGetSearchStream(queryParams) +} + +func (t *mockHttpClient) NewHttpRequest(opts *RequestOpts) (*http.Response, error) { + return t.MockNewHttpRequest(opts) +} diff --git a/httpclient/http_response_parser.go b/httpclient/http_response_parser.go new file mode 100644 index 0000000..6c928cd --- /dev/null +++ b/httpclient/http_response_parser.go @@ -0,0 +1,55 @@ +package httpclient + +import ( + "errors" + "fmt" + "io/ioutil" + "log" + "math" + "net/http" + "time" +) + +// httpResponseParser is a struct that will retry network requests if the response has a status code of 429. +type httpResponseParser struct{} + +func (h httpResponseParser) handleResponse(resp *http.Response, opts *RequestOpts, fn func(opts *RequestOpts) (*http.Response, error)) (*http.Response, error) { + // Retry with backoff if 429 + if resp.StatusCode == 429 { + log.Printf("Retrying network request %s with backoff", opts.Url) + + delay := h.getBackOffTime(opts.Retries) + log.Printf("Sleeping for %v seconds", delay) + time.Sleep(delay) + + opts.Retries += 1 + + return fn(opts) + } + + // Reject if 400 or greater + if resp.StatusCode >= 400 { + log.Printf("Network Request at %s failed: %v", opts.Url, resp.StatusCode) + + var msg string + if resp.Body != nil { + body, _ := ioutil.ReadAll(resp.Body) + msg = "Network request failed: " + string(body) + } else { + msg = "Network request failed with status" + fmt.Sprint(resp.StatusCode) + } + + return nil, errors.New(msg) + } + + return resp, nil +} + +func (h httpResponseParser) getBackOffTime(retries uint8) time.Duration { + exponentialBackoffCeilingSecs := 30 + delaySecs := int(math.Floor((math.Pow(2, float64(retries)) - 1) * 0.5)) + if delaySecs > exponentialBackoffCeilingSecs { + delaySecs = 30 + } + return time.Duration(delaySecs) * time.Second +} diff --git a/httpclient/http_response_parser_test.go b/httpclient/http_response_parser_test.go new file mode 100644 index 0000000..4eaa7f8 --- /dev/null +++ b/httpclient/http_response_parser_test.go @@ -0,0 +1,70 @@ +package httpclient + +import ( + "net/http" + "testing" +) + +func givenHttpResponseParserInstance() *httpResponseParser { + return new(httpResponseParser) +} + +func givenFakeHttpResponse(statusCode int) *http.Response { + res := new(http.Response) + res.StatusCode = statusCode + return res +} + +func TestHandleResponseShouldReturnIf200(t *testing.T) { + instance := givenHttpResponseParserInstance() + opts := new(RequestOpts) + resp := givenFakeHttpResponse(200) + + result, err := instance.handleResponse(resp, opts, func(o *RequestOpts) (*http.Response, error) { + return nil, nil + }) + + if err != nil { + t.Errorf("Expected not error, got %v", err) + } + + if result.StatusCode != 200 { + t.Errorf("Expected a status code of 200") + } +} + +func TestHandleResponseShouldRetryRequestIf429(t *testing.T) { + instance := givenHttpResponseParserInstance() + opts := new(RequestOpts) + resp := givenFakeHttpResponse(429) + + result, err := instance.handleResponse(resp, opts, func(o *RequestOpts) (*http.Response, error) { + return givenFakeHttpResponse(200), nil + }) + + if opts.Retries != 1 { + t.Errorf("Expected atleast on retry attempt, got %v", opts.Retries) + } + + if err != nil { + t.Errorf("Expected not error, got %v", err) + } + + if result.StatusCode != 200 { + t.Errorf("Expected a status code of 200") + } +} + +func TestHandleResponseShouldRejectIf400OrHigher(t *testing.T) { + instance := givenHttpResponseParserInstance() + opts := new(RequestOpts) + resp := givenFakeHttpResponse(401) + + _, err := instance.handleResponse(resp, opts, func(o *RequestOpts) (*http.Response, error) { + return nil, nil + }) + + if err == nil { + t.Errorf("Expected error, got nil") + } +} diff --git a/httpclient/httpclient.go b/httpclient/httpclient.go new file mode 100644 index 0000000..d2926f2 --- /dev/null +++ b/httpclient/httpclient.go @@ -0,0 +1,149 @@ +package httpclient + +import ( + "bytes" + "errors" + "log" + "net/http" + "strings" +) + +type twitterEndpoints map[string]string + +// Endpoints is a map of twitter endpoints used to manage rules and streams. +var Endpoints = make(twitterEndpoints) + +type ( + // IHttpClient is the interface the httpClient struct implements. + IHttpClient interface { + NewHttpRequest(opts *RequestOpts) (*http.Response, error) + GetRules() (*http.Response, error) + GetSearchStream(queryParams string) (*http.Response, error) + AddRules(queryParams string, body string) (*http.Response, error) + GenerateUrl(name string, queryParams string) (string, error) + } + + httpClient struct { + token string + } +) + +// NewHttpClient constructs a an HttpClient to interact with twitter. +func NewHttpClient(token string) *httpClient { + Endpoints["rules"] = "https://api.twitter.com/2/tweets/search/stream/rules" + Endpoints["stream"] = "https://api.twitter.com/2/tweets/search/stream" + Endpoints["token"] = "https://api.twitter.com/oauth2/token" + return &httpClient{token} +} + +// GetRules will return the current rules available for a specific API key. +func (t *httpClient) GetRules() (*http.Response, error) { + res, err := t.NewHttpRequest(&RequestOpts{ + Method: "GET", + Url: Endpoints["rules"], + Body: "", + }) + + return res, err +} + +// AddRules will add rules for you to stream with. +func (t *httpClient) AddRules(queryParams string, body string) (*http.Response, error) { + url, err := t.GenerateUrl("rules", queryParams) + + if err != nil { + return nil, err + } + + res, err := t.NewHttpRequest(&RequestOpts{ + Method: "POST", + Url: url, + Body: body, + }) + + if err != nil { + return nil, err + } + + return res, nil +} + +// GetSearchStream will start the stream with twitter. +func (t *httpClient) GetSearchStream(queryParams string) (*http.Response, error) { + // Make an HTTP GET request to GET /2/tweets/search/stream + url, err := t.GenerateUrl("stream", queryParams) + + if err != nil { + return nil, err + } + + res, err := t.NewHttpRequest(&RequestOpts{ + Method: "GET", + Url: url, + }) + + if err != nil { + return nil, err + } + + return res, nil +} + +// GenerateUrl is a utility function for httpclient package to generate a valid url for api.twitter. +func (t *httpClient) GenerateUrl(name string, queryParams string) (string, error) { + var url string + if len(queryParams) > 0 { + url = Endpoints[name] + queryParams + } else { + url = Endpoints[name] + } + + if len(url) == 0 || !strings.HasPrefix(url, "https://api.twitter.com") { + return url, errors.New("Could not find endpoint with name " + name) + } else { + return url, nil + } +} + +// NewHttpRequest performs an authenticated http request with twitter with the token this httpclient has. +func (t *httpClient) NewHttpRequest(opts *RequestOpts) (*http.Response, error) { + + var req *http.Request + var err error + if opts.Method == "GET" { + req, err = http.NewRequest(opts.Method, opts.Url, nil) + } else { + bufferBody := bytes.NewBuffer([]byte(opts.Body)) + req, err = http.NewRequest(opts.Method, opts.Url, bufferBody) + } + + if err != nil { + log.Printf("Failed to construct http request for %s: %v", opts.Url, err) + return nil, err + } + + // Set Headers + req.Header.Set("Content-Type", "application/json") + if len(opts.Headers) > 0 { + for _, header := range opts.Headers { + req.Header.Set(header.Key, header.Value) + } + } + + // Set token if this httpclient has a token set + if len(t.token) > 0 { + req.Header.Set("Authorization", "Bearer "+t.token) + } + + // Perform network request + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Printf("Failed to perform request for %s: %v", opts.Url, err) + return nil, err + } + + responseParser := new(httpResponseParser) + return responseParser.handleResponse(resp, opts, t.NewHttpRequest) + +} diff --git a/httpclient/types.go b/httpclient/types.go new file mode 100644 index 0000000..a461ca2 --- /dev/null +++ b/httpclient/types.go @@ -0,0 +1,12 @@ +package httpclient + +type RequestOpts struct { + Retries uint8 + Method string + Url string + Body string + Headers []struct { + Key string + Value string + } +} diff --git a/rules.go b/rules.go index 0844d64..cd656c1 100644 --- a/rules.go +++ b/rules.go @@ -2,6 +2,7 @@ package twitterstream import ( "encoding/json" + "github.com/fallenstedt/twitter-stream/httpclient" ) type ( @@ -12,12 +13,12 @@ type ( } rules struct { - httpClient IHttpClient + httpClient httpclient.IHttpClient } rulesResponse struct { - Data []rulesResponseValue - Meta rulesResponseMeta + Data []rulesResponseValue + Meta rulesResponseMeta Errors []rulesResponseError } @@ -32,9 +33,9 @@ type ( } rulesResponseError struct { Value string `json:"value"` - Id string `json:"id"` + Id string `json:"id"` Title string `json:"title"` - Type string `json:"type"` + Type string `json:"type"` } addRulesResponseMetaSummary struct { @@ -43,7 +44,7 @@ type ( } ) -func newRules(httpClient IHttpClient) *rules { +func newRules(httpClient httpclient.IHttpClient) *rules { return &rules{httpClient: httpClient} } @@ -51,7 +52,7 @@ func newRules(httpClient IHttpClient) *rules { // The body is a stringified object. // Learn about the possible error messages returned here https://developer.twitter.com/en/support/twitter-api/error-troubleshooting. func (t *rules) AddRules(body string, dryRun bool) (*rulesResponse, error) { - res, err := t.httpClient.addRules(func() string { + res, err := t.httpClient.AddRules(func() string { if dryRun { return "?dry_run=true" } else { @@ -75,7 +76,7 @@ func (t *rules) AddRules(body string, dryRun bool) (*rulesResponse, error) { // GetRules gets rules for a stream using twitter's GET GET /2/tweets/search/stream/rules endpoint. func (t *rules) GetRules() (*rulesResponse, error) { - res, err := t.httpClient.getRules() + res, err := t.httpClient.GetRules() if err != nil { return nil, err diff --git a/rules_test.go b/rules_test.go index ff47fc0..20778d4 100644 --- a/rules_test.go +++ b/rules_test.go @@ -3,6 +3,7 @@ package twitterstream import ( "bytes" "fmt" + "github.com/fallenstedt/twitter-stream/httpclient" "io/ioutil" "net/http" "testing" @@ -69,7 +70,7 @@ func TestAddRules(t *testing.T) { testName := fmt.Sprintf("TestAddRules (%d) %s", i, tt.body) t.Run(testName, func(t *testing.T) { - mockClient := newHttpClientMock("sometoken") + mockClient := httpclient.NewHttpClientMock("sometoken") mockClient.MockAddRules = tt.mockRequest instance := newRules(mockClient) @@ -158,7 +159,7 @@ func TestGetRules(t *testing.T) { testName := fmt.Sprintf("TestGetRules (%d)", i) t.Run(testName, func(t *testing.T) { - mockClient := newHttpClientMock("sometoken") + mockClient := httpclient.NewHttpClientMock("sometoken") mockClient.MockGetRules = tt.mockRequest instance := newRules(mockClient) diff --git a/stream.go b/stream.go index 058307a..01bb860 100644 --- a/stream.go +++ b/stream.go @@ -1,10 +1,10 @@ package twitterstream import ( + "github.com/fallenstedt/twitter-stream/httpclient" "net/http" ) - type ( // UnmarshalHook is a function that will unmarshal json. UnmarshalHook func([]byte) (interface{}, error) @@ -29,14 +29,14 @@ type ( // in a separate goroutine is not recommended because the Go bytes.Buffer is not thread safe. Stream struct { unmarshalHook UnmarshalHook - messages chan StreamMessage - httpClient IHttpClient - done chan struct{} - reader IStreamResponseBodyReader + messages chan StreamMessage + httpClient httpclient.IHttpClient + done chan struct{} + reader IStreamResponseBodyReader } ) -func newStream(httpClient IHttpClient, reader IStreamResponseBodyReader) *Stream { +func newStream(httpClient httpclient.IHttpClient, reader IStreamResponseBodyReader) *Stream { return &Stream{ unmarshalHook: func(bytes []byte) (interface{}, error) { return bytes, nil @@ -70,7 +70,7 @@ func (s *Stream) StopStream() { // See available query params here https://developer.twitter.com/en/docs/twitter-api/tweets/filtered-stream/api-reference/get-tweets-search-stream. // See an example here: https://developer.twitter.com/en/docs/twitter-api/expansions. func (s *Stream) StartStream(optionalQueryParams string) error { - res, err := s.httpClient.getSearchStream(optionalQueryParams) + res, err := s.httpClient.GetSearchStream(optionalQueryParams) if err != nil { return err diff --git a/stream_test.go b/stream_test.go index aa266f0..123d41f 100644 --- a/stream_test.go +++ b/stream_test.go @@ -3,6 +3,7 @@ package twitterstream import ( "bytes" "fmt" + "github.com/fallenstedt/twitter-stream/httpclient" "io" "io/ioutil" "net/http" @@ -10,7 +11,7 @@ import ( ) func TestGetMessages(t *testing.T) { - client := newHttpClientMock("foobar") + client := httpclient.NewHttpClientMock("foobar") reader := newStreamResponseBodyReader() instance := newStream(client, reader) @@ -22,7 +23,7 @@ func TestGetMessages(t *testing.T) { } func TestStopStream(t *testing.T) { - client := newHttpClientMock("foobar") + client := httpclient.NewHttpClientMock("foobar") reader := newStreamResponseBodyReader() instance := newStream(client, reader) @@ -36,13 +37,13 @@ func TestStopStream(t *testing.T) { func TestStartStream(t *testing.T) { var tests = []struct { - givenMockHttpRequestToStreamReturns func() IHttpClient + givenMockHttpRequestToStreamReturns func() httpclient.IHttpClient givenMockStreamResponseBodyReader func() IStreamResponseBodyReader result StreamMessage }{ { - func() IHttpClient { - mockClient := newHttpClientMock("foobar") + func() httpclient.IHttpClient { + mockClient := httpclient.NewHttpClientMock("foobar") mockClient.MockGetSearchStream = func(queryParams string) (*http.Response, error) { return &http.Response{ StatusCode: http.StatusOK, @@ -91,7 +92,6 @@ func TestStartStream(t *testing.T) { expected, _ := tt.result.Data.([]byte) res, _ := r.Data.([]byte) - if string(expected) != string(res) { t.Errorf("got %v, want %s", result, tt.result) } diff --git a/token_generator.go b/token_generator.go index 9d7d7ad..655b802 100644 --- a/token_generator.go +++ b/token_generator.go @@ -3,6 +3,7 @@ package twitterstream import ( "encoding/base64" "encoding/json" + "github.com/fallenstedt/twitter-stream/httpclient" ) type ( @@ -12,7 +13,7 @@ type ( SetApiKeyAndSecret(apiKey, apiSecret string) *tokenGenerator } tokenGenerator struct { - httpClient IHttpClient + httpClient httpclient.IHttpClient apiKey string apiSecret string } @@ -22,7 +23,7 @@ type ( } ) -func newTokenGenerator(httpClient IHttpClient) *tokenGenerator { +func newTokenGenerator(httpClient httpclient.IHttpClient) *tokenGenerator { return &tokenGenerator{httpClient: httpClient} } @@ -36,16 +37,16 @@ func (a *tokenGenerator) SetApiKeyAndSecret(apiKey, apiSecret string) *tokenGene // RequestBearerToken requests a bearer token from twitter using the apiKey and apiSecret. func (a *tokenGenerator) RequestBearerToken() (*requestBearerTokenResponse, error) { - resp, err := a.httpClient.newHttpRequest(&requestOpts{ + resp, err := a.httpClient.NewHttpRequest(&httpclient.RequestOpts{ Headers: []struct { - key string - value string + Key string + Value string }{ {"Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"}, {"Authorization", "Basic " + a.base64EncodeKeys()}, }, Method: "POST", - Url: endpoints["token"], + Url: httpclient.Endpoints["token"], Body: "grant_type=client_credentials", }) diff --git a/token_generator_test.go b/token_generator_test.go index 005862c..40decc7 100644 --- a/token_generator_test.go +++ b/token_generator_test.go @@ -3,6 +3,7 @@ package twitterstream import ( "bytes" "fmt" + "github.com/fallenstedt/twitter-stream/httpclient" "io/ioutil" "net/http" "testing" @@ -21,7 +22,7 @@ func TestSetApiKeyAndSecret(t *testing.T) { for i, tt := range tests { testName := fmt.Sprintf("(%d) %s %s", i, tt.apiKey, tt.apiSecret) t.Run(testName, func(t *testing.T) { - result := newTokenGenerator(newHttpClientMock("")) + result := newTokenGenerator(httpclient.NewHttpClientMock("")) result.SetApiKeyAndSecret(tt.apiKey, tt.apiSecret) if result.apiKey != tt.result.apiKey { @@ -37,10 +38,10 @@ func TestSetApiKeyAndSecret(t *testing.T) { func TestRequestBearerToken(t *testing.T) { var tests = []struct { - mockRequest func(opts *requestOpts) (*http.Response, error) + mockRequest func(opts *httpclient.RequestOpts) (*http.Response, error) result *requestBearerTokenResponse }{ - {func(opts *requestOpts) (*http.Response, error) { + {func(opts *httpclient.RequestOpts) (*http.Response, error) { json := `{ "token_type": "bearer", @@ -64,7 +65,7 @@ func TestRequestBearerToken(t *testing.T) { testName := fmt.Sprintf("(%d)", i) t.Run(testName, func(t *testing.T) { - mockClient := newHttpClientMock("") + mockClient := httpclient.NewHttpClientMock("") mockClient.MockNewHttpRequest = tt.mockRequest instance := newTokenGenerator(mockClient) diff --git a/twitterstream.go b/twitterstream.go index 2c47a44..7e58292 100644 --- a/twitterstream.go +++ b/twitterstream.go @@ -1,6 +1,8 @@ // Package twitterstream provides an easy way to stream tweets using Twitter's v2 Streaming API. package twitterstream +import "github.com/fallenstedt/twitter-stream/httpclient" + type twitterApi struct { Rules IRules Stream IStream @@ -8,7 +10,7 @@ type twitterApi struct { // NewTokenGenerator creates a tokenGenerator which can request a Bearer token using a twitter api key and secret. func NewTokenGenerator() *tokenGenerator { - client := newHttpClient("") + client := httpclient.NewHttpClient("") tokenGenerator := newTokenGenerator(client) return tokenGenerator } @@ -16,7 +18,7 @@ func NewTokenGenerator() *tokenGenerator { // NewTwitterStream consumes a twitter Bearer token. // It is used to interact with Twitter's v2 filtered streaming API func NewTwitterStream(token string) *twitterApi { - client := newHttpClient(token) + client := httpclient.NewHttpClient(token) rules := newRules(client) stream := newStream(client, newStreamResponseBodyReader()) return &twitterApi{Rules: rules, Stream: stream}