Skip to content

Commit 8091084

Browse files
tech(api): return latest challenge asked if not answered yet in assessments/:id/next
1 parent c04badc commit 8091084

File tree

4 files changed

+150
-0
lines changed

4 files changed

+150
-0
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

+100
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
};
@@ -92,6 +109,75 @@ describe('Shared | Unit | Domain | Use Cases | get-next-challenge', function ()
92109
});
93110
});
94111

112+
context('latest challenge asked', function () {
113+
context('when there are no challenge saved as latest challenge asked in assessment', function () {
114+
it('should compute next challenge', async function () {
115+
const assessment = domainBuilder.buildAssessment({
116+
state: Assessment.states.STARTED,
117+
type: Assessment.types.PREVIEW,
118+
lastChallengeId: null,
119+
});
120+
assessmentRepository_getStub.withArgs(assessmentId).resolves(assessment);
121+
assessmentRepository_updateLastQuestionDateStub.resolves();
122+
assessmentRepository_updateWhenNewChallengeIsAskedStub.resolves();
123+
const challenge = domainBuilder.buildChallenge({ id: 'challengeForPreview' });
124+
evaluationUsecases_getNextChallengeForPreviewStub.withArgs({}).resolves(challenge);
125+
answerRepository_findByAssessmentStub.withArgs(assessment.id).resolves([domainBuilder.buildAnswer()]);
126+
127+
const actualNextChallenge = await getNextChallenge(dependencies);
128+
129+
expect(actualNextChallenge).to.deepEqualInstance(challenge);
130+
});
131+
});
132+
133+
context('when the latest challenge asked has been answered', function () {
134+
it('should compute next challenge', async function () {
135+
const assessment = domainBuilder.buildAssessment({
136+
state: Assessment.states.STARTED,
137+
type: Assessment.types.PREVIEW,
138+
lastChallengeId: 'previousChallengeId',
139+
});
140+
assessmentRepository_getStub.withArgs(assessmentId).resolves(assessment);
141+
assessmentRepository_updateLastQuestionDateStub.resolves();
142+
assessmentRepository_updateWhenNewChallengeIsAskedStub.resolves();
143+
const challenge = domainBuilder.buildChallenge({ id: 'challengeForPreview' });
144+
evaluationUsecases_getNextChallengeForPreviewStub.withArgs({}).resolves(challenge);
145+
answerRepository_findByAssessmentStub
146+
.withArgs(assessment.id)
147+
.resolves([domainBuilder.buildAnswer({ challengeId: 'previousChallengeId' })]);
148+
149+
const actualNextChallenge = await getNextChallenge(dependencies);
150+
151+
expect(actualNextChallenge).to.deepEqualInstance(challenge);
152+
});
153+
});
154+
155+
context('when the latest challenge asked has not been answered yet', function () {
156+
it('should return latest challenge', async function () {
157+
const assessment = domainBuilder.buildAssessment({
158+
state: Assessment.states.STARTED,
159+
type: Assessment.types.PREVIEW,
160+
lastChallengeId: 'previousChallengeId',
161+
});
162+
assessmentRepository_getStub.withArgs(assessmentId).resolves(assessment);
163+
assessmentRepository_updateLastQuestionDateStub.resolves();
164+
assessmentRepository_updateWhenNewChallengeIsAskedStub.resolves();
165+
const previousChallenge = domainBuilder.buildChallenge({ id: 'previousChallengeId' });
166+
answerRepository_findByAssessmentStub
167+
.withArgs(assessment.id)
168+
.resolves([
169+
domainBuilder.buildAnswer({ challengeId: 'someChallengeId' }),
170+
domainBuilder.buildAnswer({ challengeId: 'someOtherChallengeId' }),
171+
]);
172+
challengeRepository_getStub.withArgs('previousChallengeId').resolves(previousChallenge);
173+
174+
const actualNextChallenge = await getNextChallenge(dependencies);
175+
176+
expect(actualNextChallenge).to.deepEqualInstance(previousChallenge);
177+
});
178+
});
179+
});
180+
95181
context('Assessment types', function () {
96182
context('for assessment of type PREVIEW', function () {
97183
let assessment;
@@ -104,6 +190,7 @@ describe('Shared | Unit | Domain | Use Cases | get-next-challenge', function ()
104190
assessmentRepository_getStub.withArgs(assessmentId).resolves(assessment);
105191
assessmentRepository_updateLastQuestionDateStub.resolves();
106192
assessmentRepository_updateWhenNewChallengeIsAskedStub.resolves();
193+
answerRepository_findByAssessmentStub.resolves([]);
107194
});
108195

109196
it('should call usecase and return value from preview usecase', async function () {
@@ -126,6 +213,7 @@ describe('Shared | Unit | Domain | Use Cases | get-next-challenge', function ()
126213
assessmentRepository_getStub.withArgs(assessmentId).resolves(assessment);
127214
assessmentRepository_updateLastQuestionDateStub.resolves();
128215
assessmentRepository_updateWhenNewChallengeIsAskedStub.resolves();
216+
answerRepository_findByAssessmentStub.resolves([]);
129217
});
130218

131219
it('should call usecase and return value from demo usecase', async function () {
@@ -148,6 +236,7 @@ describe('Shared | Unit | Domain | Use Cases | get-next-challenge', function ()
148236
assessmentRepository_getStub.withArgs(assessmentId).resolves(assessment);
149237
assessmentRepository_updateLastQuestionDateStub.resolves();
150238
assessmentRepository_updateWhenNewChallengeIsAskedStub.resolves();
239+
answerRepository_findByAssessmentStub.resolves([]);
151240
});
152241

153242
it('should call usecase and return value from campaign usecase', async function () {
@@ -172,6 +261,7 @@ describe('Shared | Unit | Domain | Use Cases | get-next-challenge', function ()
172261
assessmentRepository_getStub.withArgs(assessmentId).resolves(assessment);
173262
assessmentRepository_updateLastQuestionDateStub.resolves();
174263
assessmentRepository_updateWhenNewChallengeIsAskedStub.resolves();
264+
answerRepository_findByAssessmentStub.resolves([]);
175265
});
176266

177267
it('should call usecase and return value from competence evaluation usecase', async function () {
@@ -197,6 +287,7 @@ describe('Shared | Unit | Domain | Use Cases | get-next-challenge', function ()
197287
assessmentRepository_getStub.withArgs(assessmentId).resolves(assessment);
198288
assessmentRepository_updateLastQuestionDateStub.resolves();
199289
assessmentRepository_updateWhenNewChallengeIsAskedStub.resolves();
290+
answerRepository_findByAssessmentStub.resolves([]);
200291
});
201292

202293
it('should call usecase and return value from certification usecase', async function () {
@@ -222,6 +313,7 @@ describe('Shared | Unit | Domain | Use Cases | get-next-challenge', function ()
222313
assessmentRepository_getStub.withArgs(assessmentId).resolves(assessment);
223314
assessmentRepository_updateLastQuestionDateStub.resolves();
224315
assessmentRepository_updateWhenNewChallengeIsAskedStub.resolves();
316+
answerRepository_findByAssessmentStub.resolves([]);
225317
});
226318

227319
it('should return null', async function () {
@@ -246,6 +338,7 @@ describe('Shared | Unit | Domain | Use Cases | get-next-challenge', function ()
246338
});
247339
evaluationUsecases_getNextChallengeForPreviewStub.withArgs({}).resolves({ id: 'someChallengeId' });
248340
assessmentRepository_updateWhenNewChallengeIsAskedStub.resolves();
341+
answerRepository_findByAssessmentStub.resolves([]);
249342
});
250343

251344
afterEach(async function () {
@@ -267,6 +360,7 @@ describe('Shared | Unit | Domain | Use Cases | get-next-challenge', function ()
267360
context('updating last challenge asked', function () {
268361
beforeEach(function () {
269362
assessmentRepository_updateLastQuestionDateStub.resolves();
363+
answerRepository_findByAssessmentStub.resolves([]);
270364
});
271365

272366
context('when no next challenge has been found', function () {
@@ -293,6 +387,9 @@ describe('Shared | Unit | Domain | Use Cases | get-next-challenge', function ()
293387
type: Assessment.types.PREVIEW,
294388
lastChallengeId: 'currentChallengeId',
295389
});
390+
answerRepository_findByAssessmentStub.resolves([
391+
domainBuilder.buildAnswer({ challengeId: 'currentChallengeId' }),
392+
]);
296393
assessmentRepository_getStub.withArgs(assessmentId).resolves(assessment);
297394
evaluationUsecases_getNextChallengeForPreviewStub
298395
.withArgs({})
@@ -312,6 +409,9 @@ describe('Shared | Unit | Domain | Use Cases | get-next-challenge', function ()
312409
type: Assessment.types.PREVIEW,
313410
lastChallengeId: 'previousChallengeId',
314411
});
412+
answerRepository_findByAssessmentStub.resolves([
413+
domainBuilder.buildAnswer({ challengeId: 'previousChallengeId' }),
414+
]);
315415
assessmentRepository_getStub.withArgs(assessmentId).resolves(assessment);
316416
evaluationUsecases_getNextChallengeForPreviewStub
317417
.withArgs({})

0 commit comments

Comments
 (0)