1// This is a pure JS module so testing with node is fine.
2
3import {
4  TokenResponse,
5  getCurrentTimeInSeconds,
6  AccessTokenRequest,
7  RevokeTokenRequest,
8  RefreshTokenRequest,
9} from '../TokenRequest';
10import { TokenTypeHint } from '../TokenRequest.types';
11
12describe('AccessTokenRequest', () => {
13  it(`creates a token exchange request`, async () => {
14    const request = new AccessTokenRequest({
15      code: 'bacon-some-code',
16      redirectUri: 'bcn://oauth',
17      clientId: 'my-client_id',
18      scopes: ['test', 'value'],
19    });
20    // Test the query when the client secret isn't provided.
21    expect(request.getQueryBody()).toStrictEqual({
22      client_id: 'my-client_id',
23      code: 'bacon-some-code',
24      grant_type: 'authorization_code',
25      redirect_uri: 'bcn://oauth',
26      scope: 'test value',
27    });
28
29    // Test the JSON access.
30    expect(request.getRequestConfig()).toStrictEqual({
31      clientId: 'my-client_id',
32      code: 'bacon-some-code',
33      grantType: 'authorization_code',
34      redirectUri: 'bcn://oauth',
35      scopes: ['test', 'value'],
36      clientSecret: undefined,
37      extraParams: undefined,
38    });
39    // No Authorization header is included when the client secret isn't present.
40    expect(request.getHeaders()).toStrictEqual({
41      'Content-Type': 'application/x-www-form-urlencoded',
42    });
43  });
44  // This is required for reddit auth.
45  it(`can generate a fake Authorization header when the client secret is an empty string`, async () => {
46    const request = new AccessTokenRequest({
47      code: 'bacon-some-code',
48      clientSecret: '',
49      redirectUri: 'bcn://oauth',
50      clientId: 'my-client_id',
51    });
52
53    // Ensure the Authorization header is generated even with an empty string.
54    expect(request.getHeaders()).toStrictEqual({
55      Authorization: 'Basic bXktY2xpZW50X2lkOg==',
56      'Content-Type': 'application/x-www-form-urlencoded',
57    });
58  });
59
60  it(`creates a token exchange request with a client secret`, async () => {
61    const request = new AccessTokenRequest({
62      code: 'bacon-some-code',
63      clientSecret: 'secret',
64      redirectUri: 'bcn://oauth',
65      clientId: 'my-client_id',
66    });
67    // Test the query doesn't include the client id when the secret is present.
68    expect(request.getQueryBody()).toStrictEqual({
69      code: 'bacon-some-code',
70      grant_type: 'authorization_code',
71      redirect_uri: 'bcn://oauth',
72    });
73
74    // Ensure the client secret is added to the headers.
75    expect(request.getHeaders()).toStrictEqual({
76      Authorization: 'Basic bXktY2xpZW50X2lkOnNlY3JldA==',
77      'Content-Type': 'application/x-www-form-urlencoded',
78    });
79  });
80  it(`throws when a discovery doesn't contain a tokenEndpoint`, async () => {
81    const request = new AccessTokenRequest({
82      code: 'bacon-some-code',
83      redirectUri: 'bcn://oauth',
84      clientId: 'my-client_id',
85    });
86    expect(request.performAsync({ tokenEndpoint: undefined })).rejects.toThrow(
87      'without a valid tokenEndpoint'
88    );
89  });
90});
91
92describe('RefreshTokenRequest', () => {
93  it(`creates a token refresh request`, async () => {
94    const request = new RefreshTokenRequest({
95      refreshToken: 'refresh-token',
96      clientId: 'my-client_id',
97      scopes: ['test', 'value'],
98      extraParams: {
99        batman: 'and-robin',
100      },
101    });
102    // Test the query when the client secret isn't provided.
103    expect(request.getQueryBody()).toStrictEqual({
104      client_id: 'my-client_id',
105      grant_type: 'refresh_token',
106      refresh_token: 'refresh-token',
107      scope: 'test value',
108      batman: 'and-robin',
109    });
110
111    // Test the JSON access.
112    expect(request.getRequestConfig()).toStrictEqual({
113      clientId: 'my-client_id',
114      grantType: 'refresh_token',
115      refreshToken: 'refresh-token',
116      scopes: ['test', 'value'],
117      clientSecret: undefined,
118      extraParams: {
119        batman: 'and-robin',
120      },
121    });
122    // No Authorization header is included when the client secret isn't present.
123    expect(request.getHeaders()).toStrictEqual({
124      'Content-Type': 'application/x-www-form-urlencoded',
125    });
126  });
127
128  it(`throws when a discovery doesn't contain a tokenEndpoint`, async () => {
129    const request = new RefreshTokenRequest({
130      refreshToken: 'refresh-token',
131      clientId: 'my-client_id',
132      scopes: ['test', 'value'],
133    });
134    expect(request.performAsync({ tokenEndpoint: undefined })).rejects.toThrow(
135      'without a valid tokenEndpoint'
136    );
137  });
138});
139
140describe('RevokeTokenRequest', () => {
141  it(`creates a token revocation request`, async () => {
142    const request = new RevokeTokenRequest({
143      token: 'my-token',
144      tokenTypeHint: TokenTypeHint.AccessToken,
145      clientId: 'my-client_id',
146      scopes: ['test', 'value'],
147    });
148    // Test the query is serialized properly.
149    expect(request.getQueryBody()).toStrictEqual({
150      client_id: 'my-client_id',
151      token: 'my-token',
152      token_type_hint: 'access_token',
153    });
154    // No Authorization header is included when the client secret isn't present.
155    expect(request.getHeaders()).toStrictEqual({
156      'Content-Type': 'application/x-www-form-urlencoded',
157    });
158  });
159
160  it(`creates a token revocation request with a client secret`, async () => {
161    const request = new RevokeTokenRequest({
162      token: 'my-token',
163      tokenTypeHint: TokenTypeHint.AccessToken,
164      clientId: 'my-client_id',
165      clientSecret: 'my-client_secret',
166    });
167    // Test the query is serialized properly.
168    expect(request.getQueryBody()).toStrictEqual({
169      // The client_id is currently being kept in the query body
170      client_id: 'my-client_id',
171      client_secret: 'my-client_secret',
172      token: 'my-token',
173      token_type_hint: 'access_token',
174    });
175    // Ensure the client secret is added to the headers.
176    expect(request.getHeaders()).toStrictEqual({
177      Authorization: 'Basic bXktY2xpZW50X2lkOm15LWNsaWVudF9zZWNyZXQ=',
178      'Content-Type': 'application/x-www-form-urlencoded',
179    });
180  });
181  it(`throws when a discovery doesn't contain a revocationEndpoint`, async () => {
182    const request = new RevokeTokenRequest({
183      token: 'my-token',
184      tokenTypeHint: TokenTypeHint.AccessToken,
185      clientId: 'my-client_id',
186    });
187    expect(request.performAsync({ revocationEndpoint: undefined })).rejects.toThrow(
188      'without a valid revocationEndpoint'
189    );
190  });
191});
192
193describe('TokenResponse', () => {
194  it(`can always refresh when no expiresIn attribute was provided`, () => {
195    const tenMinsAgo = getCurrentTimeInSeconds() - 3600;
196    const freshToken = new TokenResponse({ accessToken: '', issuedAt: tenMinsAgo });
197    expect(TokenResponse.isTokenFresh(freshToken)).toBe(true);
198  });
199  it(`knows a token isn't fresh`, () => {
200    const fiveMins = 1800;
201    const tenMinsAgo = getCurrentTimeInSeconds() - 3600;
202    const freshToken = new TokenResponse({
203      accessToken: '',
204      issuedAt: tenMinsAgo,
205      expiresIn: fiveMins,
206    });
207    expect(TokenResponse.isTokenFresh(freshToken, 0)).toBe(false);
208  });
209});
210