Skip to content
This repository has been archived by the owner on Apr 16, 2024. It is now read-only.

Commit

Permalink
Bypass Recaptcha for automated testing (#262)
Browse files Browse the repository at this point in the history
* bypass captcha logic and separate recaptcha implemented

* Add license header

* Add enableRecaptcha flag to e2e config

* More config fixes

* Changes to the context data attribute

* chore: refactor use of e2eTestMode flag and add to context

* chore: cleanup after PR review

* chore: move mode flag to camel case

---------

Co-authored-by: Robert Bo Davis <[email protected]>
  • Loading branch information
mythilytm and robert-bo-davis authored Jul 31, 2023
1 parent 97f56d6 commit 646d436
Show file tree
Hide file tree
Showing 9 changed files with 89 additions and 40 deletions.
4 changes: 2 additions & 2 deletions assets/e2e-chat.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@

<html>
<head>
<title>Automated Ends To End Testing Chat - No Humans Allowed!</title>
<title>Automated End To End Testing Chat - No Humans Allowed!</title>
</head>
<body>
<script src="./aselo-chat.min.js"></script>
<script src="./aselo-chat.min.js" data-e2e-test-mode="true"></script>
</body>
</html>
2 changes: 1 addition & 1 deletion configurations/ca-staging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2320,5 +2320,5 @@ export const config: Configuration = {
captureIp,
contactType,
blockedEmojis,
enableRecaptcha
enableRecaptcha,
};
13 changes: 12 additions & 1 deletion configurations/e2e-development.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const flexFlowSid = 'FOfce25fd1dff726dcdae2899de86de6c5';
const defaultLanguage = 'en-US';
const captureIp = true;
const contactType: ContactType = 'ip';
const enableRecaptcha = false;

const translations: Translations = {
'en-US': {
Expand All @@ -43,7 +44,15 @@ const translations: Translations = {
const preEngagementConfig: PreEngagementFormDefinition = {
description: 'PreEngagementDescription',
submitLabel: 'StartChat',
fields: [],
fields: [
{
type: 'input-text',
name: 'nickname',
label: 'LabelNickname',
placeholder: 'Guest',
required: true,
},
],
};

const memberDisplayOptions = {
Expand All @@ -60,6 +69,7 @@ const mapHelplineLanguage: MapHelplineLanguage = (helpline) => {
}
};

// eslint-disable-next-line import/no-unused-modules
export const config: Configuration = {
accountSid,
flexFlowSid,
Expand All @@ -70,4 +80,5 @@ export const config: Configuration = {
memberDisplayOptions,
captureIp,
contactType,
enableRecaptcha,
};
10 changes: 8 additions & 2 deletions src/aselo-webchat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const getCurrentConfig = (): Configuration => {
};

const currentConfig = getCurrentConfig();
const { externalWebChatLanguage, color, backgroundColor } = getWebChatAttributeValues();
const { externalWebChatLanguage, color, backgroundColor, e2eTestMode } = getWebChatAttributeValues();

const { defaultLanguage, translations } = currentConfig;
const initialLanguage = defaultLanguage;
Expand Down Expand Up @@ -121,6 +121,7 @@ export const initWebchat = async () => {
preEngagementConfig: PLACEHOLDER_PRE_ENGAGEMENT_CONFIG,
context: {
ip,
e2eTestMode,
},
colorTheme: {
overrides: {
Expand Down Expand Up @@ -213,7 +214,12 @@ export const initWebchat = async () => {

// Replace pre engagement form
FlexWebChat.PreEngagementCanvas.Content.replace(
<PreEngagementForm key="pre-engagement" manager={manager} enableRecaptcha={currentConfig.enableRecaptcha} />,
<PreEngagementForm
key="pre-engagement"
manager={manager}
enableRecaptcha={currentConfig.enableRecaptcha}
bypassCaptcha={e2eTestMode}
/>,
);

// Render WebChat
Expand Down
5 changes: 4 additions & 1 deletion src/dom-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,8 @@ export function getWebChatAttributeValues() {
const color = document?.currentScript?.getAttribute('data-color');
const backgroundColor = document?.currentScript?.getAttribute('data-background-color');

return { externalWebChatLanguage, color, backgroundColor };
// used to turn on/off captcha and send messages to correct queue
const e2eTestMode = document?.currentScript?.getAttribute('data-e2e-test-mode') === 'true';

return { externalWebChatLanguage, color, backgroundColor, e2eTestMode };
}
2 changes: 1 addition & 1 deletion src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@
</head>
<body>
<div style="position:fixed; left:0; right:0; bottom:0; height:123px; background-color:#261E3C;z-index:999999;"></div>
<script data-z-index="16000160" src="./bundle.js"></script>
<script data-z-index="16000160" src="./bundle.js" data-e2e-test-mode="true"></script>
</body>
</html>
51 changes: 51 additions & 0 deletions src/pre-engagement-form/ReCaptcha/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Copyright (C) 2021-2023 Technology Matters
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/

import React, { useRef } from 'react';
import ReCAPTCHA from 'react-google-recaptcha';

import { RECAPTCHA_KEY } from '../../../private/secret';
import { validateUser } from './recaptchaValidation';

type Props = {
bypassCaptcha: boolean | undefined;
onRecaptchaChange: (verified: boolean) => void;
};

const ReCaptcha: React.FC<Props> = ({ bypassCaptcha, onRecaptchaChange }) => {
const recaptchaRef = useRef<ReCAPTCHA>(null);

const onChange = (token: string | null) => {
const verified = token !== null;
onRecaptchaChange(verified);

if (verified) {
try {
validateUser(token ?? '');
} catch (error) {
console.log(error);
}
}
};

if (bypassCaptcha) {
return null;
}

return <ReCAPTCHA sitekey={RECAPTCHA_KEY} size="normal" ref={recaptchaRef} onChange={onChange} />;
};

export default ReCaptcha;
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
export async function validateUser(token: string) {
try {
// eslint-disable-next-line global-require
const { RECAPTCHA_VERIFY_URL } = require('../../private/secret');
const { RECAPTCHA_VERIFY_URL } = require('../../../private/secret');
const response = await fetch(RECAPTCHA_VERIFY_URL, {
method: 'POST',
headers: {
Expand Down
40 changes: 9 additions & 31 deletions src/pre-engagement-form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import React, { useRef, useState } from 'react';
import { connect } from 'react-redux';
import { useForm, FormProvider } from 'react-hook-form';
import * as FlexWebChat from '@twilio/flex-webchat-ui';
import ReCAPTCHA from 'react-google-recaptcha';

import { AseloWebchatState } from '../aselo-webchat-state';
import type { PreEngagementForm as PreEngagementFormDefinition } from './form-components/types';
Expand All @@ -30,8 +29,7 @@ import Title from './form-components/title';
import { resetForm } from './state';
import { PLACEHOLDER_PRE_ENGAGEMENT_CONFIG } from './placeholder-form';
import { overrideLanguageOnContext } from '../language';
import { RECAPTCHA_KEY } from '../../private/secret';
import { validateUser } from './recaptchaValidation';
import ReCaptcha from './ReCaptcha';

export { PreEngagementFormDefinition, PLACEHOLDER_PRE_ENGAGEMENT_CONFIG };

Expand All @@ -40,6 +38,7 @@ export const EMAIL_PATTERN = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i;
type Props = {
manager: FlexWebChat.Manager;
enableRecaptcha?: boolean;
bypassCaptcha?: boolean;
} & ReturnType<typeof mapStateToProps> &
typeof mapDispatchToProps;

Expand All @@ -50,16 +49,14 @@ const PreEngagementForm: React.FC<Props> = ({
resetFormAction,
enableRecaptcha,
friendlyName,
bypassCaptcha,
}) => {
const methods = useForm({ defaultValues, mode: 'onChange' });
const { handleSubmit, formState } = methods;
const { isValid } = formState;

// State to keep track of whether the Recaptcha has been verified
const [isRecaptchaVerified, setIsRecaptchaVerified] = useState<boolean>(false);

const recaptchaRef = useRef<ReCAPTCHA>(null);

// Function that overrides the friendlyName value on context
const overrideFriendlyNameOnContext = () => {
const appConfig = manager.configuration;
Expand All @@ -75,11 +72,6 @@ const PreEngagementForm: React.FC<Props> = ({
manager.updateConfig(updateConfig);
};

// Handler function for when the Recaptcha token changes
const onChange = (token: string | null) => {
setIsRecaptchaVerified(token !== null); // reflect whether the token is null or not - a valid sitekey will return a token along with recaptchaRef
};

const onSubmit = handleSubmit(async (data) => {
const payload = { formData: data };

Expand All @@ -99,21 +91,9 @@ const PreEngagementForm: React.FC<Props> = ({
overrideFriendlyNameOnContext();
}

if (enableRecaptcha) {
try {
const token: string = recaptchaRef?.current?.getValue() ?? '';
validateUser(token);
// If the token is valid, submit the form payload and reset the form
await FlexWebChat.Actions.invokeAction('StartEngagement', payload);
resetFormAction();
} catch (error) {
console.log(error);
}
} else {
// when enableRecaptcha is not set
await FlexWebChat.Actions.invokeAction('StartEngagement', payload);
resetFormAction();
}
// when enableRecaptcha is not set
await FlexWebChat.Actions.invokeAction('StartEngagement', payload);
resetFormAction();
});

if (formDefinition === undefined) {
Expand All @@ -126,14 +106,12 @@ const PreEngagementForm: React.FC<Props> = ({
<form className="Twilio-DynamicForm" onSubmit={onSubmit}>
<Title title={formDefinition.description} />
{generateForm(formDefinition.fields)}
{enableRecaptcha && (
<ReCAPTCHA sitekey={RECAPTCHA_KEY} size="normal" ref={recaptchaRef} onChange={onChange} />
)}
{enableRecaptcha && <ReCaptcha bypassCaptcha={bypassCaptcha} onRecaptchaChange={setIsRecaptchaVerified} />}
{formDefinition.submitLabel && (
<SubmitButton
label={formDefinition.submitLabel}
// disabled prop set to true if the form is invalid or the Recaptcha is not verified
disabled={!isValid || ((enableRecaptcha ?? false) && !isRecaptchaVerified)}
// disabled prop set to true if the form is invalid or the Recaptcha is not verified or bypassCaptcha
disabled={!isValid || ((enableRecaptcha ?? false) && !isRecaptchaVerified && !bypassCaptcha)}
/>
)}
</form>
Expand Down

0 comments on commit 646d436

Please sign in to comment.