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

N21-2120 force migration extension #5215

Merged
merged 20 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a2e8d11
wip extend user migration
GordonNicholasCap Aug 28, 2024
62cb27a
wip 2 extend force migration endpoint
GordonNicholasCap Aug 29, 2024
77fd8d6
N21-2120 added loggables, adjusted logic
GordonNicholasCap Aug 30, 2024
92c9a71
N21-2120 added and adjusted tests
GordonNicholasCap Sep 2, 2024
f114a2b
N21-2120 fix and added api tests, adjusted checks for if school is mi…
GordonNicholasCap Sep 3, 2024
be183fe
Merge branch 'main' into N21-2120-force-migration-extension
GordonNicholasCap Sep 3, 2024
656bfb0
N21-2120 clean up, adjusted tests, removed unused exception
GordonNicholasCap Sep 3, 2024
221a40b
Merge branch 'main' into N21-2120-force-migration-extension
GordonNicholasCap Sep 3, 2024
9690c21
Merge branch 'main' into N21-2120-force-migration-extension
GordonNicholasCap Sep 4, 2024
442cd22
Merge branch 'main' into N21-2120-force-migration-extension
IgorCapCoder Sep 4, 2024
5e40407
Merge branch 'main' into N21-2120-force-migration-extension
GordonNicholasCap Sep 5, 2024
ef3b949
N21-2120 added missing case for invalid school external id
GordonNicholasCap Sep 5, 2024
1cb7919
N21-2120 fixed bad email in api spec tests
GordonNicholasCap Sep 5, 2024
f3a181e
N21-2120 extended mode for force migration
GordonNicholasCap Sep 6, 2024
09aa120
N21-2120 review changes & make sonar cloud happy
GordonNicholasCap Sep 6, 2024
d0f1d24
Merge branch 'main' into N21-2120-force-migration-extension
GordonNicholasCap Sep 6, 2024
139624b
N21-2120 happy nest lint more important than sonar cloud
GordonNicholasCap Sep 6, 2024
240291e
Merge remote-tracking branch 'origin/N21-2120-force-migration-extensi…
GordonNicholasCap Sep 6, 2024
9e2dde0
N21-2120 update api description
GordonNicholasCap Sep 6, 2024
c9caa92
Merge branch 'main' into N21-2120-force-migration-extension
GordonNicholasCap Sep 6, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { UUID } from 'bson';
import { Response } from 'supertest';
import { DeepPartial } from 'fishery';
import { ForceMigrationParams, Oauth2MigrationParams, UserLoginMigrationResponse } from '../dto';

jest.mock('jwks-rsa', () => () => {
Expand Down Expand Up @@ -1405,7 +1406,33 @@ describe('UserLoginMigrationController (API)', () => {
});
});

describe('[GET] /user-login-migrations/force-migration', () => {
describe('[POST] /user-login-migrations/force-migration', () => {
const expectSchoolMigrationUnchanged = (
school: SchoolEntity,
externalId: string,
sourceSystem: SystemEntity,
targetSystem: SystemEntity
) => {
const expectedSchoolPartial: DeepPartial<SchoolEntity> = {
externalId,
};
expect(school).toEqual(expect.objectContaining(expectedSchoolPartial));

const systems: SystemEntity[] = school?.systems.getItems();
systems?.forEach((system) => {
expect([sourceSystem.id, targetSystem.id]).toContainEqual(system.id);
});
};

const expectUserMigrated = (migratedUser: User, preMigratedUser: User, externalId: string) => {
const expectedUserPartial: DeepPartial<User> = {
externalId,
};
expect(migratedUser).toEqual(expect.objectContaining(expectedUserPartial));
expect(migratedUser.lastLoginSystemChange).not.toEqual(preMigratedUser.lastLoginSystemChange);
expect(migratedUser.previousExternalId).toEqual(preMigratedUser.externalId);
};

describe('when forcing a school to migrate', () => {
const setup = async () => {
const targetSystem: SystemEntity = systemEntityFactory
Expand Down Expand Up @@ -1464,17 +1491,309 @@ describe('UserLoginMigrationController (API)', () => {
expect(userLoginMigration.sourceSystem?.id).toEqual(sourceSystem.id);
expect(userLoginMigration.targetSystem.id).toEqual(targetSystem.id);

expect(await em.findOne(User, adminUser.id)).toEqual(
expect.objectContaining({
externalId: requestBody.externalUserId,
})
);

expect(await em.findOne(SchoolEntity, school.id)).toEqual(
const schoolEntity = await em.findOneOrFail(SchoolEntity, school.id);
expect(schoolEntity).toEqual(
expect.objectContaining({
externalId: requestBody.externalSchoolId,
})
);

const migratedUser = await em.findOneOrFail(User, adminUser.id);
expectUserMigrated(migratedUser, adminUser, requestBody.externalUserId);
});
});

describe('when forcing a user in a migrated school to migrate', () => {
describe('when the provided external school id is valid', () => {
const setup = async () => {
const targetSystem: SystemEntity = systemEntityFactory
.withOauthConfig()
.buildWithId({ alias: 'SANIS', provisioningStrategy: SystemProvisioningStrategy.SANIS });

const sourceSystem: SystemEntity = systemEntityFactory.buildWithId();

const externalSchoolId = 'externalSchoolId';
const school: SchoolEntity = schoolEntityFactory.buildWithId({
systems: [sourceSystem, targetSystem],
externalId: externalSchoolId,
});

const email = '[email protected]';
const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent({
email,
school,
});
const { superheroAccount, superheroUser } = UserAndAccountTestFactory.buildSuperhero();

const userLoginMigration: UserLoginMigrationEntity = userLoginMigrationFactory.buildWithId({
school,
targetSystem,
});

await em.persistAndFlush([
sourceSystem,
targetSystem,
school,
superheroAccount,
superheroUser,
studentAccount,
studentUser,
userLoginMigration,
]);
em.clear();

const loggedInClient = await testApiClient.login(superheroAccount);

const requestBody: ForceMigrationParams = new ForceMigrationParams();
requestBody.email = email;
requestBody.externalUserId = 'externalUserId';
requestBody.externalSchoolId = externalSchoolId;

return {
requestBody,
loggedInClient,
sourceSystem,
targetSystem,
school,
studentUser,
};
};

it('should migrate the user without changing the school migration', async () => {
const { requestBody, loggedInClient, school, sourceSystem, targetSystem, studentUser } = await setup();

const response: Response = await loggedInClient.post(`/force-migration`, requestBody);

expect(response.status).toEqual(HttpStatus.CREATED);

const userLoginMigration = await em.findOneOrFail(UserLoginMigrationEntity, {
school: school.id,
});
expect(userLoginMigration.targetSystem.id).toEqual(targetSystem.id);

const migratedSchool = await em.findOneOrFail(SchoolEntity, school.id);
expectSchoolMigrationUnchanged(migratedSchool, requestBody.externalSchoolId, sourceSystem, targetSystem);

const migratedUser = await em.findOneOrFail(User, studentUser.id);
expectUserMigrated(migratedUser, studentUser, requestBody.externalUserId);
});
});

describe('when the provided external school id is invalid', () => {
const setup = async () => {
const targetSystem: SystemEntity = systemEntityFactory
.withOauthConfig()
.buildWithId({ alias: 'SANIS', provisioningStrategy: SystemProvisioningStrategy.SANIS });

const sourceSystem: SystemEntity = systemEntityFactory.buildWithId();

const externalSchoolId = 'externalSchoolId';
const school: SchoolEntity = schoolEntityFactory.buildWithId({
systems: [sourceSystem, targetSystem],
externalId: 'otherExternalSchoolId',
});

const email = '[email protected]';
const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent({
email,
school,
});
const { superheroAccount, superheroUser } = UserAndAccountTestFactory.buildSuperhero();

const userLoginMigration: UserLoginMigrationEntity = userLoginMigrationFactory.buildWithId({
school,
targetSystem,
});

await em.persistAndFlush([
sourceSystem,
targetSystem,
school,
superheroAccount,
superheroUser,
studentAccount,
studentUser,
userLoginMigration,
]);
em.clear();

const loggedInClient = await testApiClient.login(superheroAccount);

const requestBody: ForceMigrationParams = new ForceMigrationParams();
requestBody.email = email;
requestBody.externalUserId = 'externalUserId';
requestBody.externalSchoolId = externalSchoolId;

return {
requestBody,
loggedInClient,
sourceSystem,
targetSystem,
school,
studentUser,
};
};

it('throw an UnprocessableEntityException', async () => {
const { requestBody, loggedInClient } = await setup();

const response: Response = await loggedInClient.post(`/force-migration`, requestBody);

expect(response.status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY);
});
});
});

describe('when forcing a correction on a migrated user', () => {
const setup = async () => {
const targetSystem: SystemEntity = systemEntityFactory
.withOauthConfig()
.buildWithId({ alias: 'SANIS', provisioningStrategy: SystemProvisioningStrategy.SANIS });

const sourceSystem: SystemEntity = systemEntityFactory.buildWithId();

const externalSchoolId = 'externalSchoolId';
const school: SchoolEntity = schoolEntityFactory.buildWithId({
systems: [sourceSystem, targetSystem],
externalId: externalSchoolId,
});

const userLoginMigration: UserLoginMigrationEntity = userLoginMigrationFactory.buildWithId({
school,
targetSystem,
sourceSystem,
});

const email = '[email protected]';
const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({
email,
school,
});
teacherUser.externalId = 'badExternalUserId';
const loginChange = new Date(userLoginMigration.startedAt);
loginChange.setDate(loginChange.getDate() + 1);
teacherUser.lastLoginSystemChange = loginChange;

const { superheroAccount, superheroUser } = UserAndAccountTestFactory.buildSuperhero();

await em.persistAndFlush([
sourceSystem,
targetSystem,
school,
superheroAccount,
superheroUser,
teacherAccount,
teacherUser,
userLoginMigration,
]);
em.clear();

const loggedInClient = await testApiClient.login(superheroAccount);

const requestBody: ForceMigrationParams = new ForceMigrationParams();
requestBody.email = email;
requestBody.externalUserId = 'correctExternalUserId';
requestBody.externalSchoolId = externalSchoolId;

return {
requestBody,
loggedInClient,
sourceSystem,
targetSystem,
school,
teacherUser,
};
};

it('should correct the user without changing the migration', async () => {
const { requestBody, loggedInClient, school, sourceSystem, targetSystem, teacherUser } = await setup();

const response: Response = await loggedInClient.post(`/force-migration`, requestBody);

expect(response.status).toEqual(HttpStatus.CREATED);

const userLoginMigration = await em.findOneOrFail(UserLoginMigrationEntity, { school: school.id });
expect(userLoginMigration.targetSystem.id).toEqual(targetSystem.id);

const migratedSchool = await em.findOneOrFail(SchoolEntity, school.id);
expectSchoolMigrationUnchanged(migratedSchool, requestBody.externalSchoolId, sourceSystem, targetSystem);

const expectedUserPartial: DeepPartial<User> = {
externalId: requestBody.externalUserId,
lastLoginSystemChange: teacherUser.lastLoginSystemChange,
previousExternalId: teacherUser.previousExternalId,
};
const correctedUser = await em.findOneOrFail(User, teacherUser.id);
expect(correctedUser).toEqual(expect.objectContaining(expectedUserPartial));
});
});

describe('when forcing a user migration after the migration is closed or finished', () => {
const setup = async () => {
const targetSystem: SystemEntity = systemEntityFactory
.withOauthConfig()
.buildWithId({ alias: 'SANIS', provisioningStrategy: SystemProvisioningStrategy.SANIS });

const sourceSystem: SystemEntity = systemEntityFactory.buildWithId();

const externalSchoolId = 'externalSchoolId';
const school: SchoolEntity = schoolEntityFactory.buildWithId({
systems: [sourceSystem, targetSystem],
externalId: externalSchoolId,
});

const now = new Date();
const closedAt = new Date(now);
closedAt.setMonth(now.getMonth() - 1);
const finishedAt = new Date(closedAt);
finishedAt.setDate(finishedAt.getDate() + 7);
const userLoginMigration: UserLoginMigrationEntity = userLoginMigrationFactory.buildWithId({
school,
targetSystem,
sourceSystem,
closedAt,
finishedAt,
});

const email = '[email protected]';
const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({
email,
school,
});

const { superheroAccount, superheroUser } = UserAndAccountTestFactory.buildSuperhero();

await em.persistAndFlush([
sourceSystem,
targetSystem,
school,
superheroAccount,
superheroUser,
teacherAccount,
teacherUser,
userLoginMigration,
]);
em.clear();

const loggedInClient = await testApiClient.login(superheroAccount);

const requestBody: ForceMigrationParams = new ForceMigrationParams();
requestBody.email = email;
requestBody.externalUserId = 'externalUserId';
requestBody.externalSchoolId = externalSchoolId;

return {
requestBody,
loggedInClient,
};
};

it('should throw an UnprocessableEntityException', async () => {
const { requestBody, loggedInClient } = await setup();

const response: Response = await loggedInClient.post(`/force-migration`, requestBody);

expect(response.status).toEqual(HttpStatus.UNPROCESSABLE_ENTITY);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,10 +235,9 @@ export class UserLoginMigrationController {
@ApiCreatedResponse({ description: 'The user and their school were successfully migrated' })
@ApiUnprocessableEntityResponse({
description:
'There are multiple users with the email,' +
'or the user is not an administrator,' +
'or the school is already migrated,' +
'or the external user id is already assigned',
'There are multiple users with the email' +
'or the school had closed or finished the migration' +
'or the external school id does not match with the migrated school',
})
@ApiNotFoundResponse({ description: 'There is no user with the email' })
public async forceMigration(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './school-migration-successful.loggable';
export * from './user-migration-started.loggable';
export * from './user-migration-successful.loggable';
export * from './user-migration-correction-successful-loggable';
Loading
Loading