Skip to content

Commit 57151b6

Browse files
tech(api): return latest challenge asked if not answered yet in assessments/:id/next
1 parent 668cf78 commit 57151b6

File tree

4 files changed

+163
-9
lines changed

4 files changed

+163
-9
lines changed

api/src/shared/domain/usecases/get-next-challenge.js

+17
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ export async function getNextChallenge({
55
userId,
66
locale,
77
assessmentRepository,
8+
answerRepository,
9+
challengeRepository,
810
evaluationUsecases,
911
certificationEvaluationRepository,
1012
}) {
@@ -15,6 +17,14 @@ export async function getNextChallenge({
1517
await assessmentRepository.updateLastQuestionDate({ id: assessment.id, lastQuestionDate: new Date() });
1618

1719
let nextChallenge = null;
20+
const answers = await answerRepository.findByAssessment(assessment.id);
21+
const hasAnswered = hasAnsweredLatestChallengeAsked({
22+
answers,
23+
lastChallengeId: assessment.lastChallengeId,
24+
});
25+
if (!hasAnswered) {
26+
return challengeRepository.get(assessment.lastChallengeId);
27+
}
1828
if (assessment.isCertification()) {
1929
nextChallenge = await certificationEvaluationRepository.selectNextCertificationChallenge({
2030
assessmentId: assessment.id,
@@ -47,3 +57,10 @@ export async function getNextChallenge({
4757

4858
return nextChallenge;
4959
}
60+
61+
function hasAnsweredLatestChallengeAsked({ answers, lastChallengeId }) {
62+
if (!lastChallengeId) {
63+
return true;
64+
}
65+
return answers.some((answer) => answer.challengeId === lastChallengeId);
66+
}

api/src/shared/domain/usecases/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
44
import * as complementaryCertificationBadgeRepository from '../../../certification/complementary-certification/infrastructure/repositories/complementary-certification-badge-repository.js';
55
import { evaluationUsecases } from '../../../evaluation/domain/usecases/index.js';
66
import * as badgeRepository from '../../../evaluation/infrastructure/repositories/badge-repository.js';
7+
import * as answerRepository from '../../infrastructure/repositories/answer-repository.js';
78
import * as assessmentRepository from '../../infrastructure/repositories/assessment-repository.js';
89
import * as challengeRepository from '../../infrastructure/repositories/challenge-repository.js';
910
import { repositories as sharedInjectedRepositories } from '../../infrastructure/repositories/index.js';
@@ -17,6 +18,7 @@ const usecasesWithoutInjectedDependencies = {
1718

1819
const dependencies = {
1920
assessmentRepository,
21+
answerRepository,
2022
complementaryCertificationBadgeRepository,
2123
badgeRepository,
2224
challengeRepository,

api/tests/shared/acceptance/application/assessments/assessment-controller-get-next-challenge-for-demo_test.js

+31
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Assessment } from '../../../../../src/shared/domain/models/index.js';
12
import {
23
createServer,
34
databaseBuilder,
@@ -58,6 +59,7 @@ describe('Acceptance | API | assessment-controller-get-next-challenge-for-demo',
5859
id: assessmentId,
5960
type: 'DEMO',
6061
courseId: 'course_id',
62+
state: Assessment.states.STARTED,
6163
});
6264
return databaseBuilder.commit();
6365
});
@@ -83,6 +85,7 @@ describe('Acceptance | API | assessment-controller-get-next-challenge-for-demo',
8385
id: assessmentId,
8486
type: 'DEMO',
8587
courseId: 'course_id',
88+
state: Assessment.states.STARTED,
8689
});
8790
databaseBuilder.factory.buildAnswer({ challengeId: 'first_challenge', assessmentId });
8891
return databaseBuilder.commit();
@@ -103,6 +106,34 @@ describe('Acceptance | API | assessment-controller-get-next-challenge-for-demo',
103106
});
104107
});
105108

109+
context('when the first challenge has not been answered yet', function () {
110+
beforeEach(function () {
111+
databaseBuilder.factory.buildAssessment({
112+
id: assessmentId,
113+
type: 'DEMO',
114+
courseId: 'course_id',
115+
state: Assessment.states.STARTED,
116+
lastChallengeId: 'first_challenge',
117+
});
118+
databaseBuilder.factory.buildAnswer({ challengeId: 'some_other_challenge', assessmentId });
119+
return databaseBuilder.commit();
120+
});
121+
122+
it('should return the first challenge again', async function () {
123+
// given
124+
const options = {
125+
method: 'GET',
126+
url: '/api/assessments/' + assessmentId + '/next',
127+
};
128+
129+
// when
130+
const response = await server.inject(options);
131+
132+
// then
133+
expect(response.result.data.id).to.equal('first_challenge');
134+
});
135+
});
136+
106137
context('when all challenges are answered', function () {
107138
beforeEach(function () {
108139
databaseBuilder.factory.buildAssessment({

api/tests/shared/unit/domain/usecases/get-next-challenge_test.js

+113-9
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ describe('Shared | Unit | Domain | Use Cases | get-next-challenge', function ()
99
let assessmentRepository_getStub;
1010
let assessmentRepository_updateLastQuestionDateStub;
1111
let assessmentRepository_updateWhenNewChallengeIsAskedStub;
12+
let answerRepository_findByAssessmentStub;
13+
let challengeRepository_getStub;
1214
let evaluationUsecases_getNextChallengeForPreviewStub;
1315
let evaluationUsecases_getNextChallengeForDemoStub;
1416
let evaluationUsecases_getNextChallengeForCampaignAssessmentStub;
@@ -22,6 +24,8 @@ describe('Shared | Unit | Domain | Use Cases | get-next-challenge', function ()
2224
assessmentRepository_getStub = sinon.stub().named('get');
2325
assessmentRepository_updateLastQuestionDateStub = sinon.stub().named('updateLastQuestionDate');
2426
assessmentRepository_updateWhenNewChallengeIsAskedStub = sinon.stub().named('updateWhenNewChallengeIsAsked');
27+
answerRepository_findByAssessmentStub = sinon.stub().named('findByAssessment');
28+
challengeRepository_getStub = sinon.stub().named('get');
2529
evaluationUsecases_getNextChallengeForPreviewStub = sinon.stub().named('getNextChallengeForPreview');
2630
evaluationUsecases_getNextChallengeForDemoStub = sinon.stub().named('getNextChallengeForDemo');
2731
evaluationUsecases_getNextChallengeForCampaignAssessmentStub = sinon
@@ -34,8 +38,11 @@ describe('Shared | Unit | Domain | Use Cases | get-next-challenge', function ()
3438
.stub()
3539
.named('selectNextCertificationChallenge');
3640
preventStubsToBeCalledUnexpectedly([
41+
assessmentRepository_getStub,
3742
assessmentRepository_updateLastQuestionDateStub,
3843
assessmentRepository_updateWhenNewChallengeIsAskedStub,
44+
answerRepository_findByAssessmentStub,
45+
challengeRepository_getStub,
3946
evaluationUsecases_getNextChallengeForPreviewStub,
4047
evaluationUsecases_getNextChallengeForDemoStub,
4148
evaluationUsecases_getNextChallengeForCampaignAssessmentStub,
@@ -49,6 +56,14 @@ describe('Shared | Unit | Domain | Use Cases | get-next-challenge', function ()
4956
updateWhenNewChallengeIsAsked: assessmentRepository_updateWhenNewChallengeIsAskedStub,
5057
};
5158

59+
const answerRepository = {
60+
findByAssessment: answerRepository_findByAssessmentStub,
61+
};
62+
63+
const challengeRepository = {
64+
get: challengeRepository_getStub,
65+
};
66+
5267
const evaluationUsecases = {
5368
getNextChallengeForPreview: evaluationUsecases_getNextChallengeForPreviewStub,
5469
getNextChallengeForDemo: evaluationUsecases_getNextChallengeForDemoStub,
@@ -65,6 +80,8 @@ describe('Shared | Unit | Domain | Use Cases | get-next-challenge', function ()
6580
userId,
6681
locale,
6782
assessmentRepository,
83+
answerRepository,
84+
challengeRepository,
6885
evaluationUsecases,
6986
certificationEvaluationRepository,
7087
};
@@ -77,19 +94,92 @@ describe('Shared | Unit | Domain | Use Cases | get-next-challenge', function ()
7794
assessment = domainBuilder.buildAssessment({ id: assessmentId, type: Assessment.types.PREVIEW });
7895
});
7996

80-
// eslint-disable-next-line mocha/no-setup-in-describe
81-
Object.values(Assessment.states)
82-
.filter((assessmentState) => assessmentState !== Assessment.states.STARTED)
83-
.forEach((assessmentState) => {
84-
it(`should throw a AssessmentEndedError when assessment is ${assessmentState}`, async function () {
85-
assessment.state = assessmentState;
86-
assessmentRepository_getStub.withArgs(assessmentId).resolves(assessment);
97+
/* eslint-disable mocha/no-setup-in-describe */
98+
[
99+
Assessment.states.ABORTED,
100+
Assessment.states.COMPLETED,
101+
Assessment.states.ENDED_BY_SUPERVISOR,
102+
Assessment.states.ENDED_DUE_TO_FINALIZATION,
103+
].forEach((assessmentState) => {
104+
it(`should throw a AssessmentEndedError when assessment is ${assessmentState}`, async function () {
105+
assessment.state = assessmentState;
106+
assessmentRepository_getStub.withArgs(assessmentId).resolves(assessment);
87107

88-
const err = await catchErr(getNextChallenge)(dependencies);
108+
const err = await catchErr(getNextChallenge)(dependencies);
89109

90-
expect(err).to.be.instanceOf(AssessmentEndedError);
110+
expect(err).to.be.instanceOf(AssessmentEndedError);
111+
});
112+
});
113+
/* eslint-enable mocha/no-setup-in-describe */
114+
});
115+
116+
context('latest challenge asked', function () {
117+
context('when there are no challenge saved as latest challenge asked in assessment', function () {
118+
it('should compute next challenge', async function () {
119+
const assessment = domainBuilder.buildAssessment({
120+
state: Assessment.states.STARTED,
121+
type: Assessment.types.PREVIEW,
122+
lastChallengeId: null,
91123
});
124+
assessmentRepository_getStub.withArgs(assessmentId).resolves(assessment);
125+
assessmentRepository_updateLastQuestionDateStub.resolves();
126+
assessmentRepository_updateWhenNewChallengeIsAskedStub.resolves();
127+
const challenge = domainBuilder.buildChallenge({ id: 'challengeForPreview' });
128+
evaluationUsecases_getNextChallengeForPreviewStub.withArgs({}).resolves(challenge);
129+
answerRepository_findByAssessmentStub.withArgs(assessment.id).resolves([domainBuilder.buildAnswer()]);
130+
131+
const actualNextChallenge = await getNextChallenge(dependencies);
132+
133+
expect(actualNextChallenge).to.deepEqualInstance(challenge);
92134
});
135+
});
136+
137+
context('when the latest challenge asked has been answered', function () {
138+
it('should compute next challenge', async function () {
139+
const assessment = domainBuilder.buildAssessment({
140+
state: Assessment.states.STARTED,
141+
type: Assessment.types.PREVIEW,
142+
lastChallengeId: 'previousChallengeId',
143+
});
144+
assessmentRepository_getStub.withArgs(assessmentId).resolves(assessment);
145+
assessmentRepository_updateLastQuestionDateStub.resolves();
146+
assessmentRepository_updateWhenNewChallengeIsAskedStub.resolves();
147+
const challenge = domainBuilder.buildChallenge({ id: 'challengeForPreview' });
148+
evaluationUsecases_getNextChallengeForPreviewStub.withArgs({}).resolves(challenge);
149+
answerRepository_findByAssessmentStub
150+
.withArgs(assessment.id)
151+
.resolves([domainBuilder.buildAnswer({ challengeId: 'previousChallengeId' })]);
152+
153+
const actualNextChallenge = await getNextChallenge(dependencies);
154+
155+
expect(actualNextChallenge).to.deepEqualInstance(challenge);
156+
});
157+
});
158+
159+
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',
165+
});
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);
177+
178+
const actualNextChallenge = await getNextChallenge(dependencies);
179+
180+
expect(actualNextChallenge).to.deepEqualInstance(previousChallenge);
181+
});
182+
});
93183
});
94184

95185
context('Assessment types', function () {
@@ -104,6 +194,7 @@ describe('Shared | Unit | Domain | Use Cases | get-next-challenge', function ()
104194
assessmentRepository_getStub.withArgs(assessmentId).resolves(assessment);
105195
assessmentRepository_updateLastQuestionDateStub.resolves();
106196
assessmentRepository_updateWhenNewChallengeIsAskedStub.resolves();
197+
answerRepository_findByAssessmentStub.resolves([]);
107198
});
108199

109200
it('should call usecase and return value from preview usecase', async function () {
@@ -126,6 +217,7 @@ describe('Shared | Unit | Domain | Use Cases | get-next-challenge', function ()
126217
assessmentRepository_getStub.withArgs(assessmentId).resolves(assessment);
127218
assessmentRepository_updateLastQuestionDateStub.resolves();
128219
assessmentRepository_updateWhenNewChallengeIsAskedStub.resolves();
220+
answerRepository_findByAssessmentStub.resolves([]);
129221
});
130222

131223
it('should call usecase and return value from demo usecase', async function () {
@@ -148,6 +240,7 @@ describe('Shared | Unit | Domain | Use Cases | get-next-challenge', function ()
148240
assessmentRepository_getStub.withArgs(assessmentId).resolves(assessment);
149241
assessmentRepository_updateLastQuestionDateStub.resolves();
150242
assessmentRepository_updateWhenNewChallengeIsAskedStub.resolves();
243+
answerRepository_findByAssessmentStub.resolves([]);
151244
});
152245

153246
it('should call usecase and return value from campaign usecase', async function () {
@@ -172,6 +265,7 @@ describe('Shared | Unit | Domain | Use Cases | get-next-challenge', function ()
172265
assessmentRepository_getStub.withArgs(assessmentId).resolves(assessment);
173266
assessmentRepository_updateLastQuestionDateStub.resolves();
174267
assessmentRepository_updateWhenNewChallengeIsAskedStub.resolves();
268+
answerRepository_findByAssessmentStub.resolves([]);
175269
});
176270

177271
it('should call usecase and return value from competence evaluation usecase', async function () {
@@ -197,6 +291,7 @@ describe('Shared | Unit | Domain | Use Cases | get-next-challenge', function ()
197291
assessmentRepository_getStub.withArgs(assessmentId).resolves(assessment);
198292
assessmentRepository_updateLastQuestionDateStub.resolves();
199293
assessmentRepository_updateWhenNewChallengeIsAskedStub.resolves();
294+
answerRepository_findByAssessmentStub.resolves([]);
200295
});
201296

202297
it('should call usecase and return value from certification usecase', async function () {
@@ -222,6 +317,7 @@ describe('Shared | Unit | Domain | Use Cases | get-next-challenge', function ()
222317
assessmentRepository_getStub.withArgs(assessmentId).resolves(assessment);
223318
assessmentRepository_updateLastQuestionDateStub.resolves();
224319
assessmentRepository_updateWhenNewChallengeIsAskedStub.resolves();
320+
answerRepository_findByAssessmentStub.resolves([]);
225321
});
226322

227323
it('should return null', async function () {
@@ -246,6 +342,7 @@ describe('Shared | Unit | Domain | Use Cases | get-next-challenge', function ()
246342
});
247343
evaluationUsecases_getNextChallengeForPreviewStub.withArgs({}).resolves({ id: 'someChallengeId' });
248344
assessmentRepository_updateWhenNewChallengeIsAskedStub.resolves();
345+
answerRepository_findByAssessmentStub.resolves([]);
249346
});
250347

251348
afterEach(async function () {
@@ -267,6 +364,7 @@ describe('Shared | Unit | Domain | Use Cases | get-next-challenge', function ()
267364
context('updating last challenge asked', function () {
268365
beforeEach(function () {
269366
assessmentRepository_updateLastQuestionDateStub.resolves();
367+
answerRepository_findByAssessmentStub.resolves([]);
270368
});
271369

272370
context('when no next challenge has been found', function () {
@@ -293,6 +391,9 @@ describe('Shared | Unit | Domain | Use Cases | get-next-challenge', function ()
293391
type: Assessment.types.PREVIEW,
294392
lastChallengeId: 'currentChallengeId',
295393
});
394+
answerRepository_findByAssessmentStub.resolves([
395+
domainBuilder.buildAnswer({ challengeId: 'currentChallengeId' }),
396+
]);
296397
assessmentRepository_getStub.withArgs(assessmentId).resolves(assessment);
297398
evaluationUsecases_getNextChallengeForPreviewStub
298399
.withArgs({})
@@ -312,6 +413,9 @@ describe('Shared | Unit | Domain | Use Cases | get-next-challenge', function ()
312413
type: Assessment.types.PREVIEW,
313414
lastChallengeId: 'previousChallengeId',
314415
});
416+
answerRepository_findByAssessmentStub.resolves([
417+
domainBuilder.buildAnswer({ challengeId: 'previousChallengeId' }),
418+
]);
315419
assessmentRepository_getStub.withArgs(assessmentId).resolves(assessment);
316420
evaluationUsecases_getNextChallengeForPreviewStub
317421
.withArgs({})

0 commit comments

Comments
 (0)