Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Enregistrer le début d'un passage en tant qu'événement (PIX-16812) #11567

Merged
merged 4 commits into from
Mar 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions api/src/devcomp/application/passages/controller.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
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 passage = await usecases.createPassage({ moduleId, userId });
const requestTimestamp = requestResponseUtils.extractTimestampFromRequest(request);
const userId = requestResponseUtils.extractUserIdFromRequest(request);
const passage = await usecases.createPassage({ moduleId, userId, occurredAt: new Date(requestTimestamp) });

const serializedPassage = passageSerializer.serialize(passage);
return h.response(serializedPassage).created();
Expand Down
34 changes: 28 additions & 6 deletions api/src/devcomp/domain/usecases/create-passage.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
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 _verifyIfModuleExists({ 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 });
};

async function _verifyIfModuleExists({ moduleId, moduleRepository }) {
const passage = await passageRepository.save({ moduleId, userId });
await _recordPassageEvent({ module, occurredAt, passage, passageEventRepository });

return passage;
});

async function _getModule({ moduleId, moduleRepository }) {
try {
await moduleRepository.getBySlug({ slug: moduleId });
return await moduleRepository.getBySlug({ slug: moduleId });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vu ensemble, le await est nécessaire car on est dans un try/catch 💡

} catch (e) {
if (e instanceof NotFoundError) {
throw new ModuleDoesNotExistError();
Expand All @@ -20,4 +34,12 @@ async function _verifyIfModuleExists({ 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 };
19 changes: 12 additions & 7 deletions api/tests/devcomp/unit/application/passages/controller_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
};
Expand All @@ -26,18 +22,27 @@ 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);
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, {
passageSerializer,
usecases,
extractUserIdFromRequest,
});

// then
expect(created).to.have.been.called;
expect(extractTimestampStub).to.have.been.calledOnce;
});
});

Expand Down
53 changes: 46 additions & 7 deletions api/tests/devcomp/unit/domain/usecases/create-passage_test.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
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 an ModuleNotExists', async function () {
it('should throw a ModuleDoesNotExist error', async function () {
// given
const moduleId = Symbol('moduleId');

Expand All @@ -24,7 +32,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');

Expand All @@ -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(),
Expand All @@ -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,
});
Expand All @@ -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);
});
});