Skip to content

Commit be0221d

Browse files
committed
fix(spam-protection): CHECKOUT-4852 Make sure spam protection execution status is accurate
1 parent 36435c7 commit be0221d

File tree

6 files changed

+64
-78
lines changed

6 files changed

+64
-78
lines changed

src/checkout/checkout-service.spec.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,8 +1118,6 @@ describe('CheckoutService', () => {
11181118
it('executes spam check', async () => {
11191119
await checkoutService.executeSpamCheck();
11201120

1121-
expect(spamProtectionActionCreator.initialize)
1122-
.toHaveBeenCalled();
11231121
expect(spamProtectionActionCreator.execute)
11241122
.toHaveBeenCalled();
11251123
});

src/checkout/checkout-service.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,14 +1151,9 @@ export default class CheckoutService {
11511151
* @returns A promise that resolves to the current state.
11521152
*/
11531153
executeSpamCheck(): Promise<CheckoutSelectors> {
1154-
const action = this._spamProtectionActionCreator.initialize();
1154+
const action = this._spamProtectionActionCreator.execute();
11551155

1156-
return this._dispatch(action, { queueId: 'spamProtection' })
1157-
.then(() => {
1158-
const action = this._spamProtectionActionCreator.execute();
1159-
1160-
return this._dispatch(action, { queueId: 'spamProtection' });
1161-
});
1156+
return this._dispatch(action, { queueId: 'spamProtection' });
11621157
}
11631158

11641159
/**

src/spam-protection/google-recaptcha.spec.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,12 @@ describe('GoogleRecaptcha', () => {
102102
}
103103
});
104104

105-
it('throws an error if google recaptcha is not initialized', () => {
106-
expect(() => googleRecaptcha.execute()).toThrow(NotInitializedError);
105+
it('throws an error if google recaptcha is not initialized', async () => {
106+
try {
107+
await googleRecaptcha.execute().toPromise();
108+
} catch (error) {
109+
expect(error).toBeInstanceOf(NotInitializedError);
110+
}
107111
});
108112

109113
it('execute google recaptcha', async () => {

src/spam-protection/google-recaptcha.ts

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import { NotInitializedError, NotInitializedErrorType } from '../common/error/er
88
import { SpamProtectionChallengeNotCompletedError, SpamProtectionFailedError, SpamProtectionNotLoadedError } from './errors';
99
import GoogleRecaptchaScriptLoader from './google-recaptcha-script-loader';
1010

11+
const TIMEOUT = 7000;
12+
const RETRY_INTERVAL = 250;
13+
const MAX_RETRIES = TIMEOUT / RETRY_INTERVAL;
14+
1115
export interface RecaptchaResult {
1216
error?: Error;
1317
token?: string;
@@ -59,39 +63,35 @@ export default class GoogleRecaptcha {
5963
}
6064

6165
execute(): Observable<RecaptchaResult> {
62-
const event$ = this._event$;
63-
const recaptcha = this._recaptcha;
64-
65-
if (!event$ || !recaptcha) {
66-
throw new NotInitializedError(NotInitializedErrorType.SpamProtectionNotInitialized);
67-
}
66+
return defer(() => {
67+
const event$ = this._event$;
68+
const recaptcha = this._recaptcha;
6869

69-
const timeout = 7000;
70-
const retryInterval = 250;
71-
const maxRetries = timeout / retryInterval;
70+
if (!event$ || !recaptcha) {
71+
throw new NotInitializedError(NotInitializedErrorType.SpamProtectionNotInitialized);
72+
}
7273

73-
return defer(() => {
74-
const element = document.querySelector('iframe[src*="bframe"]');
75-
76-
return element ?
77-
of(element) :
78-
throwError(new SpamProtectionNotLoadedError());
79-
})
80-
.pipe(
81-
retryWhen(errors => errors.pipe(
82-
delay(retryInterval),
83-
switchMap((error, index) =>
84-
index < maxRetries ? of(error) : throwError(error)
85-
)
86-
)),
87-
switchMap(element => {
88-
this._watchRecaptchaChallengeWindow(event$, element);
89-
recaptcha.execute();
90-
91-
return event$;
92-
}),
93-
catchError(error => of({ error }))
94-
);
74+
return defer(() => {
75+
const element = document.querySelector('iframe[src*="bframe"]');
76+
77+
return element ? of(element) : throwError(new SpamProtectionNotLoadedError());
78+
})
79+
.pipe(
80+
retryWhen(errors => errors.pipe(
81+
delay(RETRY_INTERVAL),
82+
switchMap((error, index) =>
83+
index < MAX_RETRIES ? of(error) : throwError(error)
84+
)
85+
)),
86+
switchMap(element => {
87+
this._watchRecaptchaChallengeWindow(event$, element);
88+
recaptcha.execute();
89+
90+
return event$;
91+
}),
92+
catchError(error => of({ error }))
93+
);
94+
});
9595
}
9696

9797
private _watchRecaptchaChallengeWindow(event: Subject<RecaptchaResult>, element: Element) {

src/spam-protection/spam-protection-action-creator.spec.ts

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { createRequestSender, Response } from '@bigcommerce/request-sender';
22
import { ScriptLoader } from '@bigcommerce/script-loader';
3-
import { from, of, Subject } from 'rxjs';
3+
import { merge } from 'lodash';
4+
import { from, of, ReplaySubject, Subject } from 'rxjs';
45
import { catchError, toArray } from 'rxjs/operators';
56

67
import { createCheckoutStore, CheckoutStore, CheckoutStoreState } from '../checkout';
7-
import { getCheckout, getCheckoutState, getCheckoutStoreState } from '../checkout/checkouts.mock';
8+
import { getCheckout, getCheckoutStoreState } from '../checkout/checkouts.mock';
89
import { getResponse } from '../common/http-request/responses.mock';
910

1011
import createSpamProtection from './create-spam-protection';
@@ -34,7 +35,7 @@ describe('SpamProtectionActionCreator', () => {
3435
spamProtectionRequestSender
3536
);
3637
jest.spyOn(googleRecaptcha, 'load').mockReturnValue(Promise.resolve());
37-
$event = new Subject<RecaptchaResult>();
38+
$event = new ReplaySubject<RecaptchaResult>();
3839
jest.spyOn(googleRecaptcha, 'execute').mockReturnValue($event);
3940
jest.spyOn(spamProtectionRequestSender, 'validate').mockReturnValue(Promise.resolve(response));
4041
});
@@ -96,32 +97,24 @@ describe('SpamProtectionActionCreator', () => {
9697

9798
describe('#execute()', () => {
9899
it('emits actions if able to execute spam check', async () => {
99-
const checkout = getCheckout();
100-
checkout.shouldExecuteSpamCheck = true;
101-
const checkoutState = getCheckoutState();
102-
checkoutState.data = checkout;
103-
const checkoutStoreState = {
104-
...getCheckoutStoreState(),
105-
checkout: checkoutState,
106-
};
107-
const state = {
108-
...checkoutStoreState,
109-
order: {
110-
errors: {},
111-
meta: {},
112-
statuses: {},
100+
const store = createCheckoutStore(merge({}, getCheckoutStoreState(), {
101+
checkout: {
102+
data: {
103+
shouldExecuteSpamCheck: true,
104+
},
113105
},
114-
};
115-
const store = createCheckoutStore(state);
106+
}));
116107

117-
const actions = from(spamProtectionActionCreator.execute()(store))
108+
$event.next({ token: 'spamProtectionToken' });
109+
110+
const actions = await from(spamProtectionActionCreator.execute()(store))
118111
.pipe(toArray())
119112
.toPromise();
120113

121-
$event.next({ token: 'spamProtectionToken' });
122-
123-
expect(await actions).toEqual([
114+
expect(actions).toEqual([
124115
{ type: SpamProtectionActionType.ExecuteRequested },
116+
{ type: SpamProtectionActionType.InitializeRequested },
117+
{ type: SpamProtectionActionType.InitializeSucceeded },
125118
{ type: SpamProtectionActionType.ExecuteSucceeded, payload: response.body },
126119
]);
127120
});

src/spam-protection/spam-protection-action-creator.ts

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -49,35 +49,31 @@ export default class SpamProtectionActionCreator {
4949
}
5050

5151
execute(): ThunkAction<SpamProtectionAction, InternalCheckoutSelectors> {
52-
return store => {
53-
const state = store.getState();
54-
const checkout = state.checkout.getCheckout();
55-
56-
if (!checkout) {
57-
throw new MissingDataError(MissingDataErrorType.MissingCheckout);
58-
}
59-
60-
const { shouldExecuteSpamCheck } = checkout;
52+
return store => defer(() => {
53+
const { checkout } = store.getState();
54+
const { id: checkoutId, shouldExecuteSpamCheck } = checkout.getCheckoutOrThrow();
6155

6256
if (!shouldExecuteSpamCheck) {
6357
return empty();
6458
}
6559

6660
return concat(
6761
of(createAction(SpamProtectionActionType.ExecuteRequested, undefined)),
62+
this.initialize()(store),
6863
this._googleRecaptcha.execute()
6964
.pipe(take(1))
70-
.pipe(switchMap(({ error, token }) => {
65+
.pipe(switchMap(async ({ error, token }) => {
7166
if (error || !token) {
7267
throw new SpamProtectionFailedError();
7368
}
7469

75-
return this._requestSender.validate(checkout.id, token)
76-
.then(({ body }) => createAction(SpamProtectionActionType.ExecuteSucceeded, body));
70+
const { body } = await this._requestSender.validate(checkoutId, token);
71+
72+
return createAction(SpamProtectionActionType.ExecuteSucceeded, body);
7773
}))
7874
).pipe(
7975
catchError(error => throwErrorAction(SpamProtectionActionType.ExecuteFailed, error))
8076
);
81-
};
77+
});
8278
}
8379
}

0 commit comments

Comments
 (0)