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