refactor(typings): migrate 'auth' state slice to TypeScript (#4698)

This commit is contained in:
Vladislav Shkodin 2020-12-13 13:35:07 +02:00 committed by GitHub
parent 5aec65fed5
commit 929ca380bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 104 additions and 71 deletions

View File

@ -1,5 +1,9 @@
import { actions as notifActions } from 'redux-notifications'; import { actions as notifActions } from 'redux-notifications';
import { currentBackend } from 'coreSrc/backend'; import { Credentials, User } from 'netlify-cms-lib-util';
import { ThunkDispatch } from 'redux-thunk';
import { AnyAction } from 'redux';
import { currentBackend } from '../backend';
import { State } from '../types/redux';
const { notifSend, notifClear } = notifActions; const { notifSend, notifClear } = notifActions;
@ -13,50 +17,49 @@ export const LOGOUT = 'LOGOUT';
export function authenticating() { export function authenticating() {
return { return {
type: AUTH_REQUEST, type: AUTH_REQUEST,
}; } as const;
} }
export function authenticate(userData) { export function authenticate(userData: User) {
return { return {
type: AUTH_SUCCESS, type: AUTH_SUCCESS,
payload: userData, payload: userData,
}; } as const;
} }
export function authError(error) { export function authError(error: Error) {
return { return {
type: AUTH_FAILURE, type: AUTH_FAILURE,
error: 'Failed to authenticate', error: 'Failed to authenticate',
payload: error, payload: error,
}; } as const;
} }
export function doneAuthenticating() { export function doneAuthenticating() {
return { return {
type: AUTH_REQUEST_DONE, type: AUTH_REQUEST_DONE,
}; } as const;
} }
export function useOpenAuthoring() { export function useOpenAuthoring() {
return { return {
type: USE_OPEN_AUTHORING, type: USE_OPEN_AUTHORING,
}; } as const;
} }
export function logout() { export function logout() {
return { return {
type: LOGOUT, type: LOGOUT,
}; } as const;
} }
// Check if user data token is cached and is valid // Check if user data token is cached and is valid
export function authenticateUser() { export function authenticateUser() {
return (dispatch, getState) => { return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
const state = getState(); const state = getState();
const backend = currentBackend(state.config); const backend = currentBackend(state.config);
dispatch(authenticating()); dispatch(authenticating());
return backend return Promise.resolve(backend.currentUser())
.currentUser()
.then(user => { .then(user => {
if (user) { if (user) {
if (user.useOpenAuthoring) { if (user.useOpenAuthoring) {
@ -67,15 +70,15 @@ export function authenticateUser() {
dispatch(doneAuthenticating()); dispatch(doneAuthenticating());
} }
}) })
.catch(error => { .catch((error: Error) => {
dispatch(authError(error)); dispatch(authError(error));
dispatch(logoutUser()); dispatch(logoutUser());
}); });
}; };
} }
export function loginUser(credentials) { export function loginUser(credentials: Credentials) {
return (dispatch, getState) => { return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
const state = getState(); const state = getState();
const backend = currentBackend(state.config); const backend = currentBackend(state.config);
@ -88,7 +91,7 @@ export function loginUser(credentials) {
} }
dispatch(authenticate(user)); dispatch(authenticate(user));
}) })
.catch(error => { .catch((error: Error) => {
console.error(error); console.error(error);
dispatch( dispatch(
notifSend({ notifSend({
@ -106,7 +109,7 @@ export function loginUser(credentials) {
} }
export function logoutUser() { export function logoutUser() {
return (dispatch, getState) => { return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
const state = getState(); const state = getState();
const backend = currentBackend(state.config); const backend = currentBackend(state.config);
Promise.resolve(backend.logout()).then(() => { Promise.resolve(backend.logout()).then(() => {
@ -115,3 +118,11 @@ export function logoutUser() {
}); });
}; };
} }
export type AuthAction = ReturnType<
| typeof authenticating
| typeof authenticate
| typeof authError
| typeof doneAuthenticating
| typeof logout
>;

View File

@ -1,33 +0,0 @@
import Immutable from 'immutable';
import { authenticating, authenticate, authError, logout } from 'Actions/auth';
import auth from '../auth';
describe('auth', () => {
it('should handle an empty state', () => {
expect(auth(undefined, {})).toEqual(null);
});
it('should handle an authentication request', () => {
expect(auth(undefined, authenticating())).toEqual(Immutable.Map({ isFetching: true }));
});
it('should handle authentication', () => {
expect(auth(undefined, authenticate({ email: 'joe@example.com' }))).toEqual(
Immutable.fromJS({ user: { email: 'joe@example.com' } }),
);
});
it('should handle an authentication error', () => {
expect(auth(undefined, authError(new Error('Bad credentials')))).toEqual(
Immutable.Map({
error: 'Error: Bad credentials',
}),
);
});
it('should handle logout', () => {
const initialState = Immutable.fromJS({ user: { email: 'joe@example.com' } });
const newState = auth(initialState, logout());
expect(newState.get('user')).toBeUndefined();
});
});

View File

@ -0,0 +1,32 @@
import { fromJS } from 'immutable';
import { authenticating, authenticate, authError, logout } from '../../actions/auth';
import auth, { defaultState } from '../auth';
describe('auth', () => {
it('should handle an empty state', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
expect(auth(undefined, {})).toEqual(defaultState);
});
it('should handle an authentication request', () => {
expect(auth(undefined, authenticating())).toEqual(defaultState.set('isFetching', true));
});
it('should handle authentication', () => {
const user = { name: 'joe', token: 'token' };
expect(auth(undefined, authenticate(user))).toEqual(defaultState.set('user', fromJS(user)));
});
it('should handle an authentication error', () => {
expect(auth(undefined, authError(new Error('Bad credentials')))).toEqual(
defaultState.set('error', 'Error: Bad credentials'),
);
});
it('should handle logout', () => {
const user = { name: 'joe', token: 'token' };
const newState = auth(defaultState.set('user', fromJS(user)), logout());
expect(newState.get('user')).toBeUndefined();
});
});

View File

@ -1,21 +0,0 @@
import Immutable from 'immutable';
import { AUTH_REQUEST, AUTH_SUCCESS, AUTH_FAILURE, AUTH_REQUEST_DONE, LOGOUT } from 'Actions/auth';
const auth = (state = null, action) => {
switch (action.type) {
case AUTH_REQUEST:
return Immutable.Map({ isFetching: true });
case AUTH_SUCCESS:
return Immutable.fromJS({ user: action.payload });
case AUTH_FAILURE:
return Immutable.Map({ error: action.payload && action.payload.toString() });
case AUTH_REQUEST_DONE:
return state.remove('isFetching');
case LOGOUT:
return state.remove('user').remove('isFetching');
default:
return state;
}
};
export default auth;

View File

@ -0,0 +1,42 @@
import { fromJS } from 'immutable';
import { User } from 'netlify-cms-lib-util';
import {
AUTH_REQUEST,
AUTH_SUCCESS,
AUTH_FAILURE,
AUTH_REQUEST_DONE,
LOGOUT,
AuthAction,
} from '../actions/auth';
import { StaticallyTypedRecord } from '../types/immutable';
export type Auth = StaticallyTypedRecord<{
isFetching: boolean;
user: StaticallyTypedRecord<User> | undefined;
error: string | undefined;
}>;
export const defaultState = fromJS({
isFetching: false,
user: undefined,
error: undefined,
});
const auth = (state = defaultState as Auth, action: AuthAction) => {
switch (action.type) {
case AUTH_REQUEST:
return state.set('isFetching', true);
case AUTH_SUCCESS:
return state.set('user', fromJS(action.payload));
case AUTH_FAILURE:
return state.set('error', action.payload && action.payload.toString());
case AUTH_REQUEST_DONE:
return state.set('isFetching', false);
case LOGOUT:
return state.set('user', undefined).set('isFetching', false);
default:
return state;
}
};
export default auth;

View File

@ -3,6 +3,7 @@ import { StaticallyTypedRecord } from './immutable';
import { Map, List, OrderedMap, Set } from 'immutable'; import { Map, List, OrderedMap, Set } from 'immutable';
import AssetProxy from '../valueObjects/AssetProxy'; import AssetProxy from '../valueObjects/AssetProxy';
import { MediaFile as BackendMediaFile } from '../backend'; import { MediaFile as BackendMediaFile } from '../backend';
import { Auth } from '../reducers/auth';
export type SlugConfig = StaticallyTypedRecord<{ export type SlugConfig = StaticallyTypedRecord<{
encoding: string; encoding: string;
@ -295,6 +296,7 @@ export type Status = StaticallyTypedRecord<{
}>; }>;
export interface State { export interface State {
auth: Auth;
config: Config; config: Config;
cursors: Cursors; cursors: Cursors;
collections: Collections; collections: Collections;