diff --git a/apps/server/src/infra/schulconnex-client/index.ts b/apps/server/src/infra/schulconnex-client/index.ts new file mode 100644 index 00000000000..91c0f2dfc20 --- /dev/null +++ b/apps/server/src/infra/schulconnex-client/index.ts @@ -0,0 +1,20 @@ +export { SchulconnexRestClientOptions } from './schulconnex-rest-client-options'; +export { SchulconnexClientModule } from './schulconnex-client.module'; +export { SchulconnexRestClient } from './schulconnex-rest-client'; +export { + SanisResponse, + SanisRole, + SanisGroupRole, + SanisGroupType, + SanisGruppenResponse, + SanisResponseValidationGroups, + SanisPersonResponse, + SanisAnschriftResponse, + SanisGruppenzugehoerigkeitResponse, + SanisGruppeResponse, + SanisNameResponse, + SanisOrganisationResponse, + SanisPersonenkontextResponse, + SanisSonstigeGruppenzugehoerigeResponse, +} from './response'; +export { schulconnexResponseFactory } from './testing/schulconnex-response-factory'; diff --git a/apps/server/src/infra/schulconnex-client/loggable/index.ts b/apps/server/src/infra/schulconnex-client/loggable/index.ts new file mode 100644 index 00000000000..1b4cbfe6148 --- /dev/null +++ b/apps/server/src/infra/schulconnex-client/loggable/index.ts @@ -0,0 +1 @@ +export { SchulconnexConfigurationMissingLoggable } from './schulconnex-configuration-missing.loggable'; diff --git a/apps/server/src/infra/schulconnex-client/loggable/schulconnex-configuration-missing.loggable.spec.ts b/apps/server/src/infra/schulconnex-client/loggable/schulconnex-configuration-missing.loggable.spec.ts new file mode 100644 index 00000000000..20adf239652 --- /dev/null +++ b/apps/server/src/infra/schulconnex-client/loggable/schulconnex-configuration-missing.loggable.spec.ts @@ -0,0 +1,16 @@ +import { SchulconnexConfigurationMissingLoggable } from './schulconnex-configuration-missing.loggable'; + +describe(SchulconnexConfigurationMissingLoggable.name, () => { + describe('getLogMessage', () => { + it('should return a log message', () => { + const loggable: SchulconnexConfigurationMissingLoggable = new SchulconnexConfigurationMissingLoggable(); + + const logMessage = loggable.getLogMessage(); + + expect(logMessage).toEqual({ + message: + 'SchulconnexRestClient: Missing configuration. Please check your environment variables in SCHULCONNEX_CLIENT.', + }); + }); + }); +}); diff --git a/apps/server/src/modules/user-import/loggable/user-migration-not-enable.loggable.ts b/apps/server/src/infra/schulconnex-client/loggable/schulconnex-configuration-missing.loggable.ts similarity index 50% rename from apps/server/src/modules/user-import/loggable/user-migration-not-enable.loggable.ts rename to apps/server/src/infra/schulconnex-client/loggable/schulconnex-configuration-missing.loggable.ts index 2d3de69bfbc..ec0e299c21b 100644 --- a/apps/server/src/modules/user-import/loggable/user-migration-not-enable.loggable.ts +++ b/apps/server/src/infra/schulconnex-client/loggable/schulconnex-configuration-missing.loggable.ts @@ -1,9 +1,9 @@ import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; -export class UserMigrationIsNotEnabled implements Loggable { +export class SchulconnexConfigurationMissingLoggable implements Loggable { getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { return { - message: 'Feature flag of user migration may be disable or the school is not an LDAP pilot', + message: `SchulconnexRestClient: Missing configuration. Please check your environment variables in SCHULCONNEX_CLIENT.`, }; } } diff --git a/apps/server/src/infra/schulconnex-client/request/index.ts b/apps/server/src/infra/schulconnex-client/request/index.ts new file mode 100644 index 00000000000..a0bae6cf2d6 --- /dev/null +++ b/apps/server/src/infra/schulconnex-client/request/index.ts @@ -0,0 +1 @@ +export { SchulconnexPersonenInfoParams } from './schulconnex-personen-info-params'; diff --git a/apps/server/src/infra/schulconnex-client/request/schulconnex-personen-info-params.ts b/apps/server/src/infra/schulconnex-client/request/schulconnex-personen-info-params.ts new file mode 100644 index 00000000000..35aa16da657 --- /dev/null +++ b/apps/server/src/infra/schulconnex-client/request/schulconnex-personen-info-params.ts @@ -0,0 +1,13 @@ +export type SchulconnexPropertyContext = 'personen' | 'personenkontexte' | 'organisationen' | 'gruppen' | 'beziehungen'; + +export interface SchulconnexPersonenInfoParams { + vollstaendig?: SchulconnexPropertyContext[]; + + pid?: string; + + 'personenkontext.id'?: string; + + 'organisation.id'?: string; + + 'gruppe.id'?: string; +} diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/index.ts b/apps/server/src/infra/schulconnex-client/response/index.ts similarity index 100% rename from apps/server/src/modules/provisioning/strategy/sanis/response/index.ts rename to apps/server/src/infra/schulconnex-client/response/index.ts diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-anschrift-response.ts b/apps/server/src/infra/schulconnex-client/response/sanis-anschrift-response.ts similarity index 100% rename from apps/server/src/modules/provisioning/strategy/sanis/response/sanis-anschrift-response.ts rename to apps/server/src/infra/schulconnex-client/response/sanis-anschrift-response.ts diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-geburt-response.ts b/apps/server/src/infra/schulconnex-client/response/sanis-geburt-response.ts similarity index 100% rename from apps/server/src/modules/provisioning/strategy/sanis/response/sanis-geburt-response.ts rename to apps/server/src/infra/schulconnex-client/response/sanis-geburt-response.ts diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-group-role.ts b/apps/server/src/infra/schulconnex-client/response/sanis-group-role.ts similarity index 100% rename from apps/server/src/modules/provisioning/strategy/sanis/response/sanis-group-role.ts rename to apps/server/src/infra/schulconnex-client/response/sanis-group-role.ts diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-group-type.ts b/apps/server/src/infra/schulconnex-client/response/sanis-group-type.ts similarity index 100% rename from apps/server/src/modules/provisioning/strategy/sanis/response/sanis-group-type.ts rename to apps/server/src/infra/schulconnex-client/response/sanis-group-type.ts diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-gruppe-response.ts b/apps/server/src/infra/schulconnex-client/response/sanis-gruppe-response.ts similarity index 100% rename from apps/server/src/modules/provisioning/strategy/sanis/response/sanis-gruppe-response.ts rename to apps/server/src/infra/schulconnex-client/response/sanis-gruppe-response.ts diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-gruppen-response.ts b/apps/server/src/infra/schulconnex-client/response/sanis-gruppen-response.ts similarity index 100% rename from apps/server/src/modules/provisioning/strategy/sanis/response/sanis-gruppen-response.ts rename to apps/server/src/infra/schulconnex-client/response/sanis-gruppen-response.ts diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-gruppenzugehoerigkeit-response.ts b/apps/server/src/infra/schulconnex-client/response/sanis-gruppenzugehoerigkeit-response.ts similarity index 100% rename from apps/server/src/modules/provisioning/strategy/sanis/response/sanis-gruppenzugehoerigkeit-response.ts rename to apps/server/src/infra/schulconnex-client/response/sanis-gruppenzugehoerigkeit-response.ts diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-name-response.ts b/apps/server/src/infra/schulconnex-client/response/sanis-name-response.ts similarity index 100% rename from apps/server/src/modules/provisioning/strategy/sanis/response/sanis-name-response.ts rename to apps/server/src/infra/schulconnex-client/response/sanis-name-response.ts diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-organisation-response.ts b/apps/server/src/infra/schulconnex-client/response/sanis-organisation-response.ts similarity index 100% rename from apps/server/src/modules/provisioning/strategy/sanis/response/sanis-organisation-response.ts rename to apps/server/src/infra/schulconnex-client/response/sanis-organisation-response.ts diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-person-response.ts b/apps/server/src/infra/schulconnex-client/response/sanis-person-response.ts similarity index 100% rename from apps/server/src/modules/provisioning/strategy/sanis/response/sanis-person-response.ts rename to apps/server/src/infra/schulconnex-client/response/sanis-person-response.ts diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-personenkontext-response.ts b/apps/server/src/infra/schulconnex-client/response/sanis-personenkontext-response.ts similarity index 100% rename from apps/server/src/modules/provisioning/strategy/sanis/response/sanis-personenkontext-response.ts rename to apps/server/src/infra/schulconnex-client/response/sanis-personenkontext-response.ts diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-response-validation-groups.ts b/apps/server/src/infra/schulconnex-client/response/sanis-response-validation-groups.ts similarity index 100% rename from apps/server/src/modules/provisioning/strategy/sanis/response/sanis-response-validation-groups.ts rename to apps/server/src/infra/schulconnex-client/response/sanis-response-validation-groups.ts diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-role.ts b/apps/server/src/infra/schulconnex-client/response/sanis-role.ts similarity index 100% rename from apps/server/src/modules/provisioning/strategy/sanis/response/sanis-role.ts rename to apps/server/src/infra/schulconnex-client/response/sanis-role.ts diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-sonstige-gruppenzugehoerige-response.ts b/apps/server/src/infra/schulconnex-client/response/sanis-sonstige-gruppenzugehoerige-response.ts similarity index 100% rename from apps/server/src/modules/provisioning/strategy/sanis/response/sanis-sonstige-gruppenzugehoerige-response.ts rename to apps/server/src/infra/schulconnex-client/response/sanis-sonstige-gruppenzugehoerige-response.ts diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis.response.ts b/apps/server/src/infra/schulconnex-client/response/sanis.response.ts similarity index 100% rename from apps/server/src/modules/provisioning/strategy/sanis/response/sanis.response.ts rename to apps/server/src/infra/schulconnex-client/response/sanis.response.ts diff --git a/apps/server/src/infra/schulconnex-client/schulconnex-api.interface.ts b/apps/server/src/infra/schulconnex-client/schulconnex-api.interface.ts new file mode 100644 index 00000000000..66ea77be98d --- /dev/null +++ b/apps/server/src/infra/schulconnex-client/schulconnex-api.interface.ts @@ -0,0 +1,8 @@ +import { SchulconnexPersonenInfoParams } from './request'; +import { SanisResponse } from './response'; + +export interface SchulconnexApiInterface { + getPersonInfo(accessToken: string, options?: { overrideUrl: string }): Promise; + + getPersonenInfo(params: SchulconnexPersonenInfoParams): Promise; +} diff --git a/apps/server/src/infra/schulconnex-client/schulconnex-client.module.ts b/apps/server/src/infra/schulconnex-client/schulconnex-client.module.ts new file mode 100644 index 00000000000..ec4b08dd303 --- /dev/null +++ b/apps/server/src/infra/schulconnex-client/schulconnex-client.module.ts @@ -0,0 +1,30 @@ +import { OauthAdapterService, OauthModule } from '@modules/oauth'; +import { HttpModule, HttpService } from '@nestjs/axios'; +import { DynamicModule, Global, Module } from '@nestjs/common'; +import { Logger, LoggerModule } from '@src/core/logger'; +import { SchulconnexRestClient } from './schulconnex-rest-client'; +import { SchulconnexRestClientOptions } from './schulconnex-rest-client-options'; + +@Global() +/** + * @Global is used here to make sure that the module is only instantiated once, with the configuration and can be used in every module. + * Otherwise, you need to import the module with configuration in every module where you want to use it. + */ +@Module({}) +export class SchulconnexClientModule { + static register(options: SchulconnexRestClientOptions): DynamicModule { + return { + imports: [HttpModule, OauthModule, LoggerModule], + module: SchulconnexClientModule, + providers: [ + { + provide: SchulconnexRestClient, + useFactory: (httpService: HttpService, oauthAdapterService: OauthAdapterService, logger: Logger) => + new SchulconnexRestClient(options, httpService, oauthAdapterService, logger), + inject: [HttpService, OauthAdapterService, Logger], + }, + ], + exports: [SchulconnexRestClient], + }; + } +} diff --git a/apps/server/src/infra/schulconnex-client/schulconnex-rest-client-options.ts b/apps/server/src/infra/schulconnex-client/schulconnex-rest-client-options.ts new file mode 100644 index 00000000000..d269bdc0544 --- /dev/null +++ b/apps/server/src/infra/schulconnex-client/schulconnex-rest-client-options.ts @@ -0,0 +1,9 @@ +export interface SchulconnexRestClientOptions { + apiUrl: string; + + tokenEndpoint: string; + + clientId: string; + + clientSecret: string; +} diff --git a/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.spec.ts b/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.spec.ts new file mode 100644 index 00000000000..a4312821499 --- /dev/null +++ b/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.spec.ts @@ -0,0 +1,167 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { OauthAdapterService, OAuthTokenDto } from '@modules/oauth'; +import { HttpService } from '@nestjs/axios'; +import { axiosResponseFactory } from '@shared/testing'; +import { Logger } from '@src/core/logger'; +import { of } from 'rxjs'; +import { SchulconnexConfigurationMissingLoggable } from './loggable'; +import { SanisResponse } from './response'; +import { SchulconnexRestClient } from './schulconnex-rest-client'; +import { SchulconnexRestClientOptions } from './schulconnex-rest-client-options'; +import { schulconnexResponseFactory } from './testing'; + +describe(SchulconnexRestClient.name, () => { + let client: SchulconnexRestClient; + + let httpService: DeepMocked; + let oauthAdapterService: DeepMocked; + let logger: DeepMocked; + const options: SchulconnexRestClientOptions = { + apiUrl: 'https://schulconnex.url/api', + clientId: 'clientId', + clientSecret: 'clientSecret', + tokenEndpoint: 'https://schulconnex.url/token', + }; + + beforeAll(() => { + httpService = createMock(); + oauthAdapterService = createMock(); + logger = createMock(); + + client = new SchulconnexRestClient(options, httpService, oauthAdapterService, logger); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('constructor', () => { + describe('when configuration is missing', () => { + const setup = () => { + const badOptions: SchulconnexRestClientOptions = { + apiUrl: '', + clientId: '', + clientSecret: '', + tokenEndpoint: '', + }; + return { + badOptions, + }; + }; + + it('should log a message', () => { + const { badOptions } = setup(); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const badOptionsClient = new SchulconnexRestClient(badOptions, httpService, oauthAdapterService, logger); + + expect(logger.debug).toHaveBeenCalledWith(new SchulconnexConfigurationMissingLoggable()); + }); + }); + }); + + describe('getPersonInfo', () => { + describe('when requesting person-info', () => { + const setup = () => { + const accessToken = 'accessToken'; + const response: SanisResponse = schulconnexResponseFactory.build(); + + httpService.get.mockReturnValueOnce(of(axiosResponseFactory.build({ data: response }))); + + return { + accessToken, + response, + }; + }; + + it('should make a request to a SchulConneX-API', async () => { + const { accessToken } = setup(); + + await client.getPersonInfo(accessToken); + + expect(httpService.get).toHaveBeenCalledWith(`${options.apiUrl}/person-info`, { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Accept-Encoding': 'gzip', + }, + }); + }); + + it('should return the response', async () => { + const { accessToken, response } = setup(); + + const result: SanisResponse = await client.getPersonInfo(accessToken); + + expect(result).toEqual(response); + }); + }); + + describe('when overriding the url', () => { + const setup = () => { + const accessToken = 'accessToken'; + const customUrl = 'https://override.url/person-info'; + const response: SanisResponse = schulconnexResponseFactory.build(); + + httpService.get.mockReturnValueOnce(of(axiosResponseFactory.build({ data: response }))); + + return { + accessToken, + customUrl, + }; + }; + + it('should make a request to a SchulConneX-API', async () => { + const { accessToken, customUrl } = setup(); + + await client.getPersonInfo(accessToken, { overrideUrl: customUrl }); + + expect(httpService.get).toHaveBeenCalledWith(customUrl, expect.anything()); + }); + }); + }); + + describe('getPersonenInfo', () => { + describe('when requesting personen-info', () => { + const setup = () => { + const tokens: OAuthTokenDto = new OAuthTokenDto({ + idToken: 'id_token', + accessToken: 'access_token', + refreshToken: 'refresh_token', + }); + const response: SanisResponse[] = schulconnexResponseFactory.buildList(2); + + oauthAdapterService.sendTokenRequest.mockResolvedValueOnce(tokens); + httpService.get.mockReturnValueOnce(of(axiosResponseFactory.build({ data: response }))); + + return { + tokens, + response, + }; + }; + + it('should make a request to a SchulConneX-API', async () => { + const { tokens } = setup(); + + await client.getPersonenInfo({ 'organisation.id': '1234', vollstaendig: ['personen', 'organisationen'] }); + + expect(httpService.get).toHaveBeenCalledWith( + `${options.apiUrl}/personen-info?organisation.id=1234&vollstaendig=personen%2Corganisationen`, + { + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + 'Accept-Encoding': 'gzip', + }, + } + ); + }); + + it('should return the response', async () => { + const { response } = setup(); + + const result: SanisResponse[] = await client.getPersonenInfo({ 'organisation.id': '1234' }); + + expect(result).toEqual(response); + }); + }); + }); +}); diff --git a/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.ts b/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.ts new file mode 100644 index 00000000000..c42e077d156 --- /dev/null +++ b/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.ts @@ -0,0 +1,80 @@ +import { OauthAdapterService, OAuthTokenDto } from '@modules/oauth'; +import { OAuthGrantType } from '@modules/oauth/interface/oauth-grant-type.enum'; +import { ClientCredentialsGrantTokenRequest } from '@modules/oauth/service/dto'; +import { HttpService } from '@nestjs/axios'; +import { Logger } from '@src/core/logger'; +import { AxiosResponse } from 'axios'; +import QueryString from 'qs'; +import { lastValueFrom, Observable } from 'rxjs'; +import { SchulconnexConfigurationMissingLoggable } from './loggable'; +import { SchulconnexPersonenInfoParams } from './request'; +import { SanisResponse } from './response'; +import { SchulconnexApiInterface } from './schulconnex-api.interface'; +import { SchulconnexRestClientOptions } from './schulconnex-rest-client-options'; + +export class SchulconnexRestClient implements SchulconnexApiInterface { + private readonly SCHULCONNEX_API_BASE_URL: string; + + constructor( + private readonly options: SchulconnexRestClientOptions, + private readonly httpService: HttpService, + private readonly oauthAdapterService: OauthAdapterService, + private readonly logger: Logger + ) { + this.checkOptions(); + this.SCHULCONNEX_API_BASE_URL = options.apiUrl; + } + + // TODO: N21-1678 use this in provisioning module + public async getPersonInfo(accessToken: string, options?: { overrideUrl: string }): Promise { + const url: URL = new URL(options?.overrideUrl ?? `${this.SCHULCONNEX_API_BASE_URL}/person-info`); + + const response: Promise = this.getRequest(url, accessToken); + + return response; + } + + public async getPersonenInfo(params: SchulconnexPersonenInfoParams): Promise { + const token: OAuthTokenDto = await this.requestClientCredentialToken(); + + const url: URL = new URL(`${this.SCHULCONNEX_API_BASE_URL}/personen-info`); + url.search = QueryString.stringify(params, { arrayFormat: 'comma' }); + + const response: Promise = this.getRequest(url, token.accessToken); + + return response; + } + + private checkOptions(): void { + if (!this.options.apiUrl || !this.options.clientId || !this.options.clientSecret || !this.options.tokenEndpoint) { + this.logger.debug(new SchulconnexConfigurationMissingLoggable()); + } + } + + private async getRequest(url: URL, accessToken: string): Promise { + const observable: Observable> = this.httpService.get(url.toString(), { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Accept-Encoding': 'gzip', + }, + }); + + const responseToken: AxiosResponse = await lastValueFrom(observable); + + return responseToken.data; + } + + private async requestClientCredentialToken(): Promise { + const { tokenEndpoint, clientId, clientSecret } = this.options; + + const payload: ClientCredentialsGrantTokenRequest = new ClientCredentialsGrantTokenRequest({ + client_id: clientId, + client_secret: clientSecret, + grant_type: OAuthGrantType.CLIENT_CREDENTIALS_GRANT, + }); + + const tokenDto: OAuthTokenDto = await this.oauthAdapterService.sendTokenRequest(tokenEndpoint, payload); + + return tokenDto; + } +} diff --git a/apps/server/src/infra/schulconnex-client/testing/index.ts b/apps/server/src/infra/schulconnex-client/testing/index.ts new file mode 100644 index 00000000000..673fc651a51 --- /dev/null +++ b/apps/server/src/infra/schulconnex-client/testing/index.ts @@ -0,0 +1 @@ +export { schulconnexResponseFactory } from './schulconnex-response-factory'; diff --git a/apps/server/src/infra/schulconnex-client/testing/schulconnex-response-factory.ts b/apps/server/src/infra/schulconnex-client/testing/schulconnex-response-factory.ts new file mode 100644 index 00000000000..090204c52d3 --- /dev/null +++ b/apps/server/src/infra/schulconnex-client/testing/schulconnex-response-factory.ts @@ -0,0 +1,50 @@ +import { UUID } from 'bson'; +import { Factory } from 'fishery'; +import { SanisGroupRole, SanisGroupType, SanisResponse, SanisRole } from '../response'; + +export const schulconnexResponseFactory = Factory.define(() => { + return { + pid: 'aef1f4fd-c323-466e-962b-a84354c0e713', + person: { + name: { + vorname: 'Hans', + familienname: 'Peter', + }, + geburt: { + datum: '2023-11-17', + }, + }, + personenkontexte: [ + { + id: new UUID().toString(), + rolle: SanisRole.LEIT, + organisation: { + id: new UUID('df66c8e6-cfac-40f7-b35b-0da5d8ee680e').toString(), + name: 'schoolName', + kennung: 'Kennung', + anschrift: { + ort: 'Hannover', + }, + }, + gruppen: [ + { + gruppe: { + id: new UUID().toString(), + bezeichnung: 'bezeichnung', + typ: SanisGroupType.CLASS, + }, + gruppenzugehoerigkeit: { + rollen: [SanisGroupRole.TEACHER], + }, + sonstige_gruppenzugehoerige: [ + { + rollen: [SanisGroupRole.STUDENT], + ktid: 'ktid', + }, + ], + }, + ], + }, + ], + }; +}); diff --git a/apps/server/src/modules/account/controller/api-test/account.api.spec.ts b/apps/server/src/modules/account/controller/api-test/account.api.spec.ts index 14d68ae5668..f921104e9d2 100644 --- a/apps/server/src/modules/account/controller/api-test/account.api.spec.ts +++ b/apps/server/src/modules/account/controller/api-test/account.api.spec.ts @@ -3,7 +3,7 @@ import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Account, User } from '@shared/domain/entity'; import { Permission, RoleName } from '@shared/domain/interface'; -import { accountFactory, mapUserToCurrentUser, roleFactory, schoolFactory, userFactory } from '@shared/testing'; +import { accountFactory, mapUserToCurrentUser, roleFactory, schoolEntityFactory, userFactory } from '@shared/testing'; import { AccountByIdBodyParams, AccountSearchQueryParams, @@ -39,7 +39,7 @@ describe('Account Controller (API)', () => { const defaultPasswordHash = '$2a$10$/DsztV5o6P5piW2eWJsxw.4nHovmJGBA.QNwiTmuZ/uvUc40b.Uhu'; const setup = async () => { - const school = schoolFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); const adminRoles = roleFactory.build({ name: RoleName.ADMINISTRATOR, diff --git a/apps/server/src/modules/account/services/account-db.service.spec.ts b/apps/server/src/modules/account/services/account-db.service.spec.ts index 21b8c890245..45663590e7b 100644 --- a/apps/server/src/modules/account/services/account-db.service.spec.ts +++ b/apps/server/src/modules/account/services/account-db.service.spec.ts @@ -11,7 +11,7 @@ import { Account, Role, SchoolEntity, User } from '@shared/domain/entity'; import { Permission, RoleName } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; -import { accountFactory, schoolFactory, setupEntities, userFactory } from '@shared/testing'; +import { accountFactory, schoolEntityFactory, setupEntities, userFactory } from '@shared/testing'; import bcrypt from 'bcryptjs'; import { LegacyLogger } from '../../../core/logger'; import { AccountRepo } from '../repo/account.repo'; @@ -149,7 +149,7 @@ describe('AccountDbService', () => { jest.useFakeTimers(); jest.setSystemTime(new Date(2020, 1, 1)); - mockSchool = schoolFactory.buildWithId(); + mockSchool = schoolEntityFactory.buildWithId(); mockTeacherUser = userFactory.buildWithId({ school: mockSchool, diff --git a/apps/server/src/modules/account/uc/account.uc.spec.ts b/apps/server/src/modules/account/uc/account.uc.spec.ts index b2f6e3356d1..94a2ce59922 100644 --- a/apps/server/src/modules/account/uc/account.uc.spec.ts +++ b/apps/server/src/modules/account/uc/account.uc.spec.ts @@ -11,7 +11,7 @@ import { Permission, RoleName } from '@shared/domain/interface'; import { PermissionService } from '@shared/domain/service'; import { Counted, EntityId } from '@shared/domain/types'; import { UserRepo } from '@shared/repo'; -import { accountFactory, schoolFactory, setupEntities, systemEntityFactory, userFactory } from '@shared/testing'; +import { accountFactory, schoolEntityFactory, setupEntities, systemEntityFactory, userFactory } from '@shared/testing'; import { BruteForcePrevention } from '@src/imports-from-feathers'; import { ObjectId } from 'bson'; import { @@ -246,9 +246,9 @@ describe('AccountUc', () => { }); beforeEach(() => { - mockSchool = schoolFactory.buildWithId(); - mockOtherSchool = schoolFactory.buildWithId(); - mockSchoolWithStudentVisibility = schoolFactory.buildWithId(); + mockSchool = schoolEntityFactory.buildWithId(); + mockOtherSchool = schoolEntityFactory.buildWithId(); + mockSchoolWithStudentVisibility = schoolEntityFactory.buildWithId(); mockSchoolWithStudentVisibility.permissions = new SchoolRoles(); mockSchoolWithStudentVisibility.permissions.teacher = new SchoolRolePermission(); mockSchoolWithStudentVisibility.permissions.teacher.STUDENT_LIST = true; diff --git a/apps/server/src/modules/authentication/controllers/api-test/login.api.spec.ts b/apps/server/src/modules/authentication/controllers/api-test/login.api.spec.ts index 5ebf33df21f..b46164b99b2 100644 --- a/apps/server/src/modules/authentication/controllers/api-test/login.api.spec.ts +++ b/apps/server/src/modules/authentication/controllers/api-test/login.api.spec.ts @@ -5,7 +5,7 @@ import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Account, SchoolEntity, SystemEntity, User } from '@shared/domain/entity'; import { RoleName } from '@shared/domain/interface'; -import { accountFactory, roleFactory, schoolFactory, systemEntityFactory, userFactory } from '@shared/testing'; +import { accountFactory, roleFactory, schoolEntityFactory, systemEntityFactory, userFactory } from '@shared/testing'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import crypto, { KeyPairKeyObjectResult } from 'crypto'; @@ -95,7 +95,7 @@ describe('Login Controller (api)', () => { let user: User; beforeAll(async () => { - const school = schoolFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); user = userFactory.buildWithId({ school, roles: [studentRoles] }); @@ -150,7 +150,10 @@ describe('Login Controller (api)', () => { const setup = async () => { const schoolExternalId = 'mockSchoolExternalId'; const system: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({}); - const school: SchoolEntity = schoolFactory.buildWithId({ systems: [system], externalId: schoolExternalId }); + const school: SchoolEntity = schoolEntityFactory.buildWithId({ + systems: [system], + externalId: schoolExternalId, + }); const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); const user: User = userFactory.buildWithId({ school, roles: [studentRoles], ldapDn: mockUserLdapDN }); @@ -201,7 +204,10 @@ describe('Login Controller (api)', () => { const setup = async () => { const schoolExternalId = 'mockSchoolExternalId'; const system: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({}); - const school: SchoolEntity = schoolFactory.buildWithId({ systems: [system], externalId: schoolExternalId }); + const school: SchoolEntity = schoolEntityFactory.buildWithId({ + systems: [system], + externalId: schoolExternalId, + }); const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); const user: User = userFactory.buildWithId({ school, roles: [studentRoles], ldapDn: mockUserLdapDN }); @@ -239,7 +245,7 @@ describe('Login Controller (api)', () => { const setup = async () => { const officialSchoolNumber = '01234'; const system: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({}); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [system], externalId: officialSchoolNumber, officialSchoolNumber, @@ -302,7 +308,7 @@ describe('Login Controller (api)', () => { const userExternalId = 'userExternalId'; const system = systemEntityFactory.withOauthConfig().buildWithId({}); - const school = schoolFactory.buildWithId({ systems: [system], externalId: schoolExternalId }); + const school = schoolEntityFactory.buildWithId({ systems: [system], externalId: schoolExternalId }); const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); const user = userFactory.buildWithId({ school, roles: [studentRoles], externalId: userExternalId }); const account = accountFactory.buildWithId({ @@ -392,7 +398,7 @@ describe('Login Controller (api)', () => { const userExternalId = 'userExternalId'; const system = systemEntityFactory.withOauthConfig().buildWithId({}); - const school = schoolFactory.buildWithId({ systems: [system], externalId: schoolExternalId }); + const school = schoolEntityFactory.buildWithId({ systems: [system], externalId: schoolExternalId }); const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); const user = userFactory.buildWithId({ school, roles: [studentRoles], externalId: userExternalId }); const account = accountFactory.buildWithId({ diff --git a/apps/server/src/modules/authentication/mapper/current-user.mapper.spec.ts b/apps/server/src/modules/authentication/mapper/current-user.mapper.spec.ts index 312bfeb3800..c852b725fd3 100644 --- a/apps/server/src/modules/authentication/mapper/current-user.mapper.spec.ts +++ b/apps/server/src/modules/authentication/mapper/current-user.mapper.spec.ts @@ -1,7 +1,7 @@ import { ValidationError } from '@shared/common'; import { UserDO } from '@shared/domain/domainobject/user.do'; import { Permission, RoleName } from '@shared/domain/interface'; -import { roleFactory, schoolFactory, setupEntities, userDoFactory, userFactory } from '@shared/testing'; +import { roleFactory, schoolEntityFactory, setupEntities, userDoFactory, userFactory } from '@shared/testing'; import { ICurrentUser, OauthCurrentUser } from '../interface'; import { CreateJwtPayload, JwtPayload } from '../interface/jwt-payload'; import { CurrentUserMapper } from './current-user.mapper'; @@ -65,7 +65,7 @@ describe('CurrentUserMapper', () => { describe('when systemId is provided', () => { const setup = () => { const user = userFactory.buildWithId({ - school: schoolFactory.buildWithId(), + school: schoolEntityFactory.buildWithId(), }); const systemId = 'mockSystemId'; diff --git a/apps/server/src/modules/authentication/strategy/ldap.strategy.spec.ts b/apps/server/src/modules/authentication/strategy/ldap.strategy.spec.ts index 7740b56b44e..56a27305ca3 100644 --- a/apps/server/src/modules/authentication/strategy/ldap.strategy.spec.ts +++ b/apps/server/src/modules/authentication/strategy/ldap.strategy.spec.ts @@ -13,7 +13,7 @@ import { defaultTestPassword, defaultTestPasswordHash, legacySchoolDoFactory, - schoolFactory, + schoolEntityFactory, setupEntities, systemEntityFactory, userFactory, @@ -389,7 +389,7 @@ describe('LdapStrategy', () => { const user: User = userFactory .withRoleByName(RoleName.STUDENT) - .buildWithId({ ldapDn: 'mockLdapDn', school: schoolFactory.buildWithId() }); + .buildWithId({ ldapDn: 'mockLdapDn', school: schoolEntityFactory.buildWithId() }); const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId( { systems: [system.id], previousExternalId: undefined }, @@ -452,7 +452,7 @@ describe('LdapStrategy', () => { const user: User = userFactory .withRoleByName(RoleName.STUDENT) - .buildWithId({ ldapDn: 'mockLdapDn', school: schoolFactory.buildWithId() }); + .buildWithId({ ldapDn: 'mockLdapDn', school: schoolEntityFactory.buildWithId() }); const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId( { systems: [system.id], previousExternalId: 'previousExternalId' }, diff --git a/apps/server/src/modules/authorization/domain/rules/context-external-tool.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/context-external-tool.rule.spec.ts index c47f5c0ef1e..08adc9f9fef 100644 --- a/apps/server/src/modules/authorization/domain/rules/context-external-tool.rule.spec.ts +++ b/apps/server/src/modules/authorization/domain/rules/context-external-tool.rule.spec.ts @@ -1,21 +1,21 @@ +import { ContextExternalTool } from '@modules/tool/context-external-tool/domain'; +import { ContextExternalToolEntity } from '@modules/tool/context-external-tool/entity'; +import { SchoolExternalTool } from '@modules/tool/school-external-tool/domain'; +import { SchoolExternalToolEntity } from '@modules/tool/school-external-tool/entity'; import { Test, TestingModule } from '@nestjs/testing'; +import { Role, User } from '@shared/domain/entity'; +import { Permission } from '@shared/domain/interface'; import { contextExternalToolEntityFactory, roleFactory, + schoolEntityFactory, schoolExternalToolEntityFactory, - schoolFactory, setupEntities, userFactory, } from '@shared/testing'; -import { ContextExternalTool } from '@modules/tool/context-external-tool/domain'; -import { ContextExternalToolEntity } from '@modules/tool/context-external-tool/entity'; -import { SchoolExternalTool } from '@modules/tool/school-external-tool/domain'; -import { SchoolExternalToolEntity } from '@modules/tool/school-external-tool/entity'; -import { Role, User } from '@shared/domain/entity'; -import { Permission } from '@shared/domain/interface'; -import { ContextExternalToolRule } from './context-external-tool.rule'; -import { Action } from '../type'; import { AuthorizationHelper } from '../service/authorization.helper'; +import { Action } from '../type'; +import { ContextExternalToolRule } from './context-external-tool.rule'; describe('ContextExternalToolRule', () => { let service: ContextExternalToolRule; @@ -41,7 +41,7 @@ describe('ContextExternalToolRule', () => { const role: Role = roleFactory.build({ permissions: [permissionA, permissionB] }); - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const schoolExternalToolEntity: SchoolExternalToolEntity | SchoolExternalTool = schoolExternalToolEntityFactory.build({ school, diff --git a/apps/server/src/modules/authorization/domain/rules/group.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/group.rule.spec.ts index 8229e81ec10..00119fbaad3 100644 --- a/apps/server/src/modules/authorization/domain/rules/group.rule.spec.ts +++ b/apps/server/src/modules/authorization/domain/rules/group.rule.spec.ts @@ -2,7 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { Role, SchoolEntity, User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; -import { groupFactory, roleFactory, schoolFactory, setupEntities, userFactory } from '@shared/testing'; +import { groupFactory, roleFactory, schoolEntityFactory, setupEntities, userFactory } from '@shared/testing'; import { Action, AuthorizationContext, AuthorizationHelper } from '@src/modules/authorization'; import { Group } from '@src/modules/group'; import { ObjectId } from 'bson'; @@ -92,7 +92,7 @@ describe('GroupRule', () => { describe('when the user has all required permissions and is at the same school then the group', () => { const setup = () => { const role: Role = roleFactory.buildWithId(); - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const user: User = userFactory.buildWithId({ school, roles: [role] }); const group: Group = groupFactory.build({ users: [ @@ -137,7 +137,7 @@ describe('GroupRule', () => { describe('when the user has not the required permission', () => { const setup = () => { const role: Role = roleFactory.buildWithId({ permissions: [] }); - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const user: User = userFactory.buildWithId({ school, roles: [role] }); const group: Group = groupFactory.build({ users: [ @@ -174,7 +174,7 @@ describe('GroupRule', () => { describe('when the user is at another school then the group', () => { const setup = () => { const role: Role = roleFactory.buildWithId({ permissions: [] }); - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const user: User = userFactory.buildWithId({ school, roles: [role] }); const group: Group = groupFactory.build({ users: [ diff --git a/apps/server/src/modules/authorization/domain/rules/school-external-tool.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/school-external-tool.rule.spec.ts index 2a29ef23c9f..1b04bb9dbe3 100644 --- a/apps/server/src/modules/authorization/domain/rules/school-external-tool.rule.spec.ts +++ b/apps/server/src/modules/authorization/domain/rules/school-external-tool.rule.spec.ts @@ -5,9 +5,9 @@ import { Role, User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { roleFactory, + schoolEntityFactory, schoolExternalToolEntityFactory, schoolExternalToolFactory, - schoolFactory, setupEntities, userFactory, } from '@shared/testing'; @@ -39,7 +39,7 @@ describe('SchoolExternalToolRule', () => { const role: Role = roleFactory.build({ permissions: [permissionA, permissionB] }); - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const entity: SchoolExternalToolEntity | SchoolExternalTool = schoolExternalToolEntityFactory.build(); entity.school = school; const user: User = userFactory.build({ roles: [role], school }); diff --git a/apps/server/src/modules/authorization/domain/rules/school-system-options.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/school-system-options.rule.spec.ts index 8f35bf0d1ee..0d937c165a2 100644 --- a/apps/server/src/modules/authorization/domain/rules/school-system-options.rule.spec.ts +++ b/apps/server/src/modules/authorization/domain/rules/school-system-options.rule.spec.ts @@ -5,7 +5,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { SchoolEntity, SystemEntity, User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { - schoolFactory, + schoolEntityFactory, schoolSystemOptionsFactory, setupEntities, systemEntityFactory, @@ -90,7 +90,7 @@ describe(SchoolSystemOptionsRule.name, () => { describe('when the user accesses a system at his school with the required permissions', () => { const setup = () => { const systemEntity: SystemEntity = systemEntityFactory.buildWithId(); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [systemEntity], }); const schoolSystemOptions: SchoolSystemOptions = schoolSystemOptionsFactory.build({ @@ -132,7 +132,7 @@ describe(SchoolSystemOptionsRule.name, () => { describe('when the user accesses a system at his school, but does not have the required permissions', () => { const setup = () => { const systemEntity: SystemEntity = systemEntityFactory.buildWithId(); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [systemEntity], }); const schoolSystemOptions: SchoolSystemOptions = schoolSystemOptionsFactory.build({ @@ -163,7 +163,7 @@ describe(SchoolSystemOptionsRule.name, () => { describe('when the system is not part of the users school', () => { const setup = () => { const systemEntity: SystemEntity = systemEntityFactory.buildWithId(); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [systemEntity], }); const schoolSystemOptions: SchoolSystemOptions = schoolSystemOptionsFactory.build({ @@ -195,7 +195,7 @@ describe(SchoolSystemOptionsRule.name, () => { const setup = () => { const schoolSystemOptions: SchoolSystemOptions = schoolSystemOptionsFactory.build(); const systemEntity: SystemEntity = systemEntityFactory.buildWithId(); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [systemEntity], }); const user: User = userFactory.buildWithId({ school }); diff --git a/apps/server/src/modules/authorization/domain/rules/system.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/system.rule.spec.ts index 922ac71b20a..8fb4d0173ba 100644 --- a/apps/server/src/modules/authorization/domain/rules/system.rule.spec.ts +++ b/apps/server/src/modules/authorization/domain/rules/system.rule.spec.ts @@ -3,7 +3,7 @@ import { System } from '@modules/system'; import { Test, TestingModule } from '@nestjs/testing'; import { SchoolEntity, SystemEntity, User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; -import { schoolFactory, setupEntities, systemEntityFactory, systemFactory, userFactory } from '@shared/testing'; +import { schoolEntityFactory, setupEntities, systemEntityFactory, systemFactory, userFactory } from '@shared/testing'; import { AuthorizationContextBuilder } from '../mapper'; import { AuthorizationHelper } from '../service/authorization.helper'; import { SystemRule } from './system.rule'; @@ -84,7 +84,7 @@ describe(SystemRule.name, () => { const setup = () => { const system: System = systemFactory.build(); const systemEntity: SystemEntity = systemEntityFactory.buildWithId(undefined, system.id); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [systemEntity], }); const user: User = userFactory.buildWithId({ school }); @@ -123,7 +123,7 @@ describe(SystemRule.name, () => { const setup = () => { const system: System = systemFactory.build(); const systemEntity: SystemEntity = systemEntityFactory.buildWithId(undefined, system.id); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [systemEntity], }); const user: User = userFactory.buildWithId({ school }); @@ -150,7 +150,7 @@ describe(SystemRule.name, () => { describe('when the user reads a system that is not at his school', () => { const setup = () => { const system: System = systemFactory.build(); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [], }); const user: User = userFactory.buildWithId({ school }); @@ -178,7 +178,7 @@ describe(SystemRule.name, () => { const setup = () => { const system: System = systemFactory.build({ ldapConfig: { provider: 'general' } }); const systemEntity: SystemEntity = systemEntityFactory.buildWithId(undefined, system.id); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [systemEntity], }); const user: User = userFactory.buildWithId({ school }); @@ -206,7 +206,7 @@ describe(SystemRule.name, () => { const setup = () => { const system: System = systemFactory.build({ ldapConfig: { provider: 'other provider' } }); const systemEntity: SystemEntity = systemEntityFactory.buildWithId(undefined, system.id); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [systemEntity], }); const user: User = userFactory.buildWithId({ school }); @@ -234,7 +234,7 @@ describe(SystemRule.name, () => { const setup = () => { const system: System = systemFactory.build({ ldapConfig: undefined }); const systemEntity: SystemEntity = systemEntityFactory.buildWithId(undefined, system.id); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [systemEntity], }); const user: User = userFactory.buildWithId({ school }); diff --git a/apps/server/src/modules/authorization/domain/rules/user-login-migration.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/user-login-migration.rule.spec.ts index f7fe9d3c53f..6ee893fc9d2 100644 --- a/apps/server/src/modules/authorization/domain/rules/user-login-migration.rule.spec.ts +++ b/apps/server/src/modules/authorization/domain/rules/user-login-migration.rule.spec.ts @@ -1,11 +1,11 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; -import { schoolFactory, setupEntities, userFactory, userLoginMigrationDOFactory } from '@shared/testing'; import { UserLoginMigrationDO } from '@shared/domain/domainobject'; import { Permission } from '@shared/domain/interface'; -import { Action, AuthorizationContext } from '../type'; +import { schoolEntityFactory, setupEntities, userFactory, userLoginMigrationDOFactory } from '@shared/testing'; import { AuthorizationHelper } from '../service/authorization.helper'; +import { Action, AuthorizationContext } from '../type'; import { UserLoginMigrationRule } from './user-login-migration.rule'; describe('UserLoginMigrationRule', () => { @@ -82,7 +82,7 @@ describe('UserLoginMigrationRule', () => { const setup = () => { const schoolId = new ObjectId().toHexString(); const user = userFactory.buildWithId({ - school: schoolFactory.buildWithId(undefined, schoolId), + school: schoolEntityFactory.buildWithId(undefined, schoolId), }); const userLoginMigration = userLoginMigrationDOFactory.buildWithId({ schoolId }); const context: AuthorizationContext = { @@ -119,7 +119,7 @@ describe('UserLoginMigrationRule', () => { describe('when the user has all permissions, but is at a different school', () => { const setup = () => { const user = userFactory.buildWithId({ - school: schoolFactory.buildWithId(undefined, new ObjectId().toHexString()), + school: schoolEntityFactory.buildWithId(undefined, new ObjectId().toHexString()), }); const userLoginMigration = userLoginMigrationDOFactory.buildWithId({ schoolId: new ObjectId().toHexString() }); const context: AuthorizationContext = { @@ -149,7 +149,7 @@ describe('UserLoginMigrationRule', () => { const setup = () => { const schoolId = new ObjectId().toHexString(); const user = userFactory.buildWithId({ - school: schoolFactory.buildWithId(undefined, schoolId), + school: schoolEntityFactory.buildWithId(undefined, schoolId), }); const userLoginMigration = userLoginMigrationDOFactory.buildWithId({ schoolId }); const context: AuthorizationContext = { diff --git a/apps/server/src/modules/board/controller/api-test/card-lookup.api.spec.ts b/apps/server/src/modules/board/controller/api-test/card-lookup.api.spec.ts index 6f6412376ee..89f2d5fd816 100644 --- a/apps/server/src/modules/board/controller/api-test/card-lookup.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/card-lookup.api.spec.ts @@ -15,7 +15,7 @@ import { mapUserToCurrentUser, richTextElementNodeFactory, roleFactory, - schoolFactory, + schoolEntityFactory, userFactory, } from '@shared/testing'; import { Request } from 'express'; @@ -77,7 +77,7 @@ describe(`card lookup (api)`, () => { const setup = async () => { await cleanupCollections(em); - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const roles = roleFactory.buildList(1, { // permissions: [Permission.COURSE_CREATE], }); diff --git a/apps/server/src/modules/board/service/column-board-copy.service.spec.ts b/apps/server/src/modules/board/service/column-board-copy.service.spec.ts index 268c65297e7..a772fc33745 100644 --- a/apps/server/src/modules/board/service/column-board-copy.service.spec.ts +++ b/apps/server/src/modules/board/service/column-board-copy.service.spec.ts @@ -11,7 +11,7 @@ import { columnFactory, courseFactory, linkElementFactory, - schoolFactory, + schoolEntityFactory, setupEntities, userFactory, } from '@shared/testing'; @@ -72,8 +72,8 @@ describe('column board copy service', () => { describe('when copying a column board', () => { const setup = () => { - const originalSchool = schoolFactory.buildWithId(); - const targetSchool = schoolFactory.buildWithId(); + const originalSchool = schoolEntityFactory.buildWithId(); + const targetSchool = schoolEntityFactory.buildWithId(); const course = courseFactory.buildWithId({ school: originalSchool }); const originalExternalReference = { id: course.id, diff --git a/apps/server/src/modules/class/repo/classes.repo.spec.ts b/apps/server/src/modules/class/repo/classes.repo.spec.ts index df232d7878a..9904a627e6c 100644 --- a/apps/server/src/modules/class/repo/classes.repo.spec.ts +++ b/apps/server/src/modules/class/repo/classes.repo.spec.ts @@ -5,7 +5,7 @@ import { Test } from '@nestjs/testing'; import { TestingModule } from '@nestjs/testing/testing-module'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { SchoolEntity } from '@shared/domain/entity'; -import { cleanupCollections, schoolFactory } from '@shared/testing'; +import { cleanupCollections, schoolEntityFactory } from '@shared/testing'; import { Class } from '../domain'; import { ClassEntity } from '../entity'; import { ClassesRepo } from './classes.repo'; @@ -45,7 +45,7 @@ describe(ClassesRepo.name, () => { describe('when school has classes', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const classes: ClassEntity[] = classEntityFactory.buildListWithId(3, { schoolId: school.id }); await em.persistAndFlush(classes); diff --git a/apps/server/src/modules/files-storage-client/service/copy-files.service.spec.ts b/apps/server/src/modules/files-storage-client/service/copy-files.service.spec.ts index fbc54e3cf0f..99eb7ddf3f2 100644 --- a/apps/server/src/modules/files-storage-client/service/copy-files.service.spec.ts +++ b/apps/server/src/modules/files-storage-client/service/copy-files.service.spec.ts @@ -6,7 +6,7 @@ import { courseFactory, legacyFileEntityMockFactory, lessonFactory, - schoolFactory, + schoolEntityFactory, setupEntities, } from '@shared/testing'; import { CopyFilesService } from './copy-files.service'; @@ -57,7 +57,7 @@ describe('copy files service', () => { describe('copy files of entity', () => { const setup = () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const file1 = legacyFileEntityMockFactory.build(); const file2 = legacyFileEntityMockFactory.build(); const imageHTML1 = getImageHTML(file1.id, file1.name); diff --git a/apps/server/src/modules/files-storage-client/service/files-storage-client.service.spec.ts b/apps/server/src/modules/files-storage-client/service/files-storage-client.service.spec.ts index e1a7096bc1c..f5a7b267c9a 100644 --- a/apps/server/src/modules/files-storage-client/service/files-storage-client.service.spec.ts +++ b/apps/server/src/modules/files-storage-client/service/files-storage-client.service.spec.ts @@ -1,7 +1,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; -import { schoolFactory, setupEntities, taskFactory } from '@shared/testing'; +import { schoolEntityFactory, setupEntities, taskFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { FileParamBuilder, FilesStorageClientMapper } from '../mapper'; import { CopyFilesOfParentParamBuilder } from '../mapper/copy-files-of-parent-param.builder'; @@ -45,7 +45,7 @@ describe('FilesStorageClientAdapterService', () => { describe('copyFilesOfParent', () => { it('Should call all steps.', async () => { const userId = new ObjectId().toHexString(); - const school = schoolFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); const sourceEntity = taskFactory.buildWithId({ school }); const targetEntity = taskFactory.buildWithId({ school }); @@ -69,7 +69,7 @@ describe('FilesStorageClientAdapterService', () => { it('Should call error mapper if throw an error.', async () => { const userId = new ObjectId().toHexString(); - const school = schoolFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); const sourceEntity = taskFactory.buildWithId({ school }); const targetEntity = taskFactory.buildWithId({ school }); diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-security.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-security.api.spec.ts index 38d33bb4168..3de38711ad4 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-security.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-security.api.spec.ts @@ -11,7 +11,7 @@ import { fileRecordFactory, mapUserToCurrentUser, roleFactory, - schoolFactory, + schoolEntityFactory, userFactory, } from '@shared/testing'; import NodeClam from 'clamscan'; @@ -80,7 +80,7 @@ describe(`${baseRouteName} (api)`, () => { beforeEach(async () => { await cleanupCollections(em); - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const roles = roleFactory.buildList(1, { permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW], }); diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-copy-files.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-copy-files.api.spec.ts index 6f57f89d27e..810765c99d7 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-copy-files.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-copy-files.api.spec.ts @@ -15,7 +15,7 @@ import { fileRecordFactory, mapUserToCurrentUser, roleFactory, - schoolFactory, + schoolEntityFactory, userFactory, } from '@shared/testing'; import NodeClam from 'clamscan'; @@ -127,7 +127,7 @@ describe(`${baseRouteName} (api)`, () => { describe('with bad request data', () => { beforeEach(async () => { await cleanupCollections(em); - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const roles = roleFactory.buildList(1, { permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW], }); @@ -199,7 +199,7 @@ describe(`${baseRouteName} (api)`, () => { describe(`with valid request data`, () => { beforeEach(async () => { await cleanupCollections(em); - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const roles = roleFactory.buildList(1, { permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW], }); @@ -254,7 +254,7 @@ describe(`${baseRouteName} (api)`, () => { describe('with bad request data', () => { beforeEach(async () => { await cleanupCollections(em); - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const roles = roleFactory.buildList(1, { permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW], }); @@ -295,7 +295,7 @@ describe(`${baseRouteName} (api)`, () => { beforeEach(async () => { await cleanupCollections(em); - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const roles = roleFactory.buildList(1, { permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW], }); diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-delete-files.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-delete-files.api.spec.ts index 97fed8c660e..bbdaabeae23 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-delete-files.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-delete-files.api.spec.ts @@ -15,7 +15,7 @@ import { fileRecordFactory, mapUserToCurrentUser, roleFactory, - schoolFactory, + schoolEntityFactory, userFactory, } from '@shared/testing'; import NodeClam from 'clamscan'; @@ -125,7 +125,7 @@ describe(`${baseRouteName} (api)`, () => { beforeEach(async () => { await cleanupCollections(em); - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const roles = roleFactory.buildList(1, { permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW, Permission.FILESTORAGE_REMOVE], }); @@ -177,7 +177,7 @@ describe(`${baseRouteName} (api)`, () => { beforeEach(async () => { await cleanupCollections(em); - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const roles = roleFactory.buildList(1, { permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW, Permission.FILESTORAGE_REMOVE], }); @@ -250,7 +250,7 @@ describe(`${baseRouteName} (api)`, () => { describe('with bad request data', () => { beforeEach(async () => { await cleanupCollections(em); - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const roles = roleFactory.buildList(1, { permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW, Permission.FILESTORAGE_REMOVE], }); @@ -277,7 +277,7 @@ describe(`${baseRouteName} (api)`, () => { beforeEach(async () => { await cleanupCollections(em); - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const roles = roleFactory.buildList(1, { permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW, Permission.FILESTORAGE_REMOVE], }); diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-download-upload.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-download-upload.api.spec.ts index 14843ad75e0..a3027ee5d34 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-download-upload.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-download-upload.api.spec.ts @@ -9,7 +9,13 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; -import { cleanupCollections, mapUserToCurrentUser, roleFactory, schoolFactory, userFactory } from '@shared/testing'; +import { + cleanupCollections, + mapUserToCurrentUser, + roleFactory, + schoolEntityFactory, + userFactory, +} from '@shared/testing'; import NodeClam from 'clamscan'; import { Request } from 'express'; import FileType from 'file-type-cjs/file-type-cjs-index'; @@ -138,7 +144,7 @@ describe('files-storage controller (API)', () => { beforeEach(async () => { jest.resetAllMocks(); await cleanupCollections(em); - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const roles = roleFactory.buildList(1, { permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW], }); diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-list-files.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-list-files.api.spec.ts index df220828567..b0549420d82 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-list-files.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-list-files.api.spec.ts @@ -13,7 +13,7 @@ import { fileRecordFactory, mapUserToCurrentUser, roleFactory, - schoolFactory, + schoolEntityFactory, userFactory, } from '@shared/testing'; import NodeClam from 'clamscan'; @@ -83,7 +83,7 @@ describe(`${baseRouteName} (api)`, () => { describe('with bad request data', () => { beforeEach(async () => { await cleanupCollections(em); - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const roles = roleFactory.buildList(1, { permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW], }); @@ -133,7 +133,7 @@ describe(`${baseRouteName} (api)`, () => { describe(`with valid request data`, () => { beforeEach(async () => { await cleanupCollections(em); - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const roles = roleFactory.buildList(1, { permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW], }); diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-preview.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-preview.api.spec.ts index f218cf3e6e8..1274bd99f35 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-preview.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-preview.api.spec.ts @@ -10,7 +10,13 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; -import { cleanupCollections, mapUserToCurrentUser, roleFactory, schoolFactory, userFactory } from '@shared/testing'; +import { + cleanupCollections, + mapUserToCurrentUser, + roleFactory, + schoolEntityFactory, + userFactory, +} from '@shared/testing'; import NodeClam from 'clamscan'; import { Request } from 'express'; import FileType from 'file-type-cjs/file-type-cjs-index'; @@ -153,7 +159,7 @@ describe('File Controller (API) - preview', () => { beforeEach(async () => { jest.resetAllMocks(); await cleanupCollections(em); - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const roles = roleFactory.buildList(1, { permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW], }); diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-rename-file.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-rename-file.api.spec.ts index d1d814437cf..2933a53822e 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-rename-file.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-rename-file.api.spec.ts @@ -11,7 +11,7 @@ import { fileRecordFactory, mapUserToCurrentUser, roleFactory, - schoolFactory, + schoolEntityFactory, userFactory, } from '@shared/testing'; import NodeClam from 'clamscan'; @@ -80,7 +80,7 @@ describe(`${baseRouteName} (api)`, () => { beforeEach(async () => { await cleanupCollections(em); - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const roles = roleFactory.buildList(1, { permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW, Permission.FILESTORAGE_EDIT], }); diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-restore-files.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-restore-files.api.spec.ts index 04241fdb4a5..90b60a4a16d 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-restore-files.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-restore-files.api.spec.ts @@ -15,7 +15,7 @@ import { fileRecordFactory, mapUserToCurrentUser, roleFactory, - schoolFactory, + schoolEntityFactory, userFactory, } from '@shared/testing'; import NodeClam from 'clamscan'; @@ -151,7 +151,7 @@ describe(`${baseRouteName} (api)`, () => { beforeEach(async () => { await cleanupCollections(em); const roles = roleFactory.buildList(1, { permissions: [] }); - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const user = userFactory.build({ roles, school }); await em.persistAndFlush([user]); @@ -200,7 +200,7 @@ describe(`${baseRouteName} (api)`, () => { beforeEach(async () => { await cleanupCollections(em); - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const roles = roleFactory.buildList(1, { permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW, Permission.FILESTORAGE_REMOVE], }); @@ -276,7 +276,7 @@ describe(`${baseRouteName} (api)`, () => { beforeEach(async () => { await cleanupCollections(em); const roles = roleFactory.buildList(1, { permissions: [] }); - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const user = userFactory.build({ roles, school }); await em.persistAndFlush([user]); @@ -300,7 +300,7 @@ describe(`${baseRouteName} (api)`, () => { beforeEach(async () => { await cleanupCollections(em); - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const roles = roleFactory.buildList(1, { permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW, Permission.FILESTORAGE_REMOVE], }); diff --git a/apps/server/src/modules/group/controller/api-test/group.api.spec.ts b/apps/server/src/modules/group/controller/api-test/group.api.spec.ts index 592580b47c6..1722747cdcf 100644 --- a/apps/server/src/modules/group/controller/api-test/group.api.spec.ts +++ b/apps/server/src/modules/group/controller/api-test/group.api.spec.ts @@ -9,7 +9,7 @@ import { RoleName, SortOrder } from '@shared/domain/interface'; import { groupEntityFactory, roleFactory, - schoolFactory, + schoolEntityFactory, schoolYearFactory, systemEntityFactory, TestApiClient, @@ -48,7 +48,7 @@ describe('Group (API)', () => { describe('when an admin requests a list of classes', () => { const setup = async () => { const schoolYear: SchoolYearEntity = schoolYearFactory.buildWithId(); - const school: SchoolEntity = schoolFactory.buildWithId({ currentYear: schoolYear }); + const school: SchoolEntity = schoolEntityFactory.buildWithId({ currentYear: schoolYear }); const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }); const teacherRole: Role = roleFactory.buildWithId({ name: RoleName.TEACHER }); @@ -145,7 +145,7 @@ describe('Group (API)', () => { describe('when authorized user requests a group', () => { describe('when group exists', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }); const group: GroupEntity = groupEntityFactory.buildWithId({ diff --git a/apps/server/src/modules/group/repo/group.repo.spec.ts b/apps/server/src/modules/group/repo/group.repo.spec.ts index e127155fd25..fbee2bd6020 100644 --- a/apps/server/src/modules/group/repo/group.repo.spec.ts +++ b/apps/server/src/modules/group/repo/group.repo.spec.ts @@ -8,7 +8,7 @@ import { groupEntityFactory, groupFactory, roleFactory, - schoolFactory, + schoolEntityFactory, systemEntityFactory, userDoFactory, userFactory, @@ -181,7 +181,7 @@ describe('GroupRepo', () => { describe('findBySchoolIdAndGroupTypes', () => { describe('when groups for the school exist', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const groups: GroupEntity[] = groupEntityFactory.buildListWithId(3, { type: GroupEntityTypes.CLASS, organization: school, @@ -189,7 +189,7 @@ describe('GroupRepo', () => { groups[1].type = GroupEntityTypes.COURSE; groups[2].type = GroupEntityTypes.OTHER; - const otherSchool: SchoolEntity = schoolFactory.buildWithId(); + const otherSchool: SchoolEntity = schoolEntityFactory.buildWithId(); const otherGroups: GroupEntity[] = groupEntityFactory.buildListWithId(2, { type: GroupEntityTypes.CLASS, organization: otherSchool, @@ -249,7 +249,7 @@ describe('GroupRepo', () => { describe('when no group exists', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); await em.persistAndFlush(school); em.clear(); @@ -273,7 +273,7 @@ describe('GroupRepo', () => { describe('when groups for the school exist', () => { const setup = async () => { const system: SystemEntity = systemEntityFactory.buildWithId(); - const school: SchoolEntity = schoolFactory.buildWithId({ systems: [system] }); + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [system] }); const groups: GroupEntity[] = groupEntityFactory.buildListWithId(3, { type: GroupEntityTypes.CLASS, organization: school, @@ -284,7 +284,7 @@ describe('GroupRepo', () => { groups[1].type = GroupEntityTypes.COURSE; groups[2].type = GroupEntityTypes.OTHER; - const otherSchool: SchoolEntity = schoolFactory.buildWithId({ systems: [system] }); + const otherSchool: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [system] }); const otherGroups: GroupEntity[] = groupEntityFactory.buildListWithId(2, { type: GroupEntityTypes.CLASS, organization: otherSchool, @@ -352,7 +352,7 @@ describe('GroupRepo', () => { describe('when no group exists', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const system: SystemEntity = systemEntityFactory.buildWithId(); await em.persistAndFlush([school, system]); diff --git a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-delete.api.spec.ts b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-delete.api.spec.ts index 276e86236ec..a45e05fc981 100644 --- a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-delete.api.spec.ts +++ b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-delete.api.spec.ts @@ -4,7 +4,13 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { Permission } from '@shared/domain/interface'; -import { cleanupCollections, mapUserToCurrentUser, roleFactory, schoolFactory, userFactory } from '@shared/testing'; +import { + cleanupCollections, + mapUserToCurrentUser, + roleFactory, + schoolEntityFactory, + userFactory, +} from '@shared/testing'; import { ICurrentUser } from '@src/modules/authentication'; import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; import { Request } from 'express'; @@ -73,7 +79,7 @@ describe('H5PEditor Controller (api)', () => { describe('delete h5p content', () => { beforeEach(async () => { await cleanupCollections(em); - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const roles = roleFactory.buildList(1, { permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW], }); diff --git a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-editor.api.spec.ts b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-editor.api.spec.ts index a2144cc37ba..c1212567e13 100644 --- a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-editor.api.spec.ts +++ b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-editor.api.spec.ts @@ -4,7 +4,13 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { Permission } from '@shared/domain/interface'; -import { cleanupCollections, mapUserToCurrentUser, roleFactory, schoolFactory, userFactory } from '@shared/testing'; +import { + cleanupCollections, + mapUserToCurrentUser, + roleFactory, + schoolEntityFactory, + userFactory, +} from '@shared/testing'; import { ICurrentUser } from '@src/modules/authentication'; import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; import { Request } from 'express'; @@ -91,7 +97,7 @@ describe('H5PEditor Controller (api)', () => { describe('get new h5p editor', () => { beforeEach(async () => { await cleanupCollections(em); - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const roles = roleFactory.buildList(1, { permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW], }); @@ -123,7 +129,7 @@ describe('H5PEditor Controller (api)', () => { describe('get h5p editor', () => { beforeEach(async () => { await cleanupCollections(em); - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const roles = roleFactory.buildList(1, { permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW], }); diff --git a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-player.api.spec.ts b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-player.api.spec.ts index bf728d2dd90..ca3e04bf226 100644 --- a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-player.api.spec.ts +++ b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor-get-player.api.spec.ts @@ -5,7 +5,13 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { Permission } from '@shared/domain/interface'; -import { cleanupCollections, mapUserToCurrentUser, roleFactory, schoolFactory, userFactory } from '@shared/testing'; +import { + cleanupCollections, + mapUserToCurrentUser, + roleFactory, + schoolEntityFactory, + userFactory, +} from '@shared/testing'; import { ICurrentUser } from '@src/modules/authentication'; import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; import { Request } from 'express'; @@ -83,7 +89,7 @@ describe('H5PEditor Controller (api)', () => { describe('get h5p player', () => { beforeEach(async () => { await cleanupCollections(em); - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const roles = roleFactory.buildList(1, { permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW], }); diff --git a/apps/server/src/modules/learnroom/service/course-copy.service.spec.ts b/apps/server/src/modules/learnroom/service/course-copy.service.spec.ts index 5a5dab7c5ac..1a96784a33a 100644 --- a/apps/server/src/modules/learnroom/service/course-copy.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/course-copy.service.spec.ts @@ -4,6 +4,7 @@ import { LessonCopyService } from '@modules/lesson/service'; import { ToolContextType } from '@modules/tool/common/enum'; import { ContextExternalTool } from '@modules/tool/context-external-tool/domain'; import { ContextExternalToolService } from '@modules/tool/context-external-tool/service'; +import { ToolFeatures } from '@modules/tool/tool-config'; import { Test, TestingModule } from '@nestjs/testing'; import { Course } from '@shared/domain/entity'; import { BoardRepo, CourseRepo, UserRepo } from '@shared/repo'; @@ -12,12 +13,11 @@ import { contextExternalToolFactory, courseFactory, courseGroupFactory, - schoolFactory, + schoolEntityFactory, setupEntities, userFactory, } from '@shared/testing'; import { IToolFeatures } from '@src/modules/tool/tool-config'; -import { ToolFeatures } from '@modules/tool/tool-config'; import { BoardCopyService } from './board-copy.service'; import { CourseCopyService } from './course-copy.service'; import { RoomsService } from './rooms.service'; @@ -292,7 +292,7 @@ describe('course copy service', () => { it('should set school of user', async () => { const { course } = setup(); - const destinationSchool = schoolFactory.buildWithId(); + const destinationSchool = schoolEntityFactory.buildWithId(); const targetUser = userFactory.build({ school: destinationSchool }); userRepo.findById.mockResolvedValue(targetUser); @@ -433,7 +433,7 @@ describe('course copy service', () => { describe('copy course entity', () => { it('should assign user as teacher', async () => { const { course } = setup(); - const destinationSchool = schoolFactory.buildWithId(); + const destinationSchool = schoolEntityFactory.buildWithId(); const targetUser = userFactory.build({ school: destinationSchool }); userRepo.findById.mockResolvedValue(targetUser); const status = await service.copyCourse({ userId: targetUser.id, courseId: course.id }); @@ -444,7 +444,7 @@ describe('course copy service', () => { it('should set school of user', async () => { const { course } = setup(); - const destinationSchool = schoolFactory.buildWithId(); + const destinationSchool = schoolEntityFactory.buildWithId(); const targetUser = userFactory.build({ school: destinationSchool }); userRepo.findById.mockResolvedValue(targetUser); const status = await service.copyCourse({ userId: targetUser.id, courseId: course.id }); diff --git a/apps/server/src/modules/legacy-school/controller/api-test/school.api.spec.ts b/apps/server/src/modules/legacy-school/controller/api-test/school.api.spec.ts index c023b94aa0f..a1619a6cfe4 100644 --- a/apps/server/src/modules/legacy-school/controller/api-test/school.api.spec.ts +++ b/apps/server/src/modules/legacy-school/controller/api-test/school.api.spec.ts @@ -5,7 +5,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { SchoolEntity, SystemEntity } from '@shared/domain/entity'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { - schoolFactory, + schoolEntityFactory, schoolSystemOptionsEntityFactory, systemEntityFactory, TestApiClient, @@ -42,7 +42,7 @@ describe('School (API)', () => { const system: SystemEntity = systemEntityFactory.buildWithId({ provisioningStrategy: SystemProvisioningStrategy.SANIS, }); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [system], }); const schoolSystemOptions: SchoolSystemOptionsEntity = schoolSystemOptionsEntityFactory.buildWithId({ @@ -87,7 +87,7 @@ describe('School (API)', () => { const system: SystemEntity = systemEntityFactory.buildWithId({ provisioningStrategy: SystemProvisioningStrategy.SANIS, }); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [system], }); const schoolSystemOptions: SchoolSystemOptionsEntity = schoolSystemOptionsEntityFactory.buildWithId({ @@ -131,7 +131,7 @@ describe('School (API)', () => { const system: SystemEntity = systemEntityFactory.buildWithId({ provisioningStrategy: SystemProvisioningStrategy.SANIS, }); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [system], }); const schoolSystemOptions: SchoolSystemOptionsEntity = schoolSystemOptionsEntityFactory.buildWithId({ @@ -201,7 +201,7 @@ describe('School (API)', () => { const system: SystemEntity = systemEntityFactory.buildWithId({ provisioningStrategy: SystemProvisioningStrategy.SANIS, }); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [system], }); diff --git a/apps/server/src/modules/legacy-school/repo/school-system-options.repo.spec.ts b/apps/server/src/modules/legacy-school/repo/school-system-options.repo.spec.ts index 602448b022d..22116d1c056 100644 --- a/apps/server/src/modules/legacy-school/repo/school-system-options.repo.spec.ts +++ b/apps/server/src/modules/legacy-school/repo/school-system-options.repo.spec.ts @@ -4,7 +4,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { SchoolEntity, SystemEntity } from '@shared/domain/entity'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { - schoolFactory, + schoolEntityFactory, schoolSystemOptionsEntityFactory, schoolSystemOptionsFactory, systemEntityFactory, @@ -123,7 +123,7 @@ describe(SchoolSystemOptionsRepo.name, () => { const systemEntity: SystemEntity = systemEntityFactory.buildWithId({ provisioningStrategy: SystemProvisioningStrategy.SANIS, }); - const schoolEntity: SchoolEntity = schoolFactory.buildWithId({ systems: [systemEntity] }); + const schoolEntity: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [systemEntity] }); const schoolSystemOptions: SchoolSystemOptions = schoolSystemOptionsFactory.build({ systemId: systemEntity.id, @@ -169,7 +169,7 @@ describe(SchoolSystemOptionsRepo.name, () => { const systemEntity: SystemEntity = systemEntityFactory.buildWithId({ provisioningStrategy: SystemProvisioningStrategy.SANIS, }); - const schoolEntity: SchoolEntity = schoolFactory.buildWithId({ systems: [systemEntity] }); + const schoolEntity: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [systemEntity] }); const schoolSystemOptionsEntity: SchoolSystemOptionsEntity = schoolSystemOptionsEntityFactory.buildWithId({ school: schoolEntity, system: systemEntity, @@ -238,7 +238,7 @@ describe(SchoolSystemOptionsRepo.name, () => { const systemEntity: SystemEntity = systemEntityFactory.buildWithId({ provisioningStrategy: undefined, }); - const schoolEntity: SchoolEntity = schoolFactory.buildWithId({ systems: [systemEntity] }); + const schoolEntity: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [systemEntity] }); const schoolSystemOptions: SchoolSystemOptions = schoolSystemOptionsFactory.build({ systemId: systemEntity.id, diff --git a/apps/server/src/modules/management/seed-data/schools.ts b/apps/server/src/modules/management/seed-data/schools.ts index b1cf893366a..4af89e740a5 100644 --- a/apps/server/src/modules/management/seed-data/schools.ts +++ b/apps/server/src/modules/management/seed-data/schools.ts @@ -7,7 +7,7 @@ import { SystemEntity, } from '@shared/domain/entity'; import { SchoolFeature, SchoolPurpose } from '@shared/domain/types'; -import { federalStateFactory, schoolFactory } from '@shared/testing'; +import { federalStateFactory, schoolEntityFactory } from '@shared/testing'; import { FileStorageType } from '@src/modules/school/domain/type/file-storage-type.enum'; import { ObjectId } from 'bson'; import { DeepPartial } from 'fishery'; @@ -303,7 +303,7 @@ export function generateSchools(entities: { systems, federalState, }; - const schoolEntity = schoolFactory.buildWithId(params, partial.id); + const schoolEntity = schoolEntityFactory.buildWithId(params, partial.id); schoolEntity.permissions = partial.permissions; diff --git a/apps/server/src/modules/news/mapper/news.mapper.spec.ts b/apps/server/src/modules/news/mapper/news.mapper.spec.ts index 4ffa8228045..de81fae7dcd 100644 --- a/apps/server/src/modules/news/mapper/news.mapper.spec.ts +++ b/apps/server/src/modules/news/mapper/news.mapper.spec.ts @@ -10,7 +10,7 @@ import { User, } from '@shared/domain/entity'; import { CreateNews, INewsScope, IUpdateNews, NewsTarget, NewsTargetModel } from '@shared/domain/types'; -import { courseFactory, schoolFactory, setupEntities, userFactory } from '@shared/testing'; +import { courseFactory, schoolEntityFactory, setupEntities, userFactory } from '@shared/testing'; import { CreateNewsParams, FilterNewsParams, @@ -117,7 +117,7 @@ describe('NewsMapper', () => { describe('mapToResponse', () => { it('should correctly map school news to Dto', () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const creator = userFactory.build(); const newsProps = { title: 'test title', content: 'test content' }; const schoolNews = createNews(newsProps, SchoolNews, school, creator, school); @@ -127,7 +127,7 @@ describe('NewsMapper', () => { expect(result).toStrictEqual(expected); }); it('should correctly map course news to dto', () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const creator = userFactory.build(); const course = courseFactory.build({ school }); const newsProps = { title: 'test title', content: 'test content' }; @@ -139,7 +139,7 @@ describe('NewsMapper', () => { expect(result).toStrictEqual(expected); }); it('should correctly map team news to dto', () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const team = new TeamEntity({ name: 'team #1' }); const creator = userFactory.build(); const newsProps = { title: 'test title', content: 'test content' }; diff --git a/apps/server/src/modules/oauth/oauth.module.ts b/apps/server/src/modules/oauth/oauth.module.ts index 3898a2e8547..d9dc4927a49 100644 --- a/apps/server/src/modules/oauth/oauth.module.ts +++ b/apps/server/src/modules/oauth/oauth.module.ts @@ -10,9 +10,7 @@ import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { LtiToolRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; -import { HydraSsoService } from './service/hydra.service'; -import { OauthAdapterService } from './service/oauth-adapter.service'; -import { OAuthService } from './service/oauth.service'; +import { HydraSsoService, OauthAdapterService, OAuthService } from './service'; @Module({ imports: [ @@ -28,6 +26,6 @@ import { OAuthService } from './service/oauth.service'; LegacySchoolModule, ], providers: [OAuthService, OauthAdapterService, HydraSsoService, LtiToolRepo], - exports: [OAuthService, HydraSsoService], + exports: [OAuthService, HydraSsoService, OauthAdapterService], }) export class OauthModule {} diff --git a/apps/server/src/modules/oauth/service/dto/client-credentials-grant-token-request.ts b/apps/server/src/modules/oauth/service/dto/client-credentials-grant-token-request.ts new file mode 100644 index 00000000000..5963a0f4af9 --- /dev/null +++ b/apps/server/src/modules/oauth/service/dto/client-credentials-grant-token-request.ts @@ -0,0 +1,18 @@ +import { OAuthGrantType } from '../../interface/oauth-grant-type.enum'; + +export class ClientCredentialsGrantTokenRequest { + client_id: string; + + client_secret: string; + + scope?: string; + + grant_type: OAuthGrantType.CLIENT_CREDENTIALS_GRANT; + + constructor(props: ClientCredentialsGrantTokenRequest) { + this.client_id = props.client_id; + this.client_secret = props.client_secret; + this.scope = props.scope; + this.grant_type = props.grant_type; + } +} diff --git a/apps/server/src/modules/oauth/service/dto/index.ts b/apps/server/src/modules/oauth/service/dto/index.ts index dbe924b094c..fa158fe08bc 100644 --- a/apps/server/src/modules/oauth/service/dto/index.ts +++ b/apps/server/src/modules/oauth/service/dto/index.ts @@ -3,3 +3,4 @@ export * from './oauth-token.response'; export * from './oauth-process.dto'; export * from './cookies.dto'; export * from './hydra.redirect.dto'; +export { ClientCredentialsGrantTokenRequest } from './client-credentials-grant-token-request'; diff --git a/apps/server/src/modules/oauth/service/oauth-adapter.service.spec.ts b/apps/server/src/modules/oauth/service/oauth-adapter.service.spec.ts index af03a6fdda2..63319b859d4 100644 --- a/apps/server/src/modules/oauth/service/oauth-adapter.service.spec.ts +++ b/apps/server/src/modules/oauth/service/oauth-adapter.service.spec.ts @@ -5,6 +5,7 @@ import { axiosResponseFactory } from '@shared/testing'; import { axiosErrorFactory } from '@shared/testing/factory'; import { AxiosError } from 'axios'; import { of, throwError } from 'rxjs'; +import { OAuthTokenDto } from '../interface'; import { OAuthGrantType } from '../interface/oauth-grant-type.enum'; import { TokenRequestLoggableException } from '../loggable'; import { AuthenticationCodeGrantTokenRequest, OauthTokenResponse } from './dto'; @@ -65,7 +66,7 @@ describe('OauthAdapterServive', () => { }); }); - describe('sendRequestToken', () => { + describe('sendTokenRequest', () => { const tokenResponse: OauthTokenResponse = { access_token: 'accessToken', refresh_token: 'refreshToken', @@ -85,12 +86,13 @@ describe('OauthAdapterServive', () => { describe('when it requests a token', () => { it('should get token from the external server', async () => { - const responseToken: OauthTokenResponse = await service.sendAuthenticationCodeTokenRequest( - 'tokenEndpoint', - testPayload - ); + const responseToken: OAuthTokenDto = await service.sendTokenRequest('tokenEndpoint', testPayload); - expect(responseToken).toStrictEqual(tokenResponse); + expect(responseToken).toEqual({ + idToken: tokenResponse.id_token, + accessToken: tokenResponse.access_token, + refreshToken: tokenResponse.refresh_token, + }); }); }); @@ -107,7 +109,7 @@ describe('OauthAdapterServive', () => { it('should throw an error', async () => { const { error } = setup(); - const resp = service.sendAuthenticationCodeTokenRequest('tokenEndpoint', testPayload); + const resp = service.sendTokenRequest('tokenEndpoint', testPayload); await expect(resp).rejects.toEqual(error); }); @@ -127,7 +129,7 @@ describe('OauthAdapterServive', () => { it('should throw the default sso error', async () => { const { error } = setup(); - const resp = service.sendAuthenticationCodeTokenRequest('tokenEndpoint', testPayload); + const resp = service.sendTokenRequest('tokenEndpoint', testPayload); await expect(resp).rejects.toEqual(error); }); @@ -150,7 +152,7 @@ describe('OauthAdapterServive', () => { it('should throw an error', async () => { const { axiosError } = setup(); - const resp = service.sendAuthenticationCodeTokenRequest('tokenEndpoint', testPayload); + const resp = service.sendTokenRequest('tokenEndpoint', testPayload); await expect(resp).rejects.toEqual(new TokenRequestLoggableException(axiosError)); }); diff --git a/apps/server/src/modules/oauth/service/oauth-adapter.service.ts b/apps/server/src/modules/oauth/service/oauth-adapter.service.ts index 4ab048b84c4..177aa98eed8 100644 --- a/apps/server/src/modules/oauth/service/oauth-adapter.service.ts +++ b/apps/server/src/modules/oauth/service/oauth-adapter.service.ts @@ -4,8 +4,10 @@ import { AxiosResponse, isAxiosError } from 'axios'; import JwksRsa from 'jwks-rsa'; import QueryString from 'qs'; import { lastValueFrom, Observable } from 'rxjs'; +import { OAuthTokenDto } from '../interface'; import { TokenRequestLoggableException } from '../loggable'; -import { AuthenticationCodeGrantTokenRequest, OauthTokenResponse } from './dto'; +import { TokenRequestMapper } from '../mapper/token-request.mapper'; +import { AuthenticationCodeGrantTokenRequest, ClientCredentialsGrantTokenRequest, OauthTokenResponse } from './dto'; @Injectable() export class OauthAdapterService { @@ -16,28 +18,34 @@ export class OauthAdapterService { cache: true, jwksUri, }); + const key: JwksRsa.SigningKey = await client.getSigningKey(); + return key.getPublicKey(); } - public sendAuthenticationCodeTokenRequest( + public async sendTokenRequest( tokenEndpoint: string, - payload: AuthenticationCodeGrantTokenRequest - ): Promise { + payload: AuthenticationCodeGrantTokenRequest | ClientCredentialsGrantTokenRequest + ): Promise { const urlEncodedPayload: string = QueryString.stringify(payload); + const responseTokenObservable = this.httpService.post(tokenEndpoint, urlEncodedPayload, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, }); - const responseData: Promise = this.resolveTokenRequest(responseTokenObservable); - return responseData; + + const tokenDto: OAuthTokenDto = await this.resolveTokenRequest(responseTokenObservable); + + return tokenDto; } private async resolveTokenRequest( observable: Observable> - ): Promise { + ): Promise { let responseToken: AxiosResponse; + try { responseToken = await lastValueFrom(observable); } catch (error: unknown) { @@ -47,6 +55,8 @@ export class OauthAdapterService { throw error; } - return responseToken.data; + const tokenDto: OAuthTokenDto = TokenRequestMapper.mapTokenResponseToDto(responseToken.data); + + return tokenDto; } } diff --git a/apps/server/src/modules/oauth/service/oauth.service.spec.ts b/apps/server/src/modules/oauth/service/oauth.service.spec.ts index 4f65b85005e..d472ec5afcd 100644 --- a/apps/server/src/modules/oauth/service/oauth.service.spec.ts +++ b/apps/server/src/modules/oauth/service/oauth.service.spec.ts @@ -25,7 +25,6 @@ import { OauthConfigMissingLoggableException, UserNotFoundAfterProvisioningLoggableException, } from '../loggable'; -import { OauthTokenResponse } from './dto'; import { OauthAdapterService } from './oauth-adapter.service'; import { OAuthService } from './oauth.service'; @@ -138,34 +137,34 @@ describe('OAuthService', () => { describe('requestToken', () => { const setupRequest = () => { const code = '43534543jnj543342jn2'; - const tokenResponse: OauthTokenResponse = { - access_token: 'accessToken', - refresh_token: 'refreshToken', - id_token: 'idToken', + const oauthToken: OAuthTokenDto = { + accessToken: 'accessToken', + idToken: 'idToken', + refreshToken: 'refreshToken', }; return { code, - tokenResponse, + oauthToken, }; }; beforeEach(() => { - const { tokenResponse } = setupRequest(); + const { oauthToken } = setupRequest(); oAuthEncryptionService.decrypt.mockReturnValue('decryptedSecret'); - oauthAdapterService.sendAuthenticationCodeTokenRequest.mockResolvedValue(tokenResponse); + oauthAdapterService.sendTokenRequest.mockResolvedValue(oauthToken); }); describe('when it requests a token', () => { it('should get token from the external server', async () => { - const { code, tokenResponse } = setupRequest(); + const { code, oauthToken } = setupRequest(); const result: OAuthTokenDto = await service.requestToken(code, testOauthConfig, 'redirectUri'); expect(result).toEqual({ - idToken: tokenResponse.id_token, - accessToken: tokenResponse.access_token, - refreshToken: tokenResponse.refresh_token, + idToken: oauthToken.idToken, + accessToken: oauthToken.accessToken, + refreshToken: oauthToken.refreshToken, }); }); }); @@ -222,34 +221,34 @@ describe('OAuthService', () => { oauthConfig, }); - const oauthTokenResponse: OauthTokenResponse = { - access_token: 'accessToken', - refresh_token: 'refreshToken', - id_token: 'idToken', + const oauthToken: OAuthTokenDto = { + accessToken: 'accessToken', + idToken: 'idToken', + refreshToken: 'refreshToken', }; return { authCode, system, - oauthTokenResponse, + oauthToken, oauthConfig, }; }; describe('when system does not have oauth config', () => { it('should authenticate a user', async () => { - const { authCode, system, oauthTokenResponse } = setup(); + const { authCode, system, oauthToken } = setup(); systemService.findById.mockResolvedValue(testSystem); oAuthEncryptionService.decrypt.mockReturnValue('decryptedSecret'); oauthAdapterService.getPublicKey.mockResolvedValue('publicKey'); - oauthAdapterService.sendAuthenticationCodeTokenRequest.mockResolvedValue(oauthTokenResponse); + oauthAdapterService.sendTokenRequest.mockResolvedValue(oauthToken); const result: OAuthTokenDto = await service.authenticateUser(system.id!, 'redirectUri', authCode); expect(result).toEqual({ - accessToken: oauthTokenResponse.access_token, - idToken: oauthTokenResponse.id_token, - refreshToken: oauthTokenResponse.refresh_token, + accessToken: oauthToken.accessToken, + idToken: oauthToken.idToken, + refreshToken: oauthToken.refreshToken, }); }); }); diff --git a/apps/server/src/modules/oauth/service/oauth.service.ts b/apps/server/src/modules/oauth/service/oauth.service.ts index 52a8f6c07dd..e9e184e49e5 100644 --- a/apps/server/src/modules/oauth/service/oauth.service.ts +++ b/apps/server/src/modules/oauth/service/oauth.service.ts @@ -1,6 +1,7 @@ import { DefaultEncryptionService, EncryptionService } from '@infra/encryption'; import { LegacySchoolService } from '@modules/legacy-school'; -import { OauthDataDto, ProvisioningService } from '@modules/provisioning'; +import { OauthDataDto } from '@modules/provisioning/dto/oauth-data.dto'; +import { ProvisioningService } from '@modules/provisioning/service/provisioning.service'; import { LegacySystemService } from '@modules/system'; import { SystemDto } from '@modules/system/service'; import { UserService } from '@modules/user'; @@ -20,7 +21,7 @@ import { UserNotFoundAfterProvisioningLoggableException, } from '../loggable'; import { TokenRequestMapper } from '../mapper/token-request.mapper'; -import { AuthenticationCodeGrantTokenRequest, OauthTokenResponse } from './dto'; +import { AuthenticationCodeGrantTokenRequest } from './dto'; import { OauthAdapterService } from './oauth-adapter.service'; @Injectable() @@ -125,12 +126,8 @@ export class OAuthService { async requestToken(code: string, oauthConfig: OauthConfigEntity, redirectUri: string): Promise { const payload: AuthenticationCodeGrantTokenRequest = this.buildTokenRequestPayload(code, oauthConfig, redirectUri); - const responseToken: OauthTokenResponse = await this.oauthAdapterService.sendAuthenticationCodeTokenRequest( - oauthConfig.tokenEndpoint, - payload - ); + const tokenDto: OAuthTokenDto = await this.oauthAdapterService.sendTokenRequest(oauthConfig.tokenEndpoint, payload); - const tokenDto: OAuthTokenDto = TokenRequestMapper.mapTokenResponseToDto(responseToken); return tokenDto; } diff --git a/apps/server/src/modules/provisioning/loggable/group-role-unknown.loggable.spec.ts b/apps/server/src/modules/provisioning/loggable/group-role-unknown.loggable.spec.ts index 47447c99103..9c0c9560f8a 100644 --- a/apps/server/src/modules/provisioning/loggable/group-role-unknown.loggable.spec.ts +++ b/apps/server/src/modules/provisioning/loggable/group-role-unknown.loggable.spec.ts @@ -1,4 +1,4 @@ -import { SanisGroupRole, SanisSonstigeGruppenzugehoerigeResponse } from '../strategy'; +import { SanisGroupRole, SanisSonstigeGruppenzugehoerigeResponse } from '@infra/schulconnex-client'; import { GroupRoleUnknownLoggable } from './group-role-unknown.loggable'; describe('GroupRoleUnknownLoggable', () => { diff --git a/apps/server/src/modules/provisioning/loggable/group-role-unknown.loggable.ts b/apps/server/src/modules/provisioning/loggable/group-role-unknown.loggable.ts index 0eb43237060..dd44dd8b7e7 100644 --- a/apps/server/src/modules/provisioning/loggable/group-role-unknown.loggable.ts +++ b/apps/server/src/modules/provisioning/loggable/group-role-unknown.loggable.ts @@ -1,5 +1,5 @@ import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; -import { SanisSonstigeGruppenzugehoerigeResponse } from '../strategy/sanis/response'; +import { SanisSonstigeGruppenzugehoerigeResponse } from '@infra/schulconnex-client'; export class GroupRoleUnknownLoggable implements Loggable { constructor(private readonly relation: SanisSonstigeGruppenzugehoerigeResponse) {} diff --git a/apps/server/src/modules/provisioning/strategy/sanis/index.ts b/apps/server/src/modules/provisioning/strategy/sanis/index.ts index 4f98cbd73e9..39c4c4272ac 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/index.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/index.ts @@ -1,3 +1,2 @@ -export * from './response'; export * from './sanis.strategy'; export * from './sanis-response.mapper'; diff --git a/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.spec.ts b/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.spec.ts index c3b70c695d6..275e4957d4e 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.spec.ts @@ -4,7 +4,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { RoleName } from '@shared/domain/interface'; import { Logger } from '@src/core/logger'; import { UUID } from 'bson'; -import { ExternalGroupDto, ExternalSchoolDto, ExternalUserDto } from '../../dto'; import { SanisGroupRole, SanisGroupType, @@ -13,7 +12,8 @@ import { SanisResponse, SanisRole, SanisSonstigeGruppenzugehoerigeResponse, -} from './response'; +} from '@infra/schulconnex-client'; +import { ExternalGroupDto, ExternalSchoolDto, ExternalUserDto } from '../../dto'; import { SanisResponseMapper } from './sanis-response.mapper'; describe('SanisResponseMapper', () => { diff --git a/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.ts b/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.ts index 4ec69d54149..9d56e28e57b 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.ts @@ -1,17 +1,17 @@ +import { + SanisGruppenResponse, + SanisResponse, + SanisSonstigeGruppenzugehoerigeResponse, +} from '@infra/schulconnex-client'; +import { SanisGroupRole } from '@infra/schulconnex-client/response/sanis-group-role'; +import { SanisGroupType } from '@infra/schulconnex-client/response/sanis-group-type'; +import { SanisRole } from '@infra/schulconnex-client/response/sanis-role'; import { GroupTypes } from '@modules/group'; import { Injectable } from '@nestjs/common'; import { RoleName } from '@shared/domain/interface'; import { Logger } from '@src/core/logger'; import { ExternalGroupDto, ExternalGroupUserDto, ExternalSchoolDto, ExternalUserDto } from '../../dto'; import { GroupRoleUnknownLoggable } from '../../loggable'; -import { - SanisGroupRole, - SanisGroupType, - SanisGruppenResponse, - SanisResponse, - SanisRole, - SanisSonstigeGruppenzugehoerigeResponse, -} from './response'; const RoleMapping: Record = { [SanisRole.LEHR]: RoleName.TEACHER, @@ -57,7 +57,7 @@ export class SanisResponseMapper { const mapped = new ExternalUserDto({ firstName: source.person.name.vorname, lastName: source.person.name.familienname, - roles: [this.mapSanisRoleToRoleName(source)], + roles: [SanisResponseMapper.mapSanisRoleToRoleName(source)], externalId: source.pid, birthday: source.person.geburt?.datum ? new Date(source.person.geburt?.datum) : undefined, }); @@ -65,7 +65,7 @@ export class SanisResponseMapper { return mapped; } - private mapSanisRoleToRoleName(source: SanisResponse): RoleName { + public static mapSanisRoleToRoleName(source: SanisResponse): RoleName { return RoleMapping[source.personenkontexte[0].rolle]; } diff --git a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.spec.ts b/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.spec.ts index f7b9a4fbb0e..b9b668c1ced 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.spec.ts @@ -1,4 +1,11 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { + SanisGruppenResponse, + SanisResponse, + SanisResponseValidationGroups, + SanisRole, + schulconnexResponseFactory, +} from '@infra/schulconnex-client'; import { GroupTypes } from '@modules/group/domain'; import { HttpService } from '@nestjs/axios'; import { InternalServerErrorException } from '@nestjs/common'; @@ -20,14 +27,6 @@ import { ProvisioningSystemDto, } from '../../dto'; import { OidcProvisioningService } from '../oidc/service/oidc-provisioning.service'; -import { - SanisGroupRole, - SanisGroupType, - SanisGruppenResponse, - SanisResponse, - SanisResponseValidationGroups, - SanisRole, -} from './response'; import { SanisResponseMapper } from './sanis-response.mapper'; import { SanisProvisioningStrategy } from './sanis.strategy'; import ArgsType = jest.ArgsType; @@ -93,52 +92,7 @@ describe('SanisStrategy', () => { jest.resetAllMocks(); }); - const setupSanisResponse = (): SanisResponse => { - return { - pid: 'aef1f4fd-c323-466e-962b-a84354c0e713', - person: { - name: { - vorname: 'Hans', - familienname: 'Peter', - }, - geburt: { - datum: '2023-11-17', - }, - }, - personenkontexte: [ - { - id: new UUID().toString(), - rolle: SanisRole.LEIT, - organisation: { - id: new UUID('df66c8e6-cfac-40f7-b35b-0da5d8ee680e').toString(), - name: 'schoolName', - kennung: 'Kennung', - anschrift: { - ort: 'Hannover', - }, - }, - gruppen: [ - { - gruppe: { - id: new UUID().toString(), - bezeichnung: 'bezeichnung', - typ: SanisGroupType.CLASS, - }, - gruppenzugehoerigkeit: { - rollen: [SanisGroupRole.TEACHER], - }, - sonstige_gruppenzugehoerige: [ - { - rollen: [SanisGroupRole.STUDENT], - ktid: 'ktid', - }, - ], - }, - ], - }, - ], - }; - }; + const setupSanisResponse = (): SanisResponse => schulconnexResponseFactory.build(); describe('getType is called', () => { describe('when it is called', () => { diff --git a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts b/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts index ca8d5942645..c66e4fa17f6 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts @@ -7,6 +7,7 @@ import { AxiosRequestConfig, AxiosResponse } from 'axios'; import { plainToClass } from 'class-transformer'; import { validate, ValidationError } from 'class-validator'; import { firstValueFrom } from 'rxjs'; +import { SanisGruppenResponse, SanisResponse, SanisResponseValidationGroups } from '@infra/schulconnex-client/response'; import { IProvisioningFeatures, ProvisioningFeatures } from '../../config'; import { ExternalGroupDto, @@ -17,7 +18,6 @@ import { } from '../../dto'; import { OidcProvisioningStrategy } from '../oidc/oidc.strategy'; import { OidcProvisioningService } from '../oidc/service/oidc-provisioning.service'; -import { SanisGruppenResponse, SanisResponse, SanisResponseValidationGroups } from './response'; import { SanisResponseMapper } from './sanis-response.mapper'; @Injectable() @@ -42,6 +42,7 @@ export class SanisProvisioningStrategy extends OidcProvisioningStrategy { ); } + // TODO: N21-1678 use the schulconnex rest client const axiosConfig: AxiosRequestConfig = { headers: { Authorization: `Bearer ${input.accessToken}`, diff --git a/apps/server/src/modules/pseudonym/controller/api-test/pseudonym.api.spec.ts b/apps/server/src/modules/pseudonym/controller/api-test/pseudonym.api.spec.ts index f646a401885..019ad093fba 100644 --- a/apps/server/src/modules/pseudonym/controller/api-test/pseudonym.api.spec.ts +++ b/apps/server/src/modules/pseudonym/controller/api-test/pseudonym.api.spec.ts @@ -5,12 +5,12 @@ import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { SchoolEntity } from '@shared/domain/entity'; import { - TestApiClient, - UserAndAccountTestFactory, cleanupCollections, externalToolEntityFactory, externalToolPseudonymEntityFactory, - schoolFactory, + schoolEntityFactory, + TestApiClient, + UserAndAccountTestFactory, } from '@shared/testing'; import { UUID } from 'bson'; import { Response } from 'supertest'; @@ -55,7 +55,7 @@ describe('PseudonymController (API)', () => { describe('when valid params are given', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { studentUser, studentAccount } = UserAndAccountTestFactory.buildStudent({ school }, []); const pseudonymString: string = new UUID().toString(); const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId(); @@ -89,7 +89,7 @@ describe('PseudonymController (API)', () => { describe('when pseudonym is not connected to the users school', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { studentUser, studentAccount } = UserAndAccountTestFactory.buildStudent(); const { teacherUser, teacherAccount } = UserAndAccountTestFactory.buildTeacher({ school }); const pseudonymString: string = new UUID().toString(); @@ -127,7 +127,7 @@ describe('PseudonymController (API)', () => { describe('when pseudonym does not exist in db', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { studentUser, studentAccount } = UserAndAccountTestFactory.buildStudent({ school }); const pseudonymString: string = new UUID().toString(); const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId(); diff --git a/apps/server/src/modules/pseudonym/service/feathers-roster.service.spec.ts b/apps/server/src/modules/pseudonym/service/feathers-roster.service.spec.ts index de106a751ee..9ec3c18791e 100644 --- a/apps/server/src/modules/pseudonym/service/feathers-roster.service.spec.ts +++ b/apps/server/src/modules/pseudonym/service/feathers-roster.service.spec.ts @@ -21,8 +21,8 @@ import { externalToolFactory, legacySchoolDoFactory, pseudonymFactory, + schoolEntityFactory, schoolExternalToolFactory, - schoolFactory, setupEntities, UserAndAccountTestFactory, userDoFactory, @@ -362,7 +362,7 @@ describe('FeathersRosterService', () => { describe('when valid courseId and oauth2ClientId is given', () => { const setup = () => { let courseA: Course = courseFactory.buildWithId(); - const schoolEntity: SchoolEntity = schoolFactory.buildWithId(); + const schoolEntity: SchoolEntity = schoolEntityFactory.buildWithId(); const school: LegacySchoolDo = legacySchoolDoFactory.build({ id: schoolEntity.id }); const externalTool: ExternalTool = externalToolFactory.buildWithId(); const externalToolId: string = externalTool.id as string; diff --git a/apps/server/src/modules/pseudonym/uc/pseudonym.uc.spec.ts b/apps/server/src/modules/pseudonym/uc/pseudonym.uc.spec.ts index d7b2b861c88..8c8b6683b56 100644 --- a/apps/server/src/modules/pseudonym/uc/pseudonym.uc.spec.ts +++ b/apps/server/src/modules/pseudonym/uc/pseudonym.uc.spec.ts @@ -5,7 +5,13 @@ import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { LegacySchoolDo, Pseudonym } from '@shared/domain/domainobject'; import { SchoolEntity, User } from '@shared/domain/entity'; -import { legacySchoolDoFactory, pseudonymFactory, schoolFactory, setupEntities, userFactory } from '@shared/testing'; +import { + legacySchoolDoFactory, + pseudonymFactory, + schoolEntityFactory, + setupEntities, + userFactory, +} from '@shared/testing'; import { PseudonymService } from '../service'; import { PseudonymUc } from './pseudonym.uc'; @@ -57,7 +63,7 @@ describe('PseudonymUc', () => { const setup = () => { const userId = 'userId'; const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); - const schoolEntity: SchoolEntity = schoolFactory.buildWithId(); + const schoolEntity: SchoolEntity = schoolEntityFactory.buildWithId(); const user: User = userFactory.buildWithId({ school: schoolEntity }); user.school = schoolEntity; const pseudonym: Pseudonym = new Pseudonym(pseudonymFactory.build({ userId: user.id })); @@ -115,7 +121,7 @@ describe('PseudonymUc', () => { const setup = () => { const userId = 'userId'; const user: User = userFactory.buildWithId(); - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); user.school = school; const pseudonym: Pseudonym = new Pseudonym(pseudonymFactory.build()); diff --git a/apps/server/src/modules/school/api/test/school.controller.api.spec.ts b/apps/server/src/modules/school/api/test/school.controller.api.spec.ts index 7cd7c6d7242..464018f6fb1 100644 --- a/apps/server/src/modules/school/api/test/school.controller.api.spec.ts +++ b/apps/server/src/modules/school/api/test/school.controller.api.spec.ts @@ -4,7 +4,7 @@ import { Test } from '@nestjs/testing'; import { cleanupCollections, TestApiClient } from '@shared/testing'; import { federalStateFactory, - schoolFactory, + schoolEntityFactory, schoolYearFactory, systemEntityFactory, UserAndAccountTestFactory, @@ -96,7 +96,7 @@ describe('School Controller (API)', () => { describe('when user is not in requested school', () => { const setup = async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); await em.persistAndFlush([school, studentAccount, studentUser]); @@ -123,7 +123,7 @@ describe('School Controller (API)', () => { const federalState = federalStateFactory.build(); const county = countyEmbeddableFactory.build(); const systems = systemEntityFactory.buildList(3); - const school = schoolFactory.build({ currentYear, federalState, systems, county }); + const school = schoolEntityFactory.build({ currentYear, federalState, systems, county }); const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent({ school }); await em.persistAndFlush([...schoolYears, federalState, school, studentAccount, studentUser]); @@ -205,7 +205,7 @@ describe('School Controller (API)', () => { describe('when a user is logged in', () => { const setup = async () => { - const schools = schoolFactory.buildList(3); + const schools = schoolEntityFactory.buildList(3); const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); await em.persistAndFlush([...schools, studentAccount, studentUser]); diff --git a/apps/server/src/modules/school/repo/mikro-orm/mapper/school.entity.mapper.spec.ts b/apps/server/src/modules/school/repo/mikro-orm/mapper/school.entity.mapper.spec.ts index ecfda128c30..77daaeb96f4 100644 --- a/apps/server/src/modules/school/repo/mikro-orm/mapper/school.entity.mapper.spec.ts +++ b/apps/server/src/modules/school/repo/mikro-orm/mapper/school.entity.mapper.spec.ts @@ -1,4 +1,4 @@ -import { schoolFactory } from '@shared/testing'; +import { schoolEntityFactory } from '@shared/testing'; import { School } from '../../../domain'; import { CountyEmbeddableMapper } from './county.embeddable.mapper'; import { FederalStateEntityMapper } from './federal-state.entity.mapper'; @@ -9,7 +9,7 @@ describe('SchoolEntityMapper', () => { describe('mapToDo', () => { describe('when school entity is passed', () => { const setup = () => { - const entity = schoolFactory.build(); + const entity = schoolEntityFactory.build(); const expected = new School({ id: entity.id, createdAt: entity.createdAt, diff --git a/apps/server/src/modules/school/repo/mikro-orm/school.repo.integration.spec.ts b/apps/server/src/modules/school/repo/mikro-orm/school.repo.integration.spec.ts index 3b537047a83..e4a9248fe20 100644 --- a/apps/server/src/modules/school/repo/mikro-orm/school.repo.integration.spec.ts +++ b/apps/server/src/modules/school/repo/mikro-orm/school.repo.integration.spec.ts @@ -3,7 +3,7 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { SchoolEntity } from '@shared/domain/entity/school.entity'; import { SortOrder } from '@shared/domain/interface'; -import { cleanupCollections, federalStateFactory, schoolFactory, systemEntityFactory } from '@shared/testing'; +import { cleanupCollections, federalStateFactory, schoolEntityFactory, systemEntityFactory } from '@shared/testing'; import { countyEmbeddableFactory } from '@shared/testing/factory/county.embeddable.factory'; import { MongoMemoryDatabaseModule } from '@src/infra/database'; import { SCHOOL_REPO } from '../../domain'; @@ -37,7 +37,7 @@ describe('SchoolMikroOrmRepo', () => { describe('getSchools', () => { describe('when no query and options are given', () => { const setup = async () => { - const entities = schoolFactory.buildList(3); + const entities = schoolEntityFactory.buildList(3); await em.persistAndFlush(entities); em.clear(); const schools = entities.map((entity) => SchoolEntityMapper.mapToDo(entity)); @@ -57,8 +57,8 @@ describe('SchoolMikroOrmRepo', () => { describe('when query is given', () => { const setup = async () => { const federalState = federalStateFactory.build(); - const entity1 = schoolFactory.build({ federalState }); - const entity2 = schoolFactory.build(); + const entity1 = schoolEntityFactory.build({ federalState }); + const entity2 = schoolEntityFactory.build(); await em.persistAndFlush([entity1, entity2]); em.clear(); const schoolDo1 = SchoolEntityMapper.mapToDo(entity1); @@ -81,7 +81,7 @@ describe('SchoolMikroOrmRepo', () => { describe('when pagination option is given', () => { const setup = async () => { - const entities = schoolFactory.buildList(3); + const entities = schoolEntityFactory.buildList(3); await em.persistAndFlush(entities); em.clear(); const schoolDos = entities.map((entity) => SchoolEntityMapper.mapToDo(entity)); @@ -107,8 +107,8 @@ describe('SchoolMikroOrmRepo', () => { describe('when order option is given', () => { const setup = async () => { - const entity1 = schoolFactory.build({ name: 'bbb' }); - const entity2 = schoolFactory.build({ name: 'aaa' }); + const entity1 = schoolEntityFactory.build({ name: 'bbb' }); + const entity2 = schoolEntityFactory.build({ name: 'aaa' }); await em.persistAndFlush([entity1, entity2]); em.clear(); const schoolDo1 = SchoolEntityMapper.mapToDo(entity1); @@ -147,7 +147,7 @@ describe('SchoolMikroOrmRepo', () => { const systems = systemEntityFactory.buildList(2); const county = countyEmbeddableFactory.build(); const schoolId = new ObjectId().toHexString(); - const entity = schoolFactory.buildWithId({ systems, county }, schoolId); + const entity = schoolEntityFactory.buildWithId({ systems, county }, schoolId); await em.persistAndFlush([entity]); em.clear(); const schoolDo = SchoolEntityMapper.mapToDo(entity); diff --git a/apps/server/src/modules/server/server.module.ts b/apps/server/src/modules/server/server.module.ts index 0be073527ba..24b71780085 100644 --- a/apps/server/src/modules/server/server.module.ts +++ b/apps/server/src/modules/server/server.module.ts @@ -2,6 +2,7 @@ import { Configuration } from '@hpi-schul-cloud/commons'; import { MongoDatabaseModuleOptions, MongoMemoryDatabaseModule } from '@infra/database'; import { MailModule } from '@infra/mail'; import { RabbitMQWrapperModule, RabbitMQWrapperTestModule } from '@infra/rabbitmq'; +import { SchulconnexClientModule } from '@infra/schulconnex-client'; import { Dictionary, IPrimaryKey } from '@mikro-orm/core'; import { MikroOrmModule, MikroOrmModuleSyncOptions } from '@mikro-orm/nestjs'; import { AccountApiModule } from '@modules/account/account-api.module'; @@ -25,7 +26,7 @@ import { SystemApiModule } from '@modules/system/system-api.module'; import { TaskApiModule } from '@modules/task/task-api.module'; import { TeamsApiModule } from '@modules/teams/teams-api.module'; import { ToolApiModule } from '@modules/tool/tool-api.module'; -import { ImportUserModule } from '@modules/user-import'; +import { ImportUserModule, UserImportConfigModule } from '@modules/user-import'; import { UserLoginMigrationApiModule } from '@modules/user-login-migration/user-login-migration-api.module'; import { UserApiModule } from '@modules/user/user-api.module'; import { VideoConferenceApiModule } from '@modules/video-conference/video-conference-api.module'; @@ -35,7 +36,7 @@ import { ALL_ENTITIES } from '@shared/domain/entity'; import { createConfigModuleOptions, DB_PASSWORD, DB_URL, DB_USERNAME } from '@src/config'; import { CoreModule } from '@src/core'; import { LoggerModule } from '@src/core/logger'; -import { ServerController } from './controller/server.controller'; +import { ServerController } from './controller'; import { serverConfig } from './server.config'; const serverModules = [ @@ -50,7 +51,14 @@ const serverModules = [ LessonApiModule, NewsModule, UserApiModule, + SchulconnexClientModule.register({ + apiUrl: Configuration.get('SCHULCONNEX_CLIENT__API_URL') as string, + tokenEndpoint: Configuration.get('SCHULCONNEX_CLIENT__TOKEN_ENDPOINT') as string, + clientId: Configuration.get('SCHULCONNEX_CLIENT__CLIENT_ID') as string, + clientSecret: Configuration.get('SCHULCONNEX_CLIENT__CLIENT_SECRET') as string, + }), ImportUserModule, + UserImportConfigModule, LearnroomApiModule, FilesStorageClientModule, SystemApiModule, diff --git a/apps/server/src/modules/sharing/controller/api-test/sharing-create-token.api.spec.ts b/apps/server/src/modules/sharing/controller/api-test/sharing-create-token.api.spec.ts index f69411a7149..8afd86f6c02 100644 --- a/apps/server/src/modules/sharing/controller/api-test/sharing-create-token.api.spec.ts +++ b/apps/server/src/modules/sharing/controller/api-test/sharing-create-token.api.spec.ts @@ -12,7 +12,7 @@ import { courseFactory, mapUserToCurrentUser, roleFactory, - schoolFactory, + schoolEntityFactory, userFactory, } from '@shared/testing'; import { Request } from 'express'; @@ -79,7 +79,7 @@ describe(`share token creation (api)`, () => { const setup = async () => { await cleanupCollections(em); - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const roles = roleFactory.buildList(1, { permissions: [Permission.COURSE_CREATE], }); diff --git a/apps/server/src/modules/sharing/controller/api-test/sharing-import-token.api.spec.ts b/apps/server/src/modules/sharing/controller/api-test/sharing-import-token.api.spec.ts index b679b40b1db..066a9d1e5cf 100644 --- a/apps/server/src/modules/sharing/controller/api-test/sharing-import-token.api.spec.ts +++ b/apps/server/src/modules/sharing/controller/api-test/sharing-import-token.api.spec.ts @@ -13,7 +13,7 @@ import { courseFactory, mapUserToCurrentUser, roleFactory, - schoolFactory, + schoolEntityFactory, userFactory, } from '@shared/testing'; import { Request } from 'express'; @@ -85,7 +85,7 @@ describe(`share token import (api)`, () => { const setup = async (context?: ShareTokenContext) => { await cleanupCollections(em); - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const roles = roleFactory.buildList(1, { permissions: [Permission.COURSE_CREATE], }); @@ -159,8 +159,8 @@ describe(`share token import (api)`, () => { describe('with invalid context', () => { const setup2 = async () => { - const school = schoolFactory.build(); - const otherSchool = schoolFactory.build(); + const school = schoolEntityFactory.build(); + const otherSchool = schoolEntityFactory.build(); const roles = roleFactory.buildList(1, { permissions: [Permission.COURSE_CREATE], }); diff --git a/apps/server/src/modules/sharing/controller/api-test/sharing-lookup-token.api.spec.ts b/apps/server/src/modules/sharing/controller/api-test/sharing-lookup-token.api.spec.ts index 5b7f0edc659..c712b078431 100644 --- a/apps/server/src/modules/sharing/controller/api-test/sharing-lookup-token.api.spec.ts +++ b/apps/server/src/modules/sharing/controller/api-test/sharing-lookup-token.api.spec.ts @@ -4,7 +4,7 @@ import { ServerTestModule } from '@modules/server'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission } from '@shared/domain/interface'; -import { TestApiClient, UserAndAccountTestFactory, courseFactory, schoolFactory } from '@shared/testing'; +import { courseFactory, schoolEntityFactory, TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; import { ShareTokenContextType, ShareTokenParentType } from '../../domainobject/share-token.do'; import { ShareTokenService } from '../../service'; import { ShareTokenInfoResponse } from '../dto'; @@ -159,7 +159,7 @@ describe(`share token lookup (api)`, () => { const parentType = ShareTokenParentType.Course; const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({}, [Permission.COURSE_CREATE]); - const otherSchool = schoolFactory.build(); + const otherSchool = schoolEntityFactory.build(); const course = courseFactory.build({ teachers: [teacherUser] }); await em.persistAndFlush([course, teacherAccount, teacherUser, otherSchool]); diff --git a/apps/server/src/modules/sharing/repo/share-token.repo.integration.spec.ts b/apps/server/src/modules/sharing/repo/share-token.repo.integration.spec.ts index f5e01b0e9ea..dc0f03f1d49 100644 --- a/apps/server/src/modules/sharing/repo/share-token.repo.integration.spec.ts +++ b/apps/server/src/modules/sharing/repo/share-token.repo.integration.spec.ts @@ -1,8 +1,8 @@ import { createMock } from '@golevelup/ts-jest'; +import { MongoMemoryDatabaseModule } from '@infra/database'; import { EntityManager } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; -import { MongoMemoryDatabaseModule } from '@infra/database'; -import { cleanupCollections, schoolFactory, shareTokenFactory } from '@shared/testing'; +import { cleanupCollections, schoolEntityFactory, shareTokenFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { ShareTokenContextType } from '../domainobject/share-token.do'; import { ShareTokenRepo } from './share-token.repo'; @@ -47,7 +47,7 @@ describe('ShareTokenRepo', () => { }); it('should include context id', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); await em.persistAndFlush([school]); const shareToken = shareTokenFactory.build({ context: { contextType: ShareTokenContextType.School, contextId: school.id }, diff --git a/apps/server/src/modules/sharing/uc/share-token.uc.spec.ts b/apps/server/src/modules/sharing/uc/share-token.uc.spec.ts index 63c171a04e4..de3efb4f8d6 100644 --- a/apps/server/src/modules/sharing/uc/share-token.uc.spec.ts +++ b/apps/server/src/modules/sharing/uc/share-token.uc.spec.ts @@ -1,8 +1,5 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons/lib'; -import { BadRequestException, InternalServerErrorException, NotImplementedException } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { Permission } from '@shared/domain/interface'; import { Action, @@ -14,10 +11,13 @@ import { CopyElementType, CopyStatus, CopyStatusEnum } from '@modules/copy-helpe import { CourseCopyService, CourseService } from '@modules/learnroom'; import { LessonCopyService } from '@modules/lesson'; import { TaskCopyService } from '@modules/task'; +import { BadRequestException, InternalServerErrorException, NotImplementedException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Permission } from '@shared/domain/interface'; import { courseFactory, lessonFactory, - schoolFactory, + schoolEntityFactory, setupEntities, shareTokenFactory, taskFactory, @@ -298,7 +298,7 @@ describe('ShareTokenUC', () => { describe('when restricted to same school', () => { it('should check parent write permission', async () => { - const school = schoolFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); const user = userFactory.buildWithId({ school }); const course = courseFactory.buildWithId(); authorization.getUserWithPermissions.mockResolvedValue(user); @@ -326,7 +326,7 @@ describe('ShareTokenUC', () => { }); it('should check context read permission', async () => { - const school = schoolFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); const user = userFactory.buildWithId({ school }); const course = courseFactory.buildWithId(); authorization.getUserWithPermissions.mockResolvedValue(user); @@ -354,7 +354,7 @@ describe('ShareTokenUC', () => { }); it('should call the service', async () => { - const school = schoolFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); const user = userFactory.buildWithId({ school }); const course = courseFactory.buildWithId(); authorization.getUserWithPermissions.mockResolvedValue(user); @@ -424,7 +424,7 @@ describe('ShareTokenUC', () => { describe('lookup a sharetoken', () => { describe('when parent is a course', () => { const setup = () => { - const school = schoolFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); const user = userFactory.buildWithId({ school }); authorization.getUserWithPermissions.mockResolvedValue(user); @@ -470,7 +470,7 @@ describe('ShareTokenUC', () => { describe('when parent is a lesson', () => { const setup = () => { - const school = schoolFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); const user = userFactory.buildWithId({ school }); authorization.getUserWithPermissions.mockResolvedValue(user); @@ -517,7 +517,7 @@ describe('ShareTokenUC', () => { describe('when parent is a task', () => { const setup = () => { - const school = schoolFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); const user = userFactory.buildWithId({ school }); authorization.getUserWithPermissions.mockResolvedValueOnce(user); @@ -564,7 +564,7 @@ describe('ShareTokenUC', () => { describe('when restricted to same school', () => { const setup = () => { - const school = schoolFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); const user = userFactory.buildWithId({ school }); const shareToken = shareTokenFactory.build({ context: { contextType: ShareTokenContextType.School, contextId: school.id }, @@ -593,7 +593,7 @@ describe('ShareTokenUC', () => { describe('when not restricted to same school', () => { const setup = () => { - const school = schoolFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); const user = userFactory.buildWithId({ school }); const shareToken = shareTokenFactory.build(); const parentName = 'name'; @@ -614,7 +614,7 @@ describe('ShareTokenUC', () => { describe('import share token', () => { describe('when parent is a course', () => { const setup = () => { - const school = schoolFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); const user = userFactory.buildWithId({ school }); authorization.getUserWithPermissions.mockResolvedValue(user); @@ -718,7 +718,7 @@ describe('ShareTokenUC', () => { describe('when parent is a lesson', () => { const setup = () => { - const school = schoolFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); const user = userFactory.buildWithId({ school }); authorization.getUserWithPermissions.mockResolvedValue(user); @@ -834,7 +834,7 @@ describe('ShareTokenUC', () => { describe('when parent is a task', () => { const setupTask = () => { - const school = schoolFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); const user = userFactory.buildWithId({ school }); authorization.getUserWithPermissions.mockResolvedValue(user); diff --git a/apps/server/src/modules/system/controller/api-test/system.api.spec.ts b/apps/server/src/modules/system/controller/api-test/system.api.spec.ts index 0ccc6e4a59f..4aaf7fff046 100644 --- a/apps/server/src/modules/system/controller/api-test/system.api.spec.ts +++ b/apps/server/src/modules/system/controller/api-test/system.api.spec.ts @@ -3,7 +3,7 @@ import { ServerTestModule } from '@modules/server'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { OauthConfigEntity, SchoolEntity, SystemEntity } from '@shared/domain/entity'; -import { TestApiClient, UserAndAccountTestFactory, schoolFactory, systemEntityFactory } from '@shared/testing'; +import { schoolEntityFactory, systemEntityFactory, TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; import { Response } from 'supertest'; import { PublicSystemListResponse, PublicSystemResponse } from '../dto'; @@ -112,7 +112,7 @@ describe('System (API)', () => { describe('when the endpoint is called with a known systemId', () => { const setup = async () => { const system: SystemEntity = systemEntityFactory.withLdapConfig({ provider: 'general' }).buildWithId(); - const school: SchoolEntity = schoolFactory.build({ systems: [system] }); + const school: SchoolEntity = schoolEntityFactory.build({ systems: [system] }); const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }); await em.persistAndFlush([system, adminAccount, adminUser, school]); diff --git a/apps/server/src/modules/task/service/task-copy.service.spec.ts b/apps/server/src/modules/task/service/task-copy.service.spec.ts index ca320ac60d6..4c77b83b1a7 100644 --- a/apps/server/src/modules/task/service/task-copy.service.spec.ts +++ b/apps/server/src/modules/task/service/task-copy.service.spec.ts @@ -1,18 +1,18 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { CopyElementType, CopyHelperService, CopyStatusEnum } from '@modules/copy-helper'; +import { CopyFilesService } from '@modules/files-storage-client'; import { Test, TestingModule } from '@nestjs/testing'; import { Task } from '@shared/domain/entity'; import { TaskRepo } from '@shared/repo'; import { courseFactory, + legacyFileEntityMockFactory, lessonFactory, - schoolFactory, + schoolEntityFactory, setupEntities, taskFactory, userFactory, - legacyFileEntityMockFactory, } from '@shared/testing'; -import { CopyElementType, CopyHelperService, CopyStatusEnum } from '@modules/copy-helper'; -import { CopyFilesService } from '@modules/files-storage-client'; import { TaskCopyService } from './task-copy.service'; describe('task copy service', () => { @@ -61,7 +61,7 @@ describe('task copy service', () => { describe('handleCopyTask', () => { describe('when copying task within original course', () => { const setup = () => { - const school = schoolFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); const user = userFactory.buildWithId({ school }); const destinationCourse = courseFactory.buildWithId({ school, teachers: [user] }); const destinationLesson = lessonFactory.buildWithId({ course: destinationCourse }); @@ -292,8 +292,8 @@ describe('task copy service', () => { describe('when copying task into different school', () => { it('should set the school of the copy to the school of the user', async () => { - const originalSchool = schoolFactory.buildWithId(); - const destinationSchool = schoolFactory.buildWithId(); + const originalSchool = schoolEntityFactory.buildWithId(); + const destinationSchool = schoolEntityFactory.buildWithId(); const originalCourse = courseFactory.build({ school: originalSchool }); const originalLesson = lessonFactory.build({ course: originalCourse }); const destinationCourse = courseFactory.buildWithId({ school: destinationSchool }); @@ -409,7 +409,7 @@ describe('task copy service', () => { }; const setupWithFiles = () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const file1 = legacyFileEntityMockFactory.build(); const file2 = legacyFileEntityMockFactory.build(); const imageHTML1 = getImageHTML(file1.id, file1.name); diff --git a/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-context.api.spec.ts b/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-context.api.spec.ts index 2561db5489a..6fdba299419 100644 --- a/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-context.api.spec.ts +++ b/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-context.api.spec.ts @@ -5,16 +5,16 @@ import { Test, TestingModule } from '@nestjs/testing'; import { Account, Course, SchoolEntity, User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { - TestApiClient, - UserAndAccountTestFactory, accountFactory, contextExternalToolEntityFactory, courseFactory, customParameterEntityFactory, externalToolEntityFactory, roleFactory, + schoolEntityFactory, schoolExternalToolEntityFactory, - schoolFactory, + TestApiClient, + UserAndAccountTestFactory, userFactory, } from '@shared/testing'; import { ObjectId } from 'bson'; @@ -59,7 +59,7 @@ describe('ToolContextController (API)', () => { describe('[POST] tools/context-external-tools', () => { describe('when creation of contextExternalTool is successfully', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.CONTEXT_TOOL_ADMIN, ]); @@ -137,7 +137,7 @@ describe('ToolContextController (API)', () => { describe('when user is not authorized for the requested context', () => { const setup = async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }); const course = courseFactory.build({ teachers: [teacherUser] }); const otherCourse = courseFactory.build(); @@ -178,7 +178,7 @@ describe('ToolContextController (API)', () => { describe('when external tool has no restrictions ', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.CONTEXT_TOOL_ADMIN, ]); @@ -237,7 +237,7 @@ describe('ToolContextController (API)', () => { describe('when external tool restricts to wrong context ', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.CONTEXT_TOOL_ADMIN, ]); @@ -289,7 +289,7 @@ describe('ToolContextController (API)', () => { describe('[DELETE] tools/context-external-tools/:contextExternalToolId', () => { describe('when deletion of contextExternalTool is successfully', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.CONTEXT_TOOL_ADMIN, ]); @@ -375,8 +375,8 @@ describe('ToolContextController (API)', () => { describe('[GET] tools/context-external-tools/:contextType/:contextId', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); - const otherSchool: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); + const otherSchool: SchoolEntity = schoolEntityFactory.buildWithId(); const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.CONTEXT_TOOL_ADMIN, @@ -545,7 +545,7 @@ describe('ToolContextController (API)', () => { describe('[GET] tools/context-external-tools/:contextExternalToolId', () => { describe('when the tool exists', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { teacherUser, teacherAccount } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.CONTEXT_TOOL_ADMIN, @@ -609,7 +609,7 @@ describe('ToolContextController (API)', () => { describe('when the tool does not exist', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { teacherUser, teacherAccount } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.CONTEXT_TOOL_ADMIN, @@ -639,7 +639,7 @@ describe('ToolContextController (API)', () => { describe('when user is not authorized', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const course: Course = courseFactory.buildWithId({ school, @@ -678,7 +678,7 @@ describe('ToolContextController (API)', () => { describe('when user has not the required permission', () => { const setup = async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const { studentUser, studentAccount } = UserAndAccountTestFactory.buildStudent({ school }); const course = courseFactory.build({ teachers: [studentUser], @@ -726,7 +726,7 @@ describe('ToolContextController (API)', () => { describe('[PUT] tools/context-external-tools/:contextExternalToolId', () => { describe('when update of contextExternalTool is successfully', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { teacherUser, teacherAccount } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.CONTEXT_TOOL_ADMIN, @@ -820,7 +820,7 @@ describe('ToolContextController (API)', () => { const roleWithoutPermission = roleFactory.build(); teacherUser.roles.set([roleWithoutPermission]); - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const course = courseFactory.build({ teachers: [teacherUser], school }); const contextParameter = customParameterEntityFactory.build({ scope: CustomParameterScope.CONTEXT, @@ -891,7 +891,7 @@ describe('ToolContextController (API)', () => { describe('when the user is not authenticated', () => { const setup = async () => { const { teacherUser } = UserAndAccountTestFactory.buildTeacher(); - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const course: Course = courseFactory.buildWithId({ teachers: [teacherUser], school }); const contextParameter = customParameterEntityFactory.build({ diff --git a/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-reference.api.spec.ts b/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-reference.api.spec.ts index 11952df60bc..959c9cf0f29 100644 --- a/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-reference.api.spec.ts +++ b/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-reference.api.spec.ts @@ -5,16 +5,16 @@ import { Test, TestingModule } from '@nestjs/testing'; import { Course, SchoolEntity } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { - TestApiClient, - UserAndAccountTestFactory, cleanupCollections, + contextExternalToolConfigurationStatusResponseFactory, contextExternalToolEntityFactory, courseFactory, customParameterFactory, externalToolEntityFactory, + schoolEntityFactory, schoolExternalToolEntityFactory, - schoolFactory, - contextExternalToolConfigurationStatusResponseFactory, + TestApiClient, + UserAndAccountTestFactory, } from '@shared/testing'; import { Response } from 'supertest'; @@ -62,8 +62,8 @@ describe('ToolReferenceController (API)', () => { describe('when user has no access to a tool', () => { const setup = async () => { - const schoolWithoutTool: SchoolEntity = schoolFactory.buildWithId(); - const school: SchoolEntity = schoolFactory.buildWithId(); + const schoolWithoutTool: SchoolEntity = schoolEntityFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({ school: schoolWithoutTool }); const course: Course = courseFactory.buildWithId({ school, teachers: [adminUser] }); const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId(); @@ -110,7 +110,7 @@ describe('ToolReferenceController (API)', () => { describe('when user has access for a tool', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({ school }, [ Permission.CONTEXT_TOOL_USER, ]); @@ -199,8 +199,8 @@ describe('ToolReferenceController (API)', () => { describe('when user has no access to a tool', () => { const setup = async () => { - const schoolWithoutTool: SchoolEntity = schoolFactory.buildWithId(); - const school: SchoolEntity = schoolFactory.buildWithId(); + const schoolWithoutTool: SchoolEntity = schoolEntityFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({ school: schoolWithoutTool }); const course: Course = courseFactory.buildWithId({ school, teachers: [adminUser] }); const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId(); @@ -244,7 +244,7 @@ describe('ToolReferenceController (API)', () => { describe('when user has access for a tool', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({ school }, [ Permission.CONTEXT_TOOL_USER, ]); diff --git a/apps/server/src/modules/tool/context-external-tool/entity/context-external-tool.entity.spec.ts b/apps/server/src/modules/tool/context-external-tool/entity/context-external-tool.entity.spec.ts index b41519bbef5..a1c034bfa26 100644 --- a/apps/server/src/modules/tool/context-external-tool/entity/context-external-tool.entity.spec.ts +++ b/apps/server/src/modules/tool/context-external-tool/entity/context-external-tool.entity.spec.ts @@ -1,8 +1,8 @@ import { contextExternalToolEntityFactory, externalToolEntityFactory, + schoolEntityFactory, schoolExternalToolEntityFactory, - schoolFactory, setupEntities, } from '@shared/testing'; import { CustomParameterLocation, CustomParameterScope, CustomParameterType, ToolConfigType } from '../../common/enum'; @@ -10,8 +10,8 @@ import { CustomParameterLocation, CustomParameterScope, CustomParameterType, Too import { BasicToolConfigEntity, CustomParameterEntity, - ExternalToolEntity, ExternalToolConfigEntity, + ExternalToolEntity, } from '../../external-tool/entity'; import { SchoolExternalToolEntity } from '../../school-external-tool/entity'; import { ContextExternalToolEntity } from './context-external-tool.entity'; @@ -57,7 +57,7 @@ describe('ExternalToolEntity', () => { }); const schoolTool: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ tool: externalToolEntity, - school: schoolFactory.buildWithId(), + school: schoolEntityFactory.buildWithId(), schoolParameters: [], toolVersion: 1, }); diff --git a/apps/server/src/modules/tool/external-tool/controller/api-test/tool-configuration.api.spec.ts b/apps/server/src/modules/tool/external-tool/controller/api-test/tool-configuration.api.spec.ts index 812836a99da..90fceabede8 100644 --- a/apps/server/src/modules/tool/external-tool/controller/api-test/tool-configuration.api.spec.ts +++ b/apps/server/src/modules/tool/external-tool/controller/api-test/tool-configuration.api.spec.ts @@ -6,16 +6,16 @@ import { Test, TestingModule } from '@nestjs/testing'; import { Account, Board, Course, SchoolEntity, User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { - TestApiClient, - UserAndAccountTestFactory, accountFactory, boardFactory, contextExternalToolEntityFactory, courseFactory, customParameterFactory, externalToolEntityFactory, + schoolEntityFactory, schoolExternalToolEntityFactory, - schoolFactory, + TestApiClient, + UserAndAccountTestFactory, userFactory, } from '@shared/testing'; import { Response } from 'supertest'; @@ -65,7 +65,7 @@ describe('ToolConfigurationController (API)', () => { describe('[GET] tools/:contextType/:contextId/available-tools', () => { describe('when the user is not authorized', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { studentUser, studentAccount } = UserAndAccountTestFactory.buildStudent({ school }); @@ -120,7 +120,7 @@ describe('ToolConfigurationController (API)', () => { describe('when tools are available for a context', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { teacherUser, teacherAccount } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.CONTEXT_TOOL_ADMIN, @@ -247,7 +247,7 @@ describe('ToolConfigurationController (API)', () => { describe('when no tools are available for a course', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { teacherUser, teacherAccount } = UserAndAccountTestFactory.buildTeacher({}, [ Permission.CONTEXT_TOOL_ADMIN, @@ -283,7 +283,7 @@ describe('ToolConfigurationController (API)', () => { describe('[GET] tools/school/:schoolId/available-tools', () => { describe('when the user is not authorized', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const user: User = userFactory.buildWithId({ school, roles: [] }); const account: Account = accountFactory.buildWithId({ userId: user.id }); @@ -322,7 +322,7 @@ describe('ToolConfigurationController (API)', () => { describe('when tools are available for a school', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({}, [Permission.TOOL_ADMIN]); @@ -381,7 +381,7 @@ describe('ToolConfigurationController (API)', () => { describe('when no tools are available for a school', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({}, [Permission.SCHOOL_TOOL_ADMIN]); @@ -411,7 +411,7 @@ describe('ToolConfigurationController (API)', () => { describe('GET tools/school-external-tools/:schoolExternalToolId/configuration-template', () => { describe('when the user is not authorized', () => { const setup = async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); // not on same school like the tool const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({}, []); const externalTool: ExternalToolEntity = externalToolEntityFactory.build(); @@ -444,7 +444,7 @@ describe('ToolConfigurationController (API)', () => { describe('when tool is not hidden', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({ school }, [ Permission.SCHOOL_TOOL_ADMIN, @@ -507,7 +507,7 @@ describe('ToolConfigurationController (API)', () => { describe('when tool is hidden', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({}, [Permission.SCHOOL_TOOL_ADMIN]); @@ -540,7 +540,7 @@ describe('ToolConfigurationController (API)', () => { describe('GET tools/context-external-tools/:contextExternalToolId/configuration-template', () => { describe('when the user is not authorized', () => { const setup = async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({}, [Permission.SCHOOL_TOOL_ADMIN]); // user is not part of the course const course = courseFactory.build(); @@ -582,7 +582,7 @@ describe('ToolConfigurationController (API)', () => { describe('when tool is not hidden', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { teacherUser, teacherAccount } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.CONTEXT_TOOL_ADMIN, @@ -663,7 +663,7 @@ describe('ToolConfigurationController (API)', () => { describe('when tool is hidden', () => { const setup = async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.CONTEXT_TOOL_ADMIN, ]); diff --git a/apps/server/src/modules/tool/external-tool/controller/api-test/tool.api.spec.ts b/apps/server/src/modules/tool/external-tool/controller/api-test/tool.api.spec.ts index 7de0817cc28..28ad2390ea3 100644 --- a/apps/server/src/modules/tool/external-tool/controller/api-test/tool.api.spec.ts +++ b/apps/server/src/modules/tool/external-tool/controller/api-test/tool.api.spec.ts @@ -12,8 +12,8 @@ import { externalToolElementNodeFactory, externalToolEntityFactory, externalToolFactory, + schoolEntityFactory, schoolExternalToolEntityFactory, - schoolFactory, TestApiClient, UserAndAccountTestFactory, } from '@shared/testing'; @@ -661,7 +661,7 @@ describe('ToolController (API)', () => { const toolId: string = new ObjectId().toHexString(); const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId(undefined, toolId); - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const schoolExternalToolEntitys: SchoolExternalToolEntity[] = schoolExternalToolEntityFactory.buildList(2, { tool: externalToolEntity, school, diff --git a/apps/server/src/modules/tool/school-external-tool/controller/api-test/tool-school.api.spec.ts b/apps/server/src/modules/tool/school-external-tool/controller/api-test/tool-school.api.spec.ts index 459299809fb..e4954c9940f 100644 --- a/apps/server/src/modules/tool/school-external-tool/controller/api-test/tool-school.api.spec.ts +++ b/apps/server/src/modules/tool/school-external-tool/controller/api-test/tool-school.api.spec.ts @@ -12,8 +12,8 @@ import { customParameterEntityFactory, externalToolElementNodeFactory, externalToolEntityFactory, + schoolEntityFactory, schoolExternalToolEntityFactory, - schoolFactory, TestApiClient, UserAndAccountTestFactory, userFactory, @@ -61,7 +61,7 @@ describe('ToolSchoolController (API)', () => { describe('[POST] tools/school-external-tools', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({ school }, [ Permission.SCHOOL_TOOL_ADMIN, @@ -164,7 +164,7 @@ describe('ToolSchoolController (API)', () => { describe('[DELETE] tools/school-external-tools/:schoolExternalToolId', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({ school }, [ Permission.SCHOOL_TOOL_ADMIN, @@ -229,7 +229,7 @@ describe('ToolSchoolController (API)', () => { describe('[GET] tools/school-external-tools/', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({ school }, [ Permission.SCHOOL_TOOL_ADMIN, @@ -322,7 +322,7 @@ describe('ToolSchoolController (API)', () => { describe('[GET] tools/school-external-tools/:schoolExternalToolId', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({ school }, [ Permission.SCHOOL_TOOL_ADMIN, @@ -405,7 +405,7 @@ describe('ToolSchoolController (API)', () => { describe('[PUT] tools/school-external-tools/:schoolExternalToolId', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({ school }, [ Permission.SCHOOL_TOOL_ADMIN, @@ -545,7 +545,7 @@ describe('ToolSchoolController (API)', () => { describe('when schoolExternalToolId is given ', () => { const setup = async () => { - const school = schoolFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); const schoolToolId: string = new ObjectId().toHexString(); const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId( { school }, diff --git a/apps/server/src/modules/tool/school-external-tool/entity/school-external-tool.entity.spec.ts b/apps/server/src/modules/tool/school-external-tool/entity/school-external-tool.entity.spec.ts index 932ec713f54..93c99675d6e 100644 --- a/apps/server/src/modules/tool/school-external-tool/entity/school-external-tool.entity.spec.ts +++ b/apps/server/src/modules/tool/school-external-tool/entity/school-external-tool.entity.spec.ts @@ -2,13 +2,13 @@ import { basicToolConfigFactory, customParameterEntityFactory, externalToolEntityFactory, - schoolFactory, + schoolEntityFactory, setupEntities, } from '@shared/testing'; import { schoolExternalToolConfigurationStatusEntityFactory } from '@shared/testing/factory/school-external-tool-configuration-status-entity.factory'; import { schoolExternalToolEntityFactory } from '@shared/testing/factory/school-external-tool-entity.factory'; -import { CustomParameterEntity, ExternalToolEntity, ExternalToolConfigEntity } from '../../external-tool/entity'; import { CustomParameterLocation, CustomParameterScope, CustomParameterType, ToolConfigType } from '../../common/enum'; +import { CustomParameterEntity, ExternalToolConfigEntity, ExternalToolEntity } from '../../external-tool/entity'; import { SchoolExternalToolEntity } from './school-external-tool.entity'; describe('SchoolExternalToolEntity', () => { @@ -58,7 +58,7 @@ describe('SchoolExternalToolEntity', () => { }); const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ tool: externalToolEntity, - school: schoolFactory.buildWithId(), + school: schoolEntityFactory.buildWithId(), schoolParameters: [], toolVersion: 1, status: schoolExternalToolConfigurationStatusEntityFactory.build(), @@ -97,7 +97,7 @@ describe('SchoolExternalToolEntity', () => { }); const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ tool: externalToolEntity, - school: schoolFactory.buildWithId(), + school: schoolEntityFactory.buildWithId(), schoolParameters: [], toolVersion: 1, status: schoolExternalToolConfigurationStatusEntityFactory.build(), diff --git a/apps/server/src/modules/tool/tool-launch/controller/api-test/tool-launch.controller.api.spec.ts b/apps/server/src/modules/tool/tool-launch/controller/api-test/tool-launch.controller.api.spec.ts index d313cae5d50..07ed124baf8 100644 --- a/apps/server/src/modules/tool/tool-launch/controller/api-test/tool-launch.controller.api.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/controller/api-test/tool-launch.controller.api.spec.ts @@ -5,16 +5,16 @@ import { Test, TestingModule } from '@nestjs/testing'; import { Course, SchoolEntity } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { - TestApiClient, - UserAndAccountTestFactory, basicToolConfigFactory, contextExternalToolEntityFactory, contextExternalToolFactory, courseFactory, + customParameterFactory, externalToolEntityFactory, + schoolEntityFactory, schoolExternalToolEntityFactory, - schoolFactory, - customParameterFactory, + TestApiClient, + UserAndAccountTestFactory, } from '@shared/testing'; import { schoolExternalToolConfigurationStatusEntityFactory } from '@shared/testing/factory/school-external-tool-configuration-status-entity.factory'; import { Response } from 'supertest'; @@ -58,7 +58,7 @@ describe('ToolLaunchController (API)', () => { describe('[GET] tools/context/{contextExternalToolId}/launch', () => { describe('when valid data is given', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { teacherUser, teacherAccount } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.CONTEXT_TOOL_USER, ]); @@ -126,7 +126,7 @@ describe('ToolLaunchController (API)', () => { describe('when user wants to launch an outdated tool', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { teacherUser, teacherAccount } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.CONTEXT_TOOL_USER, ]); @@ -178,7 +178,7 @@ describe('ToolLaunchController (API)', () => { describe('when user wants to launch a deactivated tool', () => { describe('when external tool is deactivated', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { teacherUser, teacherAccount } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.CONTEXT_TOOL_USER, ]); @@ -230,7 +230,7 @@ describe('ToolLaunchController (API)', () => { describe('when school external tool is deactivated', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { teacherUser, teacherAccount } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.CONTEXT_TOOL_USER, ]); @@ -285,8 +285,8 @@ describe('ToolLaunchController (API)', () => { describe('when user wants to launch tool from another school', () => { const setup = async () => { - const toolSchool: SchoolEntity = schoolFactory.buildWithId(); - const usersSchool: SchoolEntity = schoolFactory.buildWithId(); + const toolSchool: SchoolEntity = schoolEntityFactory.buildWithId(); + const usersSchool: SchoolEntity = schoolEntityFactory.buildWithId(); const { teacherUser, teacherAccount } = UserAndAccountTestFactory.buildTeacher({ school: usersSchool }, [ Permission.CONTEXT_TOOL_USER, diff --git a/apps/server/src/modules/user-import/config/index.ts b/apps/server/src/modules/user-import/config/index.ts new file mode 100644 index 00000000000..3267752a160 --- /dev/null +++ b/apps/server/src/modules/user-import/config/index.ts @@ -0,0 +1 @@ +export { UserImportFeatures, UserImportConfiguration, IUserImportFeatures } from './user-import-config'; diff --git a/apps/server/src/modules/user-import/config/user-import-config.ts b/apps/server/src/modules/user-import/config/user-import-config.ts new file mode 100644 index 00000000000..c992280dd31 --- /dev/null +++ b/apps/server/src/modules/user-import/config/user-import-config.ts @@ -0,0 +1,15 @@ +import { Configuration } from '@hpi-schul-cloud/commons/lib'; + +export const UserImportFeatures = Symbol('UserImportFeatures'); + +export interface IUserImportFeatures { + userMigrationEnabled: boolean; + userMigrationSystemId: string; +} + +export class UserImportConfiguration { + static userImportFeatures: IUserImportFeatures = { + userMigrationEnabled: Configuration.get('FEATURE_USER_MIGRATION_ENABLED') as boolean, + userMigrationSystemId: Configuration.get('FEATURE_USER_MIGRATION_SYSTEM_ID') as string, + }; +} diff --git a/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts b/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts index 74fa884fd21..b8e0cfb6acd 100644 --- a/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts +++ b/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts @@ -1,7 +1,5 @@ -import { Configuration } from '@hpi-schul-cloud/commons'; +import { SanisResponse, schulconnexResponseFactory } from '@infra/schulconnex-client'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; -import { ICurrentUser } from '@modules/authentication'; -import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; import { ServerTestModule } from '@modules/server/server.module'; import { FilterImportUserParams, @@ -19,67 +17,75 @@ import { UserMatchResponse, UserRole, } from '@modules/user-import/controller/dto'; -import { ExecutionContext, INestApplication } from '@nestjs/common'; +import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { PaginationParams } from '@shared/controller'; -import { ImportUser, MatchCreator, SchoolEntity, SystemEntity, User } from '@shared/domain/entity'; +import { Account, ImportUser, MatchCreator, SchoolEntity, SystemEntity, User } from '@shared/domain/entity'; import { Permission, RoleName, SortOrder } from '@shared/domain/interface'; import { SchoolFeature } from '@shared/domain/types'; import { + accountFactory, cleanupCollections, importUserFactory, - mapUserToCurrentUser, roleFactory, - schoolFactory, + schoolEntityFactory, systemEntityFactory, + TestApiClient, + UserAndAccountTestFactory, userFactory, } from '@shared/testing'; -import { Request } from 'express'; -import request from 'supertest'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import { OauthTokenResponse } from '@modules/oauth/service/dto'; +import { IUserImportFeatures, UserImportFeatures } from '../../config'; describe('ImportUser Controller (API)', () => { let app: INestApplication; let em: EntityManager; - let currentUser: ICurrentUser; - const authenticatedUser = async (permissions: Permission[] = [], features: SchoolFeature[] = []) => { - const system = systemEntityFactory.buildWithId(); // TODO no id? - const school = schoolFactory.build({ officialSchoolNumber: 'foo', features }); - const roles = [roleFactory.build({ name: RoleName.ADMINISTRATOR, permissions })]; - await em.persistAndFlush([school, system, ...roles]); - const user = userFactory.build({ - school, - roles, + let testApiClient: TestApiClient; + let userImportFeatures: IUserImportFeatures; + let axiosMock: MockAdapter; + + const authenticatedUser = async ( + permissions: Permission[] = [], + features: SchoolFeature[] = [], + schoolHasExternalId = true + ) => { + const system = systemEntityFactory.buildWithId(); + const school = schoolEntityFactory.build({ + officialSchoolNumber: 'foo', + features, + systems: [system], + externalId: schoolHasExternalId ? system.id : undefined, }); - await em.persistAndFlush([user]); + const roles = [roleFactory.build({ name: RoleName.ADMINISTRATOR, permissions })]; + await em.persistAndFlush([system, school, ...roles]); + const user = userFactory.buildWithId({ roles, school }); + const account = accountFactory.withUser(user).buildWithId(); + await em.persistAndFlush([user, account]); em.clear(); - return { user, roles, school, system }; + return { user, account, roles, school, system }; }; const setConfig = (systemId?: string) => { - Configuration.set('FEATURE_USER_MIGRATION_ENABLED', true); - Configuration.set('FEATURE_USER_MIGRATION_SYSTEM_ID', systemId || new ObjectId().toString()); + userImportFeatures.userMigrationEnabled = true; + userImportFeatures.userMigrationSystemId = systemId || new ObjectId().toString(); }; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ServerTestModule], - }) - .overrideGuard(JwtAuthGuard) - .useValue({ - canActivate(context: ExecutionContext) { - const req: Request = context.switchToHttp().getRequest(); - req.user = currentUser; - return true; - }, - }) - - .compile(); + }).compile(); app = moduleFixture.createNestApplication(); + await app.init(); em = app.get(EntityManager); + testApiClient = new TestApiClient(app, 'user/import'); + userImportFeatures = app.get(UserImportFeatures); + axiosMock = new MockAdapter(axios); }); afterAll(async () => { @@ -104,273 +110,326 @@ describe('ImportUser Controller (API)', () => { describe('Generic Errors', () => { describe('When feature is not enabled', () => { - let user: User; + let account: Account; beforeEach(async () => { - ({ user } = await authenticatedUser([ + ({ account } = await authenticatedUser([ Permission.SCHOOL_IMPORT_USERS_MIGRATE, Permission.SCHOOL_IMPORT_USERS_UPDATE, Permission.SCHOOL_IMPORT_USERS_VIEW, ])); - currentUser = mapUserToCurrentUser(user); - Configuration.set('FEATURE_USER_MIGRATION_ENABLED', false); + testApiClient = await testApiClient.login(account); + userImportFeatures.userMigrationEnabled = false; + userImportFeatures.userMigrationSystemId = ''; }); + afterEach(() => { setConfig(); }); + it('System is not set', async () => { - Configuration.set('FEATURE_USER_MIGRATION_SYSTEM_ID', ''); - await request(app.getHttpServer()).get('/user/import').expect(500); + await testApiClient.get().expect(HttpStatus.FORBIDDEN); }); - it('GET /user/import is UNAUTHORIZED', async () => { - await request(app.getHttpServer()).get('/user/import').expect(500); + + it('GET /user/import is forbidden', async () => { + await testApiClient.get().expect(HttpStatus.FORBIDDEN); }); - it('GET /user/import/unassigned is UNAUTHORIZED', async () => { - await request(app.getHttpServer()).get('/user/import/unassigned').expect(500); + + it('GET /user/import/unassigned is forbidden', async () => { + await testApiClient.get('unassigned').expect(HttpStatus.FORBIDDEN); }); - it('PATCH /user/import/:id/match is UNAUTHORIZED', async () => { + + it('PATCH /user/import/:id/match is forbidden', async () => { const id = new ObjectId().toString(); const params: UpdateMatchParams = { userId: new ObjectId().toString() }; - await request(app.getHttpServer()).patch(`/user/import/${id}/match`).send(params).expect(500); + await testApiClient.patch(`${id}/match`).send(params).expect(HttpStatus.FORBIDDEN); }); - it('DELETE /user/import/:id/match is UNAUTHORIZED', async () => { + + it('DELETE /user/import/:id/match is forbidden', async () => { const id = new ObjectId().toString(); - await request(app.getHttpServer()).delete(`/user/import/${id}/match`).send().expect(500); + await testApiClient.delete(`${id}/match`).send().expect(HttpStatus.FORBIDDEN); }); - it('PATCH /user/import/:id/flag is UNAUTHORIZED', async () => { + + it('PATCH /user/import/:id/flag is forbidden', async () => { const id = new ObjectId().toString(); const params: UpdateFlagParams = { flagged: true }; - await request(app.getHttpServer()).patch(`/user/import/${id}/flag`).send(params).expect(500); + await testApiClient.patch(`${id}/flag`).send(params).expect(HttpStatus.FORBIDDEN); }); - it('POST /user/import/migrate is UNAUTHORIZED', async () => { - await request(app.getHttpServer()).post(`/user/import/migrate`).send().expect(500); + + it('POST /user/import/migrate is forbidden', async () => { + await testApiClient.post('migrate').send().expect(HttpStatus.FORBIDDEN); }); - it('POST /user/import/startSync is UNAUTHORIZED', async () => { - await request(app.getHttpServer()).post(`/user/import/startSync`).send().expect(500); + + it('POST /user/import/startSync is forbidden', async () => { + await testApiClient.post('startSync').send().expect(HttpStatus.FORBIDDEN); }); - it('POST /user/import/startUserMigration is UNAUTHORIZED', async () => { - await request(app.getHttpServer()).post(`/user/import/startUserMigration`).send().expect(500); + + it('POST /user/import/startUserMigration is forbidden', async () => { + await testApiClient.post('startUserMigration').send().expect(HttpStatus.FORBIDDEN); }); }); + describe('When authorization is missing', () => { - let user: User; + let account: Account; let system: SystemEntity; + beforeEach(async () => { - ({ user, system } = await authenticatedUser()); - currentUser = mapUserToCurrentUser(user); - Configuration.set('FEATURE_USER_MIGRATION_SYSTEM_ID', system._id.toString()); + ({ account, system } = await authenticatedUser()); + testApiClient = await testApiClient.login(account); + userImportFeatures.userMigrationSystemId = system._id.toString(); }); + it('GET /user/import is UNAUTHORIZED', async () => { - await request(app.getHttpServer()).get('/user/import').expect(401); + await testApiClient.get().expect(HttpStatus.UNAUTHORIZED); }); + it('GET /user/import/unassigned is UNAUTHORIZED', async () => { - await request(app.getHttpServer()).get('/user/import/unassigned').expect(401); + await testApiClient.get('unassigned').expect(HttpStatus.UNAUTHORIZED); }); + it('PATCH /user/import/:id/match is UNAUTHORIZED', async () => { const id = new ObjectId().toString(); const params: UpdateMatchParams = { userId: new ObjectId().toString() }; - await request(app.getHttpServer()).patch(`/user/import/${id}/match`).send(params).expect(401); + await testApiClient.patch(`${id}/match`).send(params).expect(HttpStatus.UNAUTHORIZED); }); + it('DELETE /user/import/:id/match is UNAUTHORIZED', async () => { const id = new ObjectId().toString(); - await request(app.getHttpServer()).delete(`/user/import/${id}/match`).send().expect(401); + await testApiClient.delete(`${id}/match`).send().expect(HttpStatus.UNAUTHORIZED); }); + it('PATCH /user/import/:id/flag is UNAUTHORIZED', async () => { const id = new ObjectId().toString(); const params: UpdateFlagParams = { flagged: true }; - await request(app.getHttpServer()).patch(`/user/import/${id}/flag`).send(params).expect(401); + await testApiClient.patch(`${id}/flag`).send(params).expect(HttpStatus.UNAUTHORIZED); }); + it('POST /user/import/migrate is UNAUTHORIZED', async () => { - await request(app.getHttpServer()).post(`/user/import/migrate`).send().expect(401); + await testApiClient.post('migrate').send().expect(HttpStatus.UNAUTHORIZED); }); + it('POST /user/import/startSync is UNAUTHORIZED', async () => { - await request(app.getHttpServer()).post(`/user/import/startSync`).send().expect(401); + await testApiClient.post('startSync').send().expect(HttpStatus.UNAUTHORIZED); }); + it('POST /user/import/startUserMigration is UNAUTHORIZED', async () => { - await request(app.getHttpServer()).post(`/user/import/startUserMigration`).send().expect(401); + await testApiClient.post('startUserMigration').send().expect(HttpStatus.UNAUTHORIZED); }); }); describe('When school is LDAP Migration Pilot School', () => { - let user: User; + let account: Account; let school: SchoolEntity; let system: SystemEntity; + beforeEach(async () => { - ({ school, system, user } = await authenticatedUser( + ({ school, system, account } = await authenticatedUser( [Permission.SCHOOL_IMPORT_USERS_VIEW], [SchoolFeature.LDAP_UNIVENTION_MIGRATION] )); - currentUser = mapUserToCurrentUser(user); - Configuration.set('FEATURE_USER_MIGRATION_SYSTEM_ID', system._id.toString()); - Configuration.set('FEATURE_USER_MIGRATION_ENABLED', false); + testApiClient = await testApiClient.login(account); + userImportFeatures.userMigrationSystemId = system._id.toString(); + userImportFeatures.userMigrationEnabled = false; }); + it('GET user/import is authorized, despite feature not enabled', async () => { const usermatch = userFactory.build({ school }); const importuser = importUserFactory.build({ school }); await em.persistAndFlush([usermatch, importuser]); - await request(app.getHttpServer()).get('/user/import').expect(200); + await testApiClient.get().expect(HttpStatus.OK); }); }); describe('When current user has permission Permission.SCHOOL_IMPORT_USERS_VIEW', () => { - let user: User; + let account: Account; let school: SchoolEntity; let system: SystemEntity; beforeEach(async () => { - ({ school, system, user } = await authenticatedUser([Permission.SCHOOL_IMPORT_USERS_VIEW])); - currentUser = mapUserToCurrentUser(user); - Configuration.set('FEATURE_USER_MIGRATION_SYSTEM_ID', system._id.toString()); + ({ school, system, account } = await authenticatedUser([Permission.SCHOOL_IMPORT_USERS_VIEW])); + testApiClient = await testApiClient.login(account); + userImportFeatures.userMigrationSystemId = system._id.toString(); }); it('GET /user/import responds with importusers', async () => { const usermatch = userFactory.build({ school }); const importuser = importUserFactory.build({ school }); await em.persistAndFlush([usermatch, importuser]); - await request(app.getHttpServer()).get('/user/import').expect(200); + await testApiClient.get().expect(HttpStatus.OK); }); + it('GET /user/import/unassigned is UNAUTHORIZED', async () => { - await request(app.getHttpServer()).get('/user/import/unassigned').expect(200); + await testApiClient.get('unassigned').expect(HttpStatus.OK); }); + it('PATCH /user/import/:id/match is UNAUTHORIZED', async () => { const id = new ObjectId().toString(); const params: UpdateMatchParams = { userId: new ObjectId().toString() }; - await request(app.getHttpServer()).patch(`/user/import/${id}/match`).send(params).expect(401); + await testApiClient.patch(`${id}/match`).send(params).expect(HttpStatus.UNAUTHORIZED); }); + it('DELETE /user/import/:id/match is UNAUTHORIZED', async () => { const id = new ObjectId().toString(); - await request(app.getHttpServer()).delete(`/user/import/${id}/match`).send().expect(401); + await testApiClient.delete(`${id}/match`).send().expect(HttpStatus.UNAUTHORIZED); }); + it('PATCH /user/import/:id/flag is UNAUTHORIZED', async () => { const id = new ObjectId().toString(); const params: UpdateFlagParams = { flagged: true }; - await request(app.getHttpServer()).patch(`/user/import/${id}/flag`).send(params).expect(401); + await testApiClient.patch(`${id}/flag`).send(params).expect(HttpStatus.UNAUTHORIZED); }); + it('POST /user/import/migrate is UNAUTHORIZED', async () => { - await request(app.getHttpServer()).post(`/user/import/migrate`).send().expect(401); + await testApiClient.post('migrate').send().expect(HttpStatus.UNAUTHORIZED); }); + it('POST /user/import/startSync is UNAUTHORIZED', async () => { - await request(app.getHttpServer()).post(`/user/import/startSync`).send().expect(401); + await testApiClient.post('startSync').send().expect(HttpStatus.UNAUTHORIZED); }); + it('POST /user/import/startUserMigration is UNAUTHORIZED', async () => { - await request(app.getHttpServer()).post(`/user/import/startUserMigration`).send().expect(401); + await testApiClient.post('startUserMigration').send().expect(HttpStatus.UNAUTHORIZED); }); }); + describe('When current user has permission Permission.SCHOOL_IMPORT_USERS_UPDATE', () => { + let account: Account; let user: User; let school: SchoolEntity; let system: SystemEntity; + beforeEach(async () => { - ({ user, school, system } = await authenticatedUser([Permission.SCHOOL_IMPORT_USERS_UPDATE])); - currentUser = mapUserToCurrentUser(user); + ({ account, school, system, user } = await authenticatedUser([Permission.SCHOOL_IMPORT_USERS_UPDATE])); + testApiClient = await testApiClient.login(account); setConfig(system._id.toString()); }); + it('GET /user/import is UNAUTHORIZED', async () => { const usermatch = userFactory.build({ school }); const importuser = importUserFactory.build({ school }); await em.persistAndFlush([usermatch, importuser]); em.clear(); - await request(app.getHttpServer()).get('/user/import').expect(401); + await testApiClient.get().expect(HttpStatus.UNAUTHORIZED); }); + it('GET /user/import/unassigned is UNAUTHORIZED', async () => { - await request(app.getHttpServer()).get('/user/import/unassigned').expect(401); + await testApiClient.get('unassigned').expect(HttpStatus.UNAUTHORIZED); }); + it('PATCH /user/import/:id/match is allowed', async () => { const userMatch = userFactory.build({ school }); const importUser = importUserFactory.build({ school }); await em.persistAndFlush([userMatch, importUser]); em.clear(); const params: UpdateMatchParams = { userId: user.id }; - await request(app.getHttpServer()).patch(`/user/import/${importUser.id}/match`).send(params).expect(200); + await testApiClient.patch(`${importUser.id}/match`).send(params).expect(HttpStatus.OK); }); + it('DELETE /user/import/:id/match is allowed', async () => { const userMatch = userFactory.build({ school }); const importUser = importUserFactory.matched(MatchCreator.AUTO, userMatch).build({ school }); await em.persistAndFlush([userMatch, importUser]); em.clear(); - await request(app.getHttpServer()).delete(`/user/import/${importUser.id}/match`).send().expect(200); + await testApiClient.delete(`${importUser.id}/match`).send().expect(HttpStatus.OK); }); + it('PATCH /user/import/:id/flag is allowed', async () => { const importUser = importUserFactory.build({ school }); await em.persistAndFlush(importUser); em.clear(); const params: UpdateFlagParams = { flagged: true }; - await request(app.getHttpServer()).patch(`/user/import/${importUser.id}/flag`).send(params).expect(200); + await testApiClient.patch(`${importUser.id}/flag`).send(params).expect(HttpStatus.OK); }); + it('POST /user/import/migrate is UNAUTHORIZED', async () => { - await request(app.getHttpServer()).post(`/user/import/migrate`).send().expect(401); + await testApiClient.post('migrate').send().expect(HttpStatus.UNAUTHORIZED); }); + it('POST /user/import/startSync is UNAUTHORIZED', async () => { - await request(app.getHttpServer()).post(`/user/import/startSync`).send().expect(401); + await testApiClient.post('startSync').send().expect(HttpStatus.UNAUTHORIZED); }); + it('POST /user/import/startUserMigration is UNAUTHORIZED', async () => { - await request(app.getHttpServer()).post(`/user/import/startUserMigration`).send().expect(401); + await testApiClient.post('startUserMigration').send().expect(HttpStatus.UNAUTHORIZED); }); }); + describe('When current user has permissions Permission.SCHOOL_IMPORT_USERS_MIGRATE', () => { - let user: User; + let account: Account; let system: SystemEntity; + beforeEach(async () => { - ({ user, system } = await authenticatedUser()); - currentUser = mapUserToCurrentUser(user); + ({ account, system } = await authenticatedUser()); + testApiClient = await testApiClient.login(account); setConfig(system._id.toString()); }); + it('GET /user/import is UNAUTHORIZED', async () => { - await request(app.getHttpServer()).get('/user/import').expect(401); + await testApiClient.get().expect(HttpStatus.UNAUTHORIZED); }); + it('GET /user/import/unassigned is UNAUTHORIZED', async () => { - await request(app.getHttpServer()).get('/user/import/unassigned').expect(401); + await testApiClient.get('unassigned').expect(HttpStatus.UNAUTHORIZED); }); + it('PATCH /user/import/:id/match is UNAUTHORIZED', async () => { const id = new ObjectId().toString(); const params: UpdateMatchParams = { userId: new ObjectId().toString() }; - await request(app.getHttpServer()).patch(`/user/import/${id}/match`).send(params).expect(401); + await testApiClient.patch(`${id}/match`).send(params).expect(HttpStatus.UNAUTHORIZED); }); + it('DELETE /user/import/:id/match is UNAUTHORIZED', async () => { const id = new ObjectId().toString(); - await request(app.getHttpServer()).delete(`/user/import/${id}/match`).send().expect(401); + await testApiClient.delete(`${id}/match`).send().expect(HttpStatus.UNAUTHORIZED); }); + it('PATCH /user/import/:id/flag is UNAUTHORIZED', async () => { const id = new ObjectId().toString(); const params: UpdateFlagParams = { flagged: true }; - await request(app.getHttpServer()).patch(`/user/import/${id}/flag`).send(params).expect(401); + await testApiClient.patch(`${id}/flag`).send(params).expect(HttpStatus.UNAUTHORIZED); }); + it('POST /user/import/migrate is UNAUTHORIZED', async () => { - await request(app.getHttpServer()).post(`/user/import/migrate`).send().expect(401); + await testApiClient.post('migrate').send().expect(HttpStatus.UNAUTHORIZED); }); + it('POST /user/import/startSync is UNAUTHORIZED', async () => { - await request(app.getHttpServer()).post(`/user/import/startSync`).send().expect(401); + await testApiClient.post('startSync').send().expect(HttpStatus.UNAUTHORIZED); }); + it('POST /user/import/startUserMigration is UNAUTHORIZED', async () => { - await request(app.getHttpServer()).post(`/user/import/startUserMigration`).send().expect(401); + await testApiClient.post('startUserMigration').send().expect(HttpStatus.UNAUTHORIZED); }); }); }); describe('Business Errors', () => { - let user: User; + let account: Account; let school: SchoolEntity; + beforeEach(async () => { - ({ user, school } = await authenticatedUser([Permission.SCHOOL_IMPORT_USERS_UPDATE])); - currentUser = mapUserToCurrentUser(user); + ({ account, school } = await authenticatedUser([Permission.SCHOOL_IMPORT_USERS_UPDATE])); + testApiClient = await testApiClient.login(account); }); + describe('[setMatch]', () => { describe('When set a match on import user', () => { it('should fail for different school of match- and import-user', async () => { const importUser = importUserFactory.build({ school }); - const otherSchool = schoolFactory.build(); + const otherSchool = schoolEntityFactory.build(); const userMatch = userFactory.build({ school: otherSchool }); await em.persistAndFlush([userMatch, importUser]); em.clear(); const params: UpdateMatchParams = { userId: userMatch.id }; - await request(app.getHttpServer()).patch(`/user/import/${importUser.id}/match`).send(params).expect(403); + await testApiClient.patch(`${importUser.id}/match`).send(params).expect(HttpStatus.FORBIDDEN); }); + it('should fail for different school of current-/authenticated- and import-user', async () => { - const otherSchool = schoolFactory.build(); + const otherSchool = schoolEntityFactory.build(); const importUser = importUserFactory.build({ school: otherSchool }); const userMatch = userFactory.build({ school: otherSchool }); await em.persistAndFlush([userMatch, importUser]); em.clear(); const params: UpdateMatchParams = { userId: userMatch.id }; - await request(app.getHttpServer()).patch(`/user/import/${importUser.id}/match`).send(params).expect(403); + await testApiClient.patch(`${importUser.id}/match`).send(params).expect(HttpStatus.FORBIDDEN); }); }); @@ -382,33 +441,31 @@ describe('ImportUser Controller (API)', () => { await em.persistAndFlush([userMatch, importUser]); em.clear(); const params: UpdateMatchParams = { userId: userMatch.id }; - await request(app.getHttpServer()) - .patch(`/user/import/${unmatchedImportUser.id}/match`) - .send(params) - .expect(400); + await testApiClient.patch(`${unmatchedImportUser.id}/match`).send(params).expect(HttpStatus.BAD_REQUEST); }); }); }); + describe('[removeMatch]', () => { describe('When remove a match on import user', () => { it('should fail for different school of current- and import-user', async () => { - const otherSchool = schoolFactory.build(); + const otherSchool = schoolEntityFactory.build(); const importUser = importUserFactory.build({ school: otherSchool }); await em.persistAndFlush(importUser); em.clear(); - await request(app.getHttpServer()).delete(`/user/import/${importUser.id}/match`).send().expect(403); + await testApiClient.delete(`${importUser.id}/match`).send().expect(HttpStatus.FORBIDDEN); }); }); }); describe('[updateFlag]', () => { describe('When change flag on import user', () => { it('should fail for different school of current- and import-user', async () => { - const otherSchool = schoolFactory.build(); + const otherSchool = schoolEntityFactory.build(); const importUser = importUserFactory.build({ school: otherSchool }); await em.persistAndFlush(importUser); em.clear(); const params: UpdateFlagParams = { flagged: true }; - await request(app.getHttpServer()).patch(`/user/import/${importUser.id}/flag`).send(params).expect(403); + await testApiClient.patch(`${importUser.id}/flag`).send(params).expect(HttpStatus.FORBIDDEN); }); }); }); @@ -427,6 +484,7 @@ describe('ImportUser Controller (API)', () => { expect(['admin', 'auto']).toContain(match?.matchedBy); } }; + const expectAllImportUserResponsePropertiesExist = (data: ImportUserResponse, matchExists: boolean) => { expect(data).toEqual( expect.objectContaining({ @@ -447,15 +505,17 @@ describe('ImportUser Controller (API)', () => { expect(data.match).toBeUndefined(); } }; + describe('find', () => { - let user: User; + let account: Account; let school: SchoolEntity; + beforeEach(async () => { await cleanupCollections(em); - ({ user, school } = await authenticatedUser([Permission.SCHOOL_IMPORT_USERS_VIEW])); + ({ account, school } = await authenticatedUser([Permission.SCHOOL_IMPORT_USERS_VIEW])); - currentUser = mapUserToCurrentUser(user); + testApiClient = await testApiClient.login(account); }); describe('[findAllUnmatchedUsers]', () => { @@ -465,27 +525,35 @@ describe('ImportUser Controller (API)', () => { const currentSchoolsUser = userFactory.build({ school }); await em.persistAndFlush([otherSchoolsUser, currentSchoolsUser]); em.clear(); - const response = await request(app.getHttpServer()).get('/user/import/unassigned').expect(200); + + const response = await testApiClient.get('unassigned').expect(HttpStatus.OK); + const listResponse = response.body as UserMatchListResponse; expect(listResponse.data.some((elem) => elem.userId === currentSchoolsUser.id)).toEqual(true); }); + it('should not respond with assigned users', async () => { const otherSchoolsUser = userFactory.build(); const currentSchoolsUser = userFactory.build({ school }); const importUser = importUserFactory.matched(MatchCreator.AUTO, currentSchoolsUser).build({ school }); await em.persistAndFlush([otherSchoolsUser, currentSchoolsUser, importUser]); em.clear(); - const response = await request(app.getHttpServer()).get('/user/import/unassigned').expect(200); + + const response = await testApiClient.get('unassigned').expect(HttpStatus.OK); + const listResponse = response.body as UserMatchListResponse; expect(listResponse.data.some((elem) => elem.userId === currentSchoolsUser.id)).toEqual(false); }); + it('should respond userMatch with all properties', async () => { const currentSchoolsUser = userFactory.withRoleByName(RoleName.TEACHER).build({ school, }); await em.persistAndFlush([currentSchoolsUser]); em.clear(); - const response = await request(app.getHttpServer()).get('/user/import/unassigned').expect(200); + + const response = await testApiClient.get('unassigned').expect(HttpStatus.OK); + const listResponse = response.body as UserMatchListResponse; expectAllUserMatchResponsePropertiesExist(listResponse.data[0], false); }); @@ -495,22 +563,21 @@ describe('ImportUser Controller (API)', () => { const unassignedUsers = userFactory.buildList(10, { school }); await em.persistAndFlush(unassignedUsers); const query: PaginationParams = { skip: 3 }; - const response = await request(app.getHttpServer()) - .get('/user/import/unassigned') - .query(query) - .expect(200); + + const response = await testApiClient.get('unassigned').query(query).expect(HttpStatus.OK); + const result = response.body as UserMatchListResponse; expect(result.total).toBeGreaterThanOrEqual(10); expect(result.data.length).toBeGreaterThanOrEqual(7); }); + it('should limit users', async () => { const unassignedUsers = userFactory.buildList(10, { school }); await em.persistAndFlush(unassignedUsers); const query: PaginationParams = { limit: 3 }; - const response = await request(app.getHttpServer()) - .get('/user/import/unassigned') - .query(query) - .expect(200); + + const response = await testApiClient.get('unassigned').query(query).expect(HttpStatus.OK); + const result = response.body as UserMatchListResponse; expect(result.total).toBeGreaterThanOrEqual(10); expect(result.data).toHaveLength(3); @@ -524,24 +591,23 @@ describe('ImportUser Controller (API)', () => { users.push(searchUser); await em.persistAndFlush(users); const query: FilterUserParams = { name: 'ETE' }; - const response = await request(app.getHttpServer()) - .get('/user/import/unassigned') - .query(query) - .expect(200); + + const response = await testApiClient.get('unassigned').query(query).expect(HttpStatus.OK); + const result = response.body as UserMatchListResponse; expect(result.total).toEqual(1); expect(result.data.some((u) => u.userId === searchUser.id)).toEqual(true); }); + it('should match name in lastname', async () => { const users = userFactory.buildList(10, { school }); const searchUser = userFactory.build({ school, firstName: 'Peter', lastName: 'fox' }); users.push(searchUser); await em.persistAndFlush(users); const query: FilterUserParams = { name: 'X' }; - const response = await request(app.getHttpServer()) - .get('/user/import/unassigned') - .query(query) - .expect(200); + + const response = await testApiClient.get('unassigned').query(query).expect(HttpStatus.OK); + const result = response.body as UserMatchListResponse; expect(result.total).toEqual(1); expect(result.data.some((u) => u.userId === searchUser.id)).toEqual(true); @@ -549,6 +615,7 @@ describe('ImportUser Controller (API)', () => { }); }); }); + describe('[findAllImportUsers]', () => { it('should return importUsers of current school', async () => { const otherSchoolsImportUser = importUserFactory.build(); @@ -557,19 +624,24 @@ describe('ImportUser Controller (API)', () => { }); await em.persistAndFlush([otherSchoolsImportUser, currentSchoolsImportUser]); em.clear(); - const response = await request(app.getHttpServer()).get('/user/import').expect(200); + + const response = await testApiClient.get().expect(HttpStatus.OK); + const listResponse = response.body as ImportUserListResponse; expect(listResponse.data.some((elem) => elem.importUserId === currentSchoolsImportUser.id)).toEqual(true); expect(listResponse.data.some((elem) => elem.importUserId === otherSchoolsImportUser.id)).toEqual(false); expectAllImportUserResponsePropertiesExist(listResponse.data[0], false); }); + it('should return importUsers with all properties including match and roles', async () => { const otherSchoolsImportUser = importUserFactory.build(); const userMatch = userFactory.withRoleByName(RoleName.TEACHER).build({ school }); const currentSchoolsImportUser = importUserFactory.matched(MatchCreator.AUTO, userMatch).build({ school }); await em.persistAndFlush([otherSchoolsImportUser, currentSchoolsImportUser]); em.clear(); - const response = await request(app.getHttpServer()).get('/user/import').expect(200); + + const response = await testApiClient.get().expect(HttpStatus.OK); + const listResponse = response.body as ImportUserListResponse; expect(listResponse.data.some((elem) => elem.importUserId === currentSchoolsImportUser.id)).toEqual(true); expect(listResponse.data.some((elem) => elem.importUserId === otherSchoolsImportUser.id)).toEqual(false); @@ -589,12 +661,15 @@ describe('ImportUser Controller (API)', () => { sortBy: ImportUserSortOrder.FIRSTNAME, sortOrder: SortOrder.asc, }; - const response = await request(app.getHttpServer()).get('/user/import').query(query).expect(200); + + const response = await testApiClient.get().query(query).expect(HttpStatus.OK); + const listResponse = response.body as ImportUserListResponse; const smallIndex = listResponse.data.findIndex((elem) => elem.firstName === 'Anne'); const higherIndex = listResponse.data.findIndex((elem) => elem.firstName === 'Zoe'); expect(smallIndex).toBeLessThan(higherIndex); }); + it('should sort by firstname desc', async () => { const currentSchoolsImportUsers = importUserFactory.buildList(10, { school, @@ -607,12 +682,15 @@ describe('ImportUser Controller (API)', () => { sortBy: ImportUserSortOrder.FIRSTNAME, sortOrder: SortOrder.desc, }; - const response = await request(app.getHttpServer()).get('/user/import').query(query).expect(200); + + const response = await testApiClient.get().query(query).expect(HttpStatus.OK); + const listResponse = response.body as ImportUserListResponse; const smallIndex = listResponse.data.findIndex((elem) => elem.firstName === 'Zoe'); const higherIndex = listResponse.data.findIndex((elem) => elem.firstName === 'Anne'); expect(smallIndex).toBeLessThan(higherIndex); }); + it('should sort by lastname asc', async () => { const currentSchoolsImportUsers = importUserFactory.buildList(10, { school, @@ -625,12 +703,15 @@ describe('ImportUser Controller (API)', () => { sortBy: ImportUserSortOrder.LASTNAME, sortOrder: SortOrder.asc, }; - const response = await request(app.getHttpServer()).get('/user/import').query(query).expect(200); + + const response = await testApiClient.get().query(query).expect(HttpStatus.OK); + const listResponse = response.body as ImportUserListResponse; const smallIndex = listResponse.data.findIndex((elem) => elem.lastName === 'Müller'); const higherIndex = listResponse.data.findIndex((elem) => elem.lastName === 'Schmidt'); expect(smallIndex).toBeLessThan(higherIndex); }); + it('should sort by lastname desc', async () => { const currentSchoolsImportUsers = importUserFactory.buildList(10, { school, @@ -643,28 +724,36 @@ describe('ImportUser Controller (API)', () => { sortBy: ImportUserSortOrder.LASTNAME, sortOrder: SortOrder.desc, }; - const response = await request(app.getHttpServer()).get('/user/import').query(query).expect(200); + + const response = await testApiClient.get().query(query).expect(HttpStatus.OK); + const listResponse = response.body as ImportUserListResponse; const smallIndex = listResponse.data.findIndex((elem) => elem.lastName === 'Schmidt'); const higherIndex = listResponse.data.findIndex((elem) => elem.lastName === 'Müller'); expect(smallIndex).toBeLessThan(higherIndex); }); }); + describe('when use pagination', () => { it('should skip importusers', async () => { const importUsers = importUserFactory.buildList(10, { school }); await em.persistAndFlush(importUsers); const query: PaginationParams = { skip: 3 }; - const response = await request(app.getHttpServer()).get('/user/import').query(query).expect(200); + + const response = await testApiClient.get().query(query).expect(HttpStatus.OK); + const result = response.body as ImportUserListResponse; expect(result.total).toBeGreaterThanOrEqual(10); expect(result.data.length).toBeGreaterThanOrEqual(7); }); + it('should limit importusers', async () => { const importUsers = importUserFactory.buildList(10, { school }); await em.persistAndFlush(importUsers); const query: PaginationParams = { limit: 3 }; - const response = await request(app.getHttpServer()).get('/user/import').query(query).expect(200); + + const response = await testApiClient.get().query(query).expect(HttpStatus.OK); + const result = response.body as ImportUserListResponse; expect(result.total).toBeGreaterThanOrEqual(10); expect(result.data).toHaveLength(3); @@ -677,62 +766,80 @@ describe('ImportUser Controller (API)', () => { importUsers[0].firstName = 'Klaus-Peter'; await em.persistAndFlush(importUsers); const query: FilterImportUserParams = { firstName: 's-p' }; - const response = await request(app.getHttpServer()).get('/user/import').query(query).expect(200); + + const response = await testApiClient.get().query(query).expect(HttpStatus.OK); + const result = response.body as ImportUserListResponse; expect(result.data.length).toEqual(1); expect(result.data[0].firstName).toEqual('Klaus-Peter'); }); + it('should filter by lastname', async () => { const importUsers = importUserFactory.buildList(10, { school }); importUsers[0].lastName = 'Weimann'; await em.persistAndFlush(importUsers); const query: FilterImportUserParams = { lastName: 'Mann' }; - const response = await request(app.getHttpServer()).get('/user/import').query(query).expect(200); + + const response = await testApiClient.get().query(query).expect(HttpStatus.OK); + const result = response.body as ImportUserListResponse; expect(result.data.length).toEqual(1); expect(result.data[0].lastName).toEqual('Weimann'); }); + it('should filter by username', async () => { const importUsers = importUserFactory.buildList(10, { school }); importUsers[0].ldapDn = 'uid=EinarWeimann12,...'; await em.persistAndFlush(importUsers); const query: FilterImportUserParams = { loginName: 'Mann1' }; - const response = await request(app.getHttpServer()).get('/user/import').query(query).expect(200); + + const response = await testApiClient.get().query(query).expect(HttpStatus.OK); + const result = response.body as ImportUserListResponse; expect(result.data.length).toEqual(1); expect(result.data[0].loginName).toEqual('EinarWeimann12'); }); + it('should filter by one role of student, teacher, or admin', async () => { const importUsers = importUserFactory.buildList(10, { school }); importUsers[0].roleNames = [RoleName.TEACHER]; await em.persistAndFlush(importUsers); const query: FilterImportUserParams = { role: FilterRoleType.TEACHER }; - const response = await request(app.getHttpServer()).get('/user/import').query(query).expect(200); + + const response = await testApiClient.get().query(query).expect(HttpStatus.OK); + const result = response.body as ImportUserListResponse; expect(result.data.length).toEqual(1); expect(result.data[0].roleNames).toContain(UserRole.TEACHER); }); + it('should filter by class', async () => { const importUsers = importUserFactory.buildList(10, { school }); importUsers[0].classNames = ['class1', 'second']; await em.persistAndFlush(importUsers); const query: FilterImportUserParams = { classes: 'ss1' }; - const response = await request(app.getHttpServer()).get('/user/import').query(query).expect(200); + + const response = await testApiClient.get().query(query).expect(HttpStatus.OK); + const result = response.body as ImportUserListResponse; expect(result.data.length).toEqual(1); expect(result.data[0].classNames).toContain('class1'); }); + it('should filter by match type none', async () => { const importUsers = importUserFactory.buildList(10, { school }); importUsers[0].setMatch(userFactory.build({ school }), MatchCreator.AUTO); await em.persistAndFlush(importUsers); const query: FilterImportUserParams = { match: [FilterMatchType.NONE] }; - const response = await request(app.getHttpServer()).get('/user/import').query(query).expect(200); + + const response = await testApiClient.get().query(query).expect(HttpStatus.OK); + const result = response.body as ImportUserListResponse; expect(result.data.some((iu) => iu.match?.matchedBy !== MatchType.AUTO)).toEqual(true); expect(result.data.some((iu) => iu.match?.matchedBy !== MatchType.MANUAL)).toEqual(true); expect(result.data.length).toEqual(9); }); + it('should filter by match type none also deleted matches', async () => { const importUsers = importUserFactory.buildList(10, { school }); importUsers[0].setMatch(userFactory.build({ school }), MatchCreator.AUTO); @@ -740,52 +847,66 @@ describe('ImportUser Controller (API)', () => { importUsers[0].revokeMatch(); await em.persistAndFlush(importUsers); const query: FilterImportUserParams = { match: [FilterMatchType.NONE] }; - const response = await request(app.getHttpServer()).get('/user/import').query(query).expect(200); + + const response = await testApiClient.get().query(query).expect(HttpStatus.OK); + const result = response.body as ImportUserListResponse; expect(result.data.some((iu) => iu.match?.matchedBy !== MatchType.AUTO)).toEqual(true); expect(result.data.some((iu) => iu.match?.matchedBy !== MatchType.MANUAL)).toEqual(true); expect(result.data.length).toEqual(10); }); + it('should filter by match type admin (manual)', async () => { const importUsers = importUserFactory.buildList(10, { school }); importUsers[0].setMatch(userFactory.build({ school }), MatchCreator.MANUAL); await em.persistAndFlush(importUsers); const query: FilterImportUserParams = { match: [FilterMatchType.MANUAL] }; - const response = await request(app.getHttpServer()).get('/user/import').query(query).expect(200); + + const response = await testApiClient.get().query(query).expect(HttpStatus.OK); + const result = response.body as ImportUserListResponse; expect(result.data.some((iu) => iu.match?.matchedBy === MatchType.MANUAL)).toEqual(true); expect(result.data.some((iu) => iu.match?.matchedBy !== MatchType.MANUAL)).toEqual(false); expect(result.data.length).toEqual(1); }); + it('should filter by match type auto', async () => { const importUsers = importUserFactory.buildList(10, { school }); importUsers[0].setMatch(userFactory.build({ school }), MatchCreator.AUTO); await em.persistAndFlush(importUsers); const query: FilterImportUserParams = { match: [FilterMatchType.AUTO] }; - const response = await request(app.getHttpServer()).get('/user/import').query(query).expect(200); + + const response = await testApiClient.get().query(query).expect(HttpStatus.OK); + const result = response.body as ImportUserListResponse; expect(result.data.some((iu) => iu.match?.matchedBy === MatchType.AUTO)).toEqual(true); expect(result.data.some((iu) => iu.match?.matchedBy !== MatchType.AUTO)).toEqual(false); expect(result.data.length).toEqual(1); }); + it('should filter by multiple match types', async () => { const importUsers = importUserFactory.buildList(10, { school }); importUsers[0].setMatch(userFactory.build({ school }), MatchCreator.MANUAL); importUsers[1].setMatch(userFactory.build({ school }), MatchCreator.AUTO); await em.persistAndFlush(importUsers); const query: FilterImportUserParams = { match: [FilterMatchType.AUTO, FilterMatchType.MANUAL] }; - const response = await request(app.getHttpServer()).get('/user/import').query(query).expect(200); + + const response = await testApiClient.get().query(query).expect(HttpStatus.OK); + const result = response.body as ImportUserListResponse; expect(result.data.some((iu) => iu.match?.matchedBy === MatchType.MANUAL)).toEqual(true); expect(result.data.some((iu) => iu.match?.matchedBy === MatchType.AUTO)).toEqual(true); expect(result.data.length).toEqual(2); }); + it('should filter by flag enabled', async () => { const importUsers = importUserFactory.buildList(10, { school }); importUsers[0].flagged = true; await em.persistAndFlush(importUsers); const query: FilterImportUserParams = { flagged: true }; - const response = await request(app.getHttpServer()).get('/user/import').query(query).expect(200); + + const response = await testApiClient.get().query(query).expect(HttpStatus.OK); + const result = response.body as ImportUserListResponse; expect(result.data.some((iu) => iu.flagged === false)).toEqual(false); expect(result.data.some((iu) => iu.flagged === true)).toEqual(true); @@ -796,11 +917,12 @@ describe('ImportUser Controller (API)', () => { }); describe('updates', () => { - let user: User; + let account: Account; let school: SchoolEntity; + beforeEach(async () => { - ({ user, school } = await authenticatedUser([Permission.SCHOOL_IMPORT_USERS_UPDATE])); - currentUser = mapUserToCurrentUser(user); + ({ account, school } = await authenticatedUser([Permission.SCHOOL_IMPORT_USERS_UPDATE])); + testApiClient = await testApiClient.login(account); }); describe('[setMatch]', () => { @@ -815,15 +937,18 @@ describe('ImportUser Controller (API)', () => { await em.persistAndFlush([userToBeMatched, unmatchedImportUser]); em.clear(); const params: UpdateMatchParams = { userId: userToBeMatched.id }; - const result = await request(app.getHttpServer()) - .patch(`/user/import/${unmatchedImportUser.id}/match`) + + const result = await testApiClient + .patch(`${unmatchedImportUser.id}/match`) .send(params) - .expect(200); + .expect(HttpStatus.OK); + const importUserResponse = result.body as ImportUserResponse; expectAllImportUserResponsePropertiesExist(importUserResponse, true); expect(importUserResponse.match?.matchedBy).toEqual(MatchType.MANUAL); expect(importUserResponse.match?.userId).toEqual(userToBeMatched.id); }); + it('should update an existing auto match to manual', async () => { const userMatch = userFactory.withRoleByName(RoleName.STUDENT).build({ school, @@ -837,10 +962,12 @@ describe('ImportUser Controller (API)', () => { await em.persistAndFlush([userMatch, alreadyMatchedImportUser, manualUserMatch]); em.clear(); const params: UpdateMatchParams = { userId: manualUserMatch.id }; - const result = await request(app.getHttpServer()) - .patch(`/user/import/${alreadyMatchedImportUser.id}/match`) + + const result = await testApiClient + .patch(`${alreadyMatchedImportUser.id}/match`) .send(params) - .expect(200); + .expect(HttpStatus.OK); + const elem = result.body as ImportUserResponse; expectAllImportUserResponsePropertiesExist(elem, true); expect(elem.match?.matchedBy).toEqual(MatchType.MANUAL); @@ -860,20 +987,24 @@ describe('ImportUser Controller (API)', () => { }); await em.persistAndFlush([importUserWithMatch]); em.clear(); - const result = await request(app.getHttpServer()) - .delete(`/user/import/${importUserWithMatch.id}/match`) - .expect(200); + + const result = await testApiClient.delete(`${importUserWithMatch.id}/match`).send().expect(HttpStatus.OK); + expectAllImportUserResponsePropertiesExist(result.body as ImportUserResponse, false); }); + it('should not fail when importuser is not having a match', async () => { const importUserWithoutMatch = importUserFactory.build({ school, }); await em.persistAndFlush([importUserWithoutMatch]); em.clear(); - const result = await request(app.getHttpServer()) - .delete(`/user/import/${importUserWithoutMatch.id}/match`) - .expect(200); + + const result = await testApiClient + .delete(`${importUserWithoutMatch.id}/match`) + .send() + .expect(HttpStatus.OK); + expectAllImportUserResponsePropertiesExist(result.body as ImportUserResponse, false); }); }); @@ -888,14 +1019,14 @@ describe('ImportUser Controller (API)', () => { await em.persistAndFlush([importUser]); em.clear(); const params: UpdateFlagParams = { flagged: true }; - const result = await request(app.getHttpServer()) - .patch(`/user/import/${importUser.id}/flag`) - .send(params) - .expect(200); + + const result = await testApiClient.patch(`${importUser.id}/flag`).send(params).expect(HttpStatus.OK); + const response = result.body as ImportUserResponse; expectAllImportUserResponsePropertiesExist(response, false); expect(response.flagged).toEqual(true); }); + it('should remove a flag', async () => { const importUser = importUserFactory.build({ school, @@ -904,10 +1035,9 @@ describe('ImportUser Controller (API)', () => { await em.persistAndFlush([importUser]); em.clear(); const params: UpdateFlagParams = { flagged: false }; - const result = await request(app.getHttpServer()) - .patch(`/user/import/${importUser.id}/flag`) - .send(params) - .expect(200); + + const result = await testApiClient.patch(`${importUser.id}/flag`).send(params).expect(HttpStatus.OK); + const response = result.body as ImportUserResponse; expectAllImportUserResponsePropertiesExist(response, false); expect(response.flagged).toEqual(false); @@ -917,16 +1047,18 @@ describe('ImportUser Controller (API)', () => { }); describe('[migrate]', () => { - let user: User; + let account: Account; let school: SchoolEntity; + beforeEach(async () => { - ({ user, school } = await authenticatedUser([Permission.SCHOOL_IMPORT_USERS_MIGRATE])); + ({ account, school } = await authenticatedUser([Permission.SCHOOL_IMPORT_USERS_MIGRATE])); school.officialSchoolNumber = 'foo'; school.inMaintenanceSince = new Date(); school.externalId = 'foo'; school.inUserMigration = true; - currentUser = mapUserToCurrentUser(user); + testApiClient = await testApiClient.login(account); }); + describe('POST user/import/migrate', () => { it('should migrate', async () => { school.officialSchoolNumber = 'foo'; @@ -937,21 +1069,22 @@ describe('ImportUser Controller (API)', () => { await em.persistAndFlush([importUser]); em.clear(); - await request(app.getHttpServer()).post(`/user/import/migrate`).expect(201); + await testApiClient.post('migrate').expect(HttpStatus.CREATED); }); }); }); describe('[startUserMigration]', () => { - let user: User; + let account: Account; let system: SystemEntity; + describe('POST user/import/startUserMigration', () => { it('should set in user migration mode', async () => { - ({ user, system } = await authenticatedUser([Permission.SCHOOL_IMPORT_USERS_MIGRATE])); - currentUser = mapUserToCurrentUser(user); - Configuration.set('FEATURE_USER_MIGRATION_SYSTEM_ID', system._id.toString()); + ({ account, system } = await authenticatedUser([Permission.SCHOOL_IMPORT_USERS_MIGRATE])); + testApiClient = await testApiClient.login(account); + userImportFeatures.userMigrationSystemId = system._id.toString(); - await request(app.getHttpServer()).post(`/user/import/startUserMigration`).expect(201); + await testApiClient.post('startUserMigration').expect(HttpStatus.CREATED); }); }); }); @@ -959,7 +1092,7 @@ describe('ImportUser Controller (API)', () => { describe('[endSchoolMaintenance]', () => { describe('POST user/import/startSync', () => { it('should remove inMaintenanceSince from school', async () => { - const school = schoolFactory.buildWithId({ + const school = schoolEntityFactory.buildWithId({ externalId: 'foo', inMaintenanceSince: new Date(), inUserMigration: false, @@ -971,19 +1104,116 @@ describe('ImportUser Controller (API)', () => { }), ]; await em.persistAndFlush([school, ...roles]); - const user = userFactory.build({ - school, - roles, - }); - await em.persistAndFlush([user]); + + const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({ school }, [ + Permission.SCHOOL_IMPORT_USERS_MIGRATE, + ]); + + await em.persistAndFlush([adminUser, adminAccount]); em.clear(); - currentUser = mapUserToCurrentUser(user); + testApiClient = await testApiClient.login(adminAccount); + + await testApiClient.post('startSync').expect(HttpStatus.CREATED); + }); + }); + }); + }); + + describe('[POST] populateImportUsers', () => { + describe('when user is not authenticated', () => { + const setup = () => { + const notLoggedInClient = new TestApiClient(app, 'user/import'); + + return { notLoggedInClient }; + }; + + it('should return unauthorized', async () => { + const { notLoggedInClient } = setup(); + + await notLoggedInClient.post('populate-import-users').send().expect(HttpStatus.UNAUTHORIZED); + }); + }); + + describe('when migration is not activated', () => { + const setup = async () => { + const { account } = await authenticatedUser([Permission.SCHOOL_IMPORT_USERS_MIGRATE]); + const loggedInClient = await testApiClient.login(account); + + userImportFeatures.userMigrationEnabled = false; + + return { loggedInClient }; + }; + + it('should return with status forbidden', async () => { + const { loggedInClient } = await setup(); + + const response = await loggedInClient.post('populate-import-users').send(); + + expect(response.body).toEqual({ + type: 'USER_MIGRATION_IS_NOT_ENABLED', + title: 'User Migration Is Not Enabled', + message: 'Feature flag of user migration may be disable or the school is not an LDAP pilot', + code: HttpStatus.FORBIDDEN, + }); + }); + }); + + describe('when users school has no external id', () => { + const setup = async () => { + const { account, school, system } = await authenticatedUser( + [Permission.SCHOOL_IMPORT_USERS_MIGRATE], + [], + false + ); + const loggedInClient = await testApiClient.login(account); + userImportFeatures.userMigrationSystemId = system.id; + + school.externalId = undefined; - await request(app.getHttpServer()).post(`/user/import/startSync`).expect(201); + return { loggedInClient }; + }; + + it('should return with status bad request', async () => { + const { loggedInClient } = await setup(); + + const response = await loggedInClient.post('populate-import-users').send(); + + expect(response.body).toEqual({ + type: 'USER_IMPORT_SCHOOL_EXTERNAL_ID_MISSING', + title: 'User Import School External Id Missing', + message: 'Bad Request', + code: HttpStatus.BAD_REQUEST, }); }); }); + + describe('when users were populated successful', () => { + const setup = async () => { + const { account, school, system } = await authenticatedUser([Permission.SCHOOL_IMPORT_USERS_MIGRATE]); + const loggedInClient = await testApiClient.login(account); + + userImportFeatures.userMigrationEnabled = true; + userImportFeatures.userMigrationSystemId = system.id; + + axiosMock.onPost(/(.*)\/token/).reply(HttpStatus.OK, { + id_token: 'idToken', + refresh_token: 'refreshToken', + access_token: 'accessToken', + }); + + const schulconnexResponse: SanisResponse = schulconnexResponseFactory.build(); + axiosMock.onGet(/(.*)\/personen-info/).reply(HttpStatus.OK, [schulconnexResponse]); + + return { loggedInClient, account, school }; + }; + + it('should return with status created', async () => { + const { loggedInClient } = await setup(); + + await loggedInClient.post('populate-import-users').send().expect(HttpStatus.CREATED); + }); + }); }); }); }); diff --git a/apps/server/src/modules/user-import/controller/import-user.controller.spec.ts b/apps/server/src/modules/user-import/controller/import-user.controller.spec.ts index ab94fc43143..856cfebe623 100644 --- a/apps/server/src/modules/user-import/controller/import-user.controller.spec.ts +++ b/apps/server/src/modules/user-import/controller/import-user.controller.spec.ts @@ -5,7 +5,8 @@ import { ConfigModule } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { ImportUserRepo, LegacySystemRepo, UserRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; -import { UserImportUc } from '../uc/user-import.uc'; +import { UserImportService } from '../service'; +import { UserImportFetchUc, UserImportUc } from '../uc'; import { ImportUserController } from './import-user.controller'; describe('ImportUserController', () => { @@ -45,6 +46,14 @@ describe('ImportUserController', () => { provide: UserRepo, useValue: {}, }, + { + provide: UserImportService, + useValue: {}, + }, + { + provide: UserImportFetchUc, + useValue: {}, + }, ], controllers: [ImportUserController], }).compile(); diff --git a/apps/server/src/modules/user-import/controller/import-user.controller.ts b/apps/server/src/modules/user-import/controller/import-user.controller.ts index 228590f8f6d..c4ca898d434 100644 --- a/apps/server/src/modules/user-import/controller/import-user.controller.ts +++ b/apps/server/src/modules/user-import/controller/import-user.controller.ts @@ -1,13 +1,19 @@ import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; +import { + ApiBadRequestResponse, + ApiCreatedResponse, + ApiForbiddenResponse, + ApiOperation, + ApiServiceUnavailableResponse, + ApiTags, + ApiUnauthorizedResponse, +} from '@nestjs/swagger'; import { PaginationParams } from '@shared/controller'; import { ImportUser, User } from '@shared/domain/entity'; import { IFindOptions } from '@shared/domain/interface'; -import { ImportUserMapper } from '../mapper/import-user.mapper'; -import { UserMatchMapper } from '../mapper/user-match.mapper'; -import { UserImportUc } from '../uc/user-import.uc'; - +import { ImportUserMapper, UserMatchMapper } from '../mapper'; +import { UserImportFetchUc, UserImportUc } from '../uc'; import { FilterImportUserParams, FilterUserParams, @@ -24,7 +30,7 @@ import { @Authenticate('jwt') @Controller('user/import') export class ImportUserController { - constructor(private readonly userImportUc: UserImportUc, private readonly userUc: UserImportUc) {} + constructor(private readonly userImportUc: UserImportUc, private readonly userImportFetchUc: UserImportFetchUc) {} @Get() async findAllImportUsers( @@ -88,7 +94,7 @@ export class ImportUserController { const options: IFindOptions = { pagination }; const query = UserMatchMapper.mapToDomain(scope); - const [userList, total] = await this.userUc.findAllUnmatchedUsers(currentUser.userId, query, options); + const [userList, total] = await this.userImportUc.findAllUnmatchedUsers(currentUser.userId, query, options); const { skip, limit } = pagination; const dtoList = userList.map((user) => UserMatchMapper.mapToResponse(user)); const response = new UserMatchListResponse(dtoList, total, skip, limit); @@ -113,4 +119,18 @@ export class ImportUserController { async endSchoolInMaintenance(@CurrentUser() currentUser: ICurrentUser): Promise { await this.userImportUc.endSchoolInMaintenance(currentUser.userId); } + + @Post('populate-import-users') + @ApiOperation({ + summary: 'Populates import users', + description: 'Populates import users from specific user migration populate endpoint.', + }) + @ApiCreatedResponse() + @ApiUnauthorizedResponse() + @ApiServiceUnavailableResponse() + @ApiBadRequestResponse() + @ApiForbiddenResponse() + async populateImportUsers(@CurrentUser() currentUser: ICurrentUser): Promise { + await this.userImportFetchUc.populateImportUsers(currentUser.userId); + } } diff --git a/apps/server/src/modules/user-import/index.ts b/apps/server/src/modules/user-import/index.ts index 48e2da47e2a..0bd327d1d3c 100644 --- a/apps/server/src/modules/user-import/index.ts +++ b/apps/server/src/modules/user-import/index.ts @@ -1 +1,2 @@ export * from './user-import.module'; +export { UserImportConfigModule } from './user-import-config.module'; diff --git a/apps/server/src/modules/user-import/loggable/index.ts b/apps/server/src/modules/user-import/loggable/index.ts index d1ee0b00597..2e4768e36b2 100644 --- a/apps/server/src/modules/user-import/loggable/index.ts +++ b/apps/server/src/modules/user-import/loggable/index.ts @@ -1,6 +1,9 @@ -export * from './user-migration-not-enable.loggable'; +export { UserMigrationIsNotEnabledLoggableException } from './user-migration-not-enable-loggable-exception'; export * from './school-in-user-migration-start.loggable'; export * from './school-in-user-migration-end.loggable'; export * from './school-id-does-not-match-with-user-school-id.loggable'; export * from './migration-is-not-completed.loggable'; export * from './migration-may-be-completed.loggable'; +export { UserImportConfigurationFailureLoggableException } from './user-import-configuration-failure-loggable-exception'; +export { UserImportPopulateFailureLoggableException } from './user-import-populate-failure-loggable-exception'; +export { UserImportSchoolExternalIdMissingLoggableException } from './user-import-school-external-id-missing-loggable-exception'; diff --git a/apps/server/src/modules/user-import/loggable/user-import-configuration-failure-loggable-exception.spec.ts b/apps/server/src/modules/user-import/loggable/user-import-configuration-failure-loggable-exception.spec.ts new file mode 100644 index 00000000000..6c13177da53 --- /dev/null +++ b/apps/server/src/modules/user-import/loggable/user-import-configuration-failure-loggable-exception.spec.ts @@ -0,0 +1,23 @@ +import { UserImportConfigurationFailureLoggableException } from './user-import-configuration-failure-loggable-exception'; + +describe(UserImportConfigurationFailureLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const loggable = new UserImportConfigurationFailureLoggableException(); + + return { loggable }; + }; + + it('should return a loggable message', () => { + const { loggable } = setup(); + + const message = loggable.getLogMessage(); + + expect(message).toEqual({ + type: 'USER_IMPORT_CONFIGURATION_FAILURE', + message: 'Please check the user import configuration.', + stack: loggable.stack, + }); + }); + }); +}); diff --git a/apps/server/src/modules/user-import/loggable/user-import-configuration-failure-loggable-exception.ts b/apps/server/src/modules/user-import/loggable/user-import-configuration-failure-loggable-exception.ts new file mode 100644 index 00000000000..91337ccb4c9 --- /dev/null +++ b/apps/server/src/modules/user-import/loggable/user-import-configuration-failure-loggable-exception.ts @@ -0,0 +1,24 @@ +import { HttpStatus } from '@nestjs/common'; +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; +import { BusinessError } from '@shared/common'; + +export class UserImportConfigurationFailureLoggableException extends BusinessError implements Loggable { + constructor() { + super( + { + type: 'USER_IMPORT_CONFIGURATION_FAILURE', + title: 'The user import configuration has a failure.', + defaultMessage: 'Please check the user import configuration.', + }, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + type: 'USER_IMPORT_CONFIGURATION_FAILURE', + message: 'Please check the user import configuration.', + stack: this.stack, + }; + } +} diff --git a/apps/server/src/modules/user-import/loggable/user-import-populate-failure-loggable-exception.spec.ts b/apps/server/src/modules/user-import/loggable/user-import-populate-failure-loggable-exception.spec.ts new file mode 100644 index 00000000000..6d38adfaf39 --- /dev/null +++ b/apps/server/src/modules/user-import/loggable/user-import-populate-failure-loggable-exception.spec.ts @@ -0,0 +1,27 @@ +import { UserImportPopulateFailureLoggableException } from './user-import-populate-failure-loggable-exception'; + +describe(UserImportPopulateFailureLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const url = 'mockUrl'; + const loggable = new UserImportPopulateFailureLoggableException(url); + + return { loggable, url }; + }; + + it('should return a loggable message', () => { + const { loggable, url } = setup(); + + const message = loggable.getLogMessage(); + + expect(message).toEqual({ + type: 'USER_IMPORT_POPULATE_FAILURE', + message: 'While populate import users an error occurred.', + stack: loggable.stack, + data: { + url, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/user-import/loggable/user-import-populate-failure-loggable-exception.ts b/apps/server/src/modules/user-import/loggable/user-import-populate-failure-loggable-exception.ts new file mode 100644 index 00000000000..0348abfa45c --- /dev/null +++ b/apps/server/src/modules/user-import/loggable/user-import-populate-failure-loggable-exception.ts @@ -0,0 +1,27 @@ +import { HttpStatus } from '@nestjs/common'; +import { BusinessError } from '@shared/common'; +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; + +export class UserImportPopulateFailureLoggableException extends BusinessError implements Loggable { + constructor(private readonly url: string) { + super( + { + type: 'USER_IMPORT_POPULATE_FAILURE', + title: 'Fetching import user failed.', + defaultMessage: 'While fetching import users an error occurred.', + }, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + type: 'USER_IMPORT_POPULATE_FAILURE', + message: 'While populate import users an error occurred.', + stack: this.stack, + data: { + url: this.url, + }, + }; + } +} diff --git a/apps/server/src/modules/user-import/loggable/user-import-school-external-id-missing-loggable-exception.ts b/apps/server/src/modules/user-import/loggable/user-import-school-external-id-missing-loggable-exception.ts new file mode 100644 index 00000000000..024f305b499 --- /dev/null +++ b/apps/server/src/modules/user-import/loggable/user-import-school-external-id-missing-loggable-exception.ts @@ -0,0 +1,19 @@ +import { BadRequestException } from '@nestjs/common'; +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; + +export class UserImportSchoolExternalIdMissingLoggableException extends BadRequestException implements Loggable { + constructor(private readonly schoolId: string) { + super(); + } + + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + type: 'USER_IMPORT_SCHOOL_EXTERNAL_ID_MISSING', + message: 'The users school does not have an external id', + stack: this.stack, + data: { + schoolId: this.schoolId, + }, + }; + } +} diff --git a/apps/server/src/modules/user-import/loggable/user-migration-not-enable-loggable-exception.ts b/apps/server/src/modules/user-import/loggable/user-migration-not-enable-loggable-exception.ts new file mode 100644 index 00000000000..475323fb7f9 --- /dev/null +++ b/apps/server/src/modules/user-import/loggable/user-migration-not-enable-loggable-exception.ts @@ -0,0 +1,22 @@ +import { ForbiddenException } from '@nestjs/common'; +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; + +export class UserMigrationIsNotEnabledLoggableException extends ForbiddenException implements Loggable { + constructor(private readonly userId?: string, private readonly schoolId?: string) { + super({ + message: 'Feature flag of user migration may be disable or the school is not an LDAP pilot', + }); + } + + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + type: 'USER_MIGRATION_IS_NOT_ENABLED', + message: 'Feature flag of user migration may be disable or the school is not an LDAP pilot', + stack: this.stack, + data: { + userId: this.userId, + schoolId: this.schoolId, + }, + }; + } +} diff --git a/apps/server/src/modules/user-import/mapper/import-user.mapper.spec.ts b/apps/server/src/modules/user-import/mapper/import-user.mapper.spec.ts index ad7d2a2c3e4..881b8fc6c4b 100644 --- a/apps/server/src/modules/user-import/mapper/import-user.mapper.spec.ts +++ b/apps/server/src/modules/user-import/mapper/import-user.mapper.spec.ts @@ -2,7 +2,7 @@ import { BadRequestException } from '@nestjs/common'; import { MatchCreator } from '@shared/domain/entity'; import { RoleName, SortOrder } from '@shared/domain/interface'; import { MatchCreatorScope } from '@shared/domain/types'; -import { importUserFactory, schoolFactory, setupEntities, userFactory } from '@shared/testing'; +import { importUserFactory, schoolEntityFactory, setupEntities, userFactory } from '@shared/testing'; import { FilterImportUserParams, FilterMatchType, @@ -95,7 +95,7 @@ describe('[ImportUserMapper]', () => { }); describe('when user and matchedBy is defined', () => { it('should map match', () => { - const school = schoolFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); const user = userFactory.build({ school }); const importUser = importUserFactory.matched(MatchCreator.AUTO, user).build({ school }); const mockResponse = Object.create(UserMatchResponse, {}) as UserMatchResponse; diff --git a/apps/server/src/modules/user-import/mapper/index.ts b/apps/server/src/modules/user-import/mapper/index.ts new file mode 100644 index 00000000000..776e5735224 --- /dev/null +++ b/apps/server/src/modules/user-import/mapper/index.ts @@ -0,0 +1,5 @@ +export { ImportUserMatchMapper } from './match.mapper'; +export { RoleNameMapper } from './role-name.mapper'; +export { UserMatchMapper } from './user-match.mapper'; +export { ImportUserMapper } from './import-user.mapper'; +export { SchulconnexImportUserMapper } from './schulconnex-import-user.mapper'; diff --git a/apps/server/src/modules/user-import/mapper/schulconnex-import-user.mapper.ts b/apps/server/src/modules/user-import/mapper/schulconnex-import-user.mapper.ts new file mode 100644 index 00000000000..dbcc01e28ea --- /dev/null +++ b/apps/server/src/modules/user-import/mapper/schulconnex-import-user.mapper.ts @@ -0,0 +1,31 @@ +import { SanisResponse } from '@infra/schulconnex-client'; +import { SanisResponseMapper } from '@modules/provisioning'; +import { ImportUser, SchoolEntity, SystemEntity } from '@shared/domain/entity'; +import { RoleName } from '@shared/domain/interface'; + +export class SchulconnexImportUserMapper { + public static mapDataToUserImportEntities( + response: SanisResponse[], + system: SystemEntity, + school: SchoolEntity + ): ImportUser[] { + const importUsers: ImportUser[] = response.map((externalUser: SanisResponse): ImportUser => { + const role: RoleName = SanisResponseMapper.mapSanisRoleToRoleName(externalUser); + + const importUser: ImportUser = new ImportUser({ + system, + school, + ldapDn: `uid=${externalUser.person.name.vorname}.${externalUser.person.name.familienname},`, + externalId: externalUser.pid, + firstName: externalUser.person.name.vorname, + lastName: externalUser.person.name.familienname, + roleNames: ImportUser.isImportUserRole(role) ? [role] : [], + email: '', + }); + + return importUser; + }); + + return importUsers; + } +} diff --git a/apps/server/src/modules/user-import/service/index.ts b/apps/server/src/modules/user-import/service/index.ts new file mode 100644 index 00000000000..3e9ef6f5466 --- /dev/null +++ b/apps/server/src/modules/user-import/service/index.ts @@ -0,0 +1,2 @@ +export { SchulconnexFetchImportUsersService } from './strategy/schulconnex-fetch-import-users.service'; +export { UserImportService } from './user-import.service'; diff --git a/apps/server/src/modules/user-import/service/strategy/schulconnex-fetch-import-users.service.spec.ts b/apps/server/src/modules/user-import/service/strategy/schulconnex-fetch-import-users.service.spec.ts new file mode 100644 index 00000000000..594bd745704 --- /dev/null +++ b/apps/server/src/modules/user-import/service/strategy/schulconnex-fetch-import-users.service.spec.ts @@ -0,0 +1,190 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { SanisResponse, schulconnexResponseFactory, SchulconnexRestClient } from '@infra/schulconnex-client'; +import { UserService } from '@modules/user'; +import { Test, TestingModule } from '@nestjs/testing'; +import { UserDO } from '@shared/domain/domainobject'; +import { ImportUser, SchoolEntity, SystemEntity } from '@shared/domain/entity'; +import { RoleName } from '@shared/domain/interface'; +import { + importUserFactory, + schoolEntityFactory, + setupEntities, + systemEntityFactory, + userDoFactory, +} from '@shared/testing'; +import { UserImportSchoolExternalIdMissingLoggableException } from '../../loggable'; +import { SchulconnexFetchImportUsersService } from './schulconnex-fetch-import-users.service'; + +describe(SchulconnexFetchImportUsersService.name, () => { + let module: TestingModule; + let service: SchulconnexFetchImportUsersService; + + let schulconnexRestClient: DeepMocked; + let userService: DeepMocked; + + beforeAll(async () => { + await setupEntities(); + + module = await Test.createTestingModule({ + providers: [ + SchulconnexFetchImportUsersService, + { + provide: SchulconnexRestClient, + useValue: createMock(), + }, + { + provide: UserService, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(SchulconnexFetchImportUsersService); + schulconnexRestClient = module.get(SchulconnexRestClient); + userService = module.get(UserService); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + const createImportUser = (externalUserData: SanisResponse, school: SchoolEntity, system: SystemEntity): ImportUser => + importUserFactory.build({ + system, + school, + ldapDn: `uid=${externalUserData.person.name.vorname}.${externalUserData.person.name.familienname},`, + externalId: externalUserData.pid, + firstName: externalUserData.person.name.vorname, + lastName: externalUserData.person.name.familienname, + email: '', + roleNames: [RoleName.ADMINISTRATOR], + classNames: undefined, + }); + + describe('getData', () => { + describe('when fetching the data', () => { + const setup = () => { + const externalUserData: SanisResponse = schulconnexResponseFactory.build(); + const system: SystemEntity = systemEntityFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId({ + systems: [system], + externalId: 'externalSchoolId', + }); + const importUser: ImportUser = createImportUser(externalUserData, school, system); + + schulconnexRestClient.getPersonenInfo.mockResolvedValueOnce([externalUserData]); + + return { + school, + system, + importUser, + }; + }; + + it('should call the schulconnex rest client', async () => { + const { school, system } = setup(); + + await service.getData(school, system); + + expect(schulconnexRestClient.getPersonenInfo).toHaveBeenCalledWith({ + vollstaendig: ['personen', 'personenkontexte', 'organisationen'], + 'organisation.id': school.externalId, + }); + }); + + it('should return import users', async () => { + const { school, system } = setup(); + + const result: ImportUser[] = await service.getData(school, system); + + // TODO: test this somehow + expect(result).toHaveLength(1); + }); + }); + + describe('when the school has no external id', () => { + const setup = () => { + const system: SystemEntity = systemEntityFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId({ + systems: [system], + externalId: undefined, + }); + + return { + school, + system, + }; + }; + + it('should throw an error', async () => { + const { school, system } = setup(); + + await expect(service.getData(school, system)).rejects.toThrow( + UserImportSchoolExternalIdMissingLoggableException + ); + }); + }); + }); + + describe('filterAlreadyMigratedUser', () => { + describe('when the user was not migrated yet', () => { + const setup = () => { + const externalUserData: SanisResponse = schulconnexResponseFactory.build(); + const system: SystemEntity = systemEntityFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId({ + systems: [system], + externalId: 'externalSchoolId', + }); + const importUser: ImportUser = createImportUser(externalUserData, school, system); + const migratedUser: UserDO = userDoFactory.build({ externalId: externalUserData.pid }); + userService.findByExternalId.mockResolvedValueOnce(null); + + return { + systemId: system.id, + importUsers: [importUser], + migratedUser, + }; + }; + + it('should return the import users', async () => { + const { systemId, importUsers } = setup(); + + const result: ImportUser[] = await service.filterAlreadyMigratedUser(importUsers, systemId); + + // TODO test this somehow + expect(result).toHaveLength(1); + }); + }); + + describe('when the user already was migrated', () => { + const setup = () => { + const externalUserData: SanisResponse = schulconnexResponseFactory.build(); + const system: SystemEntity = systemEntityFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId({ + systems: [system], + externalId: 'externalSchoolId', + }); + const importUser: ImportUser = createImportUser(externalUserData, school, system); + const migratedUser: UserDO = userDoFactory.build({ externalId: externalUserData.pid }); + userService.findByExternalId.mockResolvedValueOnce(migratedUser); + + return { + systemId: system.id, + importUsers: [importUser], + }; + }; + + it('should return an empty array', async () => { + const { systemId, importUsers } = setup(); + + const result: ImportUser[] = await service.filterAlreadyMigratedUser(importUsers, systemId); + + expect(result).toHaveLength(0); + }); + }); + }); +}); diff --git a/apps/server/src/modules/user-import/service/strategy/schulconnex-fetch-import-users.service.ts b/apps/server/src/modules/user-import/service/strategy/schulconnex-fetch-import-users.service.ts new file mode 100644 index 00000000000..e1723bfc488 --- /dev/null +++ b/apps/server/src/modules/user-import/service/strategy/schulconnex-fetch-import-users.service.ts @@ -0,0 +1,49 @@ +import { SanisResponse, SchulconnexRestClient } from '@infra/schulconnex-client'; +import { UserService } from '@modules/user'; +import { Injectable } from '@nestjs/common'; +import { UserDO } from '@shared/domain/domainobject'; +import { ImportUser, SchoolEntity, SystemEntity } from '@shared/domain/entity'; +import { EntityId } from '@shared/domain/types'; +import { UserImportSchoolExternalIdMissingLoggableException } from '../../loggable'; +import { SchulconnexImportUserMapper } from '../../mapper'; + +@Injectable() +export class SchulconnexFetchImportUsersService { + constructor( + private readonly schulconnexRestClient: SchulconnexRestClient, + private readonly userService: UserService + ) {} + + public async getData(school: SchoolEntity, system: SystemEntity): Promise { + const externalSchoolId: string | undefined = school.externalId; + if (!externalSchoolId) { + throw new UserImportSchoolExternalIdMissingLoggableException(school.id); + } + + const response: SanisResponse[] = await this.schulconnexRestClient.getPersonenInfo({ + vollstaendig: ['personen', 'personenkontexte', 'organisationen'], + 'organisation.id': externalSchoolId, + }); + + const mappedImportUsers: ImportUser[] = SchulconnexImportUserMapper.mapDataToUserImportEntities( + response, + system, + school + ); + + return mappedImportUsers; + } + + public async filterAlreadyMigratedUser(importUsers: ImportUser[], systemId: EntityId): Promise { + const filteredUsers: ImportUser[] = ( + await Promise.all( + importUsers.map(async (importUser: ImportUser): Promise => { + const foundUser: UserDO | null = await this.userService.findByExternalId(importUser.externalId, systemId); + return foundUser ? null : importUser; + }) + ) + ).filter((user: ImportUser | null): user is ImportUser => user !== null); + + return filteredUsers; + } +} diff --git a/apps/server/src/modules/user-import/service/user-import.service.spec.ts b/apps/server/src/modules/user-import/service/user-import.service.spec.ts new file mode 100644 index 00000000000..4801c5d3f7a --- /dev/null +++ b/apps/server/src/modules/user-import/service/user-import.service.spec.ts @@ -0,0 +1,285 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { MongoMemoryDatabaseModule } from '@infra/database'; +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { UserService } from '@modules/user'; +import { Test, TestingModule } from '@nestjs/testing'; +import { LegacySchoolDo } from '@shared/domain/domainobject'; +import { ImportUser, MatchCreator, SchoolEntity, SystemEntity, User } from '@shared/domain/entity'; +import { SchoolFeature } from '@shared/domain/types'; +import { ImportUserRepo, LegacySystemRepo } from '@shared/repo'; +import { + cleanupCollections, + importUserFactory, + legacySchoolDoFactory, + schoolEntityFactory, + setupEntities, + systemEntityFactory, + userFactory, +} from '@shared/testing'; +import { IUserImportFeatures, UserImportFeatures } from '../config'; +import { UserMigrationIsNotEnabledLoggableException } from '../loggable'; +import { UserImportService } from './user-import.service'; + +describe(UserImportService.name, () => { + let module: TestingModule; + let service: UserImportService; + let em: EntityManager; + + let importUserRepo: DeepMocked; + let legacySystemRepo: DeepMocked; + let userService: DeepMocked; + + const features: IUserImportFeatures = { + userMigrationSystemId: new ObjectId().toHexString(), + userMigrationEnabled: true, + }; + + beforeAll(async () => { + await setupEntities(); + + module = await Test.createTestingModule({ + imports: [MongoMemoryDatabaseModule.forRoot()], + providers: [ + UserImportService, + { + provide: ImportUserRepo, + useValue: createMock(), + }, + { + provide: LegacySystemRepo, + useValue: createMock(), + }, + { + provide: UserService, + useValue: createMock(), + }, + { + provide: UserImportFeatures, + useValue: features, + }, + ], + }).compile(); + + service = module.get(UserImportService); + em = module.get(EntityManager); + importUserRepo = module.get(ImportUserRepo); + legacySystemRepo = module.get(LegacySystemRepo); + userService = module.get(UserService); + }); + + afterAll(async () => { + await module.close(); + }); + + beforeEach(async () => { + await cleanupCollections(em); + }); + + describe('saveImportUsers', () => { + describe('when saving import users', () => { + const setup = () => { + const importUser: ImportUser = importUserFactory.build(); + const otherImportUser: ImportUser = importUserFactory.build(); + + return { + importUsers: [importUser, otherImportUser], + }; + }; + + it('should call saveImportUsers', async () => { + const { importUsers } = setup(); + + await service.saveImportUsers(importUsers); + + expect(importUserRepo.saveImportUsers).toHaveBeenCalledWith(importUsers); + }); + }); + }); + + describe('getMigrationSystem', () => { + describe('when fetching the migration system', () => { + const setup = () => { + const system: SystemEntity = systemEntityFactory.buildWithId(undefined, features.userMigrationSystemId); + + legacySystemRepo.findById.mockResolvedValueOnce(system); + + return { + system, + }; + }; + + it('should return the system', async () => { + const { system } = setup(); + + const result: SystemEntity = await service.getMigrationSystem(); + + expect(result).toEqual(system); + }); + }); + }); + + describe('checkFeatureEnabled', () => { + describe('when the global feature is enabled', () => { + const setup = () => { + features.userMigrationEnabled = true; + + const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ features: undefined }); + + return { + school, + }; + }; + + it('should do nothing', () => { + const { school } = setup(); + + service.checkFeatureEnabled(school); + }); + }); + + describe('when the school feature is enabled', () => { + const setup = () => { + features.userMigrationEnabled = false; + + const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ + features: [SchoolFeature.LDAP_UNIVENTION_MIGRATION], + }); + + return { + school, + }; + }; + + it('should do nothing', () => { + const { school } = setup(); + + service.checkFeatureEnabled(school); + }); + }); + + describe('when the features are disabled', () => { + const setup = () => { + features.userMigrationEnabled = false; + + const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ + features: [], + }); + + return { + school, + }; + }; + + it('should do nothing', () => { + const { school } = setup(); + + expect(() => service.checkFeatureEnabled(school)).toThrow(UserMigrationIsNotEnabledLoggableException); + }); + }); + }); + + describe('matchUsers', () => { + describe('when all users have unique names', () => { + const setup = () => { + const school: SchoolEntity = schoolEntityFactory.buildWithId(); + const user1: User = userFactory.buildWithId({ firstName: 'First1', lastName: 'Last1' }); + const user2: User = userFactory.buildWithId({ firstName: 'First2', lastName: 'Last2' }); + const importUser1: ImportUser = importUserFactory.buildWithId({ + school, + firstName: user1.firstName, + lastName: user1.lastName, + }); + const importUser2: ImportUser = importUserFactory.buildWithId({ + school, + firstName: user2.firstName, + lastName: user2.lastName, + }); + + userService.findUserBySchoolAndName.mockResolvedValueOnce([user1]); + userService.findUserBySchoolAndName.mockResolvedValueOnce([user2]); + + return { + user1, + user2, + importUser1, + importUser2, + }; + }; + + it('should return all users as auto matched', async () => { + const { user1, user2, importUser1, importUser2 } = setup(); + + const result: ImportUser[] = await service.matchUsers([importUser1, importUser2]); + + expect(result).toEqual([ + { ...importUser1, user: user1, matchedBy: MatchCreator.AUTO }, + { ...importUser2, user: user2, matchedBy: MatchCreator.AUTO }, + ]); + }); + }); + + describe('when the imported users have the same names', () => { + const setup = () => { + const school: SchoolEntity = schoolEntityFactory.buildWithId(); + const user1: User = userFactory.buildWithId({ firstName: 'First', lastName: 'Last' }); + const importUser1: ImportUser = importUserFactory.buildWithId({ + school, + firstName: user1.firstName, + lastName: user1.lastName, + }); + const importUser2: ImportUser = importUserFactory.buildWithId({ + school, + firstName: user1.firstName, + lastName: user1.lastName, + }); + + userService.findUserBySchoolAndName.mockResolvedValueOnce([user1]); + userService.findUserBySchoolAndName.mockResolvedValueOnce([user1]); + + return { + user1, + importUser1, + importUser2, + }; + }; + + it('should return the users without a match', async () => { + const { importUser1, importUser2 } = setup(); + + const result: ImportUser[] = await service.matchUsers([importUser1, importUser2]); + + expect(result).toEqual([importUser1, importUser2]); + }); + }); + + describe('when existing users have the same names', () => { + const setup = () => { + const school: SchoolEntity = schoolEntityFactory.buildWithId(); + const user1: User = userFactory.buildWithId({ firstName: 'First', lastName: 'Last' }); + const user2: User = userFactory.buildWithId({ firstName: 'First', lastName: 'Last' }); + const importUser1: ImportUser = importUserFactory.buildWithId({ + school, + firstName: user1.firstName, + lastName: user1.lastName, + }); + + userService.findUserBySchoolAndName.mockResolvedValueOnce([user1, user2]); + userService.findUserBySchoolAndName.mockResolvedValueOnce([user1, user2]); + + return { + user1, + user2, + importUser1, + }; + }; + + it('should return the users without a match', async () => { + const { importUser1 } = setup(); + + const result: ImportUser[] = await service.matchUsers([importUser1]); + + expect(result).toEqual([importUser1]); + }); + }); + }); +}); diff --git a/apps/server/src/modules/user-import/service/user-import.service.ts b/apps/server/src/modules/user-import/service/user-import.service.ts new file mode 100644 index 00000000000..cc2edd06964 --- /dev/null +++ b/apps/server/src/modules/user-import/service/user-import.service.ts @@ -0,0 +1,71 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { LegacySchoolDo } from '@shared/domain/domainobject'; +import { ImportUser, MatchCreator, SystemEntity, User } from '@shared/domain/entity'; +import { SchoolFeature } from '@shared/domain/types'; +import { ImportUserRepo, LegacySystemRepo } from '@shared/repo'; +import { UserService } from '@modules/user'; +import { IUserImportFeatures, UserImportFeatures } from '../config'; +import { UserMigrationIsNotEnabledLoggableException } from '../loggable'; + +@Injectable() +export class UserImportService { + constructor( + private readonly userImportRepo: ImportUserRepo, + private readonly systemRepo: LegacySystemRepo, + private readonly userService: UserService, + @Inject(UserImportFeatures) private readonly userImportFeatures: IUserImportFeatures + ) {} + + public async saveImportUsers(importUsers: ImportUser[]): Promise { + await this.userImportRepo.saveImportUsers(importUsers); + } + + public async getMigrationSystem(): Promise { + const systemId: string = this.userImportFeatures.userMigrationSystemId; + + const system: SystemEntity = await this.systemRepo.findById(systemId); + + return system; + } + + public checkFeatureEnabled(school: LegacySchoolDo): void { + const enabled: boolean = this.userImportFeatures.userMigrationEnabled; + const isLdapPilotSchool: boolean = + !!school.features && school.features.includes(SchoolFeature.LDAP_UNIVENTION_MIGRATION); + + if (!enabled && !isLdapPilotSchool) { + throw new UserMigrationIsNotEnabledLoggableException(school.id); + } + } + + public async matchUsers(importUsers: ImportUser[]): Promise { + const importUserMap: Map = new Map(); + + importUsers.forEach((importUser) => { + const key = `${importUser.school.id}_${importUser.firstName}_${importUser.lastName}`; + importUserMap.set(key, importUser); + }); + + const matchedImportUsers: ImportUser[] = await Promise.all( + importUsers.map(async (importUser: ImportUser): Promise => { + const user: User[] = await this.userService.findUserBySchoolAndName( + importUser.school.id, + importUser.firstName, + importUser.lastName + ); + + const key = `${importUser.school.id}_${importUser.firstName}_${importUser.lastName}`; + const nameCount = importUserMap.has(key) ? 1 : 0; + + if (user.length === 1 && nameCount === 1) { + importUser.user = user[0]; + importUser.matchedBy = MatchCreator.AUTO; + } + + return importUser; + }) + ); + + return matchedImportUsers; + } +} diff --git a/apps/server/src/modules/user-import/uc/index.ts b/apps/server/src/modules/user-import/uc/index.ts new file mode 100644 index 00000000000..a92cf5651ac --- /dev/null +++ b/apps/server/src/modules/user-import/uc/index.ts @@ -0,0 +1,2 @@ +export { UserImportUc } from './user-import.uc'; +export { UserImportFetchUc } from './user-import-fetch.uc'; diff --git a/apps/server/src/modules/user-import/uc/user-import-fetch.uc.spec.ts b/apps/server/src/modules/user-import/uc/user-import-fetch.uc.spec.ts new file mode 100644 index 00000000000..effbf25d0a1 --- /dev/null +++ b/apps/server/src/modules/user-import/uc/user-import-fetch.uc.spec.ts @@ -0,0 +1,168 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { AuthorizationService } from '@modules/authorization'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ImportUser, SystemEntity, User } from '@shared/domain/entity'; +import { Permission } from '@shared/domain/interface'; +import { importUserFactory, setupEntities, systemEntityFactory, userFactory } from '@shared/testing'; +import { IUserImportFeatures, UserImportFeatures } from '../config'; +import { UserMigrationIsNotEnabledLoggableException } from '../loggable'; +import { SchulconnexFetchImportUsersService, UserImportService } from '../service'; +import { UserImportFetchUc } from './user-import-fetch.uc'; + +describe(UserImportFetchUc.name, () => { + let module: TestingModule; + let uc: UserImportFetchUc; + + let schulconnexFetchImportUsersService: DeepMocked; + let authorizationService: DeepMocked; + let userImportService: DeepMocked; + let userImportFeatures: IUserImportFeatures; + + beforeAll(async () => { + await setupEntities(); + + module = await Test.createTestingModule({ + providers: [ + UserImportFetchUc, + { + provide: UserImportFeatures, + useValue: {}, + }, + { + provide: SchulconnexFetchImportUsersService, + useValue: createMock(), + }, + { + provide: AuthorizationService, + useValue: createMock(), + }, + { + provide: UserImportService, + useValue: createMock(), + }, + ], + }).compile(); + + uc = module.get(UserImportFetchUc); + schulconnexFetchImportUsersService = module.get(SchulconnexFetchImportUsersService); + authorizationService = module.get(AuthorizationService); + userImportService = module.get(UserImportService); + userImportFeatures = module.get(UserImportFeatures); + }); + + beforeEach(() => { + Object.assign(userImportFeatures, { + userMigrationEnabled: true, + userMigrationSystemId: new ObjectId().toHexString(), + }); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('fetchImportUsers', () => { + describe('when fetching and matching users', () => { + const setup = () => { + const system: SystemEntity = systemEntityFactory.buildWithId( + undefined, + userImportFeatures.userMigrationSystemId + ); + const user: User = userFactory.buildWithId(); + const importUser: ImportUser = importUserFactory.build({ + system, + }); + + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + userImportService.getMigrationSystem.mockResolvedValueOnce(system); + schulconnexFetchImportUsersService.getData.mockResolvedValueOnce([importUser]); + schulconnexFetchImportUsersService.filterAlreadyMigratedUser.mockResolvedValueOnce([importUser]); + userImportService.matchUsers.mockResolvedValueOnce([importUser]); + + return { + user, + system, + importUser, + }; + }; + + it('should check the users permission', async () => { + const { user } = setup(); + + await uc.populateImportUsers(user.id); + + expect(authorizationService.checkAllPermissions).toHaveBeenCalledWith(user, [ + Permission.SCHOOL_IMPORT_USERS_MIGRATE, + ]); + }); + + it('should filter migrated users', async () => { + const { user, importUser, system } = setup(); + + await uc.populateImportUsers(user.id); + + expect(schulconnexFetchImportUsersService.filterAlreadyMigratedUser).toHaveBeenCalledWith( + [importUser], + system.id + ); + }); + + it('should match the users', async () => { + const { user, importUser } = setup(); + + await uc.populateImportUsers(user.id); + + expect(userImportService.matchUsers).toHaveBeenCalledWith([importUser]); + }); + + it('should save the import users', async () => { + const { user, importUser } = setup(); + + await uc.populateImportUsers(user.id); + + expect(userImportService.saveImportUsers).toHaveBeenCalledWith([importUser]); + }); + }); + }); + + describe('when the migration feature is not enabled', () => { + const setup = () => { + userImportFeatures.userMigrationEnabled = false; + + const user: User = userFactory.buildWithId(); + + return { + user, + }; + }; + + it('should throw an error', async () => { + const { user } = setup(); + + await expect(uc.populateImportUsers(user.id)).rejects.toThrow(UserMigrationIsNotEnabledLoggableException); + }); + }); + + describe('when the target system id is not defined', () => { + const setup = () => { + userImportFeatures.userMigrationSystemId = ''; + + const user: User = userFactory.buildWithId(); + + return { + user, + }; + }; + + it('should throw an error', async () => { + const { user } = setup(); + + await expect(uc.populateImportUsers(user.id)).rejects.toThrow(UserMigrationIsNotEnabledLoggableException); + }); + }); +}); diff --git a/apps/server/src/modules/user-import/uc/user-import-fetch.uc.ts b/apps/server/src/modules/user-import/uc/user-import-fetch.uc.ts new file mode 100644 index 00000000000..c9c03932e24 --- /dev/null +++ b/apps/server/src/modules/user-import/uc/user-import-fetch.uc.ts @@ -0,0 +1,43 @@ +import { AuthorizationService } from '@modules/authorization'; +import { Inject, Injectable } from '@nestjs/common'; +import { ImportUser, SystemEntity, User } from '@shared/domain/entity'; +import { Permission } from '@shared/domain/interface'; +import { EntityId } from '@shared/domain/types'; +import { IUserImportFeatures, UserImportFeatures } from '../config'; +import { UserMigrationIsNotEnabledLoggableException } from '../loggable'; +import { SchulconnexFetchImportUsersService, UserImportService } from '../service'; + +@Injectable() +export class UserImportFetchUc { + constructor( + @Inject(UserImportFeatures) private readonly userImportFeatures: IUserImportFeatures, + private readonly schulconnexFetchImportUsersService: SchulconnexFetchImportUsersService, + private readonly authorizationService: AuthorizationService, + private readonly userImportService: UserImportService + ) {} + + public async populateImportUsers(currentUserId: EntityId): Promise { + this.checkMigrationEnabled(currentUserId); + + const user: User = await this.authorizationService.getUserWithPermissions(currentUserId); + this.authorizationService.checkAllPermissions(user, [Permission.SCHOOL_IMPORT_USERS_MIGRATE]); + + const system: SystemEntity = await this.userImportService.getMigrationSystem(); + const fetchedData: ImportUser[] = await this.schulconnexFetchImportUsersService.getData(user.school, system); + + const filteredFetchedData: ImportUser[] = await this.schulconnexFetchImportUsersService.filterAlreadyMigratedUser( + fetchedData, + this.userImportFeatures.userMigrationSystemId + ); + + const matchedImportUsers: ImportUser[] = await this.userImportService.matchUsers(filteredFetchedData); + + await this.userImportService.saveImportUsers(matchedImportUsers); + } + + private checkMigrationEnabled(userId: EntityId): void { + if (!this.userImportFeatures.userMigrationEnabled || !this.userImportFeatures.userMigrationSystemId) { + throw new UserMigrationIsNotEnabledLoggableException(userId); + } + } +} diff --git a/apps/server/src/modules/user-import/uc/user-import.uc.spec.ts b/apps/server/src/modules/user-import/uc/user-import.uc.spec.ts index b694691f1f5..eea7eafe86e 100644 --- a/apps/server/src/modules/user-import/uc/user-import.uc.spec.ts +++ b/apps/server/src/modules/user-import/uc/user-import.uc.spec.ts @@ -1,7 +1,5 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { Configuration } from '@hpi-schul-cloud/commons'; import { MongoMemoryDatabaseModule } from '@infra/database'; -import { ObjectId } from '@mikro-orm/mongodb'; import { AccountService } from '@modules/account/services/account.service'; import { AuthorizationService } from '@modules/authorization'; import { LegacySchoolService } from '@modules/legacy-school'; @@ -14,9 +12,10 @@ import { ImportUser, MatchCreator, SchoolEntity, SystemEntity, User } from '@sha import { Permission } from '@shared/domain/interface'; import { MatchCreatorScope, SchoolFeature } from '@shared/domain/types'; import { ImportUserRepo, LegacySystemRepo, UserRepo } from '@shared/repo'; -import { federalStateFactory, importUserFactory, schoolFactory, userFactory } from '@shared/testing'; +import { federalStateFactory, importUserFactory, schoolEntityFactory, userFactory } from '@shared/testing'; import { systemEntityFactory } from '@shared/testing/factory/systemEntityFactory'; import { LoggerModule } from '@src/core/logger'; +import { UserImportService } from '../service'; import { LdapAlreadyPersistedException, MigrationAlreadyActivatedException, @@ -34,7 +33,7 @@ describe('[ImportUserModule]', () => { let systemRepo: DeepMocked; let userRepo: DeepMocked; let authorizationService: DeepMocked; - let configurationSpy: jest.SpyInstance; + let userImportService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -69,8 +68,13 @@ describe('[ImportUserModule]', () => { provide: AuthorizationService, useValue: createMock(), }, + { + provide: UserImportService, + useValue: createMock(), + }, ], }).compile(); + uc = module.get(UserImportUc); // TODO UserRepo not available in UserUc?! accountService = module.get(AccountService); importUserRepo = module.get(ImportUserRepo); @@ -78,6 +82,7 @@ describe('[ImportUserModule]', () => { systemRepo = module.get(LegacySystemRepo); userRepo = module.get(UserRepo); authorizationService = module.get(AuthorizationService); + userImportService = module.get(UserImportService); }); afterAll(async () => { @@ -94,19 +99,6 @@ describe('[ImportUserModule]', () => { expect(authorizationService).toBeDefined(); }); - const setConfig = (systemId?: string) => { - const mockSystemId = systemId || new ObjectId().toString(); - configurationSpy = jest.spyOn(Configuration, 'get').mockImplementation((config: string) => { - if (config === 'FEATURE_USER_MIGRATION_SYSTEM_ID') { - return mockSystemId; - } - if (config === 'FEATURE_USER_MIGRATION_ENABLED') { - return true; - } - return null; - }); - }; - const createMockSchoolDo = (school?: SchoolEntity): LegacySchoolDo => { const name = school ? school.name : 'testSchool'; const id = school ? school.id : 'someId'; @@ -134,10 +126,6 @@ describe('[ImportUserModule]', () => { }); }; - beforeEach(() => { - setConfig(); - }); - describe('[findAllImportUsers]', () => { it('Should request authorization service', async () => { const user = userFactory.buildWithId(); @@ -185,7 +173,7 @@ describe('[ImportUserModule]', () => { describe('[setMatch]', () => { describe('When not having same school for current user, user match and importuser', () => { it('should not change match', async () => { - const school = schoolFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); const user = userFactory.buildWithId(); const importUser = importUserFactory.buildWithId({ school }); const userRepoByIdSpy = jest.spyOn(userRepo, 'findById').mockResolvedValue(user); @@ -211,7 +199,7 @@ describe('[ImportUserModule]', () => { describe('When having same school for current user, user-match and importuser', () => { describe('When not having a user already assigned as match', () => { it('should set user as new match', async () => { - const school = schoolFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); const currentUser = userFactory.buildWithId({ school }); const usermatch = userFactory.buildWithId({ school }); const importUser = importUserFactory.buildWithId({ school }); @@ -252,7 +240,7 @@ describe('[ImportUserModule]', () => { describe('When having a user already assigned as match', () => { it('should not set user as new match twice', async () => { - const school = schoolFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); const currentUser = userFactory.buildWithId({ school }); const usermatch = userFactory.buildWithId({ school }); const importUser = importUserFactory.buildWithId({ school }); @@ -301,7 +289,7 @@ describe('[ImportUserModule]', () => { describe('When having permission Permission.SCHOOL_IMPORT_USERS_UPDATE', () => { describe('When not having same school for user and importuser', () => { it('should not change flag', async () => { - const school = schoolFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); const user = userFactory.buildWithId(); const importUser = importUserFactory.buildWithId({ school }); const userRepoByIdSpy = jest.spyOn(userRepo, 'findById').mockResolvedValue(user); @@ -324,7 +312,7 @@ describe('[ImportUserModule]', () => { }); describe('When having same school for user and importuser', () => { it('should enable flag', async () => { - const school = schoolFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); const user = userFactory.buildWithId({ school }); const importUser = importUserFactory.buildWithId({ school }); const userRepoByIdSpy = jest.spyOn(userRepo, 'findById').mockResolvedValue(user); @@ -350,7 +338,7 @@ describe('[ImportUserModule]', () => { importUserSaveSpy.mockRestore(); }); it('should disable flag', async () => { - const school = schoolFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); const user = userFactory.buildWithId({ school }); const importUser = importUserFactory.buildWithId({ school }); const userRepoByIdSpy = jest.spyOn(userRepo, 'findById').mockResolvedValue(user); @@ -383,7 +371,7 @@ describe('[ImportUserModule]', () => { describe('When having permission Permission.SCHOOL_IMPORT_USERS_UPDATE', () => { describe('When having same school for user and importuser', () => { it('should revoke match', async () => { - const school = schoolFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); const user = userFactory.buildWithId({ school }); const importUser = importUserFactory.matched(MatchCreator.AUTO, user).buildWithId({ school }); const schoolServiceSpy = jest @@ -415,7 +403,7 @@ describe('[ImportUserModule]', () => { }); describe('When not having same school for user and importuser', () => { it('should not revoke match', async () => { - const school = schoolFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); const user = userFactory.buildWithId(); const usermatch = userFactory.buildWithId({ school }); const importUser = importUserFactory.matched(MatchCreator.AUTO, usermatch).buildWithId({ school }); @@ -466,7 +454,7 @@ describe('[ImportUserModule]', () => { let accountServiceFindByUserIdSpy: jest.SpyInstance; beforeEach(() => { system = systemEntityFactory.buildWithId(); - school = schoolFactory.buildWithId({ systems: [system] }); + school = schoolEntityFactory.buildWithId({ systems: [system] }); school.externalId = 'foo'; school.inMaintenanceSince = new Date(); school.inUserMigration = true; @@ -599,7 +587,7 @@ describe('[ImportUserModule]', () => { let dateSpy: jest.SpyInstance; beforeEach(() => { system = systemEntityFactory.buildWithId({ ldapConfig: {} }); - school = schoolFactory.buildWithId(); + school = schoolEntityFactory.buildWithId(); school.officialSchoolNumber = 'foo'; currentUser = userFactory.buildWithId({ school }); userRepoByIdSpy = userRepo.findById.mockResolvedValueOnce(currentUser); @@ -607,7 +595,6 @@ describe('[ImportUserModule]', () => { schoolServiceSaveSpy = schoolService.save.mockReturnValueOnce(Promise.resolve(createMockSchoolDo(school))); schoolServiceSpy = schoolService.getSchoolById.mockResolvedValue(createMockSchoolDo(school)); systemRepoSpy = systemRepo.findById.mockReturnValueOnce(Promise.resolve(system)); - setConfig(system.id); dateSpy = jest.spyOn(global, 'Date').mockReturnValue(currentDate as unknown as string); }); afterEach(() => { @@ -616,14 +603,12 @@ describe('[ImportUserModule]', () => { schoolServiceSaveSpy.mockRestore(); schoolServiceSpy.mockRestore(); systemRepoSpy.mockRestore(); - configurationSpy.mockRestore(); dateSpy.mockRestore(); }); - it('Should fetch system id from configuration', async () => { + it('Should fetch system id from user import features ', async () => { await uc.startSchoolInUserMigration(currentUser.id); - expect(configurationSpy).toHaveBeenCalledWith('FEATURE_USER_MIGRATION_SYSTEM_ID'); - expect(systemRepoSpy).toHaveBeenCalledWith(system.id); + expect(userImportService.checkFeatureEnabled).toBeDefined(); }); it('Should request authorization service', async () => { await uc.startSchoolInUserMigration(currentUser.id); @@ -631,12 +616,16 @@ describe('[ImportUserModule]', () => { expect(userRepoByIdSpy).toHaveBeenCalledWith(currentUser.id, true); expect(permissionServiceSpy).toHaveBeenCalledWith(currentUser, [Permission.SCHOOL_IMPORT_USERS_MIGRATE]); }); + it('Should save school params', async () => { schoolServiceSaveSpy.mockRestore(); schoolServiceSaveSpy = schoolService.save.mockImplementation((schoolDo: LegacySchoolDo) => Promise.resolve(schoolDo) ); + userImportService.getMigrationSystem.mockResolvedValueOnce(system); + await uc.startSchoolInUserMigration(currentUser.id); + const schoolParams: LegacySchoolDo = { ...createMockSchoolDo(school) }; schoolParams.inUserMigration = true; schoolParams.externalId = 'foo'; @@ -673,7 +662,7 @@ describe('[ImportUserModule]', () => { }); it('should throw if school already has a persisted LDAP ', async () => { dateSpy.mockRestore(); - school = schoolFactory.buildWithId({ systems: [system] }); + school = schoolEntityFactory.buildWithId({ systems: [system] }); schoolServiceSpy = schoolService.getSchoolById.mockResolvedValueOnce(createMockSchoolDo(school)); const result = uc.startSchoolInUserMigration(currentUser.id, false); await expect(result).rejects.toThrowError(LdapAlreadyPersistedException); @@ -699,7 +688,7 @@ describe('[ImportUserModule]', () => { let schoolServiceSaveSpy: jest.SpyInstance; let schoolServiceSpy: jest.SpyInstance; beforeEach(() => { - school = schoolFactory.buildWithId(); + school = schoolEntityFactory.buildWithId(); school.externalId = 'foo'; school.inMaintenanceSince = new Date(); school.inUserMigration = false; diff --git a/apps/server/src/modules/user-import/uc/user-import.uc.ts b/apps/server/src/modules/user-import/uc/user-import.uc.ts index e5ec10db892..046a1eccc2f 100644 --- a/apps/server/src/modules/user-import/uc/user-import.uc.ts +++ b/apps/server/src/modules/user-import/uc/user-import.uc.ts @@ -1,25 +1,24 @@ -import { Configuration } from '@hpi-schul-cloud/commons'; +import { AccountSaveDto } from '@modules/account'; import { AccountService } from '@modules/account/services/account.service'; import { AccountDto } from '@modules/account/services/dto/account.dto'; import { AuthorizationService } from '@modules/authorization'; import { LegacySchoolService } from '@modules/legacy-school'; -import { BadRequestException, ForbiddenException, Injectable, InternalServerErrorException } from '@nestjs/common'; +import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common'; import { UserAlreadyAssignedToImportUserError } from '@shared/common'; import { LegacySchoolDo } from '@shared/domain/domainobject'; import { Account, ImportUser, MatchCreator, SystemEntity, User } from '@shared/domain/entity'; import { IFindOptions, Permission } from '@shared/domain/interface'; -import { Counted, EntityId, IImportUserScope, MatchCreatorScope, NameMatch, SchoolFeature } from '@shared/domain/types'; +import { Counted, EntityId, IImportUserScope, MatchCreatorScope, NameMatch } from '@shared/domain/types'; import { ImportUserRepo, LegacySystemRepo, UserRepo } from '@shared/repo'; import { Logger } from '@src/core/logger'; -import { AccountSaveDto } from '../../account/services/dto'; import { MigrationMayBeCompleted, MigrationMayNotBeCompleted, SchoolIdDoesNotMatchWithUserSchoolId, SchoolInUserMigrationEndLoggable, SchoolInUserMigrationStartLoggable, - UserMigrationIsNotEnabled, } from '../loggable'; +import { UserImportService } from '../service'; import { LdapAlreadyPersistedException, MigrationAlreadyActivatedException, @@ -40,20 +39,12 @@ export class UserImportUc { private readonly schoolService: LegacySchoolService, private readonly systemRepo: LegacySystemRepo, private readonly userRepo: UserRepo, - private readonly logger: Logger + private readonly logger: Logger, + private readonly userImportService: UserImportService ) { this.logger.setContext(UserImportUc.name); } - private checkFeatureEnabled(school: LegacySchoolDo): void | never { - const enabled = Configuration.get('FEATURE_USER_MIGRATION_ENABLED') as boolean; - const isLdapPilotSchool = school.features && school.features.includes(SchoolFeature.LDAP_UNIVENTION_MIGRATION); - if (!enabled && !isLdapPilotSchool) { - this.logger.warning(new UserMigrationIsNotEnabled()); - throw new InternalServerErrorException('User Migration not enabled'); - } - } - /** * Resolves with current users schools importusers and matched users. * @param currentUserId @@ -61,14 +52,14 @@ export class UserImportUc { * @param options * @returns */ - async findAllImportUsers( + public async findAllImportUsers( currentUserId: EntityId, query: IImportUserScope, options?: IFindOptions ): Promise> { const currentUser = await this.getCurrentUser(currentUserId, Permission.SCHOOL_IMPORT_USERS_VIEW); const school: LegacySchoolDo = await this.schoolService.getSchoolById(currentUser.school.id); - this.checkFeatureEnabled(school); + this.userImportService.checkFeatureEnabled(school); // TODO Change ImportUserRepo to DO to fix this workaround const countedImportUsers = await this.importUserRepo.findImportUsers(currentUser.school, query, options); return countedImportUsers; @@ -81,10 +72,10 @@ export class UserImportUc { * @param userMatchId * @returns importuser and matched user */ - async setMatch(currentUserId: EntityId, importUserId: EntityId, userMatchId: EntityId): Promise { + public async setMatch(currentUserId: EntityId, importUserId: EntityId, userMatchId: EntityId): Promise { const currentUser = await this.getCurrentUser(currentUserId, Permission.SCHOOL_IMPORT_USERS_UPDATE); const school: LegacySchoolDo = await this.schoolService.getSchoolById(currentUser.school.id); - this.checkFeatureEnabled(school); + this.userImportService.checkFeatureEnabled(school); const importUser = await this.importUserRepo.findById(importUserId); const userMatch = await this.userRepo.findById(userMatchId, true); @@ -106,10 +97,10 @@ export class UserImportUc { return importUser; } - async removeMatch(currentUserId: EntityId, importUserId: EntityId): Promise { + public async removeMatch(currentUserId: EntityId, importUserId: EntityId): Promise { const currentUser = await this.getCurrentUser(currentUserId, Permission.SCHOOL_IMPORT_USERS_UPDATE); const school: LegacySchoolDo = await this.schoolService.getSchoolById(currentUser.school.id); - this.checkFeatureEnabled(school); + this.userImportService.checkFeatureEnabled(school); const importUser = await this.importUserRepo.findById(importUserId); // check same school if (school.id !== importUser.school.id) { @@ -123,10 +114,10 @@ export class UserImportUc { return importUser; } - async updateFlag(currentUserId: EntityId, importUserId: EntityId, flagged: boolean): Promise { + public async updateFlag(currentUserId: EntityId, importUserId: EntityId, flagged: boolean): Promise { const currentUser = await this.getCurrentUser(currentUserId, Permission.SCHOOL_IMPORT_USERS_UPDATE); const school: LegacySchoolDo = await this.schoolService.getSchoolById(currentUser.school.id); - this.checkFeatureEnabled(school); + this.userImportService.checkFeatureEnabled(school); const importUser = await this.importUserRepo.findById(importUserId); // check same school @@ -150,23 +141,23 @@ export class UserImportUc { * @param options * @returns */ - async findAllUnmatchedUsers( + public async findAllUnmatchedUsers( currentUserId: EntityId, query: NameMatch, options?: IFindOptions ): Promise> { const currentUser = await this.getCurrentUser(currentUserId, Permission.SCHOOL_IMPORT_USERS_VIEW); const school: LegacySchoolDo = await this.schoolService.getSchoolById(currentUser.school.id); - this.checkFeatureEnabled(school); + this.userImportService.checkFeatureEnabled(school); // TODO Change to UserService to fix this workaround const unmatchedCountedUsers = await this.userRepo.findWithoutImportUser(currentUser.school, query, options); return unmatchedCountedUsers; } - async saveAllUsersMatches(currentUserId: EntityId): Promise { + public async saveAllUsersMatches(currentUserId: EntityId): Promise { const currentUser = await this.getCurrentUser(currentUserId, Permission.SCHOOL_IMPORT_USERS_MIGRATE); const school: LegacySchoolDo = await this.schoolService.getSchoolById(currentUser.school.id); - this.checkFeatureEnabled(school); + this.userImportService.checkFeatureEnabled(school); const filters: IImportUserScope = { matches: [MatchCreatorScope.MANUAL, MatchCreatorScope.AUTO] }; // TODO batch/paginated import? const options: IFindOptions = {}; @@ -208,7 +199,7 @@ export class UserImportUc { private async endSchoolInUserMigration(currentUserId: EntityId): Promise { const currentUser = await this.getCurrentUser(currentUserId, Permission.SCHOOL_IMPORT_USERS_MIGRATE); const school: LegacySchoolDo = await this.schoolService.getSchoolById(currentUser.school.id); - this.checkFeatureEnabled(school); + this.userImportService.checkFeatureEnabled(school); if (!school.externalId || school.inUserMigration !== true || !school.inMaintenanceSince) { this.logger.warning(new MigrationMayBeCompleted(school.inUserMigration)); throw new BadRequestException('School cannot exit from user migration mode'); @@ -217,11 +208,11 @@ export class UserImportUc { await this.schoolService.save(school); } - async startSchoolInUserMigration(currentUserId: EntityId, useCentralLdap = true): Promise { + public async startSchoolInUserMigration(currentUserId: EntityId, useCentralLdap = true): Promise { const currentUser = await this.getCurrentUser(currentUserId, Permission.SCHOOL_IMPORT_USERS_MIGRATE); const school: LegacySchoolDo = await this.schoolService.getSchoolById(currentUser.school.id); this.logger.notice(new SchoolInUserMigrationStartLoggable(currentUserId, school.name, useCentralLdap)); - this.checkFeatureEnabled(school); + this.userImportService.checkFeatureEnabled(school); this.checkSchoolNumber(school, useCentralLdap); this.checkSchoolNotInMigration(school); await this.checkNoExistingLdapBeforeStart(school); @@ -230,7 +221,7 @@ export class UserImportUc { school.inMaintenanceSince = new Date(); school.externalId = school.officialSchoolNumber; if (useCentralLdap) { - const migrationSystem = await this.getMigrationSystem(); + const migrationSystem = await this.userImportService.getMigrationSystem(); if (school.systems && !school.systems.includes(migrationSystem.id)) { school.systems.push(migrationSystem.id); } @@ -239,10 +230,10 @@ export class UserImportUc { await this.schoolService.save(school); } - async endSchoolInMaintenance(currentUserId: EntityId): Promise { + public async endSchoolInMaintenance(currentUserId: EntityId): Promise { const currentUser = await this.getCurrentUser(currentUserId, Permission.SCHOOL_IMPORT_USERS_MIGRATE); const school: LegacySchoolDo = await this.schoolService.getSchoolById(currentUser.school.id); - this.checkFeatureEnabled(school); + this.userImportService.checkFeatureEnabled(school); if (school.inUserMigration !== false || !school.inMaintenanceSince || !school.externalId) { this.logger.warning(new MigrationMayNotBeCompleted(school.inUserMigration)); throw new BadRequestException('Sync cannot be activated for school'); @@ -296,12 +287,6 @@ export class UserImportUc { return account; } - private async getMigrationSystem(): Promise { - const systemId = Configuration.get('FEATURE_USER_MIGRATION_SYSTEM_ID') as string; - const system = await this.systemRepo.findById(systemId); - return system; - } - private async checkNoExistingLdapBeforeStart(school: LegacySchoolDo): Promise { if (school.systems && school.systems?.length > 0) { for (const systemId of school.systems) { diff --git a/apps/server/src/modules/user-import/user-import-config.module.ts b/apps/server/src/modules/user-import/user-import-config.module.ts new file mode 100644 index 00000000000..34ba1e5f994 --- /dev/null +++ b/apps/server/src/modules/user-import/user-import-config.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { UserImportConfiguration, UserImportFeatures } from './config'; + +@Module({ + providers: [ + { + provide: UserImportFeatures, + useValue: UserImportConfiguration.userImportFeatures, + }, + ], + exports: [UserImportFeatures], +}) +export class UserImportConfigModule {} diff --git a/apps/server/src/modules/user-import/user-import.module.ts b/apps/server/src/modules/user-import/user-import.module.ts index 48fa730c61f..190c73f59b0 100644 --- a/apps/server/src/modules/user-import/user-import.module.ts +++ b/apps/server/src/modules/user-import/user-import.module.ts @@ -1,16 +1,41 @@ +import { SchulconnexClientModule } from '@infra/schulconnex-client'; +import { AccountModule } from '@modules/account'; +import { AuthorizationModule } from '@modules/authorization'; import { LegacySchoolModule } from '@modules/legacy-school'; +import { OauthModule } from '@modules/oauth'; +import { UserModule } from '@modules/user'; +import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { ImportUserRepo, LegacySchoolRepo, LegacySystemRepo, UserRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; -import { AccountModule } from '../account'; -import { AuthorizationModule } from '../authorization'; import { ImportUserController } from './controller/import-user.controller'; -import { UserImportUc } from './uc/user-import.uc'; +import { SchulconnexFetchImportUsersService, UserImportService } from './service'; +import { UserImportFetchUc, UserImportUc } from './uc'; +import { UserImportConfigModule } from './user-import-config.module'; @Module({ - imports: [LoggerModule, AccountModule, LegacySchoolModule, AuthorizationModule], + imports: [ + LoggerModule, + AccountModule, + LegacySchoolModule, + AuthorizationModule, + UserImportConfigModule, + HttpModule, + UserModule, + OauthModule, + SchulconnexClientModule, + ], controllers: [ImportUserController], - providers: [UserImportUc, ImportUserRepo, LegacySchoolRepo, LegacySystemRepo, UserRepo], + providers: [ + UserImportUc, + UserImportFetchUc, + ImportUserRepo, + LegacySchoolRepo, + LegacySystemRepo, + UserRepo, + UserImportService, + SchulconnexFetchImportUsersService, + ], exports: [], }) /** diff --git a/apps/server/src/modules/user-login-migration/controller/api-test/user-login-migration.api.spec.ts b/apps/server/src/modules/user-login-migration/controller/api-test/user-login-migration.api.spec.ts index b8b10c4a379..c72c462294b 100644 --- a/apps/server/src/modules/user-login-migration/controller/api-test/user-login-migration.api.spec.ts +++ b/apps/server/src/modules/user-login-migration/controller/api-test/user-login-migration.api.spec.ts @@ -1,7 +1,7 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { SanisResponse, SanisRole } from '@infra/schulconnex-client'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { OauthTokenResponse } from '@modules/oauth/service/dto'; -import { SanisResponse, SanisRole } from '@modules/provisioning'; import { ServerTestModule } from '@modules/server'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; @@ -9,12 +9,12 @@ import { SchoolEntity, SystemEntity, User } from '@shared/domain/entity'; import { UserLoginMigrationEntity } from '@shared/domain/entity/user-login-migration.entity'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { + cleanupCollections, JwtTestFactory, + schoolEntityFactory, + systemEntityFactory, TestApiClient, UserAndAccountTestFactory, - cleanupCollections, - schoolFactory, - systemEntityFactory, userFactory, userLoginMigrationFactory, } from '@shared/testing'; @@ -73,7 +73,7 @@ describe('UserLoginMigrationController (API)', () => { const date: Date = new Date(2023, 5, 4); const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [sourceSystem], }); const userLoginMigration: UserLoginMigrationEntity = userLoginMigrationFactory.buildWithId({ @@ -138,7 +138,7 @@ describe('UserLoginMigrationController (API)', () => { const date: Date = new Date(2023, 5, 4); const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [sourceSystem], }); const userLoginMigration: UserLoginMigrationEntity = userLoginMigrationFactory.buildWithId({ @@ -185,7 +185,7 @@ describe('UserLoginMigrationController (API)', () => { describe('when no user login migration exists', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }); await em.persistAndFlush([school, adminAccount, adminUser]); @@ -234,7 +234,7 @@ describe('UserLoginMigrationController (API)', () => { const setup = async () => { const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', }); @@ -318,7 +318,7 @@ describe('UserLoginMigrationController (API)', () => { const date: Date = new Date(2023, 5, 4); const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', }); @@ -357,7 +357,7 @@ describe('UserLoginMigrationController (API)', () => { const date: Date = new Date(2023, 5, 4); const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', }); @@ -397,7 +397,7 @@ describe('UserLoginMigrationController (API)', () => { const setup = async () => { const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [sourceSystem], }); @@ -477,7 +477,7 @@ describe('UserLoginMigrationController (API)', () => { const officialSchoolNumber = '12345'; const externalId = 'aef1f4fd-c323-466e-962b-a84354c0e713'; - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber, externalId, @@ -544,7 +544,7 @@ describe('UserLoginMigrationController (API)', () => { const officialSchoolNumber = '12345'; const externalId = 'aef1f4fd-c323-466e-962b-a84354c0e713'; - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber, externalId, @@ -619,7 +619,7 @@ describe('UserLoginMigrationController (API)', () => { const setup = async () => { const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', }); @@ -718,7 +718,7 @@ describe('UserLoginMigrationController (API)', () => { const setup = async () => { const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', }); @@ -756,7 +756,7 @@ describe('UserLoginMigrationController (API)', () => { const setup = async () => { const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', }); @@ -799,7 +799,7 @@ describe('UserLoginMigrationController (API)', () => { const setup = async () => { const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', }); @@ -851,7 +851,7 @@ describe('UserLoginMigrationController (API)', () => { const setup = async () => { const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', }); @@ -903,7 +903,7 @@ describe('UserLoginMigrationController (API)', () => { const setup = async () => { const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', }); @@ -933,7 +933,7 @@ describe('UserLoginMigrationController (API)', () => { const setup = async () => { const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', }); @@ -981,7 +981,7 @@ describe('UserLoginMigrationController (API)', () => { const setup = async () => { const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', }); @@ -1023,7 +1023,7 @@ describe('UserLoginMigrationController (API)', () => { const setup = async () => { const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', }); @@ -1099,7 +1099,7 @@ describe('UserLoginMigrationController (API)', () => { const setup = async () => { const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', }); @@ -1142,7 +1142,7 @@ describe('UserLoginMigrationController (API)', () => { const setup = async () => { const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', }); @@ -1208,7 +1208,7 @@ describe('UserLoginMigrationController (API)', () => { const setup = async () => { const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', }); @@ -1256,7 +1256,7 @@ describe('UserLoginMigrationController (API)', () => { const setup = async () => { const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', }); @@ -1295,7 +1295,7 @@ describe('UserLoginMigrationController (API)', () => { const setup = async () => { const sourceSystem: SystemEntity = systemEntityFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); const targetSystem: SystemEntity = systemEntityFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); - const school: SchoolEntity = schoolFactory.buildWithId({ + const school: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [sourceSystem], officialSchoolNumber: '12345', }); diff --git a/apps/server/src/modules/user/service/user.service.spec.ts b/apps/server/src/modules/user/service/user.service.spec.ts index 4ef690fe99f..a217ee65185 100644 --- a/apps/server/src/modules/user/service/user.service.spec.ts +++ b/apps/server/src/modules/user/service/user.service.spec.ts @@ -459,4 +459,30 @@ describe('UserService', () => { expect(result).toEqual(parentEmail); }); }); + + describe('findUserBySchoolAndName', () => { + describe('when searching for users by school and name', () => { + const setup = () => { + const firstName = 'Frist'; + const lastName = 'Last'; + const users: User[] = userFactory.buildListWithId(2, { firstName, lastName }); + + userRepo.findUserBySchoolAndName.mockResolvedValue(users); + + return { + firstName, + lastName, + users, + }; + }; + + it('should return a list of users', async () => { + const { firstName, lastName, users } = setup(); + + const result: User[] = await service.findUserBySchoolAndName(new ObjectId().toHexString(), firstName, lastName); + + expect(result).toEqual(users); + }); + }); + }); }); diff --git a/apps/server/src/modules/user/service/user.service.ts b/apps/server/src/modules/user/service/user.service.ts index 796b1d92181..acc97cf2d81 100644 --- a/apps/server/src/modules/user/service/user.service.ts +++ b/apps/server/src/modules/user/service/user.service.ts @@ -7,6 +7,7 @@ import { RoleDto } from '@modules/role/service/dto/role.dto'; import { RoleService } from '@modules/role/service/role.service'; import { BadRequestException, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { DataDeletionDomainOperationLoggable } from '@shared/common/loggable'; import { Page, RoleReference, UserDO } from '@shared/domain/domainobject'; import { LanguageType, User } from '@shared/domain/entity'; import { IFindOptions } from '@shared/domain/interface'; @@ -14,7 +15,6 @@ import { DomainModel, EntityId, StatusModel } from '@shared/domain/types'; import { UserRepo } from '@shared/repo'; import { UserDORepo } from '@shared/repo/user/user-do.repo'; import { Logger } from '@src/core/logger'; -import { DataDeletionDomainOperationLoggable } from '@shared/common/loggable'; import { UserConfig } from '../interfaces'; import { UserMapper } from '../mapper/user.mapper'; import { UserDto } from '../uc/dto/user.dto'; @@ -152,4 +152,10 @@ export class UserService { return parentEmails; } + + public async findUserBySchoolAndName(schoolId: EntityId, firstName: string, lastName: string): Promise { + const users: User[] = await this.userRepo.findUserBySchoolAndName(schoolId, firstName, lastName); + + return users; + } } diff --git a/apps/server/src/modules/video-conference/controller/api-test/video-conference.api.spec.ts b/apps/server/src/modules/video-conference/controller/api-test/video-conference.api.spec.ts index 25865af926e..1219a52c46e 100644 --- a/apps/server/src/modules/video-conference/controller/api-test/video-conference.api.spec.ts +++ b/apps/server/src/modules/video-conference/controller/api-test/video-conference.api.spec.ts @@ -10,7 +10,7 @@ import { cleanupCollections, courseFactory, roleFactory, - schoolFactory, + schoolEntityFactory, TestApiClient, UserAndAccountTestFactory, userFactory, @@ -151,7 +151,7 @@ describe('VideoConferenceController (API)', () => { describe('when the logoutUrl is from a wrong origin', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId({ features: [] }); + const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [] }); const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.START_MEETING, @@ -191,7 +191,7 @@ describe('VideoConferenceController (API)', () => { describe('when conference params are given', () => { describe('when school has not enabled the school feature videoconference', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId({ features: [] }); + const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [] }); const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.START_MEETING, @@ -230,7 +230,7 @@ describe('VideoConferenceController (API)', () => { describe('when user has not the required permission', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); + const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); const studentRole: Role = roleFactory.buildWithId({ name: RoleName.STUDENT, permissions: [Permission.JOIN_MEETING], @@ -272,7 +272,7 @@ describe('VideoConferenceController (API)', () => { describe('when user has the required permission', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); + const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.START_MEETING, @@ -311,7 +311,7 @@ describe('VideoConferenceController (API)', () => { describe('when conference is for scope and scopeId is already running', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); + const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.START_MEETING, @@ -362,7 +362,7 @@ describe('VideoConferenceController (API)', () => { describe('when scope and scopeId are given', () => { describe('when school has not enabled the school feature videoconference', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId({ features: [] }); + const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [] }); const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.START_MEETING, @@ -399,7 +399,7 @@ describe('VideoConferenceController (API)', () => { describe('when user has the required permission', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); + const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.START_MEETING, @@ -439,7 +439,7 @@ describe('VideoConferenceController (API)', () => { describe('when conference is not running', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); + const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.START_MEETING, @@ -488,7 +488,7 @@ describe('VideoConferenceController (API)', () => { describe('when scope and scopeId are given', () => { describe('when school has not enabled the school feature videoconference', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId({ features: [] }); + const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [] }); const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.START_MEETING, @@ -524,7 +524,7 @@ describe('VideoConferenceController (API)', () => { describe('when user has the required permission', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); + const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.START_MEETING, @@ -561,7 +561,7 @@ describe('VideoConferenceController (API)', () => { describe('when guest want meeting info of conference without waiting room', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); + const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); const expertRole: Role = roleFactory.buildWithId({ name: RoleName.EXPERT, @@ -602,7 +602,7 @@ describe('VideoConferenceController (API)', () => { describe('when conference is not running', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); + const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.START_MEETING, @@ -651,7 +651,7 @@ describe('VideoConferenceController (API)', () => { describe('when scope and scopeId are given', () => { describe('when school has not enabled the school feature videoconference', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId({ features: [] }); + const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [] }); const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.START_MEETING, @@ -688,7 +688,7 @@ describe('VideoConferenceController (API)', () => { describe('when a user without required permission wants to end a conference', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); + const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent({ school }, [ Permission.JOIN_MEETING, @@ -722,7 +722,7 @@ describe('VideoConferenceController (API)', () => { describe('when a user with required permission wants to end a conference', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); + const school: SchoolEntity = schoolEntityFactory.buildWithId({ features: [SchoolFeature.VIDEOCONFERENCE] }); const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.START_MEETING, diff --git a/apps/server/src/shared/domain/entity/course.entity.spec.ts b/apps/server/src/shared/domain/entity/course.entity.spec.ts index 4a1ff02bdd6..85af8b7499a 100644 --- a/apps/server/src/shared/domain/entity/course.entity.spec.ts +++ b/apps/server/src/shared/domain/entity/course.entity.spec.ts @@ -1,6 +1,6 @@ import { MikroORM } from '@mikro-orm/core'; import { InternalServerErrorException } from '@nestjs/common'; -import { courseFactory, courseGroupFactory, schoolFactory, setupEntities, userFactory } from '@shared/testing'; +import { courseFactory, courseGroupFactory, schoolEntityFactory, setupEntities, userFactory } from '@shared/testing'; import { ObjectId } from 'bson'; import { Course } from './course.entity'; import { CourseGroup } from './coursegroup.entity'; @@ -33,7 +33,7 @@ describe('CourseEntity', () => { describe('defaults', () => { it('should return defaults values', () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const course = new Course({ school }); expect(course.name).toEqual(DEFAULT.name); diff --git a/apps/server/src/shared/domain/entity/import-user.entity.spec.ts b/apps/server/src/shared/domain/entity/import-user.entity.spec.ts index c241bb96471..cdd22330733 100644 --- a/apps/server/src/shared/domain/entity/import-user.entity.spec.ts +++ b/apps/server/src/shared/domain/entity/import-user.entity.spec.ts @@ -1,4 +1,4 @@ -import { importUserFactory, schoolFactory, setupEntities, userFactory } from '@shared/testing'; +import { importUserFactory, schoolEntityFactory, setupEntities, userFactory } from '@shared/testing'; import { MatchCreator } from '.'; describe('ImportUser entity', () => { @@ -43,7 +43,7 @@ describe('ImportUser entity', () => { describe('match', () => { it('should set and unset both, user and matchedBy', () => { - const school = schoolFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); const user = userFactory.buildWithId({ school }); const importUser = importUserFactory.matched(MatchCreator.AUTO, user).buildWithId({ school }); expect(importUser.user).toEqual(user); diff --git a/apps/server/src/shared/domain/entity/import-user.entity.ts b/apps/server/src/shared/domain/entity/import-user.entity.ts index 34d1f8b64e8..78aa5036cb4 100644 --- a/apps/server/src/shared/domain/entity/import-user.entity.ts +++ b/apps/server/src/shared/domain/entity/import-user.entity.ts @@ -123,4 +123,8 @@ export class ImportUser extends BaseEntityWithTimestamps implements EntityWithSc this.user = undefined; this.matchedBy = undefined; } + + static isImportUserRole(role: RoleName): role is IImportUserRoleName { + return role === RoleName.ADMINISTRATOR || role === RoleName.STUDENT || role === RoleName.TEACHER; + } } diff --git a/apps/server/src/shared/domain/entity/submission.entity.spec.ts b/apps/server/src/shared/domain/entity/submission.entity.spec.ts index f65eea0b2c9..1a778f612c2 100644 --- a/apps/server/src/shared/domain/entity/submission.entity.spec.ts +++ b/apps/server/src/shared/domain/entity/submission.entity.spec.ts @@ -2,7 +2,7 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { InternalServerErrorException } from '@nestjs/common'; import { courseGroupFactory, - schoolFactory, + schoolEntityFactory, setupEntities, submissionFactory, taskFactory, @@ -21,7 +21,7 @@ describe('Submission entity', () => { describe('constructor is called', () => { const setup = () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const student = userFactory.build(); const task = taskFactory.buildWithId(); const teamMember = userFactory.build(); diff --git a/apps/server/src/shared/domain/entity/task.entity.spec.ts b/apps/server/src/shared/domain/entity/task.entity.spec.ts index a8a1007a898..9686164769c 100644 --- a/apps/server/src/shared/domain/entity/task.entity.spec.ts +++ b/apps/server/src/shared/domain/entity/task.entity.spec.ts @@ -3,7 +3,7 @@ import { courseFactory, courseGroupFactory, lessonFactory, - schoolFactory, + schoolEntityFactory, setupEntities, submissionFactory, taskFactory, @@ -858,7 +858,7 @@ describe('Task Entity', () => { describe('getSchoolId', () => { it('schould return schoolId from school', () => { - const school = schoolFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); const task = taskFactory.buildWithId({ school }); const schoolId = task.getSchoolId(); diff --git a/apps/server/src/shared/domain/entity/user.entity.spec.ts b/apps/server/src/shared/domain/entity/user.entity.spec.ts index c10a26bc2c4..0d415207189 100644 --- a/apps/server/src/shared/domain/entity/user.entity.spec.ts +++ b/apps/server/src/shared/domain/entity/user.entity.spec.ts @@ -1,5 +1,5 @@ import { MikroORM } from '@mikro-orm/core'; -import { roleFactory, schoolFactory, setupEntities, userFactory } from '@shared/testing'; +import { roleFactory, schoolEntityFactory, setupEntities, userFactory } from '@shared/testing'; import { ObjectId } from 'bson'; import { Role } from '.'; import { Permission } from '../interface'; @@ -24,7 +24,7 @@ describe('User Entity', () => { }); it('should create a user when passing required properties', () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const user = new User({ firstName: 'John', lastName: 'Cale', diff --git a/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.integration.spec.ts b/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.integration.spec.ts index eb777e8b5d8..1ac8d8171a0 100644 --- a/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.integration.spec.ts @@ -14,8 +14,8 @@ import { cleanupCollections, contextExternalToolEntityFactory, contextExternalToolFactory, + schoolEntityFactory, schoolExternalToolEntityFactory, - schoolFactory, } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { ContextExternalToolRepo } from './context-external-tool.repo'; @@ -51,7 +51,7 @@ describe('ContextExternalToolRepo', () => { }); const createExternalTools = () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const schoolExternalTool1: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ school }); const schoolExternalTool2: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ school }); const contextExternalTool1: ContextExternalToolEntity = contextExternalToolEntityFactory.buildWithId({ diff --git a/apps/server/src/shared/repo/importuser/importuser.repo.integration.spec.ts b/apps/server/src/shared/repo/importuser/importuser.repo.integration.spec.ts index 1e5d40462db..9f70c446495 100644 --- a/apps/server/src/shared/repo/importuser/importuser.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/importuser/importuser.repo.integration.spec.ts @@ -1,12 +1,11 @@ -import { EntityManager } from '@mikro-orm/mongodb'; -import { Test, TestingModule } from '@nestjs/testing'; -import { cleanupCollections, importUserFactory, schoolFactory, userFactory } from '@shared/testing'; - import { MongoMemoryDatabaseModule } from '@infra/database'; import { MikroORM, NotFoundError } from '@mikro-orm/core'; +import { EntityManager } from '@mikro-orm/mongodb'; +import { Test, TestingModule } from '@nestjs/testing'; import { IImportUserRoleName, ImportUser, MatchCreator, SchoolEntity, User } from '@shared/domain/entity'; import { RoleName } from '@shared/domain/interface'; import { MatchCreatorScope } from '@shared/domain/types'; +import { cleanupCollections, importUserFactory, schoolEntityFactory, userFactory } from '@shared/testing'; import { ImportUserRepo } from '.'; describe('ImportUserRepo', () => { @@ -16,7 +15,7 @@ describe('ImportUserRepo', () => { let orm: MikroORM; const persistedReferences = async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const user = userFactory.build({ school }); await em.persistAndFlush([school, user]); return { user, school }; @@ -105,7 +104,7 @@ describe('ImportUserRepo', () => { describe('[findImportUsers] find importUsers scope integration', () => { describe('bySchool', () => { it('should respond with given schools importUsers', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ school }); const otherSchoolsImportUser = importUserFactory.build(); await em.persistAndFlush([school, importUser, otherSchoolsImportUser]); @@ -114,15 +113,15 @@ describe('ImportUserRepo', () => { expect(count).toEqual(1); }); it('should not respond with other schools than requested', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ school }); - const otherSchoolsImportUser = importUserFactory.build({ school: schoolFactory.build() }); + const otherSchoolsImportUser = importUserFactory.build({ school: schoolEntityFactory.build() }); await em.persistAndFlush([school, importUser, otherSchoolsImportUser]); const [results] = await repo.findImportUsers(school); expect(results).not.toContain(otherSchoolsImportUser); }); it('should not respond with any school for wrong id given', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ school }); const otherSchoolsImportUser = importUserFactory.build(); await em.persistAndFlush([school, importUser, otherSchoolsImportUser]); @@ -131,7 +130,7 @@ describe('ImportUserRepo', () => { ).rejects.toThrowError('invalid school id'); }); it('should not respond with any school for wrong id given', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ school }); const otherSchoolsImportUser = importUserFactory.build(); await em.persistAndFlush([school, importUser, otherSchoolsImportUser]); @@ -143,7 +142,7 @@ describe('ImportUserRepo', () => { describe('byFirstName', () => { it('should find fully matching firstnames "exact match"', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ firstName: 'Marie-Luise', school }); const otherImportUser = importUserFactory.build({ firstName: 'Peter', school }); await em.persistAndFlush([school, importUser, otherImportUser]); @@ -153,7 +152,7 @@ describe('ImportUserRepo', () => { expect(count).toEqual(1); }); it('should find partially matching firstnames "ignoring case"', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ firstName: 'Marie-Luise', school }); const otherImportUser = importUserFactory.build({ firstName: 'Marie', school }); await em.persistAndFlush([school, importUser, otherImportUser]); @@ -163,7 +162,7 @@ describe('ImportUserRepo', () => { expect(count).toEqual(1); }); it('should find partially matching firstname "starts-with"', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ firstName: 'Marie-Luise', school }); const otherImportUser = importUserFactory.build({ firstName: 'Marie', school }); const otherImportUser2 = importUserFactory.build({ firstName: 'Peter', school }); @@ -175,7 +174,7 @@ describe('ImportUserRepo', () => { expect(count).toEqual(2); }); it('should find partially matching firstname "ends-with"', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ firstName: 'Marie-Luise', school }); const otherImportUser = importUserFactory.build({ firstName: 'Luise', school }); const otherImportUser2 = importUserFactory.build({ firstName: 'Peter', school }); @@ -187,7 +186,7 @@ describe('ImportUserRepo', () => { expect(count).toEqual(2); }); it('should skip firstname filter for undefined values', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ firstName: 'Marie-Luise', school }); const otherImportUser = importUserFactory.build({ firstName: 'Peter', school }); await em.persistAndFlush([school, importUser, otherImportUser]); @@ -197,7 +196,7 @@ describe('ImportUserRepo', () => { expect(count).toEqual(2); }); it('should skip firstname filter for empty string', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ firstName: 'Marie-Luise', school }); const otherImportUser = importUserFactory.build({ firstName: 'Peter', school }); await em.persistAndFlush([school, importUser, otherImportUser]); @@ -207,7 +206,7 @@ describe('ImportUserRepo', () => { expect(count).toEqual(2); }); it('should skip special chars from filter', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ firstName: 'Marie-Luise', school }); const otherImportUser = importUserFactory.build({ firstName: 'Peter', school }); await em.persistAndFlush([school, importUser, otherImportUser]); @@ -217,7 +216,7 @@ describe('ImportUserRepo', () => { expect(count).toEqual(2); }); it('should keep characters as filter with language letters, numbers, space and minus', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ firstName: 'Marie-Luise áàâäãåçéèêëíìîïñóòôöõúùûüýÿæœÁÀÂÄÃÅÇÉÈÊËÍÌÎÏÑÓÒÔÖÕÚÙÛÜÝŸÆŒ0987654321', school, @@ -234,7 +233,7 @@ describe('ImportUserRepo', () => { }); describe('byLastName', () => { it('should find fully matching lastNames "exact match"', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ lastName: 'Marie-Luise', school }); const otherImportUser = importUserFactory.build({ lastName: 'Peter', school }); await em.persistAndFlush([school, importUser, otherImportUser]); @@ -244,7 +243,7 @@ describe('ImportUserRepo', () => { expect(count).toEqual(1); }); it('should find partially matching lastNames "ignoring case"', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ lastName: 'Marie-Luise', school }); const otherImportUser = importUserFactory.build({ lastName: 'Marie', school }); await em.persistAndFlush([school, importUser, otherImportUser]); @@ -254,7 +253,7 @@ describe('ImportUserRepo', () => { expect(count).toEqual(1); }); it('should find partially matching lastName "starts-with"', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ lastName: 'Marie-Luise', school }); const otherImportUser = importUserFactory.build({ lastName: 'Marie', school }); const otherImportUser2 = importUserFactory.build({ lastName: 'Peter', school }); @@ -266,7 +265,7 @@ describe('ImportUserRepo', () => { expect(count).toEqual(2); }); it('should find partially matching lastName "ends-with"', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ lastName: 'Marie-Luise', school }); const otherImportUser = importUserFactory.build({ lastName: 'Luise', school }); const otherImportUser2 = importUserFactory.build({ lastName: 'Peter', school }); @@ -278,7 +277,7 @@ describe('ImportUserRepo', () => { expect(count).toEqual(2); }); it('should skip lastName filter for undefined values', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ lastName: 'Marie-Luise', school }); const otherImportUser = importUserFactory.build({ lastName: 'Peter', school }); await em.persistAndFlush([school, importUser, otherImportUser]); @@ -288,7 +287,7 @@ describe('ImportUserRepo', () => { expect(count).toEqual(2); }); it('should skip lastName filter for empty string', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ lastName: 'Marie-Luise', school }); const otherImportUser = importUserFactory.build({ lastName: 'Peter', school }); await em.persistAndFlush([school, importUser, otherImportUser]); @@ -298,7 +297,7 @@ describe('ImportUserRepo', () => { expect(count).toEqual(2); }); it('should skip special chars from filter', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ lastName: 'Marie-Luise', school }); const otherImportUser = importUserFactory.build({ lastName: 'Peter', school }); await em.persistAndFlush([school, importUser, otherImportUser]); @@ -308,7 +307,7 @@ describe('ImportUserRepo', () => { expect(count).toEqual(2); }); it('should keep characters as filter with language letters, numbers, space and minus', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ lastName: 'Marie-Luise áàâäãåçéèêëíìîïñóòôöõúùûüýÿæœÁÀÂÄÃÅÇÉÈÊËÍÌÎÏÑÓÒÔÖÕÚÙÛÜÝŸÆŒ0987654321', school, @@ -326,7 +325,7 @@ describe('ImportUserRepo', () => { }); describe('byLoginName', () => { it('should find fully matching loginNames "exact match"', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ ldapDn: 'uid=MarieLuise12,cn=schueler,cn=users,ou=1,dc=training,dc=ucs', school, @@ -339,7 +338,7 @@ describe('ImportUserRepo', () => { expect(count).toEqual(1); }); it('should find partially matching loginNames "ignoring case"', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ ldapDn: 'uid=MarieLuise12,cn=schueler,cn=users,ou=1,dc=training,dc=ucs', school, @@ -352,7 +351,7 @@ describe('ImportUserRepo', () => { expect(count).toEqual(1); }); it('should find partially matching loginName "starts-with"', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ ldapDn: 'uid=MarieLuise12,cn=schueler,cn=users,ou=1,dc=training,dc=ucs', school, @@ -370,7 +369,7 @@ describe('ImportUserRepo', () => { expect(count).toEqual(2); }); it('should find partially matching loginName "ends-with"', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ ldapDn: 'uid=MarieLuise12,cn=schueler,cn=users,ou=1,dc=training,dc=ucs', school, @@ -388,7 +387,7 @@ describe('ImportUserRepo', () => { expect(count).toEqual(2); }); it('should skip loginName filter for undefined values', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ ldapDn: 'uid=MarieLuise12,cn=schueler,cn=users,ou=1,dc=training,dc=ucs', school, @@ -401,7 +400,7 @@ describe('ImportUserRepo', () => { expect(count).toEqual(2); }); it('should skip loginName filter for empty string', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ ldapDn: 'uid=MarieLuise12,cn=schueler,cn=users,ou=1,dc=training,dc=ucs', school, @@ -414,7 +413,7 @@ describe('ImportUserRepo', () => { expect(count).toEqual(2); }); it('should skip special chars from filter', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ ldapDn: 'uid=MarieLuise12,cn=schueler,cn=users,ou=1,dc=training,dc=ucs', school, @@ -427,7 +426,7 @@ describe('ImportUserRepo', () => { expect(count).toEqual(2); }); it('should keep characters as filter with language letters, numbers, space and minus', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ ldapDn: 'uid=Marie-Luise áàâäãåçéèêëíìîïñóòôöõúùûüýÿæœÁÀÂÄÃÅÇÉÈÊËÍÌÎÏÑÓÒÔÖÕÚÙÛÜÝŸÆŒ0987654321,foo', school, @@ -445,7 +444,7 @@ describe('ImportUserRepo', () => { describe('byRole', () => { it('should contain importusers with role name administrator', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ roleNames: [RoleName.ADMINISTRATOR], school, @@ -463,7 +462,7 @@ describe('ImportUserRepo', () => { expect(count).toEqual(2); // no other role name or no role name }); it('should contain importusers with role name student', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ roleNames: [RoleName.STUDENT], school, @@ -478,7 +477,7 @@ describe('ImportUserRepo', () => { expect(count).toEqual(2); // no other role name or no role name }); it('should contain importusers with role name teacher', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ roleNames: [RoleName.TEACHER], school, @@ -496,7 +495,7 @@ describe('ImportUserRepo', () => { expect(count).toEqual(2); // no other role name or no role name }); it('should fail for all other, invalid role names', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); await em.persistAndFlush(school); await expect(async () => repo.findImportUsers(school, { role: 'foo' as unknown as IImportUserRoleName }) @@ -505,7 +504,7 @@ describe('ImportUserRepo', () => { }); describe('byClasses', () => { it('should skip whitespace as filter', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ classNames: ['1a'], school }); const otherImportUser = importUserFactory.build({ classNames: ['2a'], school }); await em.persistAndFlush([importUser, otherImportUser]); @@ -515,7 +514,7 @@ describe('ImportUserRepo', () => { expect(count).toEqual(2); }); it('should match classes with full match by ignore case', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ classNames: ['1a'], school }); const otherImportUser = importUserFactory.build({ classNames: ['2a'], school }); await em.persistAndFlush([importUser, otherImportUser]); @@ -525,7 +524,7 @@ describe('ImportUserRepo', () => { expect(count).toEqual(1); }); it('should match classes with starts-with by ignore case', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ classNames: ['1a'], school }); const otherImportUser = importUserFactory.build({ classNames: ['2a'], school }); await em.persistAndFlush([importUser, otherImportUser]); @@ -535,7 +534,7 @@ describe('ImportUserRepo', () => { expect(count).toEqual(1); }); it('should match classes with ends-with by ignore case', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ classNames: ['1a'], school }); const otherImportUser = importUserFactory.build({ classNames: ['2a'], school }); await em.persistAndFlush([importUser, otherImportUser]); @@ -545,7 +544,7 @@ describe('ImportUserRepo', () => { expect(count).toEqual(2); }); it('should trim filter value', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ classNames: ['1a'], school }); const otherImportUser = importUserFactory.build({ classNames: ['2a'], school }); await em.persistAndFlush([importUser, otherImportUser]); @@ -665,7 +664,7 @@ describe('ImportUserRepo', () => { expect(count).toEqual(3); // like without filter }); it('should skip all other, invalid match names', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ school }); await em.persistAndFlush([school, importUser]); const [results] = await repo.findImportUsers(school, { matches: ['foo'] as unknown as [MatchCreatorScope] }); @@ -675,7 +674,7 @@ describe('ImportUserRepo', () => { describe('isFlagged', () => { it('should respond with and without flagged importusers by default', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ school, }); @@ -689,7 +688,7 @@ describe('ImportUserRepo', () => { expect(results).toContain(flaggedImportUser); }); it('should respond with flagged importusers only', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUser = importUserFactory.build({ school, }); @@ -706,7 +705,7 @@ describe('ImportUserRepo', () => { describe('options: limit and offset', () => { it('should apply limit', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUsers = importUserFactory.buildList(10, { school }); await em.persistAndFlush(importUsers); const [results, count] = await repo.findImportUsers(school, {}, { pagination: { limit: 3 } }); @@ -720,7 +719,7 @@ describe('ImportUserRepo', () => { expect(count2).toEqual(10); }); it('should apply offset', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const importUsers = importUserFactory.buildList(10, { school }); await em.persistAndFlush(importUsers); const [results, count] = await repo.findImportUsers(school, {}, { pagination: { skip: 3 } }); @@ -741,7 +740,7 @@ describe('ImportUserRepo', () => { }); describe('on user (match_userId)', () => { it('[SPARSE] should allow to unset items (acceppt null or undefined multiple times)', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); await em.persistAndFlush(school); const users = userFactory.buildList(10, { school }); await em.persistAndFlush(users); @@ -765,7 +764,7 @@ describe('ImportUserRepo', () => { }); it('[UNIQUE] should prohibit same match of one user ', async () => { await orm.getSchemaGenerator().ensureIndexes(); - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); await em.persistAndFlush(school); const user = userFactory.build({ school }); await em.persistAndFlush(user); @@ -779,4 +778,24 @@ describe('ImportUserRepo', () => { }); }); }); + + describe('saveImportUsers', () => { + describe('with existing importusers', () => { + const setup = () => { + const school = schoolEntityFactory.build(); + const importUser = importUserFactory.build({ school }); + const otherImportUser = importUserFactory.build({ school }); + + return { importUser, otherImportUser }; + }; + + it('should persist importUsers', async () => { + const { importUser, otherImportUser } = setup(); + + await repo.saveImportUsers([importUser, otherImportUser]); + + await expect(em.findAndCount(ImportUser, {})).resolves.toEqual([[importUser, otherImportUser], 2]); + }); + }); + }); }); diff --git a/apps/server/src/shared/repo/importuser/importuser.repo.ts b/apps/server/src/shared/repo/importuser/importuser.repo.ts index c0ba08fa1fe..24609727546 100644 --- a/apps/server/src/shared/repo/importuser/importuser.repo.ts +++ b/apps/server/src/shared/repo/importuser/importuser.repo.ts @@ -71,4 +71,8 @@ export class ImportUserRepo extends BaseRepo { async deleteImportUsersBySchool(school: SchoolEntity): Promise { await this._em.nativeDelete(ImportUser, { school }); } + + public async saveImportUsers(importUsers: ImportUser[]): Promise { + await this._em.persistAndFlush(importUsers); + } } diff --git a/apps/server/src/shared/repo/school/legacy-school.repo.integration.spec.ts b/apps/server/src/shared/repo/school/legacy-school.repo.integration.spec.ts index 24c1e5c2866..3e66e4cd71a 100644 --- a/apps/server/src/shared/repo/school/legacy-school.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/school/legacy-school.repo.integration.spec.ts @@ -15,7 +15,7 @@ import { } from '@shared/domain/entity'; import { legacySchoolDoFactory, - schoolFactory, + schoolEntityFactory, schoolYearFactory, systemEntityFactory, userLoginMigrationFactory, @@ -82,7 +82,7 @@ describe('LegacySchoolRepo', () => { it('should create a school with embedded object', async () => { const schoolYear = schoolYearFactory.build(); - const school = schoolFactory.build({ + const school = schoolEntityFactory.build({ name: 'test', currentYear: schoolYear, previousExternalId: 'someId', @@ -114,7 +114,7 @@ describe('LegacySchoolRepo', () => { describe('findByExternalId', () => { it('should find school by external ID', async () => { const system: SystemEntity = systemEntityFactory.buildWithId(); - const schoolEntity: SchoolEntity = schoolFactory.build({ externalId: 'externalId' }); + const schoolEntity: SchoolEntity = schoolEntityFactory.build({ externalId: 'externalId' }); schoolEntity.systems.add(system); await em.persistAndFlush(schoolEntity); @@ -139,7 +139,7 @@ describe('LegacySchoolRepo', () => { describe('findBySchoolNumber', () => { it('should find school by schoolnumber', async () => { - const schoolEntity: SchoolEntity = schoolFactory.build({ officialSchoolNumber: '12345' }); + const schoolEntity: SchoolEntity = schoolEntityFactory.build({ officialSchoolNumber: '12345' }); await em.persistAndFlush(schoolEntity); @@ -157,8 +157,8 @@ describe('LegacySchoolRepo', () => { describe('when there is more than school with the same officialSchoolNumber', () => { const setup = async () => { const officialSchoolNumber = '12345'; - const schoolEntity: SchoolEntity = schoolFactory.build({ officialSchoolNumber }); - const schoolEntity2: SchoolEntity = schoolFactory.build({ officialSchoolNumber }); + const schoolEntity: SchoolEntity = schoolEntityFactory.build({ officialSchoolNumber }); + const schoolEntity2: SchoolEntity = schoolEntityFactory.build({ officialSchoolNumber }); await em.persistAndFlush([schoolEntity, schoolEntity2]); @@ -183,7 +183,7 @@ describe('LegacySchoolRepo', () => { it('should map school entity to school domain object', () => { const system: SystemEntity = systemEntityFactory.buildWithId(); const schoolYear: SchoolYearEntity = schoolYearFactory.buildWithId(); - const schoolEntity: SchoolEntity = schoolFactory.buildWithId({ + const schoolEntity: SchoolEntity = schoolEntityFactory.buildWithId({ systems: [system], features: [], currentYear: schoolYear, @@ -212,7 +212,7 @@ describe('LegacySchoolRepo', () => { }); it('should return an empty array for systems when entity systems is not initialized', () => { - const schoolEntity: SchoolEntity = schoolFactory.buildWithId({ systems: undefined }); + const schoolEntity: SchoolEntity = schoolEntityFactory.buildWithId({ systems: undefined }); const schoolDO = repo.mapEntityToDO(schoolEntity); diff --git a/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.repo.integration.spec.ts b/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.repo.integration.spec.ts index 31040835ea2..e79b83a05da 100644 --- a/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.repo.integration.spec.ts @@ -12,8 +12,8 @@ import { ExternalToolRepoMapper } from '@shared/repo/externaltool/external-tool. import { cleanupCollections, externalToolEntityFactory, + schoolEntityFactory, schoolExternalToolEntityFactory, - schoolFactory, } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { SchoolExternalToolRepo } from './school-external-tool.repo'; @@ -50,7 +50,7 @@ describe('SchoolExternalToolRepo', () => { const createTools = () => { const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId(); - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const schoolExternalTool1: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ tool: externalToolEntity, school, diff --git a/apps/server/src/shared/repo/user/user-do.repo.integration.spec.ts b/apps/server/src/shared/repo/user/user-do.repo.integration.spec.ts index baa3efbcd89..17a8fee56a8 100644 --- a/apps/server/src/shared/repo/user/user-do.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/user/user-do.repo.integration.spec.ts @@ -13,7 +13,7 @@ import { UserDORepo } from '@shared/repo/user/user-do.repo'; import { cleanupCollections, roleFactory, - schoolFactory, + schoolEntityFactory, systemEntityFactory, userDoFactory, userFactory, @@ -108,7 +108,7 @@ describe('UserRepo', () => { beforeEach(async () => { system = systemEntityFactory.buildWithId(); - school = schoolFactory.buildWithId(); + school = schoolEntityFactory.buildWithId(); school.systems.add(system); user = userFactory.buildWithId({ externalId, school }); @@ -152,7 +152,7 @@ describe('UserRepo', () => { beforeEach(async () => { system = systemEntityFactory.buildWithId(); - school = schoolFactory.buildWithId(); + school = schoolEntityFactory.buildWithId(); school.systems.add(system); user = userFactory.buildWithId({ externalId, school }); @@ -234,7 +234,7 @@ describe('UserRepo', () => { email: 'email@email.email', firstName: 'firstName', lastName: 'lastName', - school: schoolFactory.buildWithId(), + school: schoolEntityFactory.buildWithId(), ldapDn: 'ldapDn', externalId: 'externalId', language: LanguageType.DE, diff --git a/apps/server/src/shared/repo/user/user.repo.integration.spec.ts b/apps/server/src/shared/repo/user/user.repo.integration.spec.ts index bc63b15322c..92e56786cb3 100644 --- a/apps/server/src/shared/repo/user/user.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/user/user.repo.integration.spec.ts @@ -3,16 +3,16 @@ import { NotFoundError } from '@mikro-orm/core'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { MatchCreator, SystemEntity, User } from '@shared/domain/entity'; +import { UserParentsEntityProps } from '@shared/domain/entity/user-parents.entity'; import { SortOrder } from '@shared/domain/interface'; import { cleanupCollections, importUserFactory, roleFactory, - schoolFactory, + schoolEntityFactory, systemEntityFactory, userFactory, } from '@shared/testing'; -import { UserParentsEntityProps } from '@shared/domain/entity/user-parents.entity'; import { UserRepo } from './user.repo'; describe('user repo', () => { @@ -135,7 +135,7 @@ describe('user repo', () => { beforeEach(async () => { sys = systemEntityFactory.build(); await em.persistAndFlush([sys]); - const school = schoolFactory.build({ systems: [sys] }); + const school = schoolEntityFactory.build({ systems: [sys] }); // const school = schoolFactory.withSystem().build(); userA = userFactory.build({ school, externalId: '111' }); @@ -194,7 +194,7 @@ describe('user repo', () => { describe('findWithoutImportUser', () => { const persistUserAndSchool = async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const user = userFactory.build({ school }); await em.persistAndFlush([user, school]); em.clear(); @@ -231,7 +231,7 @@ describe('user repo', () => { }); it('should exclude deleted users', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const user = userFactory.build({ school, deletedAt: new Date() }); await em.persistAndFlush([school, user]); em.clear(); @@ -241,7 +241,7 @@ describe('user repo', () => { }); it('should filter users by firstName contains or lastName contains, ignore case', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const user = userFactory.build({ firstName: 'Papa', lastName: 'Pane', school }); const otherUser = userFactory.build({ school }); await em.persistAndFlush([user, otherUser]); @@ -279,7 +279,7 @@ describe('user repo', () => { }); it('should sort returned users by firstname, lastname', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const user = userFactory.build({ school, firstName: 'Anna', lastName: 'Schmidt' }); const otherUser = userFactory.build({ school, firstName: 'Peter', lastName: 'Ball' }); await em.persistAndFlush([user, otherUser]); @@ -314,7 +314,7 @@ describe('user repo', () => { }); it('should skip returned two users by one', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const user = userFactory.build({ school }); const otherUser = userFactory.build({ school }); await em.persistAndFlush([user, otherUser]); @@ -327,7 +327,7 @@ describe('user repo', () => { }); it('should limit returned users from two to one', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const user = userFactory.build({ school }); const otherUser = userFactory.build({ school }); await em.persistAndFlush([user, otherUser]); @@ -340,7 +340,7 @@ describe('user repo', () => { }); it('should throw an error by passing invalid schoolId', async () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); // id do not exist await expect(repo.findWithoutImportUser(school)).rejects.toThrowError(); }); @@ -497,4 +497,48 @@ describe('user repo', () => { }); }); }); + + describe('getParentEmailsFromUser', () => { + describe('when a user meets the criteria', () => { + const setup = async () => { + const user = userFactory.buildWithId(); + + await em.persistAndFlush(user); + em.clear(); + + return { + user, + }; + }; + + it('should return the user', async () => { + const { user } = await setup(); + + const result = await repo.findUserBySchoolAndName(user.school.id, user.firstName, user.lastName); + + expect(result).toHaveLength(1); + }); + }); + + describe('when no user meets the criteria', () => { + const setup = async () => { + const user = userFactory.buildWithId(); + + await em.persistAndFlush(user); + em.clear(); + + return { + user, + }; + }; + + it('should return an empty array', async () => { + const { user } = await setup(); + + const result = await repo.findUserBySchoolAndName(user.school.id, 'Unknown', 'User'); + + expect(result).toEqual([]); + }); + }); + }); }); diff --git a/apps/server/src/shared/repo/user/user.repo.ts b/apps/server/src/shared/repo/user/user.repo.ts index 134114913c9..bab86007782 100644 --- a/apps/server/src/shared/repo/user/user.repo.ts +++ b/apps/server/src/shared/repo/user/user.repo.ts @@ -191,4 +191,10 @@ export class UserRepo extends BaseRepo { async flush(): Promise { await this._em.flush(); } + + public async findUserBySchoolAndName(schoolId: EntityId, firstName: string, lastName: string): Promise { + const users: User[] = await this._em.find(User, { school: schoolId, firstName, lastName }); + + return users; + } } diff --git a/apps/server/src/shared/repo/userloginmigration/user-login-migration.repo.integration.spec.ts b/apps/server/src/shared/repo/userloginmigration/user-login-migration.repo.integration.spec.ts index bc2cb2d268d..a762d3eeab2 100644 --- a/apps/server/src/shared/repo/userloginmigration/user-login-migration.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/userloginmigration/user-login-migration.repo.integration.spec.ts @@ -2,11 +2,11 @@ import { createMock } from '@golevelup/ts-jest'; import { MongoMemoryDatabaseModule } from '@infra/database'; import { EntityManager } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; -import { SchoolEntity, SystemEntity } from '@shared/domain/entity'; import { UserLoginMigrationDO } from '@shared/domain/domainobject'; +import { SchoolEntity, SystemEntity } from '@shared/domain/entity'; import { UserLoginMigrationEntity } from '@shared/domain/entity/user-login-migration.entity'; -import { cleanupCollections, schoolFactory, systemEntityFactory } from '@shared/testing'; +import { cleanupCollections, schoolEntityFactory, systemEntityFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { userLoginMigrationFactory } from '../../testing/factory/user-login-migration.factory'; import { UserLoginMigrationRepo } from './user-login-migration.repo'; @@ -43,7 +43,7 @@ describe('UserLoginMigrationRepo', () => { describe('save', () => { describe('when saving a UserLoginMigrationDO', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolEntityFactory.buildWithId(); const sourceSystem: SystemEntity = systemEntityFactory.buildWithId(); const targetSystem: SystemEntity = systemEntityFactory.buildWithId(); @@ -144,7 +144,7 @@ describe('UserLoginMigrationRepo', () => { describe('when searching for a UserLoginMigration by an unknown school id', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId({ userLoginMigration: undefined }); + const school: SchoolEntity = schoolEntityFactory.buildWithId({ userLoginMigration: undefined }); await em.persistAndFlush(school); em.clear(); diff --git a/apps/server/src/shared/testing/factory/course.factory.ts b/apps/server/src/shared/testing/factory/course.factory.ts index 6850160bd24..cd6bd1c81cb 100644 --- a/apps/server/src/shared/testing/factory/course.factory.ts +++ b/apps/server/src/shared/testing/factory/course.factory.ts @@ -1,9 +1,8 @@ -import { DeepPartial } from 'fishery'; - import { Course, CourseProperties } from '@shared/domain/entity'; +import { DeepPartial } from 'fishery'; import { BaseFactory } from './base.factory'; -import { schoolFactory } from './school.factory'; +import { schoolEntityFactory } from './school-entity.factory'; import { userFactory } from './user.factory'; const oneDay = 24 * 60 * 60 * 1000; @@ -43,6 +42,6 @@ export const courseFactory = CourseFactory.define(Course, ({ sequence }) => { name: `course #${sequence}`, description: `course #${sequence} description`, color: '#FFFFFF', - school: schoolFactory.build(), + school: schoolEntityFactory.build(), }; }); diff --git a/apps/server/src/shared/testing/factory/group-entity.factory.ts b/apps/server/src/shared/testing/factory/group-entity.factory.ts index 4cc6d86a7f2..9019f145784 100644 --- a/apps/server/src/shared/testing/factory/group-entity.factory.ts +++ b/apps/server/src/shared/testing/factory/group-entity.factory.ts @@ -3,7 +3,7 @@ import { ExternalSourceEntity } from '@shared/domain/entity'; import { RoleName } from '@shared/domain/interface'; import { BaseFactory } from './base.factory'; import { roleFactory } from './role.factory'; -import { schoolFactory } from './school.factory'; +import { schoolEntityFactory } from './school-entity.factory'; import { systemEntityFactory } from './systemEntityFactory'; import { userFactory } from './user.factory'; @@ -25,7 +25,7 @@ export const groupEntityFactory = BaseFactory.define { matched(matchedBy: MatchCreator, user: User): this { const params: DeepPartial = { matchedBy, user }; + return this.params(params); } } export const importUserFactory = ImportUserFactory.define(ImportUser, ({ sequence }) => { return { - school: schoolFactory.build(), - system: systemEntityFactory.build(), + school: schoolEntityFactory.buildWithId(), + system: systemEntityFactory.buildWithId(), ldapDn: `uid=john${sequence},cn=schueler,cn=users,ou=1,dc=training,dc=ucs`, - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - externalId: uuidv4() as unknown as string, + externalId: uuidv4(), firstName: `John${sequence}`, lastName: `Doe${sequence}`, email: `user-${sequence}@example.com`, diff --git a/apps/server/src/shared/testing/factory/index.ts b/apps/server/src/shared/testing/factory/index.ts index 957d2e735a9..03a3917c1fc 100644 --- a/apps/server/src/shared/testing/factory/index.ts +++ b/apps/server/src/shared/testing/factory/index.ts @@ -24,7 +24,7 @@ export * from './news.factory'; export * from './role-dto.factory'; export * from './role.factory'; export * from './school-external-tool-entity.factory'; -export * from './school.factory'; +export * from './school-entity.factory'; export * from './schoolyear.factory'; export * from './share-token.do.factory'; export * from './storageprovider.factory'; diff --git a/apps/server/src/shared/testing/factory/news.factory.ts b/apps/server/src/shared/testing/factory/news.factory.ts index af7de567f40..de682bffcfe 100644 --- a/apps/server/src/shared/testing/factory/news.factory.ts +++ b/apps/server/src/shared/testing/factory/news.factory.ts @@ -1,7 +1,7 @@ import { CourseNews, NewsProperties, SchoolNews, TeamNews } from '@shared/domain/entity'; import { BaseFactory } from './base.factory'; import { courseFactory } from './course.factory'; -import { schoolFactory } from './school.factory'; +import { schoolEntityFactory } from './school-entity.factory'; import { teamFactory } from './team.factory'; import { userFactory } from './user.factory'; @@ -10,9 +10,9 @@ export const schoolNewsFactory = BaseFactory.define( title: `news ${sequence}`, content: `content of news ${sequence}`, displayAt: new Date(), - school: schoolFactory.build(), + school: schoolEntityFactory.build(), creator: userFactory.build(), - target: schoolFactory.build(), + target: schoolEntityFactory.build(), }; }); @@ -21,7 +21,7 @@ export const courseNewsFactory = BaseFactory.define( title: `news ${sequence}`, content: `content of news ${sequence}`, displayAt: new Date(), - school: schoolFactory.build(), + school: schoolEntityFactory.build(), creator: userFactory.build(), target: courseFactory.build(), }; @@ -32,7 +32,7 @@ export const teamNewsFactory = BaseFactory.define(Team title: `news ${sequence}`, content: `content of news ${sequence}`, displayAt: new Date(), - school: schoolFactory.build(), + school: schoolEntityFactory.build(), creator: userFactory.build(), target: teamFactory.build(), }; @@ -45,9 +45,9 @@ export const schoolUnpublishedNewsFactory = BaseFactory.define(SchoolEntity, ({ sequence }) => { +export const schoolEntityFactory = BaseFactory.define(SchoolEntity, ({ sequence }) => { return { name: `school #${sequence}`, currentYear: schoolYearFactory.build(), diff --git a/apps/server/src/shared/testing/factory/school-external-tool-entity.factory.ts b/apps/server/src/shared/testing/factory/school-external-tool-entity.factory.ts index e3f8b45cd59..f3c58bcc762 100644 --- a/apps/server/src/shared/testing/factory/school-external-tool-entity.factory.ts +++ b/apps/server/src/shared/testing/factory/school-external-tool-entity.factory.ts @@ -1,8 +1,8 @@ import { SchoolExternalToolEntity, SchoolExternalToolProperties } from '@modules/tool/school-external-tool/entity'; import { BaseFactory } from '@shared/testing/factory/base.factory'; import { externalToolEntityFactory } from './external-tool-entity.factory'; +import { schoolEntityFactory } from './school-entity.factory'; import { schoolExternalToolConfigurationStatusEntityFactory } from './school-external-tool-configuration-status-entity.factory'; -import { schoolFactory } from './school.factory'; export const schoolExternalToolEntityFactory = BaseFactory.define< SchoolExternalToolEntity, @@ -10,7 +10,7 @@ export const schoolExternalToolEntityFactory = BaseFactory.define< >(SchoolExternalToolEntity, () => { return { tool: externalToolEntityFactory.buildWithId(), - school: schoolFactory.buildWithId(), + school: schoolEntityFactory.buildWithId(), schoolParameters: [{ name: 'schoolMockParameter', value: 'mockValue' }], toolVersion: 0, status: schoolExternalToolConfigurationStatusEntityFactory.build(), diff --git a/apps/server/src/shared/testing/factory/school-system-options-entity.factory.ts b/apps/server/src/shared/testing/factory/school-system-options-entity.factory.ts index 1cd9aea5ae0..8697d6f11b5 100644 --- a/apps/server/src/shared/testing/factory/school-system-options-entity.factory.ts +++ b/apps/server/src/shared/testing/factory/school-system-options-entity.factory.ts @@ -1,7 +1,7 @@ import { SchoolSystemOptionsEntity, SchoolSystemOptionsEntityProps } from '@modules/legacy-school/entity'; import { SystemProvisioningStrategy } from '../../domain/interface/system-provisioning.strategy'; import { BaseFactory } from './base.factory'; -import { schoolFactory } from './school.factory'; +import { schoolEntityFactory } from './school-entity.factory'; import { systemEntityFactory } from './systemEntityFactory'; export const schoolSystemOptionsEntityFactory = BaseFactory.define< @@ -9,7 +9,7 @@ export const schoolSystemOptionsEntityFactory = BaseFactory.define< SchoolSystemOptionsEntityProps >(SchoolSystemOptionsEntity, () => { return { - school: schoolFactory.buildWithId(), + school: schoolEntityFactory.buildWithId(), system: systemEntityFactory.buildWithId({ provisioningStrategy: SystemProvisioningStrategy.SANIS }), provisioningOptions: { groupProvisioningOtherEnabled: false, diff --git a/apps/server/src/shared/testing/factory/submission.factory.ts b/apps/server/src/shared/testing/factory/submission.factory.ts index 7b4ee5f0455..e0ab13404bc 100644 --- a/apps/server/src/shared/testing/factory/submission.factory.ts +++ b/apps/server/src/shared/testing/factory/submission.factory.ts @@ -1,7 +1,7 @@ import { Submission, SubmissionProperties } from '@shared/domain/entity'; import { DeepPartial } from 'fishery'; import { BaseFactory } from './base.factory'; -import { schoolFactory } from './school.factory'; +import { schoolEntityFactory } from './school-entity.factory'; import { taskFactory } from './task.factory'; import { userFactory } from './user.factory'; @@ -34,7 +34,7 @@ class SubmissionFactory extends BaseFactory { export const submissionFactory = SubmissionFactory.define(Submission, ({ sequence }) => { return { - school: schoolFactory.build(), + school: schoolEntityFactory.build(), task: taskFactory.build(), student: userFactory.build(), comment: `submission comment #${sequence}`, diff --git a/apps/server/src/shared/testing/factory/task.factory.ts b/apps/server/src/shared/testing/factory/task.factory.ts index 5acc9ab2c77..909b52ee9da 100644 --- a/apps/server/src/shared/testing/factory/task.factory.ts +++ b/apps/server/src/shared/testing/factory/task.factory.ts @@ -2,7 +2,7 @@ import { Task, User } from '@shared/domain/entity'; import { TaskProperties } from '@shared/domain/types'; import { DeepPartial } from 'fishery'; import { BaseFactory } from './base.factory'; -import { schoolFactory } from './school.factory'; +import { schoolEntityFactory } from './school-entity.factory'; import { userFactory } from './user.factory'; const yesterday = new Date(Date.now() - 86400000); @@ -33,7 +33,7 @@ class TaskFactory extends BaseFactory { } export const taskFactory = TaskFactory.define(Task, ({ sequence }) => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const creator = userFactory.build({ school }); // private is by default in constructor true, but in the most test cases we need private: false return { diff --git a/apps/server/src/shared/testing/factory/teamuser.factory.ts b/apps/server/src/shared/testing/factory/teamuser.factory.ts index b2aaaf5749a..ed067157c67 100644 --- a/apps/server/src/shared/testing/factory/teamuser.factory.ts +++ b/apps/server/src/shared/testing/factory/teamuser.factory.ts @@ -1,13 +1,13 @@ import { Role, TeamUserEntity } from '@shared/domain/entity'; import { BaseFactory } from '@shared/testing/factory/base.factory'; import { roleFactory } from '@shared/testing/factory/role.factory'; -import { schoolFactory } from '@shared/testing/factory/school.factory'; import { userFactory } from '@shared/testing/factory/user.factory'; import { DeepPartial } from 'fishery'; +import { schoolEntityFactory } from './school-entity.factory'; class TeamUserFactory extends BaseFactory { withRoleAndUserId(role: Role, userId: string): this { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const params: DeepPartial = { user: userFactory.buildWithId({ school, roles: [roleFactory.build({ roles: [role] })] }, userId), school, @@ -17,7 +17,7 @@ class TeamUserFactory extends BaseFactory { } withUserId(userId: string): this { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const params: DeepPartial = { user: userFactory.buildWithId({ school }, userId), school, @@ -28,7 +28,7 @@ class TeamUserFactory extends BaseFactory { export const teamUserFactory = TeamUserFactory.define(TeamUserEntity, () => { const role = roleFactory.buildWithId(); - const school = schoolFactory.buildWithId(); + const school = schoolEntityFactory.buildWithId(); const user = userFactory.buildWithId({ roles: [role] }); return new TeamUserEntity({ diff --git a/apps/server/src/shared/testing/factory/user-and-account.test.factory.spec.ts b/apps/server/src/shared/testing/factory/user-and-account.test.factory.spec.ts index fffa48d540b..e5f75912de8 100644 --- a/apps/server/src/shared/testing/factory/user-and-account.test.factory.spec.ts +++ b/apps/server/src/shared/testing/factory/user-and-account.test.factory.spec.ts @@ -1,7 +1,7 @@ import { Account, User } from '@shared/domain/entity'; import { ObjectId } from 'bson'; import { setupEntities } from '../setup-entities'; -import { schoolFactory } from './school.factory'; +import { schoolEntityFactory } from './school-entity.factory'; import { UserAndAccountParams, UserAndAccountTestFactory } from './user-and-account.test.factory'; describe('user-and-account.test.factory', () => { @@ -10,7 +10,7 @@ describe('user-and-account.test.factory', () => { }); const createParams = () => { - const school = schoolFactory.build(); + const school = schoolEntityFactory.build(); const systemId = new ObjectId().toHexString(); const params: UserAndAccountParams = { diff --git a/apps/server/src/shared/testing/factory/user-login-migration.factory.ts b/apps/server/src/shared/testing/factory/user-login-migration.factory.ts index a3feaedbd27..66e8ab94a57 100644 --- a/apps/server/src/shared/testing/factory/user-login-migration.factory.ts +++ b/apps/server/src/shared/testing/factory/user-login-migration.factory.ts @@ -1,13 +1,13 @@ import { IUserLoginMigration, UserLoginMigrationEntity } from '../../domain/entity/user-login-migration.entity'; import { BaseFactory } from './base.factory'; -import { schoolFactory } from './school.factory'; +import { schoolEntityFactory } from './school-entity.factory'; import { systemEntityFactory } from './systemEntityFactory'; export const userLoginMigrationFactory = BaseFactory.define( UserLoginMigrationEntity, () => { return { - school: schoolFactory.buildWithId(), + school: schoolEntityFactory.buildWithId(), startedAt: new Date('2023-04-28'), targetSystem: systemEntityFactory.buildWithId(), }; diff --git a/apps/server/src/shared/testing/factory/user.factory.ts b/apps/server/src/shared/testing/factory/user.factory.ts index 99c3d45f126..b74e4b52127 100644 --- a/apps/server/src/shared/testing/factory/user.factory.ts +++ b/apps/server/src/shared/testing/factory/user.factory.ts @@ -6,7 +6,7 @@ import _ from 'lodash'; import { adminPermissions, studentPermissions, teacherPermissions, userPermissions } from '../user-role-permissions'; import { BaseFactory } from './base.factory'; import { roleFactory } from './role.factory'; -import { schoolFactory } from './school.factory'; +import { schoolEntityFactory } from './school-entity.factory'; class UserFactory extends BaseFactory { withRoleByName(name: RoleName): this { @@ -55,6 +55,6 @@ export const userFactory = UserFactory.define(User, ({ sequence }) => { lastName: `Doe ${sequence}`, email: `user-${sequence}@example.com`, roles: [], - school: schoolFactory.build(), + school: schoolEntityFactory.build(), }; }); diff --git a/config/default.schema.json b/config/default.schema.json index b871df8f497..e2b1c9a37da 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -1483,6 +1483,36 @@ "type": "string", "default": "http://localhost:3349", "description": "Address for tldraw management app" + }, + "SCHULCONNEX_CLIENT": { + "type": "object", + "description": "Configuration of the schulcloud's schulconnex client.", + "properties": { + "API_URL": { + "type": "string", + "description": "Base URL of the schulconnex API (from dof)", + "examples": ["https://api-dienste.stage.niedersachsen-login.schule/v1/"] + }, + "TOKEN_ENDPOINT": { + "type": "string", + "description": "Token endpoint of the schulconnex API (from dof)", + "examples": ["https://api-dienste.stage.niedersachsen-login.schule/v1/oauth2/token"] + }, + "CLIENT_ID": { + "type": "string", + "description": "Client ID for accessing the schulconnex API (from server vault)" + }, + "CLIENT_SECRET": { + "type": "string", + "description": "Client secret for accessing the schulconnex API (from server vault)" + } + }, + "default": { + "API_URL": "", + "TOKEN_ENDPOINT": "", + "CLIENT_ID": "", + "CLIENT_SECRET": "" + } } }, "required": [], diff --git a/config/development.json b/config/development.json index eb106993b10..60b76513405 100644 --- a/config/development.json +++ b/config/development.json @@ -76,5 +76,11 @@ "FEATURE_COLUMN_BOARD_ENABLED": true, "FEATURE_COLUMN_BOARD_SUBMISSIONS_ENABLED": true, "FEATURE_COLUMN_BOARD_LINK_ELEMENT_ENABLED": true, - "FEATURE_COLUMN_BOARD_EXTERNAL_TOOLS_ENABLED": true + "FEATURE_COLUMN_BOARD_EXTERNAL_TOOLS_ENABLED": true, + "SCHULCONNEX_CLIENT": { + "API_URL": "http://localhost:8888/v1/", + "TOKEN_ENDPOINT": "http://localhost:8888/realms/SANIS/protocol/openid-connect/token", + "CLIENT_ID": "schulcloud", + "CLIENT_SECRET": "secret" + } } diff --git a/config/test.json b/config/test.json index 08b9d373609..68c2fab1d73 100644 --- a/config/test.json +++ b/config/test.json @@ -72,5 +72,11 @@ "DB_COLLECTION_NAME": "drawings", "DB_FLUSH_SIZE": 400, "DB_MULTIPLE_COLLECTIONS": false + }, + "SCHULCONNEX_CLIENT": { + "API_URL": "http://localhost:8888/v1/", + "TOKEN_ENDPOINT": "http://localhost:8888/realms/SANIS/protocol/openid-connect/token", + "CLIENT_ID": "schulcloud", + "CLIENT_SECRET": "secret" } }