mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2025-12-19 10:07:46 -05:00
fix: redirect to api /signin from client /settings (#64398)
This commit is contained in:
committed by
GitHub
parent
1146bc23f8
commit
ac3a31e920
@@ -1,60 +1,55 @@
|
|||||||
/* eslint-disable */
|
|
||||||
// @ts-nocheck Likely need to not use ShallowRenderer
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ShallowRenderer from 'react-test-renderer/shallow';
|
import { render } from '@testing-library/react';
|
||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi, beforeAll } from 'vitest';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
import envData from '../../config/env.json';
|
import envData from '../../config/env.json';
|
||||||
import { ShowSettings } from './show-settings';
|
import ShowSettings from './show-settings';
|
||||||
|
|
||||||
vi.mock('../analytics');
|
import { createStore } from '../redux/create-store';
|
||||||
vi.mock('@growthbook/growthbook-react', () => ({
|
import { initialState } from '../redux';
|
||||||
useFeatureIsOn: () => false
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { apiLocation } = envData as Record<string, string>;
|
vi.mock('../utils/get-words');
|
||||||
|
|
||||||
|
const { apiLocation } = envData;
|
||||||
|
|
||||||
describe('<ShowSettings />', () => {
|
describe('<ShowSettings />', () => {
|
||||||
it('renders to the DOM when user is logged in', () => {
|
beforeAll(() => {
|
||||||
const shallow = new ShallowRenderer();
|
// Location is not writable normally, so we have to delete and recreate
|
||||||
shallow.render(<ShowSettings {...loggedInProps} />);
|
// https://github.com/jestjs/jest/issues/890#issuecomment-682286025
|
||||||
expect(navigate).toHaveBeenCalledTimes(0);
|
const location = window.location as string & Location;
|
||||||
const result = shallow.getRenderOutput();
|
// @ts-expect-error TS is warning us that this breaks the type of
|
||||||
expect(result.type.toString()).toBe('Symbol(react.fragment)');
|
// window.location, since it is not optional, but we are replacing it with
|
||||||
// Renders Helmet component rather than Loader
|
// an object of the same type, it is safe to ignore.
|
||||||
expect(result.props.children[0].props.title).toEqual(
|
delete global.window.location;
|
||||||
'buttons.settings | freeCodeCamp.org'
|
global.window.location = Object.assign({}, location);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not navigate if already signed in', () => {
|
||||||
|
const store = createStore({
|
||||||
|
app: { ...initialState, user: { sessionUser: 'anything truthy' } }
|
||||||
|
});
|
||||||
|
const spy = vi.spyOn(window.location, 'href', 'set');
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<ShowSettings />
|
||||||
|
</Provider>
|
||||||
);
|
);
|
||||||
|
expect(spy).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('redirects to sign in page when user is not logged in', () => {
|
it('redirects to sign in page when user is not logged in', () => {
|
||||||
const shallow = new ShallowRenderer();
|
const store = createStore({
|
||||||
shallow.render(<ShowSettings {...loggedOutProps} />);
|
app: { ...initialState, user: { sessionUser: null } }
|
||||||
expect(navigate).toHaveBeenCalledTimes(1);
|
});
|
||||||
expect(navigate).toHaveBeenCalledWith(`${apiLocation}/signin`);
|
const spy = vi.spyOn(window.location, 'href', 'set');
|
||||||
const result = shallow.getRenderOutput();
|
|
||||||
// Renders Loader rather than ShowSettings
|
render(
|
||||||
expect(result.type.displayName).toBe('Loader');
|
<Provider store={store}>
|
||||||
|
<ShowSettings />
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(spy).toHaveBeenCalledWith(`${apiLocation}/signin`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const navigate = vi.fn();
|
|
||||||
const loggedInProps = {
|
|
||||||
createFlashMessage: vi.fn(),
|
|
||||||
hardGoTo: vi.fn(),
|
|
||||||
isSignedIn: true,
|
|
||||||
navigate: navigate,
|
|
||||||
showLoading: false,
|
|
||||||
submitNewAbout: vi.fn(),
|
|
||||||
toggleTheme: vi.fn(),
|
|
||||||
updateSocials: vi.fn(),
|
|
||||||
updateIsHonest: vi.fn(),
|
|
||||||
updatePortfolio: vi.fn(),
|
|
||||||
updateQuincyEmail: vi.fn(),
|
|
||||||
user: {
|
|
||||||
about: '',
|
|
||||||
completedChallenges: []
|
|
||||||
},
|
|
||||||
verifyCert: vi.fn()
|
|
||||||
};
|
|
||||||
const loggedOutProps = { ...loggedInProps };
|
|
||||||
loggedOutProps.isSignedIn = false;
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useRef, useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import Helmet from 'react-helmet';
|
import Helmet from 'react-helmet';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
@@ -23,7 +23,6 @@ import { hardGoTo as navigate } from '../redux/actions';
|
|||||||
import {
|
import {
|
||||||
signInLoadingSelector,
|
signInLoadingSelector,
|
||||||
userSelector,
|
userSelector,
|
||||||
isSignedInSelector,
|
|
||||||
userTokenSelector
|
userTokenSelector
|
||||||
} from '../redux/selectors';
|
} from '../redux/selectors';
|
||||||
import type { User } from '../redux/prop-types';
|
import type { User } from '../redux/prop-types';
|
||||||
@@ -44,7 +43,6 @@ const { apiLocation } = envData;
|
|||||||
// TODO: update types for actions
|
// TODO: update types for actions
|
||||||
type ShowSettingsProps = {
|
type ShowSettingsProps = {
|
||||||
createFlashMessage: typeof createFlashMessage;
|
createFlashMessage: typeof createFlashMessage;
|
||||||
isSignedIn: boolean;
|
|
||||||
navigate: (location: string) => void;
|
navigate: (location: string) => void;
|
||||||
showLoading: boolean;
|
showLoading: boolean;
|
||||||
toggleSoundMode: (sound: boolean) => void;
|
toggleSoundMode: (sound: boolean) => void;
|
||||||
@@ -61,17 +59,10 @@ type ShowSettingsProps = {
|
|||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
signInLoadingSelector,
|
signInLoadingSelector,
|
||||||
userSelector,
|
userSelector,
|
||||||
isSignedInSelector,
|
|
||||||
userTokenSelector,
|
userTokenSelector,
|
||||||
(
|
(showLoading: boolean, user: User | null, userToken: string | null) => ({
|
||||||
showLoading: boolean,
|
|
||||||
user: User | null,
|
|
||||||
isSignedIn,
|
|
||||||
userToken: string | null
|
|
||||||
) => ({
|
|
||||||
showLoading,
|
showLoading,
|
||||||
user,
|
user,
|
||||||
isSignedIn,
|
|
||||||
userToken
|
userToken
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -94,7 +85,6 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const {
|
const {
|
||||||
createFlashMessage,
|
createFlashMessage,
|
||||||
isSignedIn,
|
|
||||||
toggleSoundMode,
|
toggleSoundMode,
|
||||||
toggleKeyboardShortcuts,
|
toggleKeyboardShortcuts,
|
||||||
resetEditorLayout,
|
resetEditorLayout,
|
||||||
@@ -107,8 +97,6 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
|
|||||||
userToken
|
userToken
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const isSignedInRef = useRef(isSignedIn);
|
|
||||||
|
|
||||||
const handleHashChange = () => {
|
const handleHashChange = () => {
|
||||||
const id = window.location.hash.replace('#', '');
|
const id = window.location.hash.replace('#', '');
|
||||||
if (id) {
|
if (id) {
|
||||||
@@ -127,12 +115,11 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
|
|||||||
return () => window.removeEventListener('hashchange', handleHashChange);
|
return () => window.removeEventListener('hashchange', handleHashChange);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (showLoading || !user) {
|
useEffect(() => {
|
||||||
return <Loader fullScreen={true} />;
|
if (!user) navigate(`${apiLocation}/signin`);
|
||||||
}
|
}, [user, navigate]);
|
||||||
|
|
||||||
if (!isSignedInRef.current) {
|
if (showLoading || !user) {
|
||||||
navigate(`${apiLocation}/signin`);
|
|
||||||
return <Loader fullScreen={true} />;
|
return <Loader fullScreen={true} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ export const createStore = (preloadedState = {}) => {
|
|||||||
preloadedState
|
preloadedState
|
||||||
});
|
});
|
||||||
sagaMiddleware.run(rootSaga);
|
sagaMiddleware.run(rootSaga);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
||||||
epicMiddleware.run(rootEpic);
|
epicMiddleware.run(rootEpic);
|
||||||
|
|
||||||
if (module.hot) {
|
if (module.hot) {
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import { tap, ignoreElements } from 'rxjs/operators';
|
|||||||
|
|
||||||
import { actionTypes } from './action-types';
|
import { actionTypes } from './action-types';
|
||||||
|
|
||||||
export default function hardGoToEpic(action$, _, { location }) {
|
// The third argument contains dependencies, see createEpicMiddleware
|
||||||
|
export default function hardGoToEpic(action$, _, { window }) {
|
||||||
return action$.pipe(
|
return action$.pipe(
|
||||||
ofType(actionTypes.hardGoTo),
|
ofType(actionTypes.hardGoTo),
|
||||||
tap(({ payload }) => {
|
tap(({ payload }) => {
|
||||||
location.href = payload;
|
window.location.href = payload;
|
||||||
}),
|
}),
|
||||||
ignoreElements()
|
ignoreElements()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ export const defaultDonationFormState = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState = {
|
// exported for testing purposes.
|
||||||
|
export const initialState = {
|
||||||
isRandomCompletionThreshold: false,
|
isRandomCompletionThreshold: false,
|
||||||
donatableSectionRecentlyCompleted: null,
|
donatableSectionRecentlyCompleted: null,
|
||||||
currentChallengeId: store.get(CURRENT_CHALLENGE_KEY),
|
currentChallengeId: store.get(CURRENT_CHALLENGE_KEY),
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { combineEpics } from 'redux-observable';
|
|||||||
import { epics as challengeEpics } from '../templates/Challenges/redux';
|
import { epics as challengeEpics } from '../templates/Challenges/redux';
|
||||||
import { epics as appEpics } from '.';
|
import { epics as appEpics } from '.';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
||||||
const rootEpic = combineEpics(...appEpics, ...challengeEpics);
|
const rootEpic = combineEpics(...appEpics, ...challengeEpics);
|
||||||
|
|
||||||
export default rootEpic;
|
export default rootEpic;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { navigate } from 'gatsby';
|
import { withPrefix } from 'gatsby';
|
||||||
|
import { navigate } from '@gatsbyjs/reach-router';
|
||||||
import { call, put, take, takeEvery } from 'redux-saga/effects';
|
import { call, put, take, takeEvery } from 'redux-saga/effects';
|
||||||
|
|
||||||
import { createFlashMessage } from '../../components/Flash/redux';
|
import { createFlashMessage } from '../../components/Flash/redux';
|
||||||
@@ -17,9 +18,13 @@ function* deleteAccountSaga() {
|
|||||||
message: FlashMessages.AccountDeleted
|
message: FlashMessages.AccountDeleted
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
// navigate before signing out, since /settings will attempt to sign users
|
||||||
|
// back in. Using reach-router's navigate because gatsby's resolves after
|
||||||
|
// the call. This would allow resetUserData to take place while the user is
|
||||||
|
// still on /settings.
|
||||||
|
yield call(navigate, withPrefix('/learn'));
|
||||||
// remove current user information from application state
|
// remove current user information from application state
|
||||||
yield put(resetUserData());
|
yield put(resetUserData());
|
||||||
yield call(navigate, '/learn');
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
yield put(deleteAccountError(e));
|
yield put(deleteAccountError(e));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user