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

fix(settings): fixed two IPFS gateway issues (#19700)

* fix(settings): fixed two IPFS gateway issues

- adds back in two bugfixes that were originally in #19283
  - fixes #16871
  - fixes #18140

- achieves 100% code coverage for /ui/pages/settings/security-tab
- removes the npm package `valid-url`, which has not been updated in 10 years

* changes after #20172 was merged

* improved URL validation (specifically spaces)

* better Jest coverage

* response to legobeat review

* fixing lint and Jest
This commit is contained in:
Howard Braham 2023-08-22 22:13:13 -07:00 committed by GitHub
parent b8525566f2
commit d3d30fd373
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 381 additions and 227 deletions

View File

@ -1,16 +1,16 @@
import { ethErrors, errorCodes } from 'eth-rpc-errors';
import validUrl from 'valid-url';
import { omit } from 'lodash';
import { ApprovalType } from '@metamask/controller-utils'; import { ApprovalType } from '@metamask/controller-utils';
import { errorCodes, ethErrors } from 'eth-rpc-errors';
import { omit } from 'lodash';
import { import {
MESSAGE_TYPE, MESSAGE_TYPE,
UNKNOWN_TICKER_SYMBOL, UNKNOWN_TICKER_SYMBOL,
} from '../../../../../shared/constants/app'; } from '../../../../../shared/constants/app';
import { MetaMetricsNetworkEventSource } from '../../../../../shared/constants/metametrics';
import { import {
isPrefixedFormattedHexString, isPrefixedFormattedHexString,
isSafeChainId, isSafeChainId,
} from '../../../../../shared/modules/network.utils'; } from '../../../../../shared/modules/network.utils';
import { MetaMetricsNetworkEventSource } from '../../../../../shared/constants/metametrics'; import { getValidUrl } from '../../util';
const addEthereumChain = { const addEthereumChain = {
methodNames: [MESSAGE_TYPE.ADD_ETHEREUM_CHAIN], methodNames: [MESSAGE_TYPE.ADD_ETHEREUM_CHAIN],
@ -83,27 +83,25 @@ async function addEthereumChainHandler(
); );
} }
const isLocalhost = (strUrl) => { function isLocalhostOrHttps(urlString) {
try { const url = getValidUrl(urlString);
const url = new URL(strUrl);
return url.hostname === 'localhost' || url.hostname === '127.0.0.1'; return (
} catch (error) { url !== null &&
return false; (url.hostname === 'localhost' ||
url.hostname === '127.0.0.1' ||
url.protocol === 'https:')
);
} }
};
const firstValidRPCUrl = Array.isArray(rpcUrls) const firstValidRPCUrl = Array.isArray(rpcUrls)
? rpcUrls.find( ? rpcUrls.find((rpcUrl) => isLocalhostOrHttps(rpcUrl))
(rpcUrl) => isLocalhost(rpcUrl) || validUrl.isHttpsUri(rpcUrl),
)
: null; : null;
const firstValidBlockExplorerUrl = const firstValidBlockExplorerUrl =
blockExplorerUrls !== null && Array.isArray(blockExplorerUrls) blockExplorerUrls !== null && Array.isArray(blockExplorerUrls)
? blockExplorerUrls.find( ? blockExplorerUrls.find((blockExplorerUrl) =>
(blockExplorerUrl) => isLocalhostOrHttps(blockExplorerUrl),
isLocalhost(blockExplorerUrl) ||
validUrl.isHttpsUri(blockExplorerUrl),
) )
: null; : null;

View File

@ -1,24 +1,27 @@
import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils';
import { import {
ENVIRONMENT_TYPE_POPUP,
ENVIRONMENT_TYPE_NOTIFICATION,
ENVIRONMENT_TYPE_FULLSCREEN,
ENVIRONMENT_TYPE_BACKGROUND, ENVIRONMENT_TYPE_BACKGROUND,
PLATFORM_FIREFOX, ENVIRONMENT_TYPE_FULLSCREEN,
PLATFORM_OPERA, ENVIRONMENT_TYPE_NOTIFICATION,
ENVIRONMENT_TYPE_POPUP,
PLATFORM_CHROME, PLATFORM_CHROME,
PLATFORM_EDGE, PLATFORM_EDGE,
PLATFORM_FIREFOX,
PLATFORM_OPERA,
} from '../../../shared/constants/app'; } from '../../../shared/constants/app';
import { import {
TransactionEnvelopeType,
TransactionStatus, TransactionStatus,
TransactionType, TransactionType,
TransactionEnvelopeType,
} from '../../../shared/constants/transaction'; } from '../../../shared/constants/transaction';
import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils';
import { import {
addUrlProtocolPrefix,
deferredPromise, deferredPromise,
formatTxMetaForRpcResult,
getEnvironmentType, getEnvironmentType,
getPlatform, getPlatform,
formatTxMetaForRpcResult, getValidUrl,
isWebUrl,
} from './util'; } from './util';
describe('app utils', () => { describe('app utils', () => {
@ -73,6 +76,39 @@ describe('app utils', () => {
}); });
}); });
describe('URL utils', () => {
it('should test addUrlProtocolPrefix', () => {
expect(addUrlProtocolPrefix('http://example.com')).toStrictEqual(
'http://example.com',
);
expect(addUrlProtocolPrefix('https://example.com')).toStrictEqual(
'https://example.com',
);
expect(addUrlProtocolPrefix('example.com')).toStrictEqual(
'https://example.com',
);
expect(addUrlProtocolPrefix('exa mple.com')).toStrictEqual(null);
});
it('should test isWebUrl', () => {
expect(isWebUrl('http://example.com')).toStrictEqual(true);
expect(isWebUrl('https://example.com')).toStrictEqual(true);
expect(isWebUrl('https://exa mple.com')).toStrictEqual(false);
expect(isWebUrl('')).toStrictEqual(false);
});
it('should test getValidUrl', () => {
expect(getValidUrl('http://example.com').toString()).toStrictEqual(
'http://example.com/',
);
expect(getValidUrl('https://example.com').toString()).toStrictEqual(
'https://example.com/',
);
expect(getValidUrl('https://exa%20mple.com')).toStrictEqual(null);
expect(getValidUrl('')).toStrictEqual(null);
});
});
describe('isPrefixedFormattedHexString', () => { describe('isPrefixedFormattedHexString', () => {
it('should return true for valid hex strings', () => { it('should return true for valid hex strings', () => {
expect(isPrefixedFormattedHexString('0x1')).toStrictEqual(true); expect(isPrefixedFormattedHexString('0x1')).toStrictEqual(true);

View File

@ -1,24 +1,24 @@
import urlLib from 'url';
import { AccessList } from '@ethereumjs/tx';
import BN from 'bn.js'; import BN from 'bn.js';
import { memoize } from 'lodash'; import { memoize } from 'lodash';
import { AccessList } from '@ethereumjs/tx';
import { CHAIN_IDS, TEST_CHAINS } from '../../../shared/constants/network';
import { import {
ENVIRONMENT_TYPE_POPUP,
ENVIRONMENT_TYPE_NOTIFICATION,
ENVIRONMENT_TYPE_FULLSCREEN,
ENVIRONMENT_TYPE_BACKGROUND, ENVIRONMENT_TYPE_BACKGROUND,
PLATFORM_FIREFOX, ENVIRONMENT_TYPE_FULLSCREEN,
PLATFORM_OPERA, ENVIRONMENT_TYPE_NOTIFICATION,
ENVIRONMENT_TYPE_POPUP,
PLATFORM_BRAVE,
PLATFORM_CHROME, PLATFORM_CHROME,
PLATFORM_EDGE, PLATFORM_EDGE,
PLATFORM_BRAVE, PLATFORM_FIREFOX,
PLATFORM_OPERA,
} from '../../../shared/constants/app'; } from '../../../shared/constants/app';
import { stripHexPrefix } from '../../../shared/modules/hexstring-utils'; import { CHAIN_IDS, TEST_CHAINS } from '../../../shared/constants/network';
import { import {
TransactionEnvelopeType, TransactionEnvelopeType,
TransactionMeta, TransactionMeta,
} from '../../../shared/constants/transaction'; } from '../../../shared/constants/transaction';
import { stripHexPrefix } from '../../../shared/modules/hexstring-utils';
/** /**
* @see {@link getEnvironmentType} * @see {@link getEnvironmentType}
@ -143,13 +143,13 @@ function checkAlarmExists(alarmList: { name: string }[], alarmName: string) {
} }
export { export {
getPlatform,
getEnvironmentType,
hexToBn,
BnMultiplyByFraction, BnMultiplyByFraction,
addHexPrefix, addHexPrefix,
getChainType,
checkAlarmExists, checkAlarmExists,
getChainType,
getEnvironmentType,
getPlatform,
hexToBn,
}; };
// Taken from https://stackoverflow.com/a/1349426/3696652 // Taken from https://stackoverflow.com/a/1349426/3696652
@ -235,10 +235,43 @@ export function previousValueComparator<A>(
} }
export function addUrlProtocolPrefix(urlString: string) { export function addUrlProtocolPrefix(urlString: string) {
if (!urlString.match(/(^http:\/\/)|(^https:\/\/)/u)) { let trimmed = urlString.trim();
return `https://${urlString}`;
if (trimmed.length && !urlLib.parse(trimmed).protocol) {
trimmed = `https://${trimmed}`;
} }
return urlString;
if (getValidUrl(trimmed) !== null) {
return trimmed;
}
return null;
}
export function getValidUrl(urlString: string): URL | null {
try {
const url = new URL(urlString);
if (url.hostname.length === 0 || url.pathname.length === 0) {
return null;
}
if (url.hostname !== decodeURIComponent(url.hostname)) {
return null; // will happen if there's a %, a space, or other invalid character in the hostname
}
return url;
} catch (error) {
return null;
}
}
export function isWebUrl(urlString: string): boolean {
const url = getValidUrl(urlString);
return (
url !== null && (url.protocol === 'https:' || url.protocol === 'http:')
);
} }
interface FormattedTransactionMeta { interface FormattedTransactionMeta {

View File

@ -8,7 +8,7 @@ module.exports = {
'<rootDir>/app/scripts/controllers/transactions/EtherscanRemoteTransactionSource.ts', '<rootDir>/app/scripts/controllers/transactions/EtherscanRemoteTransactionSource.ts',
'<rootDir>/app/scripts/controllers/transactions/IncomingTransactionHelper.ts', '<rootDir>/app/scripts/controllers/transactions/IncomingTransactionHelper.ts',
'<rootDir>/app/scripts/flask/**/*.js', '<rootDir>/app/scripts/flask/**/*.js',
'<rootDir>/app/scripts/lib/**/*.js', '<rootDir>/app/scripts/lib/**/*.(js|ts)',
'<rootDir>/app/scripts/lib/createRPCMethodTrackingMiddleware.js', '<rootDir>/app/scripts/lib/createRPCMethodTrackingMiddleware.js',
'<rootDir>/app/scripts/migrations/*.js', '<rootDir>/app/scripts/migrations/*.js',
'<rootDir>/app/scripts/migrations/*.ts', '<rootDir>/app/scripts/migrations/*.ts',
@ -48,8 +48,7 @@ module.exports = {
'<rootDir>/app/scripts/controllers/sign.test.ts', '<rootDir>/app/scripts/controllers/sign.test.ts',
'<rootDir>/app/scripts/controllers/decrypt-message.test.ts', '<rootDir>/app/scripts/controllers/decrypt-message.test.ts',
'<rootDir>/app/scripts/flask/**/*.test.js', '<rootDir>/app/scripts/flask/**/*.test.js',
'<rootDir>/app/scripts/lib/**/*.test.js', '<rootDir>/app/scripts/lib/**/*.test.(js|ts)',
'<rootDir>/app/scripts/lib/**/*.test.ts',
'<rootDir>/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js', '<rootDir>/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js',
'<rootDir>/app/scripts/migrations/*.test.(js|ts)', '<rootDir>/app/scripts/migrations/*.test.(js|ts)',
'<rootDir>/app/scripts/platforms/*.test.js', '<rootDir>/app/scripts/platforms/*.test.js',

View File

@ -362,7 +362,6 @@
"single-call-balance-checker-abi": "^1.0.0", "single-call-balance-checker-abi": "^1.0.0",
"unicode-confusables": "^0.1.1", "unicode-confusables": "^0.1.1",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"valid-url": "^1.0.9",
"web3-stream-provider": "^4.0.0", "web3-stream-provider": "^4.0.0",
"zxcvbn": "^4.4.2" "zxcvbn": "^4.4.2"
}, },

View File

@ -340,6 +340,8 @@
"unapprovedTypedMessagesCount": 0, "unapprovedTypedMessagesCount": 0,
"useTokenDetection": true, "useTokenDetection": true,
"useCurrencyRateCheck": true, "useCurrencyRateCheck": true,
"useNftDetection": true,
"openSeaEnabled": true,
"advancedGasFee": { "advancedGasFee": {
"maxBaseFee": "75", "maxBaseFee": "75",
"priorityFee": "2" "priorityFee": "2"

View File

@ -129,7 +129,7 @@ const RevealSeedPage = () => {
const renderPasswordPromptContent = () => { const renderPasswordPromptContent = () => {
return ( return (
<form onSubmit={(event) => handleSubmit(event)}> <form onSubmit={handleSubmit}>
<Label htmlFor="password-box">{t('enterPasswordContinue')}</Label> <Label htmlFor="password-box">{t('enterPasswordContinue')}</Label>
<TextField <TextField
inputProps={{ inputProps={{

View File

@ -1,3 +1,7 @@
import classnames from 'classnames';
import { isEqual } from 'lodash';
import log from 'loglevel';
import PropTypes from 'prop-types';
import React, { import React, {
useCallback, useCallback,
useContext, useContext,
@ -6,12 +10,18 @@ import React, {
useState, useState,
} from 'react'; } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import PropTypes from 'prop-types'; import { isWebUrl } from '../../../../../app/scripts/lib/util';
import validUrl from 'valid-url'; import {
import log from 'loglevel'; MetaMetricsEventCategory,
import classnames from 'classnames'; MetaMetricsEventName,
import { isEqual } from 'lodash'; MetaMetricsNetworkEventSource,
import { useI18nContext } from '../../../../hooks/useI18nContext'; } from '../../../../../shared/constants/metametrics';
import {
FEATURED_RPCS,
infuraProjectId,
} from '../../../../../shared/constants/network';
import fetchWithCache from '../../../../../shared/lib/fetch-with-cache';
import { decimalToHex } from '../../../../../shared/modules/conversion.utils';
import { import {
isPrefixedFormattedHexString, isPrefixedFormattedHexString,
isSafeChainId, isSafeChainId,
@ -20,27 +30,17 @@ import { jsonRpcRequest } from '../../../../../shared/modules/rpc.utils';
import ActionableMessage from '../../../../components/ui/actionable-message'; import ActionableMessage from '../../../../components/ui/actionable-message';
import Button from '../../../../components/ui/button'; import Button from '../../../../components/ui/button';
import FormField from '../../../../components/ui/form-field'; import FormField from '../../../../components/ui/form-field';
import {
setSelectedNetworkConfigurationId,
upsertNetworkConfiguration,
editAndSetNetworkConfiguration,
showModal,
setNewNetworkAdded,
} from '../../../../store/actions';
import fetchWithCache from '../../../../../shared/lib/fetch-with-cache';
import { usePrevious } from '../../../../hooks/usePrevious';
import {
MetaMetricsEventCategory,
MetaMetricsEventName,
MetaMetricsNetworkEventSource,
} from '../../../../../shared/constants/metametrics';
import {
infuraProjectId,
FEATURED_RPCS,
} from '../../../../../shared/constants/network';
import { decimalToHex } from '../../../../../shared/modules/conversion.utils';
import { MetaMetricsContext } from '../../../../contexts/metametrics'; import { MetaMetricsContext } from '../../../../contexts/metametrics';
import { getNetworkLabelKey } from '../../../../helpers/utils/i18n-helper'; import { getNetworkLabelKey } from '../../../../helpers/utils/i18n-helper';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import { usePrevious } from '../../../../hooks/usePrevious';
import {
editAndSetNetworkConfiguration,
setNewNetworkAdded,
setSelectedNetworkConfigurationId,
showModal,
upsertNetworkConfiguration,
} from '../../../../store/actions';
/** /**
* Attempts to convert the given chainId to a decimal string, for display * Attempts to convert the given chainId to a decimal string, for display
@ -74,11 +74,6 @@ const prefixChainId = (chainId) => {
return prefixedChainId; return prefixedChainId;
}; };
const isValidWhenAppended = (url) => {
const appendedRpc = `http://${url}`;
return validUrl.isWebUri(appendedRpc) && !url.match(/^https?:\/\/$/u);
};
const NetworksForm = ({ const NetworksForm = ({
addNewNetwork, addNewNetwork,
restrictHeight, restrictHeight,
@ -208,23 +203,20 @@ const NetworksForm = ({
const validateBlockExplorerURL = useCallback( const validateBlockExplorerURL = useCallback(
(url) => { (url) => {
if (!validUrl.isWebUri(url) && url !== '') { if (url.length > 0 && !isWebUrl(url)) {
let errorKey; if (isWebUrl(`https://${url}`)) {
let errorMessage; return {
key: 'urlErrorMsg',
if (isValidWhenAppended(url)) { msg: t('urlErrorMsg'),
errorKey = 'urlErrorMsg'; };
errorMessage = t('urlErrorMsg');
} else {
errorKey = 'invalidBlockExplorerURL';
errorMessage = t('invalidBlockExplorerURL');
} }
return { return {
key: errorKey, key: 'invalidBlockExplorerURL',
msg: errorMessage, msg: t('invalidBlockExplorerURL'),
}; };
} }
return null; return null;
}, },
[t], [t],
@ -407,7 +399,6 @@ const NetworksForm = ({
const validateRPCUrl = useCallback( const validateRPCUrl = useCallback(
(url) => { (url) => {
const isValidUrl = validUrl.isWebUri(url);
const [ const [
{ {
rpcUrl: matchingRPCUrl = null, rpcUrl: matchingRPCUrl = null,
@ -417,20 +408,16 @@ const NetworksForm = ({
] = networksToRender.filter((e) => e.rpcUrl === url); ] = networksToRender.filter((e) => e.rpcUrl === url);
const { rpcUrl: selectedNetworkRpcUrl } = selectedNetwork; const { rpcUrl: selectedNetworkRpcUrl } = selectedNetwork;
if (!isValidUrl && url !== '') { if (url.length > 0 && !isWebUrl(url)) {
let errorKey; if (isWebUrl(`https://${url}`)) {
let errorMessage;
if (isValidWhenAppended(url)) {
errorKey = 'urlErrorMsg';
errorMessage = t('urlErrorMsg');
} else {
errorKey = 'invalidRPC';
errorMessage = t('invalidRPC');
}
return { return {
key: errorKey, key: 'urlErrorMsg',
msg: errorMessage, msg: t('urlErrorMsg'),
};
}
return {
key: 'invalidRPC',
msg: t('invalidRPC'),
}; };
} else if (matchingRPCUrl && matchingRPCUrl !== selectedNetworkRpcUrl) { } else if (matchingRPCUrl && matchingRPCUrl !== selectedNetworkRpcUrl) {
return { return {

View File

@ -448,6 +448,7 @@ exports[`Security Tab should match snapshot 1`] = `
</div> </div>
<div <div
class="settings-page__content-item-col" class="settings-page__content-item-col"
data-testid="ipfsToggle"
> >
<label <label
class="toggle-button toggle-button--on" class="toggle-button toggle-button--on"
@ -476,7 +477,7 @@ exports[`Security Tab should match snapshot 1`] = `
<input <input
style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px;" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px;"
type="checkbox" type="checkbox"
value="dweb.link" value="true"
/> />
</div> </div>
<div <div
@ -785,12 +786,15 @@ exports[`Security Tab should match snapshot 1`] = `
Displaying NFT media and data exposes your IP address to OpenSea or other third parties. This can allow attackers to associate your IP address with your Ethereum address. NFT autodetection relies on this setting, and won't be available when this is turned off. Displaying NFT media and data exposes your IP address to OpenSea or other third parties. This can allow attackers to associate your IP address with your Ethereum address. NFT autodetection relies on this setting, and won't be available when this is turned off.
</div> </div>
</div> </div>
<div
class="settings-page__content-item"
>
<div <div
class="settings-page__content-item-col" class="settings-page__content-item-col"
data-testid="displayNftMedia" data-testid="enableOpenSeaAPI"
> >
<label <label
class="toggle-button toggle-button--off" class="toggle-button toggle-button--on"
tabindex="0" tabindex="0"
> >
<div <div
@ -800,23 +804,23 @@ exports[`Security Tab should match snapshot 1`] = `
style="width: 40px; height: 24px; padding: 0px; border-radius: 26px; display: flex; align-items: center; justify-content: center; background-color: rgb(242, 244, 246);" style="width: 40px; height: 24px; padding: 0px; border-radius: 26px; display: flex; align-items: center; justify-content: center; background-color: rgb(242, 244, 246);"
> >
<div <div
style="font-size: 11px; display: flex; align-items: center; justify-content: center; font-family: 'Helvetica Neue', Helvetica, sans-serif; position: relative; color: rgb(250, 250, 250); margin-top: auto; margin-bottom: auto; line-height: 0; opacity: 0; width: 26px; height: 20px; left: 4px;" style="font-size: 11px; display: flex; align-items: center; justify-content: center; font-family: 'Helvetica Neue', Helvetica, sans-serif; position: relative; color: rgb(250, 250, 250); margin-top: auto; margin-bottom: auto; line-height: 0; opacity: 1; width: 26px; height: 20px; left: 4px;"
/> />
<div <div
style="font-size: 11px; display: flex; align-items: center; justify-content: center; font-family: 'Helvetica Neue', Helvetica, sans-serif; position: relative; color: rgba(255, 255, 255, 0.6); bottom: 0px; margin-top: auto; margin-bottom: auto; padding-right: 5px; line-height: 0; width: 26px; height: 20px; opacity: 1;" style="font-size: 11px; display: flex; align-items: center; justify-content: center; font-family: 'Helvetica Neue', Helvetica, sans-serif; position: relative; color: rgba(255, 255, 255, 0.6); bottom: 0px; margin-top: auto; margin-bottom: auto; padding-right: 5px; line-height: 0; width: 26px; height: 20px; opacity: 0;"
/> />
</div> </div>
<div <div
style="position: absolute; height: 100%; top: 0px; left: 0px; display: flex; flex: 1; align-self: stretch; align-items: center; justify-content: flex-start;" style="position: absolute; height: 100%; top: 0px; left: 0px; display: flex; flex: 1; align-self: stretch; align-items: center; justify-content: flex-start;"
> >
<div <div
style="width: 18px; height: 18px; display: flex; align-self: center; box-shadow: none; border-radius: 50%; box-sizing: border-box; position: relative; background-color: rgb(106, 115, 125); left: 3px;" style="width: 18px; height: 18px; display: flex; align-self: center; box-shadow: none; border-radius: 50%; box-sizing: border-box; position: relative; background-color: rgb(3, 125, 214); left: 18px;"
/> />
</div> </div>
<input <input
style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px;" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px;"
type="checkbox" type="checkbox"
value="false" value="true"
/> />
</div> </div>
<div <div
@ -836,6 +840,7 @@ exports[`Security Tab should match snapshot 1`] = `
</label> </label>
</div> </div>
</div> </div>
</div>
<div <div
class="mm-box settings-page__content-row mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-space-between" class="mm-box settings-page__content-row mm-box--display-flex mm-box--flex-direction-row mm-box--justify-content-space-between"
> >
@ -878,7 +883,7 @@ exports[`Security Tab should match snapshot 1`] = `
data-testid="useNftDetection" data-testid="useNftDetection"
> >
<label <label
class="toggle-button toggle-button--off" class="toggle-button toggle-button--on"
tabindex="0" tabindex="0"
> >
<div <div
@ -888,23 +893,23 @@ exports[`Security Tab should match snapshot 1`] = `
style="width: 40px; height: 24px; padding: 0px; border-radius: 26px; display: flex; align-items: center; justify-content: center; background-color: rgb(242, 244, 246);" style="width: 40px; height: 24px; padding: 0px; border-radius: 26px; display: flex; align-items: center; justify-content: center; background-color: rgb(242, 244, 246);"
> >
<div <div
style="font-size: 11px; display: flex; align-items: center; justify-content: center; font-family: 'Helvetica Neue', Helvetica, sans-serif; position: relative; color: rgb(250, 250, 250); margin-top: auto; margin-bottom: auto; line-height: 0; opacity: 0; width: 26px; height: 20px; left: 4px;" style="font-size: 11px; display: flex; align-items: center; justify-content: center; font-family: 'Helvetica Neue', Helvetica, sans-serif; position: relative; color: rgb(250, 250, 250); margin-top: auto; margin-bottom: auto; line-height: 0; opacity: 1; width: 26px; height: 20px; left: 4px;"
/> />
<div <div
style="font-size: 11px; display: flex; align-items: center; justify-content: center; font-family: 'Helvetica Neue', Helvetica, sans-serif; position: relative; color: rgba(255, 255, 255, 0.6); bottom: 0px; margin-top: auto; margin-bottom: auto; padding-right: 5px; line-height: 0; width: 26px; height: 20px; opacity: 1;" style="font-size: 11px; display: flex; align-items: center; justify-content: center; font-family: 'Helvetica Neue', Helvetica, sans-serif; position: relative; color: rgba(255, 255, 255, 0.6); bottom: 0px; margin-top: auto; margin-bottom: auto; padding-right: 5px; line-height: 0; width: 26px; height: 20px; opacity: 0;"
/> />
</div> </div>
<div <div
style="position: absolute; height: 100%; top: 0px; left: 0px; display: flex; flex: 1; align-self: stretch; align-items: center; justify-content: flex-start;" style="position: absolute; height: 100%; top: 0px; left: 0px; display: flex; flex: 1; align-self: stretch; align-items: center; justify-content: flex-start;"
> >
<div <div
style="width: 18px; height: 18px; display: flex; align-self: center; box-shadow: none; border-radius: 50%; box-sizing: border-box; position: relative; background-color: rgb(106, 115, 125); left: 3px;" style="width: 18px; height: 18px; display: flex; align-self: center; box-shadow: none; border-radius: 50%; box-sizing: border-box; position: relative; background-color: rgb(3, 125, 214); left: 18px;"
/> />
</div> </div>
<input <input
style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px;" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px;"
type="checkbox" type="checkbox"
value="false" value="true"
/> />
</div> </div>
<div <div

View File

@ -23,8 +23,8 @@ import {
import SRPQuiz from '../../../components/app/srp-quiz-modal/SRPQuiz'; import SRPQuiz from '../../../components/app/srp-quiz-modal/SRPQuiz';
import { import {
BUTTON_SIZES, BUTTON_SIZES,
Button,
Box, Box,
Button,
Text, Text,
} from '../../../components/component-library'; } from '../../../components/component-library';
import TextField from '../../../components/ui/text-field'; import TextField from '../../../components/ui/text-field';
@ -76,10 +76,10 @@ export default class SecurityTab extends PureComponent {
}; };
state = { state = {
ipfsGateway: this.props.ipfsGateway, ipfsGateway: this.props.ipfsGateway || IPFS_DEFAULT_GATEWAY_URL,
ipfsGatewayError: '', ipfsGatewayError: '',
srpQuizModalVisible: false, srpQuizModalVisible: false,
ipfsToggle: false, ipfsToggle: this.props.ipfsGateway.length > 0,
}; };
settingsRefCounter = 0; settingsRefCounter = 0;
@ -367,51 +367,40 @@ export default class SecurityTab extends PureComponent {
renderIpfsGatewayControl() { renderIpfsGatewayControl() {
const { t } = this.context; const { t } = this.context;
const { ipfsGatewayError } = this.state;
const { useAddressBarEnsResolution, setUseAddressBarEnsResolution } =
this.props;
const handleIpfsGatewaySave = (gateway) => {
const url = gateway ? new URL(addUrlProtocolPrefix(gateway)) : '';
const { host } = url;
this.props.setIpfsGateway(host);
};
const handleIpfsGatewayChange = (url) => {
this.setState(() => {
let ipfsError = ''; let ipfsError = '';
const handleIpfsGatewayChange = (url) => {
if (url.length > 0) {
try { try {
const urlObj = new URL(addUrlProtocolPrefix(url)); const validUrl = addUrlProtocolPrefix(url);
if (!urlObj.host) {
throw new Error(); if (!validUrl) {
ipfsError = t('invalidIpfsGateway');
} }
const urlObj = new URL(validUrl);
// don't allow the use of this gateway // don't allow the use of this gateway
if (urlObj.host === 'gateway.ipfs.io') { if (urlObj.host === 'gateway.ipfs.io') {
throw new Error('Forbidden gateway'); ipfsError = t('forbiddenIpfsGateway');
}
} catch (error) {
ipfsError =
error.message === 'Forbidden gateway'
? t('forbiddenIpfsGateway')
: t('invalidIpfsGateway');
} }
handleIpfsGatewaySave(url); if (ipfsError.length === 0) {
return { this.props.setIpfsGateway(urlObj.host);
}
} catch (error) {
ipfsError = t('invalidIpfsGateway');
}
} else {
ipfsError = t('invalidIpfsGateway');
}
this.setState({
ipfsGateway: url, ipfsGateway: url,
ipfsGatewayError: ipfsError, ipfsGatewayError: ipfsError,
};
}); });
}; };
const handleIpfsToggle = (url) => {
url?.length < 1
? handleIpfsGatewayChange(IPFS_DEFAULT_GATEWAY_URL)
: handleIpfsGatewayChange('');
};
return ( return (
<Box <Box
ref={this.settingsRefs[6]} ref={this.settingsRefs[6]}
@ -426,27 +415,36 @@ export default class SecurityTab extends PureComponent {
{t('ipfsGatewayDescription')} {t('ipfsGatewayDescription')}
</div> </div>
</div> </div>
<div className="settings-page__content-item-col"> <div
className="settings-page__content-item-col"
data-testid="ipfsToggle"
>
<ToggleButton <ToggleButton
value={this.state.ipfsGateway} value={this.state.ipfsToggle}
onToggle={(value) => { onToggle={(value) => {
handleIpfsToggle(value); if (value) {
this.setState({ ipfsToggle: Boolean(value) }); // turning from true to false
this.props.setIpfsGateway('');
} else {
// turning from false to true
handleIpfsGatewayChange(this.state.ipfsGateway);
}
this.setState({ ipfsToggle: !value });
}} }}
offLabel={t('off')} offLabel={t('off')}
onLabel={t('on')} onLabel={t('on')}
/> />
</div> </div>
{!this.state.ipfsToggle && ( {this.state.ipfsToggle && (
<div className="settings-page__content-item"> <div className="settings-page__content-item">
<span>{t('addIPFSGateway')}</span> <span>{t('addIPFSGateway')}</span>
<div className="settings-page__content-item-col"> <div className="settings-page__content-item-col">
<TextField <TextField
type="text" type="text"
disabled={!this.state.ipfsGateway}
value={this.state.ipfsGateway} value={this.state.ipfsGateway}
onChange={(e) => handleIpfsGatewayChange(e.target.value)} onChange={(e) => handleIpfsGatewayChange(e.target.value)}
error={ipfsGatewayError} error={this.state.ipfsGatewayError}
fullWidth fullWidth
margin="dense" margin="dense"
/> />
@ -508,8 +506,10 @@ export default class SecurityTab extends PureComponent {
data-testid="ipfs-gateway-resolution-container" data-testid="ipfs-gateway-resolution-container"
> >
<ToggleButton <ToggleButton
value={useAddressBarEnsResolution} value={this.props.useAddressBarEnsResolution}
onToggle={(value) => setUseAddressBarEnsResolution(!value)} onToggle={(value) =>
this.props.setUseAddressBarEnsResolution(!value)
}
offLabel={t('off')} offLabel={t('off')}
onLabel={t('on')} onLabel={t('on')}
/> />
@ -696,10 +696,10 @@ export default class SecurityTab extends PureComponent {
{t('displayNftMediaDescription')} {t('displayNftMediaDescription')}
</div> </div>
</div> </div>
<div className="settings-page__content-item">
<div <div
className="settings-page__content-item-col" className="settings-page__content-item-col"
data-testid="displayNftMedia" data-testid="enableOpenSeaAPI"
> >
<ToggleButton <ToggleButton
value={openSeaEnabled} value={openSeaEnabled}
@ -722,6 +722,7 @@ export default class SecurityTab extends PureComponent {
onLabel={t('on')} onLabel={t('on')}
/> />
</div> </div>
</div>
</Box> </Box>
); );
} }

View File

@ -1,8 +1,12 @@
import { fireEvent, queryByRole, screen } from '@testing-library/react'; import { fireEvent, queryByRole, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react'; import React from 'react';
import configureMockStore from 'redux-mock-store'; import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app';
import mockState from '../../../../test/data/mock-state.json'; import mockState from '../../../../test/data/mock-state.json';
import { tEn } from '../../../../test/lib/i18n-helpers';
import { renderWithProvider } from '../../../../test/lib/render-helpers'; import { renderWithProvider } from '../../../../test/lib/render-helpers';
import SecurityTab from './security-tab.container'; import SecurityTab from './security-tab.container';
@ -21,8 +25,10 @@ describe('Security Tab', () => {
const mockStore = configureMockStore([thunk])(mockState); const mockStore = configureMockStore([thunk])(mockState);
function toggleCheckbox(testId, initialState) { function toggleCheckbox(testId, initialState, skipRender = false) {
if (!skipRender) {
renderWithProvider(<SecurityTab />, mockStore); renderWithProvider(<SecurityTab />, mockStore);
}
const container = screen.getByTestId(testId); const container = screen.getByTestId(testId);
const checkbox = queryByRole(container, 'checkbox'); const checkbox = queryByRole(container, 'checkbox');
@ -46,12 +52,31 @@ describe('Security Tab', () => {
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
}); });
it('toggles Display NFT media enabled', async () => { it('toggles opensea api enabled off', async () => {
expect(await toggleCheckbox('displayNftMedia', false)).toBe(true); expect(await toggleCheckbox('enableOpenSeaAPI', true)).toBe(true);
});
it('toggles opensea api enabled on', async () => {
mockState.metamask.openSeaEnabled = false;
const localMockStore = configureMockStore([thunk])(mockState);
renderWithProvider(<SecurityTab />, localMockStore);
expect(await toggleCheckbox('enableOpenSeaAPI', false, true)).toBe(true);
}); });
it('toggles nft detection', async () => { it('toggles nft detection', async () => {
expect(await toggleCheckbox('useNftDetection', false)).toBe(true); expect(await toggleCheckbox('useNftDetection', true)).toBe(true);
});
it('toggles nft detection from another initial state', async () => {
mockState.metamask.openSeaEnabled = false;
mockState.metamask.useNftDetection = false;
const localMockStore = configureMockStore([thunk])(mockState);
renderWithProvider(<SecurityTab />, localMockStore);
expect(await toggleCheckbox('useNftDetection', false, true)).toBe(true);
}); });
it('toggles phishing detection', async () => { it('toggles phishing detection', async () => {
@ -103,4 +128,81 @@ describe('Security Tab', () => {
screen.queryByTestId(`srp_stage_introduction`), screen.queryByTestId(`srp_stage_introduction`),
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
}); });
it('sets IPFS gateway', async () => {
const user = userEvent.setup();
renderWithProvider(<SecurityTab />, mockStore);
const ipfsField = screen.getByDisplayValue(mockState.metamask.ipfsGateway);
await user.click(ipfsField);
await userEvent.clear(ipfsField);
expect(ipfsField).toHaveValue('');
expect(screen.queryByText(tEn('invalidIpfsGateway'))).toBeInTheDocument();
expect(
screen.queryByText(tEn('forbiddenIpfsGateway')),
).not.toBeInTheDocument();
await userEvent.type(ipfsField, 'https://');
expect(ipfsField).toHaveValue('https://');
expect(screen.queryByText(tEn('invalidIpfsGateway'))).toBeInTheDocument();
expect(
screen.queryByText(tEn('forbiddenIpfsGateway')),
).not.toBeInTheDocument();
await userEvent.type(ipfsField, '//');
expect(ipfsField).toHaveValue('https:////');
expect(screen.queryByText(tEn('invalidIpfsGateway'))).toBeInTheDocument();
expect(
screen.queryByText(tEn('forbiddenIpfsGateway')),
).not.toBeInTheDocument();
await userEvent.clear(ipfsField);
await userEvent.type(ipfsField, 'gateway.ipfs.io');
expect(ipfsField).toHaveValue('gateway.ipfs.io');
expect(
screen.queryByText(tEn('invalidIpfsGateway')),
).not.toBeInTheDocument();
expect(screen.queryByText(tEn('forbiddenIpfsGateway'))).toBeInTheDocument();
});
it('toggles IPFS gateway', async () => {
mockState.metamask.ipfsGateway = '';
const localMockStore = configureMockStore([thunk])(mockState);
renderWithProvider(<SecurityTab />, localMockStore);
expect(await toggleCheckbox('ipfsToggle', false, true)).toBe(true);
expect(await toggleCheckbox('ipfsToggle', true, true)).toBe(true);
});
it('toggles ENS domains in address bar', async () => {
expect(
await toggleCheckbox('ipfs-gateway-resolution-container', false),
).toBe(true);
});
it('clicks "Add Custom Network"', async () => {
const user = userEvent.setup();
renderWithProvider(<SecurityTab />, mockStore);
// Test the default path where `getEnvironmentType() === undefined`
await user.click(screen.getByText(tEn('addCustomNetwork')));
// Now force it down the path where `getEnvironmentType() === ENVIRONMENT_TYPE_POPUP`
jest
.mocked(getEnvironmentType)
.mockImplementationOnce(() => ENVIRONMENT_TYPE_POPUP);
global.platform = { openExtensionInBrowser: jest.fn() };
await user.click(screen.getByText(tEn('addCustomNetwork')));
expect(global.platform.openExtensionInBrowser).toHaveBeenCalled();
});
}); });

View File

@ -24494,7 +24494,6 @@ __metadata:
typescript: "npm:~4.4.0" typescript: "npm:~4.4.0"
unicode-confusables: "npm:^0.1.1" unicode-confusables: "npm:^0.1.1"
uuid: "npm:^8.3.2" uuid: "npm:^8.3.2"
valid-url: "npm:^1.0.9"
vinyl: "npm:^2.2.1" vinyl: "npm:^2.2.1"
vinyl-buffer: "npm:^1.0.1" vinyl-buffer: "npm:^1.0.1"
vinyl-source-stream: "npm:^2.0.0" vinyl-source-stream: "npm:^2.0.0"
@ -34288,13 +34287,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"valid-url@npm:^1.0.9":
version: 1.0.9
resolution: "valid-url@npm:1.0.9"
checksum: 343dfaf85eb3691dc8eb93f7bc007be1ee6091e6c6d1a68bf633cb85e4bf2930e34ca9214fb2c3330de5b652510b257a8ee1ff0a0a37df0925e9dabf93ee512d
languageName: node
linkType: hard
"validate-npm-package-license@npm:^3.0.1": "validate-npm-package-license@npm:^3.0.1":
version: 3.0.4 version: 3.0.4
resolution: "validate-npm-package-license@npm:3.0.4" resolution: "validate-npm-package-license@npm:3.0.4"