chore: remove verify-can-claim-cert logic (#44574)

* chore: remove verify-can-claim-cert logic

* remove extraneous

* remove console log before Nich wakes up

* add api route back with flash

* remove unnecessary logic in completion-epic

* change tests for new layout

* dynamically use api location

* rename file

* fix Cypress api location

* fix(test): anchor does not have disabled class

* fix(tests): change js test to claim from /settings

* chore: change status to 410 (gone)

* update testing again

* oliver is nitpicky

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>

* make oliver happy

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
Shaun Hamilton
2022-01-18 16:52:49 +02:00
committed by GitHub
parent 9672c92a19
commit 9cb87d0257
12 changed files with 72 additions and 491 deletions

View File

@ -12,15 +12,13 @@ import {
certTypeTitleMap,
certTypeIdMap,
certIds,
oldDataVizId,
superBlockCertTypeMap
oldDataVizId
} from '../../../../config/certification-settings';
import { reportError } from '../middlewares/sentry-error-handler.js';
import { getChallenges } from '../utils/get-curriculum';
import { ifNoUser401 } from '../utils/middleware';
import { observeQuery } from '../utils/rx';
import { ensureLowerCaseString } from '../../common/models/user';
const {
legacyFrontEndChallengeId,
@ -50,7 +48,6 @@ export default function bootCertificate(app) {
const certTypeIds = createCertTypeIds(getChallenges());
const showCert = createShowCert(app);
const verifyCert = createVerifyCert(certTypeIds, app);
const verifyCanClaimCert = createVerifyCanClaim(certTypeIds, app);
api.put('/certificate/verify', ifNoUser401, ifNoSuperBlock404, verifyCert);
api.get('/certificate/showCert/:username/:certSlug', showCert);
@ -59,6 +56,15 @@ export default function bootCertificate(app) {
app.use(api);
}
function verifyCanClaimCert(_req, res) {
return res.status(410).json({
message: {
type: 'info',
message: 'Please reload the app, this feature is no longer available.'
}
});
}
export function getFallbackFullStackDate(completedChallenges, completedDate) {
var chalIds = [
certTypeIdMap[certTypes.respWebDesign],
@ -508,97 +514,3 @@ function createShowCert(app) {
}, next);
};
}
function createVerifyCanClaim(certTypeIds, app) {
const { User } = app.models;
function findUserByUsername$(username, fields) {
return observeQuery(User, 'findOne', {
where: { username },
fields
});
}
return function verifyCert(req, res, next) {
const { superBlock, username } = req.query;
log(superBlock);
let certType = superBlockCertTypeMap[superBlock];
log(certType);
return findUserByUsername$(ensureLowerCaseString(username), {
isFrontEndCert: true,
isBackEndCert: true,
isFullStackCert: true,
isRespWebDesignCert: true,
isFrontEndLibsCert: true,
isJsAlgoDataStructCert: true,
isDataVisCert: true,
is2018DataVisCert: true,
isApisMicroservicesCert: true,
isInfosecQaCert: true,
isQaCertV7: true,
isInfosecCertV7: true,
isSciCompPyCertV7: true,
isDataAnalysisPyCertV7: true,
isMachineLearningPyCertV7: true,
username: true,
name: true,
isHonest: true,
completedChallenges: true
}).subscribe(user => {
if (!user) {
return res.status(404).json({
message: {
type: 'info',
message: 'flash.username-not-found',
variables: { username }
}
});
}
if (!certTypeIds[certType]) {
return res.status(404).json({
message: {
type: 'info',
// TODO: create a specific 'flash.cert-not-found' message
message: 'flash.could-not-find'
}
});
}
return Observable.of(certTypeIds[certType])
.flatMap(challenge => {
const certName = certTypeTitleMap[certType];
const { tests = [] } = challenge;
let result = 'incomplete-requirements';
let status = false;
const { isHonest, completedChallenges } = user;
const isProjectsCompleted = canClaim(tests, completedChallenges);
if (isHonest && isProjectsCompleted) {
status = true;
result = 'requirements-met';
} else if (isProjectsCompleted) {
result = 'projects-completed';
} else if (isHonest) {
result = 'is-honest';
}
return Observable.just({
type: 'success',
message: { status, result },
variables: { name: certName }
});
})
.subscribe(message => {
return res.status(200).json({
response: message,
isCertMap: getUserIsCertMap(user),
// send back the completed challenges
// NOTE: we could just send back the latest challenge, but this
// ensures the challenges are synced.
completedChallenges: user.completedChallenges
});
}, next);
});
};
}

View File

@ -63,7 +63,8 @@
"submit-and-go": "Submit and go to next challenge",
"go-to-next": "Go to next challenge",
"ask-later": "Ask me later",
"start-coding": "Start coding!"
"start-coding": "Start coding!",
"go-to-settings": "Go to settings to claim your certification"
},
"landing": {
"big-heading-1": "Learn to code — for free.",

View File

@ -189,19 +189,7 @@ export const completedChallengesSelector = state =>
export const completionCountSelector = state => state[MainApp].completionCount;
export const currentChallengeIdSelector = state =>
state[MainApp].currentChallengeId;
export const stepsToClaimSelector = state => {
const user = userSelector(state);
const currentCerts = certificatesByNameSelector(user.username)(
state
).currentCerts;
return {
currentCerts: currentCerts,
isHonest: user?.isHonest,
isShowName: user?.profileUI?.showName,
isShowCerts: user?.profileUI?.showCerts,
isShowProfile: !user?.profileUI?.isLocked
};
};
export const emailSelector = state => userSelector(state).email;
export const isAVariantSelector = state => {
const email = emailSelector(state);
@ -271,6 +259,9 @@ export const userByNameSelector = username => state => {
return user[username] ?? initialState.user;
};
export const currentCertsSelector = state =>
certificatesByNameSelector(state[MainApp]?.appUsername)(state)?.currentCerts;
export const certificatesByNameSelector = username => state => {
const {
isRespWebDesignCert,

View File

@ -17,11 +17,9 @@ import {
isSignedInSelector,
submitComplete,
updateComplete,
updateFailed,
usernameSelector
updateFailed
} from '../../../redux';
import { getVerifyCanClaimCert } from '../../../utils/ajax';
import postUpdate$ from '../utils/postUpdate$';
import { actionTypes } from './action-types';
import {
@ -151,8 +149,7 @@ export default function completionEpic(action$, state$) {
switchMap(({ type }) => {
const state = state$.value;
const meta = challengeMetaSelector(state);
const { nextChallengePath, challengeType, superBlock, certification } =
meta;
const { nextChallengePath, challengeType, superBlock } = meta;
const closeChallengeModal = of(closeModal('completion'));
let submitter = () => of({ type: 'no-user-signed-in' });
@ -170,13 +167,7 @@ export default function completionEpic(action$, state$) {
}
const pathToNavigateTo = async () => {
return await findPathToNavigateTo(
certification,
nextChallengePath,
superBlock,
state,
challengeType
);
return await findPathToNavigateTo(nextChallengePath, superBlock);
};
return submitter(type, state).pipe(
@ -188,38 +179,10 @@ export default function completionEpic(action$, state$) {
);
}
async function findPathToNavigateTo(
certification,
nextChallengePath,
superBlock,
state,
challengeType
) {
let canClaimCert = false;
const isProjectSubmission = [
challengeTypes.frontEndProject,
challengeTypes.backEndProject,
challengeTypes.pythonProject
].includes(challengeType);
if (isProjectSubmission) {
const username = usernameSelector(state);
try {
const response = await getVerifyCanClaimCert(username, certification);
if (response.status === 200) {
canClaimCert = response.data?.response?.message === 'can-claim-cert';
}
} catch (err) {
console.error('failed to verify if user can claim certificate', err);
}
}
let pathToNavigateTo;
if (nextChallengePath.includes(superBlock) && !canClaimCert) {
pathToNavigateTo = nextChallengePath;
} else if (canClaimCert) {
pathToNavigateTo = `/learn/${superBlock}/#claim-cert-block`;
async function findPathToNavigateTo(nextChallengePath, superBlock) {
if (nextChallengePath.includes(superBlock)) {
return nextChallengePath;
} else {
pathToNavigateTo = `/learn/${superBlock}/#${superBlock}-projects`;
return `/learn/${superBlock}/#${superBlock}-projects`;
}
return pathToNavigateTo;
}

View File

@ -1,6 +1,6 @@
import { Button } from '@freecodecamp/react-bootstrap';
import { navigate } from 'gatsby-link';
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, MouseEvent } from 'react';
import { TFunction, withTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
@ -13,14 +13,12 @@ import { createFlashMessage } from '../../../components/Flash/redux';
import { FlashMessages } from '../../../components/Flash/redux/flash-messages';
import {
userFetchStateSelector,
stepsToClaimSelector,
isSignedInSelector
isSignedInSelector,
currentCertsSelector
} from '../../../redux';
import { User, Steps } from '../../../redux/prop-types';
import { verifyCert } from '../../../redux/settings';
import { certMap } from '../../../resources/cert-and-project-map';
import { getVerifyCanClaimCert } from '../../../utils/ajax';
import CertificationCard from './certification-card';
interface CertChallengeProps {
// TODO: create enum/reuse SuperBlocks enum somehow
@ -33,7 +31,7 @@ interface CertChallengeProps {
error: null | string;
};
isSignedIn: boolean;
steps: Steps;
currentCerts: Steps['currentCerts'];
superBlock: SuperBlocks;
t: TFunction;
title: typeof certMap[number]['title'];
@ -48,11 +46,16 @@ const honestyInfoMessage = {
const mapStateToProps = (state: unknown) => {
return createSelector(
stepsToClaimSelector,
currentCertsSelector,
userFetchStateSelector,
isSignedInSelector,
(steps, fetchState: CertChallengeProps['fetchState'], isSignedIn) => ({
steps,
(
currentCerts,
fetchState: CertChallengeProps['fetchState'],
isSignedIn
) => ({
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
currentCerts,
fetchState,
isSignedIn
})
@ -65,9 +68,8 @@ const mapDispatchToProps = {
};
const CertChallenge = ({
certification,
createFlashMessage,
steps = {},
currentCerts,
superBlock,
t,
verifyCert,
@ -76,45 +78,8 @@ const CertChallenge = ({
isSignedIn,
user: { isHonest, username }
}: CertChallengeProps): JSX.Element => {
const [canClaimCert, setCanClaimCert] = useState(false);
const [certVerificationMessage, setCertVerificationMessage] = useState('');
const [isCertified, setIsCertified] = useState(false);
const [userLoaded, setUserLoaded] = useState(false);
const [verificationComplete, setVerificationComplete] = useState(false);
const [stepState, setStepState] = useState({
numberOfSteps: 0,
completedCount: 0
});
const [hasCompletedRequiredSteps, setHasCompletedRequiredSteps] =
useState(false);
const [isProjectsCompleted, setIsProjectsCompleted] = useState(false);
useEffect(() => {
if (username) {
void (async () => {
try {
const data = await getVerifyCanClaimCert(username, certification);
if (data?.message) {
setCanClaimCert(false);
createFlashMessage(data.message);
} else {
const { status, result } = data?.response?.message;
setCanClaimCert(status);
setCertVerificationMessage(result);
}
} catch (e) {
console.error(e);
createFlashMessage({
type: 'danger',
message: FlashMessages.ReallyWeird
});
} finally {
setVerificationComplete(true);
}
})();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [username]);
// @ts-expect-error Typescript is confused
const certSlug = certMap.find(x => x.title === title).certSlug;
@ -133,40 +98,19 @@ const CertChallenge = ({
useEffect(() => {
setIsCertified(
steps?.currentCerts?.find(
currentCerts?.find(
(cert: { certSlug: string }) =>
certSlugTypeMapTyped[cert.certSlug] ===
superBlockCertTypeMapTyped[superBlock]
)?.show ?? false
);
const projectsCompleted =
canClaimCert || certVerificationMessage === 'projects-completed';
const projectsCompletedNumber = projectsCompleted ? 1 : 0;
const completedCount =
Object.values(steps).filter(
stepVal => typeof stepVal === 'boolean' && stepVal
).length + projectsCompletedNumber;
const numberOfSteps = Object.keys(steps).length;
setHasCompletedRequiredSteps(completedCount === numberOfSteps);
setStepState({ numberOfSteps, completedCount });
setIsProjectsCompleted(projectsCompleted);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [steps, canClaimCert, certVerificationMessage]);
}, [currentCerts]);
const certLocation = `/certification/${username}/${certSlug}`;
const i18nSuperBlock = t(`intro:${superBlock}.title`);
const i18nCertText = t(`intro:misc-text.certification`, {
cert: i18nSuperBlock
});
const showCertificationCard =
userLoaded &&
isSignedIn &&
(!isCertified || (!hasCompletedRequiredSteps && verificationComplete));
const createClickHandler =
(certSlug: string | undefined) => (e: { preventDefault: () => void }) => {
(certSlug: string | undefined) => (e: MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
if (isCertified) {
return navigate(certLocation);
@ -177,33 +121,19 @@ const CertChallenge = ({
};
return (
<div className='block'>
{showCertificationCard && (
<CertificationCard
i18nCertText={i18nCertText}
isProjectsCompleted={isProjectsCompleted}
steps={steps}
stepState={stepState}
superBlock={superBlock}
/>
)}
<>
{isSignedIn && (
<Button
block={true}
bsStyle='primary'
className='cert-btn'
disabled={
!canClaimCert || (isCertified && !hasCompletedRequiredSteps)
}
href={certLocation}
onClick={createClickHandler(certSlug)}
href={isCertified ? certLocation : `/settings#certification-settings`}
onClick={() => (isCertified ? createClickHandler(certSlug) : false)}
>
{isCertified && userLoaded
? t('buttons.show-cert')
: t('buttons.claim-cert')}
: t('buttons.go-to-settings')}
</Button>
)}
</>
</div>
);
};

View File

@ -1,92 +0,0 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import ScrollableAnchor from 'react-scrollable-anchor';
import { SuperBlocks } from '../../../../../config/certification-settings';
import Caret from '../../../assets/icons/caret';
import GreenNotCompleted from '../../../assets/icons/green-not-completed';
import GreenPass from '../../../assets/icons/green-pass';
import { Steps } from '../../../redux/prop-types';
import ClaimCertSteps from './claim-cert-steps';
interface CertificationCardProps {
i18nCertText: string;
isProjectsCompleted: boolean;
stepState: {
numberOfSteps: number;
completedCount: number;
};
steps: Steps;
superBlock: SuperBlocks;
}
const mapIconStyle = { height: '15px', marginRight: '10px', width: '15px' };
const CertificationCard = ({
isProjectsCompleted,
superBlock,
i18nCertText,
stepState: { completedCount, numberOfSteps },
steps
}: CertificationCardProps): JSX.Element => {
const { t } = useTranslation();
const [isExpanded, setIsExpanded] = useState(true);
const handleBlockClick = () => {
setIsExpanded(!isExpanded);
};
const {
expand: expandText,
collapse: collapseText
}: {
expand: string;
collapse: string;
} = t('intro:misc-text');
return (
<ScrollableAnchor id='claim-cert-block'>
<div className={`block ${isExpanded ? 'open' : ''}`}>
<div className='block-title-wrapper'>
<a className='block-link' href='#claim-cert-block'>
<h3 className='big-block-title'>
{t('certification-card.title')}
<span className='block-link-icon'>#</span>
</h3>
</a>
</div>
<div className='block-description'>
{t('certification-card.intro', { i18nCertText })}
</div>
<button
aria-expanded={isExpanded}
className='map-title'
onClick={handleBlockClick}
>
<Caret />
<h4 className='course-title'>
{`${isExpanded ? collapseText : expandText}`}
</h4>
<div className='map-title-completed course-title'>
{completedCount === numberOfSteps ? (
<GreenPass style={mapIconStyle} />
) : (
<GreenNotCompleted style={mapIconStyle} />
)}
<span className='map-completed-count'>{`${completedCount}/${numberOfSteps}`}</span>
</div>
</button>
{isExpanded && (
<ClaimCertSteps
i18nCertText={i18nCertText}
isProjectsCompleted={isProjectsCompleted}
steps={steps}
superBlock={superBlock}
/>
)}
</div>
</ScrollableAnchor>
);
};
CertificationCard.displayName = 'CertStatus';
export default CertificationCard;

View File

@ -1,88 +0,0 @@
import { Link } from 'gatsby';
import React from 'react';
import { withTranslation, useTranslation } from 'react-i18next';
import { SuperBlocks } from '../../../../../config/certification-settings';
import GreenNotCompleted from '../../../assets/icons/green-not-completed';
import GreenPass from '../../../assets/icons/green-pass';
import { Steps } from '../../../redux/prop-types';
const mapIconStyle = { height: '15px', marginRight: '10px', width: '15px' };
interface ClaimCertStepsProps {
i18nCertText: string;
isProjectsCompleted: boolean;
steps: Steps;
superBlock: SuperBlocks;
}
const ClaimCertSteps = ({
isProjectsCompleted,
i18nCertText,
steps,
superBlock
}: ClaimCertStepsProps): JSX.Element => {
const { t } = useTranslation();
const renderCheckMark = (isCompleted: boolean) => {
return isCompleted ? (
<GreenPass style={mapIconStyle} />
) : (
<GreenNotCompleted style={mapIconStyle} />
);
};
const settingsLink = '/settings#privacy-settings';
const honestyPolicyAnchor = '/settings#honesty-policy';
const {
isHonest = false,
isShowName = false,
isShowCerts = false,
isShowProfile = false
} = steps;
return (
<ul className='map-challenges-ul' data-cy='claim-cert-steps'>
<li className='map-challenge-title map-challenge-wrap'>
<Link to={honestyPolicyAnchor}>
<span className='badge map-badge'>{renderCheckMark(isHonest)}</span>
{t('certification-card.accept-honesty')}
</Link>
</li>
<li className='map-challenge-title map-challenge-wrap'>
<a href={`#${superBlock}-projects`}>
<span className='badge map-badge'>
{renderCheckMark(isProjectsCompleted)}
</span>
{t('certification-card.complete-project', {
i18nCertText
})}
</a>
</li>
<li className='map-challenge-title map-challenge-wrap'>
<Link to={settingsLink}>
<span className='badge map-badge'>
{renderCheckMark(isShowProfile)}
</span>
{t('certification-card.set-profile-public')}
</Link>
</li>
<li className='map-challenge-title map-challenge-wrap'>
<Link to={settingsLink}>
<span className='badge map-badge'>
{renderCheckMark(isShowCerts)}
</span>
{t('certification-card.set-certs-public')}
</Link>
</li>
<li className='map-challenge-title map-challenge-wrap'>
<Link to={settingsLink}>
<span className='badge map-badge'>{renderCheckMark(isShowName)}</span>
{t('certification-card.set-name')}
</Link>
</li>
</ul>
);
};
ClaimCertSteps.displayName = 'ClaimCertSteps';
export default withTranslation()(ClaimCertSteps);

View File

@ -1,10 +1,8 @@
import cookies from 'browser-cookies';
import envData from '../../../config/env.json';
import { FlashMessageArg } from '../components/Flash/redux';
import type {
ChallengeFile,
ClaimedCertifications,
CompletedChallenge,
User
} from '../redux/prop-types';
@ -25,6 +23,8 @@ function getCSRFToken() {
return token ?? '';
}
// TODO: Might want to handle flash messages as close to the request as possible
// to make use of the Response object (message, status, etc)
async function get<T>(path: string): Promise<T> {
return fetch(`${base}${path}`, defaultOptions).then<T>(res => res.json());
}
@ -160,31 +160,6 @@ export function getUsernameExists(username: string): Promise<boolean> {
return get(`/api/users/exists?username=${username}`);
}
export interface GetVerifyCanClaimCert {
response: {
type: string;
message: {
status: boolean;
result: string;
};
variables: {
name: string;
};
};
isCertMap: ClaimedCertifications;
completedChallenges: CompletedChallenge[];
message?: FlashMessageArg;
}
export function getVerifyCanClaimCert(
username: string,
certification: string
): Promise<GetVerifyCanClaimCert> {
return get(
`/certificate/verify-can-claim-cert?username=${username}&superBlock=${certification}`
);
}
/** POST **/
interface Donation {

View File

@ -78,7 +78,8 @@ describe('project submission', () => {
// We need to wait for everything to finish loading and hydrating, so we
// use this text as a proxy for that.
const textInNextPage = projectTitles.slice(1);
textInNextPage.push('Claim Your Certification');
// The following text exists on the donation modal
textInNextPage.push('Nicely done');
projectsInOrder.forEach(
({ block, superBlock, dashedName, solutions }, i) => {
@ -118,8 +119,10 @@ describe('project submission', () => {
// Claim and view solutions on certification page
cy.toggleAll();
cy.visit('/learn/javascript-algorithms-and-data-structures');
cy.contains('Claim Certification').click();
cy.visit('/settings');
cy.get(
`a[href="/certification/developmentuser/${projectsInOrder[0]?.superBlock}"]`
).click();
cy.contains('Show Certification').click();
projectTitles.forEach(title => {

View File

@ -34,19 +34,9 @@ describe('Responsive Web Design Superblock', () => {
cy.visit('/learn/responsive-web-design');
});
describe('Before submitting projects', () => {
it('should have a card with href "claim-cert-block"', () => {
cy.get('a[href="#claim-cert-block"]').scrollIntoView();
cy.get('a[href="#claim-cert-block"]').should('be.visible');
});
it('should have an anchor element with the text "Claim Certification", and class "disabled"', () => {
cy.get('a.disabled').should('be.visible');
cy.get('a.disabled').should('have.text', 'Claim Certification');
});
it('should have an unordered list with class "map-challenges-ul" containing 5 items', () => {
cy.get('[data-cy=claim-cert-steps]').should('be.visible');
cy.get('[data-cy=claim-cert-steps]').children().should('have.length', 5);
it('should navigate to "/settings#certification-settings" when clicking the "Go to settings to claim your certification" anchor', () => {
cy.contains('Go to settings to claim your certification').click();
cy.url().should('include', '/settings#certification-settings');
});
});
describe('After submitting all 5 projects', () => {
@ -64,7 +54,7 @@ describe('Responsive Web Design Superblock', () => {
cy.contains("I've completed this challenge")
.should('not.be.disabled')
.click();
cy.intercept('http://localhost:3000/project-completed').as(
cy.intercept(`${Cypress.env('API_LOCATION')}/project-completed`).as(
'challengeCompleted'
);
cy.contains('Submit and go to next challenge').click();
@ -74,26 +64,21 @@ describe('Responsive Web Design Superblock', () => {
cy.location().should(loc => {
expect(loc.pathname).to.not.eq(url);
});
cy.visit('/settings');
cy.get(
`[href="/certification/developmentuser/${projects.superBlock}"]`
).click();
cy.visit(`/learn/${projects.superBlock}/`);
});
});
it('should be possible to claim and view certifications from the superBlock page', () => {
it('should be possible to view certifications from the superBlock page', () => {
cy.location().should(loc => {
expect(loc.pathname).to.eq(`/learn/${projects.superBlock}/`);
});
cy.get('.donation-modal').should('be.visible');
cy.contains('Ask me later').click();
cy.get('.donation-modal').should('not.exist');
// directed to claim-cert-block section
cy.url().should('include', '#claim-cert-block');
// make sure that the window has not snapped to the top (a weird bug that
// we never figured out and so could randomly reappear)
cy.window().its('scrollY').should('not.equal', 0);
cy.contains('Claim Your Certification');
cy.contains('Claim Certification').should('not.be.disabled').click();
cy.contains('Show Certification').click();
cy.location().should(loc => {
expect(loc.pathname).to.eq(
'/certification/developmentuser/responsive-web-design'
`/certification/developmentuser/${projects.superBlock}`
);
});
});

View File

@ -28,5 +28,6 @@ module.exports = (on, config) => {
// Allows us to test the new curriculum before it's released:
config.env.SHOW_UPCOMING_CHANGES = process.env.SHOW_UPCOMING_CHANGES;
config.env.API_LOCATION = process.env.API_LOCATION;
return config;
};

View File

@ -33,7 +33,7 @@
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => {});
Cypress.Commands.add('login', () => {
cy.visit('http://localhost:3000/signin');
cy.visit(`${Cypress.env('API_LOCATION')}/signin`);
cy.contains('Welcome back');
});