Commit d3d72aa0 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch 'leipert-add-apollo-captcha-link' into 'master'

Create Apollo Link for Captcha handling

See merge request gitlab-org/gitlab!56879
parents 31e966bd bf0b2381
import { ApolloLink, Observable } from 'apollo-link';
export const apolloCaptchaLink = new ApolloLink((operation, forward) =>
forward(operation).flatMap((result) => {
const { errors = [] } = result;
// Our API will return with a top-level GraphQL error with extensions
// in case a captcha is required.
const captchaError = errors.find((e) => e?.extensions?.needs_captcha_response);
if (captchaError) {
const captchaSiteKey = captchaError.extensions.captcha_site_key;
const spamLogId = captchaError.extensions.spam_log_id;
return new Observable((observer) => {
import('~/captcha/wait_for_captcha_to_be_solved')
.then(({ waitForCaptchaToBeSolved }) => waitForCaptchaToBeSolved(captchaSiteKey))
.then((captchaResponse) => {
// If the captcha was solved correctly, we re-do our action while setting
// captcha response headers.
operation.setContext({
headers: {
'X-GitLab-Captcha-Response': captchaResponse,
'X-GitLab-Spam-Log-Id': spamLogId,
},
});
forward(operation).subscribe(observer);
})
.catch((error) => {
observer.error(error);
observer.complete();
});
});
}
return Observable.of(result);
}),
);
import { ApolloLink, Observable } from 'apollo-link';
import { apolloCaptchaLink } from '~/captcha/apollo_captcha_link';
import UnsolvedCaptchaError from '~/captcha/unsolved_captcha_error';
import { waitForCaptchaToBeSolved } from '~/captcha/wait_for_captcha_to_be_solved';
jest.mock('~/captcha/wait_for_captcha_to_be_solved');
describe('apolloCaptchaLink', () => {
const SPAM_LOG_ID = 'SPAM_LOG_ID';
const CAPTCHA_SITE_KEY = 'CAPTCHA_SITE_KEY';
const CAPTCHA_RESPONSE = 'CAPTCHA_RESPONSE';
const SUCCESS_RESPONSE = {
data: {
user: {
id: 3,
name: 'foo',
},
},
errors: [],
};
const NON_CAPTCHA_ERROR_RESPONSE = {
data: {
user: null,
},
errors: [
{
message: 'Something is severely wrong with your query.',
path: ['user'],
locations: [{ line: 2, column: 3 }],
extensions: {
message: 'Object not found',
type: 2,
},
},
],
};
const SPAM_ERROR_RESPONSE = {
data: {
user: null,
},
errors: [
{
message: 'Your Query was detected to be SPAM.',
path: ['user'],
locations: [{ line: 2, column: 3 }],
extensions: {
spam: true,
},
},
],
};
const CAPTCHA_ERROR_RESPONSE = {
data: {
user: null,
},
errors: [
{
message: 'This is an unrelated error, captcha should still work despite this.',
path: ['user'],
locations: [{ line: 2, column: 3 }],
},
{
message: 'You need to solve a Captcha.',
path: ['user'],
locations: [{ line: 2, column: 3 }],
extensions: {
spam: true,
needs_captcha_response: true,
captcha_site_key: CAPTCHA_SITE_KEY,
spam_log_id: SPAM_LOG_ID,
},
},
],
};
let link;
let mockLinkImplementation;
let mockContext;
const setupLink = (...responses) => {
mockLinkImplementation = jest.fn().mockImplementation(() => {
return Observable.of(responses.shift());
});
link = ApolloLink.from([apolloCaptchaLink, new ApolloLink(mockLinkImplementation)]);
};
function mockOperation() {
mockContext = jest.fn();
return { operationName: 'operation', variables: {}, setContext: mockContext };
}
it('successful responses are passed through', (done) => {
setupLink(SUCCESS_RESPONSE);
link.request(mockOperation()).subscribe((result) => {
expect(result).toEqual(SUCCESS_RESPONSE);
expect(mockLinkImplementation).toHaveBeenCalledTimes(1);
expect(waitForCaptchaToBeSolved).not.toHaveBeenCalled();
done();
});
});
it('non-spam related errors are passed through', (done) => {
setupLink(NON_CAPTCHA_ERROR_RESPONSE);
link.request(mockOperation()).subscribe((result) => {
expect(result).toEqual(NON_CAPTCHA_ERROR_RESPONSE);
expect(mockLinkImplementation).toHaveBeenCalledTimes(1);
expect(mockContext).not.toHaveBeenCalled();
expect(waitForCaptchaToBeSolved).not.toHaveBeenCalled();
done();
});
});
it('unresolvable SPAM errors are passed through', (done) => {
setupLink(SPAM_ERROR_RESPONSE);
link.request(mockOperation()).subscribe((result) => {
expect(result).toEqual(SPAM_ERROR_RESPONSE);
expect(mockLinkImplementation).toHaveBeenCalledTimes(1);
expect(mockContext).not.toHaveBeenCalled();
expect(waitForCaptchaToBeSolved).not.toHaveBeenCalled();
done();
});
});
describe('resolvable SPAM errors', () => {
it('re-submits request with SPAM headers if the captcha modal was solved correctly', (done) => {
waitForCaptchaToBeSolved.mockResolvedValue(CAPTCHA_RESPONSE);
setupLink(CAPTCHA_ERROR_RESPONSE, SUCCESS_RESPONSE);
link.request(mockOperation()).subscribe((result) => {
expect(result).toEqual(SUCCESS_RESPONSE);
expect(waitForCaptchaToBeSolved).toHaveBeenCalledWith(CAPTCHA_SITE_KEY);
expect(mockContext).toHaveBeenCalledWith({
headers: {
'X-GitLab-Captcha-Response': CAPTCHA_RESPONSE,
'X-GitLab-Spam-Log-Id': SPAM_LOG_ID,
},
});
expect(mockLinkImplementation).toHaveBeenCalledTimes(2);
done();
});
});
it('throws error if the captcha modal was not solved correctly', (done) => {
const error = new UnsolvedCaptchaError();
waitForCaptchaToBeSolved.mockRejectedValue(error);
setupLink(CAPTCHA_ERROR_RESPONSE, SUCCESS_RESPONSE);
link.request(mockOperation()).subscribe({
next: done.catch,
error: (result) => {
expect(result).toEqual(error);
expect(waitForCaptchaToBeSolved).toHaveBeenCalledWith(CAPTCHA_SITE_KEY);
expect(mockContext).not.toHaveBeenCalled();
expect(mockLinkImplementation).toHaveBeenCalledTimes(1);
done();
},
});
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment