-
Notifications
You must be signed in to change notification settings - Fork 56
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(api): add script to remove an organizationLeaner
Co-authored-by: mache <[email protected]>
- Loading branch information
Showing
2 changed files
with
313 additions
and
0 deletions.
There are no files selected for viewing
76 changes: 76 additions & 0 deletions
76
api/scripts/prod/delete-and-anonymise-organization-learners.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
237 changes: 237 additions & 0 deletions
237
api/tests/integration/scripts/prod/delete-and-anonymise-organization-learners_test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}); | ||
}); | ||
}); | ||
}); |