Skip to content

Commit

Permalink
[FEATURE] Ajout des règles métier pour canSelfDeleteAccount (PIX-15333)
Browse files Browse the repository at this point in the history
  • Loading branch information
pix-service-auto-merge authored Nov 29, 2024
2 parents 0bf766c + 6661601 commit 25b0026
Show file tree
Hide file tree
Showing 20 changed files with 523 additions and 21 deletions.
25 changes: 22 additions & 3 deletions api/src/privacy/domain/usecases/can-self-delete-account.usecase.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>} - 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;
};

Expand Down
9 changes: 8 additions & 1 deletion api/src/privacy/domain/usecases/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,21 @@ 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));

const usecasesWithoutInjectedDependencies = {
...(await importNamedExportsFromDirectory({ path: join(path, './'), ignoredFileNames: ['index.js'] })),
};

const dependencies = {};
const dependencies = {
candidatesApiRepository,
learnersApiRepository,
userTeamsApiRepository,
};

const usecases = injectDependencies(usecasesWithoutInjectedDependencies, dependencies);

Expand Down
Original file line number Diff line number Diff line change
@@ -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<boolean>} - 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 };
Original file line number Diff line number Diff line change
@@ -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<boolean>} - 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 };
Original file line number Diff line number Diff line change
@@ -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<UserTeamsInfo>} The user teams info.
*/
const getUserTeamsInfo = async ({ userId, dependencies = { userTeamsApi } }) => {
return dependencies.userTeamsApi.getUserTeamsInfo(userId);
};

export { getUserTeamsInfo };
11 changes: 11 additions & 0 deletions api/src/team/application/api/models/user-teams-info.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
22 changes: 22 additions & 0 deletions api/src/team/application/api/user-teams.js
Original file line number Diff line number Diff line change
@@ -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<UserTeamsInfo>} 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);
}
26 changes: 26 additions & 0 deletions api/src/team/domain/usecases/get-user-teams-info.usecase.js
Original file line number Diff line number Diff line change
@@ -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<Object>} 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,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>} - 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 });
};
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -332,6 +347,7 @@ async function findActiveAdminsByCertificationCenterId(certificationCenterId) {

const certificationCenterMembershipRepository = {
countActiveMembersForCertificationCenter,
countByUserId,
create,
disableById,
disableMembershipsByUserId,
Expand Down
12 changes: 12 additions & 0 deletions api/src/team/infrastructure/repositories/membership.repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>} - 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
});
});
});
});
Loading

0 comments on commit 25b0026

Please sign in to comment.