1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 09:57:02 +01:00

Open sea security provider warning message (#17662)

* Warning message for the OpenSea security provider

* Updated snapshots

* Removed flask usage

* Covered more test cases

* Code refactor

* Fixed errors

* Code refactor

* Fixed few issues

* Covered more code with tests
This commit is contained in:
Filip Sekulic 2023-02-23 12:38:09 +01:00 committed by GitHub
parent 6892ab2bff
commit 2acd51af2e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 510 additions and 5 deletions

View File

@ -2606,6 +2606,9 @@
"openSea": {
"message": "OpenSea (Beta)"
},
"openSeaNew": {
"message": "OpenSea"
},
"optional": {
"message": "Optional"
},
@ -2953,6 +2956,24 @@
"replace": {
"message": "replace"
},
"requestFlaggedAsMaliciousFallbackCopyReason": {
"message": "The security provider has not shared additional details"
},
"requestFlaggedAsMaliciousFallbackCopyReasonTitle": {
"message": "Request flagged as malicious"
},
"requestMayNotBeSafe": {
"message": "Request may not be safe"
},
"requestMayNotBeSafeError": {
"message": "The security provider didn't detect any known malicious activity, but it still may not be safe to continue."
},
"requestNotVerified": {
"message": "Request not verified"
},
"requestNotVerifiedError": {
"message": "Because of an error, this request was not verified by the security provider. Proceed with caution."
},
"requestsAwaitingAcknowledgement": {
"message": "requests waiting to be acknowledged"
},
@ -4002,6 +4023,9 @@
"thingsToKeep": {
"message": "Things to keep in mind:"
},
"thisIsBasedOn": {
"message": "This is based on information from "
},
"thisServiceIsExperimental": {
"message": "This service is experimental"
},

View File

@ -1,5 +1,8 @@
import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout';
import { MESSAGE_TYPE } from '../../../shared/constants/app';
const fetchWithTimeout = getFetchWithTimeout();
export async function securityProviderCheck(
requestData,
methodName,
@ -47,7 +50,7 @@ export async function securityProviderCheck(
};
}
const response = await fetch(
const response = await fetchWithTimeout(
'https://eos9d7dmfj.execute-api.us-east-1.amazonaws.com/metamask/validate',
{
method: 'POST',

View File

@ -11,6 +11,8 @@ import Typography from '../../../ui/typography';
import { TypographyVariant } from '../../../../helpers/constants/design-system';
import DepositPopover from '../../deposit-popover/deposit-popover';
import SecurityProviderBannerMessage from '../../security-provider-banner-message/security-provider-banner-message';
import { SECURITY_PROVIDER_MESSAGE_SEVERITIES } from '../../security-provider-banner-message/security-provider-banner-message.constants';
import { ConfirmPageContainerSummary, ConfirmPageContainerWarning } from '.';
export default class ConfirmPageContainerContent extends Component {
@ -55,6 +57,7 @@ export default class ConfirmPageContainerContent extends Component {
toAddress: PropTypes.string,
transactionType: PropTypes.string,
isBuyableChain: PropTypes.bool,
txData: PropTypes.object,
};
state = {
@ -166,6 +169,7 @@ export default class ConfirmPageContainerContent extends Component {
toAddress,
transactionType,
isBuyableChain,
txData,
} = this.props;
const { t } = this.context;
@ -187,6 +191,15 @@ export default class ConfirmPageContainerContent extends Component {
{ethGasPriceWarning && (
<ConfirmPageContainerWarning warning={ethGasPriceWarning} />
)}
{(txData?.securityProviderResponse?.flagAsDangerous !== undefined &&
txData?.securityProviderResponse?.flagAsDangerous !==
SECURITY_PROVIDER_MESSAGE_SEVERITIES.NOT_MALICIOUS) ||
(txData?.securityProviderResponse &&
Object.keys(txData.securityProviderResponse).length === 0) ? (
<SecurityProviderBannerMessage
securityProviderResponse={txData.securityProviderResponse}
/>
) : null}
<ConfirmPageContainerSummary
className={classnames({
'confirm-page-container-summary--border':

View File

@ -4,6 +4,7 @@ import configureMockStore from 'redux-mock-store';
import { TransactionType } from '../../../../../shared/constants/transaction';
import { renderWithProvider } from '../../../../../test/lib/render-helpers';
import { TRANSACTION_ERROR_KEY } from '../../../../helpers/constants/error-keys';
import { SECURITY_PROVIDER_MESSAGE_SEVERITIES } from '../../security-provider-banner-message/security-provider-banner-message.constants';
import ConfirmPageContainerContent from './confirm-page-container-content.component';
describe('Confirm Page Container Content', () => {
@ -47,6 +48,13 @@ describe('Confirm Page Container Content', () => {
disabled: true,
origin: 'http://localhost:4200',
hideTitle: false,
txData: {
securityProviderResponse: {
flagAsDangerous: '?',
reason: 'Some reason...',
reason_header: 'Some reason header...',
},
},
};
});
@ -126,4 +134,40 @@ describe('Confirm Page Container Content', () => {
expect(queryByText('Address Book Account 1')).not.toBeInTheDocument();
});
it('should render SecurityProviderBannerMessage component properly', () => {
const { queryByText } = renderWithProvider(
<ConfirmPageContainerContent {...props} />,
store,
);
expect(queryByText('Request not verified')).toBeInTheDocument();
expect(
queryByText(
'Because of an error, this request was not verified by the security provider. Proceed with caution.',
),
).toBeInTheDocument();
expect(
queryByText('This is based on information from'),
).toBeInTheDocument();
});
it('should not render SecurityProviderBannerMessage component when flagAsDangerous is not malicious', () => {
props.txData.securityProviderResponse = {
flagAsDangerous: SECURITY_PROVIDER_MESSAGE_SEVERITIES.NOT_MALICIOUS,
};
const { queryByText } = renderWithProvider(
<ConfirmPageContainerContent {...props} />,
store,
);
expect(queryByText('Request not verified')).toBeNull();
expect(
queryByText(
'Because of an error, this request was not verified by the security provider. Proceed with caution.',
),
).toBeNull();
expect(queryByText('This is based on information from')).toBeNull();
});
});

View File

@ -86,9 +86,7 @@ const ConfirmPageContainer = (props) => {
currentTransaction,
supportsEIP1559,
nativeCurrency,
///: BEGIN:ONLY_INCLUDE_IN(flask)
txData,
///: END:ONLY_INCLUDE_IN(flask)
assetStandard,
isApprovalOrRejection,
} = props;
@ -223,6 +221,7 @@ const ConfirmPageContainer = (props) => {
toAddress={toAddress}
transactionType={currentTransaction.type}
isBuyableChain={isBuyableChain}
txData={txData}
/>
)}
{shouldDisplayWarning && errorKey === INSUFFICIENT_FUNDS_ERROR_KEY && (
@ -346,9 +345,7 @@ ConfirmPageContainer.propTypes = {
dataComponent: PropTypes.node,
dataHexComponent: PropTypes.node,
detailsComponent: PropTypes.node,
///: BEGIN:ONLY_INCLUDE_IN(flask)
txData: PropTypes.object,
///: END:ONLY_INCLUDE_IN(flask)
tokenAddress: PropTypes.string,
nonce: PropTypes.string,
warning: PropTypes.string,

View File

@ -0,0 +1 @@
export { default } from './security-provider-banner-message';

View File

@ -0,0 +1,5 @@
export const SECURITY_PROVIDER_MESSAGE_SEVERITIES = {
NOT_MALICIOUS: 0,
MALICIOUS: 1,
NOT_SAFE: 2,
};

View File

@ -0,0 +1,74 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import {
Color,
SEVERITIES,
Size,
TypographyVariant,
} from '../../../helpers/constants/design-system';
import { I18nContext } from '../../../../.storybook/i18n';
import { BannerAlert, ButtonLink } from '../../component-library';
import Typography from '../../ui/typography/typography';
import { SECURITY_PROVIDER_MESSAGE_SEVERITIES } from './security-provider-banner-message.constants';
export default function SecurityProviderBannerMessage({
securityProviderResponse,
}) {
const t = useContext(I18nContext);
let messageTitle;
let messageText;
let severity;
if (
securityProviderResponse.flagAsDangerous ===
SECURITY_PROVIDER_MESSAGE_SEVERITIES.MALICIOUS
) {
messageTitle =
securityProviderResponse.reason_header === ''
? t('requestFlaggedAsMaliciousFallbackCopyReasonTitle')
: securityProviderResponse.reason_header;
messageText =
securityProviderResponse.reason === ''
? t('requestFlaggedAsMaliciousFallbackCopyReason')
: securityProviderResponse.reason;
severity = SEVERITIES.DANGER;
} else if (
securityProviderResponse.flagAsDangerous ===
SECURITY_PROVIDER_MESSAGE_SEVERITIES.NOT_SAFE
) {
messageTitle = t('requestMayNotBeSafe');
messageText = t('requestMayNotBeSafeError');
severity = SEVERITIES.WARNING;
} else {
messageTitle = t('requestNotVerified');
messageText = t('requestNotVerifiedError');
severity = SEVERITIES.WARNING;
}
return (
<BannerAlert
marginTop={4}
marginRight={4}
marginLeft={4}
title={messageTitle}
severity={severity}
>
<Typography variant={TypographyVariant.H6}>{messageText}</Typography>
<Typography variant={TypographyVariant.H7} color={Color.textAlternative}>
{t('thisIsBasedOn')}
<ButtonLink
size={Size.inherit}
href="https://opensea.io/"
target="_blank"
>
{t('openSeaNew')}
</ButtonLink>
</Typography>
</BannerAlert>
);
}
SecurityProviderBannerMessage.propTypes = {
securityProviderResponse: PropTypes.object,
};

View File

@ -0,0 +1,189 @@
import { fireEvent } from '@testing-library/react';
import React from 'react';
import configureMockStore from 'redux-mock-store';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import SecurityProviderBannerMessage from './security-provider-banner-message';
import { SECURITY_PROVIDER_MESSAGE_SEVERITIES } from './security-provider-banner-message.constants';
describe('Security Provider Banner Message', () => {
const store = configureMockStore()({});
const thisIsBasedOnText = 'This is based on information from';
it('should render SecurityProviderBannerMessage component properly when flagAsDangerous is malicious', () => {
const securityProviderResponse = {
flagAsDangerous: SECURITY_PROVIDER_MESSAGE_SEVERITIES.MALICIOUS,
reason:
'Approval is to an unverified smart contract known for stealing NFTs in the past.',
reason_header: 'This could be a scam',
};
const { getByText } = renderWithProvider(
<SecurityProviderBannerMessage
securityProviderResponse={securityProviderResponse}
/>,
store,
);
expect(getByText(securityProviderResponse.reason)).toBeInTheDocument();
expect(
getByText(securityProviderResponse.reason_header),
).toBeInTheDocument();
expect(getByText(thisIsBasedOnText)).toBeInTheDocument();
});
it('should render SecurityProviderBannerMessage component properly when flagAsDangerous is not safe', () => {
const securityProviderResponse = {
flagAsDangerous: SECURITY_PROVIDER_MESSAGE_SEVERITIES.NOT_SAFE,
reason: 'Some reason...',
reason_header: 'Some reason header...',
};
const requestMayNotBeSafe = 'Request may not be safe';
const requestMayNotBeSafeError =
"The security provider didn't detect any known malicious activity, but it still may not be safe to continue.";
const { getByText } = renderWithProvider(
<SecurityProviderBannerMessage
securityProviderResponse={securityProviderResponse}
/>,
store,
);
expect(getByText(requestMayNotBeSafe)).toBeInTheDocument();
expect(getByText(requestMayNotBeSafeError)).toBeInTheDocument();
expect(getByText(thisIsBasedOnText)).toBeInTheDocument();
});
it('should render SecurityProviderBannerMessage component properly when flagAsDangerous is undefined', () => {
const securityProviderResponse = {
flagAsDangerous: '?',
reason: 'Some reason...',
reason_header: 'Some reason header...',
};
const requestNotVerified = 'Request not verified';
const requestNotVerifiedError =
'Because of an error, this request was not verified by the security provider. Proceed with caution.';
const { getByText } = renderWithProvider(
<SecurityProviderBannerMessage
securityProviderResponse={securityProviderResponse}
/>,
store,
);
expect(getByText(requestNotVerified)).toBeInTheDocument();
expect(getByText(requestNotVerifiedError)).toBeInTheDocument();
expect(getByText(thisIsBasedOnText)).toBeInTheDocument();
});
it('should render SecurityProviderBannerMessage component properly when securityProviderResponse is empty', () => {
const securityProviderResponse = {};
const requestNotVerified = 'Request not verified';
const requestNotVerifiedError =
'Because of an error, this request was not verified by the security provider. Proceed with caution.';
const { getByText } = renderWithProvider(
<SecurityProviderBannerMessage
securityProviderResponse={securityProviderResponse}
/>,
store,
);
expect(getByText(requestNotVerified)).toBeInTheDocument();
expect(getByText(requestNotVerifiedError)).toBeInTheDocument();
expect(getByText(thisIsBasedOnText)).toBeInTheDocument();
});
it('should navigate to the OpenSea web page when clicked on the OpenSea button', () => {
const securityProviderResponse = {
flagAsDangerous: SECURITY_PROVIDER_MESSAGE_SEVERITIES.NOT_SAFE,
reason: 'Some reason...',
reason_header: 'Some reason header...',
};
const { getByText } = renderWithProvider(
<SecurityProviderBannerMessage
securityProviderResponse={securityProviderResponse}
/>,
store,
);
const link = getByText('OpenSea');
expect(link).toBeInTheDocument();
fireEvent.click(link);
expect(link.closest('a')).toHaveAttribute('href', 'https://opensea.io/');
});
it('should render SecurityProviderBannerMessage component properly, with predefined reason message, when a request is malicious and there is no reason given', () => {
const securityProviderResponse = {
flagAsDangerous: SECURITY_PROVIDER_MESSAGE_SEVERITIES.MALICIOUS,
reason: '',
reason_header: 'Some reason header...',
};
const reason = 'The security provider has not shared additional details';
const { getByText } = renderWithProvider(
<SecurityProviderBannerMessage
securityProviderResponse={securityProviderResponse}
/>,
store,
);
expect(
getByText(securityProviderResponse.reason_header),
).toBeInTheDocument();
expect(getByText(reason)).toBeInTheDocument();
expect(getByText(thisIsBasedOnText)).toBeInTheDocument();
});
it('should render SecurityProviderBannerMessage component properly, with predefined reason_header message, when a request is malicious and there is no reason header given', () => {
const securityProviderResponse = {
flagAsDangerous: SECURITY_PROVIDER_MESSAGE_SEVERITIES.MALICIOUS,
reason: 'Some reason...',
reason_header: '',
};
const reasonHeader = 'Request flagged as malicious';
const { getByText } = renderWithProvider(
<SecurityProviderBannerMessage
securityProviderResponse={securityProviderResponse}
/>,
store,
);
expect(getByText(reasonHeader)).toBeInTheDocument();
expect(getByText(securityProviderResponse.reason)).toBeInTheDocument();
expect(getByText(thisIsBasedOnText)).toBeInTheDocument();
});
it('should render SecurityProviderBannerMessage component properly, with predefined reason and reason_header messages, when a request is malicious and there are no reason and reason header given', () => {
const securityProviderResponse = {
flagAsDangerous: SECURITY_PROVIDER_MESSAGE_SEVERITIES.MALICIOUS,
reason: '',
reason_header: '',
};
const reasonHeader = 'Request flagged as malicious';
const reason = 'The security provider has not shared additional details';
const { getByText } = renderWithProvider(
<SecurityProviderBannerMessage
securityProviderResponse={securityProviderResponse}
/>,
store,
);
expect(getByText(reasonHeader)).toBeInTheDocument();
expect(getByText(reason)).toBeInTheDocument();
expect(getByText(thisIsBasedOnText)).toBeInTheDocument();
});
});

View File

@ -22,6 +22,8 @@ import { NETWORK_TYPES } from '../../../../shared/constants/network';
import { Numeric } from '../../../../shared/modules/Numeric';
import { EtherDenomination } from '../../../../shared/constants/common';
import ConfirmPageContainerNavigation from '../confirm-page-container/confirm-page-container-navigation';
import SecurityProviderBannerMessage from '../security-provider-banner-message/security-provider-banner-message';
import { SECURITY_PROVIDER_MESSAGE_SEVERITIES } from '../security-provider-banner-message/security-provider-banner-message.constants';
import SignatureRequestOriginalWarning from './signature-request-original-warning';
export default class SignatureRequestOriginal extends Component {
@ -135,6 +137,15 @@ export default class SignatureRequestOriginal extends Component {
return (
<div className="request-signature__body">
{(txData?.securityProviderResponse?.flagAsDangerous !== undefined &&
txData?.securityProviderResponse?.flagAsDangerous !==
SECURITY_PROVIDER_MESSAGE_SEVERITIES.NOT_MALICIOUS) ||
(txData?.securityProviderResponse &&
Object.keys(txData.securityProviderResponse).length === 0) ? (
<SecurityProviderBannerMessage
securityProviderResponse={txData.securityProviderResponse}
/>
) : null}
<div className="request-signature__origin">
<SiteOrigin
siteOrigin={txData.msgParams.origin}

View File

@ -5,6 +5,7 @@ import { MESSAGE_TYPE } from '../../../../shared/constants/app';
import mockState from '../../../../test/data/mock-state.json';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import configureStore from '../../../store/store';
import { SECURITY_PROVIDER_MESSAGE_SEVERITIES } from '../security-provider-banner-message/security-provider-banner-message.constants';
import SignatureRequestOriginal from '.';
const MOCK_SIGN_DATA = JSON.stringify({
@ -115,4 +116,37 @@ describe('SignatureRequestOriginal', () => {
expect(getByText('Message \\u202E test:')).toBeInTheDocument();
expect(getByText('Hi, \\u202E Alice!')).toBeInTheDocument();
});
it('should render SecurityProviderBannerMessage component properly', () => {
props.txData.securityProviderResponse = {
flagAsDangerous: '?',
reason: 'Some reason...',
reason_header: 'Some reason header...',
};
render();
expect(screen.getByText('Request not verified')).toBeInTheDocument();
expect(
screen.getByText(
'Because of an error, this request was not verified by the security provider. Proceed with caution.',
),
).toBeInTheDocument();
expect(
screen.getByText('This is based on information from'),
).toBeInTheDocument();
});
it('should not render SecurityProviderBannerMessage component when flagAsDangerous is not malicious', () => {
props.txData.securityProviderResponse = {
flagAsDangerous: SECURITY_PROVIDER_MESSAGE_SEVERITIES.NOT_MALICIOUS,
};
render();
expect(screen.queryByText('Request not verified')).toBeNull();
expect(
screen.queryByText(
'Because of an error, this request was not verified by the security provider. Proceed with caution.',
),
).toBeNull();
expect(screen.queryByText('This is based on information from')).toBeNull();
});
});

View File

@ -16,6 +16,8 @@ import { formatMessageParams } from '../../../../shared/modules/siwe';
import { Icon } from '../../component-library/icon/icon';
import { IconColor } from '../../../helpers/constants/design-system';
import SecurityProviderBannerMessage from '../security-provider-banner-message/security-provider-banner-message';
import { SECURITY_PROVIDER_MESSAGE_SEVERITIES } from '../security-provider-banner-message/security-provider-banner-message.constants';
import Header from './signature-request-siwe-header';
import Message from './signature-request-siwe-message';
@ -88,6 +90,15 @@ export default function SignatureRequestSIWE({
isSIWEDomainValid={isSIWEDomainValid}
subjectMetadata={targetSubjectMetadata}
/>
{(txData?.securityProviderResponse?.flagAsDangerous !== undefined &&
txData?.securityProviderResponse?.flagAsDangerous !==
SECURITY_PROVIDER_MESSAGE_SEVERITIES.NOT_MALICIOUS) ||
(txData?.securityProviderResponse &&
Object.keys(txData.securityProviderResponse).length === 0) ? (
<SecurityProviderBannerMessage
securityProviderResponse={txData.securityProviderResponse}
/>
) : null}
<Message data={formatMessageParams(parsedMessage, t)} />
{!isMatchingAddress && (
<ActionableMessage

View File

@ -24,6 +24,10 @@
&__reject-all-button {
margin-top: -15px;
}
&__origin {
margin-top: 16px;
}
}
.signature-request-header {

View File

@ -19,6 +19,8 @@ import { NETWORK_TYPES } from '../../../../shared/constants/network';
import { Numeric } from '../../../../shared/modules/Numeric';
import { EtherDenomination } from '../../../../shared/constants/common';
import ConfirmPageContainerNavigation from '../confirm-page-container/confirm-page-container-navigation';
import SecurityProviderBannerMessage from '../security-provider-banner-message/security-provider-banner-message';
import { SECURITY_PROVIDER_MESSAGE_SEVERITIES } from '../security-provider-banner-message/security-provider-banner-message.constants';
import Footer from './signature-request-footer';
import Message from './signature-request-message';
@ -225,6 +227,15 @@ export default class SignatureRequest extends PureComponent {
/>
</div>
<div className="signature-request-content">
{(txData?.securityProviderResponse?.flagAsDangerous !== undefined &&
txData?.securityProviderResponse?.flagAsDangerous !==
SECURITY_PROVIDER_MESSAGE_SEVERITIES.NOT_MALICIOUS) ||
(txData?.securityProviderResponse &&
Object.keys(txData.securityProviderResponse).length === 0) ? (
<SecurityProviderBannerMessage
securityProviderResponse={txData.securityProviderResponse}
/>
) : null}
<div className="signature-request__origin">
<SiteOrigin
siteOrigin={origin}

View File

@ -3,6 +3,7 @@ import { fireEvent } from '@testing-library/react';
import configureMockStore from 'redux-mock-store';
import mockState from '../../../../test/data/mock-state.json';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import { SECURITY_PROVIDER_MESSAGE_SEVERITIES } from '../security-provider-banner-message/security-provider-banner-message.constants';
import SignatureRequest from './signature-request.component';
describe('Signature Request Component', () => {
@ -275,5 +276,88 @@ describe('Signature Request Component', () => {
expect(getByText('Reject 2 requests')).toBeInTheDocument();
});
it('should render SecurityProviderBannerMessage component properly', () => {
const msgParams = {
data: JSON.stringify(messageData),
version: 'V4',
origin: 'test',
};
const { queryByText } = renderWithProvider(
<SignatureRequest
hardwareWalletRequiresConnection={false}
clearConfirmTransaction={() => undefined}
cancel={() => undefined}
cancelAll={() => undefined}
mostRecentOverviewPage="/"
showRejectTransactionsConfirmationModal={() => undefined}
history={{ push: '/' }}
sign={() => undefined}
txData={{
msgParams,
securityProviderResponse: {
flagAsDangerous: '?',
reason: 'Some reason...',
reason_header: 'Some reason header...',
},
}}
fromAccount={{ address: fromAddress }}
provider={{ type: 'rpc' }}
unapprovedMessagesCount={2}
/>,
store,
);
expect(queryByText('Request not verified')).toBeInTheDocument();
expect(
queryByText(
'Because of an error, this request was not verified by the security provider. Proceed with caution.',
),
).toBeInTheDocument();
expect(
queryByText('This is based on information from'),
).toBeInTheDocument();
});
it('should not render SecurityProviderBannerMessage component when flagAsDangerous is not malicious', () => {
const msgParams = {
data: JSON.stringify(messageData),
version: 'V4',
origin: 'test',
};
const { queryByText } = renderWithProvider(
<SignatureRequest
hardwareWalletRequiresConnection={false}
clearConfirmTransaction={() => undefined}
cancel={() => undefined}
cancelAll={() => undefined}
mostRecentOverviewPage="/"
showRejectTransactionsConfirmationModal={() => undefined}
history={{ push: '/' }}
sign={() => undefined}
txData={{
msgParams,
securityProviderResponse: {
flagAsDangerous:
SECURITY_PROVIDER_MESSAGE_SEVERITIES.NOT_MALICIOUS,
},
}}
fromAccount={{ address: fromAddress }}
provider={{ type: 'rpc' }}
unapprovedMessagesCount={2}
/>,
store,
);
expect(queryByText('Request not verified')).toBeNull();
expect(
queryByText(
'Because of an error, this request was not verified by the security provider. Proceed with caution.',
),
).toBeNull();
expect(queryByText('This is based on information from')).toBeNull();
});
});
});