diff --git a/src/features/user-admin/redux/actions.js b/src/features/user-admin/redux/actions.js index f18e48b..6335ca9 100644 --- a/src/features/user-admin/redux/actions.js +++ b/src/features/user-admin/redux/actions.js @@ -1 +1,2 @@ export { fetchUsers, dismissFetchUsersError } from './fetchUsers'; +export { fetchProfile, dismissFetchProfileError } from './fetchProfile'; diff --git a/src/features/user-admin/redux/constants.js b/src/features/user-admin/redux/constants.js index 3b2ffa8..e033bcb 100644 --- a/src/features/user-admin/redux/constants.js +++ b/src/features/user-admin/redux/constants.js @@ -2,3 +2,7 @@ export const USER_ADMIN_FETCH_USERS_BEGIN = 'USER_ADMIN_FETCH_USERS_BEGIN'; export const USER_ADMIN_FETCH_USERS_SUCCESS = 'USER_ADMIN_FETCH_USERS_SUCCESS'; export const USER_ADMIN_FETCH_USERS_FAILURE = 'USER_ADMIN_FETCH_USERS_FAILURE'; export const USER_ADMIN_FETCH_USERS_DISMISS_ERROR = 'USER_ADMIN_FETCH_USERS_DISMISS_ERROR'; +export const USER_ADMIN_FETCH_PROFILE_BEGIN = 'USER_ADMIN_FETCH_PROFILE_BEGIN'; +export const USER_ADMIN_FETCH_PROFILE_SUCCESS = 'USER_ADMIN_FETCH_PROFILE_SUCCESS'; +export const USER_ADMIN_FETCH_PROFILE_FAILURE = 'USER_ADMIN_FETCH_PROFILE_FAILURE'; +export const USER_ADMIN_FETCH_PROFILE_DISMISS_ERROR = 'USER_ADMIN_FETCH_PROFILE_DISMISS_ERROR'; diff --git a/src/features/user-admin/redux/fetchProfile.js b/src/features/user-admin/redux/fetchProfile.js new file mode 100644 index 0000000..eedf892 --- /dev/null +++ b/src/features/user-admin/redux/fetchProfile.js @@ -0,0 +1,86 @@ +import { + USER_ADMIN_FETCH_PROFILE_BEGIN, + USER_ADMIN_FETCH_PROFILE_SUCCESS, + USER_ADMIN_FETCH_PROFILE_FAILURE, + USER_ADMIN_FETCH_PROFILE_DISMISS_ERROR, +} from './constants'; + +export function fetchProfile(username) { + return (dispatch, getState) => { + dispatch({ + type: USER_ADMIN_FETCH_PROFILE_BEGIN, + }); + + const promise = new Promise((resolve, reject) => { + const mtx = getState().common.mtx; + const doRequest = mtx._http.authedRequest(undefined, "GET", "/profile/" + username, undefined) + doRequest.then( + (res) => { + dispatch({ + type: USER_ADMIN_FETCH_PROFILE_SUCCESS, + username: username, + profile: res, + }); + resolve(res); + }, + // Use rejectHandler as the second argument so that render errors won't be caught. + (err) => { + dispatch({ + type: USER_ADMIN_FETCH_PROFILE_FAILURE, + data: { error: err }, + }); + reject(err); + }, + ); + }); + + return promise; + }; +} + +// Async action saves request error by default, this method is used to dismiss the error info. +// If you don't want errors to be saved in Redux store, just ignore this method. +export function dismissFetchProfileError() { + return { + type: USER_ADMIN_FETCH_PROFILE_DISMISS_ERROR, + }; +} + +export function reducer(state, action) { + switch (action.type) { + case USER_ADMIN_FETCH_PROFILE_BEGIN: + // Just after a request is sent + return { + ...state, + fetchProfilePending: true, + fetchProfileError: null, + }; + + case USER_ADMIN_FETCH_PROFILE_SUCCESS: + // The request is success + return { + ...state, + userProfiles: { ...state.userProfiles, [action.username]: action.profile }, + fetchProfilePending: false, + fetchProfileError: null, + }; + + case USER_ADMIN_FETCH_PROFILE_FAILURE: + // The request is failed + return { + ...state, + fetchProfilePending: false, + fetchProfileError: action.data.error, + }; + + case USER_ADMIN_FETCH_PROFILE_DISMISS_ERROR: + // Dismiss the request failure error + return { + ...state, + fetchProfileError: null, + }; + + default: + return state; + } +} diff --git a/src/features/user-admin/redux/fetchUsers.js b/src/features/user-admin/redux/fetchUsers.js index b0a1393..fc54c4e 100644 --- a/src/features/user-admin/redux/fetchUsers.js +++ b/src/features/user-admin/redux/fetchUsers.js @@ -17,6 +17,7 @@ export function fetchUsers(args = {}) { // e.g.: handleSubmit() { this.props.actions.submitForm(data).then(()=> {}).catch(() => {}); } const promise = new Promise((resolve, reject) => { const mtx = getState().common.mtx; + if (!mtx) return; const doRequest = mtx._http.authedRequest(undefined, "GET", "/admin/users/" + mtx.credentials.userId) doRequest.then( (res) => { diff --git a/src/features/user-admin/redux/initialState.js b/src/features/user-admin/redux/initialState.js index 6284612..b84bbc6 100644 --- a/src/features/user-admin/redux/initialState.js +++ b/src/features/user-admin/redux/initialState.js @@ -7,8 +7,11 @@ // NOTE: initialState constant is necessary so that Rekit could auto add initial state when creating async actions. const initialState = { userList: [], + userProfiles: {}, fetchUsersPending: false, fetchUsersError: null, + fetchProfilePending: false, + fetchProfileError: null, }; export default initialState; diff --git a/src/features/user-admin/redux/reducer.js b/src/features/user-admin/redux/reducer.js index aafa72a..f7ea9c3 100644 --- a/src/features/user-admin/redux/reducer.js +++ b/src/features/user-admin/redux/reducer.js @@ -8,9 +8,11 @@ import initialState from './initialState'; import { reducer as fetchUsersReducer } from './fetchUsers'; +import { reducer as fetchProfileReducer } from './fetchProfile'; const reducers = [ fetchUsersReducer, + fetchProfileReducer, ]; export default function reducer(state = initialState, action) { diff --git a/tests/features/user-admin/redux/fetchProfile.test.js b/tests/features/user-admin/redux/fetchProfile.test.js new file mode 100644 index 0000000..021f0b0 --- /dev/null +++ b/tests/features/user-admin/redux/fetchProfile.test.js @@ -0,0 +1,97 @@ +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import nock from 'nock'; + +import { + USER_ADMIN_FETCH_PROFILE_BEGIN, + USER_ADMIN_FETCH_PROFILE_SUCCESS, + USER_ADMIN_FETCH_PROFILE_FAILURE, + USER_ADMIN_FETCH_PROFILE_DISMISS_ERROR, +} from '../../../../src/features/user-admin/redux/constants'; + +import { + fetchProfile, + dismissFetchProfileError, + reducer, +} from '../../../../src/features/user-admin/redux/fetchProfile'; + +const middlewares = [thunk]; +const mockStore = configureMockStore(middlewares); + +describe('user-admin/redux/fetchProfile', () => { + afterEach(() => { + nock.cleanAll(); + }); + + it('dispatches success action when fetchProfile succeeds', () => { + const store = mockStore({}); + + return store.dispatch(fetchProfile()) + .then(() => { + const actions = store.getActions(); + expect(actions[0]).toHaveProperty('type', USER_ADMIN_FETCH_PROFILE_BEGIN); + expect(actions[1]).toHaveProperty('type', USER_ADMIN_FETCH_PROFILE_SUCCESS); + }); + }); + + it('dispatches failure action when fetchProfile fails', () => { + const store = mockStore({}); + + return store.dispatch(fetchProfile({ error: true })) + .catch(() => { + const actions = store.getActions(); + expect(actions[0]).toHaveProperty('type', USER_ADMIN_FETCH_PROFILE_BEGIN); + expect(actions[1]).toHaveProperty('type', USER_ADMIN_FETCH_PROFILE_FAILURE); + expect(actions[1]).toHaveProperty('data.error', expect.anything()); + }); + }); + + it('returns correct action by dismissFetchProfileError', () => { + const expectedAction = { + type: USER_ADMIN_FETCH_PROFILE_DISMISS_ERROR, + }; + expect(dismissFetchProfileError()).toEqual(expectedAction); + }); + + it('handles action type USER_ADMIN_FETCH_PROFILE_BEGIN correctly', () => { + const prevState = { fetchUserPending: false }; + const state = reducer( + prevState, + { type: USER_ADMIN_FETCH_PROFILE_BEGIN } + ); + expect(state).not.toBe(prevState); // should be immutable + expect(state.fetchUserPending).toBe(true); + }); + + it('handles action type USER_ADMIN_FETCH_PROFILE_SUCCESS correctly', () => { + const prevState = { fetchUserPending: true }; + const state = reducer( + prevState, + { type: USER_ADMIN_FETCH_PROFILE_SUCCESS, data: {} } + ); + expect(state).not.toBe(prevState); // should be immutable + expect(state.fetchUserPending).toBe(false); + }); + + it('handles action type USER_ADMIN_FETCH_PROFILE_FAILURE correctly', () => { + const prevState = { fetchUserPending: true }; + const state = reducer( + prevState, + { type: USER_ADMIN_FETCH_PROFILE_FAILURE, data: { error: new Error('some error') } } + ); + expect(state).not.toBe(prevState); // should be immutable + expect(state.fetchUserPending).toBe(false); + expect(state.fetchUserError).toEqual(expect.anything()); + }); + + it('handles action type USER_ADMIN_FETCH_PROFILE_DISMISS_ERROR correctly', () => { + const prevState = { fetchUserError: new Error('some error') }; + const state = reducer( + prevState, + { type: USER_ADMIN_FETCH_PROFILE_DISMISS_ERROR } + ); + expect(state).not.toBe(prevState); // should be immutable + expect(state.fetchUserError).toBe(null); + }); +}); +