Skip to content

Commit

Permalink
Merge branch 'BC-7695-update-typescript' of github.com:hpi-schul-clou…
Browse files Browse the repository at this point in the history
…d/schulcloud-server into BC-7695-update-typescript
  • Loading branch information
bischofmax committed Jul 25, 2024
2 parents 5685d22 + 508ea79 commit 22139f2
Show file tree
Hide file tree
Showing 24 changed files with 758 additions and 16 deletions.
37 changes: 37 additions & 0 deletions apps/server/src/migrations/mikro-orm/Migration20240724090901.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Migration } from '@mikro-orm/migrations-mongodb';

export class Migration20240724090901 extends Migration {
async up(): Promise<void> {
const superheroRoleUpdate = await this.getCollection('roles').updateOne(
{ name: 'superhero' },
{
$addToSet: {
permissions: {
$each: ['USER_LOGIN_MIGRATION_FORCE'],
},
},
}
);

if (superheroRoleUpdate.modifiedCount > 0) {
console.info('Permission USER_LOGIN_MIGRATION_FORCE was added to role superhero.');
}
}

async down(): Promise<void> {
const superheroRoleUpdate = await this.getCollection('roles').updateOne(
{ name: 'superhero' },
{
$pull: {
permissions: {
$in: ['USER_LOGIN_MIGRATION_FORCE'],
},
},
}
);

if (superheroRoleUpdate.modifiedCount > 0) {
console.info('Rollback: Removed permission USER_LOGIN_MIGRATION_FORCE from role superhero.');
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { UUID } from 'bson';
import { Response } from 'supertest';
import { UserLoginMigrationResponse } from '../dto';
import { Oauth2MigrationParams } from '../dto/oauth2-migration.params';
import { ForceMigrationParams, Oauth2MigrationParams, UserLoginMigrationResponse } from '../dto';

jest.mock('jwks-rsa', () => () => {
return {
Expand Down Expand Up @@ -1405,4 +1404,99 @@ describe('UserLoginMigrationController (API)', () => {
});
});
});

describe('[GET] /user-login-migrations/force-migration', () => {
describe('when forcing a school to migrate', () => {
const setup = async () => {
const targetSystem: SystemEntity = systemEntityFactory
.withOauthConfig()
.buildWithId({ alias: 'SANIS', provisioningStrategy: SystemProvisioningStrategy.SANIS });

const sourceSystem: SystemEntity = systemEntityFactory.buildWithId();

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

const email = '[email protected]';
const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({
email,
school,
});
const { superheroAccount, superheroUser } = UserAndAccountTestFactory.buildSuperhero();

await em.persistAndFlush([
sourceSystem,
targetSystem,
school,
superheroAccount,
superheroUser,
adminAccount,
adminUser,
]);
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,
adminUser,
};
};

it('should start the migration for the school and migrate the user and school', async () => {
const { requestBody, loggedInClient, school, sourceSystem, targetSystem, adminUser } = 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.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(
expect.objectContaining({
externalId: requestBody.externalSchoolId,
})
);
});
});

describe('when authentication of user failed', () => {
const setup = () => {
const requestBody: ForceMigrationParams = new ForceMigrationParams();
requestBody.email = '[email protected]';
requestBody.externalUserId = 'externalUserId';
requestBody.externalSchoolId = 'externalSchoolId';

return {
requestBody,
};
};

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

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

expect(response.status).toEqual(HttpStatus.UNAUTHORIZED);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';

export class ForceMigrationParams {
@IsEmail()
@ApiProperty({ description: 'Email of the administrator' })
email!: string;

@IsString()
@IsNotEmpty()
@ApiProperty({ description: 'Target externalId to link it with an external account' })
externalUserId!: string;

@IsString()
@IsNotEmpty()
@ApiProperty({ description: 'Target externalId to link it with an external school' })
externalSchoolId!: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export { UserIdParams } from './user-id.params';
export { SchoolIdParams } from './school-id.params';
export { UserLoginMigrationSearchParams } from './user-login-migration-search.params';
export { UserLoginMigrationMandatoryParams } from './user-login-migration-mandatory.params';
export { Oauth2MigrationParams } from './oauth2-migration.params';
export { ForceMigrationParams } from './force-migration.params';
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Authenticate, CurrentUser, ICurrentUser, JWT } from '@modules/authentication';
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import {
ApiCreatedResponse,
ApiForbiddenResponse,
ApiInternalServerErrorResponse,
ApiNoContentResponse,
Expand Down Expand Up @@ -29,13 +30,14 @@ import {
UserLoginMigrationUc,
} from '../uc';
import {
ForceMigrationParams,
Oauth2MigrationParams,
SchoolIdParams,
UserLoginMigrationMandatoryParams,
UserLoginMigrationResponse,
UserLoginMigrationSearchListResponse,
UserLoginMigrationSearchParams,
} from './dto';
import { Oauth2MigrationParams } from './dto/oauth2-migration.params';

@ApiTags('UserLoginMigration')
@Controller('user-login-migrations')
Expand Down Expand Up @@ -219,12 +221,35 @@ export class UserLoginMigrationController {

@Post('migrate-to-oauth2')
@ApiOkResponse({ description: 'The User has been successfully migrated.', status: 200 })
@ApiInternalServerErrorResponse({ description: 'The migration of the User was not possible.' })
@ApiUnprocessableEntityResponse({ description: 'The migration of the User was not possible.' })
async migrateUserLogin(
@JWT() jwt: string,
@CurrentUser() currentUser: ICurrentUser,
@Body() body: Oauth2MigrationParams
): Promise<void> {
await this.userLoginMigrationUc.migrate(jwt, currentUser.userId, body.systemId, body.code, body.redirectUri);
}

@Post('force-migration')
@ApiOperation({ summary: 'Force migrate an administrator account and its school' })
@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',
})
@ApiNotFoundResponse({ description: 'There is no user with the email' })
public async forceMigration(
@CurrentUser() currentUser: ICurrentUser,
@Body() forceMigrationParams: ForceMigrationParams
): Promise<void> {
await this.userLoginMigrationUc.forceMigration(
currentUser.userId,
forceMigrationParams.email,
forceMigrationParams.externalUserId,
forceMigrationParams.externalSchoolId
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@ export * from './identical-user-login-migration-system.loggable-exception';
export * from './moin-schule-system-not-found.loggable-exception';
export { UserNotMigratedLoggableException } from './user-not-migrated.loggable-exception';
export { UserMigrationRollbackSuccessfulLoggable } from './user-migration-rollback-successful.loggable';
export { UserLoginMigrationSchoolAlreadyMigratedLoggableException } from './user-login-migration-school-already-migrated.loggable-exception';
export { UserLoginMigrationInvalidAdminLoggableException } from './user-login-migration-invalid-admin.loggable-exception';
export { UserLoginMigrationMultipleEmailUsersLoggableException } from './user-login-migration-multiple-email-users.loggable-exception';
export * from './debug';
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ObjectId } from '@mikro-orm/mongodb';
import { UserLoginMigrationInvalidAdminLoggableException } from './user-login-migration-invalid-admin.loggable-exception';

describe(UserLoginMigrationInvalidAdminLoggableException.name, () => {
describe('getLogMessage', () => {
const setup = () => {
const userId = new ObjectId().toHexString();
const exception = new UserLoginMigrationInvalidAdminLoggableException(userId);

return {
exception,
userId,
};
};

it('should return the correct log message', () => {
const { exception, userId } = setup();

const message = exception.getLogMessage();

expect(message).toEqual({
type: 'USER_LOGIN_MIGRATION_INVALID_ADMIN',
message: 'The user is not an administrator',
stack: exception.stack,
data: {
userId,
},
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { UnprocessableEntityException } from '@nestjs/common';
import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger';

export class UserLoginMigrationInvalidAdminLoggableException extends UnprocessableEntityException implements Loggable {
constructor(private readonly userId: string) {
super();
}

getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage {
return {
type: 'USER_LOGIN_MIGRATION_INVALID_ADMIN',
message: 'The user is not an administrator',
stack: this.stack,
data: {
userId: this.userId,
},
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { UserLoginMigrationMultipleEmailUsersLoggableException } from './user-login-migration-multiple-email-users.loggable-exception';

describe(UserLoginMigrationMultipleEmailUsersLoggableException.name, () => {
describe('getLogMessage', () => {
const setup = () => {
const email = '[email protected]';
const exception = new UserLoginMigrationMultipleEmailUsersLoggableException(email);

return {
exception,
email,
};
};

it('should return the correct log message', () => {
const { exception, email } = setup();

const message = exception.getLogMessage();

expect(message).toEqual({
type: 'USER_LOGIN_MIGRATION_MULTIPLE_EMAIL_USERS',
message: 'There is multiple users with this email',
stack: exception.stack,
data: {
email,
},
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { UnprocessableEntityException } from '@nestjs/common';
import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger';

export class UserLoginMigrationMultipleEmailUsersLoggableException
extends UnprocessableEntityException
implements Loggable
{
constructor(private readonly email: string) {
super();
}

getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage {
return {
type: 'USER_LOGIN_MIGRATION_MULTIPLE_EMAIL_USERS',
message: 'There is multiple users with this email',
stack: this.stack,
data: {
email: this.email,
},
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ObjectId } from '@mikro-orm/mongodb';
import { UserLoginMigrationSchoolAlreadyMigratedLoggableException } from './user-login-migration-school-already-migrated.loggable-exception';

describe(UserLoginMigrationSchoolAlreadyMigratedLoggableException.name, () => {
describe('getLogMessage', () => {
const setup = () => {
const schoolId = new ObjectId().toHexString();
const exception = new UserLoginMigrationSchoolAlreadyMigratedLoggableException(schoolId);

return {
exception,
schoolId,
};
};

it('should return the correct log message', () => {
const { exception, schoolId } = setup();

const message = exception.getLogMessage();

expect(message).toEqual({
type: 'USER_LOGIN_MIGRATION_SCHOOL_HAS_ALREADY_MIGRATED',
message: 'School has already migrated',
stack: exception.stack,
data: {
schoolId,
},
});
});
});
});
Loading

0 comments on commit 22139f2

Please sign in to comment.