// This is a pure JS module so testing with node is fine. import { TokenResponse, getCurrentTimeInSeconds, AccessTokenRequest, RevokeTokenRequest, RefreshTokenRequest, } from '../TokenRequest'; import { TokenTypeHint } from '../TokenRequest.types'; describe('AccessTokenRequest', () => { it(`creates a token exchange request`, async () => { const request = new AccessTokenRequest({ code: 'bacon-some-code', redirectUri: 'bcn://oauth', clientId: 'my-client_id', scopes: ['test', 'value'], }); // Test the query when the client secret isn't provided. expect(request.getQueryBody()).toStrictEqual({ client_id: 'my-client_id', code: 'bacon-some-code', grant_type: 'authorization_code', redirect_uri: 'bcn://oauth', scope: 'test value', }); // Test the JSON access. expect(request.getRequestConfig()).toStrictEqual({ clientId: 'my-client_id', code: 'bacon-some-code', grantType: 'authorization_code', redirectUri: 'bcn://oauth', scopes: ['test', 'value'], clientSecret: undefined, extraParams: undefined, }); // No Authorization header is included when the client secret isn't present. expect(request.getHeaders()).toStrictEqual({ 'Content-Type': 'application/x-www-form-urlencoded', }); }); // This is required for reddit auth. it(`can generate a fake Authorization header when the client secret is an empty string`, async () => { const request = new AccessTokenRequest({ code: 'bacon-some-code', clientSecret: '', redirectUri: 'bcn://oauth', clientId: 'my-client_id', }); // Ensure the Authorization header is generated even with an empty string. expect(request.getHeaders()).toStrictEqual({ Authorization: 'Basic bXktY2xpZW50X2lkOg==', 'Content-Type': 'application/x-www-form-urlencoded', }); }); it(`creates a token exchange request with a client secret`, async () => { const request = new AccessTokenRequest({ code: 'bacon-some-code', clientSecret: 'secret', redirectUri: 'bcn://oauth', clientId: 'my-client_id', }); // Test the query doesn't include the client id when the secret is present. expect(request.getQueryBody()).toStrictEqual({ code: 'bacon-some-code', grant_type: 'authorization_code', redirect_uri: 'bcn://oauth', }); // Ensure the client secret is added to the headers. expect(request.getHeaders()).toStrictEqual({ Authorization: 'Basic bXktY2xpZW50X2lkOnNlY3JldA==', 'Content-Type': 'application/x-www-form-urlencoded', }); }); it(`throws when a discovery doesn't contain a tokenEndpoint`, async () => { const request = new AccessTokenRequest({ code: 'bacon-some-code', redirectUri: 'bcn://oauth', clientId: 'my-client_id', }); expect(request.performAsync({ tokenEndpoint: undefined })).rejects.toThrow( 'without a valid tokenEndpoint' ); }); }); describe('RefreshTokenRequest', () => { it(`creates a token refresh request`, async () => { const request = new RefreshTokenRequest({ refreshToken: 'refresh-token', clientId: 'my-client_id', scopes: ['test', 'value'], extraParams: { batman: 'and-robin', }, }); // Test the query when the client secret isn't provided. expect(request.getQueryBody()).toStrictEqual({ client_id: 'my-client_id', grant_type: 'refresh_token', refresh_token: 'refresh-token', scope: 'test value', batman: 'and-robin', }); // Test the JSON access. expect(request.getRequestConfig()).toStrictEqual({ clientId: 'my-client_id', grantType: 'refresh_token', refreshToken: 'refresh-token', scopes: ['test', 'value'], clientSecret: undefined, extraParams: { batman: 'and-robin', }, }); // No Authorization header is included when the client secret isn't present. expect(request.getHeaders()).toStrictEqual({ 'Content-Type': 'application/x-www-form-urlencoded', }); }); it(`throws when a discovery doesn't contain a tokenEndpoint`, async () => { const request = new RefreshTokenRequest({ refreshToken: 'refresh-token', clientId: 'my-client_id', scopes: ['test', 'value'], }); expect(request.performAsync({ tokenEndpoint: undefined })).rejects.toThrow( 'without a valid tokenEndpoint' ); }); }); describe('RevokeTokenRequest', () => { it(`creates a token revocation request`, async () => { const request = new RevokeTokenRequest({ token: 'my-token', tokenTypeHint: TokenTypeHint.AccessToken, clientId: 'my-client_id', scopes: ['test', 'value'], }); // Test the query is serialized properly. expect(request.getQueryBody()).toStrictEqual({ client_id: 'my-client_id', token: 'my-token', token_type_hint: 'access_token', }); // No Authorization header is included when the client secret isn't present. expect(request.getHeaders()).toStrictEqual({ 'Content-Type': 'application/x-www-form-urlencoded', }); }); it(`creates a token revocation request with a client secret`, async () => { const request = new RevokeTokenRequest({ token: 'my-token', tokenTypeHint: TokenTypeHint.AccessToken, clientId: 'my-client_id', clientSecret: 'my-client_secret', }); // Test the query is serialized properly. expect(request.getQueryBody()).toStrictEqual({ // The client_id is currently being kept in the query body client_id: 'my-client_id', client_secret: 'my-client_secret', token: 'my-token', token_type_hint: 'access_token', }); // Ensure the client secret is added to the headers. expect(request.getHeaders()).toStrictEqual({ Authorization: 'Basic bXktY2xpZW50X2lkOm15LWNsaWVudF9zZWNyZXQ=', 'Content-Type': 'application/x-www-form-urlencoded', }); }); it(`throws when a discovery doesn't contain a revocationEndpoint`, async () => { const request = new RevokeTokenRequest({ token: 'my-token', tokenTypeHint: TokenTypeHint.AccessToken, clientId: 'my-client_id', }); expect(request.performAsync({ revocationEndpoint: undefined })).rejects.toThrow( 'without a valid revocationEndpoint' ); }); }); describe('TokenResponse', () => { it(`can always refresh when no expiresIn attribute was provided`, () => { const tenMinsAgo = getCurrentTimeInSeconds() - 3600; const freshToken = new TokenResponse({ accessToken: '', issuedAt: tenMinsAgo }); expect(TokenResponse.isTokenFresh(freshToken)).toBe(true); }); it(`knows a token isn't fresh`, () => { const fiveMins = 1800; const tenMinsAgo = getCurrentTimeInSeconds() - 3600; const freshToken = new TokenResponse({ accessToken: '', issuedAt: tenMinsAgo, expiresIn: fiveMins, }); expect(TokenResponse.isTokenFresh(freshToken, 0)).toBe(false); }); });