1package helix
2
3import (
4	"net/http"
5	"testing"
6)
7
8func TestGetAuthorizationURL(t *testing.T) {
9	t.Parallel()
10
11	testCases := []struct {
12		state       string
13		forceVerify bool
14		options     *Options
15		expectedURL string
16	}{
17		{
18			"",
19			false,
20			&Options{
21				ClientID:    "my-client-id",
22				RedirectURI: "https://example.com/auth/callback",
23			},
24			"https://id.twitch.tv/oauth2/authorize?response_type=code&client_id=my-client-id&redirect_uri=https://example.com/auth/callback",
25		},
26		{
27			"some-state",
28			true,
29			&Options{
30				ClientID:    "my-client-id",
31				RedirectURI: "https://example.com/auth/callback",
32				Scopes:      []string{"analytics:read:games", "bits:read", "clips:edit", "user:edit", "user:read:email"},
33			},
34			"https://id.twitch.tv/oauth2/authorize?response_type=code&client_id=my-client-id&redirect_uri=https://example.com/auth/callback&state=some-state&force_verify=true&scope=analytics:read:games%20bits:read%20clips:edit%20user:edit%20user:read:email",
35		},
36	}
37
38	for _, testCase := range testCases {
39
40		client, err := NewClient(testCase.options)
41		if err != nil {
42			t.Errorf("Did not expect an error, got \"%s\"", err.Error())
43		}
44
45		url := client.GetAuthorizationURL(testCase.state, testCase.forceVerify)
46
47		if url != testCase.expectedURL {
48			t.Errorf("expected url to be \"%s\", got \"%s\"", testCase.expectedURL, url)
49		}
50	}
51}
52
53func TestGetAppAccessToken(t *testing.T) {
54	t.Parallel()
55
56	testCases := []struct {
57		statusCode     int
58		options        *Options
59		respBody       string
60		expectedErrMsg string
61	}{
62		{
63			http.StatusBadRequest,
64			&Options{
65				ClientID:     "invalid-client-id", // invalid client id
66				ClientSecret: "valid-client-secret",
67			},
68			`{"status":400,"message":"invalid client"}`,
69			"invalid client",
70		},
71		{
72			http.StatusForbidden,
73			&Options{
74				ClientID:     "valid-client-id",
75				ClientSecret: "invalid-client-secret", // invalid client secret
76			},
77			`{"status":403,"message":"invalid client secret"}`,
78			"invalid client secret",
79		},
80		{
81			http.StatusOK,
82			&Options{
83				ClientID:     "valid-client-id",
84				ClientSecret: "valid-client-secret",
85			},
86			`{"access_token":"ajsdfloehfoihsdfhoasjfdpoiqh","expires_in":4999199}`,
87			"",
88		},
89	}
90
91	for _, testCase := range testCases {
92		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
93
94		resp, err := c.GetAppAccessToken()
95		if err != nil {
96			t.Error(err)
97		}
98
99		if resp.StatusCode != testCase.statusCode {
100			t.Errorf("expected status code to be \"%d\", got \"%d\"", testCase.statusCode, resp.StatusCode)
101		}
102
103		// Test error cases
104		if resp.StatusCode != http.StatusOK {
105			if resp.ErrorStatus != testCase.statusCode {
106				t.Errorf("expected error status to be \"%d\", got \"%d\"", testCase.statusCode, resp.ErrorStatus)
107			}
108
109			if resp.ErrorMessage != testCase.expectedErrMsg {
110				t.Errorf("expected error message to be \"%s\", got \"%s\"", testCase.expectedErrMsg, resp.ErrorMessage)
111			}
112
113			continue
114		}
115
116		// Test success cases
117		if resp.Data.AccessToken == "" {
118			t.Errorf("expected an access token but got an empty string")
119		}
120
121		if resp.Data.ExpiresIn == 0 {
122			t.Errorf("expected ExpiresIn to not be \"0\"")
123		}
124	}
125}
126
127func TestGetUserAccessToken(t *testing.T) {
128	t.Parallel()
129
130	testCases := []struct {
131		statusCode     int
132		code           string
133		scopes         []string
134		options        *Options
135		respBody       string
136		expectedErrMsg string
137	}{
138		{
139			http.StatusBadRequest,
140			"invalid-auth-code", // invalid auth code
141			[]string{"user:read:email"},
142			&Options{
143				ClientID:     "valid-client-id",
144				ClientSecret: "valid-client-secret",
145				RedirectURI:  "https://example.com/auth/callback",
146			},
147			`{"status":400,"message":"Invalid authorization code"}`,
148			"Invalid authorization code",
149		},
150		{
151			http.StatusBadRequest,
152			"valid-auth-code",
153			[]string{"user:read:email"},
154			&Options{
155				ClientID:     "invalid-client-id", // invalid client id
156				ClientSecret: "valid-client-secret",
157				RedirectURI:  "https://example.com/auth/callback",
158			},
159			`{"status":400,"message":"invalid client"}`,
160			"invalid client",
161		},
162		{
163			http.StatusForbidden,
164			"valid-auth-code",
165			[]string{"user:read:email"},
166			&Options{
167				ClientID:     "valid-client-id",
168				ClientSecret: "invalid-client-secret", // invalid client secret
169				RedirectURI:  "https://example.com/auth/callback",
170			},
171			`{"status":403,"message":"invalid client secret"}`,
172			"invalid client secret",
173		},
174		{
175			http.StatusBadRequest,
176			"valid-auth-code",
177			[]string{"user:read:email"},
178			&Options{
179				ClientID:     "valid-client-id",
180				ClientSecret: "valid-client-secret",
181				RedirectURI:  "https://example.com/invalid/callback", // invalid redirect uri
182			},
183			`{"status":400,"message":"Parameter redirect_uri does not match registeredURI"}`,
184			"Parameter redirect_uri does not match registeredURI",
185		},
186		{
187			http.StatusOK,
188			"valid-auth-code",
189			[]string{}, // no scopes
190			&Options{
191				ClientID:     "valid-client-id",
192				ClientSecret: "valid-client-secret",
193				RedirectURI:  "https://example.com/auth/callback",
194			},
195			`{"access_token":"kagsfkgiuowegfkjsbdcuiwebf","expires_in":14146,"refresh_token":"fiuhgaofohofhohdflhoiwephvlhowiehfoi"}`,
196			"",
197		},
198		{
199			http.StatusOK,
200			"valid-auth-code",
201			[]string{"analytics:read:games", "bits:read", "clips:edit", "user:edit", "user:read:email"},
202			&Options{
203				ClientID:     "valid-client-id",
204				ClientSecret: "valid-client-secret",
205				RedirectURI:  "https://example.com/auth/callback",
206			},
207			`{"access_token":"kagsfkgiuowegfkjsbdcuiwebf","expires_in":14154,"refresh_token":"fiuhgaofohofhohdflhoiwephvlhowiehfoi","scope":["analytics:read:games","bits:read","clips:edit","user:edit","user:read:email"]}`,
208			"",
209		},
210	}
211
212	for _, testCase := range testCases {
213		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
214
215		resp, err := c.GetUserAccessToken(testCase.code)
216		if err != nil {
217			t.Error(err)
218		}
219
220		if resp.StatusCode != testCase.statusCode {
221			t.Errorf("expected status code to be \"%d\", got \"%d\"", testCase.statusCode, resp.StatusCode)
222		}
223
224		// Test error cases
225		if resp.StatusCode != http.StatusOK {
226			if resp.ErrorStatus != testCase.statusCode {
227				t.Errorf("expected error status to be \"%d\", got \"%d\"", testCase.statusCode, resp.ErrorStatus)
228			}
229
230			if resp.ErrorMessage != testCase.expectedErrMsg {
231				t.Errorf("expected error message to be \"%s\", got \"%s\"", testCase.expectedErrMsg, resp.ErrorMessage)
232			}
233
234			continue
235		}
236
237		// Test success cases
238		if resp.Data.AccessToken == "" {
239			t.Errorf("expected an access token but got an empty string")
240		}
241
242		if resp.Data.RefreshToken == "" {
243			t.Errorf("expected a refresh token but got an empty string")
244		}
245
246		if resp.Data.ExpiresIn == 0 {
247			t.Errorf("expected ExpiresIn to not be \"0\"")
248		}
249
250		if len(resp.Data.Scopes) != len(testCase.scopes) {
251			t.Errorf("expected number of scope to be \"%d\", got \"%d\"", len(testCase.scopes), len(resp.Data.Scopes))
252		}
253	}
254}
255
256func TestRefreshUserAccessToken(t *testing.T) {
257	t.Parallel()
258
259	testCases := []struct {
260		statusCode     int
261		refreshToken   string
262		options        *Options
263		respBody       string
264		expectedErrMsg string
265		expectedScopes []string
266	}{
267		{
268			http.StatusBadRequest,
269			"", // no refresh token
270			&Options{
271				ClientID:     "valid-client-id",
272				ClientSecret: "valid-client-secret",
273			},
274			`{"status":400,"message":"missing refresh token"}`,
275			"missing refresh token",
276			[]string{},
277		},
278		{
279			http.StatusBadRequest,
280			"invalid-refresh-token", // invalid refresh token
281			&Options{
282				ClientID:     "valid-client-id",
283				ClientSecret: "valid-client-secret",
284			},
285			`{"status":400,"message":"Invalid refresh token"}`,
286			"Invalid refresh token",
287			[]string{},
288		},
289		{
290			http.StatusBadRequest,
291			"valid-refresh-token",
292			&Options{
293				ClientID:     "invalid-client-id", // invalid client id
294				ClientSecret: "valid-client-secret",
295			},
296			`{"status":400,"message":"invalid client"}`,
297			"invalid client",
298			[]string{},
299		},
300		{
301			http.StatusForbidden,
302			"valid-refresh-token",
303			&Options{
304				ClientID:     "valid-client-id",
305				ClientSecret: "invalid-client-secret", // invalid client secret
306			},
307			`{"status":403,"message":"invalid client secret"}`,
308			"invalid client secret",
309			[]string{},
310		},
311		{
312			http.StatusBadRequest,
313			"valid-refresh-token",
314			&Options{
315				ClientID:     "valid-client-id",
316				ClientSecret: "valid-client-secret",
317			},
318			`{"status":400,"message":"invalid scope requested: 'invalid:scope'"}`,
319			"invalid scope requested: 'invalid:scope'",
320			[]string{},
321		},
322		{
323			http.StatusOK,
324			"valid-refresh-token",
325			&Options{
326				ClientID:     "valid-client-id",
327				ClientSecret: "valid-client-secret",
328			},
329			`{"access_token":"oihhkfhsajkhfjksahfkjahsf","expires_in":13669,"refresh_token":"oihhkfhsajkhfjksahfkjahsfahsldhasld","scope":["analytics:read:games","bits:read","clips:edit","user:edit","user:read:email"]}`,
330			"",
331			[]string{"analytics:read:games", "bits:read", "clips:edit", "user:edit", "user:read:email"},
332		},
333	}
334
335	for _, testCase := range testCases {
336		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
337
338		resp, err := c.RefreshUserAccessToken(testCase.refreshToken)
339		if err != nil {
340			t.Error(err)
341		}
342
343		if resp.StatusCode != testCase.statusCode {
344			t.Errorf("expected status code to be \"%d\", got \"%d\"", testCase.statusCode, resp.StatusCode)
345		}
346
347		// Test error cases
348		if resp.StatusCode != http.StatusOK {
349			if resp.ErrorStatus != testCase.statusCode {
350				t.Errorf("expected error status to be \"%d\", got \"%d\"", testCase.statusCode, resp.ErrorStatus)
351			}
352
353			if resp.ErrorMessage != testCase.expectedErrMsg {
354				t.Errorf("expected error message to be \"%s\", got \"%s\"", testCase.expectedErrMsg, resp.ErrorMessage)
355			}
356
357			continue
358		}
359
360		// // Test success cases
361		if resp.Data.AccessToken == "" {
362			t.Errorf("expected an access token but got an empty string")
363		}
364
365		if resp.Data.RefreshToken == "" {
366			t.Errorf("expected a refresh token but got an empty string")
367		}
368
369		if resp.Data.ExpiresIn == 0 {
370			t.Errorf("expected ExpiresIn to not be \"0\"")
371		}
372
373		if len(resp.Data.Scopes) != len(testCase.expectedScopes) {
374			t.Errorf("expected number of scope to be \"%d\", got \"%d\"", len(testCase.expectedScopes), len(resp.Data.Scopes))
375		}
376	}
377}
378
379func TestRevokeUserAccessToken(t *testing.T) {
380	t.Parallel()
381
382	testCases := []struct {
383		statusCode     int
384		accessToken    string
385		options        *Options
386		respBody       string
387		expectedErrMsg string
388	}{
389		{
390			http.StatusBadRequest,
391			"valid-access-token",
392			&Options{ClientID: "invalid-client-id"}, // invalid client id
393			`{"status":400,"message":"Invalid client_id: invalid-client-id"}`,
394			"Invalid client_id: invalid-client-id",
395		},
396		{
397			http.StatusBadRequest,
398			"", // no access token
399			&Options{ClientID: "valid-client-id"},
400			`{"status":400,"message":"missing oauth token"}`,
401			"missing oauth token",
402		},
403		{
404			http.StatusOK,
405			"invalid-access-token", // invalid token still returns 200 OK response
406			&Options{ClientID: "valid-client-id"},
407			"",
408			"",
409		},
410		{
411			http.StatusOK,
412			"valid-access-token",
413			&Options{ClientID: "valid-client-id"},
414			"",
415			"",
416		},
417	}
418
419	for _, testCase := range testCases {
420		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
421
422		resp, err := c.RevokeUserAccessToken(testCase.accessToken)
423		if err != nil {
424			t.Error(err)
425		}
426
427		if resp.StatusCode != testCase.statusCode {
428			t.Errorf("expected status code to be \"%d\", got \"%d\"", testCase.statusCode, resp.StatusCode)
429		}
430
431		// Test error cases
432		if resp.StatusCode != http.StatusOK {
433			if testCase.expectedErrMsg != "" && resp.ErrorMessage != testCase.expectedErrMsg {
434				t.Errorf("expected error message to be \"%s\", got \"%s\"", testCase.expectedErrMsg, resp.ErrorMessage)
435			}
436
437			if resp.ErrorStatus != testCase.statusCode {
438				t.Errorf("expected error status to be \"%d\", got \"%d\"", testCase.statusCode, resp.ErrorStatus)
439			}
440
441			continue
442		}
443	}
444}
445