-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #45 from googleinterns/authenticate-on-all-pages-r…
…eact Authentication component that tracks user authentication status
- Loading branch information
Showing
8 changed files
with
253 additions
and
17 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import React, { useState, useEffect } from 'react'; | ||
import app from '../Firebase'; | ||
|
||
const AuthContext = React.createContext(); | ||
|
||
/** | ||
* AuthProvider component that keeps track of the authentication status of the | ||
* current user. Allows global use of this status using React Context and should | ||
* be wrapped around the contents of the App component. | ||
*/ | ||
const AuthProvider = (props) => { | ||
const [currentUser, setCurrentUser] = useState(null); | ||
const [isLoading, setIsLoading] = useState(true); | ||
|
||
useEffect(() => { | ||
// Set up listener that changes user status whenever it is updated. | ||
app.auth().onAuthStateChanged((user) => { | ||
setCurrentUser(user); | ||
setIsLoading(false); | ||
}); | ||
}, []); | ||
|
||
if (isLoading) { | ||
// TODO (Issue #25): Page initially displays "Loading..." for testing | ||
// purposes, make this blank in the deployed build. | ||
return ( <h1>Loading...</h1> ); | ||
} | ||
|
||
return ( | ||
<AuthContext.Provider value={currentUser}> | ||
{props.children} | ||
</AuthContext.Provider> | ||
); | ||
} | ||
|
||
export { AuthContext, AuthProvider }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import React, { useContext } from 'react'; | ||
import { render, act, screen, cleanup } from '@testing-library/react'; | ||
import { AuthContext, AuthProvider } from './AuthContext.js'; | ||
|
||
jest.useFakeTimers(); | ||
|
||
// All times are in milliseconds. | ||
const TIME_BEFORE_USER_IS_LOADED = 500; | ||
const TIME_WHEN_USER_IS_LOADED = 1000; | ||
const TIME_AFTER_USER_IS_LOADED = 2000; | ||
|
||
// Mock the the Firebase Auth onAuthStateChanged function, which pauses for the | ||
// time given by TIME_WHEN_USER_IS_LOADED, then returns a fake user with only | ||
// the property `name: 'Keiffer'`. | ||
const mockOnAuthStateChanged = jest.fn(callback => { | ||
setTimeout(() => { | ||
callback({ name: 'Keiffer' }) | ||
}, TIME_WHEN_USER_IS_LOADED); | ||
}); | ||
jest.mock('firebase/app', () => { | ||
return { | ||
initializeApp: () => { | ||
return { | ||
auth: () => { | ||
return { | ||
onAuthStateChanged: mockOnAuthStateChanged | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
|
||
afterEach(cleanup); | ||
|
||
describe('AuthProvider component', () => { | ||
beforeEach(() => { render(<AuthProvider />) }); | ||
|
||
it('initially displays "Loading"', () => { | ||
act(() => jest.advanceTimersByTime(TIME_BEFORE_USER_IS_LOADED)); | ||
expect(screen.getByText('Loading...')).toBeInTheDocument(); | ||
}); | ||
|
||
it('returns a provider when onAuthStateChanged is called', () => { | ||
act(() => jest.advanceTimersByTime(TIME_AFTER_USER_IS_LOADED)); | ||
expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); | ||
}); | ||
}); | ||
|
||
describe('AuthContext Consumer component', () => { | ||
// A Consumer component for AuthContext that just displays the current | ||
// user. | ||
const TestAuthContextConsumerComponent = () => { | ||
const currentUser = useContext(AuthContext); | ||
|
||
return ( | ||
<div> | ||
<div>{currentUser.name}</div> | ||
</div> | ||
); | ||
}; | ||
|
||
beforeEach(() => { | ||
render( | ||
<AuthProvider> | ||
<TestAuthContextConsumerComponent /> | ||
</AuthProvider> | ||
); | ||
}); | ||
|
||
it('initially displays "Loading"', () => { | ||
act(() => jest.advanceTimersByTime(TIME_BEFORE_USER_IS_LOADED)); | ||
expect(screen.getByText('Loading...')).toBeInTheDocument(); | ||
}); | ||
|
||
it('displays the current user when they are authenticated', () => { | ||
act(() => jest.advanceTimersByTime(TIME_AFTER_USER_IS_LOADED)); | ||
expect(screen.getByText('Keiffer')).toBeInTheDocument(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import React, { useContext } from 'react'; | ||
import { Route, Redirect } from 'react-router-dom'; | ||
import { AuthContext } from '../Auth'; | ||
|
||
import { SIGN_IN } from '../../constants/routes.js'; | ||
|
||
/** | ||
* PrivateRoute component that functions similarly to the `Route` component, | ||
* with the added check that determines if the user is currently signed in. | ||
* | ||
* This component takes the authentication status of the current user from | ||
* AuthContext. If they are authenticated, they will be allowed to view the | ||
* contents of the Route component. If they are not authenticated, they will be | ||
* redirected to the SIGN_IN page. | ||
* | ||
* @param {Object} props The following props are expected: | ||
* - component {React.Component} The component that `PrivateRoute` should render | ||
* if the user is currently authenticated. | ||
*/ | ||
const PrivateRoute = ({ component: RouteComponent, ...rest }) => { | ||
const currentUser = useContext(AuthContext); | ||
|
||
// The rest of the props passed into `PrivateRoute` are given as props to the | ||
// `Route` component as normal. The render prop is used to specify that, if | ||
// the user is signed in, the given component to render should be rendered | ||
// along with all the standard Route paths (URL path, etc.), and if the user | ||
// is not signed in, a `Redirect` prop should be rendered instead. | ||
return ( | ||
<Route | ||
{...rest} | ||
render={routeProps => | ||
currentUser ? <RouteComponent {...routeProps} /> | ||
: <Redirect to={SIGN_IN} /> | ||
} | ||
/> | ||
); | ||
} | ||
|
||
export default PrivateRoute; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import React from 'react'; | ||
import { Router, Route } from 'react-router-dom'; | ||
import { render, screen, cleanup } from '@testing-library/react'; | ||
import { createMemoryHistory } from 'history'; | ||
import PrivateRoute from './PrivateRoute.js'; | ||
|
||
import { SIGN_IN } from '../../constants/routes.js'; | ||
|
||
const history = createMemoryHistory(); | ||
|
||
// Mock the useContext function so that, when called in the PrivateRoute | ||
// component, returns null the first time and a fake user the second time. | ||
jest.mock('react', () => ( | ||
{ | ||
...(jest.requireActual('react')), | ||
useContext: jest | ||
.fn() | ||
.mockReturnValueOnce(null) | ||
.mockReturnValueOnce({ name: 'Keiffer' }) | ||
} | ||
)); | ||
|
||
describe('PrivateRoute component', () => { | ||
const TestComponent = () => { | ||
return ( | ||
<div>Hello, World!</div> | ||
); | ||
}; | ||
|
||
beforeEach(() => { | ||
render( | ||
<Router history={history}> | ||
<PrivateRoute component={TestComponent} /> | ||
<Route path={SIGN_IN} /> | ||
</Router> | ||
); | ||
}); | ||
|
||
afterEach(cleanup); | ||
|
||
// mockOnAuthStateChanged called first time, so user should be null. | ||
it('redirects to SIGN_IN when the user is not authenticated', () => { | ||
expect(history.location.pathname).toEqual(SIGN_IN); | ||
}); | ||
|
||
// mockOnAuthStateChanged called second time, so user should not be null. | ||
it('renders the given component when the user is authenticated', () => { | ||
expect(screen.getByText('Hello, World!')).toBeInTheDocument(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import { AuthContext, AuthProvider } from './AuthContext.js'; | ||
import PrivateRoute from './PrivateRoute.js'; | ||
|
||
export { AuthContext, AuthProvider, PrivateRoute }; |