Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Clicking on report link does not add external_link_clicked to si… #29969

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 76 additions & 1 deletion test/integration/confirmations/signatures/permit.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import * as backgroundConnection from '../../../../ui/store/background-connectio
import { integrationTestRender } from '../../../lib/render-helpers';
import mockMetaMaskState from '../../data/integration-init-state.json';
import { createMockImplementation } from '../../helpers';
import { getMetaMaskStateWithUnapprovedPermitSign } from './signature-helpers';
import { tEn } from '../../../lib/i18n-helpers';
import {
getMetamaskStateWithMaliciousPermit,
getMetaMaskStateWithUnapprovedPermitSign,
} from './signature-helpers';

jest.mock('../../../../ui/store/background-connection', () => ({
...jest.requireActual('../../../../ui/store/background-connection'),
Expand Down Expand Up @@ -250,4 +254,75 @@ describe('Permit Confirmation', () => {
scope.done();
expect(scope.isDone()).toBe(true);
});

it('displays the malicious banner', async () => {
const account =
mockMetaMaskState.internalAccounts.accounts[
mockMetaMaskState.internalAccounts
.selectedAccount as keyof typeof mockMetaMaskState.internalAccounts.accounts
];

const mockedMetaMaskState = getMetamaskStateWithMaliciousPermit(
account.address,
);

await act(async () => {
await integrationTestRender({
preloadedState: mockedMetaMaskState,
backgroundConnection: backgroundConnectionMocked,
});
});

const headingText = tEn('blockaidTitleDeceptive') as string;
const bodyText = tEn('blockaidDescriptionApproveFarming') as string;
expect(await screen.findByText(headingText)).toBeInTheDocument();
expect(await screen.findByText(bodyText)).toBeInTheDocument();
});

it('tracks external link clicked property in signature rejected event', async () => {
const account =
mockMetaMaskState.internalAccounts.accounts[
mockMetaMaskState.internalAccounts
.selectedAccount as keyof typeof mockMetaMaskState.internalAccounts.accounts
];

const mockedMetaMaskState = getMetamaskStateWithMaliciousPermit(
account.address,
);

await act(async () => {
await integrationTestRender({
preloadedState: mockedMetaMaskState,
backgroundConnection: backgroundConnectionMocked,
});
});

fireEvent.click(await screen.findByTestId('disclosure'));
expect(
await screen.findByTestId('alert-provider-report-link'),
).toBeInTheDocument();

fireEvent.click(await screen.findByTestId('alert-provider-report-link'));

fireEvent.click(await screen.findByTestId('confirm-footer-cancel-button'));

let updateSignatureEventFragment;

await waitFor(() => {
updateSignatureEventFragment =
mockedBackgroundConnection.submitRequestToBackground.mock.calls?.find(
(call) =>
call[0] === 'updateEventFragment' &&
JSON.stringify(call[1]).includes(
JSON.stringify({
properties: {
external_link_clicked: 'security_alert_support_link',
},
}),
),
);

expect(updateSignatureEventFragment).toBeDefined();
});
Comment on lines +311 to +326
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to use Jest's matchers rather than relying on string comparisons?

Suggested change
await waitFor(() => {
updateSignatureEventFragment =
mockedBackgroundConnection.submitRequestToBackground.mock.calls?.find(
(call) =>
call[0] === 'updateEventFragment' &&
JSON.stringify(call[1]).includes(
JSON.stringify({
properties: {
external_link_clicked: 'security_alert_support_link',
},
}),
),
);
expect(updateSignatureEventFragment).toBeDefined();
});
expect(
mockedBackgroundConnection.submitRequestToBackground,
).toHaveBeenCalledWith(
'updateEventFragment',
expect.arrayContaining([
expect.objectContaining({
properties: expect.objectContaining({
external_link_clicked: 'security_alert_support_link',
}),
}),
]),
);

});
});
45 changes: 45 additions & 0 deletions test/integration/confirmations/signatures/signature-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,51 @@ export const getMetaMaskStateWithUnapprovedPermitSign = (
};
};

export const getMetamaskStateWithMaliciousPermit = (accountAddress: string) => {
const pendingPermitId = '48a75190-45ca-11ef-9001-f3886ec2397c';
Comment on lines +94 to +95
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor nit] we can move this out to avoid reinstantiating for each getMetamaskStateWithMaliciousPermit call

Suggested change
export const getMetamaskStateWithMaliciousPermit = (accountAddress: string) => {
const pendingPermitId = '48a75190-45ca-11ef-9001-f3886ec2397c';
const pendingPermitId = '48a75190-45ca-11ef-9001-f3886ec2397c';
export const getMetamaskStateWithMaliciousPermit = (accountAddress: string) => {


const state = getMetaMaskStateWithUnapprovedPermitSign(
accountAddress,
'Permit',
);
const unapprovedTypedMessage = {
[pendingPermitId]: {
...state.unapprovedTypedMessages[pendingPermitId],
securityAlertResponse: {
block: 7596565,
result_type: 'Malicious',
reason: 'permit_farming',
description:
'permit_farming to spender 0x1661f1b207629e4f385da89cff535c8e5eb23ee3, classification: A known malicious address is involved in the transaction',
features: ['A known malicious address is involved in the transaction'],
source: 'api',
securityAlertId: 'ba944b14-aa65-45b5-ae92-f305cdba64c1',
},
},
};

state.unapprovedTypedMessages = {
...unapprovedTypedMessage,
};

console.log(state);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
console.log(state);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guessing this is forgotten

return {
...state,
signatureSecurityAlertResponses: {
'ba944b14-aa65-45b5-ae92-f305cdba64c1': {
block: 7596565,
result_type: 'Malicious',
reason: 'permit_farming',
description:
'permit_farming to spender 0x1661f1b207629e4f385da89cff535c8e5eb23ee3, classification: A known malicious address is involved in the transaction',
features: ['A known malicious address is involved in the transaction'],
source: 'api',
securityAlertId: 'ba944b14-aa65-45b5-ae92-f305cdba64c1',
},
},
};
};

export const verifyDetails = (element: Element, expectedValues: string[]) => {
expectedValues.forEach((value) => {
expect(element).toHaveTextContent(value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -491,4 +491,52 @@ describe('Contract Interaction Confirmation', () => {
expect(await screen.findByText(headingText)).toBeInTheDocument();
expect(await screen.findByText(bodyText)).toBeInTheDocument();
});

it('tracks external link clicked in transaction metrics', async () => {
const account =
mockMetaMaskState.internalAccounts.accounts[
mockMetaMaskState.internalAccounts
.selectedAccount as keyof typeof mockMetaMaskState.internalAccounts.accounts
];

const mockedMetaMaskState =
getMetaMaskStateWithMaliciousUnapprovedContractInteraction(
account.address,
);

await act(async () => {
await integrationTestRender({
preloadedState: mockedMetaMaskState,
backgroundConnection: backgroundConnectionMocked,
});
});

fireEvent.click(await screen.findByTestId('disclosure'));
expect(
await screen.findByTestId('alert-provider-report-link'),
).toBeInTheDocument();

fireEvent.click(await screen.findByTestId('alert-provider-report-link'));

fireEvent.click(await screen.findByTestId('confirm-footer-cancel-button'));

let updateTransactionEventFragment;

await waitFor(() => {
updateTransactionEventFragment =
mockedBackgroundConnection.submitRequestToBackground.mock.calls?.find(
(call) =>
call[0] === 'updateEventFragment' &&
JSON.stringify(call[1]).includes(
JSON.stringify({
properties: {
external_link_clicked: 'security_alert_support_link',
},
}),
),
);

expect(updateTransactionEventFragment).toBeDefined();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ function ReportLink({
<Text marginTop={1} display={Display.Flex}>
{t('somethingDoesntLookRight', [
<ButtonLink
data-testid="alert-provider-report-link"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We avoid adding data-testid in extension, in case its possible to query using button text it may be better.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this suggestion. I do like using findByText to also test text expectations simultaneously

key={`security-provider-button-supporturl-${provider}`}
size={ButtonLinkSize.Inherit}
href={reportUrl ?? ZENDESK_URLS.SUPPORT_URL}
Expand Down
15 changes: 15 additions & 0 deletions ui/pages/confirmations/components/confirm/title/title.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,29 @@ import { useIsNFT } from '../info/approve/hooks/use-is-nft';
import { useTokenTransactionData } from '../info/hooks/useTokenTransactionData';
import { getIsRevokeSetApprovalForAll } from '../info/utils';
import { getIsRevokeDAIPermit } from '../utils';
import { useSignatureEventFragment } from '../../../hooks/useSignatureEventFragment';
import { useTransactionEventFragment } from '../../../hooks/useTransactionEventFragment';
import { useCurrentSpendingCap } from './hooks/useCurrentSpendingCap';

function ConfirmBannerAlert({ ownerId }: { ownerId: string }) {
const { generalAlerts } = useAlerts(ownerId);
const { updateSignatureEventFragment } = useSignatureEventFragment();
const { updateTransactionEventFragment } = useTransactionEventFragment();

if (generalAlerts.length === 0) {
return null;
}

const onClickSupportLink = () => {
const properties = {
properties: {
external_link_clicked: 'security_alert_support_link',
},
};
updateSignatureEventFragment(properties);
updateTransactionEventFragment(properties, ownerId);
};

Comment on lines +39 to +48
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we create this callback with useCallback if it has ownerId dependency?

Suggested change
const onClickSupportLink = () => {
const properties = {
properties: {
external_link_clicked: 'security_alert_support_link',
},
};
updateSignatureEventFragment(properties);
updateTransactionEventFragment(properties, ownerId);
};
const onClickSupportLink = useCallback(() => {
const properties = {
properties: {
external_link_clicked: 'security_alert_support_link',
},
};
updateSignatureEventFragment(properties);
updateTransactionEventFragment(properties, ownerId);
}, [ownerId]);

return (
<Box marginTop={3}>
{generalAlerts.map((alert) => (
Expand All @@ -45,6 +59,7 @@ function ConfirmBannerAlert({ ownerId }: { ownerId: string }) {
details={alert.alertDetails}
reportUrl={alert.reportUrl}
children={alert.content}
onClickSupportLink={onClickSupportLink}
/>
</Box>
))}
Expand Down
Loading