Skip to content

Commit b6f546e

Browse files
[BUGFIX] Empêcher la validation d'un live-alert assigné à une épreuve déjà répondue (PIX-16783).
#11542
2 parents b2ed4bd + 50ebe71 commit b6f546e

File tree

9 files changed

+152
-23
lines changed

9 files changed

+152
-23
lines changed

api/src/certification/evaluation/domain/errors.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { DomainError } from '../../../shared/domain/errors.js';
22

33
class ChallengeAlreadyAnsweredError extends DomainError {
4-
constructor(message) {
5-
super(message);
4+
constructor() {
5+
super('La question a déjà été répondue.', 'ALREADY_ANSWERED_ERROR');
66
}
77
}
88

api/src/certification/session-management/domain/usecases/validate-live-alert.js

+30
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import { NotFoundError } from '../../../../shared/domain/errors.js';
99
import { CertificationIssueReport, CertificationIssueReportCategory } from '../../../../shared/domain/models/index.js';
10+
import { ChallengeAlreadyAnsweredError } from '../../../evaluation/domain/errors.js';
1011

1112
/**
1213
* @param {Object} params
@@ -23,6 +24,7 @@ export const validateLiveAlert = async ({
2324
assessmentRepository,
2425
issueReportCategoryRepository,
2526
certificationIssueReportRepository,
27+
answerRepository,
2628
}) => {
2729
const certificationChallengeLiveAlert =
2830
await certificationChallengeLiveAlertRepository.getOngoingBySessionIdAndUserId({
@@ -34,6 +36,12 @@ export const validateLiveAlert = async ({
3436
throw new NotFoundError('There is no ongoing alert for this user');
3537
}
3638

39+
await _dismissLiveAlertForAnsweredChallenge({
40+
certificationChallengeLiveAlert,
41+
certificationChallengeLiveAlertRepository,
42+
answerRepository,
43+
});
44+
3745
const assessment = await assessmentRepository.get(certificationChallengeLiveAlert.assessmentId);
3846

3947
const { certificationCourseId } = assessment;
@@ -59,3 +67,25 @@ export const validateLiveAlert = async ({
5967
certificationChallengeLiveAlert,
6068
});
6169
};
70+
71+
async function _dismissLiveAlertForAnsweredChallenge({
72+
certificationChallengeLiveAlert,
73+
certificationChallengeLiveAlertRepository,
74+
answerRepository,
75+
}) {
76+
const candidateAnswers = await answerRepository.findByAssessment(certificationChallengeLiveAlert.assessmentId);
77+
78+
const answeredAlertedChallenge = candidateAnswers.find(
79+
(answer) => answer.challengeId === certificationChallengeLiveAlert.challengeId,
80+
);
81+
82+
if (answeredAlertedChallenge) {
83+
certificationChallengeLiveAlert.dismiss();
84+
85+
await certificationChallengeLiveAlertRepository.save({
86+
certificationChallengeLiveAlert,
87+
});
88+
89+
throw new ChallengeAlreadyAnsweredError();
90+
}
91+
}

api/src/shared/application/error-manager.js

+5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import _ from 'lodash';
33

44
import * as translations from '../../../translations/index.js';
55
import { AdminMemberError } from '../../authorization/domain/errors.js';
6+
import { ChallengeAlreadyAnsweredError } from '../../certification/evaluation/domain/errors.js';
67
import {
78
CsvWithNoSessionDataError,
89
SendingEmailToRefererError,
@@ -176,6 +177,10 @@ function _mapToHttpError(error) {
176177
return new HttpErrors.PreconditionFailedError(error.message);
177178
}
178179

180+
if (error instanceof ChallengeAlreadyAnsweredError) {
181+
return new HttpErrors.UnprocessableEntityError(error.message, error.code);
182+
}
183+
179184
if (error instanceof CsvWithNoSessionDataError) {
180185
return new HttpErrors.UnprocessableEntityError(error.message, error.code);
181186
}

api/tests/certification/session-management/unit/domain/usecases/validate-live-alert_test.js

+55
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ChallengeAlreadyAnsweredError } from '../../../../../../src/certification/evaluation/domain/errors.js';
12
import { validateLiveAlert } from '../../../../../../src/certification/session-management/domain/usecases/validate-live-alert.js';
23
import { CertificationChallengeLiveAlertStatus } from '../../../../../../src/certification/shared/domain/models/CertificationChallengeLiveAlert.js';
34
import {
@@ -12,6 +13,7 @@ describe('Unit | UseCase | validate-live-alert', function () {
1213
let assessmentRepository;
1314
let issueReportCategoryRepository;
1415
let certificationIssueReportRepository;
16+
let answerRepository;
1517

1618
beforeEach(function () {
1719
certificationChallengeLiveAlertRepository = {
@@ -30,6 +32,10 @@ describe('Unit | UseCase | validate-live-alert', function () {
3032
certificationIssueReportRepository = {
3133
save: sinon.stub(),
3234
};
35+
36+
answerRepository = {
37+
findByAssessment: sinon.stub(),
38+
};
3339
});
3440

3541
describe('when the liveAlert does not exist', function () {
@@ -57,6 +63,52 @@ describe('Unit | UseCase | validate-live-alert', function () {
5763
});
5864

5965
describe('when the liveAlert exists', function () {
66+
describe('when an answer for the alerted challenge exists', function () {
67+
it('should throw an error', async function () {
68+
// given
69+
const sessionId = 123;
70+
const userId = 456;
71+
const assessmentId = 789;
72+
const questionNumber = 2;
73+
const challengeId = 'rec123';
74+
75+
const onGoingLiveAlert = domainBuilder.buildCertificationChallengeLiveAlert({
76+
questionNumber,
77+
challengeId,
78+
assessmentId,
79+
status: CertificationChallengeLiveAlertStatus.ONGOING,
80+
});
81+
82+
certificationChallengeLiveAlertRepository.getOngoingBySessionIdAndUserId
83+
.withArgs({
84+
sessionId,
85+
userId,
86+
})
87+
.resolves(onGoingLiveAlert);
88+
89+
answerRepository.findByAssessment
90+
.withArgs(onGoingLiveAlert.assessmentId)
91+
.resolves([domainBuilder.buildAnswer({ challengeId })]);
92+
93+
const error = await catchErr(validateLiveAlert)({
94+
certificationChallengeLiveAlertRepository,
95+
issueReportCategoryRepository,
96+
certificationIssueReportRepository,
97+
answerRepository,
98+
sessionId,
99+
userId,
100+
});
101+
102+
// then
103+
expect(onGoingLiveAlert.status).equals(CertificationChallengeLiveAlertStatus.DISMISSED);
104+
expect(certificationChallengeLiveAlertRepository.save).to.have.been.calledWith({
105+
certificationChallengeLiveAlert: onGoingLiveAlert,
106+
});
107+
108+
expect(error).to.be.instanceOf(ChallengeAlreadyAnsweredError);
109+
});
110+
});
111+
60112
it('should update the LiveAlert and create a new resolved CertificationIssueReport', async function () {
61113
// given
62114
const sessionId = 123;
@@ -97,6 +149,8 @@ describe('Unit | UseCase | validate-live-alert', function () {
97149
})
98150
.resolves(issueReportCategory);
99151

152+
answerRepository.findByAssessment.withArgs(liveAlert.assessmentId).resolves([]);
153+
100154
const validatedLiveAlert = domainBuilder.buildCertificationChallengeLiveAlert({
101155
assessmentId: liveAlert.assessmentId,
102156
challengeId: liveAlert.challengeId,
@@ -110,6 +164,7 @@ describe('Unit | UseCase | validate-live-alert', function () {
110164
issueReportCategoryRepository,
111165
assessmentRepository,
112166
certificationIssueReportRepository,
167+
answerRepository,
113168
subcategory,
114169
sessionId,
115170
userId,

certif/app/components/session-supervising/candidate-in-list.js

+10-5
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ export default class CandidateInList extends Component {
165165
this.displayedModal = Modals.HandledLiveAlertSuccess;
166166
} catch (error) {
167167
const errorMessage = this.intl.t(
168-
'pages.session-supervising.candidate-in-list.handle-live-alert-modal.error-handling-live-alert',
168+
'pages.session-supervising.candidate-in-list.handle-live-alert-modal.error-handling.miscellaneous',
169169
);
170170
this.pixToast.sendErrorNotification({ message: errorMessage });
171171
}
@@ -183,9 +183,14 @@ export default class CandidateInList extends Component {
183183
this.isLiveAlertValidated = true;
184184
this.displayedModal = Modals.HandledLiveAlertSuccess;
185185
} catch (err) {
186-
const errorMessage = this.intl.t(
187-
'pages.session-supervising.candidate-in-list.handle-live-alert-modal.error-handling-live-alert',
188-
);
186+
const errorMessage =
187+
err.errors[0].code === 'ALREADY_ANSWERED_ERROR'
188+
? this.intl.t(
189+
'pages.session-supervising.candidate-in-list.handle-live-alert-modal.error-handling.challenge-already-answered',
190+
)
191+
: this.intl.t(
192+
'pages.session-supervising.candidate-in-list.handle-live-alert-modal.error-handling.miscellaneous',
193+
);
189194
this.pixToast.sendErrorNotification({ message: errorMessage });
190195
}
191196
}
@@ -207,7 +212,7 @@ export default class CandidateInList extends Component {
207212
} catch (error) {
208213
this.pixToast.sendErrorNotification({
209214
message: this.intl.t(
210-
'pages.session-supervising.candidate-in-list.handle-live-alert-modal.error-handling-live-alert',
215+
'pages.session-supervising.candidate-in-list.handle-live-alert-modal.error-handling.miscellaneous',
211216
),
212217
});
213218
} finally {

certif/tests/acceptance/session-supervising-test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ module('Acceptance | Session supervising', function (hooks) {
435435
const candidateId = 12345;
436436
server.patch(
437437
`/sessions/${sessionId}/candidates/${candidateId}/validate-live-alert`,
438-
() => new Response(400),
438+
() => new Response(400, {}, { errors: [{ code: 'SOME_CODE' }] }),
439439
);
440440
this.sessionForSupervising = server.create('session-for-supervising', {
441441
id: sessionId,

certif/tests/unit/components/session-supervising/candidate-in-list-test.js

+41-13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Service from '@ember/service';
2+
import { t } from 'ember-intl/test-support';
23
import { setupTest } from 'ember-qunit';
34
import { module, test } from 'qunit';
45
import sinon from 'sinon';
@@ -38,32 +39,59 @@ module('Unit | Component | session-supervising/candidate-in-list', function (hoo
3839
});
3940
});
4041

41-
module('when there is an error', function () {
42-
test('it should call the notification service', async function (assert) {
42+
module('when there is an error', function (hooks) {
43+
let adapter, component;
44+
45+
hooks.beforeEach(function () {
4346
// given
44-
const subcategory = 'EMBED_NOT_WORKING';
45-
const component = createGlimmerComponent('component:session-supervising/candidate-in-list');
47+
class IntlStub extends Service {
48+
t = sinon.stub();
49+
}
50+
51+
this.owner.register('service:intl', IntlStub);
52+
53+
component = createGlimmerComponent('component:session-supervising/candidate-in-list');
4654
component.args.sessionId = 123;
4755
component.args.candidate = { userId: 456 };
4856
component.pixToast = { sendErrorNotification: sinon.spy() };
4957
const store = this.owner.lookup('service:store');
50-
const adapter = store.adapterFor('session-management');
58+
adapter = store.adapterFor('session-management');
5159
adapter.validateLiveAlert = sinon.stub();
52-
adapter.validateLiveAlert.rejects();
53-
54-
class IntlStub extends Service {
55-
t = sinon.stub();
56-
}
60+
});
5761

58-
this.owner.register('service:intl', IntlStub);
62+
test('it should call the notification service', async function (assert) {
63+
// given
64+
adapter.validateLiveAlert.rejects({ errors: [{}] });
5965

6066
// when
61-
await component.validateLiveAlert(subcategory);
67+
await component.validateLiveAlert('EMBED_NOT_WORKING');
6268

6369
// then
64-
sinon.assert.calledOnce(component.pixToast.sendErrorNotification);
70+
sinon.assert.calledOnceWithExactly(component.pixToast.sendErrorNotification, {
71+
message: t(
72+
'pages.session-supervising.candidate-in-list.handle-live-alert-modal.error-handling.miscellaneous',
73+
),
74+
});
6575
assert.ok(true);
6676
});
77+
78+
module('when there is a ALREADY_ANSWERED_ERROR error', function () {
79+
test('it should call the notification service with the request error message', async function (assert) {
80+
// given
81+
adapter.validateLiveAlert.rejects({ errors: [{ code: 'ALREADY_ANSWERED_ERROR', detail: 'error message' }] });
82+
83+
// when
84+
await component.validateLiveAlert('EMBED_NOT_WORKING');
85+
86+
// then
87+
sinon.assert.calledOnceWithExactly(component.pixToast.sendErrorNotification, {
88+
message: t(
89+
'pages.session-supervising.candidate-in-list.handle-live-alert-modal.error-handling.challenge-already-answered',
90+
),
91+
});
92+
assert.ok(true);
93+
});
94+
});
6795
});
6896
});
6997

certif/translations/en.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,10 @@
486486
"dismiss-alert-button": "Refuser le signalement",
487487
"validate-alert-button": "Valider le signalement"
488488
},
489-
"error-handling-live-alert": "Une erreur a eu lieu. Merci de réessayer ultérieurement.",
489+
"error-handling": {
490+
"challenge-already-answered": "The candidate has already answered this question. This alert will therefore be ignored.",
491+
"miscellaneous": "An error occured. Please try again later."
492+
},
490493
"handled": {
491494
"close-button-label": "Fermer la fenêtre de confirmation",
492495
"close-button-text": "Fermer",

certif/translations/fr.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,10 @@
486486
"dismiss-alert-button": "Refuser le signalement",
487487
"validate-alert-button": "Valider le signalement"
488488
},
489-
"error-handling-live-alert": "Une erreur a eu lieu. Merci de réessayer ultérieurement.",
489+
"error-handling": {
490+
"challenge-already-answered": "Le candidat a répondu à cette question. Cette alerte sera ignorée.",
491+
"miscellaneous": "Une erreur a eu lieu. Merci de réessayer ultérieurement."
492+
},
490493
"handled": {
491494
"close-button-label": "Fermer la fenêtre de confirmation",
492495
"close-button-text": "Fermer",

0 commit comments

Comments
 (0)