Skip to content

Commit ad8ee93

Browse files
authored
[FEATURE] Sauvegarder les événements PASSAGE_TERMINATED(PIX-16811) (#11559)
2 parents a0b9c97 + 97b6628 commit ad8ee93

File tree

7 files changed

+111
-11
lines changed

7 files changed

+111
-11
lines changed

api/src/devcomp/application/passages/controller.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { requestResponseUtils } from '../../../shared/infrastructure/utils/request-response-utils.js';
2+
13
const create = async function (request, h, { usecases, passageSerializer, extractUserIdFromRequest }) {
24
const { 'module-id': moduleId } = request.payload.data.attributes;
35
const userId = extractUserIdFromRequest(request);
@@ -17,7 +19,8 @@ const verifyAndSaveAnswer = async function (request, h, { usecases, elementAnswe
1719

1820
const terminate = async function (request, h, { usecases, passageSerializer }) {
1921
const { passageId } = request.params;
20-
const updatedPassage = await usecases.terminatePassage({ passageId });
22+
const requestTimestamp = requestResponseUtils.extractTimestampFromRequest(request);
23+
const updatedPassage = await usecases.terminatePassage({ passageId, occurredAt: new Date(requestTimestamp) });
2124
return passageSerializer.serialize(updatedPassage);
2225
};
2326

api/src/devcomp/domain/usecases/terminate-passage.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
1+
import { withTransaction } from '../../../shared/domain/DomainTransaction.js';
12
import { NotFoundError } from '../../../shared/domain/errors.js';
23
import { PassageDoesNotExistError, PassageTerminatedError } from '../errors.js';
4+
import { PassageTerminatedEvent } from '../models/passage-events/passage-events.js';
35

4-
async function terminatePassage({ passageId, passageRepository }) {
6+
const terminatePassage = withTransaction(async function ({
7+
passageId,
8+
occurredAt,
9+
passageRepository,
10+
passageEventRepository,
11+
}) {
512
const passage = await _getPassage({ passageId, passageRepository });
613
if (passage.terminatedAt) {
714
throw new PassageTerminatedError();
815
}
916
passage.terminate();
10-
return passageRepository.update({ passage });
11-
}
17+
const terminatedPassage = await passageRepository.update({ passage });
18+
const event = new PassageTerminatedEvent({ passageId, occurredAt });
19+
await passageEventRepository.record(event);
20+
return terminatedPassage;
21+
});
1222

1323
async function _getPassage({ passageId, passageRepository }) {
1424
try {

api/src/devcomp/infrastructure/repositories/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import moduleDatasource from '../datasources/learning-content/module-datasource.
33
import * as elementAnswerRepository from './element-answer-repository.js';
44
import * as elementRepository from './element-repository.js';
55
import * as moduleRepository from './module-repository.js';
6+
import * as passageEventRepository from './passage-event-repository.js';
67
import * as passageRepository from './passage-repository.js';
78
import * as trainingRepository from './training-repository.js';
89
import * as trainingTriggerRepository from './training-trigger-repository.js';
@@ -15,6 +16,7 @@ const repositoriesWithoutInjectedDependencies = {
1516
elementAnswerRepository,
1617
elementRepository,
1718
moduleRepository,
19+
passageEventRepository,
1820
passageRepository,
1921
trainingRepository,
2022
trainingTriggerRepository,

api/src/shared/infrastructure/utils/request-response-utils.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ import { tokenService } from '../../../shared/domain/services/token-service.js';
66

77
const { ENGLISH_SPOKEN, FRENCH_FRANCE, FRENCH_SPOKEN } = LOCALE;
88
const { DUTCH, SPANISH } = LANGUAGES_CODE;
9-
const requestResponseUtils = { escapeFileName, extractUserIdFromRequest, extractLocaleFromRequest };
10-
11-
export { escapeFileName, extractLocaleFromRequest, extractUserIdFromRequest, requestResponseUtils };
9+
const requestResponseUtils = {
10+
escapeFileName,
11+
extractUserIdFromRequest,
12+
extractLocaleFromRequest,
13+
extractTimestampFromRequest,
14+
};
1215

1316
function escapeFileName(fileName) {
1417
return fileName
@@ -37,3 +40,15 @@ function extractLocaleFromRequest(request) {
3740
const acceptedLanguages = [ENGLISH_SPOKEN, FRENCH_SPOKEN, FRENCH_FRANCE, DUTCH, SPANISH];
3841
return accept.language(languageHeader, acceptedLanguages) || defaultLocale;
3942
}
43+
44+
function extractTimestampFromRequest(request) {
45+
return request.headers?.['X-Request-Start'] ?? new Date().getTime();
46+
}
47+
48+
export {
49+
escapeFileName,
50+
extractLocaleFromRequest,
51+
extractTimestampFromRequest,
52+
extractUserIdFromRequest,
53+
requestResponseUtils,
54+
};

api/tests/devcomp/unit/application/passages/controller_test.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { passageController } from '../../../../../src/devcomp/application/passages/controller.js';
2+
import { requestResponseUtils } from '../../../../../src/shared/infrastructure/utils/request-response-utils.js';
23
import { expect, sinon } from '../../../../test-helper.js';
34

45
describe('Unit | Devcomp | Application | Passages | Controller', function () {
@@ -90,13 +91,17 @@ describe('Unit | Devcomp | Application | Passages | Controller', function () {
9091
describe('#terminate', function () {
9192
it('should call terminate use-case and return serialized passage', async function () {
9293
// given
94+
const requestTimestamp = new Date('2025-01-01').getTime();
9395
const serializedPassage = Symbol('serialized modules');
9496
const passageId = Symbol('passage-id');
9597
const passage = Symbol('passage');
98+
const extractTimestampStub = sinon
99+
.stub(requestResponseUtils, 'extractTimestampFromRequest')
100+
.returns(requestTimestamp);
96101
const usecases = {
97102
terminatePassage: sinon.stub(),
98103
};
99-
usecases.terminatePassage.withArgs({ passageId }).returns(passage);
104+
usecases.terminatePassage.withArgs({ passageId, occurredAt: new Date(requestTimestamp) }).returns(passage);
100105
const passageSerializer = {
101106
serialize: sinon.stub(),
102107
};
@@ -110,6 +115,7 @@ describe('Unit | Devcomp | Application | Passages | Controller', function () {
110115

111116
// then
112117
expect(returned).to.deep.equal(serializedPassage);
118+
expect(extractTimestampStub).to.have.been.calledOnce;
113119
});
114120
});
115121
});

api/tests/devcomp/unit/domain/usecases/terminate-passage_test.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import { PassageDoesNotExistError, PassageTerminatedError } from '../../../../../src/devcomp/domain/errors.js';
2+
import { PassageTerminatedEvent } from '../../../../../src/devcomp/domain/models/passage-events/passage-events.js';
23
import { terminatePassage } from '../../../../../src/devcomp/domain/usecases/terminate-passage.js';
4+
import { DomainTransaction } from '../../../../../src/shared/domain/DomainTransaction.js';
35
import { NotFoundError } from '../../../../../src/shared/domain/errors.js';
46
import { catchErr, expect, sinon } from '../../../../test-helper.js';
57

68
describe('Unit | Devcomp | Domain | UseCases | terminate-passage', function () {
9+
beforeEach(function () {
10+
sinon.stub(DomainTransaction, 'execute').callsFake((lambda) => lambda());
11+
});
12+
713
describe('#terminatePassage', function () {
814
describe('when passage is not found', function () {
915
it('should throw a PassageDoesNotExistError', async function () {
@@ -45,32 +51,45 @@ describe('Unit | Devcomp | Domain | UseCases | terminate-passage', function () {
4551
expect(error).to.be.instanceof(PassageTerminatedError);
4652
});
4753

48-
it('should call terminate method and update passage and return it', async function () {
54+
it('should call terminate method and update passage and return it, then record an event', async function () {
4955
// given
5056
const passageId = Symbol('passageId');
57+
const occurredAt = new Date('2025-01-01');
5158

5259
const passageRepository = {
5360
get: sinon.stub(),
5461
update: sinon.stub(),
5562
};
63+
const passageEventRepository = {
64+
record: sinon.stub(),
65+
};
66+
5667
const passage = {
5768
terminatedAt: null,
5869
terminate: sinon.stub(),
5970
};
6071
passageRepository.get.withArgs({ passageId }).resolves(passage);
6172

62-
const updatedPassage = Symbol();
73+
const updatedPassage = {
74+
terminatedAt: new Date('2025-03-04'),
75+
id: passageId,
76+
};
6377
passageRepository.update.withArgs({ passage }).resolves(updatedPassage);
6478

79+
const event = new PassageTerminatedEvent({ passageId, occurredAt });
80+
6581
// when
6682
const returnedPassage = await terminatePassage({
6783
passageId,
84+
occurredAt,
6885
passageRepository,
86+
passageEventRepository,
6987
});
7088

7189
// then
7290
expect(passage.terminate).to.have.been.calledOnce;
7391
expect(returnedPassage).to.equal(updatedPassage);
92+
expect(passageEventRepository.record).to.have.been.calledOnceWithExactly(event);
7493
});
7594
});
7695
});

api/tests/shared/unit/infrastructure/utils/request-response-utils_test.js

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import { LOCALE } from '../../../../../src/shared/domain/constants.js';
22
import {
33
escapeFileName,
44
extractLocaleFromRequest,
5+
extractTimestampFromRequest,
56
extractUserIdFromRequest,
67
} from '../../../../../src/shared/infrastructure/utils/request-response-utils.js';
7-
import { expect, generateAuthenticatedUserRequestHeaders } from '../../../../test-helper.js';
8+
import { expect, generateAuthenticatedUserRequestHeaders, sinon } from '../../../../test-helper.js';
89

910
const { ENGLISH_SPOKEN, FRENCH_FRANCE, FRENCH_SPOKEN } = LOCALE;
1011

@@ -81,4 +82,48 @@ describe('Unit | Utils | Request Utils', function () {
8182
});
8283
});
8384
});
85+
86+
describe('#extractTimestampFromRequest', function () {
87+
context('when "X-Request-Start" header exist', function () {
88+
it('returns the value of the header', function () {
89+
// given
90+
const startDateTimestamp = new Date('2025-01-01').getTime();
91+
const request = {
92+
headers: {
93+
'X-Request-Start': startDateTimestamp,
94+
},
95+
};
96+
97+
// when
98+
const timestamp = extractTimestampFromRequest(request);
99+
100+
// then
101+
expect(timestamp).to.equal(startDateTimestamp);
102+
});
103+
});
104+
105+
context('when "X-Request-Start" header does not exist', function () {
106+
let clock, now;
107+
108+
beforeEach(function () {
109+
now = new Date('2023-09-12');
110+
clock = sinon.useFakeTimers({ now, toFake: ['Date'] });
111+
});
112+
113+
afterEach(function () {
114+
clock.restore();
115+
});
116+
117+
it('returns a new date in timestamp', function () {
118+
// given
119+
const request = {};
120+
121+
// when
122+
const timestamp = extractTimestampFromRequest(request);
123+
124+
// then
125+
expect(timestamp).to.equal(now.getTime());
126+
});
127+
});
128+
});
84129
});

0 commit comments

Comments
 (0)