From 5ecabfb131a1956bfcd78a50c5416445299dce65 Mon Sep 17 00:00:00 2001 From: Alexandre COIN Date: Tue, 12 Nov 2024 12:36:14 +0100 Subject: [PATCH 1/3] fix(mon-pix): prevent answering question when ongoing or validated alert exits --- mon-pix/app/components/challenge/item.js | 4 ++++ .../challenge/challenge-item-test.js | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/mon-pix/app/components/challenge/item.js b/mon-pix/app/components/challenge/item.js index 7fd742d739a..e75425f56e2 100644 --- a/mon-pix/app/components/challenge/item.js +++ b/mon-pix/app/components/challenge/item.js @@ -89,6 +89,10 @@ export default class Item extends Component { @action async answerValidated(challenge, assessment, answerValue, answerTimeout, answerFocusedOut) { + if (assessment.hasOngoingChallengeLiveAlert) { + return; + } + const answer = await this._findOrCreateAnswer(challenge, assessment); answer.setProperties({ value: answerValue.trim(), diff --git a/mon-pix/tests/unit/components/challenge/challenge-item-test.js b/mon-pix/tests/unit/components/challenge/challenge-item-test.js index b2def598fcb..a73c401ec71 100644 --- a/mon-pix/tests/unit/components/challenge/challenge-item-test.js +++ b/mon-pix/tests/unit/components/challenge/challenge-item-test.js @@ -150,6 +150,27 @@ module('Unit | Component | Challenge | Item', function (hooks) { assert.ok(true); }); + module('when there is an ongoing live alert', function () { + test('it should not save the answer', async function (assert) { + // given + const assessment = EmberObject.create({ answers: [answerToChallengeOne], hasOngoingChallengeLiveAlert: true }); + const component = createGlimmerComponent('challenge/item', { challenge: challengeOne }); + component.router = { transitionTo: sinon.stub().returns() }; + component.currentUser = { isAnonymous: false }; + component.store = { + createRecord: createRecordStub, + }; + + // when + await component.answerValidated(challengeOne, assessment, answerValue, answerFocusedOut, answerTimeout); + + // then + sinon.assert.notCalled(answerToChallengeOne.save); + sinon.assert.notCalled(component.router.transitionTo); + assert.ok(true); + }); + }); + module('when saving succeeds', function () { test('should redirect to assessment-resume route', async function (assert) { // given From 541031d45bd728a5a0ca81acb87b5d82c11ef12e Mon Sep 17 00:00:00 2001 From: Alexandre COIN Date: Wed, 13 Nov 2024 10:41:44 +0100 Subject: [PATCH 2/3] feat(api): add get ongoing or live alert for challenge method --- ...ication-challenge-live-alert-repository.js | 18 ++++ ...on-challenge-live-alert-repository_test.js | 87 +++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/api/src/certification/shared/infrastructure/repositories/certification-challenge-live-alert-repository.js b/api/src/certification/shared/infrastructure/repositories/certification-challenge-live-alert-repository.js index f2bd542657b..737387bc719 100644 --- a/api/src/certification/shared/infrastructure/repositories/certification-challenge-live-alert-repository.js +++ b/api/src/certification/shared/infrastructure/repositories/certification-challenge-live-alert-repository.js @@ -60,6 +60,23 @@ const getOngoingByChallengeIdAndAssessmentId = async ({ challengeId, assessmentI return _toDomain(certificationChallengeLiveAlertDto); }; +const getOngoingOrValidatedByChallengeIdAndAssessmentId = async ({ challengeId, assessmentId }) => { + const certificationChallengeLiveAlertDto = await knex('certification-challenge-live-alerts') + .where({ + 'certification-challenge-live-alerts.challengeId': challengeId, + 'certification-challenge-live-alerts.assessmentId': assessmentId, + 'certification-challenge-live-alerts.status': CertificationChallengeLiveAlertStatus.ONGOING, + }) + .orWhere({ + 'certification-challenge-live-alerts.challengeId': challengeId, + 'certification-challenge-live-alerts.assessmentId': assessmentId, + 'certification-challenge-live-alerts.status': CertificationChallengeLiveAlertStatus.VALIDATED, + }) + .first(); + + return _toDomain(certificationChallengeLiveAlertDto); +}; + const _toDomain = (certificationChallengeLiveAlertDto) => { if (!certificationChallengeLiveAlertDto) { return null; @@ -73,5 +90,6 @@ export { getLiveAlertValidatedChallengeIdsByAssessmentId, getOngoingByChallengeIdAndAssessmentId, getOngoingBySessionIdAndUserId, + getOngoingOrValidatedByChallengeIdAndAssessmentId, save, }; diff --git a/api/tests/certification/shared/integration/infrastructure/repositories/certification-challenge-live-alert-repository_test.js b/api/tests/certification/shared/integration/infrastructure/repositories/certification-challenge-live-alert-repository_test.js index baeead8ad2f..1b8ab967fb8 100644 --- a/api/tests/certification/shared/integration/infrastructure/repositories/certification-challenge-live-alert-repository_test.js +++ b/api/tests/certification/shared/integration/infrastructure/repositories/certification-challenge-live-alert-repository_test.js @@ -312,4 +312,91 @@ describe('Integration | Repository | Certification Challenge Live Alert', functi }); }); }); + + describe('getOngoingOrValidatedByChallengeIdAndAssessmentId', function () { + const challengeId = 'rec123'; + const assessmentId = 456; + + describe('when there are no matching live alerts', function () { + it('should return null', async function () { + // given / when + const liveAlert = + await certificationChallengeLiveAlertRepository.getOngoingOrValidatedByChallengeIdAndAssessmentId({ + challengeId, + assessmentId, + }); + + // then + expect(liveAlert).to.be.null; + }); + }); + + describe('when there is a matching validated live alert', function () { + it('should return the live alert', async function () { + // given + const certificationCourse = databaseBuilder.factory.buildCertificationCourse(); + + const assessment = databaseBuilder.factory.buildAssessment({ + certificationCourseId: certificationCourse.id, + userId: certificationCourse.userId, + }); + + databaseBuilder.factory.buildCertificationChallengeLiveAlert({ + assessmentId: assessment.id, + status: CertificationChallengeLiveAlertStatus.DISMISSED, + }); + + const certificationChallengeLiveAlert = databaseBuilder.factory.buildCertificationChallengeLiveAlert({ + assessmentId: assessment.id, + status: CertificationChallengeLiveAlertStatus.VALIDATED, + }); + + await databaseBuilder.commit(); + + // when + const liveAlert = + await certificationChallengeLiveAlertRepository.getOngoingOrValidatedByChallengeIdAndAssessmentId({ + challengeId: certificationChallengeLiveAlert.challengeId, + assessmentId: assessment.id, + }); + + // then + expect(liveAlert).to.deep.equal(certificationChallengeLiveAlert); + }); + }); + + describe('when there is a matching ongoing validated alert', function () { + it('should return the live alert', async function () { + // given + const certificationCourse = databaseBuilder.factory.buildCertificationCourse(); + + const assessment = databaseBuilder.factory.buildAssessment({ + certificationCourseId: certificationCourse.id, + userId: certificationCourse.userId, + }); + + databaseBuilder.factory.buildCertificationChallengeLiveAlert({ + assessmentId: assessment.id, + status: CertificationChallengeLiveAlertStatus.DISMISSED, + }); + + const certificationChallengeLiveAlert = databaseBuilder.factory.buildCertificationChallengeLiveAlert({ + assessmentId: assessment.id, + status: CertificationChallengeLiveAlertStatus.ONGOING, + }); + + await databaseBuilder.commit(); + + // when + const liveAlert = + await certificationChallengeLiveAlertRepository.getOngoingOrValidatedByChallengeIdAndAssessmentId({ + challengeId: certificationChallengeLiveAlert.challengeId, + assessmentId: assessment.id, + }); + + // then + expect(liveAlert).to.deep.equal(certificationChallengeLiveAlert); + }); + }); + }); }); From c8a9280930dc87264a9dcb00518c1c15ef686a57 Mon Sep 17 00:00:00 2001 From: Alexandre COIN Date: Wed, 13 Nov 2024 10:56:48 +0100 Subject: [PATCH 3/3] fix(api): prevent candidate from answering the challenge if a validated live alert exists --- .../correct-answer-then-update-assessment.js | 6 +- ...rect-answer-then-update-assessment_test.js | 95 +++++++++++++------ 2 files changed, 70 insertions(+), 31 deletions(-) diff --git a/api/lib/domain/usecases/correct-answer-then-update-assessment.js b/api/lib/domain/usecases/correct-answer-then-update-assessment.js index 6ab4bd92395..534ee00ffcb 100644 --- a/api/lib/domain/usecases/correct-answer-then-update-assessment.js +++ b/api/lib/domain/usecases/correct-answer-then-update-assessment.js @@ -165,13 +165,13 @@ const correctAnswerThenUpdateAssessment = async function ({ let certificationCandidate; if (assessment.isCertification()) { - const onGoingCertificationChallengeLiveAlert = - await certificationChallengeLiveAlertRepository.getOngoingByChallengeIdAndAssessmentId({ + const ongoingOrValidatedCertificationChallengeLiveAlert = + await certificationChallengeLiveAlertRepository.getOngoingOrValidatedByChallengeIdAndAssessmentId({ challengeId: challenge.id, assessmentId: assessment.id, }); - if (onGoingCertificationChallengeLiveAlert) { + if (ongoingOrValidatedCertificationChallengeLiveAlert) { throw new ForbiddenAccess('An alert has been set.'); } diff --git a/api/tests/unit/domain/usecases/correct-answer-then-update-assessment_test.js b/api/tests/unit/domain/usecases/correct-answer-then-update-assessment_test.js index c94a6a4842f..2c143f419a3 100644 --- a/api/tests/unit/domain/usecases/correct-answer-then-update-assessment_test.js +++ b/api/tests/unit/domain/usecases/correct-answer-then-update-assessment_test.js @@ -1,4 +1,5 @@ import { correctAnswerThenUpdateAssessment } from '../../../../lib/domain/usecases/correct-answer-then-update-assessment.js'; +import { CertificationChallengeLiveAlertStatus } from '../../../../src/certification/shared/domain/models/CertificationChallengeLiveAlert.js'; import { EmptyAnswerError } from '../../../../src/evaluation/domain/errors.js'; import { AnswerJob } from '../../../../src/quest/domain/models/AnwserJob.js'; import { @@ -58,7 +59,7 @@ describe('Unit | Domain | Use Cases | correct-answer-then-update-assessment', fu flashAssessmentResultRepository = { save: sinon.stub() }; scorecardService = { computeScorecard: sinon.stub() }; knowledgeElementRepository = { findUniqByUserIdAndAssessmentId: sinon.stub() }; - certificationChallengeLiveAlertRepository = { getOngoingByChallengeIdAndAssessmentId: sinon.stub() }; + certificationChallengeLiveAlertRepository = { getOngoingOrValidatedByChallengeIdAndAssessmentId: sinon.stub() }; certificationEvaluationCandidateRepository = { findByAssessmentId: sinon.stub() }; flashAlgorithmService = { getCapacityAndErrorRate: sinon.stub() }; algorithmDataFetcherService = { fetchForFlashLevelEstimation: sinon.stub() }; @@ -1128,37 +1129,75 @@ describe('Unit | Domain | Use Cases | correct-answer-then-update-assessment', fu }); }); - context('when a live alert has been set in V3 certification', function () { - it('should throw an error', async function () { - // given - const challenge = domainBuilder.buildChallenge({ id: '123' }); - const assessment = domainBuilder.buildAssessment({ - userId, - lastQuestionDate: nowDate, - state: Assessment.states.STARTED, - }); - const answer = domainBuilder.buildAnswer({ challengeId: challenge.id }); - const certificationChallengeLiveAlert = domainBuilder.buildCertificationChallengeLiveAlert({ - assessmentId: assessment.id, - challengeId: challenge.id, - }); - assessmentRepository.get.resolves(assessment); - challengeRepository.get.withArgs(challenge.id).resolves(challenge); + context('when a live alert has been set for the current challenge in V3 certification', function () { + context('when the live alert is ongoing', function () { + it('should throw an error', async function () { + // given + const challenge = domainBuilder.buildChallenge({ id: '123' }); + const assessment = domainBuilder.buildAssessment({ + userId, + lastQuestionDate: nowDate, + state: Assessment.states.STARTED, + }); + const answer = domainBuilder.buildAnswer({ challengeId: challenge.id }); + const certificationChallengeLiveAlert = domainBuilder.buildCertificationChallengeLiveAlert({ + assessmentId: assessment.id, + challengeId: challenge.id, + status: CertificationChallengeLiveAlertStatus.ONGOING, + }); + assessmentRepository.get.resolves(assessment); + challengeRepository.get.withArgs(challenge.id).resolves(challenge); - certificationChallengeLiveAlertRepository.getOngoingByChallengeIdAndAssessmentId - .withArgs({ challengeId: challenge.id, assessmentId: assessment.id }) - .resolves(certificationChallengeLiveAlert); + certificationChallengeLiveAlertRepository.getOngoingOrValidatedByChallengeIdAndAssessmentId + .withArgs({ challengeId: challenge.id, assessmentId: assessment.id }) + .resolves(certificationChallengeLiveAlert); - // when - const error = await catchErr(correctAnswerThenUpdateAssessment)({ - answer, - userId, - ...dependencies, + // when + const error = await catchErr(correctAnswerThenUpdateAssessment)({ + answer, + userId, + ...dependencies, + }); + + // then + expect(error).to.be.an.instanceOf(ForbiddenAccess); + expect(error.message).to.equal('An alert has been set.'); }); + }); - // then - expect(error).to.be.an.instanceOf(ForbiddenAccess); - expect(error.message).to.equal('An alert has been set.'); + context('when the live alert is validated', function () { + it('should throw an error', async function () { + // given + const challenge = domainBuilder.buildChallenge({ id: '123' }); + const assessment = domainBuilder.buildAssessment({ + userId, + lastQuestionDate: nowDate, + state: Assessment.states.STARTED, + }); + const answer = domainBuilder.buildAnswer({ challengeId: challenge.id }); + const certificationChallengeLiveAlert = domainBuilder.buildCertificationChallengeLiveAlert({ + assessmentId: assessment.id, + challengeId: challenge.id, + status: CertificationChallengeLiveAlertStatus.VALIDATED, + }); + assessmentRepository.get.resolves(assessment); + challengeRepository.get.withArgs(challenge.id).resolves(challenge); + + certificationChallengeLiveAlertRepository.getOngoingOrValidatedByChallengeIdAndAssessmentId + .withArgs({ challengeId: challenge.id, assessmentId: assessment.id }) + .resolves(certificationChallengeLiveAlert); + + // when + const error = await catchErr(correctAnswerThenUpdateAssessment)({ + answer, + userId, + ...dependencies, + }); + + // then + expect(error).to.be.an.instanceOf(ForbiddenAccess); + expect(error.message).to.equal('An alert has been set.'); + }); }); });