Skip to content

Commit

Permalink
fix(JOB-192) : call several times octopod when there are more than fi…
Browse files Browse the repository at this point in the history
…fty projects
  • Loading branch information
Pierre Trollé committed Nov 30, 2017
1 parent a45116f commit 36abb56
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 106 deletions.
19 changes: 18 additions & 1 deletion server/src/domain/services/job-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,27 @@ const mailService = require('./mail-service'); // A service should not be depend

const CACHE_KEY = 'get_jobs';

function _isStatusWantedOnJobBoard(project) {
return project.status === 'mission_signed'
|| project.status === 'mission_accepted'
|| project.status === 'proposal_sent';
}

function _isKindOfProjectWantedOnJobBoard(project) {
return project.kind === 'cost_reimbursable' || project.kind === 'fixed_price';
}

function filterProjectByStatusAndKind(projects) {
return projects
.filter(project => _isStatusWantedOnJobBoard(project))
.filter(project => _isKindOfProjectWantedOnJobBoard(project));
}

async function _fetchAndCacheJobs() {
const accessToken = await octopodClient.getAccessToken();
const projects = await octopodClient.fetchProjectsToBeStaffed(accessToken);
const activities = await octopodClient.fetchActivitiesToBeStaffed(accessToken, projects);
const filterProjects = filterProjectByStatusAndKind(projects);
const activities = await octopodClient.fetchActivitiesToBeStaffed(accessToken, filterProjects);
const jobs = await jobsSerializer.serialize(projects, activities);
cache.set(CACHE_KEY, jobs);

Expand Down
24 changes: 10 additions & 14 deletions server/src/infrastructure/octopod.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,19 @@ const OctopodClient = {
});
},

_isStatusWantedOnJobBoard(project) {
return project.status === 'mission_signed'
|| project.status === 'mission_accepted'
|| project.status === 'proposal_sent';
async fetchProjectsToBeStaffed(accessToken, projects = [], page = 1) {
if (projects.length === 100 * (page - 1)) {
const moreProjects = await this.fetchProjectsToBeStaffedPerPage(accessToken, page);
const allProjects = projects.concat(moreProjects);
return this.fetchProjectsToBeStaffed(accessToken, allProjects, page + 1);
}
return projects;
},

_isKindOfProjectWantedOnJobBoard(project) {
return project.kind === 'cost_reimbursable' || project.kind === 'fixed_price';
},

fetchProjectsToBeStaffed(accessToken) {
fetchProjectsToBeStaffedPerPage(accessToken, page) {
return new Promise((resolve, reject) => {
const options = {
url: `${config.OCTOPOD_API_URL}/v0/projects?staffing_needed=true&page=1&per_page=50`,
url: `${config.OCTOPOD_API_URL}/v0/projects?staffing_needed=true&page=${page}&per_page=100`,
headers: {
Authorization: `Bearer ${accessToken}`,
},
Expand All @@ -53,10 +52,7 @@ const OctopodClient = {
reject(err);
}
const projects = JSON.parse(response.body);
const filteredProject = projects
.filter(this._isStatusWantedOnJobBoard)
.filter(this._isKindOfProjectWantedOnJobBoard);
resolve(filteredProject);
resolve(projects);
});
});
},
Expand Down
62 changes: 58 additions & 4 deletions server/test/unit/domain/services/job-service.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const jobsSerializer = require('../../../../src/infrastructure/serializers/jobs'
const cache = require('../../../../src/infrastructure/cache');
const { Subscription } = require('../../../../src/domain/models');
const mailService = require('../../../../src/domain/services/mail-service');
const projectFromOctopod = require('../../fixtures/projectFromOctopod');

const CACHE_KEY = 'get_jobs';

Expand Down Expand Up @@ -90,9 +91,57 @@ describe('Unit | Service | job-service', () => {
expect(octopodClient.fetchProjectsToBeStaffed).to.have.been.calledWith(stubbedAccessToken);
}));

it('should call Octopod to fetch activities to be staffed', () => promise.then(() => {
expect(octopodClient.fetchActivitiesToBeStaffed).to.have.been.calledWith(stubbedAccessToken, stubbedFetchedProjects);
}));
it('should call Octopod to fetch activities to be staffed with filtered projects - only proposal sent and mission accepted and signed', () => {
// given
octopodClient.fetchProjectsToBeStaffed.restore();

const projectsFromOctopod = [
projectFromOctopod('proposal_sent'),
projectFromOctopod('proposal_in_progress'),
projectFromOctopod('lead'),
projectFromOctopod('mission_accepted'),
projectFromOctopod('mission_signed'),
projectFromOctopod('mission_done'),
projectFromOctopod('proposal_lost'),
projectFromOctopod('proposal_canceled_by_client'),
projectFromOctopod('proposal_no_go'),
];
const expectedProjectsFromOctopod = [
projectFromOctopod('proposal_sent'),
projectFromOctopod('mission_accepted'),
projectFromOctopod('mission_signed'),
];
sinon.stub(octopodClient, 'fetchProjectsToBeStaffed').resolves(projectsFromOctopod);

// when
promise.then(() => {
// then
expect(octopodClient.fetchActivitiesToBeStaffed).to.have.been.calledWith(stubbedAccessToken, expectedProjectsFromOctopod);
});
});

it('should call Octopod to fetch activities to be staffed with filtered project - only "Regie (cost_reimbursable) and Forfait (fixed price)"', () => {
// given
octopodClient.fetchProjectsToBeStaffed.restore();
const projectsFromOctopod = [
projectFromOctopod('mission_signed', 'internal'),
projectFromOctopod('mission_signed', 'cost_reimbursable'),
projectFromOctopod('mission_signed', 'fixed_price'),
projectFromOctopod('mission_signed', 'framework_agreement'),
];

const expectedProjectsFromOctopod = [
projectFromOctopod('mission_signed', 'cost_reimbursable'),
projectFromOctopod('mission_signed', 'fixed_price'),
];
sinon.stub(octopodClient, 'fetchProjectsToBeStaffed').resolves(projectsFromOctopod);

// when
promise.then(() => {
// then
expect(octopodClient.fetchActivitiesToBeStaffed).to.have.been.calledWith(stubbedAccessToken, expectedProjectsFromOctopod);
});
});

it('should build jobs by merging fetched projects and activities', () => promise.then(() => {
expect(jobsSerializer.serialize).to.have.been.calledWith(stubbedFetchedProjects, stubbedFetchedActivities);
Expand Down Expand Up @@ -167,7 +216,12 @@ describe('Unit | Service | job-service', () => {

// then
return promise.then((report) => {
expect(report).to.deep.equal({ isInit: false, hasChanges: true, removedJobs: expectedRemovedJobs, addedJobs: expectedAddedJobs });
expect(report).to.deep.equal({
isInit: false,
hasChanges: true,
removedJobs: expectedRemovedJobs,
addedJobs: expectedAddedJobs,
});
});
});
});
Expand Down
133 changes: 46 additions & 87 deletions server/test/unit/infrastructure/octopod.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const request = require('request');
const OctopodClient = require('../../../src/infrastructure/octopod');
const projectFromOctopod = require('./fixtures/projectFromOctopod');
const projectFromOctopod = require('../fixtures/projectFromOctopod');
const { expect, sinon } = require('../../test-helper');

describe('Unit | Utils | octopod-client', () => {
Expand All @@ -14,11 +14,6 @@ describe('Unit | Utils | octopod-client', () => {
request.get.restore();
});

/**
* #getAccessToken
* ---------------
*/

describe('#getAccessToken', () => {
describe('with a successful request', () => {
beforeEach(() => {
Expand Down Expand Up @@ -89,59 +84,36 @@ describe('Unit | Utils | octopod-client', () => {
});
});

/**
* #fetchProjectsToBeStaffed
* -------------------------
*/

describe('#fetchProjectsToBeStaffed', () => {
let projectsFromOctopod;

describe('in any cases', () => {
beforeEach(() => {
request.get.callsFake((options, callback) => {
projectsFromOctopod = [
projectFromOctopod('proposal_sent'),
projectFromOctopod('proposal_in_progress'),
projectFromOctopod('lead'),
projectFromOctopod('mission_accepted'),
projectFromOctopod('mission_signed'),
projectFromOctopod('mission_done'),
projectFromOctopod('proposal_lost'),
projectFromOctopod('proposal_canceled_by_client'),
projectFromOctopod('proposal_no_go'),
];
const httpResponse = {
body: JSON.stringify(projectsFromOctopod),
};
callback(null, httpResponse);
});
});
describe('#fetchProjectsToBeStaffedPerPage', () => {
beforeEach(() => {
sinon.stub(OctopodClient, 'fetchProjectsToBeStaffedPerPage');
});
afterEach(() => {
OctopodClient.fetchProjectsToBeStaffedPerPage.restore();
});

it('should call Octopod API "GET /projects"', () => {
describe('when there are more than 100 projects', () => {
it('should call fetchProjectsToBeStaffedPerPage several times', () => {
// given
const accessToken = 'access-token';
const hundredOfProjects = new Array(100).fill(projectFromOctopod('proposal_sent'));
const someProjects = new Array(33).fill(projectFromOctopod('proposal_sent'));
OctopodClient.fetchProjectsToBeStaffedPerPage.onFirstCall().resolves(hundredOfProjects);
OctopodClient.fetchProjectsToBeStaffedPerPage.onSecondCall().resolves(someProjects);

// when
const promise = OctopodClient.fetchProjectsToBeStaffed(accessToken);

// then
return promise.then(() => {
const expectedOptions = {
url: 'http://octopod.url/api/v0/projects?staffing_needed=true&page=1&per_page=50',
headers: {
Authorization: 'Bearer access-token',
},
};
expect(request.get).to.have.been.calledWith(expectedOptions);
return promise.then((returnedProjects) => {
expect(OctopodClient.fetchProjectsToBeStaffedPerPage).to.have.been.calledTwice;
expect(returnedProjects.length).to.equal(133);
});
});

it('should return a rejected promise when the call to Octopod API fails', () => {
it('should return a rejected promise when the call to fetchProjectsToBeStaffedPerPage fails', () => {
// given
request.get.callsFake((options, callback) => {
callback(new Error('some error'));
});
OctopodClient.fetchProjectsToBeStaffedPerPage.rejects(new Error('some error'));

// when
const promise = OctopodClient.fetchProjectsToBeStaffed();
Expand All @@ -153,8 +125,13 @@ describe('Unit | Utils | octopod-client', () => {
});
});
});
});


describe('#fetchProjectsToBeStaffedPerPage', () => {
let projectsFromOctopod;

describe('with different projects status from octopod', () => {
describe('in any cases', () => {
beforeEach(() => {
request.get.callsFake((options, callback) => {
projectsFromOctopod = [
Expand All @@ -175,61 +152,43 @@ describe('Unit | Utils | octopod-client', () => {
});
});

it('should return "mission and proposal sent" projects in a promise', () => {
it('should call Octopod API "GET /projects"', () => {
// given
const accessToken = 'access-token';

// when
const promise = OctopodClient.fetchProjectsToBeStaffed();
const promise = OctopodClient.fetchProjectsToBeStaffedPerPage(accessToken, 8);

// then
return promise
.then((projects) => {
const expectedProjectsFromOctopod = [
projectFromOctopod('proposal_sent'),
projectFromOctopod('mission_accepted'),
projectFromOctopod('mission_signed'),
];
expect(projects).to.deep.equal(expectedProjectsFromOctopod);
return promise.then(() => {
const expectedOptions = page => ({
url: `http://octopod.url/api/v0/projects?staffing_needed=true&page=${page}&per_page=100`,
headers: {
Authorization: 'Bearer access-token',
},
});
expect(request.get).to.have.been.calledWith(expectedOptions(8));
});
});
});

describe('with different kind of projects from octopod', () => {
beforeEach(() => {
it('should return a rejected promise when the call to Octopod API fails', () => {
// given
request.get.callsFake((options, callback) => {
projectsFromOctopod = [
projectFromOctopod('mission_signed', 'internal'),
projectFromOctopod('mission_signed', 'cost_reimbursable'),
projectFromOctopod('mission_signed', 'fixed_price'),
projectFromOctopod('mission_signed', 'framework_agreement'),
];
const httpResponse = {
body: JSON.stringify(projectsFromOctopod),
};
callback(null, httpResponse);
callback(new Error('some error'));
});
});

it('should return only "Regie (cost_reimbursable) and Forfait (fixed price)" in a promise', () => {
// when
const promise = OctopodClient.fetchProjectsToBeStaffed();
const promise = OctopodClient.fetchProjectsToBeStaffedPerPage();

// then
return promise
.then((projects) => {
const expectedProjectsFromOctopod = [
projectFromOctopod('mission_signed', 'cost_reimbursable'),
projectFromOctopod('mission_signed', 'fixed_price'),
];
expect(projects).to.deep.equal(expectedProjectsFromOctopod);
});
return promise.catch((err) => {
expect(err).to.exist;
expect(err.message).to.equal('some error');
});
});
});
});

/**
* #fetchActivitiesToBeStaffed
* ---------------------------
*/

describe('#fetchActivitiesToBeStaffed', () => {
describe('when get succeeds', () => {
const accessToken = 'access-token';
Expand Down

0 comments on commit 36abb56

Please sign in to comment.