From e5664dd183460053c3b414083519e292e24578b5 Mon Sep 17 00:00:00 2001 From: Eric Lim Date: Thu, 6 Mar 2025 16:17:28 +0100 Subject: [PATCH 1/4] refactor(api): use requestResponseUtils import in create passage controller Co-authored-by: Diane Cordier --- api/src/devcomp/application/passages/controller.js | 4 ++-- .../devcomp/unit/application/passages/controller_test.js | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/api/src/devcomp/application/passages/controller.js b/api/src/devcomp/application/passages/controller.js index bb2af07fc99..b8cfe007bdd 100644 --- a/api/src/devcomp/application/passages/controller.js +++ b/api/src/devcomp/application/passages/controller.js @@ -1,8 +1,8 @@ import { requestResponseUtils } from '../../../shared/infrastructure/utils/request-response-utils.js'; -const create = async function (request, h, { usecases, passageSerializer, extractUserIdFromRequest }) { +const create = async function (request, h, { usecases, passageSerializer }) { const { 'module-id': moduleId } = request.payload.data.attributes; - const userId = extractUserIdFromRequest(request); + const userId = requestResponseUtils.extractUserIdFromRequest(request); const passage = await usecases.createPassage({ moduleId, userId }); const serializedPassage = passageSerializer.serialize(passage); diff --git a/api/tests/devcomp/unit/application/passages/controller_test.js b/api/tests/devcomp/unit/application/passages/controller_test.js index f6fd3ae0daf..ce0eae4ec4d 100644 --- a/api/tests/devcomp/unit/application/passages/controller_test.js +++ b/api/tests/devcomp/unit/application/passages/controller_test.js @@ -26,14 +26,13 @@ describe('Unit | Devcomp | Application | Passages | Controller', function () { const request = { payload: { data: { attributes: { 'module-id': moduleId } } } }; - const extractUserIdFromRequest = sinon.stub(); - extractUserIdFromRequest.withArgs(request).returns(userId); + const extractUserIdFromRequestStub = sinon.stub(requestResponseUtils, 'extractUserIdFromRequest'); + extractUserIdFromRequestStub.withArgs(request).returns(userId); // when await passageController.create({ payload: { data: { attributes: { 'module-id': moduleId } } } }, hStub, { passageSerializer, usecases, - extractUserIdFromRequest, }); // then From 8c5b16a459211c666dedff960142fe1d97b3bb32 Mon Sep 17 00:00:00 2001 From: Eric Lim Date: Thu, 6 Mar 2025 16:26:26 +0100 Subject: [PATCH 2/4] feat(api): pass request timestamp in create passage controller to usecase Co-authored-by: Diane Cordier --- api/src/devcomp/application/passages/controller.js | 3 ++- .../unit/application/passages/controller_test.js | 14 ++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/api/src/devcomp/application/passages/controller.js b/api/src/devcomp/application/passages/controller.js index b8cfe007bdd..13cc4ba72dc 100644 --- a/api/src/devcomp/application/passages/controller.js +++ b/api/src/devcomp/application/passages/controller.js @@ -2,8 +2,9 @@ import { requestResponseUtils } from '../../../shared/infrastructure/utils/reque const create = async function (request, h, { usecases, passageSerializer }) { const { 'module-id': moduleId } = request.payload.data.attributes; + const requestTimestamp = requestResponseUtils.extractTimestampFromRequest(request); const userId = requestResponseUtils.extractUserIdFromRequest(request); - const passage = await usecases.createPassage({ moduleId, userId }); + const passage = await usecases.createPassage({ moduleId, userId, occurredAt: new Date(requestTimestamp) }); const serializedPassage = passageSerializer.serialize(passage); return h.response(serializedPassage).created(); diff --git a/api/tests/devcomp/unit/application/passages/controller_test.js b/api/tests/devcomp/unit/application/passages/controller_test.js index ce0eae4ec4d..89811a2cdde 100644 --- a/api/tests/devcomp/unit/application/passages/controller_test.js +++ b/api/tests/devcomp/unit/application/passages/controller_test.js @@ -10,10 +10,6 @@ describe('Unit | Devcomp | Application | Passages | Controller', function () { const moduleId = Symbol('module-id'); const passage = Symbol('passage'); const userId = Symbol('user-id'); - const usecases = { - createPassage: sinon.stub(), - }; - usecases.createPassage.withArgs({ moduleId, userId }).returns(passage); const passageSerializer = { serialize: sinon.stub(), }; @@ -28,6 +24,15 @@ describe('Unit | Devcomp | Application | Passages | Controller', function () { const extractUserIdFromRequestStub = sinon.stub(requestResponseUtils, 'extractUserIdFromRequest'); extractUserIdFromRequestStub.withArgs(request).returns(userId); + const requestTimestamp = new Date('2025-01-01').getTime(); + const extractTimestampStub = sinon + .stub(requestResponseUtils, 'extractTimestampFromRequest') + .returns(requestTimestamp); + + const usecases = { + createPassage: sinon.stub(), + }; + usecases.createPassage.withArgs({ moduleId, userId, occurredAt: new Date(requestTimestamp) }).returns(passage); // when await passageController.create({ payload: { data: { attributes: { 'module-id': moduleId } } } }, hStub, { @@ -37,6 +42,7 @@ describe('Unit | Devcomp | Application | Passages | Controller', function () { // then expect(created).to.have.been.called; + expect(extractTimestampStub).to.have.been.calledOnce; }); }); From e4e709ce6794e858908c27877039cb93c867720b Mon Sep 17 00:00:00 2001 From: Eric Lim Date: Wed, 5 Mar 2025 08:49:50 +0100 Subject: [PATCH 3/4] feat(api): change create passage module check by returning module for record purposes --- api/src/devcomp/domain/usecases/create-passage.js | 6 +++--- .../devcomp/unit/domain/usecases/create-passage_test.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/src/devcomp/domain/usecases/create-passage.js b/api/src/devcomp/domain/usecases/create-passage.js index 61f1b7f7601..a6e77ff6cd2 100644 --- a/api/src/devcomp/domain/usecases/create-passage.js +++ b/api/src/devcomp/domain/usecases/create-passage.js @@ -2,16 +2,16 @@ import { NotFoundError } from '../../../shared/domain/errors.js'; import { ModuleDoesNotExistError } from '../errors.js'; const createPassage = async function ({ moduleId, userId, moduleRepository, passageRepository, userRepository }) { - await _verifyIfModuleExists({ moduleId, moduleRepository }); + await _getModule({ moduleId, moduleRepository }); if (userId !== null) { await userRepository.get(userId); } return passageRepository.save({ moduleId, userId }); }; -async function _verifyIfModuleExists({ moduleId, moduleRepository }) { +async function _getModule({ moduleId, moduleRepository }) { try { - await moduleRepository.getBySlug({ slug: moduleId }); + return await moduleRepository.getBySlug({ slug: moduleId }); } catch (e) { if (e instanceof NotFoundError) { throw new ModuleDoesNotExistError(); diff --git a/api/tests/devcomp/unit/domain/usecases/create-passage_test.js b/api/tests/devcomp/unit/domain/usecases/create-passage_test.js index 972571b9395..31de4613e33 100644 --- a/api/tests/devcomp/unit/domain/usecases/create-passage_test.js +++ b/api/tests/devcomp/unit/domain/usecases/create-passage_test.js @@ -6,7 +6,7 @@ import { catchErr, expect, sinon } from '../../../../test-helper.js'; describe('Unit | Devcomp | Domain | UseCases | create-passage', function () { describe('when module does not exist', function () { - it('should throw an ModuleNotExists', async function () { + it('should throw a ModuleDoesNotExist error', async function () { // given const moduleId = Symbol('moduleId'); @@ -24,7 +24,7 @@ describe('Unit | Devcomp | Domain | UseCases | create-passage', function () { }); describe('when user does not exist', function () { - it('should throw an UserNotExists', async function () { + it('should throw an UserNotExists error', async function () { // given const userId = Symbol('userId'); From b343e33990196d4faab839db4a4d36527c3296e8 Mon Sep 17 00:00:00 2001 From: Eric Lim Date: Wed, 5 Mar 2025 09:11:03 +0100 Subject: [PATCH 4/4] feat(api): record passage started event by using module content as content hash --- .../devcomp/domain/usecases/create-passage.js | 30 ++++++++++-- .../domain/usecases/create-passage_test.js | 49 +++++++++++++++++-- 2 files changed, 70 insertions(+), 9 deletions(-) diff --git a/api/src/devcomp/domain/usecases/create-passage.js b/api/src/devcomp/domain/usecases/create-passage.js index a6e77ff6cd2..ac1d0d9f8b2 100644 --- a/api/src/devcomp/domain/usecases/create-passage.js +++ b/api/src/devcomp/domain/usecases/create-passage.js @@ -1,13 +1,27 @@ +import { withTransaction } from '../../../shared/domain/DomainTransaction.js'; import { NotFoundError } from '../../../shared/domain/errors.js'; import { ModuleDoesNotExistError } from '../errors.js'; +import { PassageStartedEvent } from '../models/passage-events/passage-events.js'; -const createPassage = async function ({ moduleId, userId, moduleRepository, passageRepository, userRepository }) { - await _getModule({ moduleId, moduleRepository }); +const createPassage = withTransaction(async function ({ + moduleId, + occurredAt, + userId, + moduleRepository, + passageRepository, + passageEventRepository, + userRepository, +}) { + const module = await _getModule({ moduleId, moduleRepository }); if (userId !== null) { await userRepository.get(userId); } - return passageRepository.save({ moduleId, userId }); -}; + + const passage = await passageRepository.save({ moduleId, userId }); + await _recordPassageEvent({ module, occurredAt, passage, passageEventRepository }); + + return passage; +}); async function _getModule({ moduleId, moduleRepository }) { try { @@ -20,4 +34,12 @@ async function _getModule({ moduleId, moduleRepository }) { } } +async function _recordPassageEvent({ module, occurredAt, passage, passageEventRepository }) { + const { id: passageId } = passage; + const contentHash = module.version; + const passageStartedEvent = new PassageStartedEvent({ contentHash, passageId, occurredAt }); + + await passageEventRepository.record(passageStartedEvent); +} + export { createPassage }; diff --git a/api/tests/devcomp/unit/domain/usecases/create-passage_test.js b/api/tests/devcomp/unit/domain/usecases/create-passage_test.js index 31de4613e33..1c122b3b09f 100644 --- a/api/tests/devcomp/unit/domain/usecases/create-passage_test.js +++ b/api/tests/devcomp/unit/domain/usecases/create-passage_test.js @@ -1,10 +1,18 @@ import { ModuleDoesNotExistError } from '../../../../../src/devcomp/domain/errors.js'; +import { Module } from '../../../../../src/devcomp/domain/models/module/Module.js'; +import { Passage } from '../../../../../src/devcomp/domain/models/Passage.js'; +import { PassageStartedEvent } from '../../../../../src/devcomp/domain/models/passage-events/passage-events.js'; import { createPassage } from '../../../../../src/devcomp/domain/usecases/create-passage.js'; +import { DomainTransaction } from '../../../../../src/shared/domain/DomainTransaction.js'; import { UserNotFoundError } from '../../../../../src/shared/domain/errors.js'; import { NotFoundError } from '../../../../../src/shared/domain/errors.js'; import { catchErr, expect, sinon } from '../../../../test-helper.js'; describe('Unit | Devcomp | Domain | UseCases | create-passage', function () { + beforeEach(function () { + sinon.stub(DomainTransaction, 'execute').callsFake((lambda) => lambda()); + }); + describe('when module does not exist', function () { it('should throw a ModuleDoesNotExist error', async function () { // given @@ -48,11 +56,35 @@ describe('Unit | Devcomp | Domain | UseCases | create-passage', function () { }); }); - it('should call passage repository to save the passage', async function () { + it('should save the passage and record passage started event', async function () { // given const moduleId = Symbol('moduleId'); + const passageId = Symbol('passageId'); const userId = Symbol('userId'); - const repositoryResult = Symbol('repository-result'); + + const slug = 'les-adresses-email'; + const title = 'Les adresses email'; + const isBeta = false; + const grains = [Symbol('text')]; + const transitionTexts = []; + const details = Symbol('details'); + const version = Symbol('version'); + const module = new Module({ id: moduleId, slug, title, isBeta, grains, details, transitionTexts, version }); + + const occurredAt = new Date('2025-01-01'); + const passageCreatedAt = new Date('2025-03-05'); + const passage = new Passage({ + id: passageId, + moduleId, + userId, + createdAt: passageCreatedAt, + }); + + const passageStartedEvent = new PassageStartedEvent({ + contentHash: version, + occurredAt, + passageId, + }); const userRepositoryStub = { get: sinon.stub(), @@ -61,17 +93,23 @@ describe('Unit | Devcomp | Domain | UseCases | create-passage', function () { const moduleRepositoryStub = { getBySlug: sinon.stub(), }; - moduleRepositoryStub.getBySlug.withArgs({ slug: moduleId }).resolves(); + moduleRepositoryStub.getBySlug.withArgs({ slug: moduleId }).resolves(module); const passageRepositoryStub = { save: sinon.stub(), }; - passageRepositoryStub.save.resolves(repositoryResult); + passageRepositoryStub.save.resolves(passage); + + const passageEventRepositoryStub = { + record: sinon.stub(), + }; // when const result = await createPassage({ + occurredAt, moduleId, userId, passageRepository: passageRepositoryStub, + passageEventRepository: passageEventRepositoryStub, moduleRepository: moduleRepositoryStub, userRepository: userRepositoryStub, }); @@ -81,6 +119,7 @@ describe('Unit | Devcomp | Domain | UseCases | create-passage', function () { moduleId, userId, }); - expect(result).to.equal(repositoryResult); + expect(passageEventRepositoryStub.record).to.have.been.calledOnceWith(passageStartedEvent); + expect(result).to.equal(passage); }); });