diff --git a/package-lock.json b/package-lock.json index 4105833..f7a4a4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,20 @@ "requires": { "@types/react": "*", "redux": "^3.6.0" + }, + "dependencies": { + "redux": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz", + "integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==", + "dev": true, + "requires": { + "lodash": "^4.2.1", + "lodash-es": "^4.2.1", + "loose-envify": "^1.1.0", + "symbol-observable": "^1.0.3" + } + } } }, "alsatian": { @@ -896,15 +910,13 @@ } }, "redux": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz", - "integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.1.tgz", + "integrity": "sha512-R7bAtSkk7nY6O/OYMVR9RiBI+XghjF9rlbl5806HJbQph0LJVHZrU5oaO4q70eUKiqMRqm4y07KLTlMZ2BlVmg==", "dev": true, "requires": { - "lodash": "^4.2.1", - "lodash-es": "^4.2.1", - "loose-envify": "^1.1.0", - "symbol-observable": "^1.0.3" + "loose-envify": "^1.4.0", + "symbol-observable": "^1.2.0" } }, "reflect-metadata": { diff --git a/package.json b/package.json index 3782ae9..a1402af 100644 --- a/package.json +++ b/package.json @@ -33,24 +33,24 @@ }, "homepage": "https://github.com/NewOrbit/reauthorize#readme", "peerDependencies": { - "react": "^16.2.0", - "redux": "^3.7.2", - "react-redux": "^5.0.6" + "react": "16.x", + "redux": "4.x", + "react-redux": "5.x" }, "devDependencies": { "@types/enzyme": "^3.1.13", "@types/react": "16.0.35", "@types/react-redux": "5.0.14", - "neworbit-tslint-config": "^2.0.0", "alsatian": "^2.0.0", "enzyme": "^3.6.0", "enzyme-adapter-react-16": "^1.5.0", + "neworbit-tslint-config": "^2.0.0", + "react": "^16.2.0", "react-dom": "^16.5.0", - "tslint": "^5.0.0", + "react-redux": "^5.0.6", + "redux": "^4.0.1", "tap-xunit": "^2.2.0", - "typescript": "^2.2.2", - "react": "^16.2.0", - "redux": "^3.7.2", - "react-redux": "^5.0.6" + "tslint": "^5.0.0", + "typescript": "^2.2.2" } } diff --git a/src/authMiddleware.test.ts b/src/authMiddleware.test.ts index 8fec480..4ccd6fc 100644 --- a/src/authMiddleware.test.ts +++ b/src/authMiddleware.test.ts @@ -1,4 +1,4 @@ -import { Expect, Test, TestCase, TestFixture, SpyOn, Setup, createFunctionSpy } from "alsatian"; +import { Expect, Test, TestCase, TestFixture, SpyOn, Setup, createFunctionSpy, AsyncTest } from "alsatian"; import { configureAuthMiddleware, UNAUTHENTICATED_ERROR, UNAUTHORIZED_ERROR, AuthPayload, AuthMiddlewareOptions } from "./authMiddleware"; import { AuthState } from "./model"; import { ISpiedFunction } from "alsatian/core/spying/spied-function.i"; @@ -43,28 +43,28 @@ export class AuthMiddlewareTests { const authMiddleware = configureAuthMiddleware({ ...defaultOptions, ...options }); this.next = createFunctionSpy(); - this.invoke = (action: any) => { + this.invoke = async (action: any) => { this.error = null; try { - authMiddleware(this.store)(this.next)(action); + await authMiddleware(this.store)(this.next)(action); } catch (error) { this.error = error; } }; } - @Test("should pass through actions it cannot handle") + @AsyncTest("should pass through actions it cannot handle") @TestCase({ type: "SOME_ACTION" }) @TestCase({ type: "SOME_OTHER_ACTION" }) - public shouldPassThroughActionsItCannotHandle(action: any) { + public async shouldPassThroughActionsItCannotHandle(action: any) { this.setup(); - this.invoke(action); + await this.invoke(action); Expect(this.store.dispatch).not.toHaveBeenCalled(); Expect(this.next).toHaveBeenCalledWith(action); } - @Test("should allow authorized route") - public shouldAllowAuthorisedRoute() { + @AsyncTest("should allow authorized route") + public async shouldAllowAuthorisedRoute() { this.setup(); SpyOn(this.store, "getState").andReturn({ currentUser: { authenticated: true, roles: ["ADMIN"] }}); @@ -77,14 +77,14 @@ export class AuthMiddlewareTests { } }; - this.invoke(action); + await this.invoke(action); Expect(this.store.dispatch).not.toHaveBeenCalled(); Expect(this.next).toHaveBeenCalledWith(action); Expect(this.error).toBeNull(); } - @Test("should allow authorized route for string") - public shouldAllowAuthorisedRouteForString() { + @AsyncTest("should allow authorized route for string") + public async shouldAllowAuthorisedRouteForString() { this.setup(); SpyOn(this.store, "getState").andReturn({ currentUser: { authenticated: true, roles: ["ADMIN"] }}); @@ -97,14 +97,14 @@ export class AuthMiddlewareTests { } }; - this.invoke(action); + await this.invoke(action); Expect(this.store.dispatch).not.toHaveBeenCalled(); Expect(this.next).toHaveBeenCalledWith(action); Expect(this.error).toBeNull(); } - @Test("should not allow unauthorized route") - public shouldNotAllowUnauthorizedRoute() { + @AsyncTest("should not allow unauthorized route") + public async shouldNotAllowUnauthorizedRoute() { this.setup(); SpyOn(this.store, "getState").andReturn({ currentUser: { authenticated: true, roles: ["ADMIN"] }}); @@ -117,14 +117,14 @@ export class AuthMiddlewareTests { } }; - this.invoke(action); + await this.invoke(action); Expect(this.store.dispatch).toHaveBeenCalledWith(this.unauthorizedAction); Expect(this.next).not.toHaveBeenCalled(); Expect(this.error.message).toBe(UNAUTHORIZED_ERROR); } - @Test("should not allow route without authorize") - public shouldNotAllowRouteWithoutAuthorise() { + @AsyncTest("should not allow route without authorize") + public async shouldNotAllowRouteWithoutAuthorise() { this.setup(); SpyOn(this.store, "getState").andReturn({ currentUser: { authenticated: true, roles: ["ADMIN"] }}); @@ -136,14 +136,14 @@ export class AuthMiddlewareTests { } }; - this.invoke(action); + await this.invoke(action); Expect(this.store.dispatch).toHaveBeenCalledWith(this.unauthorizedAction); Expect(this.next).not.toHaveBeenCalled(); Expect(this.error.message).toBe(UNAUTHORIZED_ERROR); } - @Test("should allow route with no authorize if set on parent") - public shouldAllowRouteWithoutAuthoriseIfOnParent() { + @AsyncTest("should allow route with no authorize if set on parent") + public async shouldAllowRouteWithoutAuthoriseIfOnParent() { this.setup(); SpyOn(this.store, "getState").andReturn({ currentUser: { authenticated: true, roles: ["ADMIN"] }}); @@ -158,14 +158,14 @@ export class AuthMiddlewareTests { } }; - this.invoke(action); + await this.invoke(action); Expect(this.store.dispatch).not.toHaveBeenCalled(); Expect(this.next).toHaveBeenCalledWith(action); Expect(this.error).toBeNull(); } - @Test("should not allow route with authorize on that do not match even if parent does") - public shouldNotAllowRouteIfAuthoriseDontMatchButParentDoes() { + @AsyncTest("should not allow route with authorize on that do not match even if parent does") + public async shouldNotAllowRouteIfAuthoriseDontMatchButParentDoes() { this.setup(); SpyOn(this.store, "getState").andReturn({ currentUser: { authenticated: true, roles: ["ADMIN"] }}); @@ -181,14 +181,14 @@ export class AuthMiddlewareTests { } }; - this.invoke(action); + await this.invoke(action); Expect(this.store.dispatch).toHaveBeenCalledWith(this.unauthorizedAction); Expect(this.next).not.toHaveBeenCalled(); Expect(this.error.message).toBe(UNAUTHORIZED_ERROR); } - @Test("should not allow unauthenticated users") - public shouldNotAllowUnauthenticatedUsers() { + @AsyncTest("should not allow unauthenticated users") + public async shouldNotAllowUnauthenticatedUsers() { this.setup(); SpyOn(this.store, "getState").andReturn({ currentUser: { authenticated: false, roles: [] }}); @@ -201,14 +201,14 @@ export class AuthMiddlewareTests { } }; - this.invoke(action); + await this.invoke(action); Expect(this.store.dispatch).toHaveBeenCalledWith(this.unauthenticatedAction); Expect(this.next).not.toHaveBeenCalled(); Expect(this.error.message).toBe(UNAUTHENTICATED_ERROR); } - @Test("should not allow unauthenticated users with authorize undefined") - public shouldNotAllowUnauthenticatedUsersWithoutAuthorise() { + @AsyncTest("should not allow unauthenticated users with authorize undefined") + public async shouldNotAllowUnauthenticatedUsersWithoutAuthorise() { this.setup(); SpyOn(this.store, "getState").andReturn({ currentUser: { authenticated: false, roles: ["SOMETHING"] }}); @@ -220,14 +220,14 @@ export class AuthMiddlewareTests { } }; - this.invoke(action); + await this.invoke(action); Expect(this.store.dispatch).toHaveBeenCalledWith(this.unauthenticatedAction); Expect(this.next).not.toHaveBeenCalled(); Expect(this.error.message).toBe(UNAUTHENTICATED_ERROR); } - @Test("should allow unauthenticated users with authorize false") - public shouldAllowUnauthenticatedUsersWithAuthoriseFalse() { + @AsyncTest("should allow unauthenticated users with authorize false") + public async shouldAllowUnauthenticatedUsersWithAuthoriseFalse() { this.setup(); SpyOn(this.store, "getState").andReturn({ currentUser: { authenticated: false, roles: [] }}); @@ -240,14 +240,14 @@ export class AuthMiddlewareTests { } }; - this.invoke(action); + await this.invoke(action); Expect(this.store.dispatch).not.toHaveBeenCalled(); Expect(this.next).toHaveBeenCalledWith(action); Expect(this.error).toBeNull(); } - @Test("should allow authenticated users for any role for authorize true") - public shouldAllowAuthenticatedUsersWithAnyRoleForAuthoriseTrue() { + @AsyncTest("should allow authenticated users for any role for authorize true") + public async shouldAllowAuthenticatedUsersWithAnyRoleForAuthoriseTrue() { this.setup(); SpyOn(this.store, "getState").andReturn({ currentUser: { authenticated: true, roles: ["SOMETHING"] }}); @@ -260,14 +260,14 @@ export class AuthMiddlewareTests { } }; - this.invoke(action); + await this.invoke(action); Expect(this.store.dispatch).not.toHaveBeenCalled(); Expect(this.next).toHaveBeenCalledWith(action); Expect(this.error).toBeNull(); } - @Test("should dispatch unauthenticated action if api responds with a 401 and configured") - public shouldDispatchUnauthenticatedFor401() { + @AsyncTest("should dispatch unauthenticated action if api responds with a 401 and configured") + public async shouldDispatchUnauthenticatedFor401() { this.setup({ handleUnauthenticatedApiErrors: true }); @@ -292,14 +292,14 @@ export class AuthMiddlewareTests { }; }); - this.invoke(action); + await this.invoke(action); Expect(this.next).toHaveBeenCalledWith(action); Expect(this.error.message).toBe(UNAUTHENTICATED_ERROR); Expect(this.store.dispatch).toHaveBeenCalledWith(this.unauthenticatedAction); } - @Test("should dispatch unauthenticated action if error matches configured function") - public shouldDispatchUnauthenticatedForCustomError() { + @AsyncTest("should dispatch unauthenticated action if error matches configured function") + public async shouldDispatchUnauthenticatedForCustomError() { this.setup({ handleUnauthenticatedApiErrors: error => error.message === "bad error" }); @@ -320,14 +320,14 @@ export class AuthMiddlewareTests { }; }); - this.invoke(action); + await this.invoke(action); Expect(this.next).toHaveBeenCalledWith(action); Expect(this.error.message).toBe(UNAUTHENTICATED_ERROR); Expect(this.store.dispatch).toHaveBeenCalledWith(this.unauthenticatedAction); } - @Test("should not dispatch unauthenticated action if error matches configured function") - public shouldNotDispatchUnauthenticatedForCustomError() { + @AsyncTest("should not dispatch unauthenticated action if error matches configured function") + public async shouldNotDispatchUnauthenticatedForCustomError() { this.setup({ handleUnauthenticatedApiErrors: error => error.message === "bad error" }); @@ -348,14 +348,14 @@ export class AuthMiddlewareTests { }; }); - this.invoke(action); + await this.invoke(action); Expect(this.next).toHaveBeenCalledWith(action); Expect(this.error.message).toBe("another error"); Expect(this.store.dispatch).not.toHaveBeenCalled(); } - @Test("should dispatch unauthorized action if api responds with a 403 and configured") - public shouldDispatchUnauthorizedFor403() { + @AsyncTest("should dispatch unauthorized action if api responds with a 403 and configured") + public async shouldDispatchUnauthorizedFor403() { this.setup({ handleUnauthorizedApiErrors: true }); @@ -380,14 +380,14 @@ export class AuthMiddlewareTests { }; }); - this.invoke(action); + await this.invoke(action); Expect(this.next).toHaveBeenCalledWith(action); Expect(this.error.message).toBe(UNAUTHORIZED_ERROR); Expect(this.store.dispatch).toHaveBeenCalledWith(this.unauthorizedAction); } - @Test("should dispatch unauthorized action if error matches configured function") - public shouldDispatchUnauthorizedForCustomError() { + @AsyncTest("should dispatch unauthorized action if error matches configured function") + public async shouldDispatchUnauthorizedForCustomError() { this.setup({ handleUnauthorizedApiErrors: error => error.message === "bad error" }); @@ -408,14 +408,14 @@ export class AuthMiddlewareTests { }; }); - this.invoke(action); + await this.invoke(action); Expect(this.next).toHaveBeenCalledWith(action); Expect(this.error.message).toBe(UNAUTHORIZED_ERROR); Expect(this.store.dispatch).toHaveBeenCalledWith(this.unauthorizedAction); } - @Test("should not dispatch unauthorized action if error matches configured function") - public shouldNotDispatchUnauthorizedForCustomError() { + @AsyncTest("should not dispatch unauthorized action if error matches configured function") + public async shouldNotDispatchUnauthorizedForCustomError() { this.setup({ handleUnauthorizedApiErrors: error => error.message === "bad error" }); @@ -436,9 +436,41 @@ export class AuthMiddlewareTests { }; }); - this.invoke(action); + await this.invoke(action); Expect(this.next).toHaveBeenCalledWith(action); Expect(this.error.message).toBe("another error"); Expect(this.store.dispatch).not.toHaveBeenCalled(); } + + @AsyncTest("should dispatch unauthenticated action if api responds asynchronously with a 401 and configured") + public async shouldDispatchUnauthenticatedForAsync401() { + this.setup({ + handleUnauthenticatedApiErrors: true + }); + SpyOn(this.store, "getState").andReturn({ currentUser: { authenticated: true, roles: ["ADMIN"] }}); + + const action = { + type: "LOCATION_CHANGED", + payload: { + result: { + authorize: ["ADMIN"] + } + } + }; + + this.next.andCall(() => { + return Promise.reject({ + message: "Request failed with status code 401", + response: { + status: 401, + statusText: "Unauthorized" + } + }); + }); + + await this.invoke(action); + Expect(this.next).toHaveBeenCalledWith(action); + Expect(this.error.message).toBe(UNAUTHENTICATED_ERROR); + Expect(this.store.dispatch).toHaveBeenCalledWith(this.unauthenticatedAction); + } } diff --git a/src/authMiddleware.ts b/src/authMiddleware.ts index 1578593..5348a21 100644 --- a/src/authMiddleware.ts +++ b/src/authMiddleware.ts @@ -57,7 +57,7 @@ export const configureAuthMiddleware = (options: AuthMiddleware throw new Error(unauthorizedError); }; - return (api: MiddlewareAPI) => (next: Dispatch) => (action: any) => { + return (api: MiddlewareAPI) => (next: Dispatch) => async (action: any) => { if (action.type === actionType) { @@ -84,7 +84,7 @@ export const configureAuthMiddleware = (options: AuthMiddleware } try { - return next(action); + return await next(action); } catch (e) { if (isUnauthenticatedError && isUnauthenticatedError(e)) { handleUnauthenticated(api);