diff --git a/src/components/auth/AuthForm.jsx b/src/components/auth/AuthForm.jsx index cb0c1a8..3f9a0ff 100644 --- a/src/components/auth/AuthForm.jsx +++ b/src/components/auth/AuthForm.jsx @@ -11,7 +11,9 @@ const FORM_TYPE = { register: '회원가입', }; -const AuthForm = ({ type, onChange, fields }) => { +const AuthForm = ({ + type, fields, onChange, onSubmit, +}) => { const formType = FORM_TYPE[type]; const { userEmail, password } = fields; @@ -22,35 +24,49 @@ const AuthForm = ({ type, onChange, fields }) => { onChange({ name, value }); }; + const handleSubmit = (e) => { + e.preventDefault(); + + onSubmit(); + }; + return (

{formType}

- - - {type === 'register' && ( +
+ - )} + {type === 'register' && ( + + )} + +
); }; diff --git a/src/components/auth/AuthForm.test.jsx b/src/components/auth/AuthForm.test.jsx index 48b0665..5b5f90f 100644 --- a/src/components/auth/AuthForm.test.jsx +++ b/src/components/auth/AuthForm.test.jsx @@ -6,9 +6,11 @@ import AuthForm from './AuthForm'; describe('AuthForm', () => { const handleChange = jest.fn(); + const handleSubmit = jest.fn(); beforeEach(() => { handleChange.mockClear(); + handleSubmit.mockClear(); }); const renderAuthForm = ({ type, fields }) => render(( @@ -16,6 +18,7 @@ describe('AuthForm', () => { type={type} fields={fields} onChange={handleChange} + onSubmit={handleSubmit} /> )); @@ -94,5 +97,17 @@ describe('AuthForm', () => { expect(handleChange).toBeCalled(); }); }); + + it('listens event call submit', () => { + const { getByTestId } = renderAuthForm(register); + + const button = getByTestId('auth-button'); + + expect(button).not.toBeNull(); + + fireEvent.submit(button); + + expect(handleSubmit).toBeCalled(); + }); }); }); diff --git a/src/containers/auth/RegisterFormContainer.jsx b/src/containers/auth/RegisterFormContainer.jsx index ecdbc9c..036d1e9 100644 --- a/src/containers/auth/RegisterFormContainer.jsx +++ b/src/containers/auth/RegisterFormContainer.jsx @@ -1,16 +1,20 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; import { useSelector, useDispatch } from 'react-redux'; import { get } from '../../util/utils'; -import { changeAuthField } from '../../reducers/slice'; +import { changeAuthField, clearAuth, requestRegister } from '../../reducers/slice'; import AuthForm from '../../components/auth/AuthForm'; const RegisterFormContainer = () => { const dispatch = useDispatch(); + const history = useHistory(); const register = useSelector(get('register')); + const auth = useSelector(get('auth')); + const authError = useSelector(get('authError')); const onChangeRegisterField = useCallback(({ name, value }) => { dispatch( @@ -20,13 +24,33 @@ const RegisterFormContainer = () => { value, }), ); - }); + }, [dispatch]); + + const onSubmit = useCallback(() => { + dispatch(requestRegister()); + }, [dispatch]); + + useEffect(() => { + if (auth) { + history.push('/login'); + } + + if (authError) { + // TODO: 추 후 error 처리 + console.error(authError); + } + + return () => { + dispatch(clearAuth()); + }; + }, [dispatch, auth, authError]); return ( ); }; diff --git a/src/containers/auth/RegisterFormContainer.test.jsx b/src/containers/auth/RegisterFormContainer.test.jsx index 09bfc35..f7149e3 100644 --- a/src/containers/auth/RegisterFormContainer.test.jsx +++ b/src/containers/auth/RegisterFormContainer.test.jsx @@ -1,11 +1,21 @@ import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; import { render, fireEvent } from '@testing-library/react'; import RegisterFormContainer from './RegisterFormContainer'; +const mockPush = jest.fn(); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory() { + return { push: mockPush }; + }, +})); + describe('RegisterFormContainer', () => { const dispatch = jest.fn(); @@ -19,11 +29,15 @@ describe('RegisterFormContainer', () => { password: '', passwordConfirm: '', }, + auth: given.auth, + authError: given.authError, })); }); const renderRegisterFormContainer = () => render(( - + + + )); it('renders register form text', () => { @@ -62,5 +76,38 @@ describe('RegisterFormContainer', () => { }); }); }); + + it('submit event calls dispatch', () => { + const { getByTestId } = renderRegisterFormContainer(); + + const button = getByTestId('auth-button'); + + expect(button).not.toBeNull(); + + fireEvent.submit(button); + + expect(dispatch).toBeCalled(); + }); + }); + + describe('action after signing up', () => { + context('when success auth to register', () => { + given('auth', () => ({ + auth: 'seungmin@naver.com', + })); + + it('go to login page', () => { + renderRegisterFormContainer(); + + expect(mockPush).toBeCalledWith('/login'); + }); + }); + + // TODO: 현재 authError는 콘솔 출력 + context('when failure auth to register', () => { + given('authError', () => ({ + authError: 'error', + })); + }); }); }); diff --git a/src/reducers/slice.js b/src/reducers/slice.js index dcb2ae4..dedcf5b 100644 --- a/src/reducers/slice.js +++ b/src/reducers/slice.js @@ -6,6 +6,7 @@ import { getStudyGroup, getStudyGroups, postStudyGroup, + postUserRegister, } from '../services/api'; const writeInitialState = { @@ -39,6 +40,8 @@ const { actions, reducer } = createSlice({ writeField: writeInitialState, register: authInitialState.register, login: authInitialState.login, + auth: null, + authError: null, }, reducers: { @@ -95,6 +98,38 @@ const { actions, reducer } = createSlice({ draft[form][name] = value; }); }, + + clearAuthFields(state) { + const { register, login } = authInitialState; + + return { + ...state, + register, + login, + }; + }, + + setAuth(state, { payload: user }) { + return { + ...state, + auth: user, + }; + }, + + setAuthError(state, { payload: error }) { + return { + ...state, + authError: error, + }; + }, + + clearAuth(state) { + return { + ...state, + auth: null, + authError: null, + }; + }, }, }); @@ -105,6 +140,10 @@ export const { clearWriteFields, successWrite, changeAuthField, + clearAuthFields, + setAuth, + setAuthError, + clearAuth, } = actions; export const loadStudyGroups = (tag) => async (dispatch) => { @@ -135,4 +174,18 @@ export const writeStudyGroup = () => async (dispatch, getState) => { dispatch(clearWriteFields()); }; +export const requestRegister = () => async (dispatch, getState) => { + const { register: { userEmail, password } } = getState(); + + try { + const { user } = await postUserRegister({ userEmail, password }); + + dispatch(setAuth(user.email)); + + dispatch(clearAuthFields()); + } catch (error) { + setAuthError(error); + } +}; + export default reducer; diff --git a/src/reducers/slice.test.js b/src/reducers/slice.test.js index 158aa62..96e4e62 100644 --- a/src/reducers/slice.test.js +++ b/src/reducers/slice.test.js @@ -13,11 +13,17 @@ import reducer, { clearWriteFields, successWrite, changeAuthField, + clearAuthFields, + setAuth, + setAuthError, + clearAuth, + requestRegister, } from './slice'; import STUDY_GROUPS from '../../fixtures/study-groups'; import STUDY_GROUP from '../../fixtures/study-group'; import WRITE_FORM from '../../fixtures/write-form'; +import { postUserRegister } from '../services/api'; const middlewares = [thunk]; const mockStore = configureStore(middlewares); @@ -48,6 +54,8 @@ describe('reducer', () => { userEmail: '', password: '', }, + auth: null, + authError: null, }; it('returns initialState', () => { @@ -197,6 +205,69 @@ describe('reducer', () => { }); }); }); + + describe('clearAuthFields', () => { + const initialState = { + register: { + userEmail: 'seungmin@naver.com', + password: '1234', + passwordConfirm: '1234', + }, + login: { + userEmail: 'seungmin@naver.com', + password: '1234', + }, + }; + + it('auth form is all cleared', () => { + const { register, login } = reducer(initialState, clearAuthFields()); + + expect(register.userEmail).toBe(''); + expect(login.userEmail).toBe(''); + }); + }); + + describe('setAuth', () => { + const initialState = { + auth: null, + }; + + it('authentication success', () => { + const userEmail = 'seungmin@naver.com'; + + const { auth } = reducer(initialState, setAuth(userEmail)); + + expect(auth).toBe(userEmail); + }); + }); + + describe('setAuthError', () => { + const initialState = { + authError: null, + }; + + it('authentication failure', () => { + const error = 'error message'; + + const { authError } = reducer(initialState, setAuthError(error)); + + expect(authError).toBe(error); + }); + }); + + describe('clearAuth', () => { + const initialState = { + auth: 'seungmin@naver.com', + authError: 'error', + }; + + it('Clean up to auth and authError', () => { + const { auth, authError } = reducer(initialState, clearAuth()); + + expect(auth).toBe(null); + expect(authError).toBe(null); + }); + }); }); describe('async actions', () => { @@ -250,4 +321,51 @@ describe('async actions', () => { expect(actions[1]).toEqual(clearWriteFields(undefined)); }); }); + + describe('requestRegister', () => { + const register = { + userEmail: 'seungmin@naver.com', + password: '123456', + }; + + beforeEach(() => { + store = mockStore({ + register, + }); + }); + + context('without auth error', () => { + const { userEmail } = register; + + postUserRegister.mockImplementationOnce(() => ({ + user: { + email: userEmail, + }, + })); + it('dispatches requestRegister action success to return user email', async () => { + await store.dispatch(requestRegister()); + + const actions = store.getActions(); + + expect(actions[0]).toEqual(setAuth(userEmail)); + expect(actions[1]).toEqual(clearAuthFields()); + }); + }); + + context('with auth error', () => { + postUserRegister.mockImplementationOnce(() => { + throw new Error('error'); + }); + + it('dispatches requestRegister action failure to return error', async () => { + try { + await store.dispatch(requestRegister()); + } catch (error) { + const actions = store.getActions(); + + expect(actions[0]).toEqual(setAuthError(error)); + } + }); + }); + }); }); diff --git a/src/services/__mocks__/api.js b/src/services/__mocks__/api.js index 55c13c7..2dee810 100644 --- a/src/services/__mocks__/api.js +++ b/src/services/__mocks__/api.js @@ -3,3 +3,5 @@ export const getStudyGroups = async () => []; export const getStudyGroup = async () => {}; export const postStudyGroup = async () => {}; + +export const postUserRegister = jest.fn(); diff --git a/src/services/api.js b/src/services/api.js index 3500951..36b69b9 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -1,4 +1,4 @@ -import db from './firebase'; +import { db, auth } from './firebase'; export const getStudyGroups = async () => { const response = await db.collection('groups').get(); @@ -26,3 +26,23 @@ export const postStudyGroup = async (post) => { return id; }; + +export const postUserRegister = async ({ userEmail, password }) => { + const response = await auth + .createUserWithEmailAndPassword(userEmail, password); + + return response; +}; + +export const postUserLogin = async ({ userEmail, password }) => { + const response = await auth + .signInWithEmailAndPassword(userEmail, password); + + return response; +}; + +export const postUserLogout = async () => { + const response = await auth.signOut(); + + return response; +}; diff --git a/src/services/api.test.js b/src/services/api.test.js index 2a07906..ae8ddd1 100644 --- a/src/services/api.test.js +++ b/src/services/api.test.js @@ -1,7 +1,10 @@ -import * as firebase from 'firebase'; +import { db, auth } from './firebase'; import { postStudyGroup, + postUserRegister, + postUserLogin, + postUserLogout, } from './api'; import STUDY_GROUP from '../../fixtures/study-group'; @@ -14,7 +17,7 @@ describe('api', () => { describe('postStudyGroup', () => { const add = jest.fn((group) => group); const collection = jest.spyOn( - firebase.firestore(), 'collection', + db, 'collection', ).mockReturnValue({ add }); it('write a study recruitment article', async () => { @@ -25,4 +28,79 @@ describe('api', () => { expect(add).toHaveBeenCalledWith(STUDY_GROUP); }); }); + + describe('postUserRegister', () => { + const register = { + user: { + email: 'seungmin@naver.com', + password: '123456', + }, + }; + + beforeEach(() => { + auth.createUserWithEmailAndPassword = jest.fn().mockResolvedValue(register); + }); + + it('email returns after user sign up', async () => { + const { user } = await postUserRegister(register); + + const { user: { email } } = register; + + expect(user.email).toBe(email); + }); + }); + + describe('postUserRegister', () => { + const register = { + user: { + email: 'seungmin@naver.com', + password: '123456', + }, + }; + + beforeEach(() => { + auth.createUserWithEmailAndPassword = jest.fn().mockResolvedValue(register); + }); + + it('email returns after user sign up', async () => { + const { user } = await postUserRegister(register); + + const { user: { email } } = register; + + expect(user.email).toBe(email); + }); + }); + + describe('postUserLogin', () => { + const login = { + user: { + email: 'seungmin@naver.com', + password: '123456', + }, + }; + + beforeEach(() => { + auth.signInWithEmailAndPassword = jest.fn().mockResolvedValue(login); + }); + + it('email returns after user login', async () => { + const { user } = await postUserLogin(login); + + const { user: { email } } = login; + + expect(user.email).toBe(email); + }); + }); + + describe('postUserLogout', () => { + beforeEach(() => { + auth.signOut = jest.fn().mockResolvedValue(true); + }); + + it('returns true after success logout', async () => { + const response = await postUserLogout(); + + expect(response).toBe(true); + }); + }); });