From b8fae5309f1d8d2b9b2396904b0a192d94159ece Mon Sep 17 00:00:00 2001 From: Alexandre COIN Date: Fri, 4 Oct 2024 10:50:43 +0200 Subject: [PATCH 1/3] feature(api): deactivate cpf planner job with env variable --- api/sample.env | 6 + .../jobs/cpf-export-planner-job-controller.js | 4 + api/src/shared/config.js | 3 + .../cpf-export-planner-job-controller_test.js | 120 +++++++++++------- 4 files changed, 86 insertions(+), 47 deletions(-) diff --git a/api/sample.env b/api/sample.env index aee3614c971..11c68d8fcdb 100644 --- a/api/sample.env +++ b/api/sample.env @@ -863,6 +863,12 @@ TEST_REDIS_URL=redis://localhost:6379 # default: 3 # sample: CPF_PLANNER_JOB_MINIMUM_RELIABILITY_PERIOD=3 +# Toggle of the cpf planner job +# +# presence: optional +# type: string +# sample: PGBOSS_PLANNER_JOB_ENABLED=true + # Cron of the cpf planner job # # presence: required for CPF xml file generation, optional otherwise diff --git a/api/src/certification/session-management/application/jobs/cpf-export-planner-job-controller.js b/api/src/certification/session-management/application/jobs/cpf-export-planner-job-controller.js index c5b135da0c7..0c215e0380f 100644 --- a/api/src/certification/session-management/application/jobs/cpf-export-planner-job-controller.js +++ b/api/src/certification/session-management/application/jobs/cpf-export-planner-job-controller.js @@ -19,6 +19,10 @@ class CpfExportPlannerJobController extends JobScheduleController { super('CpfExportPlannerJob', { jobCron: config.cpf.plannerJob.cron }); } + get isJobEnabled() { + return config.pgBoss.plannerJobEnabled; + } + async handle({ jobId, dependencies = { cpfCertificationResultRepository, cpfExportBuilderJobRepository, logger } }) { const startDate = dayjs() .utc() diff --git a/api/src/shared/config.js b/api/src/shared/config.js index b8109aededc..c790471aee8 100644 --- a/api/src/shared/config.js +++ b/api/src/shared/config.js @@ -298,6 +298,9 @@ const configuration = (function () { importFileJobEnabled: process.env.PGBOSS_IMPORT_FILE_JOB_ENABLED ? toBoolean(process.env.PGBOSS_IMPORT_FILE_JOB_ENABLED) : true, + plannerJobEnabled: process.env.PGBOSS_PLANNER_JOB_ENABLED + ? toBoolean(process.env.PGBOSS_PLANNER_JOB_ENABLED) + : true, }, poleEmploi: { clientId: process.env.POLE_EMPLOI_CLIENT_ID, diff --git a/api/tests/certification/session-management/unit/application/jobs/cpf-export-planner-job-controller_test.js b/api/tests/certification/session-management/unit/application/jobs/cpf-export-planner-job-controller_test.js index bdad64a9e54..5d85b2b7cb1 100644 --- a/api/tests/certification/session-management/unit/application/jobs/cpf-export-planner-job-controller_test.js +++ b/api/tests/certification/session-management/unit/application/jobs/cpf-export-planner-job-controller_test.js @@ -27,56 +27,82 @@ describe('Unit | Application | Certification | Sessions Management | jobs | cpf- }; }); - it('should send to CpfExportBuilderJob chunks of certification course ids', async function () { - // given - const jobId = '237584-7648'; - const logger = { info: noop }; - sinon.stub(cpf.plannerJob, 'chunkSize').value(2); - sinon.stub(cpf.plannerJob, 'monthsToProcess').value(2); - sinon.stub(cpf.plannerJob, 'minimumReliabilityPeriod').value(2); - - const startDate = dayjs().utc().subtract(3, 'months').startOf('month').toDate(); - const endDate = dayjs().utc().subtract(2, 'months').endOf('month').toDate(); - - cpfCertificationResultRepository.countExportableCertificationCoursesByTimeRange.resolves(5); - - // when - const jobController = new CpfExportPlannerJobController(); - await jobController.handle({ - jobId, - dependencies: { cpfCertificationResultRepository, cpfExportBuilderJobRepository, logger }, - }); + describe('#isJobEnabled', function () { + it('return true when job is enabled', function () { + //given + sinon.stub(config.pgBoss, 'plannerJobEnabled').value(true); + + // when + const handler = new CpfExportPlannerJobController(); - // then - expect(cpfCertificationResultRepository.markCertificationToExport).to.have.been.callCount(3); - expect(cpfCertificationResultRepository.markCertificationToExport.getCall(0)).to.have.been.calledWithExactly({ - startDate, - endDate, - limit: 2, - offset: 0, - batchId: '237584-7648#0', + // then + expect(handler.isJobEnabled).to.be.true; }); - expect(cpfCertificationResultRepository.markCertificationToExport.getCall(1)).to.have.been.calledWithExactly({ - startDate, - endDate, - limit: 2, - offset: 2, - batchId: '237584-7648#1', + + it('return false when job is disabled', function () { + //given + sinon.stub(config.pgBoss, 'plannerJobEnabled').value(false); + + //when + const handler = new CpfExportPlannerJobController(); + + //then + expect(handler.isJobEnabled).to.be.false; }); - expect(cpfCertificationResultRepository.markCertificationToExport.getCall(2)).to.have.been.calledWithExactly({ - startDate, - endDate, - limit: 2, - offset: 4, - batchId: '237584-7648#2', + }); + + describe('#handle', function () { + it('should send to CpfExportBuilderJob chunks of certification course ids', async function () { + // given + const jobId = '237584-7648'; + const logger = { info: noop }; + sinon.stub(cpf.plannerJob, 'chunkSize').value(2); + sinon.stub(cpf.plannerJob, 'monthsToProcess').value(2); + sinon.stub(cpf.plannerJob, 'minimumReliabilityPeriod').value(2); + + const startDate = dayjs().utc().subtract(3, 'months').startOf('month').toDate(); + const endDate = dayjs().utc().subtract(2, 'months').endOf('month').toDate(); + + cpfCertificationResultRepository.countExportableCertificationCoursesByTimeRange.resolves(5); + + // when + const jobController = new CpfExportPlannerJobController(); + await jobController.handle({ + jobId, + dependencies: { cpfCertificationResultRepository, cpfExportBuilderJobRepository, logger }, + }); + + // then + expect(cpfCertificationResultRepository.markCertificationToExport).to.have.been.callCount(3); + expect(cpfCertificationResultRepository.markCertificationToExport.getCall(0)).to.have.been.calledWithExactly({ + startDate, + endDate, + limit: 2, + offset: 0, + batchId: '237584-7648#0', + }); + expect(cpfCertificationResultRepository.markCertificationToExport.getCall(1)).to.have.been.calledWithExactly({ + startDate, + endDate, + limit: 2, + offset: 2, + batchId: '237584-7648#1', + }); + expect(cpfCertificationResultRepository.markCertificationToExport.getCall(2)).to.have.been.calledWithExactly({ + startDate, + endDate, + limit: 2, + offset: 4, + batchId: '237584-7648#2', + }); + expect( + cpfCertificationResultRepository.countExportableCertificationCoursesByTimeRange, + ).to.have.been.calledWithExactly({ startDate, endDate }); + expect(cpfExportBuilderJobRepository.performAsync).to.have.been.calledOnceWith( + new CpfExportBuilderJob({ batchId: '237584-7648#0' }), + new CpfExportBuilderJob({ batchId: '237584-7648#1' }), + new CpfExportBuilderJob({ batchId: '237584-7648#2' }), + ); }); - expect( - cpfCertificationResultRepository.countExportableCertificationCoursesByTimeRange, - ).to.have.been.calledWithExactly({ startDate, endDate }); - expect(cpfExportBuilderJobRepository.performAsync).to.have.been.calledOnceWith( - new CpfExportBuilderJob({ batchId: '237584-7648#0' }), - new CpfExportBuilderJob({ batchId: '237584-7648#1' }), - new CpfExportBuilderJob({ batchId: '237584-7648#2' }), - ); }); }); From a796b4dc9bf4f9d0b1169e24d591082726e28b02 Mon Sep 17 00:00:00 2001 From: Alexandre COIN Date: Fri, 4 Oct 2024 10:54:41 +0200 Subject: [PATCH 2/3] feature(api): deactivate cpf export sender job with env variable --- api/sample.env | 6 ++ .../jobs/cpf-export-sender-job-controller.js | 4 + api/src/shared/config.js | 3 + .../cpf-export-sender-job-controller_test.js | 81 ++++++++++++------- 4 files changed, 67 insertions(+), 27 deletions(-) diff --git a/api/sample.env b/api/sample.env index 11c68d8fcdb..5d6e1211df2 100644 --- a/api/sample.env +++ b/api/sample.env @@ -881,6 +881,12 @@ TEST_REDIS_URL=redis://localhost:6379 # type: string # sample:CPF_SEND_EMAIL_JOB_RECIPIENT= +# Toggle of the cpf email job +# +# presence: optional +# type: string +# sample: PGBOSS_EXPORT_SENDER_JOB_ENABLED=true + # Cron of the cpf email job # # presence: required for CPF xml file generation, optional otherwise diff --git a/api/src/certification/session-management/application/jobs/cpf-export-sender-job-controller.js b/api/src/certification/session-management/application/jobs/cpf-export-sender-job-controller.js index 27a3fb616f8..8e03b777d33 100644 --- a/api/src/certification/session-management/application/jobs/cpf-export-sender-job-controller.js +++ b/api/src/certification/session-management/application/jobs/cpf-export-sender-job-controller.js @@ -11,6 +11,10 @@ class CpfExportSenderJobController extends JobScheduleController { super('CpfExportSenderJob', { jobCron: config.cpf.sendEmailJob.cron }); } + get isJobEnabled() { + return config.pgBoss.exportSenderJobEnabled; + } + async handle({ dependencies = { mailService } }) { const generatedFiles = await usecases.getPreSignedUrls(); diff --git a/api/src/shared/config.js b/api/src/shared/config.js index c790471aee8..044bf1f6acf 100644 --- a/api/src/shared/config.js +++ b/api/src/shared/config.js @@ -301,6 +301,9 @@ const configuration = (function () { plannerJobEnabled: process.env.PGBOSS_PLANNER_JOB_ENABLED ? toBoolean(process.env.PGBOSS_PLANNER_JOB_ENABLED) : true, + exportSenderJobEnabled: process.env.PGBOSS_EXPORT_SENDER_JOB_ENABLED + ? toBoolean(process.env.PGBOSS_EXPORT_SENDER_JOB_ENABLED) + : true, }, poleEmploi: { clientId: process.env.POLE_EMPLOI_CLIENT_ID, diff --git a/api/tests/certification/session-management/unit/application/jobs/cpf-export-sender-job-controller_test.js b/api/tests/certification/session-management/unit/application/jobs/cpf-export-sender-job-controller_test.js index c447b47eb35..5f9f4588315 100644 --- a/api/tests/certification/session-management/unit/application/jobs/cpf-export-sender-job-controller_test.js +++ b/api/tests/certification/session-management/unit/application/jobs/cpf-export-sender-job-controller_test.js @@ -1,5 +1,6 @@ import { CpfExportSenderJobController } from '../../../../../../src/certification/session-management/application/jobs/cpf-export-sender-job-controller.js'; import { usecases } from '../../../../../../src/certification/session-management/domain/usecases/index.js'; +import { config } from '../../../../../../src/shared/config.js'; import { logger } from '../../../../../../src/shared/infrastructure/utils/logger.js'; import { expect, sinon } from '../../../../../test-helper.js'; @@ -11,45 +12,71 @@ describe('Unit | Application | Certification | Sessions Management | jobs | cpf- mailService = { sendCpfEmail: sinon.stub() }; }); - describe('when generated files are found', function () { - it('should send an email with a list of generated files url', async function () { - // given - usecases.getPreSignedUrls = sinon.stub(); - usecases.getPreSignedUrls.resolves([ - 'https://bucket.url.com/file1.xml', - 'https://bucket.url.com/file2.xml', - 'https://bucket.url.com/file3.xml', - ]); + describe('#isJobEnabled', function () { + it('return true when job is enabled', function () { + //given + sinon.stub(config.pgBoss, 'exportSenderJobEnabled').value(true); // when - const jobController = new CpfExportSenderJobController(); - await jobController.handle({ dependencies: { mailService } }); + const handler = new CpfExportSenderJobController(); // then - expect(mailService.sendCpfEmail).to.have.been.calledWithExactly({ - email: 'team-all-star-certif-de-ouf@example.net', - generatedFiles: [ + expect(handler.isJobEnabled).to.be.true; + }); + + it('return false when job is disabled', function () { + //given + sinon.stub(config.pgBoss, 'exportSenderJobEnabled').value(false); + + //when + const handler = new CpfExportSenderJobController(); + + //then + expect(handler.isJobEnabled).to.be.false; + }); + }); + + describe('#handle', function () { + describe('when generated files are found', function () { + it('should send an email with a list of generated files url', async function () { + // given + usecases.getPreSignedUrls = sinon.stub(); + usecases.getPreSignedUrls.resolves([ 'https://bucket.url.com/file1.xml', 'https://bucket.url.com/file2.xml', 'https://bucket.url.com/file3.xml', - ], + ]); + + // when + const jobController = new CpfExportSenderJobController(); + await jobController.handle({ dependencies: { mailService } }); + + // then + expect(mailService.sendCpfEmail).to.have.been.calledWithExactly({ + email: 'team-all-star-certif-de-ouf@example.net', + generatedFiles: [ + 'https://bucket.url.com/file1.xml', + 'https://bucket.url.com/file2.xml', + 'https://bucket.url.com/file3.xml', + ], + }); }); }); - }); - describe('when no generated file is found', function () { - it('should not send an email', async function () { - // given - usecases.getPreSignedUrls = sinon.stub(); - usecases.getPreSignedUrls.resolves([]); + describe('when no generated file is found', function () { + it('should not send an email', async function () { + // given + usecases.getPreSignedUrls = sinon.stub(); + usecases.getPreSignedUrls.resolves([]); - // when - const jobController = new CpfExportSenderJobController(); - await jobController.handle({ dependencies: { mailService } }); + // when + const jobController = new CpfExportSenderJobController(); + await jobController.handle({ dependencies: { mailService } }); - // then - expect(mailService.sendCpfEmail).to.not.have.been.called; - expect(logger.info).to.have.been.calledWithExactly(`No CPF exports files ready to send`); + // then + expect(mailService.sendCpfEmail).to.not.have.been.called; + expect(logger.info).to.have.been.calledWithExactly(`No CPF exports files ready to send`); + }); }); }); }); From e5f47305effa10938879956654d541c991e896bc Mon Sep 17 00:00:00 2001 From: Steph0 Date: Fri, 4 Oct 2024 12:02:48 +0200 Subject: [PATCH 3/3] :sparkles: api: unschedule old cron job when disabled --- api/src/shared/config.js | 1 + api/tests/unit/worker_test.js | 19 +++++++++++++++++++ api/worker.js | 6 ++++++ 3 files changed, 26 insertions(+) diff --git a/api/src/shared/config.js b/api/src/shared/config.js index 044bf1f6acf..d5cbf116c92 100644 --- a/api/src/shared/config.js +++ b/api/src/shared/config.js @@ -490,6 +490,7 @@ const configuration = (function () { config.cpf.sendEmailJob = { recipient: 'team-all-star-certif-de-ouf@example.net', + cron: '0 3 * * *', }; config.jwtConfig.livretScolaire = { secret: 'secretosmose', tokenLifespan: '1h' }; diff --git a/api/tests/unit/worker_test.js b/api/tests/unit/worker_test.js index 5ecc885d8bf..c879f93ae4c 100644 --- a/api/tests/unit/worker_test.js +++ b/api/tests/unit/worker_test.js @@ -157,5 +157,24 @@ describe('#registerJobs', function () { 'legyNameForScheduleComputeOrganizationLearnersCertificabilityJobController', ); }); + + context('when a cron job is disabled', function () { + it('unschedule the job', async function () { + //given + sinon.stub(config.cpf.sendEmailJob, 'cron').value('0 21 * * *'); + sinon.stub(config.pgBoss, 'exportSenderJobEnabled').value(false); + + await registerJobs({ + jobGroup: JobGroup.DEFAULT, + dependencies: { + startPgBoss: startPgBossStub, + createJobQueues: createJobQueuesStub, + }, + }); + + // then + expect(jobQueueStub.unscheduleCronJob).to.have.been.calledWithExactly('CpfExportSenderJob'); + }); + }); }); }); diff --git a/api/worker.js b/api/worker.js index 727019cec84..f7f46a725d0 100644 --- a/api/worker.js +++ b/api/worker.js @@ -113,6 +113,12 @@ export async function registerJobs({ jobGroup, dependencies = { startPgBoss, cre } } else { logger.warn(`Job "${job.jobName}" is disabled.`); + + // For cronJob we need to unschedule older cron + if (job.jobCron) { + await jobQueues.unscheduleCronJob(job.jobName); + logger.info(`Job CRON "${job.jobName}" is unscheduled.`); + } } }