Skip to content

Commit 82abf87

Browse files
committedMar 27, 2025
feat(api): do not return not yet answered challenge if challenge became not playable in the mean time
1 parent 57151b6 commit 82abf87

File tree

4 files changed

+101
-21
lines changed

4 files changed

+101
-21
lines changed
 

Diff for: ‎api/src/shared/domain/models/Challenge.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ const Accessibility = Object.freeze({
2020
OK: 'OK',
2121
});
2222

23+
const Statuses = Object.freeze({
24+
VALIDATED: 'validé',
25+
ARCHIVED: 'archivé',
26+
OBSOLETE: 'périmé',
27+
});
28+
2329
/**
2430
* Traduction: Épreuve
2531
*/
@@ -163,6 +169,10 @@ class Challenge {
163169
return this._isCompliant('Tablet');
164170
}
165171

172+
get isOperative() {
173+
return [Statuses.VALIDATED, Statuses.ARCHIVED].includes(this.status);
174+
}
175+
166176
get isAccessible() {
167177
return (
168178
(this.blindnessCompatibility === Accessibility.OK || this.blindnessCompatibility === Accessibility.RAS) &&
@@ -208,4 +218,4 @@ class Challenge {
208218

209219
Challenge.Type = ChallengeType;
210220

211-
export { Accessibility, Challenge, ChallengeType as Type };
221+
export { Accessibility, Challenge, Statuses, ChallengeType as Type };

Diff for: ‎api/src/shared/domain/usecases/get-next-challenge.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ export async function getNextChallenge({
2323
lastChallengeId: assessment.lastChallengeId,
2424
});
2525
if (!hasAnswered) {
26-
return challengeRepository.get(assessment.lastChallengeId);
26+
nextChallenge = await challengeRepository.get(assessment.lastChallengeId);
27+
if (nextChallenge.isOperative) {
28+
return nextChallenge;
29+
} else {
30+
nextChallenge = null;
31+
}
2732
}
2833
if (assessment.isCertification()) {
2934
nextChallenge = await certificationEvaluationRepository.selectNextCertificationChallenge({

Diff for: ‎api/tests/shared/unit/domain/usecases/get-next-challenge_test.js

+48-18
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { AssessmentEndedError } from '../../../../../src/shared/domain/errors.js';
2+
import { Statuses } from '../../../../../src/shared/domain/models/Challenge.js';
23
import { Assessment } from '../../../../../src/shared/domain/models/index.js';
34
import { getNextChallenge } from '../../../../../src/shared/domain/usecases/get-next-challenge.js';
45
import { catchErr, domainBuilder, expect, preventStubsToBeCalledUnexpectedly, sinon } from '../../../../test-helper.js';
@@ -157,27 +158,56 @@ describe('Shared | Unit | Domain | Use Cases | get-next-challenge', function ()
157158
});
158159

159160
context('when the latest challenge asked has not been answered yet', function () {
160-
it('should return latest challenge', async function () {
161-
const assessment = domainBuilder.buildAssessment({
162-
state: Assessment.states.STARTED,
163-
type: Assessment.types.PREVIEW,
164-
lastChallengeId: 'previousChallengeId',
161+
context('when challenge is operative', function () {
162+
it('should return latest challenge', async function () {
163+
const assessment = domainBuilder.buildAssessment({
164+
state: Assessment.states.STARTED,
165+
type: Assessment.types.PREVIEW,
166+
lastChallengeId: 'previousChallengeId',
167+
});
168+
assessmentRepository_getStub.withArgs(assessmentId).resolves(assessment);
169+
assessmentRepository_updateLastQuestionDateStub.resolves();
170+
assessmentRepository_updateWhenNewChallengeIsAskedStub.resolves();
171+
const previousChallenge = domainBuilder.buildChallenge({
172+
id: 'previousChallengeId',
173+
status: Statuses.VALIDATED,
174+
});
175+
answerRepository_findByAssessmentStub
176+
.withArgs(assessment.id)
177+
.resolves([
178+
domainBuilder.buildAnswer({ challengeId: 'someChallengeId' }),
179+
domainBuilder.buildAnswer({ challengeId: 'someOtherChallengeId' }),
180+
]);
181+
challengeRepository_getStub.withArgs('previousChallengeId').resolves(previousChallenge);
182+
183+
const actualNextChallenge = await getNextChallenge(dependencies);
184+
185+
expect(actualNextChallenge).to.deepEqualInstance(previousChallenge);
165186
});
166-
assessmentRepository_getStub.withArgs(assessmentId).resolves(assessment);
167-
assessmentRepository_updateLastQuestionDateStub.resolves();
168-
assessmentRepository_updateWhenNewChallengeIsAskedStub.resolves();
169-
const previousChallenge = domainBuilder.buildChallenge({ id: 'previousChallengeId' });
170-
answerRepository_findByAssessmentStub
171-
.withArgs(assessment.id)
172-
.resolves([
173-
domainBuilder.buildAnswer({ challengeId: 'someChallengeId' }),
174-
domainBuilder.buildAnswer({ challengeId: 'someOtherChallengeId' }),
175-
]);
176-
challengeRepository_getStub.withArgs('previousChallengeId').resolves(previousChallenge);
187+
});
188+
context('when challenge is not operative', function () {
189+
it('should compute next challenge', async function () {
190+
const assessment = domainBuilder.buildAssessment({
191+
state: Assessment.states.STARTED,
192+
type: Assessment.types.PREVIEW,
193+
lastChallengeId: 'previousChallengeId',
194+
});
195+
assessmentRepository_getStub.withArgs(assessmentId).resolves(assessment);
196+
assessmentRepository_updateLastQuestionDateStub.resolves();
197+
assessmentRepository_updateWhenNewChallengeIsAskedStub.resolves();
198+
const challenge = domainBuilder.buildChallenge({ id: 'nextChallengeForPreview' });
199+
const previousChallenge = domainBuilder.buildChallenge({
200+
id: 'previousChallengeId',
201+
status: Statuses.OBSOLETE,
202+
});
203+
evaluationUsecases_getNextChallengeForPreviewStub.withArgs({}).resolves(challenge);
204+
answerRepository_findByAssessmentStub.withArgs(assessment.id).resolves([]);
205+
challengeRepository_getStub.withArgs('previousChallengeId').resolves(previousChallenge);
177206

178-
const actualNextChallenge = await getNextChallenge(dependencies);
207+
const actualNextChallenge = await getNextChallenge(dependencies);
179208

180-
expect(actualNextChallenge).to.deepEqualInstance(previousChallenge);
209+
expect(actualNextChallenge).to.deepEqualInstance(challenge);
210+
});
181211
});
182212
});
183213
});

Diff for: ‎api/tests/unit/domain/models/Challenge_test.js

+36-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { ValidatorQCU } from '../../../../src/evaluation/domain/models/Validator
44
import { ValidatorQROC } from '../../../../src/evaluation/domain/models/ValidatorQROC.js';
55
import { ValidatorQROCMDep } from '../../../../src/evaluation/domain/models/ValidatorQROCMDep.js';
66
import { ValidatorQROCMInd } from '../../../../src/evaluation/domain/models/ValidatorQROCMInd.js';
7-
import { Accessibility, Challenge } from '../../../../src/shared/domain/models/Challenge.js';
7+
import { Accessibility, Challenge, Statuses } from '../../../../src/shared/domain/models/Challenge.js';
88
import { Skill } from '../../../../src/shared/domain/models/Skill.js';
99
import { domainBuilder, expect } from '../../../test-helper.js';
1010

@@ -366,4 +366,39 @@ describe('Unit | Domain | Models | Challenge', function () {
366366
);
367367
});
368368
});
369+
370+
describe('#isOperative', function () {
371+
it('should return true when challenge is validated', function () {
372+
// given
373+
const challenge = domainBuilder.buildChallenge({ status: Statuses.VALIDATED });
374+
375+
// when
376+
const isOperative = challenge.isOperative;
377+
378+
// then
379+
expect(isOperative).to.be.true;
380+
});
381+
382+
it('should return true when challenge is archived', function () {
383+
// given
384+
const challenge = domainBuilder.buildChallenge({ status: Statuses.ARCHIVED });
385+
386+
// when
387+
const isOperative = challenge.isOperative;
388+
389+
// then
390+
expect(isOperative).to.be.true;
391+
});
392+
393+
it('should return false when challenge is obsolete', function () {
394+
// given
395+
const challenge = domainBuilder.buildChallenge({ status: Statuses.OBSOLETE });
396+
397+
// when
398+
const isOperative = challenge.isOperative;
399+
400+
// then
401+
expect(isOperative).to.be.false;
402+
});
403+
});
369404
});

0 commit comments

Comments
 (0)