diff --git a/mobile/package.json b/mobile/package.json index 7f0c6b4..1066260 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -36,6 +36,7 @@ "@babel/core": "^7.6.2", "@babel/runtime": "^7.6.2", "@react-native-community/eslint-config": "^1.0.0", + "@testing-library/react-hooks": "^3.3.0", "@types/jest": "^24.0.24", "@types/react-native": "^0.62.0", "@types/react-native-vector-icons": "^6.4.5", diff --git a/mobile/src/__tests__/hooks/auth.ts b/mobile/src/__tests__/hooks/auth.ts new file mode 100644 index 0000000..4b27221 --- /dev/null +++ b/mobile/src/__tests__/hooks/auth.ts @@ -0,0 +1,187 @@ +import { renderHook, act } from '@testing-library/react-hooks'; +import AsyncStorage from '@react-native-community/async-storage'; +import MockAdapter from 'axios-mock-adapter'; + +import { useAuth, AuthProvider } from '../../hooks/auth'; +import api from '../../services/api'; + +const setItemSpy = jest.spyOn(AsyncStorage, 'setItem'); +const multiSetSpy = jest.spyOn(AsyncStorage, 'multiSet'); +const multiGetSpy = jest.spyOn(AsyncStorage, 'multiGet'); +const multiRemoveSpy = jest.spyOn(AsyncStorage, 'multiRemove'); + +const mockApi = new MockAdapter(api); + +describe('Auth hook', () => { + it('should be able get context', async () => { + const { result } = renderHook(() => useAuth(), { + wrapper: AuthProvider, + }); + + await act(async () => { + expect(result.current.user).toBeUndefined(); + expect(result.current.loading).toBe(true); + expect(typeof result.current.signIn).toBe('function'); + expect(typeof result.current.signOut).toBe('function'); + }); + }); + + it('should be able to sign in', async () => { + const apiResponse = { + user: { + id: 'user-id', + name: 'user-name', + email: 'user@email.com', + avatar_url: 'user-avatar.png', + }, + token: 'user-token', + }; + + mockApi.onPost('/sessions').reply(200, apiResponse); + + const { result, waitForNextUpdate } = renderHook(() => useAuth(), { + wrapper: AuthProvider, + }); + + result.current.signIn({ + email: 'user@email.com', + password: 'user-password', + }); + + await waitForNextUpdate(); + + expect(result.current.user.email).toEqual('user@email.com'); + + expect(multiSetSpy).toHaveBeenCalledWith( + expect.arrayContaining([ + ['@GoBarber:token', apiResponse.token], + ['@GoBarber:user', JSON.stringify(apiResponse.user)], + ]), + ); + }); + + it('should be able recover data from AsyncStorage', async () => { + const mockData = { + user: { + id: 'user-id', + name: 'user-name', + email: 'user@email.com', + avatar_url: 'user-avatar.png', + }, + token: 'user-token', + }; + + multiGetSpy.mockImplementation( + (keys: string[]): Promise<[string, string | null][]> => { + return new Promise(resolve => { + const values = keys.map(key => { + switch (key) { + case '@GoBarber:user': + return ['@GoBarber:user', JSON.stringify(mockData.user)]; + + case '@GoBarber:token': + return ['@GoBarber:token', JSON.stringify(mockData.token)]; + + default: + return [key, null]; + } + }); + + const returningValue = values as [string, string | null][]; + + setTimeout(() => { + resolve(returningValue); + }, 200); + }); + }, + ); + + const { result, waitForNextUpdate } = renderHook(() => useAuth(), { + wrapper: AuthProvider, + }); + + await waitForNextUpdate(); + + expect(result.current.user).toEqual(expect.objectContaining(mockData.user)); + }); + + it('should be able to sign out', async () => { + const mockData = { + user: { + id: 'user-id', + name: 'user-name', + email: 'user@email.com', + avatar_url: 'user-avatar.png', + }, + token: 'user-token', + }; + + multiGetSpy.mockImplementation( + (keys: string[]): Promise<[string, string | null][]> => { + return new Promise(resolve => { + const values = keys.map(key => { + switch (key) { + case '@GoBarber:user': + return ['@GoBarber:user', JSON.stringify(mockData.user)]; + + case '@GoBarber:token': + return ['@GoBarber:token', JSON.stringify(mockData.token)]; + + default: + return [key, null]; + } + }); + + const returningValue = values as [string, string | null][]; + + setTimeout(() => { + resolve(returningValue); + }, 200); + }); + }, + ); + + const { result, waitForNextUpdate } = renderHook(() => useAuth(), { + wrapper: AuthProvider, + }); + + await waitForNextUpdate(); + + expect(result.current.user).toEqual(expect.objectContaining(mockData.user)); + + result.current.signOut(); + + expect(multiRemoveSpy).toHaveBeenCalledWith([ + '@GoBarber:token', + '@GoBarber:user', + ]); + + await waitForNextUpdate(); + + expect(result.current.user).toBeUndefined(); + }); + + it('should be able to sign out', async () => { + const user = { + id: 'user-id', + name: 'user-name', + email: 'user@email.com', + avatar_url: 'user-avatar.png', + }; + + const { result, waitForNextUpdate } = renderHook(() => useAuth(), { + wrapper: AuthProvider, + }); + + result.current.updateUser(user); + + expect(setItemSpy).toHaveBeenCalledWith( + '@GoBarber:user', + JSON.stringify(user), + ); + + await waitForNextUpdate(); + + expect(result.current.user).toEqual(expect.objectContaining(user)); + }); +}); diff --git a/mobile/src/hooks/auth.tsx b/mobile/src/hooks/auth.tsx index e420d5f..c6c32a7 100644 --- a/mobile/src/hooks/auth.tsx +++ b/mobile/src/hooks/auth.tsx @@ -107,10 +107,6 @@ const AuthProvider: React.FC = ({ children }) => { function useAuth(): AuthContextData { const context = useContext(AuthContext); - if (!context) { - throw new Error('useAuth must be used within an AuthProvider'); - } - return context; } diff --git a/mobile/yarn.lock b/mobile/yarn.lock index 87ae4a3..83445b1 100644 --- a/mobile/yarn.lock +++ b/mobile/yarn.lock @@ -632,6 +632,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.5.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.5.tgz#303d8bd440ecd5a491eae6117fd3367698674c5c" + integrity sha512-otddXKhdNn7d0ptoFRHtMLa8LqDxLYwTjB4nYgM1yy5N6gU/MUf8zqyyLltCH3yAVitBzmwK4us+DD0l/MauAg== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.0.0", "@babel/template@^7.4.0", "@babel/template@^7.8.3", "@babel/template@^7.8.6": version "7.8.6" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b" @@ -1090,6 +1097,14 @@ ramda "^0.26.1" redent "^2.0.0" +"@testing-library/react-hooks@^3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-3.3.0.tgz#dc217bfce8e7c34a99c811d73d23feef957b7c1d" + integrity sha512-rE9geI1+HJ6jqXkzzJ6abREbeud6bLF8OmF+Vyc7gBoPwZAEVBYjbC1up5nNoVfYBhO5HUwdD4u9mTehAUeiyw== + dependencies: + "@babel/runtime" "^7.5.4" + "@types/testing-library__react-hooks" "^3.0.0" + "@types/babel__core@^7.1.0": version "7.1.7" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.7.tgz#1dacad8840364a57c98d0dd4855c6dd3752c6b89" @@ -1203,7 +1218,7 @@ dependencies: "@types/react" "*" -"@types/react-test-renderer@16.9.2": +"@types/react-test-renderer@*", "@types/react-test-renderer@16.9.2": version "16.9.2" resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.9.2.tgz#e1c408831e8183e5ad748fdece02214a7c2ab6c5" integrity sha512-4eJr1JFLIAlWhzDkBCkhrOIWOvOxcCAfQh+jiKg7l/nNZcCIL2MHl2dZhogIFKyHzedVWHaVP1Yydq/Ruu4agw== @@ -1233,6 +1248,14 @@ "@types/react-native" "*" csstype "^2.2.0" +"@types/testing-library__react-hooks@^3.0.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@types/testing-library__react-hooks/-/testing-library__react-hooks-3.2.0.tgz#52f3a109bef06080e3b1e3ae7ea1c014ce859897" + integrity sha512-dE8iMTuR5lzB+MqnxlzORlXzXyCL0EKfzH0w/lau20OpkHD37EaWjZDz0iNG8b71iEtxT4XKGmSKAGVEqk46mw== + dependencies: + "@types/react" "*" + "@types/react-test-renderer" "*" + "@types/yargs-parser@*": version "15.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d"