{
- ({ dirty, isValid }) => (
-
-
- Instaclone
-
- ({
- color: theme.colors.gray[6],
- fontSize: 17,
- width: '80%',
- })}
- mb="sm"
- >
- Sign up to see photos from people you don't really care about.
-
+ ({ dirty, isValid }) => (
+
+
+ Instaclone
+
+ ({
+ color: theme.colors.gray[6],
+ fontSize: 17,
+ width: '80%',
+ })}
+ mb="sm"
+ >
+ Sign up to see photos from people you don't really care about.
+
-
-
-
-
-
+
+
+
+
+
- {
- errorMessage && {errorMessage}
- }
+ {
+ errorMessage && {errorMessage}
+ }
-
- Have an account?
- {' '}
-
- Log in
-
-
-
- )
- }
+
+ Have an account?
+ {' '}
+
+ Log in
+
+
+
+ )
+ }
);
diff --git a/frontend/src/features/users/UserProfile/UserProfile.tsx b/frontend/src/features/users/UserProfile/UserProfile.tsx
index b2fbd5f..0260202 100644
--- a/frontend/src/features/users/UserProfile/UserProfile.tsx
+++ b/frontend/src/features/users/UserProfile/UserProfile.tsx
@@ -2,7 +2,7 @@ import { useParams } from 'react-router-dom';
import { useMediaQuery } from '@mantine/hooks';
import { useEffect } from 'react';
import GoBackNavbar from '../../../common/components/Navbars/GoBackNavbar/GoBackNavbar';
-import UserProfileInfo from './UserProfileInfo';
+import UserProfileInfo from './UserProfileInfo/UserProfileInfo';
import UserProfileInfoBar from './UserProfileInfoBar/UserProfileInfoBar';
import UserProfileImageGrid from './UserProfileImageGrid/UserProfileImageGrid';
import DesktopNavbar from '../../../common/components/Navbars/DesktopNavbar/DesktopNavbar';
@@ -37,19 +37,18 @@ function UserProfile() {
return (
<>
{
- user ? (
-
- ) : (
-
- )
- }
+ user ? (
+
+ ) : (
+
+ )
+ }
{
diff --git a/frontend/src/features/users/UserProfile/UserProfileEdit/UserProfileEdit.tsx b/frontend/src/features/users/UserProfile/UserProfileEdit/UserProfileEdit.tsx
index 41c9e3f..22bb51a 100644
--- a/frontend/src/features/users/UserProfile/UserProfileEdit/UserProfileEdit.tsx
+++ b/frontend/src/features/users/UserProfile/UserProfileEdit/UserProfileEdit.tsx
@@ -16,9 +16,9 @@ import {
useField,
} from 'formik';
import * as Yup from 'yup';
+import { useNavigate } from 'react-router-dom';
import FormikTextInput from '../../../../common/components/FormikTextInput';
-import { useAppSelector } from '../../../../common/hooks/selector-dispatch-hooks';
-import useAuth from '../../../../common/hooks/useAuth';
+import { useAppSelector, useAppDispatch } from '../../../../common/hooks/selector-dispatch-hooks';
import {
selectUserByUsername,
useEditUserMutation,
@@ -30,6 +30,7 @@ import placeholderIcon from '../../../../assets/placeholder-icon.jpeg';
import ChangeAvatarModal from './ChangeAvatarModal/ChangeAvatarModal';
import GoBackNavbar from '../../../../common/components/Navbars/GoBackNavbar/GoBackNavbar';
import Alert from '../../../../common/components/Alert/Alert';
+import { updateCurrentUsername } from '../../../auth/authSlice';
interface UserProfileEditProps {
user: string | null
@@ -78,7 +79,8 @@ function UserProfileEdit({ user }: UserProfileEditProps) {
modalOpened,
setModalOpened,
}] = useUserProfileImageUpload(setAlertText);
- const [, { updateTokenUsername }] = useAuth();
+ const dispatch = useAppDispatch();
+ const navigate = useNavigate();
if (userObject) {
const { id: userId } = userObject;
@@ -112,9 +114,18 @@ function UserProfileEdit({ user }: UserProfileEditProps) {
) => {
try {
await editUser({ updatedUserFields: values, id: userId }).unwrap();
- setAlertText('Profile saved.');
actions.resetForm({ values });
- if (values.username) updateTokenUsername(values.username);
+
+ const wasUsernameChanged = values.username
+ && values.username
+ !== userObject.username;
+
+ if (wasUsernameChanged) {
+ dispatch(updateCurrentUsername(values.username!));
+ navigate(`/${values.username}`);
+ } else {
+ setAlertText('Profile saved.');
+ }
} catch (error) {
console.log(error);
}
diff --git a/frontend/src/features/users/UserProfile/UserProfileInfo.styles.ts b/frontend/src/features/users/UserProfile/UserProfileInfo/UserProfileInfo.styles.ts
similarity index 78%
rename from frontend/src/features/users/UserProfile/UserProfileInfo.styles.ts
rename to frontend/src/features/users/UserProfile/UserProfileInfo/UserProfileInfo.styles.ts
index 54fffcc..3c55e4f 100644
--- a/frontend/src/features/users/UserProfile/UserProfileInfo.styles.ts
+++ b/frontend/src/features/users/UserProfile/UserProfileInfo/UserProfileInfo.styles.ts
@@ -11,20 +11,6 @@ export default createStyles((theme) => ({
marginBottom: 25,
},
},
- buttonRoot: {
- height: 30,
- width: '100%',
- },
- buttonOutline: {
- borderColor: theme.colors.gray[4],
- color: theme.colors.instaDark[6],
- },
- editButtonRoot: {
- [`@media (min-width: ${theme.breakpoints.md}px)`]: {
- width: 'inherit',
- },
- },
-
// main section -> container holding avatar and name/buttons
mainSection: {
display: 'flex',
@@ -56,12 +42,6 @@ export default createStyles((theme) => ({
flexDirection: 'column',
gap: 10,
},
- mainSectionButtonGroup: {
- display: 'flex',
- flexDirection: 'row',
- gap: 10,
- maxWidth: 250,
- },
mainSectionNameBtns: {
display: 'flex',
flexDirection: 'column',
@@ -72,12 +52,6 @@ export default createStyles((theme) => ({
gap: 10,
},
},
- followButtonRoot: {
- flexShrink: 2,
- '@media (max-width: 335px)': {
- padding: '0 10px',
- },
- },
placeholderIcon: {
width: '100%',
},
@@ -108,4 +82,9 @@ export default createStyles((theme) => ({
top: 0,
left: 0,
},
+ activeOpacityLight: {
+ '&:active': {
+ opacity: 0.5,
+ },
+ },
}));
diff --git a/frontend/src/features/users/UserProfile/UserProfileInfo.tsx b/frontend/src/features/users/UserProfile/UserProfileInfo/UserProfileInfo.tsx
similarity index 56%
rename from frontend/src/features/users/UserProfile/UserProfileInfo.tsx
rename to frontend/src/features/users/UserProfile/UserProfileInfo/UserProfileInfo.tsx
index 58b957f..894f210 100644
--- a/frontend/src/features/users/UserProfile/UserProfileInfo.tsx
+++ b/frontend/src/features/users/UserProfile/UserProfileInfo/UserProfileInfo.tsx
@@ -3,29 +3,53 @@ import {
Avatar,
Text,
Container,
- Button,
UnstyledButton,
Loader,
+ Group,
+ Popover,
} from '@mantine/core';
import { useMediaQuery } from '@mantine/hooks';
-import { Link } from 'react-router-dom';
+import { IconDots } from '@tabler/icons-react';
import useStyles from './UserProfileInfo.styles';
-import { User } from '../../../app/types';
-import UserProfileInfoBar from './UserProfileInfoBar/UserProfileInfoBar';
-import useUserProfileImageUpload from '../../../common/hooks/useUserProfileImageUpload';
-import placeholderIcon from '../../../assets/placeholder-icon.jpeg';
-import ChangeAvatarModal from './UserProfileEdit/ChangeAvatarModal/ChangeAvatarModal';
-import Alert from '../../../common/components/Alert/Alert';
-import { useFollowUserByIdMutation, useUnfollowUserByIdMutation } from '../../../app/apiSlice';
-import getErrorMessage from '../../../common/utils/getErrorMessage';
+import { User } from '../../../../app/types';
+import UserProfileInfoBar from '../UserProfileInfoBar/UserProfileInfoBar';
+import useUserProfileImageUpload from '../../../../common/hooks/useUserProfileImageUpload';
+import placeholderIcon from '../../../../assets/placeholder-icon.jpeg';
+import ChangeAvatarModal from '../UserProfileEdit/ChangeAvatarModal/ChangeAvatarModal';
+import Alert from '../../../../common/components/Alert/Alert';
+import UserProfileInfoButtons from './UserProfileInfoButtons/UserProfileInfoButtons';
+import useAuth from '../../../../common/hooks/useAuth';
interface UserProfileInfoProps {
user: User;
- isCurrentUserLoggedIn: boolean;
isCurrentUserProfile?: boolean;
isCurrentUserFollowing: boolean;
}
+interface UserProfileBioNameProps {
+ fullName: string;
+ bio?: string;
+}
+
+function UserProfileBioName({ fullName, bio }: UserProfileBioNameProps) {
+ return (
+
+ {fullName}
+ {
+ bio && (
+
+ {bio}
+
+ )
+ }
+
+ );
+}
+
+UserProfileBioName.defaultProps = {
+ bio: '',
+};
+
/**
* Component that displays the user's profile image, username, and bio
* (top section above the user's posts in the user profile page)
@@ -33,10 +57,9 @@ interface UserProfileInfoProps {
function UserProfileInfo({
user,
isCurrentUserProfile,
- isCurrentUserLoggedIn,
isCurrentUserFollowing,
}: UserProfileInfoProps) {
- const { classes, cx } = useStyles();
+ const { classes } = useStyles();
const isMediumScreenOrWider = useMediaQuery('(min-width: 992px)');
const [alertText, setAlertText] = useState('');
const [
@@ -46,74 +69,8 @@ function UserProfileInfo({
isDeleting, isImageUpdating, modalOpened, setModalOpened,
},
] = useUserProfileImageUpload(setAlertText);
-
- const [followUser, { isLoading: isFollowLoading }] = useFollowUserByIdMutation();
- const [unfollowUser, { isLoading: isUnfollowLoading }] = useUnfollowUserByIdMutation();
-
- const onFollowBtnClick = async () => {
- const followFunc = isCurrentUserFollowing ? unfollowUser : followUser;
-
- try {
- await followFunc(user.id).unwrap();
- } catch (error) {
- console.error(getErrorMessage(error));
- }
- };
-
- const buttons = () => {
- if (isCurrentUserProfile) {
- return (
-
- );
- }
-
- if (isCurrentUserLoggedIn) {
- return (
-
-
-
-
- );
- }
-
- return null;
- };
-
- const bio = () => (
-
- {user.fullName}
-
- {user.bio}
-
-
- );
+ const [isLogoutPopoverOpen, setIsLogoutPopoverOpen] = useState(false);
+ const [, { logout }] = useAuth();
return (
<>
@@ -181,10 +138,45 @@ function UserProfileInfo({
-
- {user.username}
-
- {buttons()}
+
+
+ {user.username}
+
+
+ {
+ (isCurrentUserProfile && !isMediumScreenOrWider) && (
+ setIsLogoutPopoverOpen(false)}
+ target={(
+ setIsLogoutPopoverOpen(!isLogoutPopoverOpen)}
+ data-testid="user-profile-info-dots"
+ />
+ )}
+ position="left"
+ spacing="xs"
+ >
+ {
+ await logout();
+ }
+ }
+ >
+ Log out
+
+
+ )
+ }
+
+
+
{isMediumScreenOrWider && (
<>
@@ -193,13 +185,21 @@ function UserProfileInfo({
followerCount={user.followers?.length ?? 0}
followingCount={user.following?.length ?? 0}
/>
- {bio()}
+
>
)}
- {!isMediumScreenOrWider && bio()}
+ {!isMediumScreenOrWider && (
+
+ )}
>
);
diff --git a/frontend/src/features/users/UserProfile/UserProfileInfo/UserProfileInfoButtons/UserProfileInfoButtons.styles.tsx b/frontend/src/features/users/UserProfile/UserProfileInfo/UserProfileInfoButtons/UserProfileInfoButtons.styles.tsx
new file mode 100644
index 0000000..aa50d79
--- /dev/null
+++ b/frontend/src/features/users/UserProfile/UserProfileInfo/UserProfileInfoButtons/UserProfileInfoButtons.styles.tsx
@@ -0,0 +1,29 @@
+import { createStyles } from '@mantine/core';
+
+export default createStyles((theme) => ({
+ buttonRoot: {
+ height: 30,
+ width: '100%',
+ },
+ buttonOutline: {
+ borderColor: theme.colors.gray[4],
+ color: theme.colors.instaDark[6],
+ },
+ editButtonRoot: {
+ [`@media (min-width: ${theme.breakpoints.md}px)`]: {
+ width: 'inherit',
+ },
+ },
+ followButtonRoot: {
+ flexShrink: 2,
+ '@media (max-width: 335px)': {
+ padding: '0 10px',
+ },
+ },
+ mainSectionButtonGroup: {
+ display: 'flex',
+ flexDirection: 'row',
+ gap: 10,
+ maxWidth: 250,
+ },
+}));
diff --git a/frontend/src/features/users/UserProfile/UserProfileInfo/UserProfileInfoButtons/UserProfileInfoButtons.tsx b/frontend/src/features/users/UserProfile/UserProfileInfo/UserProfileInfoButtons/UserProfileInfoButtons.tsx
new file mode 100644
index 0000000..4ae74f3
--- /dev/null
+++ b/frontend/src/features/users/UserProfile/UserProfileInfo/UserProfileInfoButtons/UserProfileInfoButtons.tsx
@@ -0,0 +1,87 @@
+import { Button } from '@mantine/core';
+import { Link } from 'react-router-dom';
+import useAuth from '../../../../../common/hooks/useAuth';
+import {
+ useFollowUserByIdMutation,
+ useUnfollowUserByIdMutation,
+} from '../../../../../app/apiSlice';
+import getErrorMessage from '../../../../../common/utils/getErrorMessage';
+import useStyles from './UserProfileInfoButtons.styles';
+
+interface UserProfileInfoButtonsProps {
+ userId: string;
+ isCurrentUserProfile?: boolean;
+ isCurrentUserFollowing: boolean;
+}
+
+function UserProfileInfoButtons({
+ userId,
+ isCurrentUserProfile,
+ isCurrentUserFollowing,
+}: UserProfileInfoButtonsProps) {
+ const [user] = useAuth();
+ const { classes, cx } = useStyles();
+ const isCurrentUserLoggedIn = user != null;
+ const [followUser, { isLoading: isFollowLoading }] = useFollowUserByIdMutation();
+ const [unfollowUser, { isLoading: isUnfollowLoading }] = useUnfollowUserByIdMutation();
+
+ const onFollowBtnClick = async () => {
+ const followFunc = isCurrentUserFollowing ? unfollowUser : followUser;
+
+ try {
+ await followFunc(userId).unwrap();
+ } catch (error) {
+ console.error(getErrorMessage(error));
+ }
+ };
+
+ if (isCurrentUserProfile) {
+ return (
+
+ );
+ }
+
+ if (isCurrentUserLoggedIn) {
+ return (
+
+
+
+
+ );
+ }
+
+ return null;
+}
+
+UserProfileInfoButtons.defaultProps = {
+ isCurrentUserProfile: false,
+};
+
+export default UserProfileInfoButtons;
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx
index 5fec5c5..fe9f01d 100644
--- a/frontend/src/index.tsx
+++ b/frontend/src/index.tsx
@@ -8,12 +8,6 @@ import App from './app/App';
import theme from './app/theme';
import reportWebVitals from './reportWebVitals';
import { store } from './app/store';
-import { initAuthedUser } from './features/auth/authSlice';
-import { apiSlice } from './app/apiSlice';
-/* TODO: create a /validendpoint token to verify that the token is valid?
-if valid, setAuthedUser */
-store.dispatch(initAuthedUser());
-store.dispatch(apiSlice.endpoints.getUsers.initiate());
ReactDOM.render(
diff --git a/frontend/src/test/int/appAuthRefreshFlow.test.tsx b/frontend/src/test/int/appAuthRefreshFlow.test.tsx
new file mode 100644
index 0000000..e933232
--- /dev/null
+++ b/frontend/src/test/int/appAuthRefreshFlow.test.tsx
@@ -0,0 +1,46 @@
+import { rest } from 'msw';
+import { renderWithRouter, screen, mockLogout } from '../utils/test-utils';
+import server from '../mocks/server';
+import App from '../../app/App';
+
+beforeAll(() => server.listen());
+afterEach(() => {
+ server.resetHandlers();
+ mockLogout({ resetApiState: true });
+});
+afterAll(() => server.close());
+
+it('if App refresh request is successful, user is redirected to homepage', async () => {
+ renderWithRouter();
+
+ const homepage = await screen.findByTestId('homepage-container');
+ expect(homepage).toBeVisible();
+});
+
+it('displays a loading spinner while App refresh request is pending and disappears after', async () => {
+ renderWithRouter();
+
+ const loader = await screen.findByTestId('app-loader');
+
+ expect(loader).toBeVisible();
+
+ await screen.findByTestId('homepage-container');
+
+ expect(loader).not.toBeInTheDocument();
+});
+
+it('if App refresh request fails, user is redirected to login page', async () => {
+ server.use(
+ rest.post('/api/auth/refresh', (_req, res, ctx) => res(ctx.status(401), ctx.json({
+ error: 'refresh token missing or invalid.',
+ }))),
+ );
+
+ renderWithRouter();
+
+ const loginText = await screen.findByText(/don't have an account?/i);
+ const loginButton = await screen.findByRole('button', { name: /log in/i });
+
+ expect(loginText).toBeVisible();
+ expect(loginButton).toBeVisible();
+});
diff --git a/frontend/src/test/int/editpostdetails.test.tsx b/frontend/src/test/int/editPostDetailsPostCreation.test.tsx
similarity index 91%
rename from frontend/src/test/int/editpostdetails.test.tsx
rename to frontend/src/test/int/editPostDetailsPostCreation.test.tsx
index b92dd39..ff497c5 100644
--- a/frontend/src/test/int/editpostdetails.test.tsx
+++ b/frontend/src/test/int/editPostDetailsPostCreation.test.tsx
@@ -10,8 +10,8 @@ import {
Providers,
waitFor,
waitForElementToBeRemoved,
+ testStore,
} from '../utils/test-utils';
-import { store } from '../../app/store';
import { fakeUser } from '../mocks/handlers';
import server from '../mocks/server';
import { apiSlice } from '../../app/apiSlice';
@@ -22,7 +22,7 @@ beforeAll(() => server.listen());
beforeEach(() => {
const fakeTokenInfo = {
username: fakeUser.username,
- token: 'supersecrettoken',
+ accessToken: 'supersecrettoken',
};
mockLogin({ fakeTokenInfo });
});
@@ -33,7 +33,7 @@ afterEach(() => {
afterAll(() => server.close());
test('creating a post navigates to homepage and displays an alert', async () => {
- await store.dispatch(apiSlice.endpoints.getUsers.initiate());
+ await testStore.dispatch(apiSlice.endpoints.getUsers.initiate());
const history = createBrowserHistory();
history.push('/create/details', { croppedImage: testImage });
@@ -59,7 +59,7 @@ test('creating a post navigates to homepage and displays an alert', async () =>
});
test('share button not visible while post is being created', async () => {
- await store.dispatch(apiSlice.endpoints.getUsers.initiate());
+ await testStore.dispatch(apiSlice.endpoints.getUsers.initiate());
const history = createBrowserHistory();
history.push('/create/details', { croppedImage: testImage });
@@ -92,7 +92,7 @@ test('unsuccessful post creation navigates to homepage and displays an alert', a
rest.post('/api/posts', (req, res, ctx) => res(ctx.status(500))),
);
- await store.dispatch(apiSlice.endpoints.getUsers.initiate());
+ await testStore.dispatch(apiSlice.endpoints.getUsers.initiate());
const history = createBrowserHistory();
history.push('/create/details', { croppedImage: testImage });
diff --git a/frontend/src/test/int/gobacknavbar.test.tsx b/frontend/src/test/int/goBackNavbarNavigation.tsx
similarity index 85%
rename from frontend/src/test/int/gobacknavbar.test.tsx
rename to frontend/src/test/int/goBackNavbarNavigation.tsx
index f9a5e1b..2a85107 100644
--- a/frontend/src/test/int/gobacknavbar.test.tsx
+++ b/frontend/src/test/int/goBackNavbarNavigation.tsx
@@ -1,20 +1,20 @@
import {
- screen, renderWithRouter, mockLogin, mockLogout,
+ screen,
+ renderWithRouter,
+ mockLogin,
+ mockLogout,
} from '../utils/test-utils';
import { fakeUser } from '../mocks/handlers';
-import { store } from '../../app/store';
import server from '../mocks/server';
-import { apiSlice } from '../../app/apiSlice';
import App from '../../app/App';
beforeAll(() => server.listen());
beforeEach(() => {
const fakeTokenInfo = {
username: fakeUser.username,
- token: 'supersecrettoken',
+ accessToken: 'supersecrettoken',
};
mockLogin({ fakeTokenInfo });
- store.dispatch(apiSlice.endpoints.getUsers.initiate());
});
afterEach(() => {
mockLogout({ resetApiState: true });
diff --git a/frontend/src/test/int/httpRefreshInterceptor.test.tsx b/frontend/src/test/int/httpRefreshInterceptor.test.tsx
new file mode 100644
index 0000000..0e6c234
--- /dev/null
+++ b/frontend/src/test/int/httpRefreshInterceptor.test.tsx
@@ -0,0 +1,79 @@
+import { rest } from 'msw';
+import PostComponent from '../../features/posts/PostComponent/PostComponent';
+import {
+ renderWithRouter,
+ screen,
+ waitFor,
+ mockLogout,
+} from '../utils/test-utils';
+import server from '../mocks/server';
+import '@testing-library/jest-dom/extend-expect';
+import { fakeUser } from '../mocks/handlers';
+import testImageDataUrl from '../../../cypress/fixtures/test-image-data-url';
+
+beforeAll(() => server.listen());
+afterEach(() => {
+ server.resetHandlers();
+ mockLogout({ resetApiState: true });
+});
+afterAll(() => server.close());
+
+/*
+ Just some background on this test (because I'll surely forget):
+
+ The apiSlice intercepts HTTP requests and checks if a request
+ returns 401 (unauthorized), which usually would mean that the access token is expired.
+
+ If it does, the apiSlice makes a call to
+ the refresh endpoint to get a new access token and then retries the original request.
+
+ This test is just to make that sure that works as expected.
+
+ From the user's perspective, the user should not see any difference in the UI.
+
+ This test will use the PostComponent as the vehicle to test this through the like button.
+
+ The user should be able to like a post like normal.
+
+ So as far as assertions go, we just need to make sure that the like count updates like normal.
+*/
+
+test('user with "expired" access token can still like a post', async () => {
+ // setting up the server to return a 401 on the first like attempt
+ server.use(
+ rest.post('/api/likes', (req, res, ctx) => {
+ console.log('POST /api/likes - failure');
+ return res.once(ctx.status(401), ctx.json({ error: 'token expired.' }));
+ }),
+ rest.post('/api/likes', (req, res, ctx) => {
+ console.log('POST /api/likes - success');
+ return res(ctx.status(201));
+ }),
+ );
+
+ const post = {
+ id: '1',
+ creator: { ...fakeUser },
+ caption: 'This is a post',
+ image: { url: testImageDataUrl, publicId: 'fakepublicid' },
+ comments: [],
+ likes: [],
+ createdAt: '2021-08-01T00:00:00.000Z',
+ updatedAt: '2021-08-01T00:00:00.000Z',
+ };
+ const setAlertText = jest.fn();
+
+ const { user } = renderWithRouter();
+
+ expect(screen.queryByText(/1 like/i)).not.toBeInTheDocument();
+
+ const likeButton = screen.getByTestId('like-post-btn');
+
+ await user.click(likeButton);
+
+ // if the like count was updated
+ // then we know the second like was made and was successful
+ await waitFor(async () => {
+ expect(screen.getByText(/1 like/i)).toBeVisible();
+ });
+});
diff --git a/frontend/src/test/int/editpostimage.test.tsx b/frontend/src/test/int/imageUpload.test.tsx
similarity index 91%
rename from frontend/src/test/int/editpostimage.test.tsx
rename to frontend/src/test/int/imageUpload.test.tsx
index 8a888aa..9530bc3 100644
--- a/frontend/src/test/int/editpostimage.test.tsx
+++ b/frontend/src/test/int/imageUpload.test.tsx
@@ -7,9 +7,9 @@ import {
waitFor,
fireEvent,
dataURItoBlob,
+ testStore,
} from '../utils/test-utils';
import { fakeUser } from '../mocks/handlers';
-import { store } from '../../app/store';
import server from '../mocks/server';
import { apiSlice } from '../../app/apiSlice';
import App from '../../app/App';
@@ -19,10 +19,10 @@ beforeAll(() => server.listen());
beforeEach(() => {
const fakeTokenInfo = {
username: fakeUser.username,
- token: 'supersecrettoken',
+ accessToken: 'supersecrettoken',
};
mockLogin({ fakeTokenInfo });
- store.dispatch(apiSlice.endpoints.getUsers.initiate());
+ testStore.dispatch(apiSlice.endpoints.getUsers.initiate());
});
afterEach(() => {
mockLogout({ resetApiState: true });
diff --git a/frontend/src/test/int/login.test.tsx b/frontend/src/test/int/login.test.tsx
deleted file mode 100644
index 9c23486..0000000
--- a/frontend/src/test/int/login.test.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { rest } from 'msw';
-import {
- renderWithRouter,
- screen,
- waitFor,
- mockLogout,
-} from '../utils/test-utils';
-import App from '../../app/App';
-import '@testing-library/jest-dom/extend-expect';
-import { fakeUser } from '../mocks/handlers';
-import server from '../mocks/server';
-
-beforeAll(() => server.listen());
-afterEach(() => {
- mockLogout({ resetApiState: true });
- server.resetHandlers();
-});
-afterAll(() => server.close());
-
-const loginFields = {
- username: fakeUser.username,
- password: 'secret',
-};
-
-test('user can login successfully', async () => {
- const { user } = renderWithRouter();
-
- await user.type(screen.getByPlaceholderText(/username/i), loginFields.username);
- await user.type(screen.getByPlaceholderText(/password/i), loginFields.password);
- await user.click(screen.getByRole('button'));
-
- await waitFor(() => {
- const token = localStorage.getItem('instacloneSCToken');
- expect(token).not.toBeNull();
- const parsedToken = JSON.parse(token!);
- expect(parsedToken.username).toBe(loginFields.username);
- });
-
- expect(screen.getByTestId('homepage-container')).toBeVisible();
-});
-
-test('error is displayed on unsuccessful login', async () => {
- server.use(
- rest.post('/api/login', (req, res, ctx) => res(ctx.status(401), ctx.json({ error: 'Invalid username or password' }))),
- );
-
- const { user } = renderWithRouter();
-
- await user.type(screen.getByPlaceholderText(/username/i), loginFields.username);
- await user.type(screen.getByPlaceholderText(/password/i), loginFields.password);
- await user.click(screen.getByRole('button'));
-
- await waitFor(() => {
- const token = localStorage.getItem('instacloneSCToken');
- expect(token).toBeNull();
- expect(screen.getByText(/invalid username or password/i)).toBeVisible();
- });
-});
diff --git a/frontend/src/test/int/userprofile.test.tsx b/frontend/src/test/int/navigatingToUserProfiles.tsx
similarity index 55%
rename from frontend/src/test/int/userprofile.test.tsx
rename to frontend/src/test/int/navigatingToUserProfiles.tsx
index 157f8fe..0593602 100644
--- a/frontend/src/test/int/userprofile.test.tsx
+++ b/frontend/src/test/int/navigatingToUserProfiles.tsx
@@ -5,17 +5,17 @@ import {
mockLogout,
mockLogin,
renderWithRouter,
+ testStore,
} from '../utils/test-utils';
import App from '../../app/App';
import server from '../mocks/server';
import { fakeUser } from '../mocks/handlers';
import '@testing-library/jest-dom/extend-expect';
-import { store } from '../../app/store';
import { apiSlice } from '../../app/apiSlice';
const fakeTokenInfo = {
username: fakeUser.username,
- token: 'supersecrettoken',
+ accessToken: 'supersecrettoken',
};
beforeAll(() => server.listen());
@@ -34,11 +34,11 @@ test('navigating to profile page of non-existing user displays not found page',
});
test('navigating to profile page of existing user displays their profile', async () => {
- store.dispatch(apiSlice.endpoints.getUsers.initiate());
-
+ testStore.dispatch(apiSlice.endpoints.getUsers.initiate());
renderWithRouter(, { route: '/bobbydob' });
+
await waitFor(() => {
- expect(screen.getByText(/bobbydob/i)).toBeVisible();
+ expect(screen.getAllByText(/bobbydob/i)).toHaveLength(2);
});
});
@@ -55,14 +55,18 @@ test('if not logged in and profile has avatar image, clicking on avatar does not
publicId: 'fakePublicId',
},
}]))),
+ rest.post('/api/auth/refresh', (req, res, ctx) => {
+ console.log('POST /api/auth/refresh - failure');
+ ctx.delay(2500);
+
+ return res(ctx.status(401), ctx.json({ error: 'refresh token missing or invalid.' }));
+ }),
);
- store.dispatch(apiSlice.endpoints.getUsers.initiate());
+ testStore.dispatch(apiSlice.endpoints.getUsers.initiate());
const { user } = renderWithRouter(, { route: `/${fakeUser.username}` });
- mockLogout({ resetApiState: false });
-
const profileAvatar = await screen.findByTestId('profile-info-avatar');
await user.click(profileAvatar);
@@ -70,40 +74,3 @@ test('if not logged in and profile has avatar image, clicking on avatar does not
expect(screen.queryByTestId('change-avatar-modal')).not.toBeInTheDocument();
});
});
-
-test('if logged in and profile has avatar image, clicking on avatar displays modal', async () => {
- mockLogin({ fakeTokenInfo });
-
- server.use(
- rest.get('/api/users', (req, res, ctx) => res(ctx.status(200), ctx.json([{
- ...fakeUser,
- image: {
- url: 'https://i.picsum.photos/id/237/200/300.jpg?hmac=TmmQSbShHz9CdQm0NkEjx1Dyh_Y984R9LpNrpvH2D_U',
- publicId: 'fakePublicId',
- },
- }]))),
- );
-
- store.dispatch(apiSlice.endpoints.getUsers.initiate());
- const { user } = renderWithRouter(, { route: '/accounts/edit' });
-
- const avatar = await screen.findByTestId('avatar');
- await user.click(avatar);
-
- await waitFor(async () => {
- expect(screen.queryByTestId('change-avatar-modal')).toBeVisible();
- });
-});
-
-// testing ChangeAvatarModal features inside UseProfile
-test('when clicking on avatar, modal does not appear if user does not have an image', async () => {
- mockLogin({ fakeTokenInfo });
-
- store.dispatch(apiSlice.endpoints.getUsers.initiate());
- const { user } = renderWithRouter(, { route: '/accounts/edit' });
-
- await waitFor(async () => {
- await user.click(screen.getByTestId('avatar'));
- expect(screen.queryByText(/remove current photo/i)).toBeNull();
- });
-});
diff --git a/frontend/src/test/int/requireAuthLoginRedirect.test.tsx b/frontend/src/test/int/requireAuthLoginRedirect.test.tsx
new file mode 100644
index 0000000..3f644d7
--- /dev/null
+++ b/frontend/src/test/int/requireAuthLoginRedirect.test.tsx
@@ -0,0 +1,43 @@
+import { rest } from 'msw';
+import {
+ screen,
+ renderWithRouter,
+ mockLogout,
+ waitFor,
+} from '../utils/test-utils';
+import App from '../../app/App';
+import '@testing-library/jest-dom/extend-expect';
+import { fakeUser } from '../mocks/handlers';
+import server from '../mocks/server';
+
+beforeAll(() => server.listen());
+afterEach(() => {
+ mockLogout({ resetApiState: true });
+ server.resetHandlers();
+});
+afterAll(() => server.close());
+
+test('RequireAuth.tsx protects route, Login.tsx redirects user back to protected router after they login', async () => {
+ server.use(
+ rest.post('/api/auth/refresh', (req, res, ctx) => {
+ console.log('POST /api/auth/refresh - failure');
+ ctx.delay(2500);
+
+ return res(ctx.status(401), ctx.json({ error: 'refresh token missing or invalid.' }));
+ }),
+ );
+
+ const { user } = renderWithRouter(, { route: '/accounts/edit' });
+
+ const usernameInput = await screen.findByPlaceholderText(/username/i);
+ const passwordInput = await screen.findByPlaceholderText(/password/i);
+ const loginButton = await screen.findByRole('button');
+
+ await user.type(usernameInput, fakeUser.username);
+ await user.type(passwordInput, 'secret');
+ await user.click(loginButton);
+
+ await waitFor(() => {
+ expect(screen.getByText(/bio/i)).toBeVisible();
+ });
+});
diff --git a/frontend/src/test/int/requireauth.test.tsx b/frontend/src/test/int/requireauth.test.tsx
deleted file mode 100644
index 39581ef..0000000
--- a/frontend/src/test/int/requireauth.test.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import {
- screen,
- renderWithRouter,
- mockLogin,
- mockLogout,
- waitFor,
-} from '../utils/test-utils';
-import App from '../../app/App';
-import '@testing-library/jest-dom/extend-expect';
-import { fakeUser } from '../mocks/handlers';
-import server from '../mocks/server';
-import { apiSlice } from '../../app/apiSlice';
-import { store } from '../../app/store';
-
-beforeAll(() => server.listen());
-afterEach(() => {
- mockLogout({ resetApiState: true });
- server.resetHandlers();
-});
-afterAll(() => server.close());
-
-test('redirects user to login form when not logged in', () => {
- renderWithRouter(
- ,
- );
-
- expect(screen.getByText(/Don't have an account/i)).toBeVisible();
-});
-
-test('logged in user is taken to home page', async () => {
- const fakeTokenInfo = {
- username: 'bobbydob',
- token: 'supersecrettoken',
- };
- mockLogin({ fakeTokenInfo });
-
- renderWithRouter(
- ,
- );
-
- expect(screen.getByTestId('homepage-container')).toBeVisible();
-});
-
-test('when user logs in, it redirects user to route they were attempting to visit', async () => {
- store.dispatch(apiSlice.endpoints.getUsers.initiate());
-
- const { user } = renderWithRouter(, { route: '/accounts/edit' });
- await user.type(screen.getByPlaceholderText(/username/i), fakeUser.username);
- await user.type(screen.getByPlaceholderText(/password/i), 'secret');
- await user.click(screen.getByRole('button'));
-
- await waitFor(() => {
- expect(screen.getByText(/bio/i)).toBeVisible();
- });
-});
diff --git a/frontend/src/test/int/signup.test.tsx b/frontend/src/test/int/signup.test.tsx
deleted file mode 100644
index 616f8e5..0000000
--- a/frontend/src/test/int/signup.test.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import { rest } from 'msw';
-import {
- screen,
- waitFor,
- mockLogin,
- mockLogout,
- renderWithRouter,
-} from '../utils/test-utils';
-import App from '../../app/App';
-import server from '../mocks/server';
-import { fakeUser } from '../mocks/handlers';
-import '@testing-library/jest-dom/extend-expect';
-
-beforeAll(() => server.listen());
-afterEach(() => {
- mockLogout({ resetApiState: true });
- server.resetHandlers();
-});
-afterAll(() => server.close());
-
-const signupFields = {
- email: fakeUser.email,
- username: fakeUser.username,
- fullName: fakeUser.fullName,
- password: 'secret',
-};
-
-jest.setTimeout(10000);
-
-test('user can signup successfully', async () => {
- const { user } = renderWithRouter();
-
- await user.click(screen.getByText('Sign up'));
- await user.type(screen.getByPlaceholderText(/email/i), signupFields.email);
- await user.type(screen.getByPlaceholderText(/username/i), signupFields.username);
- await user.type(screen.getByPlaceholderText(/password/i), signupFields.password);
- await user.type(screen.getByPlaceholderText(/full name/i), signupFields.fullName);
-
- await user.click(screen.getByRole('button'));
-
- // Testing that user is logged in after successful signup
-
- await waitFor(() => {
- const token = localStorage.getItem('instacloneSCToken');
- expect(token).not.toBeNull();
- const parsedToken = JSON.parse(token!);
- expect(parsedToken.username).toBe(signupFields.username);
- });
-
- expect(screen.getByTestId('homepage-container')).toBeVisible();
-});
-
-test('error is displayed on unsuccessful signup', async () => {
- server.use(
- rest.post('/api/users', (req, res, ctx) => res(ctx.status(400), ctx.json({ error: 'That username is already taken!' }))),
- );
-
- const { user } = renderWithRouter();
-
- await user.click(screen.getByText('Sign up'));
- await user.type(screen.getByPlaceholderText(/email/i), signupFields.email);
- await user.type(screen.getByPlaceholderText(/username/i), signupFields.username);
- await user.type(screen.getByPlaceholderText(/password/i), signupFields.password);
- await user.type(screen.getByPlaceholderText(/full name/i), signupFields.fullName);
-
- await user.click(screen.getByRole('button'));
-
- await waitFor(() => {
- const token = localStorage.getItem('instacloneSCToken');
- expect(token).toBeNull();
- expect(screen.getByText(/That username is already taken/i)).toBeVisible();
- });
-});
-
-test('user is redirected to home page if they visit the signup page and are already logged in', async () => {
- const fakeTokenInfo = {
- username: 'bobbydob',
- token: 'supersecrettoken',
- };
- mockLogin({ fakeTokenInfo });
-
- renderWithRouter(, { route: '/signup' });
-
- expect(screen.queryByText(/don't have an account?/i)).toBeNull();
- expect(screen.getByTestId('homepage-container')).toBeVisible();
-});
diff --git a/frontend/src/test/int/userProfileEditChangeAvatarModal.test.tsx b/frontend/src/test/int/userProfileEditChangeAvatarModal.test.tsx
new file mode 100644
index 0000000..b9ffa9e
--- /dev/null
+++ b/frontend/src/test/int/userProfileEditChangeAvatarModal.test.tsx
@@ -0,0 +1,62 @@
+import { waitFor } from '@testing-library/react';
+import { rest } from 'msw';
+import { apiSlice } from '../../app/apiSlice';
+import UserProfileEdit from '../../features/users/UserProfile/UserProfileEdit/UserProfileEdit';
+import { fakeUser } from '../mocks/handlers';
+import server from '../mocks/server';
+import {
+ mockLogin,
+ testStore,
+ renderWithRouter,
+ screen,
+ mockLogout,
+} from '../utils/test-utils';
+
+const fakeTokenInfo = {
+ username: fakeUser.username,
+ accessToken: 'supersecrettoken',
+};
+
+beforeAll(() => server.listen());
+afterEach(() => {
+ mockLogout({ resetApiState: true });
+ server.resetHandlers();
+});
+afterAll(() => server.close());
+
+test('if logged in and profile has avatar image, clicking on avatar displays modal', async () => {
+ mockLogin({ fakeTokenInfo });
+
+ server.use(
+ rest.get('/api/users', (req, res, ctx) => res(ctx.status(200), ctx.json([{
+ ...fakeUser,
+ image: {
+ url: 'https://i.picsum.photos/id/237/200/300.jpg?hmac=TmmQSbShHz9CdQm0NkEjx1Dyh_Y984R9LpNrpvH2D_U',
+ publicId: 'fakePublicId',
+ },
+ }]))),
+ );
+
+ testStore.dispatch(apiSlice.endpoints.getUsers.initiate());
+ const { user } = renderWithRouter();
+
+ const avatar = await screen.findByTestId('avatar');
+ await user.click(avatar);
+
+ await waitFor(async () => {
+ expect(screen.queryByTestId('change-avatar-modal')).toBeVisible();
+ });
+});
+
+// testing ChangeAvatarModal features inside UseProfile
+test('when clicking on avatar, modal does not appear if user does not have an image', async () => {
+ mockLogin({ fakeTokenInfo });
+ testStore.dispatch(apiSlice.endpoints.getUsers.initiate());
+
+ const { user } = renderWithRouter();
+
+ await waitFor(async () => {
+ await user.click(screen.getByTestId('avatar'));
+ expect(screen.queryByText(/remove current photo/i)).toBeNull();
+ });
+});
diff --git a/frontend/src/test/int/userprofileedit.test.tsx b/frontend/src/test/int/userprofileedit.test.tsx
index a4258f3..298c2ea 100644
--- a/frontend/src/test/int/userprofileedit.test.tsx
+++ b/frontend/src/test/int/userprofileedit.test.tsx
@@ -6,19 +6,19 @@ import {
mockLogout,
waitFor,
fireEvent,
+ testStore,
} from '../utils/test-utils';
import App from '../../app/App';
import '@testing-library/jest-dom/extend-expect';
import { fakeUser } from '../mocks/handlers';
import server from '../mocks/server';
import { apiSlice } from '../../app/apiSlice';
-import { store } from '../../app/store';
beforeAll(() => server.listen());
beforeEach(() => {
const fakeTokenInfo = {
username: fakeUser.username,
- token: 'supersecrettoken',
+ accessToken: 'supersecrettoken',
};
mockLogin({ fakeTokenInfo });
});
@@ -43,7 +43,7 @@ test('user with an image can upload a new one', async () => {
const file = new File(['hello'], 'anyfile.png', { type: 'image/png' });
- store.dispatch(apiSlice.endpoints.getUsers.initiate());
+ testStore.dispatch(apiSlice.endpoints.getUsers.initiate());
const { user } = renderWithRouter(, { route: '/accounts/edit' });
const avatarImages = await screen.findAllByRole('img');
@@ -71,7 +71,7 @@ test('user with an image can upload a new one', async () => {
});
test('updating username updates the JWT username field', async () => {
- store.dispatch(apiSlice.endpoints.getUsers.initiate());
+ testStore.dispatch(apiSlice.endpoints.getUsers.initiate());
const { user } = renderWithRouter(, { route: '/accounts/edit' });
const usernameInput = await screen.findByPlaceholderText(/username/i);
@@ -92,7 +92,7 @@ test('updating username updates the JWT username field', async () => {
// testing ChangeAvatarModal features inside UserProfileEdit
test('when clicking on avatar, modal does not appear if user does not have an image', async () => {
- store.dispatch(apiSlice.endpoints.getUsers.initiate());
+ testStore.dispatch(apiSlice.endpoints.getUsers.initiate());
const { user } = renderWithRouter(, { route: '/accounts/edit' });
await waitFor(async () => {
@@ -112,7 +112,7 @@ test('when clicking on avatar, modal does appear if user has an image', async ()
}]))),
);
- store.dispatch(apiSlice.endpoints.getUsers.initiate());
+ testStore.dispatch(apiSlice.endpoints.getUsers.initiate());
const { user } = renderWithRouter(, { route: '/accounts/edit' });
const avatar = await screen.findByTestId('avatar');
diff --git a/frontend/src/test/int/viewingposts.test.tsx b/frontend/src/test/int/viewingPostsAfterPosting.test.tsx
similarity index 94%
rename from frontend/src/test/int/viewingposts.test.tsx
rename to frontend/src/test/int/viewingPostsAfterPosting.test.tsx
index 29208c2..a226a86 100644
--- a/frontend/src/test/int/viewingposts.test.tsx
+++ b/frontend/src/test/int/viewingPostsAfterPosting.test.tsx
@@ -9,8 +9,8 @@ import {
render,
Providers,
waitFor,
+ testStore,
} from '../utils/test-utils';
-import { store } from '../../app/store';
import { fakeUser } from '../mocks/handlers';
import server from '../mocks/server';
import { apiSlice } from '../../app/apiSlice';
@@ -21,7 +21,7 @@ beforeAll(() => server.listen());
beforeEach(() => {
const fakeTokenInfo = {
username: fakeUser.username,
- token: 'supersecrettoken',
+ accessToken: 'supersecrettoken',
};
mockLogin({ fakeTokenInfo });
});
@@ -39,7 +39,7 @@ afterAll(() => server.close());
*/
test('post is viewable in homepage after creating it', async () => {
- await store.dispatch(apiSlice.endpoints.getUsers.initiate());
+ await testStore.dispatch(apiSlice.endpoints.getUsers.initiate());
const history = createBrowserHistory();
history.push('/create/details', { croppedImage: testImage });
@@ -69,7 +69,7 @@ test('post is viewable in homepage after creating it', async () => {
});
test('post is viewable in the user profile after creating it', async () => {
- await store.dispatch(apiSlice.endpoints.getUsers.initiate());
+ await testStore.dispatch(apiSlice.endpoints.getUsers.initiate());
const history = createBrowserHistory();
history.push('/create/details', { croppedImage: testImage });
@@ -109,7 +109,7 @@ test('post is viewable in the user profile after creating it', async () => {
});
test('post is viewable in the post view page', async () => {
- await store.dispatch(apiSlice.endpoints.getUsers.initiate());
+ await testStore.dispatch(apiSlice.endpoints.getUsers.initiate());
const history = createBrowserHistory();
history.push('/create/details', { croppedImage: testImage });
@@ -159,7 +159,7 @@ test('post is viewable in the post view page', async () => {
});
test('when a post has a caption, it is viewable in the post view page', async () => {
- await store.dispatch(apiSlice.endpoints.getUsers.initiate());
+ await testStore.dispatch(apiSlice.endpoints.getUsers.initiate());
const history = createBrowserHistory();
history.push('/create/details', { croppedImage: testImage });
diff --git a/frontend/src/test/mocks/handlers.ts b/frontend/src/test/mocks/handlers.ts
index b8fa7cd..412fdab 100644
--- a/frontend/src/test/mocks/handlers.ts
+++ b/frontend/src/test/mocks/handlers.ts
@@ -21,6 +21,11 @@ export const fakeUser: User = {
following: [],
};
+export const fakeAccessToken = {
+ username: fakeUser.username,
+ accessToken: 'supersecrettoken',
+};
+
const posts: Post[] = [];
const users: User[] = [fakeUser];
@@ -75,12 +80,19 @@ export const handlers = [
return res(ctx.status(404));
}),
- rest.post('/api/login', (req, res, ctx) => {
- console.log('POST /api/login');
- return res(ctx.status(200), ctx.json({
- username: req.body.username,
- token: 'supersecrettoken',
- }));
+ rest.post('/api/auth/login', (req, res, ctx) => {
+ console.log('POST /api/auth/login');
+ return res(ctx.status(200), ctx.json(fakeAccessToken));
+ }),
+ rest.post('/api/auth/logout', (_req, res, ctx) => {
+ console.log('POST /api/auth/logout');
+ return res(ctx.status(204));
+ }),
+ rest.post('/api/auth/refresh', (req, res, ctx) => {
+ console.log('POST /api/auth/refresh');
+ ctx.delay(2500);
+
+ return res(ctx.status(200), ctx.json(fakeAccessToken));
}),
rest.put('/api/users/:id', (req, res, ctx) => {
console.log('PUT /api/users/:id');
diff --git a/frontend/src/test/utils/mockLogin.test.ts b/frontend/src/test/utils/mockLogin.test.ts
index 40f505f..61371cb 100644
--- a/frontend/src/test/utils/mockLogin.test.ts
+++ b/frontend/src/test/utils/mockLogin.test.ts
@@ -1,29 +1,19 @@
-import { mockLogin } from './test-utils';
-import { store } from '../../app/store';
-import { removeCurrentUser } from '../../features/auth/authSlice';
+import { mockLogin, testStore } from './test-utils';
+import { removeAuthenticatedState } from '../../features/auth/authSlice';
afterEach(() => {
- localStorage.removeItem('instacloneSCToken');
- store.dispatch(removeCurrentUser());
+ testStore.dispatch(removeAuthenticatedState());
});
const fakeTokenInfo = {
username: 'bobbydob',
- token: 'supersecrettoken',
+ accessToken: 'supersecrettoken',
};
-test('mockLogin saves a token in localStorage', () => {
+test('mockLogin stores the access token data in the Redux store', () => {
mockLogin({ fakeTokenInfo });
- const storedToken = localStorage.getItem('instacloneSCToken');
-
- expect(storedToken).not.toBeNull();
- expect(JSON.parse(storedToken!)).toEqual(fakeTokenInfo);
-});
-
-test('mockLogin stores the token info in the Redux store', () => {
- mockLogin({ fakeTokenInfo });
- const state = store.getState();
+ const state = testStore.getState();
expect(state.auth.username).toBe(fakeTokenInfo.username);
- expect(state.auth.token).toBe(fakeTokenInfo.token);
+ expect(state.auth.accessToken).toBe(fakeTokenInfo.accessToken);
});
diff --git a/frontend/src/test/utils/test-utils.tsx b/frontend/src/test/utils/test-utils.tsx
index 3b17939..f7becb1 100644
--- a/frontend/src/test/utils/test-utils.tsx
+++ b/frontend/src/test/utils/test-utils.tsx
@@ -1,18 +1,27 @@
+/* eslint-disable max-len */
import React, { ReactElement, ReactNode } from 'react';
import { render, RenderOptions } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MantineProvider } from '@mantine/core';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
+import { configureStore } from '@reduxjs/toolkit';
import theme from '../../app/theme';
-import { store } from '../../app/store';
-import { initAuthedUser, removeCurrentUser } from '../../features/auth/authSlice';
+import authReducer, { removeAuthenticatedState, setAuthenticatedState } from '../../features/auth/authSlice';
import { apiSlice } from '../../app/apiSlice';
+export const testStore = configureStore({
+ reducer: {
+ [apiSlice.reducerPath]: apiSlice.reducer,
+ auth: authReducer,
+ },
+ middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(apiSlice.middleware),
+});
+
export function Providers({ children }: { children: ReactNode }) {
return (
-
+
{children}
@@ -40,7 +49,7 @@ export const renderWithRouter = (
window.history.pushState({}, 'string', route);
const view = customRender(
- { ui }
+ {ui}
,
);
return {
@@ -50,33 +59,34 @@ export const renderWithRouter = (
};
interface MockLogInOptions {
- fakeTokenInfo: { username: string, token: string }
+ fakeTokenInfo: { username: string, accessToken: string }
}
/**
- * Mocks a logged in state in the DOM and Redux.
+ * Mocks a logged in state in Redux.
* @param {{ username: string, token: string }} fakeTokenInfo - The mock token object
* @param {string} fakeTokenInfo.username - The test user's username
* @param {string} fakeTokenInfo.token - The mock token. Can be any string.
*/
export const mockLogin = ({ fakeTokenInfo }: MockLogInOptions) => {
- localStorage.setItem('instacloneSCToken', JSON.stringify(fakeTokenInfo));
- store.dispatch(initAuthedUser());
+ testStore.dispatch(setAuthenticatedState(fakeTokenInfo));
};
/**
- * Mocks a logged out state by resetting the `api`,
- * clearing the `auth` state and removing the application's JWT token from `localStorage`.
+ * Mocks a logged out state by resetting the `api` and
+ * clearing the authenticated state in the Redux store.
+ *
+ * @param {{ resetApiState: boolean }} options - The options object
+ * @param {boolean} options.resetApiState - Boolean variable that will reset the `api` state if set to true.
*/
export const mockLogout = ({ resetApiState }: {
resetApiState: boolean
}) => {
if (resetApiState) {
- store.dispatch(apiSlice.util.resetApiState());
+ testStore.dispatch(apiSlice.util.resetApiState());
}
- store.dispatch(removeCurrentUser());
- localStorage.removeItem('instacloneSCToken');
+ testStore.dispatch(removeAuthenticatedState());
};
/**