Skip to content

Commit

Permalink
N21-1971 cancel migration wizard (#5026)
Browse files Browse the repository at this point in the history
  • Loading branch information
mrikallab authored May 30, 2024
1 parent bc68e72 commit 580daf4
Show file tree
Hide file tree
Showing 12 changed files with 369 additions and 10 deletions.
5 changes: 5 additions & 0 deletions apps/server/src/modules/server/server.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { LanguageType } from '@shared/domain/interface';
import { SchulcloudTheme } from '@shared/domain/types';
import type { CoreModuleConfig } from '@src/core';
import type { MailConfig } from '@src/infra/mail/interfaces/mail-config';
import { UserImportConfig } from '../user-import/user-import-config';
import { Timezone } from './types/timezone.enum';

export enum NodeEnvType {
Expand Down Expand Up @@ -61,6 +62,7 @@ export interface ServerConfig
DeletionConfig,
CollaborativeTextEditorConfig,
ProvisioningConfig,
UserImportConfig,
AlertConfig {
NODE_ENV: NodeEnvType;
SC_DOMAIN: string;
Expand Down Expand Up @@ -232,6 +234,9 @@ const config: ServerConfig = {
I18N__DEFAULT_LANGUAGE: Configuration.get('I18N__DEFAULT_LANGUAGE') as unknown as LanguageType,
I18N__FALLBACK_LANGUAGE: Configuration.get('I18N__FALLBACK_LANGUAGE') as unknown as LanguageType,
I18N__DEFAULT_TIMEZONE: Configuration.get('I18N__DEFAULT_TIMEZONE') as Timezone,
IMPORTUSER_SAVE_ALL_MATCHES_REQUEST_TIMEOUT_MS: Configuration.get(
'IMPORTUSER_SAVE_ALL_MATCHES_REQUEST_TIMEOUT_MS'
) as number,
SCHULCONNEX_CLIENT__API_URL: Configuration.has('SCHULCONNEX_CLIENT__API_URL')
? (Configuration.get('SCHULCONNEX_CLIENT__API_URL') as string)
: undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ import { ImportUser, MatchCreator, SchoolEntity, SystemEntity, User } from '@sha
import { Permission, RoleName, SortOrder } from '@shared/domain/interface';
import { SchoolFeature } from '@shared/domain/types';
import {
TestApiClient,
UserAndAccountTestFactory,
cleanupCollections,
importUserFactory,
roleFactory,
schoolEntityFactory,
systemEntityFactory,
TestApiClient,
UserAndAccountTestFactory,
userFactory,
} from '@shared/testing';
import { AccountEntity } from '@src/modules/account/domain/entity/account.entity';
Expand All @@ -46,11 +46,12 @@ describe('ImportUser Controller (API)', () => {
const authenticatedUser = async (
permissions: Permission[] = [],
features: SchoolFeature[] = [],
schoolHasExternalId = true
schoolHasExternalId = true,
officialSchoolNumber = 'foo'
) => {
const system = systemEntityFactory.buildWithId();
const school = schoolEntityFactory.build({
officialSchoolNumber: 'foo',
officialSchoolNumber,
features,
systems: [system],
externalId: schoolHasExternalId ? system.id : undefined,
Expand Down Expand Up @@ -1118,4 +1119,65 @@ describe('ImportUser Controller (API)', () => {
});
});
});

describe('[POST] /user/import/cancel', () => {
describe('when user is unauthorized', () => {
const setup = () => {
return {
notLoggedInClient: new TestApiClient(app, 'user/import'),
};
};

it('should return unauthorized', async () => {
const { notLoggedInClient } = setup();

await notLoggedInClient.post('cancel').expect(HttpStatus.UNAUTHORIZED);
});
});

describe('when user has no permission', () => {
const setup = async () => {
const { account, system } = await authenticatedUser([]);
setConfig(system._id.toString());
const loggedInClient = await testApiClient.login(account);

return {
loggedInClient,
};
};

it('should return unauthorized', async () => {
const { loggedInClient } = await setup();

await loggedInClient.post('cancel').expect(HttpStatus.UNAUTHORIZED);
});
});

describe('when import was successfully canceled', () => {
const setup = async () => {
const { school, system, account } = await authenticatedUser(
[Permission.SCHOOL_IMPORT_USERS_MIGRATE],
[],
true,
'00100'
);
setConfig(system._id.toString());

const importusers = importUserFactory.buildList(10, { school });
await em.persistAndFlush(importusers);

const loggedInClient = await testApiClient.login(account);

return {
loggedInClient,
};
};

it('should return no content', async () => {
const { loggedInClient } = await setup();

await loggedInClient.post('cancel').expect(HttpStatus.NO_CONTENT);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication';
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common';
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Query } from '@nestjs/common';
import {
ApiBadRequestResponse,
ApiCreatedResponse,
ApiForbiddenResponse,
ApiNoContentResponse,
ApiOperation,
ApiServiceUnavailableResponse,
ApiTags,
Expand Down Expand Up @@ -103,6 +104,7 @@ export class ImportUserController {
return response as unknown as UserMatchListResponse;
}

@RequestTimeout('IMPORTUSER_SAVE_ALL_MATCHES_REQUEST_TIMEOUT_MS')
@Post('migrate')
async saveAllUsersMatches(@CurrentUser() currentUser: ICurrentUser): Promise<void> {
await this.userImportUc.saveAllUsersMatches(currentUser.userId);
Expand Down Expand Up @@ -135,4 +137,17 @@ export class ImportUserController {
async populateImportUsers(@CurrentUser() currentUser: ICurrentUser): Promise<void> {
await this.userImportFetchUc.populateImportUsers(currentUser.userId);
}

@Post('cancel')
@ApiOperation({
summary: 'Cancel migration wizard',
description: 'Cancel current migration process',
})
@ApiNoContentResponse()
@ApiUnauthorizedResponse()
@ApiForbiddenResponse()
@HttpCode(HttpStatus.NO_CONTENT)
async cancelMigration(@CurrentUser() currentUser: ICurrentUser): Promise<void> {
await this.userImportUc.cancelMigration(currentUser.userId);
}
}
2 changes: 2 additions & 0 deletions apps/server/src/modules/user-import/loggable/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export { UserImportSchoolExternalIdMissingLoggableException } from './user-impor
export { SchoolNotMigratedLoggableException } from './school-not-migrated.loggable-exception';
export { UserMigrationIsNotEnabled } from './user-migration-not-enable.loggable';
export { UserMigrationIsNotEnabledLoggableException } from './user-migration-not-enable-loggable-exception';
export { UserMigrationCanceledLoggable } from './user-migration-canceled.loggable';
export { UserAlreadyMigratedLoggable } from './user-already-migrated.loggable';
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { UserAlreadyMigratedLoggable } from './user-already-migrated.loggable';

describe('UserAlreadyMigratedLoggable', () => {
describe('getLogMessage', () => {
const setup = () => {
const userId = 'userId';
const loggable = new UserAlreadyMigratedLoggable(userId);

return {
loggable,
userId,
};
};

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

expect(loggable.getLogMessage()).toEqual({
message: 'The user has migrated already and will be skipped during migration process.',
data: {
userId,
},
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger';

export class UserAlreadyMigratedLoggable implements Loggable {
constructor(private readonly userId: string) {}

getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage {
return {
message: 'The user has migrated already and will be skipped during migration process.',
data: {
userId: this.userId,
},
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { legacySchoolDoFactory } from '@shared/testing';
import { UserMigrationCanceledLoggable } from './user-migration-canceled.loggable';

describe('UserMigrationCanceledLoggable', () => {
describe('getLogMessage', () => {
const setup = () => {
const school = legacySchoolDoFactory.buildWithId({ name: 'Schoolname' });
const loggable = new UserMigrationCanceledLoggable(school);

return {
loggable,
school,
};
};

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

const message = loggable.getLogMessage();

expect(message).toEqual({
message: 'The user migration was canceled.',
data: {
schoolName: school.name,
schoolId: school.id,
},
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { LegacySchoolDo } from '@shared/domain/domainobject';
import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger';

export class UserMigrationCanceledLoggable implements Loggable {
constructor(private readonly school: LegacySchoolDo) {}

getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage {
return {
message: 'The user migration was canceled.',
data: {
schoolName: this.school.name,
schoolId: this.school.id,
},
};
}
}
Loading

0 comments on commit 580daf4

Please sign in to comment.