use pending and error module for auth, me and logout

This commit is contained in:
Sebastian Sdorra
2018-07-30 15:29:23 +02:00
parent e122a254c3
commit b825de3058
9 changed files with 295 additions and 240 deletions

View File

@@ -3,7 +3,13 @@ import Main from "./Main";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import { withRouter } from "react-router-dom"; import { withRouter } from "react-router-dom";
import { fetchMe } from "../modules/auth"; import {
fetchMe,
isAuthenticated,
getMe,
isFetchMePending,
getFetchMeFailure
} from "../modules/auth";
import "./App.css"; import "./App.css";
import "../components/modals/ConfirmAlert.css"; import "../components/modals/ConfirmAlert.css";
@@ -14,11 +20,15 @@ import { Footer, Header } from "../components/layout";
type Props = { type Props = {
me: Me, me: Me,
authenticated: boolean,
error: Error, error: Error,
loading: boolean, loading: boolean,
authenticated?: boolean,
t: string => string, // dispatcher functions
fetchMe: () => void fetchMe: () => void,
// context props
t: string => string
}; };
class App extends Component<Props> { class App extends Component<Props> {
@@ -62,15 +72,15 @@ const mapDispatchToProps = (dispatch: any) => {
}; };
const mapStateToProps = state => { const mapStateToProps = state => {
let mapped = state.auth.me || {}; const authenticated = isAuthenticated(state);
const me = getMe(state);
if (state.auth.login) { const loading = isFetchMePending(state);
mapped.authenticated = state.auth.login.authenticated; const error = getFetchMeFailure(state);
}
return { return {
...mapped, authenticated,
me: mapped.entry me,
loading,
error
}; };
}; };

View File

@@ -3,7 +3,12 @@ import React from "react";
import { Redirect, withRouter } from "react-router-dom"; import { Redirect, withRouter } from "react-router-dom";
import injectSheet from "react-jss"; import injectSheet from "react-jss";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import { login } from "../modules/auth"; import {
login,
isAuthenticated,
isLoginPending,
getLoginFailure
} from "../modules/auth";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { InputField } from "../components/forms"; import { InputField } from "../components/forms";
@@ -32,17 +37,18 @@ const styles = {
}; };
type Props = { type Props = {
authenticated?: boolean, authenticated: boolean,
loading?: boolean, loading: boolean,
error?: Error, error: Error,
// dispatcher props
login: (username: string, password: string) => void,
// context props
t: string => string, t: string => string,
classes: any, classes: any,
from: any, from: any,
location: any, location: any
login: (username: string, password: string) => void
}; };
type State = { type State = {
@@ -135,7 +141,14 @@ class Login extends React.Component<Props, State> {
} }
const mapStateToProps = state => { const mapStateToProps = state => {
return state.auth.login || {}; const authenticated = isAuthenticated(state);
const loading = isLoginPending(state);
const error = getLoginFailure(state);
return {
authenticated,
loading,
error
};
}; };
const mapDispatchToProps = dispatch => { const mapDispatchToProps = dispatch => {

View File

@@ -4,16 +4,25 @@ import { connect } from "react-redux";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import { Redirect } from "react-router-dom"; import { Redirect } from "react-router-dom";
import { logout, isAuthenticated } from "../modules/auth"; import {
logout,
isAuthenticated,
isLogoutPending,
getLogoutFailure
} from "../modules/auth";
import ErrorPage from "../components/ErrorPage"; import ErrorPage from "../components/ErrorPage";
import Loading from "../components/Loading"; import Loading from "../components/Loading";
type Props = { type Props = {
t: string => string,
loading: boolean,
authenticated: boolean, authenticated: boolean,
error?: Error, loading: boolean,
logout: () => void error: Error,
// dispatcher functions
logout: () => void,
// context props
t: string => string
}; };
class Logout extends React.Component<Props> { class Logout extends React.Component<Props> {
@@ -22,8 +31,7 @@ class Logout extends React.Component<Props> {
} }
render() { render() {
const { authenticated, loading, t, error } = this.props; const { authenticated, loading, error, t } = this.props;
// TODO logout is called twice
if (error) { if (error) {
return ( return (
<ErrorPage <ErrorPage
@@ -41,9 +49,14 @@ class Logout extends React.Component<Props> {
} }
const mapStateToProps = state => { const mapStateToProps = state => {
let mapped = state.auth.logout || {}; const authenticated = isAuthenticated(state);
mapped.authenticated = isAuthenticated(state); const loading = isLogoutPending(state);
return mapped; const error = getLogoutFailure(state);
return {
authenticated,
loading,
error
};
}; };
const mapDispatchToProps = dispatch => { const mapDispatchToProps = dispatch => {

View File

@@ -1,102 +1,50 @@
// @flow // @flow
import type { Me } from "../types/Me"; import type { Me } from "../types/Me";
import * as types from "./types";
import { apiClient, UNAUTHORIZED_ERROR } from "../apiclient"; import { apiClient, UNAUTHORIZED_ERROR } from "../apiclient";
import { isPending } from "./pending";
import { getFailure } from "./failure";
// Action // Action
export const LOGIN_REQUEST = "scm/auth/LOGIN_REQUEST"; export const LOGIN = "scm/auth/LOGIN";
export const LOGIN_SUCCESS = "scm/auth/LOGIN_SUCCESS"; export const LOGIN_PENDING = `${LOGIN}_${types.PENDING_SUFFIX}`;
export const LOGIN_FAILURE = "scm/auth/LOGIN_FAILURE"; export const LOGIN_SUCCESS = `${LOGIN}_${types.SUCCESS_SUFFIX}`;
export const LOGIN_FAILURE = `${LOGIN}_${types.FAILURE_SUFFIX}`;
export const FETCH_ME_REQUEST = "scm/auth/FETCH_ME_REQUEST"; export const FETCH_ME = "scm/auth/FETCH_ME";
export const FETCH_ME_SUCCESS = "scm/auth/FETCH_ME_SUCCESS"; export const FETCH_ME_PENDING = `${FETCH_ME}_${types.PENDING_SUFFIX}`;
export const FETCH_ME_FAILURE = "scm/auth/FETCH_ME_FAILURE"; export const FETCH_ME_SUCCESS = `${FETCH_ME}_${types.SUCCESS_SUFFIX}`;
export const FETCH_ME_UNAUTHORIZED = "scm/auth/FETCH_ME_UNAUTHORIZED"; export const FETCH_ME_FAILURE = `${FETCH_ME}_${types.FAILURE_SUFFIX}`;
export const FETCH_ME_UNAUTHORIZED = `${FETCH_ME}_UNAUTHORIZED`;
export const LOGOUT_REQUEST = "scm/auth/LOGOUT_REQUEST"; export const LOGOUT = "scm/auth/LOGOUT";
export const LOGOUT_SUCCESS = "scm/auth/LOGOUT_SUCCESS"; export const LOGOUT_PENDING = `${LOGOUT}_${types.PENDING_SUFFIX}`;
export const LOGOUT_FAILURE = "scm/auth/LOGOUT_FAILURE"; export const LOGOUT_SUCCESS = `${LOGOUT}_${types.SUCCESS_SUFFIX}`;
export const LOGOUT_FAILURE = `${LOGOUT}_${types.FAILURE_SUFFIX}`;
// Reducer // Reducer
const initialState = { const initialState = {};
me: { loading: true }
};
export default function reducer(state: any = initialState, action: any = {}) { export default function reducer(state: Object = initialState, action: Object) {
switch (action.type) { switch (action.type) {
case LOGIN_REQUEST:
return {
...state,
login: {
loading: true
}
};
case LOGIN_SUCCESS: case LOGIN_SUCCESS:
return {
...state,
login: {
authenticated: true
}
};
case LOGIN_FAILURE:
return {
...state,
login: {
error: action.payload
}
};
case FETCH_ME_REQUEST:
return {
...state,
me: {
loading: true
}
};
case FETCH_ME_SUCCESS: case FETCH_ME_SUCCESS:
return { return {
...state, ...state,
me: { me: action.payload,
entry: action.payload authenticated: true
},
login: {
authenticated: true
}
}; };
case FETCH_ME_UNAUTHORIZED: case FETCH_ME_UNAUTHORIZED:
return { return {
...state,
me: {}, me: {},
login: { authenticated: false
authenticated: false
}
};
case FETCH_ME_FAILURE:
return {
...state,
me: {
error: action.payload
}
};
case LOGOUT_REQUEST:
return {
...state,
logout: {
loading: true
}
}; };
case LOGOUT_SUCCESS: case LOGOUT_SUCCESS:
return initialState; return initialState;
case LOGOUT_FAILURE:
return {
...state,
logout: {
error: action.payload
}
};
default: default:
return state; return state;
} }
@@ -104,15 +52,16 @@ export default function reducer(state: any = initialState, action: any = {}) {
// Action Creators // Action Creators
export const loginRequest = () => { export const loginPending = () => {
return { return {
type: LOGIN_REQUEST type: LOGIN_PENDING
}; };
}; };
export const loginSuccess = () => { export const loginSuccess = (me: Me) => {
return { return {
type: LOGIN_SUCCESS type: LOGIN_SUCCESS,
payload: me
}; };
}; };
@@ -123,9 +72,9 @@ export const loginFailure = (error: Error) => {
}; };
}; };
export const logoutRequest = () => { export const logoutPending = () => {
return { return {
type: LOGOUT_REQUEST type: LOGOUT_PENDING
}; };
}; };
@@ -142,9 +91,9 @@ export const logoutFailure = (error: Error) => {
}; };
}; };
export const fetchMeRequest = () => { export const fetchMePending = () => {
return { return {
type: FETCH_ME_REQUEST type: FETCH_ME_PENDING
}; };
}; };
@@ -157,7 +106,8 @@ export const fetchMeSuccess = (me: Me) => {
export const fetchMeUnauthenticated = () => { export const fetchMeUnauthenticated = () => {
return { return {
type: FETCH_ME_UNAUTHORIZED type: FETCH_ME_UNAUTHORIZED,
resetPending: true
}; };
}; };
@@ -175,6 +125,17 @@ const LOGIN_URL = "/auth/access_token";
// side effects // side effects
const callFetchMe = (): Promise<Me> => {
return apiClient
.get(ME_URL)
.then(response => {
return response.json();
})
.then(json => {
return { name: json.name, displayName: json.displayName };
});
};
export const login = (username: string, password: string) => { export const login = (username: string, password: string) => {
const login_data = { const login_data = {
cookie: true, cookie: true,
@@ -183,12 +144,14 @@ export const login = (username: string, password: string) => {
password password
}; };
return function(dispatch: any) { return function(dispatch: any) {
dispatch(loginRequest()); dispatch(loginPending());
return apiClient return apiClient
.post(LOGIN_URL, login_data) .post(LOGIN_URL, login_data)
.then(response => { .then(response => {
dispatch(fetchMe()); return callFetchMe();
dispatch(loginSuccess()); })
.then(me => {
dispatch(loginSuccess(me));
}) })
.catch(err => { .catch(err => {
dispatch(loginFailure(err)); dispatch(loginFailure(err));
@@ -198,16 +161,10 @@ export const login = (username: string, password: string) => {
export const fetchMe = () => { export const fetchMe = () => {
return function(dispatch: any) { return function(dispatch: any) {
dispatch(fetchMeRequest()); dispatch(fetchMePending());
return apiClient return callFetchMe()
.get(ME_URL)
.then(response => {
return response.json();
})
.then(me => { .then(me => {
dispatch( dispatch(fetchMeSuccess(me));
fetchMeSuccess({ userName: me.name, displayName: me.displayName })
);
}) })
.catch((error: Error) => { .catch((error: Error) => {
if (error === UNAUTHORIZED_ERROR) { if (error === UNAUTHORIZED_ERROR) {
@@ -221,12 +178,11 @@ export const fetchMe = () => {
export const logout = () => { export const logout = () => {
return function(dispatch: any) { return function(dispatch: any) {
dispatch(logoutRequest()); dispatch(logoutPending());
return apiClient return apiClient
.delete(LOGIN_URL) .delete(LOGIN_URL)
.then(() => { .then(() => {
dispatch(logoutSuccess()); dispatch(logoutSuccess());
dispatch(fetchMe());
}) })
.catch(error => { .catch(error => {
dispatch(logoutFailure(error)); dispatch(logoutFailure(error));
@@ -236,6 +192,41 @@ export const logout = () => {
// selectors // selectors
export const isAuthenticated = (state: any): boolean => { const stateAuth = (state: Object): Object => {
return state.auth && state.auth.login && state.auth.login.authenticated; return state.auth || {};
};
export const isAuthenticated = (state: Object) => {
if (stateAuth(state).authenticated) {
return true;
}
return false;
};
export const getMe = (state: Object): Me => {
return stateAuth(state).me;
};
export const isFetchMePending = (state: Object) => {
return isPending(state, FETCH_ME);
};
export const getFetchMeFailure = (state: Object) => {
return getFailure(state, FETCH_ME);
};
export const isLoginPending = (state: Object) => {
return isPending(state, LOGIN);
};
export const getLoginFailure = (state: Object) => {
return getFailure(state, LOGIN);
};
export const isLogoutPending = (state: Object) => {
return isPending(state, LOGOUT);
};
export const getLogoutFailure = (state: Object) => {
return getFailure(state, LOGOUT);
}; };

View File

@@ -3,109 +3,69 @@ import reducer, {
logout, logout,
logoutSuccess, logoutSuccess,
loginSuccess, loginSuccess,
fetchMeRequest,
loginRequest,
logoutRequest,
fetchMeFailure,
fetchMeUnauthenticated, fetchMeUnauthenticated,
loginFailure,
logoutFailure,
LOGIN_REQUEST,
FETCH_ME_REQUEST,
LOGIN_SUCCESS, LOGIN_SUCCESS,
login, login,
LOGIN_FAILURE, LOGIN_FAILURE,
LOGOUT_FAILURE, LOGOUT_FAILURE,
LOGOUT_SUCCESS,
FETCH_ME_SUCCESS, FETCH_ME_SUCCESS,
fetchMe, fetchMe,
FETCH_ME_FAILURE, FETCH_ME_FAILURE,
FETCH_ME_UNAUTHORIZED, FETCH_ME_UNAUTHORIZED,
isAuthenticated isAuthenticated,
LOGIN_PENDING,
FETCH_ME_PENDING,
LOGOUT_PENDING,
getMe,
isFetchMePending,
isLoginPending,
isLogoutPending,
getFetchMeFailure,
LOGIN,
FETCH_ME,
LOGOUT,
getLoginFailure,
getLogoutFailure
} from "./auth"; } from "./auth";
import configureMockStore from "redux-mock-store"; import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk"; import thunk from "redux-thunk";
import fetchMock from "fetch-mock"; import fetchMock from "fetch-mock";
import { LOGOUT_REQUEST, LOGOUT_SUCCESS } from "./auth";
const me = { name: "tricia", displayName: "Tricia McMillian" };
describe("auth reducer", () => { describe("auth reducer", () => {
it("should initialize in loading state ", () => {
const state = reducer();
expect(state.me.loading).toBeTruthy();
});
it("should set me and login on successful fetch of me", () => { it("should set me and login on successful fetch of me", () => {
const state = reducer(undefined, fetchMeSuccess({ username: "tricia" })); const state = reducer(undefined, fetchMeSuccess(me));
expect(state.me.loading).toBeFalsy(); expect(state.me).toBe(me);
expect(state.me.entry.username).toBe("tricia"); expect(state.authenticated).toBe(true);
expect(state.login.authenticated).toBeTruthy();
}); });
it("should set authenticated to false", () => { it("should set authenticated to false", () => {
const initialState = { const initialState = {
login: { authenticated: true,
authenticated: true me
},
me: {
username: "tricia"
}
}; };
const state = reducer(initialState, fetchMeUnauthenticated()); const state = reducer(initialState, fetchMeUnauthenticated());
expect(state.me.username).toBeUndefined(); expect(state.me.name).toBeUndefined();
expect(state.login.authenticated).toBeFalsy(); expect(state.authenticated).toBe(false);
}); });
it("should reset the state after logout", () => { it("should reset the state after logout", () => {
const initialState = { const initialState = {
login: { authenticated: true,
authenticated: true me
},
me: {
username: "tricia"
}
}; };
const state = reducer(initialState, logoutSuccess()); const state = reducer(initialState, logoutSuccess());
expect(state.me.loading).toBeTruthy(); expect(state.me).toBeUndefined();
expect(state.me.entry).toBeFalsy(); expect(state.authenticated).toBeUndefined();
expect(state.login).toBeUndefined();
}); });
it("should set state authenticated after login", () => { it("should set state authenticated and me after login", () => {
const state = reducer(undefined, loginSuccess()); const state = reducer(undefined, loginSuccess(me));
expect(state.login.authenticated).toBeTruthy(); expect(state.me).toBe(me);
}); expect(state.authenticated).toBe(true);
it("should set me to loading", () => {
const state = reducer({ me: { loading: false } }, fetchMeRequest());
expect(state.me.loading).toBeTruthy();
});
it("should set login to loading", () => {
const state = reducer({ login: { loading: false } }, loginRequest());
expect(state.login.loading).toBeTruthy();
});
it("should set logout to loading", () => {
const state = reducer({ logout: { loading: false } }, logoutRequest());
expect(state.logout.loading).toBeTruthy();
});
it("should set me to error", () => {
const error = new Error("failed");
const state = reducer(undefined, fetchMeFailure(error));
expect(state.me.error).toBe(error);
});
it("should set login to error", () => {
const error = new Error("failed");
const state = reducer(undefined, loginFailure(error));
expect(state.login.error).toBe(error);
});
it("should set logout to error", () => {
const error = new Error("failed");
const state = reducer(undefined, logoutFailure(error));
expect(state.logout.error).toBe(error);
}); });
}); });
@@ -129,16 +89,13 @@ describe("auth actions", () => {
}); });
fetchMock.getOnce("/scm/api/rest/v2/me", { fetchMock.getOnce("/scm/api/rest/v2/me", {
body: { body: me,
username: "tricia"
},
headers: { "content-type": "application/json" } headers: { "content-type": "application/json" }
}); });
const expectedActions = [ const expectedActions = [
{ type: LOGIN_REQUEST }, { type: LOGIN_PENDING },
{ type: FETCH_ME_REQUEST }, { type: LOGIN_SUCCESS, payload: me }
{ type: LOGIN_SUCCESS }
]; ];
const store = mockStore({}); const store = mockStore({});
@@ -156,7 +113,7 @@ describe("auth actions", () => {
const store = mockStore({}); const store = mockStore({});
return store.dispatch(login("tricia", "secret123")).then(() => { return store.dispatch(login("tricia", "secret123")).then(() => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions[0].type).toEqual(LOGIN_REQUEST); expect(actions[0].type).toEqual(LOGIN_PENDING);
expect(actions[1].type).toEqual(LOGIN_FAILURE); expect(actions[1].type).toEqual(LOGIN_FAILURE);
expect(actions[1].payload).toBeDefined(); expect(actions[1].payload).toBeDefined();
}); });
@@ -164,15 +121,15 @@ describe("auth actions", () => {
it("should dispatch fetch me success", () => { it("should dispatch fetch me success", () => {
fetchMock.getOnce("/scm/api/rest/v2/me", { fetchMock.getOnce("/scm/api/rest/v2/me", {
body: { name: "sorbot", displayName: "Sorbot" }, body: me,
headers: { "content-type": "application/json" } headers: { "content-type": "application/json" }
}); });
const expectedActions = [ const expectedActions = [
{ type: FETCH_ME_REQUEST }, { type: FETCH_ME_PENDING },
{ {
type: FETCH_ME_SUCCESS, type: FETCH_ME_SUCCESS,
payload: { userName: "sorbot", displayName: "Sorbot" } payload: me
} }
]; ];
@@ -191,7 +148,7 @@ describe("auth actions", () => {
const store = mockStore({}); const store = mockStore({});
return store.dispatch(fetchMe()).then(() => { return store.dispatch(fetchMe()).then(() => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions[0].type).toEqual(FETCH_ME_REQUEST); expect(actions[0].type).toEqual(FETCH_ME_PENDING);
expect(actions[1].type).toEqual(FETCH_ME_FAILURE); expect(actions[1].type).toEqual(FETCH_ME_FAILURE);
expect(actions[1].payload).toBeDefined(); expect(actions[1].payload).toBeDefined();
}); });
@@ -203,8 +160,8 @@ describe("auth actions", () => {
}); });
const expectedActions = [ const expectedActions = [
{ type: FETCH_ME_REQUEST }, { type: FETCH_ME_PENDING },
{ type: FETCH_ME_UNAUTHORIZED } { type: FETCH_ME_UNAUTHORIZED, resetPending: true }
]; ];
const store = mockStore({}); const store = mockStore({});
@@ -225,9 +182,8 @@ describe("auth actions", () => {
}); });
const expectedActions = [ const expectedActions = [
{ type: LOGOUT_REQUEST }, { type: LOGOUT_PENDING },
{ type: LOGOUT_SUCCESS }, { type: LOGOUT_SUCCESS }
{ type: FETCH_ME_REQUEST }
]; ];
const store = mockStore({}); const store = mockStore({});
@@ -245,7 +201,7 @@ describe("auth actions", () => {
const store = mockStore({}); const store = mockStore({});
return store.dispatch(logout()).then(() => { return store.dispatch(logout()).then(() => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions[0].type).toEqual(LOGOUT_REQUEST); expect(actions[0].type).toEqual(LOGOUT_PENDING);
expect(actions[1].type).toEqual(LOGOUT_FAILURE); expect(actions[1].type).toEqual(LOGOUT_FAILURE);
expect(actions[1].payload).toBeDefined(); expect(actions[1].payload).toBeDefined();
}); });
@@ -253,18 +209,71 @@ describe("auth actions", () => {
}); });
describe("auth selectors", () => { describe("auth selectors", () => {
it("should be false", () => { const error = new Error("yo it failed");
expect(isAuthenticated({})).toBeFalsy();
expect(isAuthenticated({ auth: {} })).toBeFalsy(); it("should be false, if authenticated is undefined or false", () => {
expect(isAuthenticated({ auth: { login: {} } })).toBeFalsy(); expect(isAuthenticated({})).toBe(false);
expect( expect(isAuthenticated({ auth: {} })).toBe(false);
isAuthenticated({ auth: { login: { authenticated: false } } }) expect(isAuthenticated({ auth: { authenticated: false } })).toBe(false);
).toBeFalsy();
}); });
it("shuld be true", () => { it("should be true, if authenticated is true", () => {
expect( expect(isAuthenticated({ auth: { authenticated: true } })).toBe(true);
isAuthenticated({ auth: { login: { authenticated: true } } }) });
).toBeTruthy();
it("should return me", () => {
expect(getMe({ auth: { me } })).toBe(me);
});
it("should return undefined, if me is not set", () => {
expect(getMe({})).toBeUndefined();
});
it("should return true, if FETCH_ME is pending", () => {
expect(isFetchMePending({ pending: { [FETCH_ME]: true } })).toBe(true);
});
it("should return false, if FETCH_ME is not in pending state", () => {
expect(isFetchMePending({ pending: {} })).toBe(false);
});
it("should return true, if LOGIN is pending", () => {
expect(isLoginPending({ pending: { [LOGIN]: true } })).toBe(true);
});
it("should return false, if LOGIN is not in pending state", () => {
expect(isLoginPending({ pending: {} })).toBe(false);
});
it("should return true, if LOGOUT is pending", () => {
expect(isLogoutPending({ pending: { [LOGOUT]: true } })).toBe(true);
});
it("should return false, if LOGOUT is not in pending state", () => {
expect(isLogoutPending({ pending: {} })).toBe(false);
});
it("should return the error, if failure state is set for FETCH_ME", () => {
expect(getFetchMeFailure({ failure: { [FETCH_ME]: error } })).toBe(error);
});
it("should return unknown, if failure state is not set for FETCH_ME", () => {
expect(getFetchMeFailure({})).toBeUndefined();
});
it("should return the error, if failure state is set for LOGIN", () => {
expect(getLoginFailure({ failure: { [LOGIN]: error } })).toBe(error);
});
it("should return unknown, if failure state is not set for LOGIN", () => {
expect(getLoginFailure({})).toBeUndefined();
});
it("should return the error, if failure state is set for LOGOUT", () => {
expect(getLogoutFailure({ failure: { [LOGOUT]: error } })).toBe(error);
});
it("should return unknown, if failure state is not set for LOGOUT", () => {
expect(getLogoutFailure({})).toBeUndefined();
}); });
}); });

View File

@@ -1,8 +1,13 @@
// @flow // @flow
import type { Action } from "../types/Action"; import type { Action } from "../types/Action";
import * as types from "./types";
const PENDING_SUFFIX = "_PENDING"; const PENDING_SUFFIX = "_" + types.PENDING_SUFFIX;
const RESET_PATTERN = /^(.*)_(SUCCESS|FAILURE|RESET)$/; const RESET_ACTIONTYPES = [
types.SUCCESS_SUFFIX,
types.FAILURE_SUFFIX,
types.RESET_SUFFIX
];
function removeFromState(state: Object, identifier: string) { function removeFromState(state: Object, identifier: string) {
let newState = {}; let newState = {};
@@ -32,13 +37,16 @@ export default function reducer(state: Object = {}, action: Action): Object {
[identifier]: true [identifier]: true
}; };
} else { } else {
const matches = RESET_PATTERN.exec(type); const index = type.lastIndexOf("_");
if (matches) { if (index > 0) {
let identifier = matches[1]; const actionType = type.substring(index + 1);
if (action.itemId) { if (RESET_ACTIONTYPES.indexOf(actionType) >= 0 || action.resetPending) {
identifier += "/" + action.itemId; let identifier = type.substring(0, index);
if (action.itemId) {
identifier += "/" + action.itemId;
}
return removeFromState(state, identifier);
} }
return removeFromState(state, identifier);
} }
} }
return state; return state;

View File

@@ -47,6 +47,16 @@ describe("pending reducer", () => {
expect(newState["FETCH_ITEMS"]).toBeFalsy(); expect(newState["FETCH_ITEMS"]).toBeFalsy();
}); });
it("should reset pending state for FETCH_ITEMS, if resetPending prop is available", () => {
const newState = reducer(
{
FETCH_ITEMS: true
},
{ type: "FETCH_ITEMS_SOMETHING", resetPending: true }
);
expect(newState["FETCH_ITEMS"]).toBeFalsy();
});
it("should reset pending state for FETCH_ITEMS after FETCH_ITEMS_SUCCESS, but should not affect others", () => { it("should reset pending state for FETCH_ITEMS after FETCH_ITEMS_SUCCESS, but should not affect others", () => {
const newState = reducer( const newState = reducer(
{ {

View File

@@ -2,5 +2,6 @@
export type Action = { export type Action = {
type: string, type: string,
payload?: any, payload?: any,
itemId?: string | number itemId?: string | number,
resetPending?: boolean
}; };

View File

@@ -1,6 +1,6 @@
// @flow // @flow
export type Me = { export type Me = {
userName: string, name: string,
displayName: string displayName: string
}; };