Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Authenticate user for trip #76

Merged
merged 9 commits into from
Jul 28, 2020
28 changes: 15 additions & 13 deletions frontend/src/components/Auth/AuthContext.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@ import { AuthContext, AuthProvider } from './AuthContext.js';

jest.useFakeTimers();

// Mock the the Firebase Auth onAuthStateChanged function, which pauses for 1
// second before returning a fake user with only a name field set.
// 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' })
}, 1000);
}, TIME_WHEN_USER_IS_LOADED);
});
jest.mock('firebase/app', () => {
return {
Expand All @@ -25,18 +31,18 @@ jest.mock('firebase/app', () => {
}
});

afterEach(cleanup);

describe('AuthProvider component', () => {
beforeEach(() => { render(<AuthProvider />) });

afterEach(cleanup);

it('initially displays "Loading"', () => {
act(() => jest.advanceTimersByTime(500));
act(() => jest.advanceTimersByTime(TIME_BEFORE_USER_IS_LOADED));
expect(screen.getByText('Loading...')).toBeInTheDocument();
});

it('returns a provider when onAuthStateChanged is called', () => {
act(() => jest.advanceTimersByTime(2000));
act(() => jest.advanceTimersByTime(TIME_AFTER_USER_IS_LOADED));
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});
});
Expand All @@ -62,17 +68,13 @@ describe('AuthContext Consumer component', () => {
);
});

afterEach(() => {
cleanup();
});

it('initially displays "Loading"', () => {
act(() => jest.advanceTimersByTime(500));
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(2000));
act(() => jest.advanceTimersByTime(TIME_AFTER_USER_IS_LOADED));
expect(screen.getByText('Keiffer')).toBeInTheDocument();
});
});
87 changes: 80 additions & 7 deletions frontend/src/components/ViewActivities/index.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,89 @@
import React from 'react';
import app from '../Firebase';
import { getUserUid } from '../AuthUtils';
import ActivityList from './activitylist.js';

import * as DB from '../../constants/database.js';
keiffer01 marked this conversation as resolved.
Show resolved Hide resolved

/**
* ViewActivities component.
* The view activities page. First checks that the current user is authorized to
* view the current trip (i.e. they are a collaborator for it). If so, the
* ActivityList component is displayed as normal. If not, an error is displayed
* instead.
keiffer01 marked this conversation as resolved.
Show resolved Hide resolved
*
* @param {Object} props This component expects the following props:
* - `tripId` {string} The trip's ID. This is sent to the component through the URL.
*/
class ViewActivities extends React.Component {
/** @inheritdoc */
constructor(props) {
super(props);
this.tripId = props.match.params.tripId;
this.state = {
collaborators: undefined,
isLoading: true,
error: undefined
}
}

/** @inheritdoc */
componentDidMount() {
app.firestore()
.collection(DB.COLLECTION_TRIPS)
.doc(this.tripId)
.get()
.then(doc => {
this.setState({
collaborators: doc.get(DB.TRIPS_COLLABORATORS),
isLoading: false,
error: undefined
});
})
.catch(e => {
this.setState({
collaborators: undefined,
isLoading: true,
error: e
})
});
}

/** @inheritdoc */
render() {
return (
<div>
<h1>View Activities</h1>
</div>
);
// Case where there was a Firebase error.
if (this.state.error !== undefined) {
// TODO (Issue #74): Redirect to an error page instead.
return (
<div>
Oops, looks like something went wrong. Please wait a few minutes and
try again.
</div>
);
}
// Case where the trip details are still being fetched.
if (this.state.isLoading) {
// TODO (Issue #25): Please remember to make this a blank div in the
// deployed build lol.
return <div>Loading Part 2: Electric Boogaloo</div>;
}
// Case where the trip could not be found.
else if (this.state.collaborators === undefined) {
// TODO (Issue #74): Redirect to an error page instead.
return <div>Sorry, we couldn't find the trip you were looking for.</div>;
}
keiffer01 marked this conversation as resolved.
Show resolved Hide resolved
// Case where the current user is not authorized to view the page
else if (!this.state.collaborators.includes(getUserUid())) {
// TODO (Issue #74): Redirect to an error page instead.
return <div>Sorry, you're not authorized to view this trip.</div>;
keiffer01 marked this conversation as resolved.
Show resolved Hide resolved
}
else {
return (
<div className='activity-page'>
<ActivityList tripId={this.tripId}/>
</div>
);
}
}
};
}

export default ViewActivities;
73 changes: 73 additions & 0 deletions frontend/src/components/ViewActivities/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React from 'react';
import { render, screen, cleanup } from '@testing-library/react';
import ViewActivities from './index.js';
import authUtils from '../AuthUtils';

const FAKE_USER = 'totally-legit-user';
const FAKE_TRIPID = '12345';
const RESULT_AUTHORIZED = FAKE_TRIPID;
const RESULT_NOT_AUTHORIZED =
'Sorry, you\'re not authorized to view this trip.';
const RESULT_TRIP_DOESNT_EXIST =
'Sorry, we couldn\'t find the trip you were looking for.';

keiffer01 marked this conversation as resolved.
Show resolved Hide resolved
// Mock the getUserUid auth utility function to return a fake UID as given by
// FAKE_USER.
jest.mock('../AuthUtils');
authUtils.getUserUid.mockReturnValue(FAKE_USER);

// Mock the ActivityList component to simply render the passed-in tripId.
jest.mock('./activitylist.js', () => (props) => (
<div>{props.tripId}</div>
));

// Mock the different collaborator fields that can be returned from Firebase
// Firestore. The first time, it returns an array containing the fake user. The
// second time, it returns an array that does not contain the fake user. The
// third time, it returns undefined (imitating Firebase being unable to find the
// trip).
const mockGet = jest.fn()
.mockResolvedValueOnce({ get: function() {return [FAKE_USER]} })
.mockResolvedValueOnce({ get: function() {return []} })
.mockResolvedValueOnce({ get: function() {return undefined} });
jest.mock('firebase/app', () => {
return {
initializeApp: () => {
return {
firestore: () => {
return {
collection: (collectionPath) => {
return {
doc: (documentPath) => {
return {
get: mockGet
};
}
};
}
};
}
};
}
};
});

describe('ViewActivities page', () => {
beforeEach(() => {
render(<ViewActivities match={{params: {tripId: FAKE_TRIPID}}}/>);
});

afterEach(cleanup);

it('Displays ActivityList when the user is a collaborator', () => {
expect(screen.getByText(RESULT_AUTHORIZED)).toBeInTheDocument();
});

it('Displays the relevant error when the user is not a collaborator', () => {
expect(screen.getByText(RESULT_NOT_AUTHORIZED)).toBeInTheDocument();
});

it('Displays the relevant error when the trip could not be found', () => {
expect(screen.getByText(RESULT_TRIP_DOESNT_EXIST)).toBeInTheDocument();
})
});
16 changes: 16 additions & 0 deletions frontend/src/constants/database.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* This file specifies the database collection and field names.
*/
export const COLLECTION_TRIPS = 'trips';
export const TRIPS_NAME = 'name';
export const TRIPS_DESCRIPTION = 'description';
export const TRIPS_DESTINATION = 'destination';
export const TRIPS_COLLABORATORS = 'collaborators';
export const TRIPS_START_DATE = 'start_date';
export const TRIPS_END_DATE = 'end_date';

export const COLLECTION_ACTIVITIES = 'activities';
export const ACTIVITIES_START_TIME = 'start_time';
export const ACTIVITIES_END_TIME = 'end_time';
export const ACTIVITIES_TITLE = 'title';
export const ACTIVITIES_DESCRIPTION = 'description';