Skip to content

Commit

Permalink
feat(api): add script to remove an organizationLeaner
Browse files Browse the repository at this point in the history
Co-authored-by: mache <[email protected]>
  • Loading branch information
lionelB and machestla authored Nov 20, 2024
1 parent c809123 commit ca23570
Show file tree
Hide file tree
Showing 2 changed files with 313 additions and 0 deletions.
76 changes: 76 additions & 0 deletions api/scripts/prod/delete-and-anonymise-organization-learners.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { DomainTransaction } from '../../lib/infrastructure/DomainTransaction.js';
import { removeByOrganizationLearnerIds } from '../../src/prescription/learner-management/infrastructure/repositories/campaign-participation-repository.js';
import { removeByIds } from '../../src/prescription/learner-management/infrastructure/repositories/organization-learner-repository.js';
import { commaSeparatedNumberParser } from '../../src/shared/application/scripts/parsers.js';
import { Script } from '../../src/shared/application/scripts/script.js';
import { ScriptRunner } from '../../src/shared/application/scripts/script-runner.js';
// Définition du script
export class DeleteAndAnonymiseOrgnizationLearnerScript extends Script {
constructor() {
super({
description: 'Deletes organization-learners and anonymise their related data',
permanent: true,
options: {
organizationLearnerIds: {
type: '<array>number',
describe: 'a list of comma separated organization learner ids',
demandOption: true,
coerce: commaSeparatedNumberParser(),
},
},
});
}

async handle({
options,
logger,
campaignParticipationRepository = { removeByOrganizationLearnerIds },
organizationLearnerRepository = { removeByIds },
}) {
const engineeringUserId = process.env.ENGINEERING_USER_ID;

logger.info(`Anonymise ${options.organizationLearnerIds.length} learners`);
await DomainTransaction.execute(async () => {
await campaignParticipationRepository.removeByOrganizationLearnerIds({
organizationLearnerIds: options.organizationLearnerIds,
userId: engineeringUserId,
});

await organizationLearnerRepository.removeByIds({
organizationLearnerIds: options.organizationLearnerIds,
userId: engineeringUserId,
});

await anonymizeDeletedOrganizationLearners(options.organizationLearnerIds);

const participations = await anonymizeDeletedOrganizationLearnersParticipations(options.organizationLearnerIds);

await detachAssessments(participations.map((participation) => participation.id));
});
}
}

async function anonymizeDeletedOrganizationLearners(organizationLearnerIds) {
const knexConnection = DomainTransaction.getConnection();
await knexConnection('organization-learners')
.update({ firstName: '', lastName: '', userId: null, updatedAt: new Date() })
.whereIn('id', organizationLearnerIds)
.whereNotNull('deletedAt');
}

async function anonymizeDeletedOrganizationLearnersParticipations(organizationLearnerIds) {
const knexConnection = DomainTransaction.getConnection();
return knexConnection('campaign-participations')
.update({ participantExternalId: null, userId: null })
.whereIn('organizationLearnerId', organizationLearnerIds)
.whereNotNull('deletedAt')
.returning('id');
}
async function detachAssessments(participationIds) {
const knexConnection = DomainTransaction.getConnection();
await knexConnection('assessments')
.update({ campaignParticipationId: null })
.whereIn('campaignParticipationId', participationIds);
}
// Exécution du script
await ScriptRunner.execute(import.meta.url, DeleteAndAnonymiseOrgnizationLearnerScript);
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import { expect } from 'chai';
import sinon from 'sinon';

import { DeleteAndAnonymiseOrgnizationLearnerScript } from '../../../../scripts/prod/delete-and-anonymise-organization-learners.js';
import { databaseBuilder, knex } from '../../../test-helper.js';

describe('DeleteAndAnonymiseOrgnizationLearnerScript', function () {
describe('Options', function () {
it('has the correct options', function () {
const script = new DeleteAndAnonymiseOrgnizationLearnerScript();
const { options } = script.metaInfo;

expect(options.organizationLearnerIds).to.deep.include({
type: '<array>number',
describe: 'a list of comma separated organization learner ids',
demandOption: true,
});
expect(options.organizationLearnerIds.coerce).to.be.a('function');
});

it('parses list of organizationLearnerIds', async function () {
const ids = '1,2,3';
const script = new DeleteAndAnonymiseOrgnizationLearnerScript();
const { options } = script.metaInfo;
const parsedData = await options.organizationLearnerIds.coerce(ids);

expect(parsedData).to.deep.equals([1, 2, 3]);
});
});

describe('Handle', function () {
let script;
let logger;
const ENGINEERING_USER_ID = 99999;

beforeEach(async function () {
script = new DeleteAndAnonymiseOrgnizationLearnerScript();
logger = { info: sinon.spy(), error: sinon.spy() };
sinon.stub(process, 'env').value({ ENGINEERING_USER_ID });
});

describe('anonymise organization learners', function () {
let learner, otherLearner, campaign, organization, clock, now;

beforeEach(async function () {
now = new Date('2024-01-17');
clock = sinon.useFakeTimers({ now, toFake: ['Date'] });

databaseBuilder.factory.buildUser({ id: ENGINEERING_USER_ID });
organization = databaseBuilder.factory.buildOrganization();
campaign = databaseBuilder.factory.buildCampaign({ organizationId: organization.id });
learner = databaseBuilder.factory.prescription.organizationLearners.buildOrganizationLearner({
id: 123,
firstName: 'johnny',
lastName: 'five',
organizationId: organization.id,
});
otherLearner = databaseBuilder.factory.prescription.organizationLearners.buildOrganizationLearner({
id: 456,
organizationId: organization.id,
});

await databaseBuilder.commit();
});

afterEach(function () {
clock.restore();
});

it('delete organization learners', async function () {
// given
const organizationLearnerIds = [learner.id];

// when
await script.handle({ options: { organizationLearnerIds }, logger });

// then
const organizationLearnerResult = await knex('organization-learners').whereNotNull('deletedAt');
expect(organizationLearnerResult).lengthOf(1);
expect(organizationLearnerResult[0].id).to.equal(learner.id);
expect(organizationLearnerResult[0].updatedAt).to.deep.equal(now);
expect(organizationLearnerResult[0].deletedAt).to.deep.equal(now);
expect(organizationLearnerResult[0].deletedBy).to.equal(ENGINEERING_USER_ID);
});

it('anonymise given deleted organization learners id', async function () {
// when
await script.handle({
options: { organizationLearnerIds: [learner.id] },
logger,
});
// then
const organizationLearnerResult = await knex('organization-learners').whereNull('userId').first();
expect(organizationLearnerResult.id).to.equal(learner.id);
expect(organizationLearnerResult.firstName).to.equal('');
expect(organizationLearnerResult.lastName).to.equal('');
});

it('anonymise and delete participations', async function () {
// given
databaseBuilder.factory.buildCampaignParticipation({
userId: otherLearner.userId,
organizationLearnerId: otherLearner.id,
participantExternalId: 'another-learner',
});

databaseBuilder.factory.buildCampaignParticipation({
userId: learner.userId,
organizationLearnerId: learner.id,
participantExternalId: 'first',
campaignId: campaign.id,
isImproved: true,
});
databaseBuilder.factory.buildCampaignParticipation({
userId: learner.userId,
organizationLearnerId: learner.id,
participantExternalId: 'second',
campaignId: campaign.id,
});

await databaseBuilder.commit();

// when
await script.handle({
options: { organizationLearnerIds: [learner.id] },
logger,
});

// then
const participationResult = await knex('campaign-participations').whereNotNull('deletedAt');
expect(participationResult).lengthOf(2);
expect(participationResult[0].deletedAt).to.deep.equal(now);
expect(participationResult[0].deletedBy).to.equal(ENGINEERING_USER_ID);
expect(participationResult[0].participantExternalId).to.be.null;
expect(participationResult[0].userId).to.be.null;
expect(participationResult[1].deletedAt).to.deep.equal(now);
expect(participationResult[1].deletedBy).to.equal(ENGINEERING_USER_ID);
expect(participationResult[1].participantExternalId).to.be.null;
expect(participationResult[1].userId).to.be.null;
});

it('detach its assessments', async function () {
// given
const otherParticipation = databaseBuilder.factory.buildCampaignParticipation({
userId: otherLearner.userId,
organizationLearnerId: otherLearner.id,
participantExternalId: 'other-learner',
});
databaseBuilder.factory.buildAssessment({
userId: otherLearner.userId,
campaignParticipationId: otherParticipation.id,
});

const campaignParticipation = databaseBuilder.factory.buildCampaignParticipation({
userId: learner.userId,
organizationLearnerId: learner.id,
participantExternalId: 'coucou',
});

databaseBuilder.factory.buildAssessment({
userId: learner.userId,
campaignParticipationId: campaignParticipation.id,
});
databaseBuilder.factory.buildAssessment({
userId: learner.userId,
campaignParticipationId: campaignParticipation.id,
});

await databaseBuilder.commit();

// when
await script.handle({
options: { organizationLearnerIds: [learner.id] },
logger,
});

// then
const assessmentResults = await knex('assessments').whereNull('campaignParticipationId');
expect(assessmentResults).lengthOf(2);
});

// We skip this test since there is a database
// non-nullable constraint on campaignParticipationId
// Until this constraint is removed we skip the tests
// eslint-disable-next-line mocha/no-skipped-tests
it.skip('detach its user recommended trainings', async function () {
// given
const training = databaseBuilder.factory.buildTraining();
const training2 = databaseBuilder.factory.buildTraining();

const otherCampaignParticipation = databaseBuilder.factory.buildCampaignParticipation({
userId: otherLearner.userId,
organizationLearnerId: otherLearner.id,
participantExternalId: 'coucou',
});
databaseBuilder.factory.buildUserRecommendedTraining({
campaignParticipationId: otherCampaignParticipation.id,
trainingId: training.id,
userId: otherLearner.userId,
});

const campaignParticipation = databaseBuilder.factory.buildCampaignParticipation({
userId: learner.userId,
organizationLearnerId: learner.id,
participantExternalId: 'coucou',
});

databaseBuilder.factory.buildUserRecommendedTraining({
campaignParticipationId: campaignParticipation.id,
trainingId: training.id,
userId: learner.userId,
});
databaseBuilder.factory.buildUserRecommendedTraining({
campaignParticipationId: campaignParticipation.id,
trainingId: training2.id,
userId: learner.userId,
});

await databaseBuilder.commit();

// when
await script.handle({
options: { organizationLearnerIds: [learner.id] },
logger,
});

// then
const recommendedTrainingResults = await knex('user-recommended-trainings').whereNull('userId');
expect(recommendedTrainingResults).lengthOf(2);
expect(recommendedTrainingResults[0].campaignParticipationId).to.be.null;
expect(recommendedTrainingResults[0].campaignParticipationId).to.be.null;
expect(recommendedTrainingResults[1].campaignParticipationId).to.be.null;
expect(recommendedTrainingResults[1].campaignParticipationId).to.be.null;
});
});
});
});

0 comments on commit ca23570

Please sign in to comment.