import MockAdapter from 'axios-mock-adapter'; import { registerCaptchaModalInterceptor } from '~/captcha/captcha_modal_axios_interceptor'; import UnsolvedCaptchaError from '~/captcha/unsolved_captcha_error'; import { waitForCaptchaToBeSolved } from '~/captcha/wait_for_captcha_to_be_solved'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_CONFLICT, HTTP_STATUS_METHOD_NOT_ALLOWED, HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK, } from '~/lib/utils/http_status'; jest.mock('~/captcha/wait_for_captcha_to_be_solved'); describe('registerCaptchaModalInterceptor', () => { const SPAM_LOG_ID = 'SPAM_LOG_ID'; const CAPTCHA_SITE_KEY = 'CAPTCHA_SITE_KEY'; const CAPTCHA_SUCCESS = 'CAPTCHA_SUCCESS'; const CAPTCHA_RESPONSE = 'CAPTCHA_RESPONSE'; const AXIOS_RESPONSE = { text: 'AXIOS_RESPONSE' }; const NEEDS_CAPTCHA_RESPONSE = { needs_captcha_response: true, captcha_site_key: CAPTCHA_SITE_KEY, spam_log_id: SPAM_LOG_ID, }; const unsupportedMethods = ['delete', 'get', 'head', 'options']; const supportedMethods = ['patch', 'post', 'put']; let mock; beforeEach(() => { waitForCaptchaToBeSolved.mockRejectedValue(new UnsolvedCaptchaError()); mock = new MockAdapter(axios); mock.onAny('/endpoint-without-captcha').reply(HTTP_STATUS_OK, AXIOS_RESPONSE); mock.onAny('/endpoint-with-unrelated-error').reply(HTTP_STATUS_NOT_FOUND, AXIOS_RESPONSE); mock.onAny('/endpoint-with-captcha').reply((config) => { if (!supportedMethods.includes(config.method)) { return [HTTP_STATUS_METHOD_NOT_ALLOWED, { method: config.method }]; } const data = JSON.parse(config.data); const { 'X-GitLab-Captcha-Response': captchaResponse, 'X-GitLab-Spam-Log-Id': spamLogId, } = config.headers; if (captchaResponse === CAPTCHA_RESPONSE && spamLogId === SPAM_LOG_ID) { return [HTTP_STATUS_OK, { ...data, method: config.method, CAPTCHA_SUCCESS }]; } return [HTTP_STATUS_CONFLICT, NEEDS_CAPTCHA_RESPONSE]; }); axios.interceptors.response.handlers = []; registerCaptchaModalInterceptor(axios); }); afterEach(() => { mock.restore(); }); describe.each([...supportedMethods, ...unsupportedMethods])('For HTTP method %s', (method) => { it('successful requests are passed through', async () => { const { data, status } = await axios[method]('/endpoint-without-captcha'); expect(status).toEqual(HTTP_STATUS_OK); expect(data).toEqual(AXIOS_RESPONSE); expect(mock.history[method]).toHaveLength(1); }); it('error requests without needs_captcha_response_errors are passed through', async () => { await expect(() => axios[method]('/endpoint-with-unrelated-error')).rejects.toThrow( expect.objectContaining({ response: expect.objectContaining({ status: HTTP_STATUS_NOT_FOUND, data: AXIOS_RESPONSE, }), }), ); expect(mock.history[method]).toHaveLength(1); }); }); describe.each(supportedMethods)('For HTTP method %s', (method) => { describe('error requests with needs_captcha_response_errors', () => { const submittedData = { ID: 12345 }; const submittedHeaders = { 'Submitted-Header': 67890 }; it('re-submits request if captcha was solved correctly', async () => { waitForCaptchaToBeSolved.mockResolvedValueOnce(CAPTCHA_RESPONSE); const axiosResponse = await axios[method]('/endpoint-with-captcha', submittedData, { headers: submittedHeaders, }); const { data: returnedData, config: { headers: returnedHeaders }, } = axiosResponse; expect(waitForCaptchaToBeSolved).toHaveBeenCalledWith(CAPTCHA_SITE_KEY); expect(returnedData).toEqual({ ...submittedData, CAPTCHA_SUCCESS, method }); expect(returnedHeaders).toEqual( expect.objectContaining({ ...submittedHeaders, 'X-GitLab-Captcha-Response': CAPTCHA_RESPONSE, 'X-GitLab-Spam-Log-Id': SPAM_LOG_ID, }), ); expect(mock.history[method]).toHaveLength(2); }); it('does not re-submit request if captcha was not solved', async () => { await expect(() => axios[method]('/endpoint-with-captcha', submittedData)).rejects.toThrow( new UnsolvedCaptchaError(), ); expect(waitForCaptchaToBeSolved).toHaveBeenCalledWith(CAPTCHA_SITE_KEY); expect(mock.history[method]).toHaveLength(1); }); }); }); describe.each(unsupportedMethods)('For HTTP method %s', (method) => { it('ignores captcha response', async () => { await expect(() => axios[method]('/endpoint-with-captcha')).rejects.toThrow( expect.objectContaining({ response: expect.objectContaining({ status: HTTP_STATUS_METHOD_NOT_ALLOWED, data: { method }, }), }), ); expect(waitForCaptchaToBeSolved).not.toHaveBeenCalled(); expect(mock.history[method]).toHaveLength(1); }); }); });