Skip to content

Commit 638dc1b

Browse files
committed
Improve test coverage
1 parent cf2b707 commit 638dc1b

7 files changed

Lines changed: 426 additions & 161 deletions

File tree

api/api_app_access_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package api_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"github.com/adeithe/go-twitch/api"
9+
"github.com/adeithe/go-twitch/apitest"
10+
"github.com/stretchr/testify/require"
11+
"golang.org/x/oauth2"
12+
)
13+
14+
func TestAuthorization_AppAccess(t *testing.T) {
15+
mock := apitest.NewMockAPI(t, apitest.EnableHTTP2(), apitest.WithTLS())
16+
ctx, cancel := context.WithCancel(context.WithValue(t.Context(), oauth2.HTTPClient, mock.Client()))
17+
defer cancel()
18+
19+
clientID, clientSecret, err := mock.RegisterApplication()
20+
require.NoError(t, err)
21+
22+
t.Run("Valid", func(t *testing.T) {
23+
auth := api.AppAccess(clientID, clientSecret)
24+
token1, err := auth.Token(ctx)
25+
require.NoError(t, err)
26+
require.Empty(t, token1.RefreshToken)
27+
require.NotEmpty(t, token1.AccessToken)
28+
29+
token2, err := auth.Token(ctx)
30+
require.NoError(t, err)
31+
require.Empty(t, token1.RefreshToken)
32+
require.NotEmpty(t, token2.AccessToken)
33+
require.Equal(t, token1.AccessToken, token2.AccessToken)
34+
})
35+
36+
t.Run("Expired", func(t *testing.T) {
37+
auth := api.AppAccess(clientID, clientSecret)
38+
token1, err := auth.Token(ctx)
39+
require.NoError(t, err)
40+
require.Empty(t, token1.RefreshToken)
41+
require.NotEmpty(t, token1.AccessToken)
42+
token1.Expiry = time.Now().Add(-time.Hour)
43+
44+
token2, err := auth.Token(ctx)
45+
require.NoError(t, err)
46+
require.Empty(t, token1.RefreshToken)
47+
require.NotEmpty(t, token2.AccessToken)
48+
require.NotEqual(t, token1.AccessToken, token2.AccessToken)
49+
})
50+
}

api/api_calls_test.go

Lines changed: 254 additions & 154 deletions
Large diffs are not rendered by default.

api/api_user_token.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func (a *userToken) Client(ctx context.Context) *http.Client {
2626
return oauth2.NewClient(ctx, a.TokenSource(ctx))
2727
}
2828

29-
// TokenSource returns an oauth2.TokenSource that maintains a Twitch API app access token.
29+
// TokenSource returns an oauth2.TokenSource that maintains a Twitch API user access token.
3030
func (a *userToken) TokenSource(ctx context.Context) oauth2.TokenSource {
3131
if a.source != nil {
3232
return a.source
@@ -35,7 +35,7 @@ func (a *userToken) TokenSource(ctx context.Context) oauth2.TokenSource {
3535
return a.source
3636
}
3737

38-
// Token returns a valid Twitch API app access token, renewing it if necessary.
38+
// Token returns a valid Twitch API user access token, renewing it if necessary.
3939
func (a *userToken) Token(ctx context.Context) (*oauth2.Token, error) {
4040
return a.TokenSource(ctx).Token()
4141
}

api/api_user_token_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package api_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"github.com/adeithe/go-twitch/api"
9+
"github.com/adeithe/go-twitch/apitest"
10+
"github.com/stretchr/testify/require"
11+
"golang.org/x/oauth2"
12+
"golang.org/x/oauth2/twitch"
13+
)
14+
15+
func TestAuthorization_UserToken(t *testing.T) {
16+
mock := apitest.NewMockAPI(t, apitest.EnableHTTP2(), apitest.WithTLS())
17+
ctx, cancel := context.WithCancel(context.WithValue(t.Context(), oauth2.HTTPClient, mock.Client()))
18+
defer cancel()
19+
20+
clientID, clientSecret, err := mock.RegisterApplication()
21+
require.NoError(t, err)
22+
oauthConfig := &oauth2.Config{
23+
ClientID: clientID,
24+
ClientSecret: clientSecret,
25+
Endpoint: twitch.Endpoint,
26+
RedirectURL: "http://localhost:8080/callback",
27+
}
28+
29+
t.Run("Valid", func(t *testing.T) {
30+
exchange, err := oauthConfig.Exchange(ctx, "gulfwdmys5lsm6qyz4xiz9q32l10", oauth2.AccessTypeOffline)
31+
require.NoError(t, err)
32+
33+
auth := api.UserToken(oauthConfig, exchange)
34+
token, err := auth.Token(ctx)
35+
require.NoError(t, err)
36+
require.Equal(t, exchange.AccessToken, token.AccessToken)
37+
})
38+
39+
t.Run("Expired", func(t *testing.T) {
40+
exchange, err := oauthConfig.Exchange(ctx, "gulfwdmys5lsm6qyz4xiz9q32l10", oauth2.AccessTypeOffline)
41+
require.NoError(t, err)
42+
43+
exchange.Expiry = time.Now().Add(-time.Hour)
44+
auth := api.UserToken(oauthConfig, exchange)
45+
token, err := auth.Token(ctx)
46+
require.NoError(t, err)
47+
require.NotEqual(t, exchange.AccessToken, token.AccessToken)
48+
})
49+
}

apitest/options.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ func QueryParamEquals(key, value string) ValidatorFunc {
9494
}
9595

9696
// BodyParamEquals returns a ValidatorFunc that checks if a body parameter equals a specific value.
97-
func BodyParamEquals(key, value string) ValidatorFunc {
97+
func BodyParamEquals[T comparable](key string, value T) ValidatorFunc {
9898
return func(req *http.Request) error {
9999
m := make(map[string]any)
100100
bs, _ := io.ReadAll(req.Body)
@@ -108,8 +108,12 @@ func BodyParamEquals(key, value string) ValidatorFunc {
108108
return errors.New("missing body parameter: " + key)
109109
}
110110

111+
if _, ok := val.(T); !ok {
112+
return errors.New("body parameter " + key + " is not of expected type, got " + fmt.Sprintf("%T", val))
113+
}
114+
111115
if val != value {
112-
return errors.New("expected body parameter " + key + " to be " + value + ", got " + fmt.Sprintf("%v", val))
116+
return errors.New("expected body parameter " + key + " to be " + fmt.Sprintf("%v", value) + ", got " + fmt.Sprintf("%v", val))
113117
}
114118
return nil
115119
}

apitest/server.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ type ValidatorFunc func(req *http.Request) error
5555
//
5656
// If a response has already been added for the given path and method, it will be overwritten.
5757
func SetMockValidator(m *MockTwitchAPI, method, path string, validators ...ValidatorFunc) *MockTwitchAPIEndpoint {
58-
return SetMockResponse(m, method, path, &api.ResponseData[any]{}, validators...)
58+
return SetMockResponse[any](m, method, path, nil, validators...)
5959
}
6060

6161
// SetMockResponse adds a mock response for the given path.
@@ -106,7 +106,7 @@ func NewMockAPI(t TestingT, opts ...MockTwitchAPIOption) *MockTwitchAPI {
106106
client := mock.Client()
107107
url, _ := url.Parse(srv.URL)
108108
client.Transport = newMockTransport(client, url)
109-
mux.Handle("/id/twitch/tv/oauth2/token", mock)
109+
mux.Handle("/id/twitch/tv/{rest...}", mock)
110110
mux.Handle("/api/twitch/tv/{rest...}", mock)
111111
return mock
112112
}
@@ -257,6 +257,9 @@ func (m *MockTwitchAPI) ServeHTTP(res http.ResponseWriter, req *http.Request) {
257257

258258
handler.Successes++
259259
res.WriteHeader(data.Status)
260+
if handler.data == nil {
261+
return
262+
}
260263
_ = writer.Encode(handler.data)
261264
}
262265

@@ -312,7 +315,8 @@ func (m *MockTwitchAPI) handleOAuth(res http.ResponseWriter, req *http.Request)
312315

313316
switch grantType {
314317
case "client_credentials":
315-
case "code":
318+
break
319+
case "authorization_code", "refresh_token":
316320
refreshToken = token.RefreshToken
317321
default:
318322
m.oauthToken.Failures++

apitest/server_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ package apitest_test
22

33
import (
44
"context"
5+
"io"
56
"net/http"
67
"testing"
78

89
"github.com/adeithe/go-twitch/api"
910
"github.com/adeithe/go-twitch/apitest"
1011
"github.com/stretchr/testify/require"
12+
"golang.org/x/oauth2"
13+
"golang.org/x/oauth2/twitch"
1114
)
1215

1316
func TestMockAPI(t *testing.T) {
@@ -278,3 +281,58 @@ func TestMockAPI_TokenMismatch(t *testing.T) {
278281
require.Exactly(t, 0, endpoint.Successes)
279282
require.Exactly(t, 1, endpoint.Failures)
280283
}
284+
285+
func TestMockAPI_OAuth2_EndpointNotFound(t *testing.T) {
286+
// The /oauth2/authorize endpoint is not supported by the apitest package.
287+
// This is because it's the user-facing endpoint for Twitch's OAuth2 flow and is not used by the API client.
288+
// This test ensures that requests to unsupported endpoints are properly handled by the mock server.
289+
req, err := http.NewRequest(http.MethodPost, "http://id.twitch.tv/oauth2/authorize", nil)
290+
require.NoError(t, err)
291+
292+
mock := apitest.NewMockAPI(t)
293+
res, err := mock.Client().Do(req)
294+
require.NoError(t, err)
295+
require.Equal(t, http.StatusNotFound, res.StatusCode)
296+
297+
bs, err := io.ReadAll(res.Body)
298+
require.NoError(t, err)
299+
require.Exactly(t, "404 Not Found", string(bs))
300+
}
301+
302+
func TestMockAPI_OAuth2_InvalidClient(t *testing.T) {
303+
req, err := http.NewRequest(http.MethodPost, "http://id.twitch.tv/oauth2/token", nil)
304+
require.NoError(t, err)
305+
306+
mock := apitest.NewMockAPI(t)
307+
res, err := mock.Client().Do(req)
308+
require.NoError(t, err)
309+
require.Equal(t, http.StatusBadRequest, res.StatusCode)
310+
311+
bs, err := io.ReadAll(res.Body)
312+
require.NoError(t, err)
313+
require.JSONEq(t, `{"status": 400, "message": "invalid client"}`, string(bs))
314+
}
315+
316+
func TestMockAPI_OAuth2_InvalidGrantType(t *testing.T) {
317+
mock := apitest.NewMockAPI(t)
318+
ctx, cancel := context.WithCancel(context.WithValue(t.Context(), oauth2.HTTPClient, mock.Client()))
319+
defer cancel()
320+
321+
oauthEndpoint := mock.OAuthTokenEndpoint()
322+
clientID, clientSecret, err := mock.RegisterApplication()
323+
require.NoError(t, err)
324+
oauth2Config := &oauth2.Config{
325+
ClientID: clientID,
326+
ClientSecret: clientSecret,
327+
Endpoint: twitch.Endpoint,
328+
}
329+
330+
oauth2Config.Endpoint.AuthStyle = oauth2.AuthStyleInParams
331+
token, err := oauth2Config.PasswordCredentialsToken(ctx, "username", "password")
332+
require.Nil(t, token)
333+
require.Error(t, err)
334+
require.Contains(t, err.Error(), "invalid grant type")
335+
require.Exactly(t, 1, oauthEndpoint.TimesCalled)
336+
require.Exactly(t, 0, oauthEndpoint.Successes)
337+
require.Exactly(t, 1, oauthEndpoint.Failures)
338+
}

0 commit comments

Comments
 (0)