Skip to content

Commit

Permalink
Improved jsdoc
Browse files Browse the repository at this point in the history
Removed restoring of original fetch
Updated tests
  • Loading branch information
Domagoj Rukavina committed Aug 18, 2017
1 parent 8eeb9dc commit e39fe02
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 84 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,7 @@
"nock": "^8.0.0",
"sinon": "^1.17.4"
},
"dependencies": {}
"dependencies": {
"lodash": "^4.17.4"
}
}
19 changes: 3 additions & 16 deletions src/AccessTokenProvider.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { ERROR_INVALID_CONFIG } from './const';

/**
* Provides a way for renewing access token with correct refresh token. It will automatically
* dispatch a call to server with request provided via config. It also ensures that
* access token is fetched only once no matter how many requests are trying to get
* a renewed version of access token at the moment. All subsequent requests will be chained
* to renewing fetch promise and resolved once the response is received.
*/
export default class AccessTokenProvider {
class AccessTokenProvider {
constructor(fetch, config) {
this.fetch = fetch;

Expand All @@ -29,20 +27,13 @@ export default class AccessTokenProvider {
this.handleFetchAccessTokenResponse = this.handleFetchAccessTokenResponse.bind(this);
this.handleAccessToken = this.handleAccessToken.bind(this);
this.handleError = this.handleError.bind(this);
this.isConfigValid = this.isConfigValid.bind(this);
}

/**
* Configures access token provider
*/
configure(config) {
this.config = { ...this.config, ...config };

if (!this.isConfigValid(this.config)) {
throw new Error(ERROR_INVALID_CONFIG);
}

this.config = config;
}

/**
Expand Down Expand Up @@ -135,10 +126,6 @@ export default class AccessTokenProvider {
.then(token => this.handleAccessToken(token, resolve))
.catch(error => this.handleError(error, reject));
}

isConfigValid() {
return this.config.isResponseUnauthorized &&
this.config.createAccessTokenRequest &&
this.config.parseAccessToken;
}
}

export default AccessTokenProvider;
143 changes: 96 additions & 47 deletions src/FetchInterceptor.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import isFunction from 'lodash/isFunction';
import {
ERROR_INVALID_CONFIG,
} from './const';
Expand All @@ -6,27 +7,70 @@ import TokenExpiredException from './services/TokenExpiredException';
import RetryCountExceededException from './services/RetryCountExceededException';
import AccessTokenProvider from './AccessTokenProvider';

/**
* Prepares signed request object which can be used for renewing access token
*
* @callback createAccessTokenRequest
* @param {string} refreshToken Refresh token used to sign the request
* @returns {Request} Signed request object which can be used to get access token
*/

/**
* Parses access token from access token response object
*
* @callback parseAccessToken
* @param {Response} response Response object with access token
* @returns {string} Access token parsed from response
*/

/**
* Checks whether interceptor will intercept this request or just let it pass through
*
* @callback shouldIntercept
* @param {Request} request Request object
* @returns {bool} A value indicating whether this request should be intercepted
*/

/**
* Checks whether provided response invalidates current access token
*
* @callback shouldInvalidateAccessToken
* @param {Response} response Response object
* @returns {bool} A value indicating whether token should be invalidated
*/

/**
* Adds authorization for intercepted requests
*
* @callback authorizeRequest
* @param {Request} request Request object being intercepted
* @param {string} accessToken Current access token
* @returns {Request} Authorized request object
*/

const getDefaultConfig = () => ({
fetchRetryCount: 1,
createAccessTokenRequest: null,
shouldIntercept: () => false,
shouldInvalidateAccessToken: () => false,
isResponseUnauthorized: http.isResponseUnauthorized,
parseAccessToken: null,
authorizeRequest: null,
onAccessTokenChange: null,
onResponse: null,
});

/**
* Provides a default implementation for intercepting fetch requests. It will try to resolve
* unauthorized responses by renewing the access token and repeating the initial request.
*/
export default class FetchInterceptor {
class FetchInterceptor {
constructor(fetch) {
// stores reference to vanilla fetch method
this.fetch = fetch;
this.accessTokenProvider = new AccessTokenProvider(this.fetch);

this.config = {
fetchRetryCount: 1,
createAccessTokenRequest: null,
shouldIntercept: () => false,
shouldInvalidateAccessToken: () => false,
isResponseUnauthorized: http.isResponseUnauthorized,
parseAccessToken: null,
authorizeRequest: null,
onAccessTokenChange: null,
onResponse: null,
};
this.config = getDefaultConfig();

this.intercept = this.intercept.bind(this);

Expand All @@ -49,37 +93,28 @@ export default class FetchInterceptor {
* Configures fetch interceptor with given config object. All required properties can optionally
* return a promise which will be resolved by fetch interceptor automatically.
*
* @param config
*
* (Required) Prepare fetch request for renewing new access token
* createAccessTokenRequest: (refreshToken) => request,
*
* (Required) Parses access token from access token response
* parseAccessToken: (response) => accessToken,
*
* (Required) Defines whether interceptor will intercept this request or just let it pass through
* shouldIntercept: (request) => boolean,
*
* (Required) Defines whether access token will be invalidated after this response
* shouldInvalidateAccessToken: (response) => boolean,
*
* (Required) Adds authorization for intercepted requests
* authorizeRequest: (request, accessToken) => authorizedRequest,
*
* Checks if response should be considered unauthorized (by default only 401 responses are
* considered unauthorized. Override this method if you need to trigger token renewal for
* other response statuses.
* isResponseUnauthorized: (response) => boolean,
*
* Number of retries after initial request was unauthorized
* fetchRetryCount: 1,
*
* Event invoked when access token has changed
* onAccessTokenChange: null,
*
* Event invoked when response is resolved
* onResponse: null,
*
* @param {object} config
* @param {createAccessTokenRequest} config.createAccessTokenRequest
* Prepare fetch request for renewing new access token
* @param {parseAccessToken} config.parseAccessToken
* Parses access token from access token response
* @param {shouldIntercept} config.shouldIntercept
* Defines whether interceptor will intercept this request or just let it pass through
* @param {shouldInvalidateAccessToken} config.shouldInvalidateAccessToken
* Defines whether access token will be invalidated after this response
* @param {authorizeRequest} config.authorizeRequest
* Adds authorization for intercepted requests
* @param {function} [config.isResponseUnauthorized=null]
* Checks if response should be considered unauthorized (by default only 401 responses are
* considered unauthorized. Override this method if you need to trigger token renewal for
* other response statuses.
* @param {number} [config.fetchRetryCount=1]
* Number of retries after initial request was unauthorized
* @param {number} [config.onAccessTokenChange=null]
* Event invoked when access token has changed
* @param {number} [config.onResponse=null]
* Event invoked when response is resolved
* </pre>
*/
configure(config) {
this.config = { ...this.config, ...config };
Expand All @@ -93,8 +128,8 @@ export default class FetchInterceptor {

/**
* Authorizes fetch interceptor with given refresh token
* @param refreshToken
* @param accessToken
* @param {string} refreshToken Refresh token
* @param {string} accessToken Access token
*/
authorize(refreshToken, accessToken) {
this.accessTokenProvider.authorize(refreshToken, accessToken);
Expand All @@ -115,6 +150,15 @@ export default class FetchInterceptor {
this.accessTokenProvider.clear();
}

/**
* Clears current authorization and restores default configuration, e.g. interceptor
* will stop intercepting requests.
*/
unload() {
this.clear();
this.config = getDefaultConfig();
}

/**
* Main intercept method, you should chain this inside wrapped fetch call
* @param args Args initially provided to fetch method
Expand All @@ -125,8 +169,11 @@ export default class FetchInterceptor {
}

isConfigValid() {
return this.config.shouldIntercept &&
this.config.authorizeRequest;
return this.config.shouldIntercept && isFunction(this.config.shouldIntercept) &&
this.config.authorizeRequest && isFunction(this.config.authorizeRequest) &&
this.config.isResponseUnauthorized && isFunction(this.config.isResponseUnauthorized) &&
this.config.createAccessTokenRequest && isFunction(this.config.createAccessTokenRequest) &&
this.config.parseAccessToken && isFunction(this.config.parseAccessToken);
}

resolveIntercept(resolve, reject, ...args) {
Expand Down Expand Up @@ -356,3 +403,5 @@ export default class FetchInterceptor {
throw new Error(error);
}
}

export default FetchInterceptor;
14 changes: 4 additions & 10 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { isResponseUnauthorized } from './services/http';
import FetchInterceptor from './FetchInterceptor';

let interceptor = null;
let nativeFetch = null;
let environment = null;

export function attach(env) {
Expand All @@ -17,8 +16,6 @@ export function attach(env) {
throw Error('You should attach only once.');
}

nativeFetch = env.fetch;

// for now add default interceptor
interceptor = new FetchInterceptor(env.fetch);

Expand All @@ -40,7 +37,8 @@ function initialize() {

/**
* Initializes and configures interceptor
* @param config
* @param config Configuration object
* @see FetchInterceptor#configure
*/
export function configure(config) {
if (!interceptor) {
Expand All @@ -53,6 +51,7 @@ export function configure(config) {
/**
* Initializes tokens which will be used by interceptor
* @param args
* @see FetchInterceptor#authorize
*/
export function authorize(...args) {
interceptor.authorize(...args);
Expand Down Expand Up @@ -86,12 +85,7 @@ export function isActive() {
*/
export function unload() {
if (interceptor) {
interceptor.clear();
interceptor = null;
}

if (environment) {
environment.fetch = nativeFetch;
interceptor.unload();
}
}

Expand Down
29 changes: 29 additions & 0 deletions test/FetchInterceptor.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,35 @@ describe('fetch-intercept', function () {
});
});

describe('unload', function () {
beforeEach(done => {
server.start(done);
});

afterEach(done => {
server.stop(done);
});

it('should clear authorization tokens and stop intercepting requests', done => {
fetchInterceptor.configure(configuration());
fetchInterceptor.authorize('refreshToken', 'accessToken');
fetchInterceptor.unload();

// assert that authorization has been cleared
const { refreshToken, accessToken } = fetchInterceptor.getAuthorization();
expect(refreshToken).to.be.null;
expect(accessToken).to.be.null;

// assert that fetch now works ok without interceptor
fetch('http://localhost:5000/200').then((response) => {
expect(response.status).to.be.equal(200);
done();
}).catch(err => {
done(err);
});
});
});

describe('should not change default fetch behaviour', () => {
describe('server is running', () => {
beforeEach(done => {
Expand Down
11 changes: 1 addition & 10 deletions test/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,4 @@ describe('index', function() {
expect(accessToken).to.equal('access-token');
});
});

describe('unload', function() {
it('should unload and detach interceptor', function (){
fetchInterceptor.configure(config());
fetchInterceptor.unload();

expect(fetchInterceptor.isActive()).to.be.false;
});
});
});
});

0 comments on commit e39fe02

Please sign in to comment.