Skip to content
Merged
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
109 changes: 109 additions & 0 deletions __tests__/shared/services/recruitCRM.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/* eslint-env jest */
import fetch from 'isomorphic-fetch';
import { logger } from 'topcoder-react-lib';
import Service from '../../../src/shared/services/recruitCRM';

jest.mock('isomorphic-fetch', () => jest.fn());
jest.mock('topcoder-react-lib', () => ({
logger: {
error: jest.fn(),
},
}));

describe('RecruitCRM application service', () => {
const originalFormData = global.FormData;
const originalHeaders = global.Headers;
let formData;

beforeEach(() => {
formData = { append: jest.fn() };
global.FormData = jest.fn(() => formData);
global.Headers = jest.fn(headers => headers);
fetch.mockReset();
logger.error.mockReset();
});

afterAll(() => {
global.FormData = originalFormData;
global.Headers = originalHeaders;
});

it('returns the JSON application result for a successful proxy response', async () => {
const json = jest.fn().mockResolvedValue({ id: 'application-id' });
fetch.mockResolvedValue({
ok: true,
status: 200,
headers: { get: jest.fn(() => 'application/json; charset=utf-8') },
json,
});

const result = await new Service().applyForJob(
'job-slug',
{ resume: 'resume-file', first_name: 'Ada' },
'token-v3',
);

expect(result).toEqual({ id: 'application-id' });
expect(formData.append).toHaveBeenNthCalledWith(1, 'resume', 'resume-file');
expect(formData.append).toHaveBeenNthCalledWith(2, 'form', JSON.stringify({ first_name: 'Ada' }));
});

it('rejects an HTML edge error without trying to parse it as JSON', async () => {
const json = jest.fn();
fetch.mockResolvedValue({
ok: false,
status: 404,
headers: { get: jest.fn(() => 'text/html; charset=utf-8') },
json,
});

await expect(new Service().applyForJob(
'job-slug',
{ resume: 'resume-file' },
'token-v3',
)).rejects.toMatchObject({
message: "We couldn't submit your application. Please try again.",
status: 404,
});
expect(json).not.toHaveBeenCalled();
expect(logger.error).toHaveBeenCalledTimes(1);
});

it('rejects a successful response when its body is not JSON', async () => {
const json = jest.fn();
fetch.mockResolvedValue({
ok: true,
status: 200,
headers: { get: jest.fn(() => 'text/html') },
json,
});

await expect(new Service().applyForJob(
'job-slug',
{ resume: 'resume-file' },
'token-v3',
)).rejects.toMatchObject({
message: "We couldn't submit your application. Please try again.",
status: 200,
});
expect(json).not.toHaveBeenCalled();
});

it('replaces JSON parser details with the safe application error', async () => {
fetch.mockResolvedValue({
ok: true,
status: 200,
headers: { get: jest.fn(() => 'application/json') },
json: jest.fn().mockRejectedValue(new SyntaxError("Unexpected token '<'")),
});

await expect(new Service().applyForJob(
'job-slug',
{ resume: 'resume-file' },
'token-v3',
)).rejects.toMatchObject({
message: "We couldn't submit your application. Please try again.",
status: 200,
});
});
});
29 changes: 22 additions & 7 deletions src/shared/services/recruitCRM.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,12 @@ export default class Service {
}

/**
* applyForJob for candidate
* @param {string} id The job id to apply to
* @param {object} payload The apply payload
* @param {string} tokenV3 User token
* Submits a candidate's application and optional resume through the RecruitCRM proxy.
* @param {string} id The job ID to apply to.
* @param {object} payload The normalized application payload.
* @param {string} tokenV3 The user's v3 authentication token.
* @returns {Promise<object>} The parsed application response.
* @throws {Error} When the proxy rejects the request or returns a non-JSON or malformed response.
*/
async applyForJob(id, payload, tokenV3) {
const { resume } = payload;
Expand All @@ -90,11 +92,24 @@ export default class Service {
}),
credentials: 'omit',
});
if (!res.ok) {
const error = new Error('Failed to apply for job');
const contentType = res.headers && res.headers.get
? res.headers.get('content-type') : '';
const isJson = typeof contentType === 'string'
&& /^application\/(?:[a-z0-9.+-]+\+)?json(?:\s*;|$)/i.test(contentType.trim());
if (!res.ok || !isJson) {
const error = new Error("We couldn't submit your application. Please try again.");
error.status = res.status;
logger.error(error, res);
throw error;
}
try {
return await res.json();
} catch (parseError) {
const error = new Error("We couldn't submit your application. Please try again.");
error.status = res.status;
logger.error(error, parseError);
throw error;
}
return res.json();
}

/**
Expand Down
Loading