From 2d7b9223671cab415a1d141ea485cb7ecffcb7fd Mon Sep 17 00:00:00 2001 From: Benjamin Petetot Date: Mon, 25 Nov 2024 17:06:48 +0100 Subject: [PATCH 1/9] feat(api): add countByUserId in organizations memberships repo --- .../repositories/membership.repository.js | 12 ++++++++ .../membership-repository.test.js | 29 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/api/src/team/infrastructure/repositories/membership.repository.js b/api/src/team/infrastructure/repositories/membership.repository.js index d9adf42dc7e..1724cd24855 100644 --- a/api/src/team/infrastructure/repositories/membership.repository.js +++ b/api/src/team/infrastructure/repositories/membership.repository.js @@ -14,6 +14,18 @@ const ORGANIZATIONS_TABLE = 'organizations'; const MEMBERSHIPS_TABLE = 'memberships'; const USERS_TABLE = 'users'; +/** + * Get the number of active memberships for a user + * + * @param {string} userId - The ID of the user + * @returns {Promise} - The number of active memberships + */ +export const countByUserId = async function (userId) { + const knexConnection = DomainTransaction.getConnection(); + const { count } = await knexConnection(MEMBERSHIPS_TABLE).where({ userId, disabledAt: null }).count('id').first(); + return count; +}; + export const create = async (userId, organizationId, organizationRole) => { const knexConnection = DomainTransaction.getConnection(); try { diff --git a/api/tests/team/integration/infrastructure/repositories/membership-repository.test.js b/api/tests/team/integration/infrastructure/repositories/membership-repository.test.js index 2cda06336ea..437c5c182d5 100644 --- a/api/tests/team/integration/infrastructure/repositories/membership-repository.test.js +++ b/api/tests/team/integration/infrastructure/repositories/membership-repository.test.js @@ -70,6 +70,35 @@ describe('Integration | Team | Infrastructure | Repository | membership-reposito }); }); + describe('#countByUserId', function () { + it("counts all the user's memberships in organizations", async function () { + // given + const userId = databaseBuilder.factory.buildUser().id; + databaseBuilder.factory.buildMembership({ userId }); + databaseBuilder.factory.buildMembership({ userId }); + databaseBuilder.factory.buildMembership(); + await databaseBuilder.commit(); + + // when + const membershipCount = await membershipRepository.countByUserId(userId); + + // then + expect(membershipCount).to.equal(2); + }); + + it('does not count disabled memberships', async function () { + // given + const { userId } = databaseBuilder.factory.buildMembership({ disabledAt: new Date() }); + await databaseBuilder.commit(); + + // when + const membershipCount = await membershipRepository.countByUserId(userId); + + // then + expect(membershipCount).to.equal(0); + }); + }); + describe('#get', function () { let existingMembershipId; From 968d00e1ac09fe4385bf8a755eb636e9ebc57850 Mon Sep 17 00:00:00 2001 From: Benjamin Petetot Date: Mon, 25 Nov 2024 17:07:17 +0100 Subject: [PATCH 2/9] feat(api): add countByUserId in certif center memberships repo --- ...tification-center-membership.repository.js | 18 +++++++++++- ...ation-center-membership.repository.test.js | 29 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/api/src/team/infrastructure/repositories/certification-center-membership.repository.js b/api/src/team/infrastructure/repositories/certification-center-membership.repository.js index a0035652a7b..8b6bb215252 100644 --- a/api/src/team/infrastructure/repositories/certification-center-membership.repository.js +++ b/api/src/team/infrastructure/repositories/certification-center-membership.repository.js @@ -61,6 +61,21 @@ const countActiveMembersForCertificationCenter = async function (certificationCe return count; }; +/** + * Get the number of active memberships for a user + * + * @param {string} userId - The ID of the user + * @returns {Promise} - The number of active memberships + */ +const countByUserId = async function (userId) { + const knexConnection = DomainTransaction.getConnection(); + const { count } = await knexConnection(CERTIFICATION_CENTER_MEMBERSHIP_TABLE_NAME) + .where({ userId, disabledAt: null }) + .count('id') + .first(); + return count; +}; + const create = async function ({ certificationCenterId, role, userId }) { await knex(CERTIFICATION_CENTER_MEMBERSHIP_TABLE_NAME).insert({ certificationCenterId, role, userId }); }; @@ -297,7 +312,7 @@ const findById = async function (certificationCenterMembershipId) { }; const findOneWithCertificationCenterIdAndUserId = async function ({ certificationCenterId, userId }) { - const certificationCenterMembership = await knex('certification-center-memberships') + const certificationCenterMembership = await knex(CERTIFICATION_CENTER_MEMBERSHIP_TABLE_NAME) .where({ certificationCenterId, userId }) .first(); @@ -332,6 +347,7 @@ async function findActiveAdminsByCertificationCenterId(certificationCenterId) { const certificationCenterMembershipRepository = { countActiveMembersForCertificationCenter, + countByUserId, create, disableById, disableMembershipsByUserId, diff --git a/api/tests/team/integration/infrastructure/repositories/certification-center-membership.repository.test.js b/api/tests/team/integration/infrastructure/repositories/certification-center-membership.repository.test.js index 0900548bd82..5a52efae0ff 100644 --- a/api/tests/team/integration/infrastructure/repositories/certification-center-membership.repository.test.js +++ b/api/tests/team/integration/infrastructure/repositories/certification-center-membership.repository.test.js @@ -48,6 +48,35 @@ describe('Integration | Team | Infrastructure | Repository | Certification Cente }); }); + describe('#countByUserId', function () { + it("counts all the user's memberships in certification centers", async function () { + // given + const userId = databaseBuilder.factory.buildUser().id; + databaseBuilder.factory.buildCertificationCenterMembership({ userId }); + databaseBuilder.factory.buildCertificationCenterMembership({ userId }); + databaseBuilder.factory.buildCertificationCenterMembership(); + await databaseBuilder.commit(); + + // when + const membershipCount = await certificationCenterMembershipRepository.countByUserId(userId); + + // then + expect(membershipCount).to.equal(2); + }); + + it('does not count disabled memberships', async function () { + // given + const { userId } = databaseBuilder.factory.buildCertificationCenterMembership({ disabledAt: new Date() }); + await databaseBuilder.commit(); + + // when + const membershipCount = await certificationCenterMembershipRepository.countByUserId(userId); + + // then + expect(membershipCount).to.equal(0); + }); + }); + describe('#create', function () { afterEach(async function () { await knex('certification-center-memberships').delete(); From f1079bad3a73ac8ae1cc5420b3c843776bee0269 Mon Sep 17 00:00:00 2001 From: Benjamin Petetot Date: Mon, 25 Nov 2024 17:07:47 +0100 Subject: [PATCH 3/9] feat(api): get user teams access use case --- .../usecases/get-user-teams-info.usecase.js | 26 +++++++++++ .../get-user-teams-info.usecase.test.js | 44 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 api/src/team/domain/usecases/get-user-teams-info.usecase.js create mode 100644 api/tests/team/integration/domain/usecases/get-user-teams-info.usecase.test.js diff --git a/api/src/team/domain/usecases/get-user-teams-info.usecase.js b/api/src/team/domain/usecases/get-user-teams-info.usecase.js new file mode 100644 index 00000000000..40a7829ebc7 --- /dev/null +++ b/api/src/team/domain/usecases/get-user-teams-info.usecase.js @@ -0,0 +1,26 @@ +/** + * Get the user's team information. + * + * @param {Object} params - The parameters. + * @param {string} params.userId - The ID of the user. + * @param {Object} params.adminMemberRepository - The repository for Pix Admin membership. + * @param {Object} params.certificationCenterMembershipRepository - The repository for certification center memberships. + * @param {Object} params.membershipRepository - The repository for organization memberships. + * @returns {Promise} The user's team information. + */ +export const getUserTeamsInfo = async function ({ + userId, + adminMemberRepository, + certificationCenterMembershipRepository, + membershipRepository, +}) { + const pixAdminMembership = await adminMemberRepository.get({ userId }); + const organizationMembershipsCount = await membershipRepository.countByUserId(userId); + const certificationCenterMembershipsCount = await certificationCenterMembershipRepository.countByUserId(userId); + + return { + isPixAgent: pixAdminMembership?.hasAccessToAdminScope ?? false, + isOrganizationMember: organizationMembershipsCount > 0, + isCertificationCenterMember: certificationCenterMembershipsCount > 0, + }; +}; diff --git a/api/tests/team/integration/domain/usecases/get-user-teams-info.usecase.test.js b/api/tests/team/integration/domain/usecases/get-user-teams-info.usecase.test.js new file mode 100644 index 00000000000..1c2708230e7 --- /dev/null +++ b/api/tests/team/integration/domain/usecases/get-user-teams-info.usecase.test.js @@ -0,0 +1,44 @@ +import { PIX_ADMIN } from '../../../../../src/authorization/domain/constants.js'; +import { usecases } from '../../../../../src/team/domain/usecases/index.js'; +import { databaseBuilder, expect } from '../../../../test-helper.js'; + +describe('Integration | Team | Domain | Usecases | getUserTeamsInfo', function () { + context('when user is a Pix agent, organization member, and certification center member', function () { + it('returns correct user teams information', async function () { + // given + const userId = databaseBuilder.factory.buildUser().id; + await databaseBuilder.factory.buildPixAdminRole({ userId, role: PIX_ADMIN.ROLES.SUPER_ADMIN }); + await databaseBuilder.factory.buildMembership({ userId }); + await databaseBuilder.factory.buildCertificationCenterMembership({ userId }); + await databaseBuilder.commit(); + + // when + const result = await usecases.getUserTeamsInfo({ userId }); + + // then + expect(result).to.deep.equal({ + isPixAgent: true, + isOrganizationMember: true, + isCertificationCenterMember: true, + }); + }); + }); + + context('when user does not belong to any team', function () { + it('returns correct user teams information', async function () { + // given + const userId = databaseBuilder.factory.buildUser().id; + await databaseBuilder.commit(); + + // when + const result = await usecases.getUserTeamsInfo({ userId }); + + // then + expect(result).to.deep.equal({ + isPixAgent: false, + isOrganizationMember: false, + isCertificationCenterMember: false, + }); + }); + }); +}); From 253f7b2d1013392cf01e8d443e7bb50f09fc04ef Mon Sep 17 00:00:00 2001 From: Benjamin Petetot Date: Mon, 25 Nov 2024 17:08:06 +0100 Subject: [PATCH 4/9] feat(api): create user teams access API --- .../application/api/models/user-teams-info.js | 11 ++++++ api/src/team/application/api/user-teams.js | 22 ++++++++++++ .../application/api/user-teams.test.js | 21 ++++++++++++ .../unit/application/api/user-teams.test.js | 34 +++++++++++++++++++ 4 files changed, 88 insertions(+) create mode 100644 api/src/team/application/api/models/user-teams-info.js create mode 100644 api/src/team/application/api/user-teams.js create mode 100644 api/tests/team/integration/application/api/user-teams.test.js create mode 100644 api/tests/team/unit/application/api/user-teams.test.js diff --git a/api/src/team/application/api/models/user-teams-info.js b/api/src/team/application/api/models/user-teams-info.js new file mode 100644 index 00000000000..507f83af541 --- /dev/null +++ b/api/src/team/application/api/models/user-teams-info.js @@ -0,0 +1,11 @@ +/** + * @class + * @classdesc Model representing a user teams info. + */ +export class UserTeamsInfo { + constructor({ isPixAgent, isOrganizationMember, isCertificationCenterMember } = {}) { + this.isPixAgent = isPixAgent; + this.isOrganizationMember = isOrganizationMember; + this.isCertificationCenterMember = isCertificationCenterMember; + } +} diff --git a/api/src/team/application/api/user-teams.js b/api/src/team/application/api/user-teams.js new file mode 100644 index 00000000000..c5e202bf685 --- /dev/null +++ b/api/src/team/application/api/user-teams.js @@ -0,0 +1,22 @@ +import { usecases } from '../../domain/usecases/index.js'; +import { UserTeamsInfo } from './models/user-teams-info.js'; + +/** + * @module UserTeamsApi + */ + +/** + * Retrieves information user's teams. + * + * @param {string} userId - The ID of the user. + * @returns {Promise} A promise that resolves to an instance of UserTeamsInfo. + * @throws {TypeError} preconditions failed + */ +export async function getUserTeamsInfo(userId) { + if (!userId) { + throw new TypeError('userId is required'); + } + + const userTeamsInfo = await usecases.getUserTeamsInfo({ userId }); + return new UserTeamsInfo(userTeamsInfo); +} diff --git a/api/tests/team/integration/application/api/user-teams.test.js b/api/tests/team/integration/application/api/user-teams.test.js new file mode 100644 index 00000000000..a2ec5f43a21 --- /dev/null +++ b/api/tests/team/integration/application/api/user-teams.test.js @@ -0,0 +1,21 @@ +import { UserTeamsInfo } from '../../../../../src/team/application/api/models/user-teams-info.js'; +import { getUserTeamsInfo } from '../../../../../src/team/application/api/user-teams.js'; +import { databaseBuilder, expect } from '../../../../test-helper.js'; + +describe('Team | Integration | Application | API | user-teams', function () { + describe('#getUserTeamsInfo', function () { + it("returns user's teams info", async function () { + // given + const userId = databaseBuilder.factory.buildUser().id; + await databaseBuilder.commit(); + + // when + const result = await getUserTeamsInfo(userId); + + // then + expect(result).to.deep.equal( + new UserTeamsInfo({ isPixAgent: false, isOrganizationMember: false, isCertificationCenterMember: false }), + ); + }); + }); +}); diff --git a/api/tests/team/unit/application/api/user-teams.test.js b/api/tests/team/unit/application/api/user-teams.test.js new file mode 100644 index 00000000000..a741cc62ac8 --- /dev/null +++ b/api/tests/team/unit/application/api/user-teams.test.js @@ -0,0 +1,34 @@ +import { UserTeamsInfo } from '../../../../../src/team/application/api/models/user-teams-info.js'; +import { getUserTeamsInfo } from '../../../../../src/team/application/api/user-teams.js'; +import { usecases } from '../../../../../src/team/domain/usecases/index.js'; +import { catchErr, expect, sinon } from '../../../../test-helper.js'; + +describe('Team | Unit | Application | API | user-teams', function () { + describe('#getUserTeamsInfo', function () { + it("returns user's teams access", async function () { + // given + const userId = 1; + const userTeamsInfo = { isPixAgent: true, isOrganizationMember: true, isCertificationCenterMember: true }; + const getUserTeamsInfoStub = sinon.stub(usecases, 'getUserTeamsInfo').resolves(userTeamsInfo); + + // when + const result = await getUserTeamsInfo(userId); + + // then + expect(result).to.deep.equal(new UserTeamsInfo(userTeamsInfo)); + expect(getUserTeamsInfoStub).to.have.been.calledOnceWith({ userId }); + }); + + it('throws a "TypeError" when "userId" is not defined', async function () { + // given + const getUserTeamsInfoStub = sinon.stub(usecases, 'getUserTeamsInfo'); + + // when + const error = await catchErr(getUserTeamsInfo)(undefined); + + // then + expect(error).to.be.instanceOf(TypeError); + expect(getUserTeamsInfoStub).to.not.have.been.called; + }); + }); +}); From 32c2efca8d94d435d910aa764b02bfcc550720f6 Mon Sep 17 00:00:00 2001 From: Benjamin Petetot Date: Mon, 25 Nov 2024 18:23:02 +0100 Subject: [PATCH 5/9] feat(api): in privacy context, add a repo for user-teams-api --- .../repositories/user-teams-api.repository.js | 16 +++++++++++ .../user-teams-api.repository.test.js | 27 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 api/src/privacy/infrastructure/repositories/user-teams-api.repository.js create mode 100644 api/tests/privacy/unit/infrastructure/repositories/user-teams-api.repository.test.js diff --git a/api/src/privacy/infrastructure/repositories/user-teams-api.repository.js b/api/src/privacy/infrastructure/repositories/user-teams-api.repository.js new file mode 100644 index 00000000000..97edab9c268 --- /dev/null +++ b/api/src/privacy/infrastructure/repositories/user-teams-api.repository.js @@ -0,0 +1,16 @@ +import * as userTeamsApi from '../../../team/application/api/user-teams.js'; + +/** + * Get user teams info. + * + * @param {Object} params - The parameters. + * @param {string} params.userId - The ID of the user. + * @param {Object} [params.dependencies] - The dependencies. + * @param {Object} [params.dependencies.userTeamsApi] - The user teams API. + * @returns {Promise} The user teams info. + */ +const getUserTeamsInfo = async ({ userId, dependencies = { userTeamsApi } }) => { + return dependencies.userTeamsApi.getUserTeamsInfo(userId); +}; + +export { getUserTeamsInfo }; diff --git a/api/tests/privacy/unit/infrastructure/repositories/user-teams-api.repository.test.js b/api/tests/privacy/unit/infrastructure/repositories/user-teams-api.repository.test.js new file mode 100644 index 00000000000..e444a4ab4b6 --- /dev/null +++ b/api/tests/privacy/unit/infrastructure/repositories/user-teams-api.repository.test.js @@ -0,0 +1,27 @@ +import { getUserTeamsInfo } from '../../../../../src/privacy/infrastructure/repositories/user-teams-api.repository.js'; +import { expect } from '../../../../test-helper.js'; + +describe('Unit | Privacy | Infrastructure | Repositories | user-teams-api', function () { + describe('#getUserTeamsInfo', function () { + it('returns user teams information', async function () { + // given + const userTeamsInfo = { + isPixAgent: false, + isOrganizationMember: false, + isCertificationCenterMember: false, + }; + + const dependencies = { + userTeamsApi: { + getUserTeamsInfo: async () => userTeamsInfo, + }, + }; + + // when + const result = await getUserTeamsInfo({ userId: '123', dependencies }); + + // then + expect(result).to.be.deep.equal(userTeamsInfo); + }); + }); +}); From ba744f0e32a3c5384607a744bda567f932f427fd Mon Sep 17 00:00:00 2001 From: Benjamin Petetot Date: Mon, 25 Nov 2024 18:23:21 +0100 Subject: [PATCH 6/9] feat(api): in privacy context, add a repo for learners-api --- .../repositories/learners-api.repository.js | 16 ++++++++++++++ .../learners-api.repository.test.js | 21 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 api/src/privacy/infrastructure/repositories/learners-api.repository.js create mode 100644 api/tests/privacy/unit/infrastructure/repositories/learners-api.repository.test.js diff --git a/api/src/privacy/infrastructure/repositories/learners-api.repository.js b/api/src/privacy/infrastructure/repositories/learners-api.repository.js new file mode 100644 index 00000000000..24967db5af0 --- /dev/null +++ b/api/src/privacy/infrastructure/repositories/learners-api.repository.js @@ -0,0 +1,16 @@ +import * as learnersApi from '../../../prescription/learner-management/application/api/learners-api.js'; + +/** + * Checks if the user has been a learner. + * + * @param {Object} params - The parameters. + * @param {string} params.userId - The ID of the user. + * @param {Object} [params.dependencies] - The dependencies. + * @param {Object} [params.dependencies.learnersApi] - The learners API. + * @returns {Promise} - A promise that resolves to a boolean indicating if the user has been a learner. + */ +const hasBeenLearner = async ({ userId, dependencies = { learnersApi } }) => { + return dependencies.learnersApi.hasBeenLearner({ userId }); +}; + +export { hasBeenLearner }; diff --git a/api/tests/privacy/unit/infrastructure/repositories/learners-api.repository.test.js b/api/tests/privacy/unit/infrastructure/repositories/learners-api.repository.test.js new file mode 100644 index 00000000000..2957856360d --- /dev/null +++ b/api/tests/privacy/unit/infrastructure/repositories/learners-api.repository.test.js @@ -0,0 +1,21 @@ +import { hasBeenLearner } from '../../../../../src/privacy/infrastructure/repositories/learners-api.repository.js'; +import { expect } from '../../../../test-helper.js'; + +describe('Unit | Privacy | Infrastructure | Repositories | learners-api', function () { + describe('#hasBeenLearner', function () { + it('indicates if user has been a learner', async function () { + // given + const dependencies = { + learnersApi: { + hasBeenLearner: async () => true, + }, + }; + + // when + const result = await hasBeenLearner({ userId: '123', dependencies }); + + // then + expect(result).to.be.true; + }); + }); +}); From 14c19880b5a252855168bbe0fbc0b16a924feeb4 Mon Sep 17 00:00:00 2001 From: Benjamin Petetot Date: Mon, 25 Nov 2024 18:23:40 +0100 Subject: [PATCH 7/9] feat(api): in privacy context, add a repo for candidates-api --- .../repositories/candidates-api.repository.js | 16 ++++++++++++++ .../candidates-api.repository.test.js | 21 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 api/src/privacy/infrastructure/repositories/candidates-api.repository.js create mode 100644 api/tests/privacy/unit/infrastructure/repositories/candidates-api.repository.test.js diff --git a/api/src/privacy/infrastructure/repositories/candidates-api.repository.js b/api/src/privacy/infrastructure/repositories/candidates-api.repository.js new file mode 100644 index 00000000000..0afbe220dba --- /dev/null +++ b/api/src/privacy/infrastructure/repositories/candidates-api.repository.js @@ -0,0 +1,16 @@ +import * as candidatesApi from '../../../certification/enrolment/application/api/candidates-api.js'; + +/** + * Checks if the user has been a candidate. + * + * @param {Object} params - The parameters. + * @param {string} params.userId - The ID of the user. + * @param {Object} [params.dependencies] - The dependencies. + * @param {Object} [params.dependencies.candidatesApi] - The candidates API. + * @returns {Promise} - A promise that resolves to a boolean indicating if the user has been a candidate. + */ +const hasBeenCandidate = async ({ userId, dependencies = { candidatesApi } }) => { + return dependencies.candidatesApi.hasBeenCandidate({ userId }); +}; + +export { hasBeenCandidate }; diff --git a/api/tests/privacy/unit/infrastructure/repositories/candidates-api.repository.test.js b/api/tests/privacy/unit/infrastructure/repositories/candidates-api.repository.test.js new file mode 100644 index 00000000000..803626902f3 --- /dev/null +++ b/api/tests/privacy/unit/infrastructure/repositories/candidates-api.repository.test.js @@ -0,0 +1,21 @@ +import { hasBeenCandidate } from '../../../../../src/privacy/infrastructure/repositories/candidates-api.repository.js'; +import { expect } from '../../../../test-helper.js'; + +describe('Unit | Privacy | Infrastructure | Repositories | candidates-api', function () { + describe('#hasBeenCandidate', function () { + it('indicates if user has been a candidate to certification', async function () { + // given + const dependencies = { + candidatesApi: { + hasBeenCandidate: async () => true, + }, + }; + + // when + const result = await hasBeenCandidate({ userId: '123', dependencies }); + + // then + expect(result).to.be.true; + }); + }); +}); From b4ca8f35fe1adee98a976498f680963e7ed66bff Mon Sep 17 00:00:00 2001 From: Benjamin Petetot Date: Mon, 25 Nov 2024 18:24:15 +0100 Subject: [PATCH 8/9] feat(api): inject repositories for privacy context usecases --- api/src/privacy/domain/usecases/index.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/api/src/privacy/domain/usecases/index.js b/api/src/privacy/domain/usecases/index.js index a3a2676af50..edda72c3096 100644 --- a/api/src/privacy/domain/usecases/index.js +++ b/api/src/privacy/domain/usecases/index.js @@ -3,6 +3,9 @@ import { fileURLToPath } from 'node:url'; import { injectDependencies } from '../../../shared/infrastructure/utils/dependency-injection.js'; import { importNamedExportsFromDirectory } from '../../../shared/infrastructure/utils/import-named-exports-from-directory.js'; +import * as candidatesApiRepository from '../../infrastructure/repositories/candidates-api.repository.js'; +import * as learnersApiRepository from '../../infrastructure/repositories/learners-api.repository.js'; +import * as userTeamsApiRepository from '../../infrastructure/repositories/user-teams-api.repository.js'; const path = dirname(fileURLToPath(import.meta.url)); @@ -10,7 +13,11 @@ const usecasesWithoutInjectedDependencies = { ...(await importNamedExportsFromDirectory({ path: join(path, './'), ignoredFileNames: ['index.js'] })), }; -const dependencies = {}; +const dependencies = { + candidatesApiRepository, + learnersApiRepository, + userTeamsApiRepository, +}; const usecases = injectDependencies(usecasesWithoutInjectedDependencies, dependencies); From 6661601b32ec0b0d2efad6f9fbdf23e7cb68dc90 Mon Sep 17 00:00:00 2001 From: Benjamin Petetot Date: Mon, 25 Nov 2024 18:24:42 +0100 Subject: [PATCH 9/9] feat(api): add business rules to can-self-delete-account usecase --- .../can-self-delete-account.usecase.js | 25 +++- .../can-self-delete-account.usecase.test.js | 17 +-- .../can-self-delete-account.usecase.test.js | 130 ++++++++++++++++++ 3 files changed, 153 insertions(+), 19 deletions(-) create mode 100644 api/tests/privacy/unit/domain/usecases/can-self-delete-account.usecase.test.js diff --git a/api/src/privacy/domain/usecases/can-self-delete-account.usecase.js b/api/src/privacy/domain/usecases/can-self-delete-account.usecase.js index bbcdaa8b6a9..977cf39ae47 100644 --- a/api/src/privacy/domain/usecases/can-self-delete-account.usecase.js +++ b/api/src/privacy/domain/usecases/can-self-delete-account.usecase.js @@ -5,14 +5,33 @@ import { config } from '../../../shared/config.js'; * * @param {Object} params - The parameters for the use case. * @param {number} params.userId - The ID of the user. - * @param {Object} [params.featureToggles] - The feature toggles configuration. + * @param {Object} params.featureToggles - The feature toggles configuration. + * @param {Object} params.candidatesApiRepository - The repository for candidate-related operations. + * @param {Object} params.learnersApiRepository - The repository for learner-related operations. + * @param {Object} params.userTeamsApiRepository - The repository for user team access operations. * @returns {Promise} - A promise that resolves to a boolean indicating if self-account deletion is enabled. */ -const canSelfDeleteAccount = async ({ featureToggles = config.featureToggles }) => { +const canSelfDeleteAccount = async ({ + userId, + featureToggles = config.featureToggles, + candidatesApiRepository, + learnersApiRepository, + userTeamsApiRepository, +}) => { const { isSelfAccountDeletionEnabled } = featureToggles; - if (!isSelfAccountDeletionEnabled) return false; + const hasBeenLearner = await learnersApiRepository.hasBeenLearner({ userId }); + if (hasBeenLearner) return false; + + const hasBeenCandidate = await candidatesApiRepository.hasBeenCandidate({ userId }); + if (hasBeenCandidate) return false; + + const userTeamsInfo = await userTeamsApiRepository.getUserTeamsInfo({ userId }); + if (userTeamsInfo.isPixAgent) return false; + if (userTeamsInfo.isOrganizationMember) return false; + if (userTeamsInfo.isCertificationCenterMember) return false; + return true; }; diff --git a/api/tests/privacy/integration/domain/usecases/can-self-delete-account.usecase.test.js b/api/tests/privacy/integration/domain/usecases/can-self-delete-account.usecase.test.js index b96b902ad8d..e63f0496ad7 100644 --- a/api/tests/privacy/integration/domain/usecases/can-self-delete-account.usecase.test.js +++ b/api/tests/privacy/integration/domain/usecases/can-self-delete-account.usecase.test.js @@ -2,22 +2,7 @@ import { usecases } from '../../../../../src/privacy/domain/usecases/index.js'; import { databaseBuilder, expect } from '../../../../test-helper.js'; describe('Integration | Privacy | Domain | UseCase | can-self-delete-account', function () { - context('Feature flag is disabled', function () { - it('returns false', async function () { - // given - const featureToggles = { isSelfAccountDeletionEnabled: false }; - const user = databaseBuilder.factory.buildUser(); - await databaseBuilder.commit(); - - // when - const result = await usecases.canSelfDeleteAccount({ userId: user.id, featureToggles }); - - // then - expect(result).to.be.false; - }); - }); - - context('Feature flag is enabled', function () { + context('When user is eligible', function () { it('returns true', async function () { // given const featureToggles = { isSelfAccountDeletionEnabled: true }; diff --git a/api/tests/privacy/unit/domain/usecases/can-self-delete-account.usecase.test.js b/api/tests/privacy/unit/domain/usecases/can-self-delete-account.usecase.test.js new file mode 100644 index 00000000000..334a431b403 --- /dev/null +++ b/api/tests/privacy/unit/domain/usecases/can-self-delete-account.usecase.test.js @@ -0,0 +1,130 @@ +import { usecases } from '../../../../../src/privacy/domain/usecases/index.js'; +import { expect, sinon } from '../../../../test-helper.js'; + +describe('Unit | Privacy | Domain | UseCase | can-self-delete-account', function () { + const userId = '123'; + let dependencies; + + beforeEach(function () { + dependencies = { + featureToggles: { isSelfAccountDeletionEnabled: false }, + learnersApiRepository: { hasBeenLearner: sinon.stub().resolves(false) }, + candidatesApiRepository: { hasBeenCandidate: sinon.stub().resolves(false) }, + userTeamsApiRepository: { + getUserTeamsInfo: sinon.stub().resolves({ + isPixAgent: false, + isOrganizationMember: false, + isCertificationCenterMember: false, + }), + }, + }; + }); + + context('When feature flag is enabled', function () { + beforeEach(function () { + sinon.stub(dependencies.featureToggles, 'isSelfAccountDeletionEnabled').value(true); + }); + + context('When user is eligible', function () { + it('returns true', async function () { + // when + const result = await usecases.canSelfDeleteAccount({ userId, ...dependencies }); + + // then + expect(result).to.be.true; + }); + }); + + context('When user has been a learner', function () { + it('returns false', async function () { + // given + dependencies.learnersApiRepository.hasBeenLearner.withArgs({ userId }).resolves(true); + + // when + const result = await usecases.canSelfDeleteAccount({ userId, ...dependencies }); + + // then + expect(result).to.be.false; + }); + }); + + context('User has been a candidate to certification', function () { + it('returns false', async function () { + // given + dependencies.candidatesApiRepository.hasBeenCandidate.withArgs({ userId }).resolves(true); + + // when + const result = await usecases.canSelfDeleteAccount({ userId, ...dependencies }); + + // then + expect(result).to.be.false; + }); + }); + + context('User if user is a Pix agent', function () { + it('returns false', async function () { + // given + dependencies.userTeamsApiRepository.getUserTeamsInfo.withArgs({ userId }).resolves({ + isPixAgent: true, + isOrganizationMember: false, + isCertificationCenterMember: false, + }); + + // when + const result = await usecases.canSelfDeleteAccount({ userId, ...dependencies }); + + // then + expect(result).to.be.false; + }); + }); + + context('User if user is member of an organization', function () { + it('returns false', async function () { + // given + dependencies.userTeamsApiRepository.getUserTeamsInfo.withArgs({ userId }).resolves({ + isPixAgent: false, + isOrganizationMember: true, + isCertificationCenterMember: false, + }); + + // when + const result = await usecases.canSelfDeleteAccount({ userId, ...dependencies }); + + // then + expect(result).to.be.false; + }); + }); + + context('User if user is member of a certification center', function () { + it('returns false', async function () { + // given + dependencies.userTeamsApiRepository.getUserTeamsInfo.withArgs({ userId }).resolves({ + isPixAgent: false, + isOrganizationMember: false, + isCertificationCenterMember: true, + }); + + // when + const result = await usecases.canSelfDeleteAccount({ userId, ...dependencies }); + + // then + expect(result).to.be.false; + }); + }); + }); + + context('Feature flag is disabled', function () { + context('When user is eligible', function () { + it('returns false', async function () { + // given + sinon.stub(dependencies.featureToggles, 'isSelfAccountDeletionEnabled').value(false); + + // when + const result = await usecases.canSelfDeleteAccount({ userId: '123', ...dependencies }); + + // then + expect(result).to.be.false; + }); + }); + }); +});