From 01b8681ecc9d3281b12318f15618916729c0a676 Mon Sep 17 00:00:00 2001 From: Thomas Feldtkeller Date: Fri, 14 Jul 2023 12:37:43 +0200 Subject: [PATCH 01/24] review comments 14-07-23 --- .../controller/account.controller.spec.ts | 3 ++- .../account/controller/account.controller.ts | 4 +++- .../controller/api-test/account.api.spec.ts | 7 ++++++- .../account/controller/dto/password-pattern.ts | 3 ++- .../account-entity-to-dto.mapper.spec.ts | 5 +++++ .../mapper/account-entity-to-dto.mapper.ts | 2 ++ .../account/mapper/account-response.mapper.ts | 1 + .../src/modules/account/repo/account.repo.ts | 5 +++++ .../src/modules/account/review-comments.md | 13 +++++++++++++ .../account/services/account-db.service.ts | 17 +++++++++++------ .../account/services/account-idm.service.ts | 12 ++++++++++-- .../services/account.service.abstract.ts | 3 +++ .../account/services/account.service.ts | 10 +++++++++- .../services/account.validation.service.ts | 6 ++++-- .../account/services/dto/account.dto.ts | 1 + .../src/modules/account/uc/account.uc.ts | 18 +++++++++++++++++- .../src/shared/domain/interface/account.ts | 1 + 17 files changed, 95 insertions(+), 16 deletions(-) create mode 100644 apps/server/src/modules/account/review-comments.md diff --git a/apps/server/src/modules/account/controller/account.controller.spec.ts b/apps/server/src/modules/account/controller/account.controller.spec.ts index ef15672edc5..093fdde0546 100644 --- a/apps/server/src/modules/account/controller/account.controller.spec.ts +++ b/apps/server/src/modules/account/controller/account.controller.spec.ts @@ -1,7 +1,8 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { AccountController } from './account.controller'; import { AccountUc } from '../uc/account.uc'; +import { AccountController } from './account.controller'; +// TODO: delete this file describe('account.controller', () => { let module: TestingModule; let controller: AccountController; diff --git a/apps/server/src/modules/account/controller/account.controller.ts b/apps/server/src/modules/account/controller/account.controller.ts index 2256cb9fc90..23c07326d82 100644 --- a/apps/server/src/modules/account/controller/account.controller.ts +++ b/apps/server/src/modules/account/controller/account.controller.ts @@ -1,8 +1,8 @@ import { Body, Controller, Delete, Get, Param, Patch, Query } from '@nestjs/common'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; import { EntityNotFoundError, ForbiddenOperationError, ValidationError } from '@shared/common'; import { ICurrentUser } from '@src/modules/authentication'; +import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; import { AccountUc } from '../uc/account.uc'; import { AccountByIdBodyParams, @@ -33,6 +33,8 @@ export class AccountController { @Query() query: AccountSearchQueryParams ): Promise { return this.accountUc.searchAccounts(currentUser, query); + + // TODO: mapping from domain to api dto should be a responsability of the controller (also every other function here) } @Get(':id') 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 d1f0463cdfd..aa8922390f2 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 @@ -2,7 +2,6 @@ import { EntityManager } from '@mikro-orm/core'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Account, Permission, RoleName, User } from '@shared/domain'; -import { ICurrentUser } from '@src/modules/authentication'; import { accountFactory, mapUserToCurrentUser, roleFactory, schoolFactory, userFactory } from '@shared/testing'; import { AccountByIdBodyParams, @@ -11,6 +10,8 @@ import { PatchMyAccountParams, PatchMyPasswordParams, } from '@src/modules/account/controller/dto'; +import { ICurrentUser } from '@src/modules/authentication'; +// HINT: general todo to not get JwtAuthGuard from within the module import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; import { ServerTestModule } from '@src/modules/server/server.module'; import { Request } from 'express'; @@ -38,6 +39,7 @@ describe('Account Controller (API)', () => { const defaultPasswordHash = '$2a$10$/DsztV5o6P5piW2eWJsxw.4nHovmJGBA.QNwiTmuZ/uvUc40b.Uhu'; const setup = async () => { + // TODO: revisit test structure. setup for each functional situation, return values instead of global variables const school = schoolFactory.buildWithId(); const adminRoles = roleFactory.build({ @@ -69,12 +71,15 @@ describe('Account Controller (API)', () => { em.persist([adminUser, teacherUser, studentUser, superheroUser]); em.persist([adminAccount, teacherAccount, studentAccount, superheroAccount]); await em.flush(); + + // TODO: return {adminAccount, teacherAccount, ...} }; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ServerTestModule], }) + // TODO: We need to remove this old hack, use the new api helper insteads .overrideGuard(JwtAuthGuard) .useValue({ canActivate(context: ExecutionContext) { diff --git a/apps/server/src/modules/account/controller/dto/password-pattern.ts b/apps/server/src/modules/account/controller/dto/password-pattern.ts index 6ca2fd9fab2..bc41132265c 100644 --- a/apps/server/src/modules/account/controller/dto/password-pattern.ts +++ b/apps/server/src/modules/account/controller/dto/password-pattern.ts @@ -1,2 +1,3 @@ -// TODO Compare with client +// TODO: check if this has happened +// TODO: Compare with client export const passwordPattern = /^(?=.*[A-Z])(?=.*[0-9])(?=.*[a-z])(?=.*[-_!<>§$%&/()=?\\;:,.#+*~'])\S.{6,253}\S$/; diff --git a/apps/server/src/modules/account/mapper/account-entity-to-dto.mapper.spec.ts b/apps/server/src/modules/account/mapper/account-entity-to-dto.mapper.spec.ts index 8e9522434eb..fc459b63f18 100644 --- a/apps/server/src/modules/account/mapper/account-entity-to-dto.mapper.spec.ts +++ b/apps/server/src/modules/account/mapper/account-entity-to-dto.mapper.spec.ts @@ -14,7 +14,10 @@ describe('AccountEntityToDtoMapper', () => { }); describe('mapToDto', () => { + // TODO: describe(when... it(should...) + // TODO: use setup function it('should map all fields', () => { + // TODO: use Account factory instead const testEntity: Account = { _id: new ObjectId(), id: 'id', @@ -32,6 +35,7 @@ describe('AccountEntityToDtoMapper', () => { }; const ret = AccountEntityToDtoMapper.mapToDto(testEntity); + // TODO: compare with object instead, to have only a single expect() expect(ret.id).toBe(testEntity.id); expect(ret.createdAt).toEqual(testEntity.createdAt); expect(ret.updatedAt).toEqual(testEntity.createdAt); @@ -47,6 +51,7 @@ describe('AccountEntityToDtoMapper', () => { }); it('should ignore missing ids', () => { + // TODO: use Account factory instead const testEntity: Account = { _id: new ObjectId(), id: 'id', diff --git a/apps/server/src/modules/account/mapper/account-entity-to-dto.mapper.ts b/apps/server/src/modules/account/mapper/account-entity-to-dto.mapper.ts index 417497b3218..d8af59e6716 100644 --- a/apps/server/src/modules/account/mapper/account-entity-to-dto.mapper.ts +++ b/apps/server/src/modules/account/mapper/account-entity-to-dto.mapper.ts @@ -19,6 +19,8 @@ export class AccountEntityToDtoMapper { }); } + // TODO: use Counted instead of [Account[], number] + // TODO: adjust naming of accountEntities static mapSearchResult(accountEntities: [Account[], number]): Counted { const foundAccounts = accountEntities[0]; const accountDtos: AccountDto[] = AccountEntityToDtoMapper.mapAccountsToDto(foundAccounts); diff --git a/apps/server/src/modules/account/mapper/account-response.mapper.ts b/apps/server/src/modules/account/mapper/account-response.mapper.ts index 3ecbc110add..f8fd8e310d5 100644 --- a/apps/server/src/modules/account/mapper/account-response.mapper.ts +++ b/apps/server/src/modules/account/mapper/account-response.mapper.ts @@ -3,6 +3,7 @@ import { AccountDto } from '@src/modules/account/services/dto/account.dto'; import { AccountResponse } from '../controller/dto'; export class AccountResponseMapper { + // TODO: remove this one static mapToResponseFromEntity(account: Account): AccountResponse { return new AccountResponse({ id: account.id, diff --git a/apps/server/src/modules/account/repo/account.repo.ts b/apps/server/src/modules/account/repo/account.repo.ts index fb68f0a759b..e848973c5c6 100644 --- a/apps/server/src/modules/account/repo/account.repo.ts +++ b/apps/server/src/modules/account/repo/account.repo.ts @@ -15,7 +15,9 @@ export class AccountRepo extends BaseRepo { * Finds an account by user id. * @param userId the user id */ + // TODO: here only EntityIds should arrive async findByUserId(userId: EntityId | ObjectId): Promise { + // TODO: you can use userId directly, without constructing an objectId return this._em.findOne(Account, { userId: new ObjectId(userId) }); } @@ -47,6 +49,8 @@ export class AccountRepo extends BaseRepo { await this._em.flush(); } + // TODO: the default values for skip and limit, are they required and/or correct here? + // TODO: use counted for the return type async searchByUsernameExactMatch(username: string, skip = 0, limit = 1): Promise<[Account[], number]> { return this.searchByUsername(username, skip, limit, true); } @@ -80,6 +84,7 @@ export class AccountRepo extends BaseRepo { limit: number, exactMatch: boolean ): Promise<[Account[], number]> { + // TODO: check that injections are not possible, eg make sure sanitizeHTML has been called at some point (for username) // escapes every character, that's not a unicode letter or number const escapedUsername = username.replace(/[^(\p{L}\p{N})]/gu, '\\$&'); const searchUsername = exactMatch ? `^${escapedUsername}$` : escapedUsername; diff --git a/apps/server/src/modules/account/review-comments.md b/apps/server/src/modules/account/review-comments.md new file mode 100644 index 00000000000..215e6522d4b --- /dev/null +++ b/apps/server/src/modules/account/review-comments.md @@ -0,0 +1,13 @@ +# Review Comments 14.7.23 + +- move mapper into repo folder +- write an md file or flow diagram describing how things work +- in what layer do the services belong? + +- look at ALL spec files, adjust to test structure +- naming of DO vs Entity (DO is the leading, "Account", entity is just the datalayer representation "AccountEntity") + +- new decisions for loggables + + +looked at this module only. \ No newline at end of file diff --git a/apps/server/src/modules/account/services/account-db.service.ts b/apps/server/src/modules/account/services/account-db.service.ts index 1209ed86744..45de6126ecf 100644 --- a/apps/server/src/modules/account/services/account-db.service.ts +++ b/apps/server/src/modules/account/services/account-db.service.ts @@ -1,13 +1,15 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { Injectable } from '@nestjs/common'; -import bcrypt from 'bcryptjs'; import { EntityNotFoundError } from '@shared/common'; import { Account, Counted, EntityId } from '@shared/domain'; -import { AccountRepo } from '../repo/account.repo'; +import bcrypt from 'bcryptjs'; import { AccountEntityToDtoMapper } from '../mapper'; -import { AccountDto, AccountSaveDto } from './dto'; -import { AbstractAccountService } from './account.service.abstract'; +import { AccountRepo } from '../repo/account.repo'; import { AccountLookupService } from './account-lookup.service'; +import { AbstractAccountService } from './account.service.abstract'; +import { AccountDto, AccountSaveDto } from './dto'; + +// HINT: do more empty lines :) @Injectable() export class AccountServiceDb extends AbstractAccountService { @@ -32,6 +34,7 @@ export class AccountServiceDb extends AbstractAccountService { } async findByUserIdOrFail(userId: EntityId): Promise { + // TODO: use a findOneorFail in the background -> then the custom error will no longer be needed const accountEntity = await this.accountRepo.findByUserId(userId); if (!accountEntity) { throw new EntityNotFoundError('Account'); @@ -46,6 +49,8 @@ export class AccountServiceDb extends AbstractAccountService { async save(accountDto: AccountSaveDto): Promise { let account: Account; + // HINT: mapping could be done by a mapper (though this whole file is subject to be removed in the future) + // HINT: today we have logic to map back into unit work in the baseDO if (accountDto.id) { const internalId = await this.getInternalId(accountDto.id); account = await this.accountRepo.findById(internalId); @@ -75,7 +80,7 @@ export class AccountServiceDb extends AbstractAccountService { credentialHash: accountDto.credentialHash, }); - await this.accountRepo.save(account); + await this.accountRepo.save(account); // HINT: this can be done once in the end } return AccountEntityToDtoMapper.mapToDto(account); } @@ -128,7 +133,7 @@ export class AccountServiceDb extends AbstractAccountService { if (!account.password) { return Promise.resolve(false); } - return bcrypt.compare(comparePassword, account.password); + return bcrypt.compare(comparePassword, account.password); // hint: first get result, then return seperately } private async getInternalId(id: EntityId | ObjectId): Promise { diff --git a/apps/server/src/modules/account/services/account-idm.service.ts b/apps/server/src/modules/account/services/account-idm.service.ts index 5f2351ce2ea..032e1a23f1b 100644 --- a/apps/server/src/modules/account/services/account-idm.service.ts +++ b/apps/server/src/modules/account/services/account-idm.service.ts @@ -2,11 +2,11 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { Injectable, NotImplementedException } from '@nestjs/common'; import { EntityNotFoundError } from '@shared/common'; import { Counted, EntityId, IAccount, IAccountUpdate } from '@shared/domain'; -import { IdentityManagementService, IdentityManagementOauthService } from '@shared/infra/identity-management'; +import { IdentityManagementOauthService, IdentityManagementService } from '@shared/infra/identity-management'; import { AccountIdmToDtoMapper } from '../mapper'; +import { AccountLookupService } from './account-lookup.service'; import { AbstractAccountService } from './account.service.abstract'; import { AccountDto, AccountSaveDto } from './dto'; -import { AccountLookupService } from './account-lookup.service'; @Injectable() export class AccountServiceIdm extends AbstractAccountService { @@ -25,6 +25,7 @@ export class AccountServiceIdm extends AbstractAccountService { return account; } + // TODO: this needs a better solution. probably needs followup meeting to come up with something async findMultipleByUserId(userIds: EntityId[]): Promise { const results = new Array(); for (const userId of userIds) { @@ -32,6 +33,7 @@ export class AccountServiceIdm extends AbstractAccountService { // eslint-disable-next-line no-await-in-loop results.push(await this.identityManager.findAccountByFctIntId(userId)); } catch { + // TODO: dont simply forget errors. maybe use a filter instead? // ignore entry } } @@ -44,12 +46,14 @@ export class AccountServiceIdm extends AbstractAccountService { const result = await this.identityManager.findAccountByFctIntId(userId); return this.accountIdmToDtoMapper.mapToDto(result); } catch { + // TODO: dont simply forget errors return null; } } async findByUserIdOrFail(userId: EntityId): Promise { try { + // TODO: reuse code here? const result = await this.identityManager.findAccountByFctIntId(userId); return this.accountIdmToDtoMapper.mapToDto(result); } catch { @@ -91,11 +95,15 @@ export class AccountServiceIdm extends AbstractAccountService { attRefFunctionalIntId: accountDto.userId, attRefFunctionalExtId: accountDto.systemId, }; + // TODO: probably do some method extraction here if (accountDto.id) { let idmId: string | undefined; + // TODO: extract into a method that hides the trycatch try { idmId = await this.getIdmAccountId(accountDto.id); } catch { + // TODO: logging + // HINT: does the method even need to throw? idmId = undefined; } if (idmId) { diff --git a/apps/server/src/modules/account/services/account.service.abstract.ts b/apps/server/src/modules/account/services/account.service.abstract.ts index b2e198f6a86..d25dbc0ac4a 100644 --- a/apps/server/src/modules/account/services/account.service.abstract.ts +++ b/apps/server/src/modules/account/services/account.service.abstract.ts @@ -2,6 +2,8 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { Counted, EntityId } from '@shared/domain'; import { AccountDto, AccountSaveDto } from './dto'; +// TODO: split functions which are only needed for feathers + export abstract class AbstractAccountService { abstract findById(id: EntityId): Promise; @@ -11,6 +13,7 @@ export abstract class AbstractAccountService { abstract findByUserIdOrFail(userId: EntityId): Promise; + // HINT: it would be preferable to use entityId here. Needs to be checked if this is blocked by lecacy code abstract findByUsernameAndSystemId(username: string, systemId: EntityId | ObjectId): Promise; abstract save(accountDto: AccountSaveDto): Promise; diff --git a/apps/server/src/modules/account/services/account.service.ts b/apps/server/src/modules/account/services/account.service.ts index b70c8843c92..8938b323e87 100644 --- a/apps/server/src/modules/account/services/account.service.ts +++ b/apps/server/src/modules/account/services/account.service.ts @@ -4,7 +4,8 @@ import { ConfigService } from '@nestjs/config'; import { ValidationError } from '@shared/common'; import { Counted } from '@shared/domain'; import { isEmail, validateOrReject } from 'class-validator'; -import { LegacyLogger } from '../../../core/logger'; +import { LegacyLogger } from '../../../core/logger'; // TODO: use path alias +// TODO: account needs to define its own config, which is made available for the server import { IServerConfig } from '../../server/server.config'; import { AccountServiceDb } from './account-db.service'; import { AccountServiceIdm } from './account-idm.service'; @@ -12,6 +13,11 @@ import { AbstractAccountService } from './account.service.abstract'; import { AccountValidationService } from './account.validation.service'; import { AccountDto, AccountSaveDto } from './dto'; +/* TODO: extract a service that contains all things required by feathers, +which is responsible for the additionally required validation + +it should be clearly visible which functions are only needed for feathers, and easy to remove them */ + @Injectable() export class AccountService extends AbstractAccountService { private readonly accountImpl: AbstractAccountService; @@ -73,6 +79,7 @@ export class AccountService extends AbstractAccountService { } async saveWithValidation(dto: AccountSaveDto): Promise { + // TODO: move as much as possible into the class validator await validateOrReject(dto); // sanatizeUsername ✔ if (!dto.systemId) { @@ -103,6 +110,7 @@ export class AccountService extends AbstractAccountService { // dto.password = undefined; // } + // TODO: split validation from saving, so it can be used independently await this.save(dto); } diff --git a/apps/server/src/modules/account/services/account.validation.service.ts b/apps/server/src/modules/account/services/account.validation.service.ts index fc47569ed71..2cabc9eabb3 100644 --- a/apps/server/src/modules/account/services/account.validation.service.ts +++ b/apps/server/src/modules/account/services/account.validation.service.ts @@ -5,9 +5,11 @@ import { AccountEntityToDtoMapper } from '../mapper/account-entity-to-dto.mapper import { AccountRepo } from '../repo/account.repo'; @Injectable() +// TODO: naming? export class AccountValidationService { constructor(private accountRepo: AccountRepo, private userRepo: UserRepo) {} + // TODO: this should be refactored and rewritten more nicely async isUniqueEmail(email: string, userId?: EntityId, accountId?: EntityId, systemId?: EntityId): Promise { const [foundUsers, [accounts]] = await Promise.all([ // Test coverage: Missing branch null check; unreachable @@ -27,12 +29,12 @@ export class AccountValidationService { } async isUniqueEmailForUser(email: string, userId: EntityId): Promise { - const account = await this.accountRepo.findByUserId(userId); + const account = await this.accountRepo.findByUserId(userId); // TODO: findOrFail? return this.isUniqueEmail(email, userId, account?.id, account?.systemId?.toString()); } async isUniqueEmailForAccount(email: string, accountId: EntityId): Promise { - const account = await this.accountRepo.findById(accountId); + const account = await this.accountRepo.findById(accountId); // TODO: findOrFail? return this.isUniqueEmail(email, account.userId?.toString(), account.id, account?.systemId?.toString()); } } diff --git a/apps/server/src/modules/account/services/dto/account.dto.ts b/apps/server/src/modules/account/services/dto/account.dto.ts index 760be1f2453..c3765576e50 100644 --- a/apps/server/src/modules/account/services/dto/account.dto.ts +++ b/apps/server/src/modules/account/services/dto/account.dto.ts @@ -1,6 +1,7 @@ import { EntityId } from '@shared/domain'; import { AccountSaveDto } from './account-save.dto'; +// TODO: this vs account-save.dto? please clean up :) export class AccountDto extends AccountSaveDto { readonly id: EntityId; diff --git a/apps/server/src/modules/account/uc/account.uc.ts b/apps/server/src/modules/account/uc/account.uc.ts index b416f6e804e..b576dcc3979 100644 --- a/apps/server/src/modules/account/uc/account.uc.ts +++ b/apps/server/src/modules/account/uc/account.uc.ts @@ -8,6 +8,7 @@ import { } from '@shared/common/error'; import { Account, EntityId, Permission, PermissionService, Role, RoleName, School, User } from '@shared/domain'; import { UserRepo } from '@shared/repo'; +// TODO: module internals should be imported with relative paths import { AccountService } from '@src/modules/account/services/account.service'; import { AccountDto } from '@src/modules/account/services/dto/account.dto'; @@ -42,6 +43,14 @@ export class AccountUc { private readonly configService: ConfigService ) {} + /* HINT: there is a lot of logic here that would belong into service layer, + but since that wasnt decided when this code was written this work is not prioritised right now + + Also this is mostly directly ported feathers code, that needs a general refactoring/rewrite pass + + also it should use the new authorisation service + */ + /** * This method processes the request on the GET account search endpoint from the account controller. * @@ -55,7 +64,9 @@ export class AccountUc { const limit = query.limit ?? 10; const executingUser = await this.userRepo.findById(currentUser.userId, true); + // HINT: this can be extracted if (query.type === AccountSearchType.USERNAME) { + // HINT: even superheroes should in the future be permission based if (!(await this.isSuperhero(currentUser))) { throw new ForbiddenOperationError('Current user is not authorized to search for accounts.'); } @@ -72,8 +83,10 @@ export class AccountUc { } const account = await this.accountService.findByUserId(query.value); if (account) { + // HINT: skip and limit should be from the query return new AccountSearchListResponse([AccountResponseMapper.mapToResponse(account)], 1, 0, 1); } + // HINT: skip and limit should be from the query return new AccountSearchListResponse([], 0, 0, 0); } @@ -93,7 +106,7 @@ export class AccountUc { throw new ForbiddenOperationError('Current user is not authorized to search for accounts.'); } const account = await this.accountService.findById(params.id); - return AccountResponseMapper.mapToResponse(account); + return AccountResponseMapper.mapToResponse(account); // TODO: mapping should be done in controller } async saveAccount(dto: AccountSaveDto): Promise { @@ -162,6 +175,8 @@ export class AccountUc { throw new EntityNotFoundError(Account.name); } } + // TODO: mapping from domain to api dto should be a responsability of the controller + return AccountResponseMapper.mapToResponse(targetAccount); } @@ -300,6 +315,7 @@ export class AccountUc { } } + // TODO: remove /** * * @deprecated this is for legacy login strategies only. Login strategies in Nest.js should use {@link AuthenticationService} diff --git a/apps/server/src/shared/domain/interface/account.ts b/apps/server/src/shared/domain/interface/account.ts index 5c4d61d06ce..b79c70e5bb4 100644 --- a/apps/server/src/shared/domain/interface/account.ts +++ b/apps/server/src/shared/domain/interface/account.ts @@ -1,3 +1,4 @@ +// TODO: somehow make clear that this is either a local or a keycloak account. For example by defining this as a union type export type IAccount = { id: string; username?: string; From 40fb0c0d0403027b842b5adb2b3e6b642a4cca93 Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Thu, 14 Sep 2023 15:14:39 +0200 Subject: [PATCH 02/24] Delete auto generated test file. --- .../controller/account.controller.spec.ts | 25 ------------------- 1 file changed, 25 deletions(-) delete mode 100644 apps/server/src/modules/account/controller/account.controller.spec.ts diff --git a/apps/server/src/modules/account/controller/account.controller.spec.ts b/apps/server/src/modules/account/controller/account.controller.spec.ts deleted file mode 100644 index 093fdde0546..00000000000 --- a/apps/server/src/modules/account/controller/account.controller.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AccountUc } from '../uc/account.uc'; -import { AccountController } from './account.controller'; - -// TODO: delete this file -describe('account.controller', () => { - let module: TestingModule; - let controller: AccountController; - beforeAll(async () => { - module = await Test.createTestingModule({ - providers: [ - AccountController, - { - provide: AccountUc, - useValue: {}, - }, - ], - }).compile(); - controller = module.get(AccountController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); From 8db2af2ebe11e1007a7797fb65f98003ce4314b6 Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Thu, 14 Sep 2023 15:17:30 +0200 Subject: [PATCH 03/24] Use setup method and account factory in account entity to dto mapper test. --- .../account-entity-to-dto.mapper.spec.ts | 144 +++++++----------- .../shared/testing/factory/account.factory.ts | 28 ++++ 2 files changed, 87 insertions(+), 85 deletions(-) diff --git a/apps/server/src/modules/account/mapper/account-entity-to-dto.mapper.spec.ts b/apps/server/src/modules/account/mapper/account-entity-to-dto.mapper.spec.ts index fc459b63f18..6539a5418b9 100644 --- a/apps/server/src/modules/account/mapper/account-entity-to-dto.mapper.spec.ts +++ b/apps/server/src/modules/account/mapper/account-entity-to-dto.mapper.spec.ts @@ -1,5 +1,5 @@ import { Account } from '@shared/domain'; -import { ObjectId } from 'bson'; +import { accountFactory } from '@shared/testing'; import { AccountEntityToDtoMapper } from './account-entity-to-dto.mapper'; describe('AccountEntityToDtoMapper', () => { @@ -14,106 +14,80 @@ describe('AccountEntityToDtoMapper', () => { }); describe('mapToDto', () => { - // TODO: describe(when... it(should...) - // TODO: use setup function - it('should map all fields', () => { - // TODO: use Account factory instead - const testEntity: Account = { - _id: new ObjectId(), - id: 'id', - createdAt: new Date(), - updatedAt: new Date(), - userId: new ObjectId(), - username: 'username', - activated: true, - credentialHash: 'credentialHash', - expiresAt: new Date(), - lasttriedFailedLogin: new Date(), - password: 'password', - systemId: new ObjectId(), - token: 'token', - }; - const ret = AccountEntityToDtoMapper.mapToDto(testEntity); - - // TODO: compare with object instead, to have only a single expect() - expect(ret.id).toBe(testEntity.id); - expect(ret.createdAt).toEqual(testEntity.createdAt); - expect(ret.updatedAt).toEqual(testEntity.createdAt); - expect(ret.userId).toBe(testEntity.userId?.toString()); - expect(ret.username).toBe(testEntity.username); - expect(ret.activated).toBe(testEntity.activated); - expect(ret.credentialHash).toBe(testEntity.credentialHash); - expect(ret.expiresAt).toBe(testEntity.expiresAt); - expect(ret.lasttriedFailedLogin).toBe(testEntity.lasttriedFailedLogin); - expect(ret.password).toBe(testEntity.password); - expect(ret.systemId).toBe(testEntity.systemId?.toString()); - expect(ret.token).toBe(testEntity.token); - }); + describe('When mapping AccountEntity to AccountDto', () => { + const setup = () => { + const fullEntity = accountFactory.withAllProperties().buildWithId({}, '000000000000000000000001'); + + const missingSystemUserIdEntity: Account = accountFactory.withoutSystemAndUserId().build(); - it('should ignore missing ids', () => { - // TODO: use Account factory instead - const testEntity: Account = { - _id: new ObjectId(), - id: 'id', - username: 'username', - createdAt: new Date(), - updatedAt: new Date(), + return { fullEntity, missingSystemUserIdEntity }; }; - const ret = AccountEntityToDtoMapper.mapToDto(testEntity); - expect(ret.userId).toBeUndefined(); - expect(ret.systemId).toBeUndefined(); + it('should map all fields', () => { + const { fullEntity } = setup(); + + const ret = AccountEntityToDtoMapper.mapToDto(fullEntity); + + expect({ ...ret, _id: fullEntity._id }).toMatchObject(fullEntity); + }); + + it('should ignore missing ids', () => { + const { missingSystemUserIdEntity } = setup(); + + const ret = AccountEntityToDtoMapper.mapToDto(missingSystemUserIdEntity); + + expect(ret.userId).toBeUndefined(); + expect(ret.systemId).toBeUndefined(); + }); }); }); describe('mapSearchResult', () => { - it('should use actual date if date is', () => { - const testEntity1: Account = { - _id: new ObjectId(), - id: '1', - username: '1', - createdAt: new Date(), - updatedAt: new Date(), - }; - const testEntity2: Account = { - _id: new ObjectId(), - id: '2', - username: '2', - createdAt: new Date(), - updatedAt: new Date(), + describe('When mapping multiple Account entities', () => { + const setup = () => { + const testEntity1: Account = accountFactory.buildWithId({}, '000000000000000000000001'); + const testEntity2: Account = accountFactory.buildWithId({}, '000000000000000000000002'); + + const testAmount = 10; + + const testEntities = [testEntity1, testEntity2]; + + return { testEntities, testAmount }; }; - const testAmount = 10; - const [accounts, total] = AccountEntityToDtoMapper.mapSearchResult([[testEntity1, testEntity2], testAmount]); + it('should map exact same amount of entities', () => { + const { testEntities, testAmount } = setup(); - expect(total).toBe(testAmount); - expect(accounts).toHaveLength(2); - expect(accounts).toContainEqual(expect.objectContaining({ id: '1' })); - expect(accounts).toContainEqual(expect.objectContaining({ id: '2' })); + const [accounts, total] = AccountEntityToDtoMapper.mapSearchResult([testEntities, testAmount]); + + expect(total).toBe(testAmount); + expect(accounts).toHaveLength(2); + expect(accounts).toContainEqual(expect.objectContaining({ id: '000000000000000000000001' })); + expect(accounts).toContainEqual(expect.objectContaining({ id: '000000000000000000000002' })); + }); }); }); describe('mapAccountsToDto', () => { - it('should use actual date if date is', () => { - const testEntity1: Account = { - _id: new ObjectId(), - username: '1', - id: '1', - createdAt: new Date(), - updatedAt: new Date(), - }; - const testEntity2: Account = { - _id: new ObjectId(), - username: '2', - id: '2', - createdAt: new Date(), - updatedAt: new Date(), + describe('When mapping multiple Account entities', () => { + const setup = () => { + const testEntity1: Account = accountFactory.buildWithId({}, '000000000000000000000001'); + const testEntity2: Account = accountFactory.buildWithId({}, '000000000000000000000002'); + + const testEntities = [testEntity1, testEntity2]; + + return testEntities; }; - const ret = AccountEntityToDtoMapper.mapAccountsToDto([testEntity1, testEntity2]); - expect(ret).toHaveLength(2); - expect(ret).toContainEqual(expect.objectContaining({ id: '1' })); - expect(ret).toContainEqual(expect.objectContaining({ id: '2' })); + it('should map all entities', () => { + const testEntities = setup(); + + const ret = AccountEntityToDtoMapper.mapAccountsToDto(testEntities); + + expect(ret).toHaveLength(2); + expect(ret).toContainEqual(expect.objectContaining({ id: '000000000000000000000001' })); + expect(ret).toContainEqual(expect.objectContaining({ id: '000000000000000000000002' })); + }); }); }); }); diff --git a/apps/server/src/shared/testing/factory/account.factory.ts b/apps/server/src/shared/testing/factory/account.factory.ts index df0f261a83c..055224df377 100644 --- a/apps/server/src/shared/testing/factory/account.factory.ts +++ b/apps/server/src/shared/testing/factory/account.factory.ts @@ -21,6 +21,34 @@ class AccountFactory extends BaseFactory { return this.params(params); } + + withAllProperties(): this { + return this.params({ + userId: new ObjectId(), + username: 'username', + activated: true, + credentialHash: 'credentialHash', + expiresAt: new Date(), + lasttriedFailedLogin: new Date(), + password: 'password', + systemId: new ObjectId(), + token: 'token', + }).afterBuild((acc) => { + return { + ...acc, + createdAt: new Date(), + updatedAt: new Date(), + }; + }); + } + + withoutSystemAndUserId(): this { + return this.params({ + username: 'username', + systemId: undefined, + userId: undefined, + }); + } } export const defaultTestPassword = 'DummyPasswd!1'; From 2a33de2ed33b25794003d26a206cb822048e6889 Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Thu, 14 Sep 2023 15:58:07 +0200 Subject: [PATCH 04/24] Use setup method and factories in account response mapper test. --- .../mapper/account-response.mapper.spec.ts | 79 +++++++++---------- 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/apps/server/src/modules/account/mapper/account-response.mapper.spec.ts b/apps/server/src/modules/account/mapper/account-response.mapper.spec.ts index a2eb6be29b1..05c345f166b 100644 --- a/apps/server/src/modules/account/mapper/account-response.mapper.spec.ts +++ b/apps/server/src/modules/account/mapper/account-response.mapper.spec.ts @@ -1,60 +1,57 @@ import { Account } from '@shared/domain'; import { AccountDto } from '@src/modules/account/services/dto/account.dto'; -import { ObjectId } from '@mikro-orm/mongodb'; +import { accountDtoFactory, accountFactory } from '@shared/testing'; import { AccountResponseMapper } from '.'; describe('AccountResponseMapper', () => { describe('mapToResponseFromEntity', () => { - it('should map all fields', () => { - const testEntity: Account = { - _id: new ObjectId(), - id: new ObjectId().toString(), - userId: new ObjectId(), - activated: true, - username: 'username', - createdAt: new Date(), - updatedAt: new Date(), - }; - const ret = AccountResponseMapper.mapToResponseFromEntity(testEntity); + describe('When mapping AccountEntity to AccountResponse', () => { + const setup = () => { + const testEntityAllFields: Account = accountFactory.withAllProperties().buildWithId(); - expect(ret.id).toBe(testEntity.id); - expect(ret.userId).toBe(testEntity.userId?.toString()); - expect(ret.activated).toBe(testEntity.activated); - expect(ret.username).toBe(testEntity.username); - }); + const testEntityMissingUserId: Account = accountFactory.withoutSystemAndUserId().build(); - it('should ignore missing userId', () => { - const testEntity: Account = { - _id: new ObjectId(), - id: new ObjectId().toString(), - userId: undefined, - activated: true, - username: 'username', - createdAt: new Date(), - updatedAt: new Date(), + return { testEntityAllFields, testEntityMissingUserId }; }; - const ret = AccountResponseMapper.mapToResponseFromEntity(testEntity); - expect(ret.userId).toBeUndefined(); + it('should map all fields', () => { + const { testEntityAllFields } = setup(); + + const ret = AccountResponseMapper.mapToResponseFromEntity(testEntityAllFields); + + expect(ret.id).toBe(testEntityAllFields.id); + expect(ret.userId).toBe(testEntityAllFields.userId?.toString()); + expect(ret.activated).toBe(testEntityAllFields.activated); + expect(ret.username).toBe(testEntityAllFields.username); + }); + + it('should ignore missing userId', () => { + const { testEntityMissingUserId } = setup(); + + const ret = AccountResponseMapper.mapToResponseFromEntity(testEntityMissingUserId); + + expect(ret.userId).toBeUndefined(); + }); }); }); describe('mapToResponse', () => { - it('should map all fields', () => { - const testDto: AccountDto = { - id: new ObjectId().toString(), - userId: new ObjectId().toString(), - activated: true, - username: 'username', - createdAt: new Date(), - updatedAt: new Date(), + describe('When mapping AccountDto to AccountResponse', () => { + const setup = () => { + const testDto: AccountDto = accountDtoFactory.buildWithId(); + return testDto; }; - const ret = AccountResponseMapper.mapToResponse(testDto); - expect(ret.id).toBe(testDto.id); - expect(ret.userId).toBe(testDto.userId?.toString()); - expect(ret.activated).toBe(testDto.activated); - expect(ret.username).toBe(testDto.username); + it('should map all fields', () => { + const testDto = setup(); + + const ret = AccountResponseMapper.mapToResponse(testDto); + + expect(ret.id).toBe(testDto.id); + expect(ret.userId).toBe(testDto.userId?.toString()); + expect(ret.activated).toBe(testDto.activated); + expect(ret.username).toBe(testDto.username); + }); }); }); }); From c7d00f1343490a572f1da1c7acc533a16fe26fa0 Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Fri, 15 Sep 2023 11:00:40 +0200 Subject: [PATCH 05/24] Use setup method and test structure in account idm to dto mapper tests. --- .../account-idm-to-dto.mapper.db.spec.ts | 62 +++++++++------ .../account-idm-to-dto.mapper.idm.spec.ts | 75 ++++++++++++------- 2 files changed, 87 insertions(+), 50 deletions(-) diff --git a/apps/server/src/modules/account/mapper/account-idm-to-dto.mapper.db.spec.ts b/apps/server/src/modules/account/mapper/account-idm-to-dto.mapper.db.spec.ts index c4a9e572d98..7960a0b5115 100644 --- a/apps/server/src/modules/account/mapper/account-idm-to-dto.mapper.db.spec.ts +++ b/apps/server/src/modules/account/mapper/account-idm-to-dto.mapper.db.spec.ts @@ -24,9 +24,9 @@ describe('AccountIdmToDtoMapperDb', () => { afterAll(async () => { await module.close(); }); - describe('when mapping from entity to dto', () => { - describe('mapToDto', () => { - it('should map all fields', () => { + describe('mapToDto', () => { + describe('when mapping from entity to dto', () => { + const setup = () => { const testIdmEntity: IAccount = { id: 'id', username: 'username', @@ -38,6 +38,12 @@ describe('AccountIdmToDtoMapperDb', () => { attRefFunctionalIntId: 'attRefFunctionalIntId', attRefFunctionalExtId: 'attRefFunctionalExtId', }; + return testIdmEntity; + }; + + it('should map all fields', () => { + const testIdmEntity = setup(); + const ret = mapper.mapToDto(testIdmEntity); expect(ret).toEqual( @@ -52,30 +58,42 @@ describe('AccountIdmToDtoMapperDb', () => { }) ); }); + }); + + describe('when date is undefined', () => { + const setup = () => { + const testIdmEntity: IAccount = { + id: 'id', + }; + return testIdmEntity; + }; - describe('when date is undefined', () => { - it('should use actual date', () => { - const testIdmEntity: IAccount = { - id: 'id', - }; - const ret = mapper.mapToDto(testIdmEntity); + it('should use actual date', () => { + const testIdmEntity = setup(); - const now = new Date(); - expect(ret.createdAt).toEqual(now); - expect(ret.updatedAt).toEqual(now); - }); + const ret = mapper.mapToDto(testIdmEntity); + + const now = new Date(); + expect(ret.createdAt).toEqual(now); + expect(ret.updatedAt).toEqual(now); }); + }); - describe('when a fields value is missing', () => { - it('should fill with empty string', () => { - const testIdmEntity: IAccount = { - id: 'id', - }; - const ret = mapper.mapToDto(testIdmEntity); + describe('when a fields value is missing', () => { + const setup = () => { + const testIdmEntity: IAccount = { + id: 'id', + }; + return testIdmEntity; + }; + + it('should fill with empty string', () => { + const testIdmEntity = setup(); + + const ret = mapper.mapToDto(testIdmEntity); - expect(ret.id).toBe(''); - expect(ret.username).toBe(''); - }); + expect(ret.id).toBe(''); + expect(ret.username).toBe(''); }); }); }); diff --git a/apps/server/src/modules/account/mapper/account-idm-to-dto.mapper.idm.spec.ts b/apps/server/src/modules/account/mapper/account-idm-to-dto.mapper.idm.spec.ts index b700f759667..70619a11b83 100644 --- a/apps/server/src/modules/account/mapper/account-idm-to-dto.mapper.idm.spec.ts +++ b/apps/server/src/modules/account/mapper/account-idm-to-dto.mapper.idm.spec.ts @@ -30,39 +30,52 @@ describe('AccountIdmToDtoMapperIdm', () => { await module.close(); }); - describe('when mapping from entity to dto', () => { - it('should map all fields', () => { - const testIdmEntity: IAccount = { - id: 'id', - username: 'username', - email: 'email', - firstName: 'firstName', - lastName: 'lastName', - createdDate: new Date(), - attRefTechnicalId: 'attRefTechnicalId', - attRefFunctionalIntId: 'attRefFunctionalIntId', - attRefFunctionalExtId: 'attRefFunctionalExtId', + describe('mapToDto', () => { + describe('when mapping from entity to dto', () => { + const setup = () => { + const testIdmEntity: IAccount = { + id: 'id', + username: 'username', + email: 'email', + firstName: 'firstName', + lastName: 'lastName', + createdDate: new Date(), + attRefTechnicalId: 'attRefTechnicalId', + attRefFunctionalIntId: 'attRefFunctionalIntId', + attRefFunctionalExtId: 'attRefFunctionalExtId', + }; + return testIdmEntity; }; - const ret = mapper.mapToDto(testIdmEntity); - - expect(ret).toEqual( - expect.objectContaining>({ - id: testIdmEntity.id, - idmReferenceId: undefined, - userId: testIdmEntity.attRefFunctionalIntId, - systemId: testIdmEntity.attRefFunctionalExtId, - createdAt: testIdmEntity.createdDate, - updatedAt: testIdmEntity.createdDate, - username: testIdmEntity.username, - }) - ); - }); + it('should map all fields', () => { + const testIdmEntity = setup(); + + const ret = mapper.mapToDto(testIdmEntity); + + expect(ret).toEqual( + expect.objectContaining>({ + id: testIdmEntity.id, + idmReferenceId: undefined, + userId: testIdmEntity.attRefFunctionalIntId, + systemId: testIdmEntity.attRefFunctionalExtId, + createdAt: testIdmEntity.createdDate, + updatedAt: testIdmEntity.createdDate, + username: testIdmEntity.username, + }) + ); + }); + }); describe('when date is undefined', () => { - it('should use actual date', () => { + const setup = () => { const testIdmEntity: IAccount = { id: 'id', }; + return testIdmEntity; + }; + + it('should use actual date', () => { + const testIdmEntity = setup(); + const ret = mapper.mapToDto(testIdmEntity); expect(ret.createdAt).toEqual(now); @@ -71,10 +84,16 @@ describe('AccountIdmToDtoMapperIdm', () => { }); describe('when a fields value is missing', () => { - it('should fill with empty string', () => { + const setup = () => { const testIdmEntity: IAccount = { id: 'id', }; + return testIdmEntity; + }; + + it('should fill with empty string', () => { + const testIdmEntity = setup(); + const ret = mapper.mapToDto(testIdmEntity); expect(ret.username).toBe(''); From 8234ea3034ff98a7edb751426e3a0239e2bbd1f0 Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Fri, 15 Sep 2023 12:40:37 +0200 Subject: [PATCH 06/24] Use setup method and better test structure in account repo integration test. --- .../repo/account.repo.integration.spec.ts | 412 ++++++++++++------ 1 file changed, 279 insertions(+), 133 deletions(-) diff --git a/apps/server/src/modules/account/repo/account.repo.integration.spec.ts b/apps/server/src/modules/account/repo/account.repo.integration.spec.ts index bf4a44119fe..e3733cc4a39 100644 --- a/apps/server/src/modules/account/repo/account.repo.integration.spec.ts +++ b/apps/server/src/modules/account/repo/account.repo.integration.spec.ts @@ -10,7 +10,6 @@ describe('account repo', () => { let module: TestingModule; let em: EntityManager; let repo: AccountRepo; - let mockAccounts: Account[]; beforeAll(async () => { module = await Test.createTestingModule({ @@ -25,16 +24,6 @@ describe('account repo', () => { await module.close(); }); - beforeEach(async () => { - mockAccounts = [ - accountFactory.build({ username: 'John Doe' }), - accountFactory.build({ username: 'Marry Doe' }), - accountFactory.build({ username: 'Susi Doe' }), - accountFactory.build({ username: 'Tim Doe' }), - ]; - await em.persistAndFlush(mockAccounts); - }); - afterEach(async () => { await cleanupCollections(em); }); @@ -44,183 +33,340 @@ describe('account repo', () => { }); describe('findByUserId', () => { - it('should findByUserId', async () => { - const accountToFind = accountFactory.build(); - await em.persistAndFlush(accountToFind); - em.clear(); - const account = await repo.findByUserId(accountToFind.userId ?? ''); - expect(account?.id).toEqual(accountToFind.id); + describe('When calling findByUserId with id', () => { + const setup = async () => { + const accountToFind = accountFactory.build(); + await em.persistAndFlush(accountToFind); + em.clear(); + return accountToFind; + }; + + it('should find user with id', async () => { + const accountToFind = await setup(); + const account = await repo.findByUserId(accountToFind.userId ?? ''); + expect(account?.id).toEqual(accountToFind.id); + }); }); }); describe('findByUsernameAndSystemId', () => { - it('should return account', async () => { - const accountToFind = accountFactory.withSystemId(new ObjectId(10)).build(); - await em.persistAndFlush(accountToFind); - em.clear(); - const account = await repo.findByUsernameAndSystemId(accountToFind.username ?? '', accountToFind.systemId ?? ''); - expect(account?.username).toEqual(accountToFind.username); + describe('When username and systemId are given', () => { + const setup = async () => { + const accountToFind = accountFactory.withSystemId(new ObjectId(10)).build(); + await em.persistAndFlush(accountToFind); + em.clear(); + return accountToFind; + }; + + it('should return account', async () => { + const accountToFind = await setup(); + const account = await repo.findByUsernameAndSystemId( + accountToFind.username ?? '', + accountToFind.systemId ?? '' + ); + expect(account?.username).toEqual(accountToFind.username); + }); }); - it('should return null', async () => { - const account = await repo.findByUsernameAndSystemId('', new ObjectId(undefined)); - expect(account).toBeNull(); + + describe('When username and systemId are not given', () => { + it('should return null', async () => { + const account = await repo.findByUsernameAndSystemId('', new ObjectId(undefined)); + expect(account).toBeNull(); + }); }); }); describe('findMultipleByUserId', () => { - it('should find multiple user by id', async () => { - const anAccountToFind = accountFactory.build(); - const anotherAccountToFind = accountFactory.build(); - await em.persistAndFlush(anAccountToFind); - await em.persistAndFlush(anotherAccountToFind); - em.clear(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const accounts = await repo.findMultipleByUserId([anAccountToFind.userId!, anotherAccountToFind.userId!]); - expect(accounts).toContainEqual(anAccountToFind); - expect(accounts).toContainEqual(anotherAccountToFind); - expect(accounts).toHaveLength(2); + describe('When multiple user ids are given', () => { + const setup = async () => { + const anAccountToFind = accountFactory.build(); + const anotherAccountToFind = accountFactory.build(); + await em.persistAndFlush(anAccountToFind); + await em.persistAndFlush(anotherAccountToFind); + em.clear(); + + return { anAccountToFind, anotherAccountToFind }; + }; + + it('should find multiple users', async () => { + const { anAccountToFind, anotherAccountToFind } = await setup(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const accounts = await repo.findMultipleByUserId([anAccountToFind.userId!, anotherAccountToFind.userId!]); + expect(accounts).toContainEqual(anAccountToFind); + expect(accounts).toContainEqual(anotherAccountToFind); + expect(accounts).toHaveLength(2); + }); }); - it('should return empty list if no results', async () => { - const accountToFind = accountFactory.build(); - await em.persistAndFlush(accountToFind); - em.clear(); - const accounts = await repo.findMultipleByUserId(['123456789012', '098765432101']); - expect(accounts).toHaveLength(0); + describe('When not existing user ids are given', () => { + it('should return empty list', async () => { + const accounts = await repo.findMultipleByUserId(['123456789012', '098765432101']); + expect(accounts).toHaveLength(0); + }); }); }); describe('findByUserIdOrFail', () => { - it('should find a user by id', async () => { - const accountToFind = accountFactory.build(); - await em.persistAndFlush(accountToFind); - em.clear(); - const account = await repo.findByUserIdOrFail(accountToFind.userId ?? ''); - expect(account.id).toEqual(accountToFind.id); + describe('When existing id is given', () => { + const setup = async () => { + const accountToFind = accountFactory.build(); + await em.persistAndFlush(accountToFind); + em.clear(); + return accountToFind; + }; + it('should find a user', async () => { + const accountToFind = await setup(); + const account = await repo.findByUserIdOrFail(accountToFind.userId ?? ''); + expect(account.id).toEqual(accountToFind.id); + }); }); - it('should throw if id does not exist', async () => { - const accountToFind = accountFactory.build(); - await em.persistAndFlush(accountToFind); - em.clear(); - await expect(repo.findByUserIdOrFail('123456789012')).rejects.toThrow(NotFoundError); + describe('When id does not exist', () => { + it('should throw', async () => { + await expect(repo.findByUserIdOrFail('123456789012')).rejects.toThrow(NotFoundError); + }); }); }); describe('getObjectReference', () => { - it('should return a valid reference', async () => { - const user = userFactory.buildWithId(); - const account = accountFactory.build({ userId: user.id }); - await em.persistAndFlush([user, account]); - - const reference = repo.getObjectReference(User, account.userId ?? ''); - - expect(reference).toBe(user); + describe('When a user id is given', () => { + const setup = async () => { + const user = userFactory.buildWithId(); + const account = accountFactory.build({ userId: user.id }); + await em.persistAndFlush([user, account]); + return { user, account }; + }; + it('should return a valid reference', async () => { + const { user, account } = await setup(); + + const reference = repo.getObjectReference(User, account.userId ?? ''); + + expect(reference).toBe(user); + }); }); }); describe('saveWithoutFlush', () => { - it('should add an account to the persist stack', () => { - const account = accountFactory.build(); - - repo.saveWithoutFlush(account); - expect(em.getUnitOfWork().getPersistStack().size).toBe(1); + describe('When calling saveWithoutFlush', () => { + const setup = () => { + const account = accountFactory.build(); + return account; + }; + it('should add an account to the persist stack', () => { + const account = setup(); + + repo.saveWithoutFlush(account); + expect(em.getUnitOfWork().getPersistStack().size).toBe(1); + }); }); }); describe('flush', () => { - it('should flush after save', async () => { - const account = accountFactory.build(); - em.persist(account); + describe('When repo is flushed', () => { + const setup = () => { + const account = accountFactory.build(); + em.persist(account); + return account; + }; + + it('should save account', async () => { + const account = setup(); - expect(account.id).toBeNull(); + expect(account.id).toBeNull(); - await repo.flush(); + await repo.flush(); - expect(account.id).not.toBeNull(); + expect(account.id).not.toBeNull(); + }); }); }); - describe('findByUsername', () => { - it('should find account by user name', async () => { - const originalUsername = 'USER@EXAMPLE.COM'; - const account = accountFactory.build({ username: originalUsername }); - await em.persistAndFlush([account]); - em.clear(); - - const [result] = await repo.searchByUsernameExactMatch('USER@EXAMPLE.COM'); - expect(result).toHaveLength(1); - expect(result[0]).toEqual(expect.objectContaining({ username: originalUsername })); - - const [result2] = await repo.searchByUsernamePartialMatch('user'); - expect(result2).toHaveLength(1); - expect(result2[0]).toEqual(expect.objectContaining({ username: originalUsername })); + describe('searchByUsernamePartialMatch', () => { + describe('When searching with a partial user name', () => { + const setup = async () => { + const originalUsername = 'USER@EXAMPLE.COM'; + const partialUsername = 'user'; + const account = accountFactory.build({ username: originalUsername }); + await em.persistAndFlush([account]); + em.clear(); + return { originalUsername, partialUsername, account }; + }; + + it('should find exact one user', async () => { + const { originalUsername, partialUsername } = await setup(); + const [result] = await repo.searchByUsernamePartialMatch(partialUsername); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(expect.objectContaining({ username: originalUsername })); + }); }); - it('should find account by user name, ignoring case', async () => { - const originalUsername = 'USER@EXAMPLE.COM'; - const account = accountFactory.build({ username: originalUsername }); - await em.persistAndFlush([account]); - em.clear(); - - let [accounts] = await repo.searchByUsernameExactMatch('USER@example.COM'); - expect(accounts).toHaveLength(1); - expect(accounts[0]).toEqual(expect.objectContaining({ username: originalUsername })); + }); - [accounts] = await repo.searchByUsernameExactMatch('user@example.com'); - expect(accounts).toHaveLength(1); - expect(accounts[0]).toEqual(expect.objectContaining({ username: originalUsername })); + describe('searchByUsernameExactMatch', () => { + describe('When searching for an exact match', () => { + const setup = async () => { + const originalUsername = 'USER@EXAMPLE.COM'; + const account = accountFactory.build({ username: originalUsername }); + await em.persistAndFlush([account]); + em.clear(); + return { originalUsername, account }; + }; + + it('should find exact one account', async () => { + const { originalUsername } = await setup(); + + const [result] = await repo.searchByUsernameExactMatch(originalUsername); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(expect.objectContaining({ username: originalUsername })); + }); }); - it('should not find by wildcard', async () => { - const originalUsername = 'USER@EXAMPLE.COM'; - const account = accountFactory.build({ username: originalUsername }); - await em.persistAndFlush([account]); - em.clear(); - let [accounts] = await repo.searchByUsernameExactMatch('USER@EXAMPLECCOM'); - expect(accounts).toHaveLength(0); + describe('When searching by username', () => { + const setup = async () => { + const originalUsername = 'USER@EXAMPLE.COM'; + const partialLowerCaseUsername = 'USER@example.COM'; + const lowercaseUsername = 'user@example.com'; + const account = accountFactory.build({ username: originalUsername }); + await em.persistAndFlush([account]); + em.clear(); + return { originalUsername, partialLowerCaseUsername, lowercaseUsername, account }; + }; + + it('should find account by user name, ignoring case', async () => { + const { originalUsername, partialLowerCaseUsername, lowercaseUsername } = await setup(); + + let [accounts] = await repo.searchByUsernameExactMatch(partialLowerCaseUsername); + expect(accounts).toHaveLength(1); + expect(accounts[0]).toEqual(expect.objectContaining({ username: originalUsername })); + + [accounts] = await repo.searchByUsernameExactMatch(lowercaseUsername); + expect(accounts).toHaveLength(1); + expect(accounts[0]).toEqual(expect.objectContaining({ username: originalUsername })); + }); + }); - [accounts] = await repo.searchByUsernameExactMatch('.*'); - expect(accounts).toHaveLength(0); + describe('When using wildcard', () => { + const setup = async () => { + const originalUsername = 'USER@EXAMPLE.COM'; + const missingDotUserName = 'USER@EXAMPLECCOM'; + const wildcard = '.*'; + const account = accountFactory.build({ username: originalUsername }); + await em.persistAndFlush([account]); + em.clear(); + return { originalUsername, missingDotUserName, wildcard, account }; + }; + + it('should not find account', async () => { + const { missingDotUserName, wildcard } = await setup(); + + let [accounts] = await repo.searchByUsernameExactMatch(missingDotUserName); + expect(accounts).toHaveLength(0); + + [accounts] = await repo.searchByUsernameExactMatch(wildcard); + expect(accounts).toHaveLength(0); + }); }); }); - describe('deleteId', () => { - it('should delete an account by id', async () => { - const account = accountFactory.buildWithId(); - await em.persistAndFlush([account]); + describe('deleteById', () => { + describe('When an id is given', () => { + const setup = async () => { + const account = accountFactory.buildWithId(); + await em.persistAndFlush([account]); + + return account; + }; - await expect(repo.deleteById(account.id)).resolves.not.toThrow(); + it('should delete an account by id', async () => { + const account = await setup(); - await expect(repo.findById(account.id)).rejects.toThrow(NotFoundError); + await expect(repo.deleteById(account.id)).resolves.not.toThrow(); + + await expect(repo.findById(account.id)).rejects.toThrow(NotFoundError); + }); }); }); describe('deleteByUserId', () => { - it('should delete an account by user id', async () => { - const user = userFactory.buildWithId(); - const account = accountFactory.build({ userId: user.id }); - await em.persistAndFlush([user, account]); + describe('When an user id is given', () => { + const setup = async () => { + const user = userFactory.buildWithId(); + const account = accountFactory.build({ userId: user.id }); + await em.persistAndFlush([user, account]); + + return { user, account }; + }; - await expect(repo.deleteByUserId(user.id)).resolves.not.toThrow(); + it('should delete an account by user id', async () => { + const { user, account } = await setup(); - await expect(repo.findById(account.id)).rejects.toThrow(NotFoundError); + await expect(repo.deleteByUserId(user.id)).resolves.not.toThrow(); + + await expect(repo.findById(account.id)).rejects.toThrow(NotFoundError); + }); }); }); describe('findMany', () => { - it('should find all accounts', async () => { - const foundAccounts = await repo.findMany(); - expect(foundAccounts).toEqual(mockAccounts); - }); - it('limit the result set ', async () => { - const limit = 1; - const foundAccounts = await repo.findMany(0, limit); - expect(foundAccounts).toHaveLength(limit); - }); - it('skip n entries ', async () => { - const offset = 2; - const foundAccounts = await repo.findMany(offset); - expect(foundAccounts).toHaveLength(mockAccounts.length - offset); + describe('When no limit and offset are given', () => { + const setup = async () => { + const mockAccounts = [ + accountFactory.build({ username: 'John Doe' }), + accountFactory.build({ username: 'Marry Doe' }), + accountFactory.build({ username: 'Susi Doe' }), + accountFactory.build({ username: 'Tim Doe' }), + ]; + await em.persistAndFlush(mockAccounts); + return mockAccounts; + }; + + it('should find all accounts', async () => { + const mockAccounts = await setup(); + const foundAccounts = await repo.findMany(); + expect(foundAccounts).toEqual(mockAccounts); + }); + }); + + describe('When limit is given', () => { + const setup = async () => { + const limit = 1; + + const mockAccounts = [ + accountFactory.build({ username: 'John Doe' }), + accountFactory.build({ username: 'Marry Doe' }), + accountFactory.build({ username: 'Susi Doe' }), + accountFactory.build({ username: 'Tim Doe' }), + ]; + await em.persistAndFlush(mockAccounts); + return { limit, mockAccounts }; + }; + + it('should limit the result set', async () => { + const { limit } = await setup(); + const foundAccounts = await repo.findMany(0, limit); + expect(foundAccounts).toHaveLength(limit); + }); + }); + + describe('When offset is given', () => { + const setup = async () => { + const offset = 2; + + const mockAccounts = [ + accountFactory.build({ username: 'John Doe' }), + accountFactory.build({ username: 'Marry Doe' }), + accountFactory.build({ username: 'Susi Doe' }), + accountFactory.build({ username: 'Tim Doe' }), + ]; + await em.persistAndFlush(mockAccounts); + return { offset, mockAccounts }; + }; + + it('should skip n entries', async () => { + const { offset, mockAccounts } = await setup(); + + const foundAccounts = await repo.findMany(offset); + expect(foundAccounts).toHaveLength(mockAccounts.length - offset); + }); }); }); }); From 4208bc26621987d17b99f58a428c0343a84c605f Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Mon, 18 Sep 2023 16:33:55 +0200 Subject: [PATCH 07/24] Use setup and new test structure in account api test (part 1). --- .../controller/api-test/account.api.spec.ts | 950 +++++++++++++++--- 1 file changed, 814 insertions(+), 136 deletions(-) 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 aa8922390f2..583b6968cc5 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 @@ -1,44 +1,53 @@ -import { EntityManager } from '@mikro-orm/core'; -import { ExecutionContext, INestApplication } from '@nestjs/common'; +import { EntityManager } from '@mikro-orm/mongodb'; +import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Account, Permission, RoleName, User } from '@shared/domain'; -import { accountFactory, mapUserToCurrentUser, roleFactory, schoolFactory, userFactory } from '@shared/testing'; import { - AccountByIdBodyParams, + accountFactory, + roleFactory, + schoolFactory, + userFactory, + TestApiClient, + cleanupCollections, +} from '@shared/testing'; +import { AccountSearchQueryParams, AccountSearchType, PatchMyAccountParams, PatchMyPasswordParams, } from '@src/modules/account/controller/dto'; -import { ICurrentUser } from '@src/modules/authentication'; -// HINT: general todo to not get JwtAuthGuard from within the module -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; import { ServerTestModule } from '@src/modules/server/server.module'; -import { Request } from 'express'; -import request from 'supertest'; describe('Account Controller (API)', () => { const basePath = '/account'; let app: INestApplication; let em: EntityManager; + let testApiClient: TestApiClient; - let adminAccount: Account; - let teacherAccount: Account; - let studentAccount: Account; - let superheroAccount: Account; + // let adminAccount: Account; + // let teacherAccount: Account; + // let studentAccount: Account; + // let superheroAccount: Account; - let adminUser: User; - let teacherUser: User; - let studentUser: User; - let superheroUser: User; + // let adminUser: User; + // let teacherUser: User; + // let studentUser: User; + // let superheroUser: User; - let currentUser: ICurrentUser; + // let currentUser: ICurrentUser; const defaultPassword = 'DummyPasswd!1'; const defaultPasswordHash = '$2a$10$/DsztV5o6P5piW2eWJsxw.4nHovmJGBA.QNwiTmuZ/uvUc40b.Uhu'; - const setup = async () => { + const mapUserToAccount = (user: User): Account => + accountFactory.buildWithId({ + userId: user.id, + username: user.email, + password: defaultPasswordHash, + }); + + /* const setup = async () => { // TODO: revisit test structure. setup for each functional situation, return values instead of global variables const school = schoolFactory.buildWithId(); @@ -73,30 +82,21 @@ describe('Account Controller (API)', () => { await em.flush(); // TODO: return {adminAccount, teacherAccount, ...} - }; + }; */ beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ServerTestModule], - }) - // TODO: We need to remove this old hack, use the new api helper insteads - .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, basePath); }); beforeEach(async () => { - await setup(); + await cleanupCollections(em); }); afterAll(async () => { @@ -105,158 +105,527 @@ describe('Account Controller (API)', () => { }); describe('[PATCH] me/password', () => { - it(`should update the current user's (temporary) password`, async () => { - currentUser = mapUserToCurrentUser(studentUser, studentAccount); - const params: PatchMyPasswordParams = { - password: 'Valid12$', - confirmPassword: 'Valid12$', + describe('When patching with a valid password', () => { + const setup = async () => { + const school = schoolFactory.buildWithId(); + const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); + const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); + const studentAccount = mapUserToAccount(studentUser); + + em.persist(school); + em.persist([studentRoles]); + em.persist([studentUser]); + em.persist([studentAccount]); + await em.flush(); + + const loggedInClient = await testApiClient.login(studentAccount); + + const passwordPatchParams: PatchMyPasswordParams = { + password: 'Valid12$', + confirmPassword: 'Valid12$', + }; + + return { passwordPatchParams, loggedInClient, studentAccount }; }; - await request(app.getHttpServer()) // - .patch(`${basePath}/me/password`) - .send(params) - .expect(200); - const updatedAccount = await em.findOneOrFail(Account, studentAccount.id); - expect(updatedAccount.password).not.toEqual(defaultPasswordHash); + it(`should update the current user's (temporary) password`, async () => { + const { passwordPatchParams, loggedInClient, studentAccount } = await setup(); + + await loggedInClient.patch('/me/password', passwordPatchParams).expect(200); + + const updatedAccount = await em.findOneOrFail(Account, studentAccount.id); + expect(updatedAccount.password).not.toEqual(defaultPasswordHash); + }); }); - it('should reject if new password is weak', async () => { - currentUser = mapUserToCurrentUser(studentUser, studentAccount); - const params: PatchMyPasswordParams = { - password: 'weak', - confirmPassword: 'weak', + + describe('When using a weak password', () => { + const setup = async () => { + const school = schoolFactory.buildWithId(); + const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); + const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); + const studentAccount = mapUserToAccount(studentUser); + + em.persist(school); + em.persist([studentRoles]); + em.persist([studentUser]); + em.persist([studentAccount]); + await em.flush(); + + const loggedInClient = await testApiClient.login(studentAccount); + + const passwordPatchParams: PatchMyPasswordParams = { + password: 'weak', + confirmPassword: 'weak', + }; + + return { passwordPatchParams, loggedInClient }; }; - await request(app.getHttpServer()) // - .patch(`${basePath}/me/password`) - .send(params) - .expect(400); + + it('should reject the password change', async () => { + const { passwordPatchParams, loggedInClient } = await setup(); + + await loggedInClient.patch('/me/password', passwordPatchParams).expect(400); + }); }); }); describe('[PATCH] me', () => { - it(`should update a users account`, async () => { - const newEmailValue = 'new@mail.com'; - currentUser = mapUserToCurrentUser(studentUser, studentAccount); - const params: PatchMyAccountParams = { - passwordOld: defaultPassword, - email: newEmailValue, + describe('When patching the account with account info', () => { + const setup = async () => { + const school = schoolFactory.buildWithId(); + const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); + const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); + const studentAccount = mapUserToAccount(studentUser); + + em.persist(school); + em.persist([studentRoles]); + em.persist([studentUser]); + em.persist([studentAccount]); + await em.flush(); + + const loggedInClient = await testApiClient.login(studentAccount); + + const newEmailValue = 'new@mail.com'; + + const patchMyAccountParams: PatchMyAccountParams = { + passwordOld: defaultPassword, + email: newEmailValue, + }; + return { newEmailValue, patchMyAccountParams, loggedInClient, studentAccount }; }; - await request(app.getHttpServer()) // - .patch(`${basePath}/me`) - .send(params) - .expect(200); - const updatedAccount = await em.findOneOrFail(Account, studentAccount.id); - expect(updatedAccount.username).toEqual(newEmailValue); + it(`should update a users account`, async () => { + const { newEmailValue, patchMyAccountParams, loggedInClient, studentAccount } = await setup(); + + await loggedInClient.patch('/me', patchMyAccountParams).expect(200); + + const updatedAccount = await em.findOneOrFail(Account, studentAccount.id); + expect(updatedAccount.username).toEqual(newEmailValue); + }); }); - it('should reject if new email is not valid', async () => { - currentUser = mapUserToCurrentUser(studentUser, studentAccount); - const params: PatchMyAccountParams = { - passwordOld: defaultPassword, - email: 'invalid', + describe('When patching with a not valid email', () => { + const setup = async () => { + const school = schoolFactory.buildWithId(); + const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); + const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); + const studentAccount = mapUserToAccount(studentUser); + + em.persist(school); + em.persist([studentRoles]); + em.persist([studentUser]); + em.persist([studentAccount]); + await em.flush(); + + const loggedInClient = await testApiClient.login(studentAccount); + + const newEmailValue = 'new@mail.com'; + + const patchMyAccountParams: PatchMyAccountParams = { + passwordOld: defaultPassword, + email: 'invalid', + }; + return { newEmailValue, patchMyAccountParams, loggedInClient }; }; - await request(app.getHttpServer()) // - .patch(`${basePath}/me`) - .send(params) - .expect(400); + + it('should reject patch request', async () => { + const { patchMyAccountParams, loggedInClient } = await setup(); + + await loggedInClient.patch('/me', patchMyAccountParams).expect(400); + }); }); }); describe('[GET]', () => { - it('should search for user id', async () => { - currentUser = mapUserToCurrentUser(superheroUser, superheroAccount); - const query: AccountSearchQueryParams = { - type: AccountSearchType.USER_ID, - value: studentUser.id, - skip: 5, - limit: 5, + describe('When searching with a superhero user', () => { + const setup = async () => { + const school = schoolFactory.buildWithId(); + + const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); + const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); + + const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); + const superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); + + const studentAccount = mapUserToAccount(studentUser); + const superheroAccount = mapUserToAccount(superheroUser); + + em.persist(school); + em.persist([studentRoles, superheroRoles]); + em.persist([studentUser, superheroUser]); + em.persist([studentAccount, superheroAccount]); + await em.flush(); + + const loggedInClient = await testApiClient.login(superheroAccount); + + const query: AccountSearchQueryParams = { + type: AccountSearchType.USER_ID, + value: studentUser.id, + skip: 5, + limit: 5, + }; + + return { query, loggedInClient }; }; - await request(app.getHttpServer()) // - .get(`${basePath}`) - .query(query) - .send() - .expect(200); + it('should successfully search for user id', async () => { + const { query, loggedInClient } = await setup(); + + await loggedInClient.get().query(query).send().expect(200); + }); }); + // If skip is too big, just return an empty list. // We testing it here, because we are mocking the database in the use case unit tests // and for realistic behavior we need database. - it('should search for user id with large skip', async () => { - currentUser = mapUserToCurrentUser(superheroUser); - const query: AccountSearchQueryParams = { - type: AccountSearchType.USER_ID, - value: studentUser.id, - skip: 50000, - limit: 5, + describe('When searching with a superhero user with large skip', () => { + const setup = async () => { + const school = schoolFactory.buildWithId(); + + const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); + const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); + + const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); + const superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); + + const studentAccount = mapUserToAccount(studentUser); + const superheroAccount = mapUserToAccount(superheroUser); + + em.persist(school); + em.persist([studentRoles, superheroRoles]); + em.persist([studentUser, superheroUser]); + em.persist([studentAccount, superheroAccount]); + await em.flush(); + + const loggedInClient = await testApiClient.login(superheroAccount); + + const query: AccountSearchQueryParams = { + type: AccountSearchType.USER_ID, + value: studentUser.id, + skip: 50000, + limit: 5, + }; + + return { query, loggedInClient }; }; - await request(app.getHttpServer()) // - .get(`${basePath}`) - .query(query) - .send() - .expect(200); + it('should search for user id', async () => { + const { query, loggedInClient } = await setup(); + + await loggedInClient.get().query(query).send().expect(200); + }); }); - it('should search for user name', async () => { - currentUser = mapUserToCurrentUser(superheroUser, superheroAccount); - const query: AccountSearchQueryParams = { - type: AccountSearchType.USERNAME, - value: '', - skip: 5, - limit: 5, + + describe('When searching with a superhero user', () => { + const setup = async () => { + const school = schoolFactory.buildWithId(); + + const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); + const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); + + const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); + const superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); + + const studentAccount = mapUserToAccount(studentUser); + const superheroAccount = mapUserToAccount(superheroUser); + + em.persist(school); + em.persist([studentRoles, superheroRoles]); + em.persist([studentUser, superheroUser]); + em.persist([studentAccount, superheroAccount]); + await em.flush(); + + const loggedInClient = await testApiClient.login(superheroAccount); + + const query: AccountSearchQueryParams = { + type: AccountSearchType.USERNAME, + value: '', + skip: 5, + limit: 5, + }; + + return { query, loggedInClient }; }; - await request(app.getHttpServer()) // - .get(`${basePath}`) - .query(query) - .send() - .expect(200); + it('should search for user name', async () => { + const { query, loggedInClient } = await setup(); + + await loggedInClient.get().query(query).send().expect(200); + }); }); - it('should reject if type is unknown', async () => { - currentUser = mapUserToCurrentUser(superheroUser, superheroAccount); - const query: AccountSearchQueryParams = { - type: '' as AccountSearchType, - value: '', - skip: 5, - limit: 5, + + describe('When searching with a superhero user', () => { + const setup = async () => { + const school = schoolFactory.buildWithId(); + + const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); + const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); + + const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); + const superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); + + const studentAccount = mapUserToAccount(studentUser); + const superheroAccount = mapUserToAccount(superheroUser); + + em.persist(school); + em.persist([studentRoles, superheroRoles]); + em.persist([studentUser, superheroUser]); + em.persist([studentAccount, superheroAccount]); + await em.flush(); + + const loggedInClient = await testApiClient.login(superheroAccount); + + const query: AccountSearchQueryParams = { + type: '' as AccountSearchType, + value: '', + skip: 5, + limit: 5, + }; + + return { query, loggedInClient }; }; - await request(app.getHttpServer()) // - .get(`${basePath}`) - .query(query) - .send() - .expect(400); + + it('should reject if type is unknown', async () => { + const { query, loggedInClient } = await setup(); + + await loggedInClient.get().query(query).send().expect(400); + }); }); - it('should reject if user is not authorized', async () => { - currentUser = mapUserToCurrentUser(adminUser, adminAccount); - const query: AccountSearchQueryParams = { - type: AccountSearchType.USERNAME, - value: '', - skip: 5, - limit: 5, + describe('When searching with an admin user (not authorized)', () => { + const setup = async () => { + const school = schoolFactory.buildWithId(); + + const adminRoles = roleFactory.build({ + name: RoleName.ADMINISTRATOR, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }); + const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); + + const adminUser = userFactory.buildWithId({ school, roles: [adminRoles] }); + const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); + + const adminAccount = mapUserToAccount(adminUser); + const studentAccount = mapUserToAccount(studentUser); + + em.persist(school); + em.persist([adminRoles, studentRoles]); + em.persist([adminUser, studentUser]); + em.persist([adminAccount, studentAccount]); + await em.flush(); + + const loggedInClient = await testApiClient.login(adminAccount); + + const query: AccountSearchQueryParams = { + type: AccountSearchType.USERNAME, + value: '', + skip: 5, + limit: 5, + }; + + return { query, loggedInClient, studentAccount }; }; - await request(app.getHttpServer()) // - .get(`${basePath}`) - .query(query) - .send() - .expect(403); + + it('should reject search for user', async () => { + const { query, loggedInClient } = await setup(); + + await loggedInClient.get().query(query).send().expect(403); + }); }); }); + /* describe('[GET] :id', () => { + describe('', () => { + const setup = async () => { + const school = schoolFactory.buildWithId(); + + const adminRoles = roleFactory.build({ + name: RoleName.ADMINISTRATOR, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }); + const teacherRoles = roleFactory.build({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] }); + const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); + const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); + + const adminUser = userFactory.buildWithId({ school, roles: [adminRoles] }); + const teacherUser = userFactory.buildWithId({ school, roles: [teacherRoles] }); + const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); + const superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); + + const mapUserToAccount = (user: User): Account => + accountFactory.buildWithId({ + userId: user.id, + username: user.email, + password: defaultPasswordHash, + }); + const adminAccount = mapUserToAccount(adminUser); + const teacherAccount = mapUserToAccount(teacherUser); + const studentAccount = mapUserToAccount(studentUser); + const superheroAccount = mapUserToAccount(superheroUser); + + em.persist(school); + em.persist([adminRoles, teacherRoles, studentRoles, superheroRoles]); + em.persist([adminUser, teacherUser, studentUser, superheroUser]); + em.persist([adminAccount, teacherAccount, studentAccount, superheroAccount]); + await em.flush(); + + const loggedInClient = await testApiClient.login(studentAccount); + + const params: PatchMyPasswordParams = { + password: 'Valid12$', + confirmPassword: 'Valid12$', + }; + + return { params, loggedInClient, studentAccount }; + }; + }); it('should return account for account id', async () => { - currentUser = mapUserToCurrentUser(superheroUser, superheroAccount); + // currentUser = mapUserToCurrentUser(superheroUser, superheroAccount); await request(app.getHttpServer()) // .get(`${basePath}/${studentAccount.id}`) .expect(200); }); + describe('', () => { + const setup = async () => { + const school = schoolFactory.buildWithId(); + + const adminRoles = roleFactory.build({ + name: RoleName.ADMINISTRATOR, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }); + const teacherRoles = roleFactory.build({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] }); + const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); + const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); + + const adminUser = userFactory.buildWithId({ school, roles: [adminRoles] }); + const teacherUser = userFactory.buildWithId({ school, roles: [teacherRoles] }); + const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); + const superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); + + const mapUserToAccount = (user: User): Account => + accountFactory.buildWithId({ + userId: user.id, + username: user.email, + password: defaultPasswordHash, + }); + const adminAccount = mapUserToAccount(adminUser); + const teacherAccount = mapUserToAccount(teacherUser); + const studentAccount = mapUserToAccount(studentUser); + const superheroAccount = mapUserToAccount(superheroUser); + + em.persist(school); + em.persist([adminRoles, teacherRoles, studentRoles, superheroRoles]); + em.persist([adminUser, teacherUser, studentUser, superheroUser]); + em.persist([adminAccount, teacherAccount, studentAccount, superheroAccount]); + await em.flush(); + + const loggedInClient = await testApiClient.login(studentAccount); + + const params: PatchMyPasswordParams = { + password: 'Valid12$', + confirmPassword: 'Valid12$', + }; + + return { params, loggedInClient, studentAccount }; + }; + }); it('should reject if id has invalid format', async () => { - currentUser = mapUserToCurrentUser(superheroUser, superheroAccount); + // currentUser = mapUserToCurrentUser(superheroUser, superheroAccount); await request(app.getHttpServer()) // .get(`${basePath}/qwerty`) .send() .expect(400); }); + describe('', () => { + const setup = async () => { + const school = schoolFactory.buildWithId(); + + const adminRoles = roleFactory.build({ + name: RoleName.ADMINISTRATOR, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }); + const teacherRoles = roleFactory.build({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] }); + const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); + const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); + + const adminUser = userFactory.buildWithId({ school, roles: [adminRoles] }); + const teacherUser = userFactory.buildWithId({ school, roles: [teacherRoles] }); + const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); + const superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); + + const mapUserToAccount = (user: User): Account => + accountFactory.buildWithId({ + userId: user.id, + username: user.email, + password: defaultPasswordHash, + }); + const adminAccount = mapUserToAccount(adminUser); + const teacherAccount = mapUserToAccount(teacherUser); + const studentAccount = mapUserToAccount(studentUser); + const superheroAccount = mapUserToAccount(superheroUser); + + em.persist(school); + em.persist([adminRoles, teacherRoles, studentRoles, superheroRoles]); + em.persist([adminUser, teacherUser, studentUser, superheroUser]); + em.persist([adminAccount, teacherAccount, studentAccount, superheroAccount]); + await em.flush(); + + const loggedInClient = await testApiClient.login(studentAccount); + + const params: PatchMyPasswordParams = { + password: 'Valid12$', + confirmPassword: 'Valid12$', + }; + + return { params, loggedInClient, studentAccount }; + }; + }); it('should reject if user is not a authorized', async () => { - currentUser = mapUserToCurrentUser(adminUser, adminAccount); + // currentUser = mapUserToCurrentUser(adminUser, adminAccount); await request(app.getHttpServer()) // .get(`${basePath}/${studentAccount.id}`) .expect(403); }); + describe('', () => { + const setup = async () => { + const school = schoolFactory.buildWithId(); + + const adminRoles = roleFactory.build({ + name: RoleName.ADMINISTRATOR, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }); + const teacherRoles = roleFactory.build({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] }); + const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); + const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); + + const adminUser = userFactory.buildWithId({ school, roles: [adminRoles] }); + const teacherUser = userFactory.buildWithId({ school, roles: [teacherRoles] }); + const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); + const superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); + + const mapUserToAccount = (user: User): Account => + accountFactory.buildWithId({ + userId: user.id, + username: user.email, + password: defaultPasswordHash, + }); + const adminAccount = mapUserToAccount(adminUser); + const teacherAccount = mapUserToAccount(teacherUser); + const studentAccount = mapUserToAccount(studentUser); + const superheroAccount = mapUserToAccount(superheroUser); + + em.persist(school); + em.persist([adminRoles, teacherRoles, studentRoles, superheroRoles]); + em.persist([adminUser, teacherUser, studentUser, superheroUser]); + em.persist([adminAccount, teacherAccount, studentAccount, superheroAccount]); + await em.flush(); + + const loggedInClient = await testApiClient.login(studentAccount); + + const params: PatchMyPasswordParams = { + password: 'Valid12$', + confirmPassword: 'Valid12$', + }; + + return { params, loggedInClient, studentAccount }; + }; + }); it('should reject not existing account id', async () => { currentUser = mapUserToCurrentUser(superheroUser, superheroAccount); await request(app.getHttpServer()) // @@ -266,6 +635,50 @@ describe('Account Controller (API)', () => { }); describe('[PATCH] :id', () => { + describe('', () => { + const setup = async () => { + const school = schoolFactory.buildWithId(); + + const adminRoles = roleFactory.build({ + name: RoleName.ADMINISTRATOR, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }); + const teacherRoles = roleFactory.build({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] }); + const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); + const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); + + const adminUser = userFactory.buildWithId({ school, roles: [adminRoles] }); + const teacherUser = userFactory.buildWithId({ school, roles: [teacherRoles] }); + const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); + const superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); + + const mapUserToAccount = (user: User): Account => + accountFactory.buildWithId({ + userId: user.id, + username: user.email, + password: defaultPasswordHash, + }); + const adminAccount = mapUserToAccount(adminUser); + const teacherAccount = mapUserToAccount(teacherUser); + const studentAccount = mapUserToAccount(studentUser); + const superheroAccount = mapUserToAccount(superheroUser); + + em.persist(school); + em.persist([adminRoles, teacherRoles, studentRoles, superheroRoles]); + em.persist([adminUser, teacherUser, studentUser, superheroUser]); + em.persist([adminAccount, teacherAccount, studentAccount, superheroAccount]); + await em.flush(); + + const loggedInClient = await testApiClient.login(studentAccount); + + const params: PatchMyPasswordParams = { + password: 'Valid12$', + confirmPassword: 'Valid12$', + }; + + return { params, loggedInClient, studentAccount }; + }; + }); it('should update account', async () => { currentUser = mapUserToCurrentUser(superheroUser, superheroAccount); const body: AccountByIdBodyParams = { @@ -278,6 +691,50 @@ describe('Account Controller (API)', () => { .send(body) .expect(200); }); + describe('', () => { + const setup = async () => { + const school = schoolFactory.buildWithId(); + + const adminRoles = roleFactory.build({ + name: RoleName.ADMINISTRATOR, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }); + const teacherRoles = roleFactory.build({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] }); + const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); + const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); + + const adminUser = userFactory.buildWithId({ school, roles: [adminRoles] }); + const teacherUser = userFactory.buildWithId({ school, roles: [teacherRoles] }); + const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); + const superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); + + const mapUserToAccount = (user: User): Account => + accountFactory.buildWithId({ + userId: user.id, + username: user.email, + password: defaultPasswordHash, + }); + const adminAccount = mapUserToAccount(adminUser); + const teacherAccount = mapUserToAccount(teacherUser); + const studentAccount = mapUserToAccount(studentUser); + const superheroAccount = mapUserToAccount(superheroUser); + + em.persist(school); + em.persist([adminRoles, teacherRoles, studentRoles, superheroRoles]); + em.persist([adminUser, teacherUser, studentUser, superheroUser]); + em.persist([adminAccount, teacherAccount, studentAccount, superheroAccount]); + await em.flush(); + + const loggedInClient = await testApiClient.login(studentAccount); + + const params: PatchMyPasswordParams = { + password: 'Valid12$', + confirmPassword: 'Valid12$', + }; + + return { params, loggedInClient, studentAccount }; + }; + }); it('should reject if user is not authorized', async () => { currentUser = mapUserToCurrentUser(studentUser, studentAccount); const body: AccountByIdBodyParams = { @@ -290,6 +747,50 @@ describe('Account Controller (API)', () => { .send(body) .expect(403); }); + describe('', () => { + const setup = async () => { + const school = schoolFactory.buildWithId(); + + const adminRoles = roleFactory.build({ + name: RoleName.ADMINISTRATOR, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }); + const teacherRoles = roleFactory.build({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] }); + const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); + const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); + + const adminUser = userFactory.buildWithId({ school, roles: [adminRoles] }); + const teacherUser = userFactory.buildWithId({ school, roles: [teacherRoles] }); + const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); + const superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); + + const mapUserToAccount = (user: User): Account => + accountFactory.buildWithId({ + userId: user.id, + username: user.email, + password: defaultPasswordHash, + }); + const adminAccount = mapUserToAccount(adminUser); + const teacherAccount = mapUserToAccount(teacherUser); + const studentAccount = mapUserToAccount(studentUser); + const superheroAccount = mapUserToAccount(superheroUser); + + em.persist(school); + em.persist([adminRoles, teacherRoles, studentRoles, superheroRoles]); + em.persist([adminUser, teacherUser, studentUser, superheroUser]); + em.persist([adminAccount, teacherAccount, studentAccount, superheroAccount]); + await em.flush(); + + const loggedInClient = await testApiClient.login(studentAccount); + + const params: PatchMyPasswordParams = { + password: 'Valid12$', + confirmPassword: 'Valid12$', + }; + + return { params, loggedInClient, studentAccount }; + }; + }); it('should reject not existing account id', async () => { currentUser = mapUserToCurrentUser(superheroUser, studentAccount); const body: AccountByIdBodyParams = { @@ -305,24 +806,200 @@ describe('Account Controller (API)', () => { }); describe('[DELETE] :id', () => { + describe('', () => { + const setup = async () => { + const school = schoolFactory.buildWithId(); + + const adminRoles = roleFactory.build({ + name: RoleName.ADMINISTRATOR, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }); + const teacherRoles = roleFactory.build({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] }); + const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); + const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); + + const adminUser = userFactory.buildWithId({ school, roles: [adminRoles] }); + const teacherUser = userFactory.buildWithId({ school, roles: [teacherRoles] }); + const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); + const superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); + + const mapUserToAccount = (user: User): Account => + accountFactory.buildWithId({ + userId: user.id, + username: user.email, + password: defaultPasswordHash, + }); + const adminAccount = mapUserToAccount(adminUser); + const teacherAccount = mapUserToAccount(teacherUser); + const studentAccount = mapUserToAccount(studentUser); + const superheroAccount = mapUserToAccount(superheroUser); + + em.persist(school); + em.persist([adminRoles, teacherRoles, studentRoles, superheroRoles]); + em.persist([adminUser, teacherUser, studentUser, superheroUser]); + em.persist([adminAccount, teacherAccount, studentAccount, superheroAccount]); + await em.flush(); + + const loggedInClient = await testApiClient.login(studentAccount); + + const params: PatchMyPasswordParams = { + password: 'Valid12$', + confirmPassword: 'Valid12$', + }; + + return { params, loggedInClient, studentAccount }; + }; + }); it('should delete account', async () => { currentUser = mapUserToCurrentUser(superheroUser, studentAccount); await request(app.getHttpServer()) // .delete(`${basePath}/${studentAccount.id}`) .expect(200); }); + describe('', () => { + const setup = async () => { + const school = schoolFactory.buildWithId(); + + const adminRoles = roleFactory.build({ + name: RoleName.ADMINISTRATOR, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }); + const teacherRoles = roleFactory.build({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] }); + const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); + const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); + + const adminUser = userFactory.buildWithId({ school, roles: [adminRoles] }); + const teacherUser = userFactory.buildWithId({ school, roles: [teacherRoles] }); + const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); + const superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); + + const mapUserToAccount = (user: User): Account => + accountFactory.buildWithId({ + userId: user.id, + username: user.email, + password: defaultPasswordHash, + }); + const adminAccount = mapUserToAccount(adminUser); + const teacherAccount = mapUserToAccount(teacherUser); + const studentAccount = mapUserToAccount(studentUser); + const superheroAccount = mapUserToAccount(superheroUser); + + em.persist(school); + em.persist([adminRoles, teacherRoles, studentRoles, superheroRoles]); + em.persist([adminUser, teacherUser, studentUser, superheroUser]); + em.persist([adminAccount, teacherAccount, studentAccount, superheroAccount]); + await em.flush(); + + const loggedInClient = await testApiClient.login(studentAccount); + + const params: PatchMyPasswordParams = { + password: 'Valid12$', + confirmPassword: 'Valid12$', + }; + + return { params, loggedInClient, studentAccount }; + }; + }); it('should reject invalid account id format', async () => { currentUser = mapUserToCurrentUser(superheroUser, studentAccount); await request(app.getHttpServer()) // .delete(`${basePath}/qwerty`) .expect(400); }); + describe('', () => { + const setup = async () => { + const school = schoolFactory.buildWithId(); + + const adminRoles = roleFactory.build({ + name: RoleName.ADMINISTRATOR, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }); + const teacherRoles = roleFactory.build({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] }); + const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); + const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); + + const adminUser = userFactory.buildWithId({ school, roles: [adminRoles] }); + const teacherUser = userFactory.buildWithId({ school, roles: [teacherRoles] }); + const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); + const superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); + + const mapUserToAccount = (user: User): Account => + accountFactory.buildWithId({ + userId: user.id, + username: user.email, + password: defaultPasswordHash, + }); + const adminAccount = mapUserToAccount(adminUser); + const teacherAccount = mapUserToAccount(teacherUser); + const studentAccount = mapUserToAccount(studentUser); + const superheroAccount = mapUserToAccount(superheroUser); + + em.persist(school); + em.persist([adminRoles, teacherRoles, studentRoles, superheroRoles]); + em.persist([adminUser, teacherUser, studentUser, superheroUser]); + em.persist([adminAccount, teacherAccount, studentAccount, superheroAccount]); + await em.flush(); + + const loggedInClient = await testApiClient.login(studentAccount); + + const params: PatchMyPasswordParams = { + password: 'Valid12$', + confirmPassword: 'Valid12$', + }; + + return { params, loggedInClient, studentAccount }; + }; + }); it('should reject if user is not a authorized', async () => { currentUser = mapUserToCurrentUser(adminUser, adminAccount); await request(app.getHttpServer()) // .delete(`${basePath}/${studentAccount.id}`) .expect(403); }); + describe('', () => { + const setup = async () => { + const school = schoolFactory.buildWithId(); + + const adminRoles = roleFactory.build({ + name: RoleName.ADMINISTRATOR, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }); + const teacherRoles = roleFactory.build({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] }); + const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); + const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); + + const adminUser = userFactory.buildWithId({ school, roles: [adminRoles] }); + const teacherUser = userFactory.buildWithId({ school, roles: [teacherRoles] }); + const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); + const superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); + + const mapUserToAccount = (user: User): Account => + accountFactory.buildWithId({ + userId: user.id, + username: user.email, + password: defaultPasswordHash, + }); + const adminAccount = mapUserToAccount(adminUser); + const teacherAccount = mapUserToAccount(teacherUser); + const studentAccount = mapUserToAccount(studentUser); + const superheroAccount = mapUserToAccount(superheroUser); + + em.persist(school); + em.persist([adminRoles, teacherRoles, studentRoles, superheroRoles]); + em.persist([adminUser, teacherUser, studentUser, superheroUser]); + em.persist([adminAccount, teacherAccount, studentAccount, superheroAccount]); + await em.flush(); + + const loggedInClient = await testApiClient.login(studentAccount); + + const params: PatchMyPasswordParams = { + password: 'Valid12$', + confirmPassword: 'Valid12$', + }; + + return { params, loggedInClient, studentAccount }; + }; + }); it('should reject not existing account id', async () => { currentUser = mapUserToCurrentUser(superheroUser, studentAccount); await request(app.getHttpServer()) // @@ -330,4 +1007,5 @@ describe('Account Controller (API)', () => { .expect(404); }); }); + */ }); From 9e2126c495d9ce507b1856fc51b6e33f413c96de Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Tue, 19 Sep 2023 10:58:03 +0200 Subject: [PATCH 08/24] Use setup and new test structure in account api test (part 2). --- .../controller/api-test/account.api.spec.ts | 587 ++++-------------- 1 file changed, 129 insertions(+), 458 deletions(-) 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 583b6968cc5..d7826ca6d1a 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 @@ -11,6 +11,7 @@ import { cleanupCollections, } from '@shared/testing'; import { + AccountByIdBodyParams, AccountSearchQueryParams, AccountSearchType, PatchMyAccountParams, @@ -25,18 +26,6 @@ describe('Account Controller (API)', () => { let em: EntityManager; let testApiClient: TestApiClient; - // let adminAccount: Account; - // let teacherAccount: Account; - // let studentAccount: Account; - // let superheroAccount: Account; - - // let adminUser: User; - // let teacherUser: User; - // let studentUser: User; - // let superheroUser: User; - - // let currentUser: ICurrentUser; - const defaultPassword = 'DummyPasswd!1'; const defaultPasswordHash = '$2a$10$/DsztV5o6P5piW2eWJsxw.4nHovmJGBA.QNwiTmuZ/uvUc40b.Uhu'; @@ -47,43 +36,6 @@ describe('Account Controller (API)', () => { password: defaultPasswordHash, }); - /* const setup = async () => { - // TODO: revisit test structure. setup for each functional situation, return values instead of global variables - const school = schoolFactory.buildWithId(); - - const adminRoles = roleFactory.build({ - name: RoleName.ADMINISTRATOR, - permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], - }); - const teacherRoles = roleFactory.build({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] }); - const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); - const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); - - adminUser = userFactory.buildWithId({ school, roles: [adminRoles] }); - teacherUser = userFactory.buildWithId({ school, roles: [teacherRoles] }); - studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); - superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); - - const mapUserToAccount = (user: User): Account => - accountFactory.buildWithId({ - userId: user.id, - username: user.email, - password: defaultPasswordHash, - }); - adminAccount = mapUserToAccount(adminUser); - teacherAccount = mapUserToAccount(teacherUser); - studentAccount = mapUserToAccount(studentUser); - superheroAccount = mapUserToAccount(superheroUser); - - em.persist(school); - em.persist([adminRoles, teacherRoles, studentRoles, superheroRoles]); - em.persist([adminUser, teacherUser, studentUser, superheroUser]); - em.persist([adminAccount, teacherAccount, studentAccount, superheroAccount]); - await em.flush(); - - // TODO: return {adminAccount, teacherAccount, ...} - }; */ - beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ServerTestModule], @@ -100,7 +52,7 @@ describe('Account Controller (API)', () => { }); afterAll(async () => { - // await cleanupCollections(em); + await cleanupCollections(em); await app.close(); }); @@ -112,10 +64,7 @@ describe('Account Controller (API)', () => { const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); const studentAccount = mapUserToAccount(studentUser); - em.persist(school); - em.persist([studentRoles]); - em.persist([studentUser]); - em.persist([studentAccount]); + em.persist([school, studentRoles, studentUser, studentAccount]); await em.flush(); const loggedInClient = await testApiClient.login(studentAccount); @@ -145,10 +94,7 @@ describe('Account Controller (API)', () => { const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); const studentAccount = mapUserToAccount(studentUser); - em.persist(school); - em.persist([studentRoles]); - em.persist([studentUser]); - em.persist([studentAccount]); + em.persist([school, studentRoles, studentUser, studentAccount]); await em.flush(); const loggedInClient = await testApiClient.login(studentAccount); @@ -177,10 +123,7 @@ describe('Account Controller (API)', () => { const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); const studentAccount = mapUserToAccount(studentUser); - em.persist(school); - em.persist([studentRoles]); - em.persist([studentUser]); - em.persist([studentAccount]); + em.persist([school, studentRoles, studentUser, studentAccount]); await em.flush(); const loggedInClient = await testApiClient.login(studentAccount); @@ -210,10 +153,7 @@ describe('Account Controller (API)', () => { const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); const studentAccount = mapUserToAccount(studentUser); - em.persist(school); - em.persist([studentRoles]); - em.persist([studentUser]); - em.persist([studentAccount]); + em.persist([school, studentRoles, studentUser, studentAccount]); await em.flush(); const loggedInClient = await testApiClient.login(studentAccount); @@ -429,110 +369,58 @@ describe('Account Controller (API)', () => { }); }); - /* describe('[GET] :id', () => { - describe('', () => { + describe('When searching with a superhero user', () => { const setup = async () => { const school = schoolFactory.buildWithId(); - const adminRoles = roleFactory.build({ - name: RoleName.ADMINISTRATOR, - permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], - }); - const teacherRoles = roleFactory.build({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] }); const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); - const adminUser = userFactory.buildWithId({ school, roles: [adminRoles] }); - const teacherUser = userFactory.buildWithId({ school, roles: [teacherRoles] }); const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); const superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); - const mapUserToAccount = (user: User): Account => - accountFactory.buildWithId({ - userId: user.id, - username: user.email, - password: defaultPasswordHash, - }); - const adminAccount = mapUserToAccount(adminUser); - const teacherAccount = mapUserToAccount(teacherUser); const studentAccount = mapUserToAccount(studentUser); const superheroAccount = mapUserToAccount(superheroUser); em.persist(school); - em.persist([adminRoles, teacherRoles, studentRoles, superheroRoles]); - em.persist([adminUser, teacherUser, studentUser, superheroUser]); - em.persist([adminAccount, teacherAccount, studentAccount, superheroAccount]); + em.persist([studentRoles, superheroRoles]); + em.persist([studentUser, superheroUser]); + em.persist([studentAccount, superheroAccount]); await em.flush(); - const loggedInClient = await testApiClient.login(studentAccount); - - const params: PatchMyPasswordParams = { - password: 'Valid12$', - confirmPassword: 'Valid12$', - }; + const loggedInClient = await testApiClient.login(superheroAccount); - return { params, loggedInClient, studentAccount }; + return { loggedInClient, studentAccount }; }; + it('should return account for account id', async () => { + const { loggedInClient, studentAccount } = await setup(); + await loggedInClient.get(`/${studentAccount.id}`).expect(200); + }); }); - it('should return account for account id', async () => { - // currentUser = mapUserToCurrentUser(superheroUser, superheroAccount); - await request(app.getHttpServer()) // - .get(`${basePath}/${studentAccount.id}`) - .expect(200); - }); - describe('', () => { + describe('When searching with a superhero user', () => { const setup = async () => { const school = schoolFactory.buildWithId(); - const adminRoles = roleFactory.build({ - name: RoleName.ADMINISTRATOR, - permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], - }); - const teacherRoles = roleFactory.build({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] }); - const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); - - const adminUser = userFactory.buildWithId({ school, roles: [adminRoles] }); - const teacherUser = userFactory.buildWithId({ school, roles: [teacherRoles] }); - const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); const superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); - - const mapUserToAccount = (user: User): Account => - accountFactory.buildWithId({ - userId: user.id, - username: user.email, - password: defaultPasswordHash, - }); - const adminAccount = mapUserToAccount(adminUser); - const teacherAccount = mapUserToAccount(teacherUser); - const studentAccount = mapUserToAccount(studentUser); const superheroAccount = mapUserToAccount(superheroUser); - em.persist(school); - em.persist([adminRoles, teacherRoles, studentRoles, superheroRoles]); - em.persist([adminUser, teacherUser, studentUser, superheroUser]); - em.persist([adminAccount, teacherAccount, studentAccount, superheroAccount]); + em.persist([school, superheroRoles, superheroUser, superheroAccount]); await em.flush(); - const loggedInClient = await testApiClient.login(studentAccount); - - const params: PatchMyPasswordParams = { - password: 'Valid12$', - confirmPassword: 'Valid12$', - }; + const loggedInClient = await testApiClient.login(superheroAccount); - return { params, loggedInClient, studentAccount }; + return { loggedInClient }; }; + + it('should reject if id has invalid format', async () => { + const { loggedInClient } = await setup(); + await loggedInClient.get(`/qwerty`).send().expect(400); + }); }); - it('should reject if id has invalid format', async () => { - // currentUser = mapUserToCurrentUser(superheroUser, superheroAccount); - await request(app.getHttpServer()) // - .get(`${basePath}/qwerty`) - .send() - .expect(400); - }); - describe('', () => { + + describe('When searching with a not authorized user', () => { const setup = async () => { const school = schoolFactory.buildWithId(); @@ -540,373 +428,204 @@ describe('Account Controller (API)', () => { name: RoleName.ADMINISTRATOR, permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], }); - const teacherRoles = roleFactory.build({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] }); const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); - const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); const adminUser = userFactory.buildWithId({ school, roles: [adminRoles] }); - const teacherUser = userFactory.buildWithId({ school, roles: [teacherRoles] }); const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); - const superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); - const mapUserToAccount = (user: User): Account => - accountFactory.buildWithId({ - userId: user.id, - username: user.email, - password: defaultPasswordHash, - }); const adminAccount = mapUserToAccount(adminUser); - const teacherAccount = mapUserToAccount(teacherUser); const studentAccount = mapUserToAccount(studentUser); - const superheroAccount = mapUserToAccount(superheroUser); em.persist(school); - em.persist([adminRoles, teacherRoles, studentRoles, superheroRoles]); - em.persist([adminUser, teacherUser, studentUser, superheroUser]); - em.persist([adminAccount, teacherAccount, studentAccount, superheroAccount]); + em.persist([adminRoles, studentRoles]); + em.persist([adminUser, studentUser]); + em.persist([adminAccount, studentAccount]); await em.flush(); - const loggedInClient = await testApiClient.login(studentAccount); - - const params: PatchMyPasswordParams = { - password: 'Valid12$', - confirmPassword: 'Valid12$', - }; + const loggedInClient = await testApiClient.login(adminAccount); - return { params, loggedInClient, studentAccount }; + return { loggedInClient, studentAccount }; }; + it('should reject request', async () => { + const { loggedInClient, studentAccount } = await setup(); + await loggedInClient.get(`/${studentAccount.id}`).expect(403); + }); }); - it('should reject if user is not a authorized', async () => { - // currentUser = mapUserToCurrentUser(adminUser, adminAccount); - await request(app.getHttpServer()) // - .get(`${basePath}/${studentAccount.id}`) - .expect(403); - }); - describe('', () => { + + describe('When searching with a superhero user', () => { const setup = async () => { const school = schoolFactory.buildWithId(); - const adminRoles = roleFactory.build({ - name: RoleName.ADMINISTRATOR, - permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], - }); - const teacherRoles = roleFactory.build({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] }); - const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); - - const adminUser = userFactory.buildWithId({ school, roles: [adminRoles] }); - const teacherUser = userFactory.buildWithId({ school, roles: [teacherRoles] }); - const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); const superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); - - const mapUserToAccount = (user: User): Account => - accountFactory.buildWithId({ - userId: user.id, - username: user.email, - password: defaultPasswordHash, - }); - const adminAccount = mapUserToAccount(adminUser); - const teacherAccount = mapUserToAccount(teacherUser); - const studentAccount = mapUserToAccount(studentUser); const superheroAccount = mapUserToAccount(superheroUser); - em.persist(school); - em.persist([adminRoles, teacherRoles, studentRoles, superheroRoles]); - em.persist([adminUser, teacherUser, studentUser, superheroUser]); - em.persist([adminAccount, teacherAccount, studentAccount, superheroAccount]); + em.persist([school, superheroRoles, superheroUser, superheroAccount]); await em.flush(); - const loggedInClient = await testApiClient.login(studentAccount); - - const params: PatchMyPasswordParams = { - password: 'Valid12$', - confirmPassword: 'Valid12$', - }; + const loggedInClient = await testApiClient.login(superheroAccount); - return { params, loggedInClient, studentAccount }; + return { loggedInClient }; }; - }); - it('should reject not existing account id', async () => { - currentUser = mapUserToCurrentUser(superheroUser, superheroAccount); - await request(app.getHttpServer()) // - .get(`${basePath}/000000000000000000000000`) - .expect(404); + + it('should reject not existing account id', async () => { + const { loggedInClient } = await setup(); + await loggedInClient.get(`/000000000000000000000000`).expect(404); + }); }); }); describe('[PATCH] :id', () => { - describe('', () => { + describe('When using a superhero user', () => { const setup = async () => { const school = schoolFactory.buildWithId(); - const adminRoles = roleFactory.build({ - name: RoleName.ADMINISTRATOR, - permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], - }); - const teacherRoles = roleFactory.build({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] }); const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); - const adminUser = userFactory.buildWithId({ school, roles: [adminRoles] }); - const teacherUser = userFactory.buildWithId({ school, roles: [teacherRoles] }); const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); const superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); - const mapUserToAccount = (user: User): Account => - accountFactory.buildWithId({ - userId: user.id, - username: user.email, - password: defaultPasswordHash, - }); - const adminAccount = mapUserToAccount(adminUser); - const teacherAccount = mapUserToAccount(teacherUser); const studentAccount = mapUserToAccount(studentUser); const superheroAccount = mapUserToAccount(superheroUser); em.persist(school); - em.persist([adminRoles, teacherRoles, studentRoles, superheroRoles]); - em.persist([adminUser, teacherUser, studentUser, superheroUser]); - em.persist([adminAccount, teacherAccount, studentAccount, superheroAccount]); + em.persist([studentRoles, superheroRoles]); + em.persist([studentUser, superheroUser]); + em.persist([studentAccount, superheroAccount]); await em.flush(); - const loggedInClient = await testApiClient.login(studentAccount); + const loggedInClient = await testApiClient.login(superheroAccount); - const params: PatchMyPasswordParams = { - password: 'Valid12$', - confirmPassword: 'Valid12$', + const body: AccountByIdBodyParams = { + password: defaultPassword, + username: studentAccount.username, + activated: true, }; - return { params, loggedInClient, studentAccount }; + return { body, loggedInClient, studentAccount }; }; + + it('should update account', async () => { + const { body, loggedInClient, studentAccount } = await setup(); + + await loggedInClient.patch(`/${studentAccount.id}`, body).expect(200); + }); }); - it('should update account', async () => { - currentUser = mapUserToCurrentUser(superheroUser, superheroAccount); - const body: AccountByIdBodyParams = { - password: defaultPassword, - username: studentAccount.username, - activated: true, - }; - await request(app.getHttpServer()) // - .patch(`${basePath}/${studentAccount.id}`) - .send(body) - .expect(200); - }); - describe('', () => { + + describe('When the user is not authorized', () => { const setup = async () => { const school = schoolFactory.buildWithId(); - - const adminRoles = roleFactory.build({ - name: RoleName.ADMINISTRATOR, - permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], - }); - const teacherRoles = roleFactory.build({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] }); const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); - const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); - - const adminUser = userFactory.buildWithId({ school, roles: [adminRoles] }); - const teacherUser = userFactory.buildWithId({ school, roles: [teacherRoles] }); const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); - const superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); - - const mapUserToAccount = (user: User): Account => - accountFactory.buildWithId({ - userId: user.id, - username: user.email, - password: defaultPasswordHash, - }); - const adminAccount = mapUserToAccount(adminUser); - const teacherAccount = mapUserToAccount(teacherUser); const studentAccount = mapUserToAccount(studentUser); - const superheroAccount = mapUserToAccount(superheroUser); - em.persist(school); - em.persist([adminRoles, teacherRoles, studentRoles, superheroRoles]); - em.persist([adminUser, teacherUser, studentUser, superheroUser]); - em.persist([adminAccount, teacherAccount, studentAccount, superheroAccount]); + em.persist([school, studentRoles, studentUser, studentAccount]); await em.flush(); const loggedInClient = await testApiClient.login(studentAccount); - const params: PatchMyPasswordParams = { - password: 'Valid12$', - confirmPassword: 'Valid12$', + const body: AccountByIdBodyParams = { + password: defaultPassword, + username: studentAccount.username, + activated: true, }; - return { params, loggedInClient, studentAccount }; + return { body, loggedInClient, studentAccount }; }; + it('should reject update request', async () => { + const { body, loggedInClient, studentAccount } = await setup(); + + await loggedInClient.patch(`/${studentAccount.id}`, body).expect(403); + }); }); - it('should reject if user is not authorized', async () => { - currentUser = mapUserToCurrentUser(studentUser, studentAccount); - const body: AccountByIdBodyParams = { - password: defaultPassword, - username: studentAccount.username, - activated: true, - }; - await request(app.getHttpServer()) // - .patch(`${basePath}/${studentAccount.id}`) - .send(body) - .expect(403); - }); - describe('', () => { + + describe('When updating with a superhero user', () => { const setup = async () => { const school = schoolFactory.buildWithId(); - const adminRoles = roleFactory.build({ - name: RoleName.ADMINISTRATOR, - permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], - }); - const teacherRoles = roleFactory.build({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] }); const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); - const adminUser = userFactory.buildWithId({ school, roles: [adminRoles] }); - const teacherUser = userFactory.buildWithId({ school, roles: [teacherRoles] }); const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); const superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); - const mapUserToAccount = (user: User): Account => - accountFactory.buildWithId({ - userId: user.id, - username: user.email, - password: defaultPasswordHash, - }); - const adminAccount = mapUserToAccount(adminUser); - const teacherAccount = mapUserToAccount(teacherUser); const studentAccount = mapUserToAccount(studentUser); const superheroAccount = mapUserToAccount(superheroUser); em.persist(school); - em.persist([adminRoles, teacherRoles, studentRoles, superheroRoles]); - em.persist([adminUser, teacherUser, studentUser, superheroUser]); - em.persist([adminAccount, teacherAccount, studentAccount, superheroAccount]); + em.persist([studentRoles, superheroRoles]); + em.persist([studentUser, superheroUser]); + em.persist([studentAccount, superheroAccount]); await em.flush(); - const loggedInClient = await testApiClient.login(studentAccount); + const loggedInClient = await testApiClient.login(superheroAccount); - const params: PatchMyPasswordParams = { - password: 'Valid12$', - confirmPassword: 'Valid12$', + const body: AccountByIdBodyParams = { + password: defaultPassword, + username: studentAccount.username, + activated: true, }; - return { params, loggedInClient, studentAccount }; + return { body, loggedInClient }; }; - }); - it('should reject not existing account id', async () => { - currentUser = mapUserToCurrentUser(superheroUser, studentAccount); - const body: AccountByIdBodyParams = { - password: defaultPassword, - username: studentAccount.username, - activated: true, - }; - await request(app.getHttpServer()) // - .patch(`${basePath}/000000000000000000000000`) - .send(body) - .expect(404); + it('should reject not existing account id', async () => { + const { body, loggedInClient } = await setup(); + await loggedInClient.patch('/000000000000000000000000', body).expect(404); + }); }); }); describe('[DELETE] :id', () => { - describe('', () => { + describe('When using a superhero user', () => { const setup = async () => { const school = schoolFactory.buildWithId(); - const adminRoles = roleFactory.build({ - name: RoleName.ADMINISTRATOR, - permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], - }); - const teacherRoles = roleFactory.build({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] }); const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); - const adminUser = userFactory.buildWithId({ school, roles: [adminRoles] }); - const teacherUser = userFactory.buildWithId({ school, roles: [teacherRoles] }); const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); const superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); - const mapUserToAccount = (user: User): Account => - accountFactory.buildWithId({ - userId: user.id, - username: user.email, - password: defaultPasswordHash, - }); - const adminAccount = mapUserToAccount(adminUser); - const teacherAccount = mapUserToAccount(teacherUser); const studentAccount = mapUserToAccount(studentUser); const superheroAccount = mapUserToAccount(superheroUser); em.persist(school); - em.persist([adminRoles, teacherRoles, studentRoles, superheroRoles]); - em.persist([adminUser, teacherUser, studentUser, superheroUser]); - em.persist([adminAccount, teacherAccount, studentAccount, superheroAccount]); + em.persist([studentRoles, superheroRoles]); + em.persist([studentUser, superheroUser]); + em.persist([studentAccount, superheroAccount]); await em.flush(); - const loggedInClient = await testApiClient.login(studentAccount); - - const params: PatchMyPasswordParams = { - password: 'Valid12$', - confirmPassword: 'Valid12$', - }; + const loggedInClient = await testApiClient.login(superheroAccount); - return { params, loggedInClient, studentAccount }; + return { loggedInClient, studentAccount }; }; + it('should delete account', async () => { + const { loggedInClient, studentAccount } = await setup(); + await loggedInClient.delete(`/${studentAccount.id}`).expect(200); + }); }); - it('should delete account', async () => { - currentUser = mapUserToCurrentUser(superheroUser, studentAccount); - await request(app.getHttpServer()) // - .delete(`${basePath}/${studentAccount.id}`) - .expect(200); - }); - describe('', () => { + + describe('When using a superhero user', () => { const setup = async () => { const school = schoolFactory.buildWithId(); - - const adminRoles = roleFactory.build({ - name: RoleName.ADMINISTRATOR, - permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], - }); - const teacherRoles = roleFactory.build({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] }); - const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); - - const adminUser = userFactory.buildWithId({ school, roles: [adminRoles] }); - const teacherUser = userFactory.buildWithId({ school, roles: [teacherRoles] }); - const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); const superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); - - const mapUserToAccount = (user: User): Account => - accountFactory.buildWithId({ - userId: user.id, - username: user.email, - password: defaultPasswordHash, - }); - const adminAccount = mapUserToAccount(adminUser); - const teacherAccount = mapUserToAccount(teacherUser); - const studentAccount = mapUserToAccount(studentUser); const superheroAccount = mapUserToAccount(superheroUser); - em.persist(school); - em.persist([adminRoles, teacherRoles, studentRoles, superheroRoles]); - em.persist([adminUser, teacherUser, studentUser, superheroUser]); - em.persist([adminAccount, teacherAccount, studentAccount, superheroAccount]); + em.persist([school, superheroRoles, superheroUser, superheroAccount]); await em.flush(); - const loggedInClient = await testApiClient.login(studentAccount); - - const params: PatchMyPasswordParams = { - password: 'Valid12$', - confirmPassword: 'Valid12$', - }; + const loggedInClient = await testApiClient.login(superheroAccount); - return { params, loggedInClient, studentAccount }; + return { loggedInClient }; }; + it('should reject invalid account id format', async () => { + const { loggedInClient } = await setup(); + await loggedInClient.delete('/qwerty').expect(400); + }); }); - it('should reject invalid account id format', async () => { - currentUser = mapUserToCurrentUser(superheroUser, studentAccount); - await request(app.getHttpServer()) // - .delete(`${basePath}/qwerty`) - .expect(400); - }); - describe('', () => { + describe('When using a not authorized (admin) user', () => { const setup = async () => { const school = schoolFactory.buildWithId(); @@ -914,98 +633,50 @@ describe('Account Controller (API)', () => { name: RoleName.ADMINISTRATOR, permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], }); - const teacherRoles = roleFactory.build({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] }); const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); - const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); const adminUser = userFactory.buildWithId({ school, roles: [adminRoles] }); - const teacherUser = userFactory.buildWithId({ school, roles: [teacherRoles] }); const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); - const superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); - const mapUserToAccount = (user: User): Account => - accountFactory.buildWithId({ - userId: user.id, - username: user.email, - password: defaultPasswordHash, - }); const adminAccount = mapUserToAccount(adminUser); - const teacherAccount = mapUserToAccount(teacherUser); const studentAccount = mapUserToAccount(studentUser); - const superheroAccount = mapUserToAccount(superheroUser); em.persist(school); - em.persist([adminRoles, teacherRoles, studentRoles, superheroRoles]); - em.persist([adminUser, teacherUser, studentUser, superheroUser]); - em.persist([adminAccount, teacherAccount, studentAccount, superheroAccount]); + em.persist([adminRoles, studentRoles]); + em.persist([adminUser, studentUser]); + em.persist([adminAccount, studentAccount]); await em.flush(); - const loggedInClient = await testApiClient.login(studentAccount); - - const params: PatchMyPasswordParams = { - password: 'Valid12$', - confirmPassword: 'Valid12$', - }; + const loggedInClient = await testApiClient.login(adminAccount); - return { params, loggedInClient, studentAccount }; + return { loggedInClient, studentAccount }; }; + + it('should reject delete request', async () => { + const { loggedInClient, studentAccount } = await setup(); + await loggedInClient.delete(`/${studentAccount.id}`).expect(403); + }); }); - it('should reject if user is not a authorized', async () => { - currentUser = mapUserToCurrentUser(adminUser, adminAccount); - await request(app.getHttpServer()) // - .delete(`${basePath}/${studentAccount.id}`) - .expect(403); - }); + describe('', () => { const setup = async () => { const school = schoolFactory.buildWithId(); - - const adminRoles = roleFactory.build({ - name: RoleName.ADMINISTRATOR, - permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], - }); - const teacherRoles = roleFactory.build({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] }); - const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); - - const adminUser = userFactory.buildWithId({ school, roles: [adminRoles] }); - const teacherUser = userFactory.buildWithId({ school, roles: [teacherRoles] }); - const studentUser = userFactory.buildWithId({ school, roles: [studentRoles] }); const superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); - - const mapUserToAccount = (user: User): Account => - accountFactory.buildWithId({ - userId: user.id, - username: user.email, - password: defaultPasswordHash, - }); - const adminAccount = mapUserToAccount(adminUser); - const teacherAccount = mapUserToAccount(teacherUser); - const studentAccount = mapUserToAccount(studentUser); const superheroAccount = mapUserToAccount(superheroUser); - em.persist(school); - em.persist([adminRoles, teacherRoles, studentRoles, superheroRoles]); - em.persist([adminUser, teacherUser, studentUser, superheroUser]); - em.persist([adminAccount, teacherAccount, studentAccount, superheroAccount]); + em.persist([school, superheroRoles, superheroUser, superheroAccount]); await em.flush(); - const loggedInClient = await testApiClient.login(studentAccount); - - const params: PatchMyPasswordParams = { - password: 'Valid12$', - confirmPassword: 'Valid12$', - }; + const loggedInClient = await testApiClient.login(superheroAccount); - return { params, loggedInClient, studentAccount }; + return { loggedInClient }; }; - }); - it('should reject not existing account id', async () => { - currentUser = mapUserToCurrentUser(superheroUser, studentAccount); - await request(app.getHttpServer()) // - .delete(`${basePath}/000000000000000000000000`) - .expect(404); + + it('should reject not existing account id', async () => { + const { loggedInClient } = await setup(); + await loggedInClient.delete('/000000000000000000000000').expect(404); + }); }); }); - */ }); From 836b97c2f88149ce82e568cf4ed818a9b99a9efc Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Tue, 19 Sep 2023 11:00:08 +0200 Subject: [PATCH 09/24] Add missing description for test case. --- .../src/modules/account/controller/api-test/account.api.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d7826ca6d1a..bf61c3077cb 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 @@ -658,7 +658,7 @@ describe('Account Controller (API)', () => { }); }); - describe('', () => { + describe('When using a superhero user', () => { const setup = async () => { const school = schoolFactory.buildWithId(); const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); From f9ea21e844268b43f908b99dae12ebec5d7f3343 Mon Sep 17 00:00:00 2001 From: Alaitwni Date: Thu, 21 Sep 2023 16:44:06 +0200 Subject: [PATCH 10/24] update function and solve todo --- .../src/modules/account/services/account-db.service.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/server/src/modules/account/services/account-db.service.ts b/apps/server/src/modules/account/services/account-db.service.ts index 45de6126ecf..2ea02eeb3c4 100644 --- a/apps/server/src/modules/account/services/account-db.service.ts +++ b/apps/server/src/modules/account/services/account-db.service.ts @@ -34,11 +34,7 @@ export class AccountServiceDb extends AbstractAccountService { } async findByUserIdOrFail(userId: EntityId): Promise { - // TODO: use a findOneorFail in the background -> then the custom error will no longer be needed - const accountEntity = await this.accountRepo.findByUserId(userId); - if (!accountEntity) { - throw new EntityNotFoundError('Account'); - } + const accountEntity = await this.accountRepo.findByUserIdOrFail(userId); return AccountEntityToDtoMapper.mapToDto(accountEntity); } From 31da828d416a724f72eb32fa6eb094aab02c81de Mon Sep 17 00:00:00 2001 From: MajedAlaitwniCap Date: Thu, 21 Sep 2023 16:55:15 +0200 Subject: [PATCH 11/24] Use setup and new test structure in account db service test --- .../services/account-db.service.spec.ts | 991 +++++++++++------- 1 file changed, 641 insertions(+), 350 deletions(-) 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 ddea45d19ca..23f8c478a5a 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 @@ -3,9 +3,9 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { EntityNotFoundError } from '@shared/common'; -import { Account, EntityId, Permission, Role, RoleName, School, User } from '@shared/domain'; +import { Account, EntityId } from '@shared/domain'; import { IdentityManagementService } from '@shared/infra/identity-management/identity-management.service'; -import { accountFactory, schoolFactory, setupEntities, userFactory } from '@shared/testing'; +import { accountFactory, setupEntities, userFactory } from '@shared/testing'; import { AccountEntityToDtoMapper } from '@src/modules/account/mapper'; import { AccountDto } from '@src/modules/account/services/dto'; import { IServerConfig } from '@src/modules/server'; @@ -19,23 +19,11 @@ import { AbstractAccountService } from './account.service.abstract'; describe('AccountDbService', () => { let module: TestingModule; let accountService: AbstractAccountService; - let mockAccounts: Account[]; - let accountRepo: AccountRepo; + let accountRepo: DeepMocked; let accountLookupServiceMock: DeepMocked; const defaultPassword = 'DummyPasswd!1'; - let mockSchool: School; - - let mockTeacherUser: User; - let mockStudentUser: User; - let mockUserWithoutAccount: User; - - let mockTeacherAccount: Account; - let mockStudentAccount: Account; - - let mockAccountWithSystemId: Account; - afterAll(async () => { await module.close(); }); @@ -47,69 +35,7 @@ describe('AccountDbService', () => { AccountLookupService, { provide: AccountRepo, - useValue: { - save: jest.fn().mockImplementation((account: Account): Promise => { - if (account.username === 'fail@to.update') { - return Promise.reject(); - } - const accountEntity = mockAccounts.find((tempAccount) => tempAccount.userId === account.userId); - if (accountEntity) { - Object.assign(accountEntity, account); - } - - return Promise.resolve(); - }), - deleteById: jest.fn().mockImplementation((): Promise => Promise.resolve()), - findMultipleByUserId: (userIds: EntityId[]): Promise => { - const accounts = mockAccounts.filter((tempAccount) => - userIds.find((userId) => tempAccount.userId?.toString() === userId) - ); - return Promise.resolve(accounts); - }, - findByUserId: (userId: EntityId): Promise => { - const account = mockAccounts.find((tempAccount) => tempAccount.userId?.toString() === userId); - if (account) { - return Promise.resolve(account); - } - return Promise.resolve(null); - }, - findByUserIdOrFail: (userId: EntityId): Promise => { - const account = mockAccounts.find((tempAccount) => tempAccount.userId?.toString() === userId); - - if (account) { - return Promise.resolve(account); - } - throw new EntityNotFoundError(Account.name); - }, - findByUsernameAndSystemId: (username: string, systemId: EntityId | ObjectId): Promise => { - const account = mockAccounts.find( - (tempAccount) => tempAccount.username === username && tempAccount.systemId === systemId - ); - if (account) { - return Promise.resolve(account); - } - return Promise.resolve(null); - }, - - findById: jest.fn().mockImplementation((accountId: EntityId | ObjectId): Promise => { - const account = mockAccounts.find((tempAccount) => tempAccount.id === accountId.toString()); - - if (account) { - return Promise.resolve(account); - } - throw new EntityNotFoundError(Account.name); - }), - searchByUsernameExactMatch: jest - .fn() - .mockImplementation((): Promise<[Account[], number]> => Promise.resolve([[mockTeacherAccount], 1])), - searchByUsernamePartialMatch: jest - .fn() - .mockImplementation( - (): Promise<[Account[], number]> => Promise.resolve([mockAccounts, mockAccounts.length]) - ), - deleteByUserId: jest.fn().mockImplementation((): Promise => Promise.resolve()), - findMany: jest.fn().mockImplementation((): Promise => Promise.resolve(mockAccounts)), - }, + useValue: createMock(), }, { provide: LegacyLogger, @@ -125,14 +51,7 @@ describe('AccountDbService', () => { }, { provide: AccountLookupService, - useValue: createMock({ - getInternalId: (id: EntityId | ObjectId): Promise => { - if (ObjectId.isValid(id)) { - return Promise.resolve(new ObjectId(id)); - } - return Promise.resolve(null); - }, - }), + useValue: createMock(), }, ], }).compile(); @@ -143,28 +62,9 @@ describe('AccountDbService', () => { }); beforeEach(() => { + jest.resetAllMocks(); jest.useFakeTimers(); jest.setSystemTime(new Date(2020, 1, 1)); - - mockSchool = schoolFactory.buildWithId(); - - mockTeacherUser = userFactory.buildWithId({ - school: mockSchool, - roles: [new Role({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] })], - }); - mockStudentUser = userFactory.buildWithId({ - school: mockSchool, - roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], - }); - mockUserWithoutAccount = userFactory.buildWithId({ - school: mockSchool, - roles: [new Role({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] })], - }); - mockTeacherAccount = accountFactory.buildWithId({ userId: mockTeacherUser.id, password: defaultPassword }); - mockStudentAccount = accountFactory.buildWithId({ userId: mockStudentUser.id, password: defaultPassword }); - - mockAccountWithSystemId = accountFactory.withSystemId(new ObjectId()).build(); - mockAccounts = [mockTeacherAccount, mockStudentAccount, mockAccountWithSystemId]; }); afterEach(() => { @@ -173,294 +73,615 @@ describe('AccountDbService', () => { }); describe('findById', () => { - it( - 'should return accountDto', - async () => { - const resultAccount = await accountService.findById(mockTeacherAccount.id); - expect(resultAccount).toEqual(AccountEntityToDtoMapper.mapToDto(mockTeacherAccount)); - }, - 10 * 60 * 1000 - ); + describe('when searching by Id', () => { + const setup = () => { + const mockTeacherAccount = accountFactory.buildWithId(); + const mockTeacherAccountDto = AccountEntityToDtoMapper.mapToDto(mockTeacherAccount); + + mockTeacherAccountDto.username = 'changedUsername@example.org'; + mockTeacherAccountDto.activated = false; + + accountRepo.findById.mockResolvedValue(mockTeacherAccount); + + return { mockTeacherAccount }; + }; + it( + 'should return accountDto', + async () => { + const { mockTeacherAccount } = setup(); + + const resultAccount = await accountService.findById(mockTeacherAccount.id); + expect(resultAccount).toEqual(AccountEntityToDtoMapper.mapToDto(mockTeacherAccount)); + }, + 10 * 60 * 1000 + ); + }); }); describe('findByUserId', () => { - it('should return accountDto', async () => { - const resultAccount = await accountService.findByUserId(mockTeacherUser.id); - expect(resultAccount).toEqual(AccountEntityToDtoMapper.mapToDto(mockTeacherAccount)); + describe('when user id exists', () => { + const setup = () => { + const mockTeacherUser = userFactory.buildWithId(); + + const mockTeacherAccount = accountFactory.buildWithId(); + + accountRepo.findByUserId.mockImplementation((userId: EntityId | ObjectId): Promise => { + if (userId === mockTeacherUser.id) { + return Promise.resolve(mockTeacherAccount); + } + return Promise.resolve(null); + }); + + return { mockTeacherUser, mockTeacherAccount }; + }; + it('should return accountDto', async () => { + const { mockTeacherUser, mockTeacherAccount } = setup(); + const resultAccount = await accountService.findByUserId(mockTeacherUser.id); + expect(resultAccount).toEqual(AccountEntityToDtoMapper.mapToDto(mockTeacherAccount)); + }); }); - it('should return null', async () => { - const resultAccount = await accountService.findByUserId('nonExistentId'); - expect(resultAccount).toBeNull(); + + describe('when user id not exists', () => { + it('should return null', async () => { + const resultAccount = await accountService.findByUserId('nonExistentId'); + expect(resultAccount).toBeNull(); + }); }); }); describe('findByUsernameAndSystemId', () => { - it('should return accountDto', async () => { - const resultAccount = await accountService.findByUsernameAndSystemId( - mockAccountWithSystemId.username, - mockAccountWithSystemId.systemId ?? '' - ); - expect(resultAccount).not.toBe(undefined); + describe('when user name and system id exists', () => { + const setup = () => { + const mockAccountWithSystemId = accountFactory.withSystemId(new ObjectId()).build(); + accountRepo.findByUsernameAndSystemId.mockResolvedValue(mockAccountWithSystemId); + return { mockAccountWithSystemId }; + }; + it('should return accountDto', async () => { + const { mockAccountWithSystemId } = setup(); + const resultAccount = await accountService.findByUsernameAndSystemId( + mockAccountWithSystemId.username, + mockAccountWithSystemId.systemId ?? '' + ); + expect(resultAccount).not.toBe(undefined); + }); }); - it('should return null if username does not exist', async () => { - const resultAccount = await accountService.findByUsernameAndSystemId( - 'nonExistentUsername', - mockAccountWithSystemId.systemId ?? '' - ); - expect(resultAccount).toBeNull(); + + describe('when only system id exists', () => { + const setup = () => { + const mockAccountWithSystemId = accountFactory.withSystemId(new ObjectId()).build(); + accountRepo.findByUsernameAndSystemId.mockImplementation( + (username: string, systemId: EntityId | ObjectId): Promise => { + if (mockAccountWithSystemId.username === username && mockAccountWithSystemId.systemId === systemId) { + return Promise.resolve(mockAccountWithSystemId); + } + return Promise.resolve(null); + } + ); + return { mockAccountWithSystemId }; + }; + it('should return null if username does not exist', async () => { + const { mockAccountWithSystemId } = setup(); + const resultAccount = await accountService.findByUsernameAndSystemId( + 'nonExistentUsername', + mockAccountWithSystemId.systemId ?? '' + ); + expect(resultAccount).toBeNull(); + }); }); - it('should return null if system id does not exist', async () => { - const resultAccount = await accountService.findByUsernameAndSystemId( - mockAccountWithSystemId.username, - 'nonExistentSystemId' ?? '' - ); - expect(resultAccount).toBeNull(); + + describe('when only user name exists', () => { + const setup = () => { + const mockAccountWithSystemId = accountFactory.withSystemId(new ObjectId()).build(); + accountRepo.findByUsernameAndSystemId.mockImplementation( + (username: string, systemId: EntityId | ObjectId): Promise => { + if (mockAccountWithSystemId.username === username && mockAccountWithSystemId.systemId === systemId) { + return Promise.resolve(mockAccountWithSystemId); + } + return Promise.resolve(null); + } + ); + return { mockAccountWithSystemId }; + }; + it('should return null if system id does not exist', async () => { + const { mockAccountWithSystemId } = setup(); + const resultAccount = await accountService.findByUsernameAndSystemId( + mockAccountWithSystemId.username, + 'nonExistentSystemId' ?? '' + ); + expect(resultAccount).toBeNull(); + }); }); }); describe('findMultipleByUserId', () => { - it('should return multiple accountDtos', async () => { - const resultAccounts = await accountService.findMultipleByUserId([mockTeacherUser.id, mockStudentUser.id]); - expect(resultAccounts).toContainEqual(AccountEntityToDtoMapper.mapToDto(mockTeacherAccount)); - expect(resultAccounts).toContainEqual(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); - expect(resultAccounts).toHaveLength(2); + describe('when search existing multiple Ids', () => { + const setup = () => { + const mockTeacherUser = userFactory.buildWithId(); + const mockStudentUser = userFactory.buildWithId(); + + const mockTeacherAccount = accountFactory.buildWithId({ + userId: mockTeacherUser.id, + password: defaultPassword, + }); + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPassword, + }); + + accountRepo.findMultipleByUserId.mockImplementation((userIds: (EntityId | ObjectId)[]): Promise => { + const accounts = [mockStudentAccount, mockTeacherAccount].filter((tempAccount) => + userIds.find((userId) => tempAccount.userId?.toString() === userId) + ); + return Promise.resolve(accounts); + }); + return { mockStudentUser, mockStudentAccount, mockTeacherUser, mockTeacherAccount }; + }; + it('should return multiple accountDtos', async () => { + const { mockStudentUser, mockStudentAccount, mockTeacherUser, mockTeacherAccount } = setup(); + const resultAccounts = await accountService.findMultipleByUserId([mockTeacherUser.id, mockStudentUser.id]); + expect(resultAccounts).toContainEqual(AccountEntityToDtoMapper.mapToDto(mockTeacherAccount)); + expect(resultAccounts).toContainEqual(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + expect(resultAccounts).toHaveLength(2); + }); }); - it('should return empty array on mismatch', async () => { - const resultAccount = await accountService.findMultipleByUserId(['nonExistentId1']); - expect(resultAccount).toHaveLength(0); + + describe('when only user name exists', () => { + const setup = () => { + const mockTeacherAccount = accountFactory.buildWithId(); + const mockStudentAccount = accountFactory.buildWithId(); + + accountRepo.findMultipleByUserId.mockImplementation((userIds: (EntityId | ObjectId)[]): Promise => { + const accounts = [mockStudentAccount, mockTeacherAccount].filter((tempAccount) => + userIds.find((userId) => tempAccount.userId?.toString() === userId) + ); + return Promise.resolve(accounts); + }); + return {}; + }; + it('should return empty array on mismatch', async () => { + setup(); + const resultAccount = await accountService.findMultipleByUserId(['nonExistentId1']); + expect(resultAccount).toHaveLength(0); + }); }); }); describe('findByUserIdOrFail', () => { - it('should return accountDto', async () => { - const resultAccount = await accountService.findByUserIdOrFail(mockTeacherUser.id); - expect(resultAccount).toEqual(AccountEntityToDtoMapper.mapToDto(mockTeacherAccount)); + describe('when user exists', () => { + const setup = () => { + const mockTeacherUser = userFactory.buildWithId(); + const mockTeacherAccount = accountFactory.buildWithId({ + userId: mockTeacherUser.id, + password: defaultPassword, + }); + + accountRepo.findByUserIdOrFail.mockResolvedValue(mockTeacherAccount); + + return { mockTeacherUser, mockTeacherAccount }; + }; + + it('should return accountDto', async () => { + const { mockTeacherUser, mockTeacherAccount } = setup(); + const resultAccount = await accountService.findByUserIdOrFail(mockTeacherUser.id); + expect(resultAccount).toEqual(AccountEntityToDtoMapper.mapToDto(mockTeacherAccount)); + }); }); - it('should throw EntityNotFoundError', async () => { - await expect(accountService.findByUserIdOrFail('nonExistentId')).rejects.toThrow(EntityNotFoundError); + + describe('when user not exists', () => { + const setup = () => { + const mockTeacherUser = userFactory.buildWithId(); + const mockTeacherAccount = accountFactory.buildWithId({ + userId: mockTeacherUser.id, + password: defaultPassword, + }); + accountRepo.findByUserIdOrFail.mockImplementation((userId: EntityId | ObjectId): Promise => { + if (mockTeacherUser.id === userId) { + return Promise.resolve(mockTeacherAccount); + } + throw new EntityNotFoundError(Account.name); + }); + return {}; + }; + it('should throw EntityNotFoundError', async () => { + setup(); + await expect(accountService.findByUserIdOrFail('nonExistentId')).rejects.toThrow(EntityNotFoundError); + }); }); }); describe('save', () => { - it('should update an existing account', async () => { - const mockTeacherAccountDto = AccountEntityToDtoMapper.mapToDto(mockTeacherAccount); - mockTeacherAccountDto.username = 'changedUsername@example.org'; - mockTeacherAccountDto.activated = false; - const ret = await accountService.save(mockTeacherAccountDto); - - expect(ret).toBeDefined(); - expect(ret).toMatchObject({ - id: mockTeacherAccount.id, - username: mockTeacherAccountDto.username, - activated: mockTeacherAccountDto.activated, - systemId: mockTeacherAccount.systemId, - userId: mockTeacherAccount.userId, + describe('when update an existing account', () => { + const setup = () => { + const mockTeacherAccount = accountFactory.buildWithId(); + const mockTeacherAccountDto = AccountEntityToDtoMapper.mapToDto(mockTeacherAccount); + + mockTeacherAccountDto.username = 'changedUsername@example.org'; + mockTeacherAccountDto.activated = false; + accountRepo.findById.mockResolvedValue(mockTeacherAccount); + accountLookupServiceMock.getInternalId.mockResolvedValue(mockTeacherAccount._id); + accountRepo.save.mockResolvedValue(); + + return { mockTeacherAccountDto, mockTeacherAccount }; + }; + + it('should update', async () => { + const { mockTeacherAccountDto, mockTeacherAccount } = setup(); + const ret = await accountService.save(mockTeacherAccountDto); + + expect(accountRepo.save).toBeCalledTimes(1); + expect(ret).toBeDefined(); + expect(ret).toMatchObject({ + id: mockTeacherAccount.id, + username: mockTeacherAccountDto.username, + activated: mockTeacherAccountDto.activated, + systemId: mockTeacherAccount.systemId, + userId: mockTeacherAccount.userId, + }); }); }); - it("should update an existing account's system", async () => { - const mockTeacherAccountDto = AccountEntityToDtoMapper.mapToDto(mockTeacherAccount); - mockTeacherAccountDto.username = 'changedUsername@example.org'; - mockTeacherAccountDto.systemId = '123456789012'; - const ret = await accountService.save(mockTeacherAccountDto); - expect(ret).toBeDefined(); - expect(ret).toMatchObject({ - id: mockTeacherAccount.id, - username: mockTeacherAccountDto.username, - activated: mockTeacherAccount.activated, - systemId: new ObjectId(mockTeacherAccountDto.systemId), - userId: mockTeacherAccount.userId, + describe("when update an existing account's system", () => { + const setup = () => { + const mockTeacherAccount = accountFactory.buildWithId(); + const mockTeacherAccountDto = AccountEntityToDtoMapper.mapToDto(mockTeacherAccount); + + mockTeacherAccountDto.username = 'changedUsername@example.org'; + mockTeacherAccountDto.systemId = '123456789012'; + accountRepo.findById.mockResolvedValue(mockTeacherAccount); + accountLookupServiceMock.getInternalId.mockResolvedValue(mockTeacherAccount._id); + accountRepo.save.mockResolvedValue(); + + return { mockTeacherAccountDto, mockTeacherAccount }; + }; + it("should update an existing account's system", async () => { + const { mockTeacherAccountDto, mockTeacherAccount } = setup(); + + const ret = await accountService.save(mockTeacherAccountDto); + expect(ret).toBeDefined(); + expect(ret).toMatchObject({ + id: mockTeacherAccount.id, + username: mockTeacherAccountDto.username, + activated: mockTeacherAccount.activated, + systemId: new ObjectId(mockTeacherAccountDto.systemId), + userId: mockTeacherAccount.userId, + }); }); }); - it("should update an existing account's user", async () => { - const mockTeacherAccountDto = AccountEntityToDtoMapper.mapToDto(mockTeacherAccount); - mockTeacherAccountDto.username = 'changedUsername@example.org'; - mockTeacherAccountDto.userId = mockStudentUser.id; - const ret = await accountService.save(mockTeacherAccountDto); - expect(ret).toBeDefined(); - expect(ret).toMatchObject({ - id: mockTeacherAccount.id, - username: mockTeacherAccountDto.username, - activated: mockTeacherAccount.activated, - systemId: mockTeacherAccount.systemId, - userId: new ObjectId(mockStudentUser.id), + + describe("when update an existing account's user", () => { + const setup = () => { + const mockTeacherAccount = accountFactory.buildWithId(); + const mockStudentUser = accountFactory.buildWithId(); + const mockTeacherAccountDto = AccountEntityToDtoMapper.mapToDto(mockTeacherAccount); + + mockTeacherAccountDto.username = 'changedUsername@example.org'; + mockTeacherAccountDto.userId = mockStudentUser.id; + accountRepo.findById.mockResolvedValue(mockTeacherAccount); + accountLookupServiceMock.getInternalId.mockResolvedValue(mockTeacherAccount._id); + accountRepo.save.mockResolvedValue(); + + return { mockStudentUser, mockTeacherAccountDto, mockTeacherAccount }; + }; + it('should update', async () => { + const { mockStudentUser, mockTeacherAccountDto, mockTeacherAccount } = setup(); + + const ret = await accountService.save(mockTeacherAccountDto); + expect(ret).toBeDefined(); + expect(ret).toMatchObject({ + id: mockTeacherAccount.id, + username: mockTeacherAccountDto.username, + activated: mockTeacherAccount.activated, + systemId: mockTeacherAccount.systemId, + userId: new ObjectId(mockStudentUser.id), + }); }); }); - it("should keep existing account's system undefined on update", async () => { - const mockTeacherAccountDto = AccountEntityToDtoMapper.mapToDto(mockTeacherAccount); - mockTeacherAccountDto.username = 'changedUsername@example.org'; - mockTeacherAccountDto.systemId = undefined; - const ret = await accountService.save(mockTeacherAccountDto); - expect(ret).toBeDefined(); - expect(ret).toMatchObject({ - id: mockTeacherAccount.id, - username: mockTeacherAccountDto.username, - activated: mockTeacherAccount.activated, - systemId: mockTeacherAccountDto.systemId, - userId: mockTeacherAccount.userId, + describe("when existing account's system is undefined", () => { + const setup = () => { + const mockTeacherAccount = accountFactory.buildWithId(); + const mockTeacherAccountDto = AccountEntityToDtoMapper.mapToDto(mockTeacherAccount); + + mockTeacherAccountDto.username = 'changedUsername@example.org'; + mockTeacherAccountDto.systemId = undefined; + + accountRepo.findById.mockResolvedValue(mockTeacherAccount); + accountLookupServiceMock.getInternalId.mockResolvedValue(mockTeacherAccount._id); + accountRepo.save.mockResolvedValue(); + + return { mockTeacherAccountDto, mockTeacherAccount }; + }; + it('should keep undefined on update', async () => { + const { mockTeacherAccountDto, mockTeacherAccount } = setup(); + + const ret = await accountService.save(mockTeacherAccountDto); + expect(ret).toBeDefined(); + expect(ret).toMatchObject({ + id: mockTeacherAccount.id, + username: mockTeacherAccountDto.username, + activated: mockTeacherAccount.activated, + systemId: mockTeacherAccountDto.systemId, + userId: mockTeacherAccount.userId, + }); + }); + }); + + describe('when account not exists', () => { + const setup = () => { + const mockUserWithoutAccount = userFactory.buildWithId(); + + const accountToSave: AccountDto = { + createdAt: new Date(), + updatedAt: new Date(), + username: 'asdf@asdf.de', + userId: mockUserWithoutAccount.id, + systemId: '012345678912', + password: defaultPassword, + } as AccountDto; + (accountRepo.findById as jest.Mock).mockClear(); + (accountRepo.save as jest.Mock).mockClear(); + + return { accountToSave }; + }; + it('should save a new account', async () => { + const { accountToSave } = setup(); + + const ret = await accountService.save(accountToSave); + expect(ret).toBeDefined(); + expect(ret).toMatchObject({ + username: accountToSave.username, + userId: new ObjectId(accountToSave.userId), + systemId: new ObjectId(accountToSave.systemId), + createdAt: accountToSave.createdAt, + updatedAt: accountToSave.updatedAt, + }); }); }); - it('should save a new account', async () => { - const accountToSave: AccountDto = { - createdAt: new Date(), - updatedAt: new Date(), - username: 'asdf@asdf.de', - userId: mockUserWithoutAccount.id, - systemId: '012345678912', - password: defaultPassword, - } as AccountDto; - (accountRepo.findById as jest.Mock).mockClear(); - (accountRepo.save as jest.Mock).mockClear(); - const ret = await accountService.save(accountToSave); - expect(ret).toBeDefined(); - expect(ret).toMatchObject({ - username: accountToSave.username, - userId: new ObjectId(accountToSave.userId), - systemId: new ObjectId(accountToSave.systemId), - createdAt: accountToSave.createdAt, - updatedAt: accountToSave.updatedAt, + + describe("when account's system undefined", () => { + const setup = () => { + const mockUserWithoutAccount = userFactory.buildWithId(); + + const accountToSave: AccountDto = { + createdAt: new Date(), + updatedAt: new Date(), + username: 'asdf@asdf.de', + userId: mockUserWithoutAccount.id, + password: defaultPassword, + } as AccountDto; + (accountRepo.findById as jest.Mock).mockClear(); + (accountRepo.save as jest.Mock).mockClear(); + + return { accountToSave }; + }; + it('should keep undefined on save', async () => { + const { accountToSave } = setup(); + + const ret = await accountService.save(accountToSave); + expect(ret).toBeDefined(); + expect(ret).toMatchObject({ + systemId: undefined, + }); }); }); - it("should keep account's system undefined on save", async () => { - const accountToSave: AccountDto = { - createdAt: new Date(), - updatedAt: new Date(), - username: 'asdf@asdf.de', - userId: mockUserWithoutAccount.id, - password: defaultPassword, - } as AccountDto; - (accountRepo.findById as jest.Mock).mockClear(); - (accountRepo.save as jest.Mock).mockClear(); - const ret = await accountService.save(accountToSave); - expect(ret).toBeDefined(); - expect(ret).toMatchObject({ - systemId: undefined, + describe('when save account', () => { + const setup = () => { + const mockUserWithoutAccount = userFactory.buildWithId(); + + const accountToSave = { + createdAt: new Date(), + updatedAt: new Date(), + username: 'asdf@asdf.de', + userId: mockUserWithoutAccount.id, + systemId: '012345678912', + password: defaultPassword, + } as AccountDto; + (accountRepo.findById as jest.Mock).mockClear(); + (accountRepo.save as jest.Mock).mockClear(); + + return { accountToSave }; + }; + it('should encrypt password', async () => { + const { accountToSave } = setup(); + + await accountService.save(accountToSave); + const ret = await accountService.save(accountToSave); + expect(ret).toBeDefined(); + expect(ret).not.toMatchObject({ + password: defaultPassword, + }); }); }); - it('should encrypt password', async () => { - const accountToSave = { - createdAt: new Date(), - updatedAt: new Date(), - username: 'asdf@asdf.de', - userId: mockUserWithoutAccount.id, - systemId: '012345678912', - password: defaultPassword, - } as AccountDto; - (accountRepo.findById as jest.Mock).mockClear(); - (accountRepo.save as jest.Mock).mockClear(); - await accountService.save(accountToSave); - const ret = await accountService.save(accountToSave); - expect(ret).toBeDefined(); - expect(ret).not.toMatchObject({ - password: defaultPassword, + describe('when creating a new account', () => { + const setup = () => { + const spy = jest.spyOn(accountRepo, 'save'); + const dto = { + username: 'john.doe@domain.tld', + password: '', + } as AccountDto; + (accountRepo.findById as jest.Mock).mockClear(); + (accountRepo.save as jest.Mock).mockClear(); + + return { spy, dto }; + }; + it('should set password to undefined if password is empty', async () => { + const { spy, dto } = setup(); + + await expect(accountService.save(dto)).resolves.not.toThrow(); + expect(accountRepo.findById).not.toHaveBeenCalled(); + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + password: undefined, + }) + ); }); }); - it('should set password to undefined if password is empty while creating a new account', async () => { - const spy = jest.spyOn(accountRepo, 'save'); - const dto = { - username: 'john.doe@domain.tld', - password: '', - } as AccountDto; - (accountRepo.findById as jest.Mock).mockClear(); - (accountRepo.save as jest.Mock).mockClear(); - await expect(accountService.save(dto)).resolves.not.toThrow(); - expect(accountRepo.findById).not.toHaveBeenCalled(); - expect(spy).toHaveBeenCalledWith( - expect.objectContaining({ + describe('when password is empty while editing an existing account', () => { + const setup = () => { + const mockTeacherAccount = accountFactory.buildWithId(); + + const spy = jest.spyOn(accountRepo, 'save'); + const dto = { + id: mockTeacherAccount.id, password: undefined, - }) - ); - }); + } as AccountDto; - it('should not change password if password is empty while editing an existing account', async () => { - const spy = jest.spyOn(accountRepo, 'save'); - const dto = { - id: mockTeacherAccount.id, - // username: 'john.doe@domain.tld', - password: undefined, - } as AccountDto; - (accountRepo.findById as jest.Mock).mockClear(); - (accountRepo.save as jest.Mock).mockClear(); - await expect(accountService.save(dto)).resolves.not.toThrow(); - expect(accountRepo.findById).toHaveBeenCalled(); - expect(spy).toHaveBeenCalledWith( - expect.objectContaining({ - password: defaultPassword, - }) - ); + accountRepo.findById.mockResolvedValue(mockTeacherAccount); + accountLookupServiceMock.getInternalId.mockResolvedValue(mockTeacherAccount._id); + accountRepo.save.mockResolvedValue(); + + return { mockTeacherAccount, spy, dto }; + }; + it('should not change password', async () => { + const { mockTeacherAccount, spy, dto } = setup(); + await expect(accountService.save(dto)).resolves.not.toThrow(); + expect(accountRepo.findById).toHaveBeenCalled(); + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + password: mockTeacherAccount.password, + }) + ); + }); }); }); describe('updateUsername', () => { - it('should update an existing account but no other information', async () => { - const mockTeacherAccountDto = AccountEntityToDtoMapper.mapToDto(mockTeacherAccount); - const newUsername = 'newUsername'; - const ret = await accountService.updateUsername(mockTeacherAccount.id, newUsername); - - expect(ret).toBeDefined(); - expect(ret).toMatchObject({ - ...mockTeacherAccountDto, - username: newUsername, + describe('when updating username', () => { + const setup = () => { + const mockTeacherAccount = accountFactory.buildWithId(); + const mockTeacherAccountDto = AccountEntityToDtoMapper.mapToDto(mockTeacherAccount); + const newUsername = 'newUsername'; + + accountRepo.findById.mockResolvedValue(mockTeacherAccount); + accountLookupServiceMock.getInternalId.mockResolvedValue(mockTeacherAccount._id); + + return { mockTeacherAccount, mockTeacherAccountDto, newUsername }; + }; + it('should update only user name', async () => { + const { mockTeacherAccount, mockTeacherAccountDto, newUsername } = setup(); + const ret = await accountService.updateUsername(mockTeacherAccount.id, newUsername); + expect(ret).toBeDefined(); + expect(ret).toMatchObject({ + ...mockTeacherAccountDto, + username: newUsername, + }); }); }); }); describe('updateLastTriedFailedLogin', () => { - it('should update last tried failed login', async () => { - const mockTeacherAccountDto = AccountEntityToDtoMapper.mapToDto(mockTeacherAccount); - const theNewDate = new Date(); - const ret = await accountService.updateLastTriedFailedLogin(mockTeacherAccount.id, theNewDate); - - expect(ret).toBeDefined(); - expect(ret).toMatchObject({ - ...mockTeacherAccountDto, - lasttriedFailedLogin: theNewDate, + describe('when update last failed Login', () => { + const setup = () => { + const mockTeacherAccount = accountFactory.buildWithId(); + const mockTeacherAccountDto = AccountEntityToDtoMapper.mapToDto(mockTeacherAccount); + const theNewDate = new Date(); + + accountRepo.findById.mockResolvedValue(mockTeacherAccount); + accountLookupServiceMock.getInternalId.mockResolvedValue(mockTeacherAccount._id); + + return { mockTeacherAccount, mockTeacherAccountDto, theNewDate }; + }; + it('should update last tried failed login', async () => { + const { mockTeacherAccount, mockTeacherAccountDto, theNewDate } = setup(); + const ret = await accountService.updateLastTriedFailedLogin(mockTeacherAccount.id, theNewDate); + + expect(ret).toBeDefined(); + expect(ret).toMatchObject({ + ...mockTeacherAccountDto, + lasttriedFailedLogin: theNewDate, + }); }); }); }); describe('validatePassword', () => { - it('should validate password', async () => { - const ret = await accountService.validatePassword( - { password: await bcrypt.hash(defaultPassword, 10) } as unknown as AccountDto, - defaultPassword - ); - expect(ret).toBe(true); + describe('when accepted Password', () => { + const setup = async () => { + const ret = await accountService.validatePassword( + { password: await bcrypt.hash(defaultPassword, 10) } as unknown as AccountDto, + defaultPassword + ); + + return { ret }; + }; + it('should validate password', async () => { + const { ret } = await setup(); + + expect(ret).toBe(true); + }); }); - it('should report wrong password', async () => { - const ret = await accountService.validatePassword( - { password: await bcrypt.hash(defaultPassword, 10) } as unknown as AccountDto, - 'incorrectPwd' - ); - expect(ret).toBe(false); + + describe('when wrong Password', () => { + const setup = async () => { + const ret = await accountService.validatePassword( + { password: await bcrypt.hash(defaultPassword, 10) } as unknown as AccountDto, + 'incorrectPwd' + ); + + return { ret }; + }; + it('should report', async () => { + const { ret } = await setup(); + + expect(ret).toBe(false); + }); }); - it('should report missing account password', async () => { - const ret = await accountService.validatePassword({ password: undefined } as AccountDto, 'incorrectPwd'); - expect(ret).toBe(false); + + describe('when missing account password', () => { + const setup = async () => { + const ret = await accountService.validatePassword({ password: undefined } as AccountDto, 'incorrectPwd'); + + return { ret }; + }; + it('should report', async () => { + const { ret } = await setup(); + + expect(ret).toBe(false); + }); }); }); describe('updatePassword', () => { - it('should update password', async () => { - const newPassword = 'newPassword'; - const ret = await accountService.updatePassword(mockTeacherAccount.id, newPassword); + describe('when update Password', () => { + const setup = () => { + const mockTeacherAccount = accountFactory.buildWithId(); + const newPassword = 'newPassword'; + + accountRepo.findById.mockResolvedValue(mockTeacherAccount); + accountLookupServiceMock.getInternalId.mockResolvedValue(mockTeacherAccount._id); + + return { mockTeacherAccount, newPassword }; + }; + it('should update password', async () => { + const { mockTeacherAccount, newPassword } = setup(); - expect(ret).toBeDefined(); - if (ret.password) { - await expect(bcrypt.compare(newPassword, ret.password)).resolves.toBe(true); - } else { - fail('return password is undefined'); - } + const ret = await accountService.updatePassword(mockTeacherAccount.id, newPassword); + + expect(ret).toBeDefined(); + if (ret.password) { + await expect(bcrypt.compare(newPassword, ret.password)).resolves.toBe(true); + } else { + fail('return password is undefined'); + } + }); }); }); describe('delete', () => { describe('when deleting existing account', () => { + const setup = () => { + const mockTeacherAccount = accountFactory.buildWithId(); + + accountRepo.findById.mockResolvedValue(mockTeacherAccount); + accountLookupServiceMock.getInternalId.mockResolvedValue(mockTeacherAccount._id); + + return { mockTeacherAccount }; + }; it('should delete account via repo', async () => { + const { mockTeacherAccount } = setup(); await accountService.delete(mockTeacherAccount.id); expect(accountRepo.deleteById).toHaveBeenCalledWith(new ObjectId(mockTeacherAccount.id)); }); @@ -468,55 +689,125 @@ describe('AccountDbService', () => { describe('when deleting non existing account', () => { const setup = () => { + const mockTeacherAccount = accountFactory.buildWithId(); + + accountRepo.findById.mockResolvedValue(mockTeacherAccount); accountLookupServiceMock.getInternalId.mockResolvedValueOnce(null); + + return { mockTeacherAccount }; }; it('should throw', async () => { - setup(); + const { mockTeacherAccount } = setup(); await expect(accountService.delete(mockTeacherAccount.id)).rejects.toThrow(); }); }); }); describe('deleteByUserId', () => { - it('should delete the account with given user id via repo', async () => { - await accountService.deleteByUserId(mockTeacherAccount.userId?.toString() ?? ''); - expect(accountRepo.deleteByUserId).toHaveBeenCalledWith(mockTeacherAccount.userId); + describe('when delete account with given user id', () => { + const setup = () => { + const mockTeacherUser = userFactory.buildWithId(); + + const mockTeacherAccount = accountFactory.buildWithId({ + userId: mockTeacherUser.id, + password: defaultPassword, + }); + + accountRepo.findById.mockResolvedValue(mockTeacherAccount); + accountLookupServiceMock.getInternalId.mockResolvedValue(mockTeacherAccount._id); + + return { mockTeacherUser, mockTeacherAccount }; + }; + it('should delete via repo', async () => { + const { mockTeacherUser, mockTeacherAccount } = setup(); + + await accountService.deleteByUserId(mockTeacherAccount.userId?.toString() ?? ''); + expect(accountRepo.deleteByUserId).toHaveBeenCalledWith(mockTeacherUser.id); + }); }); }); describe('searchByUsernamePartialMatch', () => { - it('should call repo', async () => { - const partialUserName = 'admin'; - const skip = 2; - const limit = 10; - const [accounts, total] = await accountService.searchByUsernamePartialMatch(partialUserName, skip, limit); - expect(accountRepo.searchByUsernamePartialMatch).toHaveBeenCalledWith(partialUserName, skip, limit); - expect(total).toBe(mockAccounts.length); + describe('when searching by part of username', () => { + const setup = () => { + const partialUserName = 'admin'; + const skip = 2; + const limit = 10; + const mockTeacherAccount = accountFactory.buildWithId(); + const mockStudentAccount = accountFactory.buildWithId(); + const mockAccountWithSystemId = accountFactory.withSystemId(new ObjectId()).build(); + const mockAccounts = [mockTeacherAccount, mockStudentAccount, mockAccountWithSystemId]; + + accountRepo.findById.mockResolvedValue(mockTeacherAccount); + accountRepo.searchByUsernamePartialMatch.mockResolvedValue([ + [mockTeacherAccount, mockStudentAccount, mockAccountWithSystemId], + 3, + ]); + accountLookupServiceMock.getInternalId.mockResolvedValue(mockTeacherAccount._id); + + return { partialUserName, skip, limit, mockTeacherAccount, mockAccounts }; + }; + it('should call repo', async () => { + const { partialUserName, skip, limit, mockTeacherAccount, mockAccounts } = setup(); + const [accounts, total] = await accountService.searchByUsernamePartialMatch(partialUserName, skip, limit); + expect(accountRepo.searchByUsernamePartialMatch).toHaveBeenCalledWith(partialUserName, skip, limit); + expect(total).toBe(mockAccounts.length); - expect(accounts[0]).toEqual(AccountEntityToDtoMapper.mapToDto(mockTeacherAccount)); + expect(accounts[0]).toEqual(AccountEntityToDtoMapper.mapToDto(mockTeacherAccount)); + }); }); }); + describe('searchByUsernameExactMatch', () => { - it('should call repo', async () => { - const partialUserName = 'admin'; - const [accounts, total] = await accountService.searchByUsernameExactMatch(partialUserName); - expect(accountRepo.searchByUsernameExactMatch).toHaveBeenCalledWith(partialUserName); - expect(total).toBe(1); - expect(accounts[0]).toEqual(AccountEntityToDtoMapper.mapToDto(mockTeacherAccount)); + describe('when searching by username', () => { + const setup = () => { + const partialUserName = 'admin'; + const mockTeacherAccount = accountFactory.buildWithId(); + + accountRepo.searchByUsernameExactMatch.mockResolvedValue([[mockTeacherAccount], 1]); + + return { partialUserName, mockTeacherAccount }; + }; + it('should call repo', async () => { + const { partialUserName, mockTeacherAccount } = setup(); + const [accounts, total] = await accountService.searchByUsernameExactMatch(partialUserName); + expect(accountRepo.searchByUsernameExactMatch).toHaveBeenCalledWith(partialUserName); + expect(total).toBe(1); + expect(accounts[0]).toEqual(AccountEntityToDtoMapper.mapToDto(mockTeacherAccount)); + }); }); }); - describe('findMany', () => { - it('should call repo', async () => { - const foundAccounts = await accountService.findMany(1, 1); - expect(accountRepo.findMany).toHaveBeenCalledWith(1, 1); - expect(foundAccounts).toBeDefined(); - }); - it('should call repo', async () => { - const foundAccounts = await accountService.findMany(); - expect(accountRepo.findMany).toHaveBeenCalledWith(0, 100); - expect(foundAccounts).toBeDefined(); + describe('when find many one time', () => { + const setup = () => { + const mockTeacherAccount = accountFactory.buildWithId(); + + accountRepo.findMany.mockResolvedValue([mockTeacherAccount]); + + return {}; + }; + it('should call repo', async () => { + setup(); + const foundAccounts = await accountService.findMany(1, 1); + expect(accountRepo.findMany).toHaveBeenCalledWith(1, 1); + expect(foundAccounts).toBeDefined(); + }); + }); + describe('when call find many more than one time', () => { + const setup = () => { + const mockTeacherAccount = accountFactory.buildWithId(); + + accountRepo.findMany.mockResolvedValue([mockTeacherAccount]); + + return {}; + }; + it('should call repo', async () => { + setup(); + const foundAccounts = await accountService.findMany(); + expect(accountRepo.findMany).toHaveBeenCalledWith(0, 100); + expect(foundAccounts).toBeDefined(); + }); }); }); }); From a958007faa0ca7752c9eb05370e0ecf4739f3275 Mon Sep 17 00:00:00 2001 From: MajedAlaitwniCap Date: Fri, 22 Sep 2023 12:02:08 +0200 Subject: [PATCH 12/24] new test structure account idm service integration test --- .../account-idm.service.integration.spec.ts | 150 ++++++++++-------- 1 file changed, 87 insertions(+), 63 deletions(-) diff --git a/apps/server/src/modules/account/services/account-idm.service.integration.spec.ts b/apps/server/src/modules/account/services/account-idm.service.integration.spec.ts index ac7a7cdeee6..2d3490bbca6 100644 --- a/apps/server/src/modules/account/services/account-idm.service.integration.spec.ts +++ b/apps/server/src/modules/account/services/account-idm.service.integration.spec.ts @@ -92,79 +92,103 @@ describe('AccountIdmService Integration', () => { } }); - it('save should create a new account', async () => { - if (!isIdmReachable) return; - const createdAccount = await accountIdmService.save(testAccount); - const foundAccount = await identityManagementService.findAccountById(createdAccount.idmReferenceId ?? ''); - - expect(foundAccount).toEqual( - expect.objectContaining({ - id: createdAccount.idmReferenceId ?? '', - username: createdAccount.username, - attRefTechnicalId: technicalRefId, - attRefFunctionalIntId: createdAccount.userId, - attRefFunctionalExtId: createdAccount.systemId, - }) - ); + describe('save', () => { + describe('when account not exists', () => { + it('should create a new account', async () => { + if (!isIdmReachable) return; + const createdAccount = await accountIdmService.save(testAccount); + const foundAccount = await identityManagementService.findAccountById(createdAccount.idmReferenceId ?? ''); + + expect(foundAccount).toEqual( + expect.objectContaining({ + id: createdAccount.idmReferenceId ?? '', + username: createdAccount.username, + attRefTechnicalId: technicalRefId, + attRefFunctionalIntId: createdAccount.userId, + attRefFunctionalExtId: createdAccount.systemId, + }) + ); + }); + }); }); - it('save should update existing account', async () => { - if (!isIdmReachable) return; - const newUsername = 'jane.doe@mail.tld'; - const idmId = await createAccount(); - - await accountIdmService.save({ - id: technicalRefId, - username: newUsername, + describe('save', () => { + describe('when account exists', () => { + it('should update account', async () => { + if (!isIdmReachable) return; + const newUsername = 'jane.doe@mail.tld'; + const idmId = await createAccount(); + + await accountIdmService.save({ + id: technicalRefId, + username: newUsername, + }); + const foundAccount = await identityManagementService.findAccountById(idmId); + + expect(foundAccount).toEqual( + expect.objectContaining({ + id: idmId, + username: newUsername, + }) + ); + }); }); - const foundAccount = await identityManagementService.findAccountById(idmId); - - expect(foundAccount).toEqual( - expect.objectContaining({ - id: idmId, - username: newUsername, - }) - ); }); - it('updateUsername should update username', async () => { - if (!isIdmReachable) return; - const newUserName = 'jane.doe@mail.tld'; - const idmId = await createAccount(); - await accountIdmService.updateUsername(technicalRefId, newUserName); - - const foundAccount = await identityManagementService.findAccountById(idmId); - - expect(foundAccount).toEqual( - expect.objectContaining>({ - username: newUserName, - }) - ); + describe('updateUsername', () => { + describe('when updating username', () => { + it('should update only username', async () => { + if (!isIdmReachable) return; + const newUserName = 'jane.doe@mail.tld'; + const idmId = await createAccount(); + await accountIdmService.updateUsername(technicalRefId, newUserName); + + const foundAccount = await identityManagementService.findAccountById(idmId); + + expect(foundAccount).toEqual( + expect.objectContaining>({ + username: newUserName, + }) + ); + }); + }); }); - it('updatePassword should update password', async () => { - if (!isIdmReachable) return; - await createAccount(); - await expect(accountIdmService.updatePassword(technicalRefId, 'newPassword')).resolves.not.toThrow(); + describe('updatePassword', () => { + describe('when updating with permitted password', () => { + it('should update password', async () => { + if (!isIdmReachable) return; + await createAccount(); + await expect(accountIdmService.updatePassword(technicalRefId, 'newPassword')).resolves.not.toThrow(); + }); + }); }); - it('delete should remove account', async () => { - if (!isIdmReachable) return; - const idmId = await createAccount(); - const foundAccount = await identityManagementService.findAccountById(idmId); - expect(foundAccount).toBeDefined(); - - await accountIdmService.delete(technicalRefId); - await expect(identityManagementService.findAccountById(idmId)).rejects.toThrow(); + describe('delete', () => { + describe('when delete account', () => { + it('should remove account', async () => { + if (!isIdmReachable) return; + const idmId = await createAccount(); + const foundAccount = await identityManagementService.findAccountById(idmId); + expect(foundAccount).toBeDefined(); + + await accountIdmService.delete(technicalRefId); + await expect(identityManagementService.findAccountById(idmId)).rejects.toThrow(); + }); + }); }); - it('deleteByUserId should remove account', async () => { - if (!isIdmReachable) return; - const idmId = await createAccount(); - const foundAccount = await identityManagementService.findAccountById(idmId); - expect(foundAccount).toBeDefined(); - - await accountIdmService.deleteByUserId(testAccount.userId ?? ''); - await expect(identityManagementService.findAccountById(idmId)).rejects.toThrow(); + describe('deleteByUserId', () => { + describe('when deleting by UserId', () => { + it('should remove account', async () => { + if (!isIdmReachable) return; + const idmId = await createAccount(); + const foundAccount = await identityManagementService.findAccountById(idmId); + expect(foundAccount).toBeDefined(); + + await accountIdmService.deleteByUserId(testAccount.userId ?? ''); + await expect(identityManagementService.findAccountById(idmId)).rejects.toThrow(); + }); + }); }); }); From edd618d2f1484df4fd92be0725fb7487466cf492 Mon Sep 17 00:00:00 2001 From: MajedAlaitwniCap Date: Mon, 25 Sep 2023 10:58:07 +0200 Subject: [PATCH 13/24] use setup for account idm service integration test --- .../account-idm.service.integration.spec.ts | 46 ++++++++++++++----- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/apps/server/src/modules/account/services/account-idm.service.integration.spec.ts b/apps/server/src/modules/account/services/account-idm.service.integration.spec.ts index 2d3490bbca6..d41c4ebe1f2 100644 --- a/apps/server/src/modules/account/services/account-idm.service.integration.spec.ts +++ b/apps/server/src/modules/account/services/account-idm.service.integration.spec.ts @@ -114,21 +114,27 @@ describe('AccountIdmService Integration', () => { describe('save', () => { describe('when account exists', () => { + const setup = async () => { + const newUserName = 'jane.doe@mail.tld'; + const idmId = await createAccount(); + + return { idmId, newUserName }; + }; it('should update account', async () => { if (!isIdmReachable) return; - const newUsername = 'jane.doe@mail.tld'; - const idmId = await createAccount(); + const { idmId, newUserName } = await setup(); await accountIdmService.save({ id: technicalRefId, - username: newUsername, + username: newUserName, }); + const foundAccount = await identityManagementService.findAccountById(idmId); expect(foundAccount).toEqual( expect.objectContaining({ id: idmId, - username: newUsername, + username: newUserName, }) ); }); @@ -137,12 +143,17 @@ describe('AccountIdmService Integration', () => { describe('updateUsername', () => { describe('when updating username', () => { - it('should update only username', async () => { - if (!isIdmReachable) return; + const setup = async () => { const newUserName = 'jane.doe@mail.tld'; const idmId = await createAccount(); - await accountIdmService.updateUsername(technicalRefId, newUserName); + return { newUserName, idmId }; + }; + it('should update only username', async () => { + if (!isIdmReachable) return; + const { newUserName, idmId } = await setup(); + + await accountIdmService.updateUsername(technicalRefId, newUserName); const foundAccount = await identityManagementService.findAccountById(idmId); expect(foundAccount).toEqual( @@ -156,9 +167,12 @@ describe('AccountIdmService Integration', () => { describe('updatePassword', () => { describe('when updating with permitted password', () => { + const setup = async () => { + await createAccount(); + }; it('should update password', async () => { if (!isIdmReachable) return; - await createAccount(); + await setup(); await expect(accountIdmService.updatePassword(technicalRefId, 'newPassword')).resolves.not.toThrow(); }); }); @@ -166,10 +180,14 @@ describe('AccountIdmService Integration', () => { describe('delete', () => { describe('when delete account', () => { - it('should remove account', async () => { - if (!isIdmReachable) return; + const setup = async () => { const idmId = await createAccount(); const foundAccount = await identityManagementService.findAccountById(idmId); + return { idmId, foundAccount }; + }; + it('should remove account', async () => { + if (!isIdmReachable) return; + const { idmId, foundAccount } = await setup(); expect(foundAccount).toBeDefined(); await accountIdmService.delete(technicalRefId); @@ -180,10 +198,14 @@ describe('AccountIdmService Integration', () => { describe('deleteByUserId', () => { describe('when deleting by UserId', () => { - it('should remove account', async () => { - if (!isIdmReachable) return; + const setup = async () => { const idmId = await createAccount(); const foundAccount = await identityManagementService.findAccountById(idmId); + return { idmId, foundAccount }; + }; + it('should remove account', async () => { + if (!isIdmReachable) return; + const { idmId, foundAccount } = await setup(); expect(foundAccount).toBeDefined(); await accountIdmService.deleteByUserId(testAccount.userId ?? ''); From 0575f0b07f401f4b484320512a1f97230c1dba19 Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Mon, 25 Sep 2023 11:46:19 +0200 Subject: [PATCH 14/24] Use setup and new test structure in account.uc.spec.ts. --- .../src/modules/account/uc/account.uc.spec.ts | 4048 ++++++++++++----- 1 file changed, 2973 insertions(+), 1075 deletions(-) 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 05cedccf5b1..84ca3f88296 100644 --- a/apps/server/src/modules/account/uc/account.uc.spec.ts +++ b/apps/server/src/modules/account/uc/account.uc.spec.ts @@ -4,13 +4,11 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AuthorizationError, EntityNotFoundError, ForbiddenOperationError, ValidationError } from '@shared/common'; import { Account, - Counted, EntityId, Permission, PermissionService, Role, RoleName, - School, SchoolRolePermission, SchoolRoles, User, @@ -37,62 +35,19 @@ import { AccountUc } from './account.uc'; describe('AccountUc', () => { let module: TestingModule; let accountUc: AccountUc; - let userRepo: UserRepo; - let accountService: AccountService; - let accountValidationService: AccountValidationService; + let userRepo: DeepMocked; + let accountService: DeepMocked; + let accountValidationService: DeepMocked; let configService: DeepMocked; - let mockSchool: School; - let mockOtherSchool: School; - let mockSchoolWithStudentVisibility: School; - - let mockSuperheroUser: User; - let mockAdminUser: User; - let mockTeacherUser: User; - let mockOtherTeacherUser: User; - let mockTeacherNoUserNoSchoolPermissionUser: User; - let mockTeacherNoUserPermissionUser: User; - let mockStudentSchoolPermissionUser: User; - let mockStudentUser: User; - let mockOtherStudentUser: User; - let mockDifferentSchoolAdminUser: User; - let mockDifferentSchoolTeacherUser: User; - let mockDifferentSchoolStudentUser: User; - let mockUnknownRoleUser: User; - let mockExternalUser: User; - let mockUserWithoutAccount: User; - let mockUserWithoutRole: User; - let mockStudentUserWithoutAccount: User; - let mockOtherStudentSchoolPermissionUser: User; - - let mockSuperheroAccount: Account; - let mockTeacherAccount: Account; - let mockOtherTeacherAccount: Account; - let mockTeacherNoUserPermissionAccount: Account; - let mockTeacherNoUserNoSchoolPermissionAccount: Account; - let mockAdminAccount: Account; - let mockStudentAccount: Account; - let mockStudentSchoolPermissionAccount: Account; - let mockDifferentSchoolAdminAccount: Account; - let mockDifferentSchoolTeacherAccount: Account; - let mockDifferentSchoolStudentAccount: Account; - let mockUnknownRoleUserAccount: Account; - let mockExternalUserAccount: Account; - let mockAccountWithoutRole: Account; - let mockAccountWithoutUser: Account; - let mockAccountWithSystemId: Account; - let mockAccountWithLastFailedLogin: Account; - let mockAccountWithOldLastFailedLogin: Account; - let mockAccountWithNoLastFailedLogin: Account; - let mockAccounts: Account[]; - let mockUsers: User[]; - const defaultPassword = 'DummyPasswd!1'; const otherPassword = 'DummyPasswd!2'; const defaultPasswordHash = '$2a$10$/DsztV5o6P5piW2eWJsxw.4nHovmJGBA.QNwiTmuZ/uvUc40b.Uhu'; const LOGIN_BLOCK_TIME = 15; afterAll(async () => { + jest.restoreAllMocks(); + jest.resetAllMocks(); await module.close(); }); @@ -102,103 +57,7 @@ describe('AccountUc', () => { AccountUc, { provide: AccountService, - useValue: { - saveWithValidation: jest.fn().mockImplementation((account: AccountDto): Promise => { - if (account.username === 'fail@to.update') { - return Promise.reject(); - } - const accountEntity = mockAccounts.find( - (tempAccount) => tempAccount.userId?.toString() === account.userId - ); - if (accountEntity) { - Object.assign(accountEntity, account); - return Promise.resolve(); - } - return Promise.reject(); - }), - save: jest.fn().mockImplementation((account: AccountDto): Promise => { - if (account.username === 'fail@to.update') { - return Promise.reject(); - } - const accountEntity = mockAccounts.find( - (tempAccount) => tempAccount.userId?.toString() === account.userId - ); - if (accountEntity) { - Object.assign(accountEntity, account); - return Promise.resolve(); - } - return Promise.reject(); - }), - delete: (id: EntityId): Promise => { - const account = mockAccounts.find((tempAccount) => tempAccount.id?.toString() === id); - - if (account) { - return Promise.resolve(AccountEntityToDtoMapper.mapToDto(account)); - } - throw new EntityNotFoundError(Account.name); - }, - create: (): Promise => Promise.resolve(), - findByUserId: (userId: EntityId): Promise => { - const account = mockAccounts.find((tempAccount) => tempAccount.userId?.toString() === userId); - - if (account) { - return Promise.resolve(AccountEntityToDtoMapper.mapToDto(account)); - } - return Promise.resolve(null); - }, - findByUserIdOrFail: (userId: EntityId): Promise => { - const account = mockAccounts.find((tempAccount) => tempAccount.userId?.toString() === userId); - - if (account) { - return Promise.resolve(AccountEntityToDtoMapper.mapToDto(account)); - } - if (userId === 'accountWithoutUser') { - return Promise.resolve(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); - } - throw new EntityNotFoundError(Account.name); - }, - findById: (accountId: EntityId): Promise => { - const account = mockAccounts.find((tempAccount) => tempAccount.id === accountId); - - if (account) { - return Promise.resolve(AccountEntityToDtoMapper.mapToDto(account)); - } - throw new EntityNotFoundError(Account.name); - }, - findByUsernameAndSystemId: (username: string, systemId: EntityId | ObjectId): Promise => { - const account = mockAccounts.find( - (tempAccount) => tempAccount.username === username && tempAccount.systemId === systemId - ); - if (account) { - return Promise.resolve(AccountEntityToDtoMapper.mapToDto(account)); - } - throw new EntityNotFoundError(Account.name); - }, - searchByUsernameExactMatch: (username: string): Promise> => { - const account = mockAccounts.find((tempAccount) => tempAccount.username === username); - - if (account) { - return Promise.resolve([[AccountEntityToDtoMapper.mapToDto(account)], 1]); - } - if (username === 'not@available.username') { - return Promise.resolve([[AccountEntityToDtoMapper.mapToDto(mockOtherTeacherAccount)], 1]); - } - if (username === 'multiple@account.username') { - return Promise.resolve([ - mockAccounts.map((mockAccount) => AccountEntityToDtoMapper.mapToDto(mockAccount)), - mockAccounts.length, - ]); - } - return Promise.resolve([[], 0]); - }, - searchByUsernamePartialMatch: (): Promise> => - Promise.resolve([ - mockAccounts.map((mockAccount) => AccountEntityToDtoMapper.mapToDto(mockAccount)), - mockAccounts.length, - ]), - updateLastTriedFailedLogin: jest.fn(), - validatePassword: jest.fn().mockResolvedValue(true), - }, + useValue: createMock(), }, { provide: ConfigService, @@ -206,42 +65,12 @@ describe('AccountUc', () => { }, { provide: UserRepo, - useValue: { - findById: (userId: EntityId): Promise => { - const user = mockUsers.find((tempUser) => tempUser.id === userId); - if (user) { - return Promise.resolve(user); - } - throw new EntityNotFoundError(User.name); - }, - findByEmail: (email: string): Promise => { - const user = mockUsers.find((tempUser) => tempUser.email === email); - - if (user) { - return Promise.resolve([user]); - } - if (email === 'not@available.email') { - return Promise.resolve([mockExternalUser]); - } - if (email === 'multiple@user.email') { - return Promise.resolve(mockUsers); - } - return Promise.resolve([]); - }, - save: jest.fn().mockImplementation((user: User): Promise => { - if (user.firstName === 'failToUpdate' || user.email === 'user-fail@to.update') { - return Promise.reject(); - } - return Promise.resolve(); - }), - }, + useValue: createMock(), }, PermissionService, { provide: AccountValidationService, - useValue: { - isUniqueEmail: jest.fn().mockResolvedValue(true), - }, + useValue: createMock(), }, ], }).compile(); @@ -249,983 +78,3052 @@ describe('AccountUc', () => { accountUc = module.get(AccountUc); userRepo = module.get(UserRepo); accountService = module.get(AccountService); - await setupEntities(); accountValidationService = module.get(AccountValidationService); configService = module.get(ConfigService); + await setupEntities(); }); beforeEach(() => { - mockSchool = schoolFactory.buildWithId(); - mockOtherSchool = schoolFactory.buildWithId(); - mockSchoolWithStudentVisibility = schoolFactory.buildWithId(); - mockSchoolWithStudentVisibility.permissions = new SchoolRoles(); - mockSchoolWithStudentVisibility.permissions.teacher = new SchoolRolePermission(); - mockSchoolWithStudentVisibility.permissions.teacher.STUDENT_LIST = true; - - mockSuperheroUser = userFactory.buildWithId({ - school: mockSchool, - roles: [ - new Role({ - name: RoleName.SUPERHERO, - permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], - }), - ], + jest.clearAllMocks(); + jest.resetAllMocks(); + }); + + describe('updateMyAccount', () => { + describe('When user does not exist', () => { + const setup = () => { + userRepo.findById.mockImplementation(() => { + throw new EntityNotFoundError(User.name); + }); + }; + + it('should throw EntityNotFoundError', async () => { + setup(); + await expect(accountUc.updateMyAccount('accountWithoutUser', { passwordOld: defaultPassword })).rejects.toThrow( + EntityNotFoundError + ); + }); }); - mockAdminUser = userFactory.buildWithId({ - school: mockSchool, - roles: [ - new Role({ - name: RoleName.ADMINISTRATOR, - permissions: [ - Permission.TEACHER_EDIT, - Permission.STUDENT_EDIT, - Permission.STUDENT_LIST, - Permission.TEACHER_LIST, - Permission.TEACHER_CREATE, - Permission.STUDENT_CREATE, - Permission.TEACHER_DELETE, - Permission.STUDENT_DELETE, + + describe('When account does not exist', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockUserWithoutAccount = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.ADMINISTRATOR, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }), ], - }), - ], - }); - mockTeacherUser = userFactory.buildWithId({ - school: mockSchool, - roles: [ - new Role({ - name: RoleName.TEACHER, - permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], - }), - ], - }); - mockOtherTeacherUser = userFactory.buildWithId({ - school: mockSchool, - roles: [ - new Role({ - name: RoleName.TEACHER, - permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], - }), - ], - }); - mockTeacherNoUserPermissionUser = userFactory.buildWithId({ - school: mockSchoolWithStudentVisibility, - roles: [ - new Role({ - name: RoleName.TEACHER, - permissions: [], - }), - ], - }); - mockTeacherNoUserNoSchoolPermissionUser = userFactory.buildWithId({ - school: mockSchool, - roles: [ - new Role({ - name: RoleName.TEACHER, - permissions: [], - }), - ], - }); - mockStudentSchoolPermissionUser = userFactory.buildWithId({ - school: mockSchoolWithStudentVisibility, - roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], - }); - mockOtherStudentSchoolPermissionUser = userFactory.buildWithId({ - school: mockSchoolWithStudentVisibility, - roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], - }); - mockStudentUser = userFactory.buildWithId({ - school: mockSchool, - roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], - }); - mockOtherStudentUser = userFactory.buildWithId({ - school: mockSchool, - roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], - }); - mockDifferentSchoolAdminUser = userFactory.buildWithId({ - school: mockOtherSchool, - roles: [...mockAdminUser.roles], - }); - mockDifferentSchoolTeacherUser = userFactory.buildWithId({ - school: mockOtherSchool, - roles: [...mockTeacherUser.roles], - }); - mockDifferentSchoolStudentUser = userFactory.buildWithId({ - school: mockOtherSchool, - roles: [...mockStudentUser.roles], - }); - mockUserWithoutAccount = userFactory.buildWithId({ - school: mockSchool, - roles: [ - new Role({ - name: RoleName.ADMINISTRATOR, - permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], - }), - ], - }); - mockUserWithoutRole = userFactory.buildWithId({ - school: mockSchool, - roles: [], - }); - mockUnknownRoleUser = userFactory.buildWithId({ - school: mockSchool, - roles: [new Role({ name: 'undefinedRole' as RoleName, permissions: ['' as Permission] })], - }); - mockExternalUser = userFactory.buildWithId({ - school: mockSchool, - roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], - }); - mockStudentUserWithoutAccount = userFactory.buildWithId({ - school: mockSchool, - roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], - }); + }); - mockSuperheroAccount = accountFactory.buildWithId({ - userId: mockSuperheroUser.id, - password: defaultPasswordHash, - }); - mockTeacherAccount = accountFactory.buildWithId({ - userId: mockTeacherUser.id, - password: defaultPasswordHash, - }); - mockOtherTeacherAccount = accountFactory.buildWithId({ - userId: mockOtherTeacherUser.id, - password: defaultPasswordHash, - }); - mockTeacherNoUserPermissionAccount = accountFactory.buildWithId({ - userId: mockTeacherNoUserPermissionUser.id, - password: defaultPasswordHash, - }); - mockTeacherNoUserNoSchoolPermissionAccount = accountFactory.buildWithId({ - userId: mockTeacherNoUserNoSchoolPermissionUser.id, - password: defaultPasswordHash, - }); - mockAdminAccount = accountFactory.buildWithId({ - userId: mockAdminUser.id, - password: defaultPasswordHash, - }); - mockStudentAccount = accountFactory.buildWithId({ - userId: mockStudentUser.id, - password: defaultPasswordHash, - }); - mockStudentSchoolPermissionAccount = accountFactory.buildWithId({ - userId: mockStudentSchoolPermissionUser.id, - password: defaultPasswordHash, - }); - mockAccountWithoutRole = accountFactory.buildWithId({ - userId: mockUserWithoutRole.id, - password: defaultPasswordHash, - }); - mockDifferentSchoolAdminAccount = accountFactory.buildWithId({ - userId: mockDifferentSchoolAdminUser.id, - password: defaultPasswordHash, - }); - mockDifferentSchoolTeacherAccount = accountFactory.buildWithId({ - userId: mockDifferentSchoolTeacherUser.id, - password: defaultPasswordHash, - }); - mockDifferentSchoolStudentAccount = accountFactory.buildWithId({ - userId: mockDifferentSchoolStudentUser.id, - password: defaultPasswordHash, - }); - mockUnknownRoleUserAccount = accountFactory.buildWithId({ - userId: mockUnknownRoleUser.id, - password: defaultPasswordHash, - }); - const externalSystem = systemFactory.buildWithId(); - mockExternalUserAccount = accountFactory.buildWithId({ - userId: mockExternalUser.id, - password: defaultPasswordHash, - systemId: externalSystem.id, - }); - mockAccountWithoutUser = accountFactory.buildWithId({ - userId: undefined, - password: defaultPasswordHash, - systemId: systemFactory.buildWithId().id, - }); - mockAccountWithSystemId = accountFactory.withSystemId(new ObjectId(10)).build(); - mockAccountWithLastFailedLogin = accountFactory.buildWithId({ - userId: undefined, - password: defaultPasswordHash, - systemId: systemFactory.buildWithId().id, - lasttriedFailedLogin: new Date(), - }); - mockAccountWithOldLastFailedLogin = accountFactory.buildWithId({ - userId: undefined, - password: defaultPasswordHash, - systemId: systemFactory.buildWithId().id, - lasttriedFailedLogin: new Date(new Date().getTime() - LOGIN_BLOCK_TIME - 1), - }); - mockAccountWithNoLastFailedLogin = accountFactory.buildWithId({ - userId: undefined, - password: defaultPasswordHash, - systemId: systemFactory.buildWithId().id, - lasttriedFailedLogin: undefined, - }); + accountService.findByUserIdOrFail.mockImplementation((): Promise => { + throw new EntityNotFoundError(Account.name); + }); - mockUsers = [ - mockSuperheroUser, - mockAdminUser, - mockTeacherUser, - mockOtherTeacherUser, - mockTeacherNoUserPermissionUser, - mockTeacherNoUserNoSchoolPermissionUser, - mockStudentUser, - mockStudentSchoolPermissionUser, - mockDifferentSchoolAdminUser, - mockDifferentSchoolTeacherUser, - mockDifferentSchoolStudentUser, - mockUnknownRoleUser, - mockExternalUser, - mockUserWithoutRole, - mockUserWithoutAccount, - mockStudentUserWithoutAccount, - mockOtherStudentUser, - mockOtherStudentSchoolPermissionUser, - ]; - - mockAccounts = [ - mockSuperheroAccount, - mockAdminAccount, - mockTeacherAccount, - mockOtherTeacherAccount, - mockTeacherNoUserPermissionAccount, - mockTeacherNoUserNoSchoolPermissionAccount, - mockStudentAccount, - mockStudentSchoolPermissionAccount, - mockDifferentSchoolAdminAccount, - mockDifferentSchoolTeacherAccount, - mockDifferentSchoolStudentAccount, - mockUnknownRoleUserAccount, - mockExternalUserAccount, - mockAccountWithoutRole, - mockAccountWithoutUser, - mockAccountWithSystemId, - mockAccountWithLastFailedLogin, - mockAccountWithOldLastFailedLogin, - mockAccountWithNoLastFailedLogin, - ]; - }); + return { mockUserWithoutAccount }; + }; - describe('updateMyAccount', () => { - it('should throw if user does not exist', async () => { - mockStudentUser.forcePasswordChange = true; - mockStudentUser.preferences = { firstLogin: true }; - await expect(accountUc.updateMyAccount('accountWithoutUser', { passwordOld: defaultPassword })).rejects.toThrow( - EntityNotFoundError - ); - }); - it('should throw if account does not exist', async () => { - await expect( - accountUc.updateMyAccount(mockUserWithoutAccount.id, { - passwordOld: defaultPassword, - }) - ).rejects.toThrow(EntityNotFoundError); - }); - it('should throw if account is external', async () => { - await expect( - accountUc.updateMyAccount(mockExternalUserAccount.userId?.toString() ?? '', { - passwordOld: defaultPassword, - }) - ).rejects.toThrow(ForbiddenOperationError); - }); - it('should throw if password does not match', async () => { - jest.spyOn(accountService, 'validatePassword').mockResolvedValueOnce(false); - await expect( - accountUc.updateMyAccount(mockStudentUser.id, { - passwordOld: 'DoesNotMatch', - }) - ).rejects.toThrow(AuthorizationError); - }); - it('should throw if changing own name is not allowed', async () => { - await expect( - accountUc.updateMyAccount(mockStudentUser.id, { - passwordOld: defaultPassword, - firstName: 'newFirstName', - }) - ).rejects.toThrow(ForbiddenOperationError); - await expect( - accountUc.updateMyAccount(mockStudentUser.id, { - passwordOld: defaultPassword, - lastName: 'newLastName', - }) - ).rejects.toThrow(ForbiddenOperationError); - }); - it('should allow to update email', async () => { - await expect( - accountUc.updateMyAccount(mockStudentUser.id, { - passwordOld: defaultPassword, - email: 'an@available.mail', - }) - ).resolves.not.toThrow(); - }); - it('should use email as account user name in lower case', async () => { - const accountSaveSpy = jest.spyOn(accountService, 'save'); - const testMail = 'AN@AVAILABLE.MAIL'; - await expect( - accountUc.updateMyAccount(mockStudentUser.id, { - passwordOld: defaultPassword, - email: testMail, - }) - ).resolves.not.toThrow(); - expect(accountSaveSpy).toBeCalledWith(expect.objectContaining({ username: testMail.toLowerCase() })); - }); - it('should use email as user email in lower case', async () => { - const userUpdateSpy = jest.spyOn(userRepo, 'save'); - const testMail = 'AN@AVAILABLE.MAIL'; - await expect( - accountUc.updateMyAccount(mockStudentUser.id, { - passwordOld: defaultPassword, - email: testMail, - }) - ).resolves.not.toThrow(); - expect(userUpdateSpy).toBeCalledWith(expect.objectContaining({ email: testMail.toLowerCase() })); - }); - it('should always update account user name AND user email together.', async () => { - const accountSaveSpy = jest.spyOn(accountService, 'save'); - const userUpdateSpy = jest.spyOn(userRepo, 'save'); - const testMail = 'an@available.mail'; - await expect( - accountUc.updateMyAccount(mockStudentUser.id, { - passwordOld: defaultPassword, - email: testMail, - }) - ).resolves.not.toThrow(); - expect(userUpdateSpy).toBeCalledWith(expect.objectContaining({ email: testMail.toLowerCase() })); - expect(accountSaveSpy).toBeCalledWith(expect.objectContaining({ username: testMail.toLowerCase() })); - }); - it('should throw if new email already in use', async () => { - const accountIsUniqueEmailSpy = jest.spyOn(accountValidationService, 'isUniqueEmail'); - accountIsUniqueEmailSpy.mockResolvedValueOnce(false); - await expect( - accountUc.updateMyAccount(mockStudentUser.id, { - passwordOld: defaultPassword, - email: mockAdminUser.email, - }) - ).rejects.toThrow(ValidationError); - }); - it('should allow to update with strong password', async () => { - await expect( - accountUc.updateMyAccount(mockStudentUser.id, { - passwordOld: defaultPassword, - passwordNew: otherPassword, - }) - ).resolves.not.toThrow(); - }); - it('should allow to update first and last name if teacher', async () => { - await expect( - accountUc.updateMyAccount(mockTeacherUser.id, { - passwordOld: defaultPassword, - firstName: 'newFirstName', - }) - ).resolves.not.toThrow(); - await expect( - accountUc.updateMyAccount(mockTeacherUser.id, { - passwordOld: defaultPassword, - lastName: 'newLastName', - }) - ).resolves.not.toThrow(); - }); - it('should allow to update first and last name if admin', async () => { - await expect( - accountUc.updateMyAccount(mockAdminUser.id, { - passwordOld: defaultPassword, - firstName: 'newFirstName', - }) - ).resolves.not.toThrow(); - await expect( - accountUc.updateMyAccount(mockAdminUser.id, { - passwordOld: defaultPassword, - lastName: 'newLastName', - }) - ).resolves.not.toThrow(); - }); - it('should allow to update first and last name if superhero', async () => { - await expect( - accountUc.updateMyAccount(mockSuperheroUser.id, { - passwordOld: defaultPassword, - firstName: 'newFirstName', - }) - ).resolves.not.toThrow(); - await expect( - accountUc.updateMyAccount(mockSuperheroUser.id, { - passwordOld: defaultPassword, - lastName: 'newLastName', - }) - ).resolves.not.toThrow(); - }); - it('should throw if user can not be updated', async () => { - await expect( - accountUc.updateMyAccount(mockTeacherUser.id, { - passwordOld: defaultPassword, - firstName: 'failToUpdate', - }) - ).rejects.toThrow(EntityNotFoundError); - }); - it('should throw if account can not be updated', async () => { - await expect( - accountUc.updateMyAccount(mockStudentUser.id, { - passwordOld: defaultPassword, - email: 'fail@to.update', - }) - ).rejects.toThrow(EntityNotFoundError); - }); - it('should not update password if no new password', async () => { - const spy = jest.spyOn(accountService, 'save'); - await accountUc.updateMyAccount(mockStudentUser.id, { - passwordOld: defaultPassword, - passwordNew: undefined, - email: 'newemail@to.update', - }); - expect(spy).toHaveBeenCalledWith( - expect.objectContaining({ - password: undefined, - }) - ); + it('should throw', async () => { + const { mockUserWithoutAccount } = setup(); + await expect( + accountUc.updateMyAccount(mockUserWithoutAccount.id, { + passwordOld: defaultPassword, + }) + ).rejects.toThrow(EntityNotFoundError); + }); }); - }); + describe('When account is external', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); - describe('replaceMyTemporaryPassword', () => { - it('should throw if passwords do not match', async () => { - await expect( - accountUc.replaceMyTemporaryPassword( - mockStudentAccount.userId?.toString() ?? '', - defaultPassword, - 'FooPasswd!1' - ) - ).rejects.toThrow(ForbiddenOperationError); - }); + const mockExternalUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const externalSystem = systemFactory.buildWithId(); + const mockExternalUserAccount = accountFactory.buildWithId({ + userId: mockExternalUser.id, + password: defaultPasswordHash, + systemId: externalSystem.id, + }); - it('should throw if account does not exist', async () => { - await expect( - accountUc.replaceMyTemporaryPassword(mockUserWithoutAccount.id, defaultPassword, defaultPassword) - ).rejects.toThrow(EntityNotFoundError); - }); - it('should throw if user does not exist', async () => { - await expect( - accountUc.replaceMyTemporaryPassword('accountWithoutUser', defaultPassword, defaultPassword) - ).rejects.toThrow(EntityNotFoundError); - }); - it('should throw if account is external', async () => { - await expect( - accountUc.replaceMyTemporaryPassword( - mockExternalUserAccount.userId?.toString() ?? '', - defaultPassword, - defaultPassword - ) - ).rejects.toThrow(ForbiddenOperationError); - }); - it('should throw if not the users password is temporary', async () => { - mockStudentUser.forcePasswordChange = false; - mockStudentUser.preferences = { firstLogin: true }; - await expect( - accountUc.replaceMyTemporaryPassword( - mockStudentAccount.userId?.toString() ?? '', - defaultPassword, - defaultPassword - ) - ).rejects.toThrow(ForbiddenOperationError); - }); - it('should throw, if old password is the same as new password', async () => { - mockStudentUser.forcePasswordChange = false; - mockStudentUser.preferences = { firstLogin: false }; - await expect( - accountUc.replaceMyTemporaryPassword( - mockStudentAccount.userId?.toString() ?? '', - defaultPassword, - defaultPassword - ) - ).rejects.toThrow(ForbiddenOperationError); - }); - it('should throw, if old password is undefined', async () => { - mockStudentUser.forcePasswordChange = false; - mockStudentUser.preferences = { firstLogin: false }; - mockStudentAccount.password = undefined; - await expect( - accountUc.replaceMyTemporaryPassword( - mockStudentAccount.userId?.toString() ?? '', - defaultPassword, - defaultPassword - ) - ).rejects.toThrow(Error); - }); - it('should allow to set strong password, if the admin manipulated the users password', async () => { - mockStudentUser.forcePasswordChange = true; - mockStudentUser.preferences = { firstLogin: true }; - jest.spyOn(accountService, 'validatePassword').mockResolvedValueOnce(false); - await expect( - accountUc.replaceMyTemporaryPassword(mockStudentAccount.userId?.toString() ?? '', otherPassword, otherPassword) - ).resolves.not.toThrow(); - }); - it('should allow to set strong password, if this is the users first login', async () => { - mockStudentUser.forcePasswordChange = false; - mockStudentUser.preferences = { firstLogin: false }; - jest.spyOn(accountService, 'validatePassword').mockResolvedValueOnce(false); - await expect( - accountUc.replaceMyTemporaryPassword(mockStudentAccount.userId?.toString() ?? '', otherPassword, otherPassword) - ).resolves.not.toThrow(); - }); - it('should allow to set strong password, if this is the users first login (if undefined)', async () => { - mockStudentUser.forcePasswordChange = false; - mockStudentUser.preferences = undefined; - jest.spyOn(accountService, 'validatePassword').mockResolvedValueOnce(false); - await expect( - accountUc.replaceMyTemporaryPassword(mockStudentAccount.userId?.toString() ?? '', otherPassword, otherPassword) - ).resolves.not.toThrow(); - }); - it('should throw if user can not be updated', async () => { - mockStudentUser.forcePasswordChange = false; - mockStudentUser.preferences = { firstLogin: false }; - mockStudentUser.firstName = 'failToUpdate'; - jest.spyOn(accountService, 'validatePassword').mockResolvedValueOnce(false); - await expect( - accountUc.replaceMyTemporaryPassword(mockStudentAccount.userId?.toString() ?? '', otherPassword, otherPassword) - ).rejects.toThrow(EntityNotFoundError); - }); - it('should throw if account can not be updated', async () => { - mockStudentUser.forcePasswordChange = false; - mockStudentUser.preferences = { firstLogin: false }; - mockStudentAccount.username = 'fail@to.update'; - jest.spyOn(accountService, 'validatePassword').mockResolvedValueOnce(false); - await expect( - accountUc.replaceMyTemporaryPassword(mockStudentAccount.userId?.toString() ?? '', otherPassword, otherPassword) - ).rejects.toThrow(EntityNotFoundError); - }); - }); + accountService.findByUserIdOrFail.mockResolvedValueOnce( + AccountEntityToDtoMapper.mapToDto(mockExternalUserAccount) + ); - describe('searchAccounts', () => { - it('should return one account, if search type is userId', async () => { - const accounts = await accountUc.searchAccounts( - { userId: mockSuperheroUser.id } as ICurrentUser, - { type: AccountSearchType.USER_ID, value: mockStudentUser.id } as AccountSearchQueryParams - ); - const expected = new AccountSearchListResponse( - [AccountResponseMapper.mapToResponseFromEntity(mockStudentAccount)], - 1, - 0, - 1 - ); - expect(accounts).toStrictEqual(expected); - }); - it('should return empty list, if account is not found', async () => { - const accounts = await accountUc.searchAccounts( - { userId: mockSuperheroUser.id } as ICurrentUser, - { type: AccountSearchType.USER_ID, value: mockUserWithoutAccount.id } as AccountSearchQueryParams - ); - const expected = new AccountSearchListResponse([], 0, 0, 0); - expect(accounts).toStrictEqual(expected); - }); - it('should return one or more accounts, if search type is username', async () => { - const accounts = await accountUc.searchAccounts( - { userId: mockSuperheroUser.id } as ICurrentUser, - { type: AccountSearchType.USERNAME, value: '' } as AccountSearchQueryParams - ); - expect(accounts.skip).toEqual(0); - expect(accounts.limit).toEqual(10); - expect(accounts.total).toBeGreaterThan(1); - expect(accounts.data.length).toBeGreaterThan(1); - }); - it('should throw, if user has not the right permissions', async () => { - await expect( - accountUc.searchAccounts( - { userId: mockTeacherUser.id } as ICurrentUser, - { type: AccountSearchType.USER_ID, value: mockAdminUser.id } as AccountSearchQueryParams - ) - ).rejects.toThrow(ForbiddenOperationError); - - await expect( - accountUc.searchAccounts( - { userId: mockStudentUser.id } as ICurrentUser, - { type: AccountSearchType.USER_ID, value: mockOtherStudentUser.id } as AccountSearchQueryParams - ) - ).rejects.toThrow(ForbiddenOperationError); - - await expect( - accountUc.searchAccounts( - { userId: mockStudentUser.id } as ICurrentUser, - { type: AccountSearchType.USER_ID, value: mockTeacherUser.id } as AccountSearchQueryParams - ) - ).rejects.toThrow(ForbiddenOperationError); - }); - it('should throw, if search type is unknown', async () => { - await expect( - accountUc.searchAccounts( - { userId: mockSuperheroUser.id } as ICurrentUser, - { type: '' as AccountSearchType } as AccountSearchQueryParams - ) - ).rejects.toThrow('Invalid search type.'); - }); - it('should throw, if user is no superhero', async () => { - await expect( - accountUc.searchAccounts( - { userId: mockTeacherUser.id } as ICurrentUser, - { type: AccountSearchType.USERNAME, value: mockStudentUser.id } as AccountSearchQueryParams - ) - ).rejects.toThrow(ForbiddenOperationError); - }); + return { mockExternalUserAccount }; + }; + it('should throw ForbiddenOperationError', async () => { + const { mockExternalUserAccount } = setup(); - describe('hasPermissionsToAccessAccount', () => { - beforeEach(() => { - configService.get.mockReturnValue(false); - }); - it('admin can access teacher of the same school via user id', async () => { - const currentUser = { userId: mockAdminUser.id } as ICurrentUser; - const params = { type: AccountSearchType.USER_ID, value: mockTeacherUser.id } as AccountSearchQueryParams; - await expect(accountUc.searchAccounts(currentUser, params)).resolves.not.toThrow(); + await expect( + accountUc.updateMyAccount(mockExternalUserAccount.userId?.toString() ?? '', { + passwordOld: defaultPassword, + }) + ).rejects.toThrow(ForbiddenOperationError); }); - it('admin can access student of the same school via user id', async () => { - const currentUser = { userId: mockAdminUser.id } as ICurrentUser; - const params = { type: AccountSearchType.USER_ID, value: mockStudentUser.id } as AccountSearchQueryParams; - await expect(accountUc.searchAccounts(currentUser, params)).resolves.not.toThrow(); - }); - it('admin can not access admin of the same school via user id', async () => { - const currentUser = { userId: mockAdminUser.id } as ICurrentUser; - const params = { type: AccountSearchType.USER_ID, value: mockAdminUser.id } as AccountSearchQueryParams; - await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); + }); + + describe('When password does not match', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValueOnce(mockStudentUser); + accountService.findByUserIdOrFail.mockResolvedValueOnce(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + accountService.validatePassword.mockResolvedValueOnce(false); + + return { mockStudentUser }; + }; + it('should throw AuthorizationError', async () => { + const { mockStudentUser } = setup(); + await expect( + accountUc.updateMyAccount(mockStudentUser.id, { + passwordOld: 'DoesNotMatch', + }) + ).rejects.toThrow(AuthorizationError); }); - it('admin can not access any account of a foreign school via user id', async () => { - const currentUser = { userId: mockDifferentSchoolAdminUser.id } as ICurrentUser; + }); + + describe('When changing own name is not allowed', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); - let params = { type: AccountSearchType.USER_ID, value: mockTeacherUser.id } as AccountSearchQueryParams; - await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValue(mockStudentUser); + accountService.findByUserIdOrFail.mockResolvedValue(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + accountService.validatePassword.mockResolvedValue(true); - params = { type: AccountSearchType.USER_ID, value: mockStudentUser.id } as AccountSearchQueryParams; - await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); + return { mockStudentUser }; + }; + it('should throw ForbiddenOperationError', async () => { + const { mockStudentUser } = setup(); + await expect( + accountUc.updateMyAccount(mockStudentUser.id, { + passwordOld: defaultPassword, + firstName: 'newFirstName', + }) + ).rejects.toThrow(ForbiddenOperationError); + await expect( + accountUc.updateMyAccount(mockStudentUser.id, { + passwordOld: defaultPassword, + lastName: 'newLastName', + }) + ).rejects.toThrow(ForbiddenOperationError); }); - it('teacher can access teacher of the same school via user id', async () => { - const currentUser = { userId: mockTeacherUser.id } as ICurrentUser; - const params = { type: AccountSearchType.USER_ID, value: mockOtherTeacherUser.id } as AccountSearchQueryParams; - await expect(accountUc.searchAccounts(currentUser, params)).resolves.not.toThrow(); + }); + + describe('When using student user', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValue(mockStudentUser); + accountService.findByUserIdOrFail.mockResolvedValue(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + accountService.validatePassword.mockResolvedValue(true); + + return { mockStudentUser }; + }; + it('should allow to update email', async () => { + const { mockStudentUser } = setup(); + await expect( + accountUc.updateMyAccount(mockStudentUser.id, { + passwordOld: defaultPassword, + email: 'an@available.mail', + }) + ).resolves.not.toThrow(); }); - it('teacher can access student of the same school via user id', async () => { - const currentUser = { userId: mockTeacherUser.id } as ICurrentUser; - const params = { type: AccountSearchType.USER_ID, value: mockStudentUser.id } as AccountSearchQueryParams; - await expect(accountUc.searchAccounts(currentUser, params)).resolves.not.toThrow(); + }); + describe('When using student user', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValue(mockStudentUser); + accountService.findByUserIdOrFail.mockResolvedValue(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + accountService.validatePassword.mockResolvedValue(true); + accountValidationService.isUniqueEmail.mockResolvedValueOnce(true); + const accountSaveSpy = jest.spyOn(accountService, 'save'); + + return { mockStudentUser, accountSaveSpy }; + }; + it('should use email as account user name in lower case', async () => { + const { mockStudentUser, accountSaveSpy } = setup(); + + const testMail = 'AN@AVAILABLE.MAIL'; + await expect( + accountUc.updateMyAccount(mockStudentUser.id, { + passwordOld: defaultPassword, + email: testMail, + }) + ).resolves.not.toThrow(); + expect(accountSaveSpy).toBeCalledWith(expect.objectContaining({ username: testMail.toLowerCase() })); }); - it('teacher can not access admin of the same school via user id', async () => { - const currentUser = { userId: mockTeacherUser.id } as ICurrentUser; - const params = { type: AccountSearchType.USER_ID, value: mockAdminUser.id } as AccountSearchQueryParams; - await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); + }); + + describe('When using student user', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValue(mockStudentUser); + accountService.findByUserIdOrFail.mockResolvedValue(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + accountService.validatePassword.mockResolvedValue(true); + accountValidationService.isUniqueEmail.mockResolvedValueOnce(true); + + const userUpdateSpy = jest.spyOn(userRepo, 'save'); + + return { mockStudentUser, userUpdateSpy }; + }; + it('should use email as user email in lower case', async () => { + const { mockStudentUser, userUpdateSpy } = setup(); + const testMail = 'AN@AVAILABLE.MAIL'; + await expect( + accountUc.updateMyAccount(mockStudentUser.id, { + passwordOld: defaultPassword, + email: testMail, + }) + ).resolves.not.toThrow(); + expect(userUpdateSpy).toBeCalledWith(expect.objectContaining({ email: testMail.toLowerCase() })); }); - it('teacher can not access any account of a foreign school via user id', async () => { - const currentUser = { userId: mockDifferentSchoolTeacherUser.id } as ICurrentUser; + }); + describe('When using student user', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); - let params = { type: AccountSearchType.USER_ID, value: mockTeacherUser.id } as AccountSearchQueryParams; - await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); + userRepo.findById.mockResolvedValue(mockStudentUser); + accountService.findByUserIdOrFail.mockResolvedValue(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + accountService.validatePassword.mockResolvedValue(true); + accountValidationService.isUniqueEmail.mockResolvedValueOnce(true); - params = { type: AccountSearchType.USER_ID, value: mockStudentUser.id } as AccountSearchQueryParams; - await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); + const accountSaveSpy = jest.spyOn(accountService, 'save'); + const userUpdateSpy = jest.spyOn(userRepo, 'save'); + + return { mockStudentUser, accountSaveSpy, userUpdateSpy }; + }; + it('should always update account user name AND user email together.', async () => { + const { mockStudentUser, accountSaveSpy, userUpdateSpy } = setup(); + const testMail = 'an@available.mail'; + await expect( + accountUc.updateMyAccount(mockStudentUser.id, { + passwordOld: defaultPassword, + email: testMail, + }) + ).resolves.not.toThrow(); + expect(userUpdateSpy).toBeCalledWith(expect.objectContaining({ email: testMail.toLowerCase() })); + expect(accountSaveSpy).toBeCalledWith(expect.objectContaining({ username: testMail.toLowerCase() })); }); - it('teacher can access student of the same school via user id if school has global permission', async () => { - configService.get.mockReturnValue(true); - const currentUser = { userId: mockTeacherNoUserPermissionUser.id } as ICurrentUser; - const params = { - type: AccountSearchType.USER_ID, - value: mockStudentSchoolPermissionUser.id, - } as AccountSearchQueryParams; - await expect(accountUc.searchAccounts(currentUser, params)).resolves.not.toThrow(); + }); + + describe('When using student user', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValue(mockStudentUser); + accountService.findByUserIdOrFail.mockResolvedValue(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + accountService.validatePassword.mockResolvedValue(true); + accountValidationService.isUniqueEmail.mockResolvedValueOnce(false); + + return { mockStudentUser }; + }; + it('should throw if new email already in use', async () => { + const { mockStudentUser } = setup(); + await expect( + accountUc.updateMyAccount(mockStudentUser.id, { + passwordOld: defaultPassword, + email: 'already@in.use', + }) + ).rejects.toThrow(ValidationError); }); - it('teacher can not access student of the same school if school has no global permission', async () => { - configService.get.mockReturnValue(true); - const currentUser = { userId: mockTeacherNoUserNoSchoolPermissionUser.id } as ICurrentUser; - const params = { type: AccountSearchType.USER_ID, value: mockStudentUser.id } as AccountSearchQueryParams; - await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(ForbiddenOperationError); + }); + + describe('When using student user', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValue(mockStudentUser); + accountService.findByUserIdOrFail.mockResolvedValue(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + accountService.validatePassword.mockResolvedValue(true); + + return { mockStudentUser }; + }; + it('should allow to update with strong password', async () => { + const { mockStudentUser } = setup(); + await expect( + accountUc.updateMyAccount(mockStudentUser.id, { + passwordOld: defaultPassword, + passwordNew: otherPassword, + }) + ).resolves.not.toThrow(); }); + }); + + describe('When using teacher user', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockTeacherUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.TEACHER, + permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], + }), + ], + }); + + const mockTeacherAccount = accountFactory.buildWithId({ + userId: mockTeacherUser.id, + password: defaultPasswordHash, + }); - it('student can not access student of the same school if school has global permission', async () => { - configService.get.mockReturnValue(true); - const currentUser = { userId: mockStudentSchoolPermissionUser.id } as ICurrentUser; - const params = { - type: AccountSearchType.USER_ID, - value: mockOtherStudentSchoolPermissionUser.id, - } as AccountSearchQueryParams; - await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(ForbiddenOperationError); + userRepo.findById.mockResolvedValue(mockTeacherUser); + accountService.findByUserIdOrFail.mockResolvedValue(AccountEntityToDtoMapper.mapToDto(mockTeacherAccount)); + accountService.validatePassword.mockResolvedValue(true); + + return { mockTeacherUser }; + }; + it('should allow to update first and last name', async () => { + const { mockTeacherUser } = setup(); + await expect( + accountUc.updateMyAccount(mockTeacherUser.id, { + passwordOld: defaultPassword, + firstName: 'newFirstName', + }) + ).resolves.not.toThrow(); + await expect( + accountUc.updateMyAccount(mockTeacherUser.id, { + passwordOld: defaultPassword, + lastName: 'newLastName', + }) + ).resolves.not.toThrow(); }); - it('student can not access any other account via user id', async () => { - const currentUser = { userId: mockStudentUser.id } as ICurrentUser; + }); + + describe('When using admin user', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockAdminUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.ADMINISTRATOR, + permissions: [ + Permission.TEACHER_EDIT, + Permission.STUDENT_EDIT, + Permission.STUDENT_LIST, + Permission.TEACHER_LIST, + Permission.TEACHER_CREATE, + Permission.STUDENT_CREATE, + Permission.TEACHER_DELETE, + Permission.STUDENT_DELETE, + ], + }), + ], + }); - let params = { type: AccountSearchType.USER_ID, value: mockAdminUser.id } as AccountSearchQueryParams; - await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); + const mockAdminAccount = accountFactory.buildWithId({ + userId: mockAdminUser.id, + password: defaultPasswordHash, + }); - params = { type: AccountSearchType.USER_ID, value: mockTeacherUser.id } as AccountSearchQueryParams; - await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); + userRepo.findById.mockResolvedValue(mockAdminUser); + accountService.findByUserIdOrFail.mockResolvedValue(AccountEntityToDtoMapper.mapToDto(mockAdminAccount)); + accountService.validatePassword.mockResolvedValue(true); - params = { type: AccountSearchType.USER_ID, value: mockStudentUser.id } as AccountSearchQueryParams; - await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); + return { mockAdminUser }; + }; + it('should allow to update first and last name', async () => { + const { mockAdminUser } = setup(); + await expect( + accountUc.updateMyAccount(mockAdminUser.id, { + passwordOld: defaultPassword, + firstName: 'newFirstName', + }) + ).resolves.not.toThrow(); + await expect( + accountUc.updateMyAccount(mockAdminUser.id, { + passwordOld: defaultPassword, + lastName: 'newLastName', + }) + ).resolves.not.toThrow(); }); - it('superhero can access any account via username', async () => { - const currentUser = { userId: mockSuperheroUser.id } as ICurrentUser; + }); + + describe('When using superhero user', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockSuperheroUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.SUPERHERO, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }), + ], + }); + const mockSuperheroAccount = accountFactory.buildWithId({ + userId: mockSuperheroUser.id, + password: defaultPasswordHash, + }); - let params = { type: AccountSearchType.USERNAME, value: mockAdminAccount.username } as AccountSearchQueryParams; - await expect(accountUc.searchAccounts(currentUser, params)).resolves.not.toThrow(); + userRepo.findById.mockResolvedValue(mockSuperheroUser); + accountService.findByUserIdOrFail.mockResolvedValue(AccountEntityToDtoMapper.mapToDto(mockSuperheroAccount)); + accountService.validatePassword.mockResolvedValue(true); - params = { type: AccountSearchType.USERNAME, value: mockTeacherAccount.username } as AccountSearchQueryParams; - await expect(accountUc.searchAccounts(currentUser, params)).resolves.not.toThrow(); + return { mockSuperheroUser }; + }; + it('should allow to update first and last name ', async () => { + const { mockSuperheroUser } = setup(); + await expect( + accountUc.updateMyAccount(mockSuperheroUser.id, { + passwordOld: defaultPassword, + firstName: 'newFirstName', + }) + ).resolves.not.toThrow(); + await expect( + accountUc.updateMyAccount(mockSuperheroUser.id, { + passwordOld: defaultPassword, + lastName: 'newLastName', + }) + ).resolves.not.toThrow(); + }); + }); - params = { type: AccountSearchType.USERNAME, value: mockStudentAccount.username } as AccountSearchQueryParams; - await expect(accountUc.searchAccounts(currentUser, params)).resolves.not.toThrow(); + describe('When user can not be updated', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); - params = { - type: AccountSearchType.USERNAME, - value: mockDifferentSchoolAdminAccount.username, - } as AccountSearchQueryParams; - await expect(accountUc.searchAccounts(currentUser, params)).resolves.not.toThrow(); + const mockTeacherUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.TEACHER, + permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], + }), + ], + }); + const mockTeacherAccount = accountFactory.buildWithId({ + userId: mockTeacherUser.id, + password: defaultPasswordHash, + }); - params = { - type: AccountSearchType.USERNAME, - value: mockDifferentSchoolTeacherAccount.username, - } as AccountSearchQueryParams; - await expect(accountUc.searchAccounts(currentUser, params)).resolves.not.toThrow(); + userRepo.findById.mockResolvedValue(mockTeacherUser); + userRepo.save.mockRejectedValueOnce(undefined); + accountService.findByUserIdOrFail.mockResolvedValue(AccountEntityToDtoMapper.mapToDto(mockTeacherAccount)); + accountService.validatePassword.mockResolvedValue(true); - params = { - type: AccountSearchType.USERNAME, - value: mockDifferentSchoolStudentAccount.username, - } as AccountSearchQueryParams; - await expect(accountUc.searchAccounts(currentUser, params)).resolves.not.toThrow(); + return { mockTeacherUser, mockTeacherAccount }; + }; + it('should throw EntityNotFoundError', async () => { + const { mockTeacherUser } = setup(); + await expect( + accountUc.updateMyAccount(mockTeacherUser.id, { + passwordOld: defaultPassword, + firstName: 'failToUpdate', + }) + ).rejects.toThrow(EntityNotFoundError); }); }); - }); - describe('findAccountById', () => { - it('should return an account, if the current user is a superhero', async () => { - const account = await accountUc.findAccountById( - { userId: mockSuperheroUser.id } as ICurrentUser, - { id: mockStudentAccount.id } as AccountByIdParams - ); - expect(account).toStrictEqual( - expect.objectContaining({ - id: mockStudentAccount.id, - username: mockStudentAccount.username, + describe('When account can not be updated', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const mockStudentAccount = accountFactory.buildWithId({ userId: mockStudentUser.id, - activated: mockStudentAccount.activated, - }) - ); - }); - it('should throw, if the current user is no superhero', async () => { - await expect( - accountUc.findAccountById( - { userId: mockTeacherUser.id } as ICurrentUser, - { id: mockStudentAccount.id } as AccountByIdParams - ) - ).rejects.toThrow(ForbiddenOperationError); - }); - it('should throw, if no account matches the search term', async () => { - await expect( - accountUc.findAccountById({ userId: mockSuperheroUser.id } as ICurrentUser, { id: 'xxx' } as AccountByIdParams) - ).rejects.toThrow(EntityNotFoundError); - }); - it('should throw, if target account has no user', async () => { - await expect( - accountUc.findAccountById({ userId: mockSuperheroUser.id } as ICurrentUser, { id: 'xxx' } as AccountByIdParams) - ).rejects.toThrow(EntityNotFoundError); - }); - }); + password: defaultPasswordHash, + }); - describe('saveAccount', () => { - afterEach(() => { - jest.clearAllMocks(); + userRepo.findById.mockResolvedValue(mockStudentUser); + userRepo.save.mockResolvedValueOnce(undefined); + accountService.findByUserIdOrFail.mockResolvedValue(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + accountService.validatePassword.mockResolvedValue(true); + accountService.save.mockRejectedValueOnce(undefined); + + accountValidationService.isUniqueEmail.mockResolvedValue(true); + + return { mockStudentUser }; + }; + it('should throw EntityNotFoundError', async () => { + const { mockStudentUser } = setup(); + await expect( + accountUc.updateMyAccount(mockStudentUser.id, { + passwordOld: defaultPassword, + email: 'fail@to.update', + }) + ).rejects.toThrow(EntityNotFoundError); + }); }); - it('should call account service', async () => { - const spy = jest.spyOn(accountService, 'saveWithValidation'); - const params: AccountSaveDto = { - username: 'john.doe@domain.tld', - password: defaultPassword, + describe('When no new password is given', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValue(mockStudentUser); + accountService.findByUserIdOrFail.mockResolvedValue(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + accountService.validatePassword.mockResolvedValue(true); + const spyAccountServiceSave = jest.spyOn(accountService, 'save'); + + accountValidationService.isUniqueEmail.mockResolvedValue(true); + + return { mockStudentUser, spyAccountServiceSave }; }; - await accountUc.saveAccount(params); - expect(spy).toHaveBeenCalledWith( - expect.objectContaining({ - username: 'john.doe@domain.tld', - }) - ); + it('should not update password', async () => { + const { mockStudentUser, spyAccountServiceSave } = setup(); + await accountUc.updateMyAccount(mockStudentUser.id, { + passwordOld: defaultPassword, + passwordNew: undefined, + email: 'newemail@to.update', + }); + expect(spyAccountServiceSave).toHaveBeenCalledWith( + expect.objectContaining({ + password: undefined, + }) + ); + }); }); }); - describe('updateAccountById', () => { - it('should throw if executing user does not exist', async () => { - const currentUser = { userId: '000000000000000' } as ICurrentUser; - const params = { id: mockStudentAccount.id } as AccountByIdParams; - const body = {} as AccountByIdBodyParams; - await expect(accountUc.updateAccountById(currentUser, params, body)).rejects.toThrow(EntityNotFoundError); - }); - it('should throw if target account does not exist', async () => { - const currentUser = { userId: mockAdminUser.id } as ICurrentUser; - const params = { id: '000000000000000' } as AccountByIdParams; - const body = {} as AccountByIdBodyParams; - await expect(accountUc.updateAccountById(currentUser, params, body)).rejects.toThrow(EntityNotFoundError); - }); - it('should update target account password', async () => { - const previousPasswordHash = mockStudentAccount.password; - const currentUser = { userId: mockSuperheroUser.id } as ICurrentUser; - const params = { id: mockStudentAccount.id } as AccountByIdParams; - const body = { password: defaultPassword } as AccountByIdBodyParams; - expect(mockStudentUser.forcePasswordChange).toBeFalsy(); - await accountUc.updateAccountById(currentUser, params, body); - expect(mockStudentAccount.password).not.toBe(previousPasswordHash); - expect(mockStudentUser.forcePasswordChange).toBeTruthy(); - }); - it('should update target account username', async () => { - const newUsername = 'newUsername'; - const currentUser = { userId: mockSuperheroUser.id } as ICurrentUser; - const params = { id: mockStudentAccount.id } as AccountByIdParams; - const body = { username: newUsername } as AccountByIdBodyParams; - expect(mockStudentAccount.username).not.toBe(newUsername); - await accountUc.updateAccountById(currentUser, params, body); - expect(mockStudentAccount.username).toBe(newUsername.toLowerCase()); - }); - it('should update target account activation state', async () => { - const currentUser = { userId: mockSuperheroUser.id } as ICurrentUser; - const params = { id: mockStudentAccount.id } as AccountByIdParams; - const body = { activated: false } as AccountByIdBodyParams; - await accountUc.updateAccountById(currentUser, params, body); - expect(mockStudentAccount.activated).toBeFalsy(); - }); - it('should throw if account can not be updated', async () => { - const currentUser = { userId: mockAdminUser.id } as ICurrentUser; - const params = { id: mockStudentAccount.id } as AccountByIdParams; - const body = { username: 'fail@to.update' } as AccountByIdBodyParams; - await expect(accountUc.updateAccountById(currentUser, params, body)).rejects.toThrow(EntityNotFoundError); - }); - it('should throw if user can not be updated', async () => { - const currentUser = { userId: mockAdminUser.id } as ICurrentUser; - const params = { id: mockStudentAccount.id } as AccountByIdParams; - const body = { username: 'user-fail@to.update' } as AccountByIdBodyParams; - await expect(accountUc.updateAccountById(currentUser, params, body)).rejects.toThrow(EntityNotFoundError); + describe('replaceMyTemporaryPassword', () => { + describe('When passwords do not match', () => { + it('should throw ForbiddenOperationError', async () => { + await expect(accountUc.replaceMyTemporaryPassword('userId', defaultPassword, 'FooPasswd!1')).rejects.toThrow( + ForbiddenOperationError + ); + }); }); - it('should throw if target account has no user', async () => { - await expect( - accountUc.updateAccountById( - { userId: mockSuperheroUser.id } as ICurrentUser, - { id: mockAccountWithoutUser.id } as AccountByIdParams, - { username: 'user-fail@to.update' } as AccountByIdBodyParams - ) - ).rejects.toThrow(EntityNotFoundError); + + describe('When account does not exist', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + const mockUserWithoutAccount = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.ADMINISTRATOR, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }), + ], + }); + + userRepo.findById.mockResolvedValueOnce(mockUserWithoutAccount); + accountService.findByUserIdOrFail.mockImplementation(() => { + throw new EntityNotFoundError(Account.name); + }); + + return { mockUserWithoutAccount }; + }; + it('should throw EntityNotFoundError', async () => { + const { mockUserWithoutAccount } = setup(); + await expect( + accountUc.replaceMyTemporaryPassword(mockUserWithoutAccount.id, defaultPassword, defaultPassword) + ).rejects.toThrow(EntityNotFoundError); + }); }); - it('should throw if new username already in use', async () => { - const accountIsUniqueEmailSpy = jest.spyOn(accountValidationService, 'isUniqueEmail'); - accountIsUniqueEmailSpy.mockResolvedValueOnce(false); - const currentUser = { userId: mockAdminUser.id } as ICurrentUser; - const params = { id: mockStudentAccount.id } as AccountByIdParams; - const body = { username: mockOtherTeacherAccount.username } as AccountByIdBodyParams; - await expect(accountUc.updateAccountById(currentUser, params, body)).rejects.toThrow(ValidationError); + + describe('When user does not exist', () => { + const setup = () => { + userRepo.findById.mockRejectedValueOnce(undefined); + }; + it('should throw EntityNotFoundError', async () => { + setup(); + await expect( + accountUc.replaceMyTemporaryPassword('accountWithoutUser', defaultPassword, defaultPassword) + ).rejects.toThrow(EntityNotFoundError); + }); }); - describe('hasPermissionsToUpdateAccount', () => { - it('admin can edit teacher', async () => { - const currentUser = { userId: mockAdminUser.id } as ICurrentUser; - const params = { id: mockTeacherAccount.id } as AccountByIdParams; - const body = {} as AccountByIdBodyParams; - await expect(accountUc.updateAccountById(currentUser, params, body)).resolves.not.toThrow(); + describe('When account is external', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockExternalUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const externalSystem = systemFactory.buildWithId(); + const mockExternalUserAccount = accountFactory.buildWithId({ + userId: mockExternalUser.id, + password: defaultPasswordHash, + systemId: externalSystem.id, + }); + + userRepo.findById.mockResolvedValueOnce(mockExternalUser); + accountService.findByUserIdOrFail.mockResolvedValueOnce( + AccountEntityToDtoMapper.mapToDto(mockExternalUserAccount) + ); + + return { mockExternalUserAccount }; + }; + it('should throw ForbiddenOperationError', async () => { + const { mockExternalUserAccount } = setup(); + await expect( + accountUc.replaceMyTemporaryPassword( + mockExternalUserAccount.userId?.toString() ?? '', + defaultPassword, + defaultPassword + ) + ).rejects.toThrow(ForbiddenOperationError); }); - it('teacher can edit student', async () => { - const currentUser = { userId: mockTeacherUser.id } as ICurrentUser; + }); + describe('When not the users password is temporary', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + forcePasswordChange: false, + preferences: { firstLogin: true }, + }); + + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValueOnce(mockStudentUser); + accountService.findByUserIdOrFail.mockResolvedValueOnce(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + + return { mockStudentAccount }; + }; + it('should throw ForbiddenOperationError', async () => { + const { mockStudentAccount } = setup(); + await expect( + accountUc.replaceMyTemporaryPassword( + mockStudentAccount.userId?.toString() ?? '', + defaultPassword, + defaultPassword + ) + ).rejects.toThrow(ForbiddenOperationError); + }); + }); + + describe('When old password is the same as new password', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + forcePasswordChange: false, + preferences: { firstLogin: false }, + }); + + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValueOnce(mockStudentUser); + accountService.findByUserIdOrFail.mockResolvedValueOnce(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + accountService.validatePassword.mockResolvedValueOnce(true); + + return { mockStudentAccount }; + }; + it('should throw ForbiddenOperationError', async () => { + const { mockStudentAccount } = setup(); + await expect( + accountUc.replaceMyTemporaryPassword( + mockStudentAccount.userId?.toString() ?? '', + defaultPassword, + defaultPassword + ) + ).rejects.toThrow(ForbiddenOperationError); + }); + }); + + describe('When old password is undefined', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + forcePasswordChange: false, + preferences: { firstLogin: false }, + }); + + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: undefined, + }); + + userRepo.findById.mockResolvedValueOnce(mockStudentUser); + accountService.findByUserIdOrFail.mockResolvedValueOnce(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + accountService.validatePassword.mockResolvedValueOnce(true); + + return { mockStudentAccount }; + }; + it('should throw Error', async () => { + const { mockStudentAccount } = setup(); + await expect( + accountUc.replaceMyTemporaryPassword( + mockStudentAccount.userId?.toString() ?? '', + defaultPassword, + defaultPassword + ) + ).rejects.toThrow(Error); + }); + }); + + describe('When the admin manipulated the users password', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + forcePasswordChange: true, + preferences: { firstLogin: true }, + }); + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValueOnce(mockStudentUser); + accountService.findByUserIdOrFail.mockResolvedValueOnce(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + accountService.validatePassword.mockResolvedValueOnce(false); + return { mockStudentAccount }; + }; + it('should allow to set strong password', async () => { + const { mockStudentAccount } = setup(); + + await expect( + accountUc.replaceMyTemporaryPassword( + mockStudentAccount.userId?.toString() ?? '', + otherPassword, + otherPassword + ) + ).resolves.not.toThrow(); + }); + }); + + describe('When this is the users first login', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + forcePasswordChange: false, + preferences: { firstLogin: false }, + }); + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValueOnce(mockStudentUser); + accountService.findByUserIdOrFail.mockResolvedValueOnce(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + accountService.validatePassword.mockResolvedValueOnce(false); + + return { mockStudentAccount }; + }; + it('should allow to set strong password', async () => { + const { mockStudentAccount } = setup(); + + await expect( + accountUc.replaceMyTemporaryPassword( + mockStudentAccount.userId?.toString() ?? '', + otherPassword, + otherPassword + ) + ).resolves.not.toThrow(); + }); + }); + + describe('When this is the users first login (if undefined)', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + forcePasswordChange: false, + }); + mockStudentUser.preferences = undefined; + + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValueOnce(mockStudentUser); + accountService.findByUserIdOrFail.mockResolvedValueOnce(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + accountService.validatePassword.mockResolvedValueOnce(false); + + return { mockStudentAccount }; + }; + it('should allow to set strong password', async () => { + const { mockStudentAccount } = setup(); + await expect( + accountUc.replaceMyTemporaryPassword( + mockStudentAccount.userId?.toString() ?? '', + otherPassword, + otherPassword + ) + ).resolves.not.toThrow(); + }); + }); + + describe('When user can not be updated', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + firstName: 'failToUpdate', + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + preferences: { firstLogin: false }, + forcePasswordChange: false, + }); + + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValueOnce(mockStudentUser); + userRepo.save.mockRejectedValueOnce(undefined); + accountService.findByUserIdOrFail.mockResolvedValueOnce(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + accountService.validatePassword.mockResolvedValueOnce(false); + + return { mockStudentAccount }; + }; + it('should throw EntityNotFoundError', async () => { + const { mockStudentAccount } = setup(); + await expect( + accountUc.replaceMyTemporaryPassword( + mockStudentAccount.userId?.toString() ?? '', + otherPassword, + otherPassword + ) + ).rejects.toThrow(EntityNotFoundError); + }); + }); + + describe('When account can not be updated', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + forcePasswordChange: false, + preferences: { firstLogin: false }, + }); + + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + username: 'fail@to.update', + }); + + userRepo.findById.mockResolvedValueOnce(mockStudentUser); + userRepo.save.mockResolvedValueOnce(); + accountService.findByUserIdOrFail.mockResolvedValueOnce(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + accountService.save.mockRejectedValueOnce(undefined); + accountService.validatePassword.mockResolvedValueOnce(false); + + return { mockStudentAccount }; + }; + it('should throw EntityNotFoundError', async () => { + const { mockStudentAccount } = setup(); + await expect( + accountUc.replaceMyTemporaryPassword( + mockStudentAccount.userId?.toString() ?? '', + otherPassword, + otherPassword + ) + ).rejects.toThrow(EntityNotFoundError); + }); + }); + }); + + describe('searchAccounts', () => { + describe('When search type is userId', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + const mockSchoolWithStudentVisibility = schoolFactory.buildWithId(); + mockSchoolWithStudentVisibility.permissions = new SchoolRoles(); + mockSchoolWithStudentVisibility.permissions.teacher = new SchoolRolePermission(); + mockSchoolWithStudentVisibility.permissions.teacher.STUDENT_LIST = true; + + const mockSuperheroUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.SUPERHERO, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }), + ], + }); + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValueOnce(mockSuperheroUser).mockResolvedValueOnce(mockStudentUser); + accountService.findByUserId.mockResolvedValueOnce(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + + return { mockSuperheroUser, mockStudentUser, mockStudentAccount }; + }; + it('should return one account', async () => { + const { mockSuperheroUser, mockStudentUser, mockStudentAccount } = setup(); + const accounts = await accountUc.searchAccounts( + { userId: mockSuperheroUser.id } as ICurrentUser, + { type: AccountSearchType.USER_ID, value: mockStudentUser.id } as AccountSearchQueryParams + ); + const expected = new AccountSearchListResponse( + [AccountResponseMapper.mapToResponseFromEntity(mockStudentAccount)], + 1, + 0, + 1 + ); + expect(accounts).toStrictEqual(expected); + }); + }); + + describe('When account is not found', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockSuperheroUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.SUPERHERO, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }), + ], + }); + const mockUserWithoutAccount = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.ADMINISTRATOR, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }), + ], + }); + + userRepo.findById.mockResolvedValueOnce(mockSuperheroUser).mockResolvedValueOnce(mockUserWithoutAccount); + + return { mockSuperheroUser, mockUserWithoutAccount }; + }; + it('should return empty list', async () => { + const { mockSuperheroUser, mockUserWithoutAccount } = setup(); + const accounts = await accountUc.searchAccounts( + { userId: mockSuperheroUser.id } as ICurrentUser, + { type: AccountSearchType.USER_ID, value: mockUserWithoutAccount.id } as AccountSearchQueryParams + ); + const expected = new AccountSearchListResponse([], 0, 0, 0); + expect(accounts).toStrictEqual(expected); + }); + }); + describe('When search type is username', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockSuperheroUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.SUPERHERO, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }), + ], + }); + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValueOnce(mockSuperheroUser).mockResolvedValueOnce(mockSuperheroUser); + accountService.searchByUsernamePartialMatch.mockResolvedValueOnce([ + [ + AccountEntityToDtoMapper.mapToDto(mockStudentAccount), + AccountEntityToDtoMapper.mapToDto(mockStudentAccount), + ], + 2, + ]); + + return { mockSuperheroUser }; + }; + it('should return one or more accounts, ', async () => { + const { mockSuperheroUser } = setup(); + const accounts = await accountUc.searchAccounts( + { userId: mockSuperheroUser.id } as ICurrentUser, + { type: AccountSearchType.USERNAME, value: '' } as AccountSearchQueryParams + ); + expect(accounts.skip).toEqual(0); + expect(accounts.limit).toEqual(10); + expect(accounts.total).toBeGreaterThan(1); + expect(accounts.data.length).toBeGreaterThan(1); + }); + }); + + describe('When user has not the right permissions', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockAdminUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.ADMINISTRATOR, + permissions: [ + Permission.TEACHER_EDIT, + Permission.STUDENT_EDIT, + Permission.STUDENT_LIST, + Permission.TEACHER_LIST, + Permission.TEACHER_CREATE, + Permission.STUDENT_CREATE, + Permission.TEACHER_DELETE, + Permission.STUDENT_DELETE, + ], + }), + ], + }); + const mockTeacherUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.TEACHER, + permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], + }), + ], + }); + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const mockOtherStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + + userRepo.findById.mockResolvedValueOnce(mockTeacherUser).mockResolvedValueOnce(mockAdminUser); + userRepo.findById.mockResolvedValueOnce(mockStudentUser).mockResolvedValueOnce(mockOtherStudentUser); + userRepo.findById.mockResolvedValueOnce(mockStudentUser).mockResolvedValueOnce(mockTeacherUser); + + return { mockTeacherUser, mockAdminUser, mockStudentUser, mockOtherStudentUser }; + }; + it('should throw ForbiddenOperationError', async () => { + const { mockTeacherUser, mockAdminUser, mockStudentUser, mockOtherStudentUser } = setup(); + await expect( + accountUc.searchAccounts( + { userId: mockTeacherUser.id } as ICurrentUser, + { type: AccountSearchType.USER_ID, value: mockAdminUser.id } as AccountSearchQueryParams + ) + ).rejects.toThrow(ForbiddenOperationError); + + await expect( + accountUc.searchAccounts( + { userId: mockStudentUser.id } as ICurrentUser, + { type: AccountSearchType.USER_ID, value: mockOtherStudentUser.id } as AccountSearchQueryParams + ) + ).rejects.toThrow(ForbiddenOperationError); + + await expect( + accountUc.searchAccounts( + { userId: mockStudentUser.id } as ICurrentUser, + { type: AccountSearchType.USER_ID, value: mockTeacherUser.id } as AccountSearchQueryParams + ) + ).rejects.toThrow(ForbiddenOperationError); + }); + }); + + describe('When search type is unknown', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockSuperheroUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.SUPERHERO, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }), + ], + }); + + userRepo.findById.mockResolvedValueOnce(mockSuperheroUser); + + return { mockSuperheroUser }; + }; + it('should throw', async () => { + const { mockSuperheroUser } = setup(); + await expect( + accountUc.searchAccounts( + { userId: mockSuperheroUser.id } as ICurrentUser, + { type: '' as AccountSearchType } as AccountSearchQueryParams + ) + ).rejects.toThrow('Invalid search type.'); + }); + }); + + describe('When user is no superhero', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockTeacherUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.TEACHER, + permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], + }), + ], + }); + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + + userRepo.findById.mockResolvedValueOnce(mockTeacherUser).mockResolvedValueOnce(mockStudentUser); + + return { mockStudentUser, mockTeacherUser }; + }; + it('should throw ForbiddenOperationError', async () => { + const { mockTeacherUser, mockStudentUser } = setup(); + await expect( + accountUc.searchAccounts( + { userId: mockTeacherUser.id } as ICurrentUser, + { type: AccountSearchType.USERNAME, value: mockStudentUser.id } as AccountSearchQueryParams + ) + ).rejects.toThrow(ForbiddenOperationError); + }); + }); + describe('hasPermissionsToAccessAccount', () => { + describe('When using an admin', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockAdminUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.ADMINISTRATOR, + permissions: [ + Permission.TEACHER_EDIT, + Permission.STUDENT_EDIT, + Permission.STUDENT_LIST, + Permission.TEACHER_LIST, + Permission.TEACHER_CREATE, + Permission.STUDENT_CREATE, + Permission.TEACHER_DELETE, + Permission.STUDENT_DELETE, + ], + }), + ], + }); + const mockTeacherUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.TEACHER, + permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], + }), + ], + }); + + configService.get.mockReturnValue(false); + userRepo.findById.mockResolvedValueOnce(mockAdminUser).mockResolvedValueOnce(mockTeacherUser); + + return { mockAdminUser, mockTeacherUser }; + }; + it('should be able to access teacher of the same school via user id', async () => { + const { mockAdminUser, mockTeacherUser } = setup(); + const currentUser = { userId: mockAdminUser.id } as ICurrentUser; + const params = { type: AccountSearchType.USER_ID, value: mockTeacherUser.id } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).resolves.not.toThrow(); + }); + }); + + describe('When using an admin', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockAdminUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.ADMINISTRATOR, + permissions: [ + Permission.TEACHER_EDIT, + Permission.STUDENT_EDIT, + Permission.STUDENT_LIST, + Permission.TEACHER_LIST, + Permission.TEACHER_CREATE, + Permission.STUDENT_CREATE, + Permission.TEACHER_DELETE, + Permission.STUDENT_DELETE, + ], + }), + ], + }); + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + + configService.get.mockReturnValue(false); + userRepo.findById.mockResolvedValueOnce(mockAdminUser).mockResolvedValueOnce(mockStudentUser); + + return { mockAdminUser, mockStudentUser }; + }; + it('should be able to access student of the same school via user id', async () => { + const { mockAdminUser, mockStudentUser } = setup(); + const currentUser = { userId: mockAdminUser.id } as ICurrentUser; + const params = { type: AccountSearchType.USER_ID, value: mockStudentUser.id } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).resolves.not.toThrow(); + }); + }); + + describe('When using an admin', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockAdminUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.ADMINISTRATOR, + permissions: [ + Permission.TEACHER_EDIT, + Permission.STUDENT_EDIT, + Permission.STUDENT_LIST, + Permission.TEACHER_LIST, + Permission.TEACHER_CREATE, + Permission.STUDENT_CREATE, + Permission.TEACHER_DELETE, + Permission.STUDENT_DELETE, + ], + }), + ], + }); + + configService.get.mockReturnValue(false); + userRepo.findById.mockResolvedValueOnce(mockAdminUser).mockResolvedValueOnce(mockAdminUser); + + return { mockAdminUser }; + }; + + it('should not be able to access admin of the same school via user id', async () => { + const { mockAdminUser } = setup(); + const currentUser = { userId: mockAdminUser.id } as ICurrentUser; + const params = { type: AccountSearchType.USER_ID, value: mockAdminUser.id } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); + }); + }); + + describe('When using an admin', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + const mockOtherSchool = schoolFactory.buildWithId(); + + const mockAdminUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.ADMINISTRATOR, + permissions: [ + Permission.TEACHER_EDIT, + Permission.STUDENT_EDIT, + Permission.STUDENT_LIST, + Permission.TEACHER_LIST, + Permission.TEACHER_CREATE, + Permission.STUDENT_CREATE, + Permission.TEACHER_DELETE, + Permission.STUDENT_DELETE, + ], + }), + ], + }); + const mockTeacherUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.TEACHER, + permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], + }), + ], + }); + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const mockDifferentSchoolAdminUser = userFactory.buildWithId({ + school: mockOtherSchool, + roles: [...mockAdminUser.roles], + }); + + configService.get.mockReturnValue(false); + userRepo.findById.mockResolvedValueOnce(mockDifferentSchoolAdminUser).mockResolvedValueOnce(mockTeacherUser); + userRepo.findById.mockResolvedValueOnce(mockDifferentSchoolAdminUser).mockResolvedValueOnce(mockStudentUser); + + return { mockDifferentSchoolAdminUser, mockTeacherUser, mockStudentUser }; + }; + it('should not be able to access any account of a foreign school via user id', async () => { + const { mockDifferentSchoolAdminUser, mockTeacherUser, mockStudentUser } = setup(); + const currentUser = { userId: mockDifferentSchoolAdminUser.id } as ICurrentUser; + + let params = { type: AccountSearchType.USER_ID, value: mockTeacherUser.id } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); + + params = { type: AccountSearchType.USER_ID, value: mockStudentUser.id } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); + }); + }); + + describe('When using a teacher', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockTeacherUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.TEACHER, + permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], + }), + ], + }); + const mockOtherTeacherUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.TEACHER, + permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], + }), + ], + }); + + configService.get.mockReturnValue(false); + userRepo.findById.mockResolvedValueOnce(mockTeacherUser).mockResolvedValueOnce(mockOtherTeacherUser); + + return { mockTeacherUser, mockOtherTeacherUser }; + }; + it('should be able to access teacher of the same school via user id', async () => { + const { mockTeacherUser, mockOtherTeacherUser } = setup(); + const currentUser = { userId: mockTeacherUser.id } as ICurrentUser; + const params = { + type: AccountSearchType.USER_ID, + value: mockOtherTeacherUser.id, + } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).resolves.not.toThrow(); + }); + }); + + describe('When using a teacher', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockTeacherUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.TEACHER, + permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], + }), + ], + }); + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + + configService.get.mockReturnValue(false); + userRepo.findById.mockResolvedValueOnce(mockTeacherUser).mockResolvedValueOnce(mockStudentUser); + + return { mockTeacherUser, mockStudentUser }; + }; + it('should be able to access student of the same school via user id', async () => { + const { mockTeacherUser, mockStudentUser } = setup(); + const currentUser = { userId: mockTeacherUser.id } as ICurrentUser; + const params = { type: AccountSearchType.USER_ID, value: mockStudentUser.id } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).resolves.not.toThrow(); + }); + }); + + describe('When using a teacher', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockAdminUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.ADMINISTRATOR, + permissions: [ + Permission.TEACHER_EDIT, + Permission.STUDENT_EDIT, + Permission.STUDENT_LIST, + Permission.TEACHER_LIST, + Permission.TEACHER_CREATE, + Permission.STUDENT_CREATE, + Permission.TEACHER_DELETE, + Permission.STUDENT_DELETE, + ], + }), + ], + }); + const mockTeacherUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.TEACHER, + permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], + }), + ], + }); + + configService.get.mockReturnValue(false); + userRepo.findById.mockResolvedValueOnce(mockTeacherUser).mockResolvedValueOnce(mockAdminUser); + + return { mockTeacherUser, mockAdminUser }; + }; + it('should not be able to access admin of the same school via user id', async () => { + const { mockTeacherUser, mockAdminUser } = setup(); + const currentUser = { userId: mockTeacherUser.id } as ICurrentUser; + const params = { type: AccountSearchType.USER_ID, value: mockAdminUser.id } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); + }); + }); + + describe('When using a teacher', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + const mockOtherSchool = schoolFactory.buildWithId(); + + const mockTeacherUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.TEACHER, + permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], + }), + ], + }); + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const mockDifferentSchoolTeacherUser = userFactory.buildWithId({ + school: mockOtherSchool, + roles: [...mockTeacherUser.roles], + }); + + configService.get.mockReturnValue(false); + userRepo.findById + .mockResolvedValueOnce(mockDifferentSchoolTeacherUser) + .mockResolvedValueOnce(mockTeacherUser); + userRepo.findById + .mockResolvedValueOnce(mockDifferentSchoolTeacherUser) + .mockResolvedValueOnce(mockStudentUser); + + return { mockDifferentSchoolTeacherUser, mockTeacherUser, mockStudentUser }; + }; + it('should not be able to access any account of a foreign school via user id', async () => { + const { mockDifferentSchoolTeacherUser, mockTeacherUser, mockStudentUser } = setup(); + const currentUser = { userId: mockDifferentSchoolTeacherUser.id } as ICurrentUser; + + let params = { type: AccountSearchType.USER_ID, value: mockTeacherUser.id } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); + + params = { type: AccountSearchType.USER_ID, value: mockStudentUser.id } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); + }); + }); + describe('When using a teacher', () => { + const setup = () => { + const mockSchoolWithStudentVisibility = schoolFactory.buildWithId(); + mockSchoolWithStudentVisibility.permissions = new SchoolRoles(); + mockSchoolWithStudentVisibility.permissions.teacher = new SchoolRolePermission(); + mockSchoolWithStudentVisibility.permissions.teacher.STUDENT_LIST = true; + + const mockTeacherNoUserPermissionUser = userFactory.buildWithId({ + school: mockSchoolWithStudentVisibility, + roles: [ + new Role({ + name: RoleName.TEACHER, + permissions: [], + }), + ], + }); + const mockStudentSchoolPermissionUser = userFactory.buildWithId({ + school: mockSchoolWithStudentVisibility, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + + configService.get.mockReturnValue(true); + userRepo.findById + .mockResolvedValueOnce(mockTeacherNoUserPermissionUser) + .mockResolvedValueOnce(mockStudentSchoolPermissionUser); + + return { mockTeacherNoUserPermissionUser, mockStudentSchoolPermissionUser }; + }; + it('should be able to access student of the same school via user id if school has global permission', async () => { + const { mockTeacherNoUserPermissionUser, mockStudentSchoolPermissionUser } = setup(); + + const currentUser = { userId: mockTeacherNoUserPermissionUser.id } as ICurrentUser; + const params = { + type: AccountSearchType.USER_ID, + value: mockStudentSchoolPermissionUser.id, + } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).resolves.not.toThrow(); + }); + }); + + describe('When using a teacher', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockTeacherNoUserNoSchoolPermissionUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.TEACHER, + permissions: [], + }), + ], + }); + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + + configService.get.mockReturnValue(false); + userRepo.findById + .mockResolvedValueOnce(mockTeacherNoUserNoSchoolPermissionUser) + .mockResolvedValueOnce(mockStudentUser); + + return { mockTeacherNoUserNoSchoolPermissionUser, mockStudentUser }; + }; + it('should not be able to access student of the same school if school has no global permission', async () => { + const { mockTeacherNoUserNoSchoolPermissionUser, mockStudentUser } = setup(); + configService.get.mockReturnValue(true); + const currentUser = { userId: mockTeacherNoUserNoSchoolPermissionUser.id } as ICurrentUser; + const params = { type: AccountSearchType.USER_ID, value: mockStudentUser.id } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(ForbiddenOperationError); + }); + }); + + describe('When using a student', () => { + const setup = () => { + const mockSchoolWithStudentVisibility = schoolFactory.buildWithId(); + mockSchoolWithStudentVisibility.permissions = new SchoolRoles(); + mockSchoolWithStudentVisibility.permissions.teacher = new SchoolRolePermission(); + mockSchoolWithStudentVisibility.permissions.teacher.STUDENT_LIST = true; + + const mockStudentSchoolPermissionUser = userFactory.buildWithId({ + school: mockSchoolWithStudentVisibility, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const mockOtherStudentSchoolPermissionUser = userFactory.buildWithId({ + school: mockSchoolWithStudentVisibility, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + + configService.get.mockReturnValue(false); + userRepo.findById + .mockResolvedValueOnce(mockStudentSchoolPermissionUser) + .mockResolvedValueOnce(mockOtherStudentSchoolPermissionUser); + + return { mockStudentSchoolPermissionUser, mockOtherStudentSchoolPermissionUser }; + }; + it('should not be able to access student of the same school if school has global permission', async () => { + const { mockStudentSchoolPermissionUser, mockOtherStudentSchoolPermissionUser } = setup(); + configService.get.mockReturnValue(true); + const currentUser = { userId: mockStudentSchoolPermissionUser.id } as ICurrentUser; + const params = { + type: AccountSearchType.USER_ID, + value: mockOtherStudentSchoolPermissionUser.id, + } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(ForbiddenOperationError); + }); + }); + + describe('When using a student', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockAdminUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.ADMINISTRATOR, + permissions: [ + Permission.TEACHER_EDIT, + Permission.STUDENT_EDIT, + Permission.STUDENT_LIST, + Permission.TEACHER_LIST, + Permission.TEACHER_CREATE, + Permission.STUDENT_CREATE, + Permission.TEACHER_DELETE, + Permission.STUDENT_DELETE, + ], + }), + ], + }); + const mockTeacherUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.TEACHER, + permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], + }), + ], + }); + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + + configService.get.mockReturnValue(false); + userRepo.findById.mockResolvedValueOnce(mockStudentUser).mockResolvedValueOnce(mockAdminUser); + userRepo.findById.mockResolvedValueOnce(mockStudentUser).mockResolvedValueOnce(mockTeacherUser); + userRepo.findById.mockResolvedValueOnce(mockStudentUser).mockResolvedValueOnce(mockStudentUser); + + return { mockStudentUser, mockAdminUser, mockTeacherUser }; + }; + it('should not be able to access any other account via user id', async () => { + const { mockStudentUser, mockAdminUser, mockTeacherUser } = setup(); + const currentUser = { userId: mockStudentUser.id } as ICurrentUser; + + let params = { type: AccountSearchType.USER_ID, value: mockAdminUser.id } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); + + params = { type: AccountSearchType.USER_ID, value: mockTeacherUser.id } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); + + params = { type: AccountSearchType.USER_ID, value: mockStudentUser.id } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); + }); + }); + + describe('When using a superhero', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + const mockOtherSchool = schoolFactory.buildWithId(); + + const mockSuperheroUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.SUPERHERO, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }), + ], + }); + const mockAdminUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.ADMINISTRATOR, + permissions: [ + Permission.TEACHER_EDIT, + Permission.STUDENT_EDIT, + Permission.STUDENT_LIST, + Permission.TEACHER_LIST, + Permission.TEACHER_CREATE, + Permission.STUDENT_CREATE, + Permission.TEACHER_DELETE, + Permission.STUDENT_DELETE, + ], + }), + ], + }); + const mockTeacherUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.TEACHER, + permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], + }), + ], + }); + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const mockDifferentSchoolAdminUser = userFactory.buildWithId({ + school: mockOtherSchool, + roles: [...mockAdminUser.roles], + }); + const mockDifferentSchoolTeacherUser = userFactory.buildWithId({ + school: mockOtherSchool, + roles: [...mockTeacherUser.roles], + }); + const mockDifferentSchoolStudentUser = userFactory.buildWithId({ + school: mockOtherSchool, + roles: [...mockStudentUser.roles], + }); + + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + const mockDifferentSchoolAdminAccount = accountFactory.buildWithId({ + userId: mockDifferentSchoolAdminUser.id, + password: defaultPasswordHash, + }); + const mockDifferentSchoolTeacherAccount = accountFactory.buildWithId({ + userId: mockDifferentSchoolTeacherUser.id, + password: defaultPasswordHash, + }); + const mockAdminAccount = accountFactory.buildWithId({ + userId: mockAdminUser.id, + password: defaultPasswordHash, + }); + const mockTeacherAccount = accountFactory.buildWithId({ + userId: mockTeacherUser.id, + password: defaultPasswordHash, + }); + + const mockDifferentSchoolStudentAccount = accountFactory.buildWithId({ + userId: mockDifferentSchoolStudentUser.id, + password: defaultPasswordHash, + }); + + configService.get.mockReturnValue(false); + userRepo.findById.mockResolvedValue(mockSuperheroUser); + accountService.searchByUsernamePartialMatch.mockResolvedValue([[], 0]); + + return { + mockSuperheroUser, + mockAdminAccount, + mockTeacherAccount, + mockStudentAccount, + mockDifferentSchoolAdminAccount, + mockDifferentSchoolTeacherAccount, + mockDifferentSchoolStudentAccount, + }; + }; + it('should be able to access any account via username', async () => { + const { + mockSuperheroUser, + mockAdminAccount, + mockTeacherAccount, + mockStudentAccount, + mockDifferentSchoolAdminAccount, + mockDifferentSchoolTeacherAccount, + mockDifferentSchoolStudentAccount, + } = setup(); + + const currentUser = { userId: mockSuperheroUser.id } as ICurrentUser; + + let params = { + type: AccountSearchType.USERNAME, + value: mockAdminAccount.username, + } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).resolves.not.toThrow(); + + params = { type: AccountSearchType.USERNAME, value: mockTeacherAccount.username } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).resolves.not.toThrow(); + + params = { type: AccountSearchType.USERNAME, value: mockStudentAccount.username } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).resolves.not.toThrow(); + + params = { + type: AccountSearchType.USERNAME, + value: mockDifferentSchoolAdminAccount.username, + } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).resolves.not.toThrow(); + + params = { + type: AccountSearchType.USERNAME, + value: mockDifferentSchoolTeacherAccount.username, + } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).resolves.not.toThrow(); + + params = { + type: AccountSearchType.USERNAME, + value: mockDifferentSchoolStudentAccount.username, + } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).resolves.not.toThrow(); + }); + }); + }); + }); + + describe('findAccountById', () => { + describe('When the current user is a superhero', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockSuperheroUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.SUPERHERO, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }), + ], + }); + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValueOnce(mockSuperheroUser); + accountService.findById.mockResolvedValueOnce(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + + return { mockSuperheroUser, mockStudentUser, mockStudentAccount }; + }; + it('should return an account', async () => { + const { mockSuperheroUser, mockStudentUser, mockStudentAccount } = setup(); + const account = await accountUc.findAccountById( + { userId: mockSuperheroUser.id } as ICurrentUser, + { id: mockStudentAccount.id } as AccountByIdParams + ); + expect(account).toStrictEqual( + expect.objectContaining({ + id: mockStudentAccount.id, + username: mockStudentAccount.username, + userId: mockStudentUser.id, + activated: mockStudentAccount.activated, + }) + ); + }); + }); + + describe('When the current user is no superhero', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockTeacherUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.TEACHER, + permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], + }), + ], + }); + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValueOnce(mockTeacherUser); + + return { mockTeacherUser, mockStudentAccount }; + }; + it('should throw ForbiddenOperationError', async () => { + const { mockTeacherUser, mockStudentAccount } = setup(); + await expect( + accountUc.findAccountById( + { userId: mockTeacherUser.id } as ICurrentUser, + { id: mockStudentAccount.id } as AccountByIdParams + ) + ).rejects.toThrow(ForbiddenOperationError); + }); + }); + + describe('When no account matches the search term', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockSuperheroUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.SUPERHERO, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }), + ], + }); + + userRepo.findById.mockResolvedValueOnce(mockSuperheroUser); + accountService.findById.mockImplementation((): Promise => { + throw new EntityNotFoundError(Account.name); + }); + + return { mockSuperheroUser }; + }; + it('should throw EntityNotFoundError', async () => { + const { mockSuperheroUser } = setup(); + await expect( + accountUc.findAccountById( + { userId: mockSuperheroUser.id } as ICurrentUser, + { id: 'xxx' } as AccountByIdParams + ) + ).rejects.toThrow(EntityNotFoundError); + }); + }); + + describe('When target account has no user', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockSuperheroUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.SUPERHERO, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }), + ], + }); + + userRepo.findById.mockResolvedValueOnce(mockSuperheroUser); + accountService.findById.mockImplementation((): Promise => { + throw new EntityNotFoundError(Account.name); + }); + + return { mockSuperheroUser }; + }; + it('should throw EntityNotFoundError', async () => { + const { mockSuperheroUser } = setup(); + await expect( + accountUc.findAccountById( + { userId: mockSuperheroUser.id } as ICurrentUser, + { id: 'xxx' } as AccountByIdParams + ) + ).rejects.toThrow(EntityNotFoundError); + }); + }); + }); + + describe('saveAccount', () => { + describe('When saving an account', () => { + const setup = () => { + const spy = jest.spyOn(accountService, 'saveWithValidation'); + + return { spy }; + }; + it('should call account service', async () => { + const { spy } = setup(); + + const params: AccountSaveDto = { + username: 'john.doe@domain.tld', + password: defaultPassword, + }; + await accountUc.saveAccount(params); + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + username: 'john.doe@domain.tld', + }) + ); + }); + }); + }); + + describe('updateAccountById', () => { + describe('When executing user does not exist', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockImplementation((): Promise => { + throw new EntityNotFoundError(User.name); + }); + + return { mockStudentAccount }; + }; + it('should throw EntityNotFoundError', async () => { + const { mockStudentAccount } = setup(); + const currentUser = { userId: '000000000000000' } as ICurrentUser; const params = { id: mockStudentAccount.id } as AccountByIdParams; const body = {} as AccountByIdBodyParams; - await expect(accountUc.updateAccountById(currentUser, params, body)).resolves.not.toThrow(); + await expect(accountUc.updateAccountById(currentUser, params, body)).rejects.toThrow(EntityNotFoundError); }); - it('admin can edit student', async () => { + }); + + describe('When target account does not exist', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockAdminUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.ADMINISTRATOR, + permissions: [ + Permission.TEACHER_EDIT, + Permission.STUDENT_EDIT, + Permission.STUDENT_LIST, + Permission.TEACHER_LIST, + Permission.TEACHER_CREATE, + Permission.STUDENT_CREATE, + Permission.TEACHER_DELETE, + Permission.STUDENT_DELETE, + ], + }), + ], + }); + + const mockAccountWithoutUser = accountFactory.buildWithId({ + userId: undefined, + password: defaultPasswordHash, + systemId: systemFactory.buildWithId().id, + }); + + userRepo.findById.mockResolvedValue(mockAdminUser); + accountService.findById.mockResolvedValue(AccountEntityToDtoMapper.mapToDto(mockAccountWithoutUser)); + + return { mockAdminUser }; + }; + it('should throw EntityNotFoundError', async () => { + const { mockAdminUser } = setup(); const currentUser = { userId: mockAdminUser.id } as ICurrentUser; - const params = { id: mockStudentAccount.id } as AccountByIdParams; + const params = { id: '000000000000000' } as AccountByIdParams; const body = {} as AccountByIdBodyParams; - await expect(accountUc.updateAccountById(currentUser, params, body)).resolves.not.toThrow(); + await expect(accountUc.updateAccountById(currentUser, params, body)).rejects.toThrow(EntityNotFoundError); }); - it('teacher cannot edit other teacher', async () => { - const currentUser = { userId: mockTeacherUser.id } as ICurrentUser; - const params = { id: mockOtherTeacherAccount.id } as AccountByIdParams; - const body = {} as AccountByIdBodyParams; - await expect(accountUc.updateAccountById(currentUser, params, body)).rejects.toThrow(ForbiddenOperationError); + }); + + describe('When using superhero user', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockSuperheroUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.SUPERHERO, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }), + ], + }); + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValueOnce(mockSuperheroUser).mockResolvedValueOnce(mockStudentUser); + accountService.findById.mockResolvedValueOnce(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + + userRepo.save.mockResolvedValue(); + accountService.save.mockImplementation((account: AccountSaveDto): Promise => { + Object.assign(mockStudentAccount, account); + return Promise.resolve(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + }); + + return { mockStudentAccount, mockStudentUser, mockSuperheroUser }; + }; + it('should update target account password', async () => { + const { mockStudentAccount, mockSuperheroUser, mockStudentUser } = setup(); + const previousPasswordHash = mockStudentAccount.password; + const currentUser = { userId: mockSuperheroUser.id } as ICurrentUser; + const params = { id: mockStudentAccount.id } as AccountByIdParams; + const body = { password: defaultPassword } as AccountByIdBodyParams; + expect(mockStudentUser.forcePasswordChange).toBeFalsy(); + await accountUc.updateAccountById(currentUser, params, body); + expect(mockStudentAccount.password).not.toBe(previousPasswordHash); + expect(mockStudentUser.forcePasswordChange).toBeTruthy(); }); - it("other school's admin cannot edit teacher", async () => { - const currentUser = { userId: mockDifferentSchoolAdminUser.id } as ICurrentUser; - const params = { id: mockTeacherAccount.id } as AccountByIdParams; - const body = {} as AccountByIdBodyParams; - await expect(accountUc.updateAccountById(currentUser, params, body)).rejects.toThrow(ForbiddenOperationError); + }); + + describe('When using superhero user', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockSuperheroUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.SUPERHERO, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }), + ], + }); + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValueOnce(mockSuperheroUser).mockResolvedValueOnce(mockStudentUser); + accountService.findById.mockResolvedValueOnce(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + + userRepo.save.mockResolvedValue(); + accountService.save.mockImplementation((account: AccountSaveDto): Promise => { + Object.assign(mockStudentAccount, account); + return Promise.resolve(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + }); + accountValidationService.isUniqueEmail.mockResolvedValue(true); + + return { mockStudentAccount, mockSuperheroUser }; + }; + it('should update target account username', async () => { + const { mockStudentAccount, mockSuperheroUser } = setup(); + const newUsername = 'newUsername'; + const currentUser = { userId: mockSuperheroUser.id } as ICurrentUser; + const params = { id: mockStudentAccount.id } as AccountByIdParams; + const body = { username: newUsername } as AccountByIdBodyParams; + expect(mockStudentAccount.username).not.toBe(newUsername); + await accountUc.updateAccountById(currentUser, params, body); + expect(mockStudentAccount.username).toBe(newUsername.toLowerCase()); }); - it('superhero can edit admin', async () => { + }); + + describe('When using superhero user', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockSuperheroUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.SUPERHERO, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }), + ], + }); + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValueOnce(mockSuperheroUser).mockResolvedValueOnce(mockStudentUser); + accountService.findById.mockResolvedValueOnce(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + + userRepo.save.mockResolvedValue(); + accountService.save.mockImplementation((account: AccountSaveDto): Promise => { + Object.assign(mockStudentAccount, account); + return Promise.resolve(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + }); + + return { mockStudentAccount, mockSuperheroUser }; + }; + it('should update target account activation state', async () => { + const { mockStudentAccount, mockSuperheroUser } = setup(); const currentUser = { userId: mockSuperheroUser.id } as ICurrentUser; - const params = { id: mockAdminAccount.id } as AccountByIdParams; - const body = {} as AccountByIdBodyParams; - await expect(accountUc.updateAccountById(currentUser, params, body)).resolves.not.toThrow(); + const params = { id: mockStudentAccount.id } as AccountByIdParams; + const body = { activated: false } as AccountByIdBodyParams; + await accountUc.updateAccountById(currentUser, params, body); + expect(mockStudentAccount.activated).toBeFalsy(); }); - it('undefined user role fails by default', async () => { - const currentUser = { userId: mockUnknownRoleUser.id } as ICurrentUser; - const params = { id: mockAccountWithoutRole.id } as AccountByIdParams; - const body = {} as AccountByIdBodyParams; - await expect(accountUc.updateAccountById(currentUser, params, body)).rejects.toThrow(ForbiddenOperationError); + }); + + describe('When using an admin user', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockAdminUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.ADMINISTRATOR, + permissions: [ + Permission.TEACHER_EDIT, + Permission.STUDENT_EDIT, + Permission.STUDENT_LIST, + Permission.TEACHER_LIST, + Permission.TEACHER_CREATE, + Permission.STUDENT_CREATE, + Permission.TEACHER_DELETE, + Permission.STUDENT_DELETE, + ], + }), + ], + }); + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValueOnce(mockAdminUser).mockResolvedValueOnce(mockStudentUser); + accountService.findById.mockResolvedValueOnce(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + + userRepo.save.mockResolvedValue(); + accountService.save.mockRejectedValueOnce(undefined); + + accountValidationService.isUniqueEmail.mockResolvedValue(true); + + return { mockStudentAccount, mockAdminUser }; + }; + it('should throw if account can not be updated', async () => { + const { mockStudentAccount, mockAdminUser } = setup(); + const currentUser = { userId: mockAdminUser.id } as ICurrentUser; + const params = { id: mockStudentAccount.id } as AccountByIdParams; + const body = { username: 'fail@to.update' } as AccountByIdBodyParams; + await expect(accountUc.updateAccountById(currentUser, params, body)).rejects.toThrow(EntityNotFoundError); }); - it('user without role cannot be edited', async () => { + }); + + describe('When user can not be updated', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockAdminUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.ADMINISTRATOR, + permissions: [ + Permission.TEACHER_EDIT, + Permission.STUDENT_EDIT, + Permission.STUDENT_LIST, + Permission.TEACHER_LIST, + Permission.TEACHER_CREATE, + Permission.STUDENT_CREATE, + Permission.TEACHER_DELETE, + Permission.STUDENT_DELETE, + ], + }), + ], + }); + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValueOnce(mockAdminUser).mockResolvedValueOnce(mockStudentUser); + accountService.findById.mockResolvedValueOnce(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + + userRepo.save.mockRejectedValueOnce(undefined); + + accountValidationService.isUniqueEmail.mockResolvedValue(true); + + return { mockStudentAccount, mockAdminUser }; + }; + it('should throw EntityNotFoundError', async () => { + const { mockStudentAccount, mockAdminUser } = setup(); const currentUser = { userId: mockAdminUser.id } as ICurrentUser; - const params = { id: mockUnknownRoleUserAccount.id } as AccountByIdParams; - const body = {} as AccountByIdBodyParams; - await expect(accountUc.updateAccountById(currentUser, params, body)).rejects.toThrow(ForbiddenOperationError); + const params = { id: mockStudentAccount.id } as AccountByIdParams; + const body = { username: 'user-fail@to.update' } as AccountByIdBodyParams; + await expect(accountUc.updateAccountById(currentUser, params, body)).rejects.toThrow(EntityNotFoundError); }); }); - }); - describe('deleteAccountById', () => { - it('should delete an account, if current user is authorized', async () => { - await expect( - accountUc.deleteAccountById( - { userId: mockSuperheroUser.id } as ICurrentUser, - { id: mockStudentAccount.id } as AccountByIdParams - ) - ).resolves.not.toThrow(); + describe('if target account has no user', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockSuperheroUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.SUPERHERO, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }), + ], + }); + const mockAccountWithoutUser = accountFactory.buildWithId({ + userId: undefined, + password: defaultPasswordHash, + systemId: systemFactory.buildWithId().id, + }); + + userRepo.findById.mockResolvedValueOnce(mockSuperheroUser); + accountService.findById.mockResolvedValueOnce(AccountEntityToDtoMapper.mapToDto(mockAccountWithoutUser)); + + return { mockSuperheroUser, mockAccountWithoutUser }; + }; + + it('should throw EntityNotFoundError', async () => { + const { mockSuperheroUser, mockAccountWithoutUser } = setup(); + await expect( + accountUc.updateAccountById( + { userId: mockSuperheroUser.id } as ICurrentUser, + { id: mockAccountWithoutUser.id } as AccountByIdParams, + { username: 'user-fail@to.update' } as AccountByIdBodyParams + ) + ).rejects.toThrow(EntityNotFoundError); + }); }); - it('should throw, if the current user is no superhero', async () => { - await expect( - accountUc.deleteAccountById( - { userId: mockAdminUser.id } as ICurrentUser, - { id: mockStudentAccount.id } as AccountByIdParams - ) - ).rejects.toThrow(ForbiddenOperationError); + + describe('When new username already in use', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockAdminUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.ADMINISTRATOR, + permissions: [ + Permission.TEACHER_EDIT, + Permission.STUDENT_EDIT, + Permission.STUDENT_LIST, + Permission.TEACHER_LIST, + Permission.TEACHER_CREATE, + Permission.STUDENT_CREATE, + Permission.TEACHER_DELETE, + Permission.STUDENT_DELETE, + ], + }), + ], + }); + const mockOtherTeacherUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.TEACHER, + permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], + }), + ], + }); + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + + const mockOtherTeacherAccount = accountFactory.buildWithId({ + userId: mockOtherTeacherUser.id, + password: defaultPasswordHash, + }); + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValueOnce(mockAdminUser).mockResolvedValueOnce(mockStudentUser); + accountService.findById.mockResolvedValueOnce(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + + userRepo.save.mockRejectedValueOnce(undefined); + + accountValidationService.isUniqueEmail.mockResolvedValueOnce(false); + + return { mockStudentAccount, mockAdminUser, mockOtherTeacherAccount }; + }; + it('should throw ValidationError', async () => { + const { mockStudentAccount, mockAdminUser, mockOtherTeacherAccount } = setup(); + const currentUser = { userId: mockAdminUser.id } as ICurrentUser; + const params = { id: mockStudentAccount.id } as AccountByIdParams; + const body = { username: mockOtherTeacherAccount.username } as AccountByIdBodyParams; + await expect(accountUc.updateAccountById(currentUser, params, body)).rejects.toThrow(ValidationError); + }); }); - it('should throw, if no account matches the search term', async () => { - await expect( - accountUc.deleteAccountById( - { userId: mockSuperheroUser.id } as ICurrentUser, - { id: 'xxx' } as AccountByIdParams - ) - ).rejects.toThrow(EntityNotFoundError); + + describe('hasPermissionsToUpdateAccount', () => { + describe('When using an admin user', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockAdminUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.ADMINISTRATOR, + permissions: [ + Permission.TEACHER_EDIT, + Permission.STUDENT_EDIT, + Permission.STUDENT_LIST, + Permission.TEACHER_LIST, + Permission.TEACHER_CREATE, + Permission.STUDENT_CREATE, + Permission.TEACHER_DELETE, + Permission.STUDENT_DELETE, + ], + }), + ], + }); + const mockTeacherUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.TEACHER, + permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], + }), + ], + }); + + const mockTeacherAccount = accountFactory.buildWithId({ + userId: mockTeacherUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValueOnce(mockAdminUser).mockResolvedValueOnce(mockTeacherUser); + accountService.findById.mockResolvedValueOnce(AccountEntityToDtoMapper.mapToDto(mockTeacherAccount)); + + return { mockAdminUser, mockTeacherAccount }; + }; + it('should not throw when editing a teacher', async () => { + const { mockAdminUser, mockTeacherAccount } = setup(); + const currentUser = { userId: mockAdminUser.id } as ICurrentUser; + const params = { id: mockTeacherAccount.id } as AccountByIdParams; + const body = {} as AccountByIdBodyParams; + await expect(accountUc.updateAccountById(currentUser, params, body)).resolves.not.toThrow(); + }); + }); + + describe('When using a teacher user', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockTeacherUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.TEACHER, + permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], + }), + ], + }); + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValueOnce(mockTeacherUser).mockResolvedValueOnce(mockStudentUser); + accountService.findById.mockResolvedValueOnce(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + + return { mockStudentAccount, mockTeacherUser }; + }; + it('should not throw when editing a student', async () => { + const { mockTeacherUser, mockStudentAccount } = setup(); + const currentUser = { userId: mockTeacherUser.id } as ICurrentUser; + const params = { id: mockStudentAccount.id } as AccountByIdParams; + const body = {} as AccountByIdBodyParams; + await expect(accountUc.updateAccountById(currentUser, params, body)).resolves.not.toThrow(); + }); + }); + describe('When using an admin user', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockAdminUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.ADMINISTRATOR, + permissions: [ + Permission.TEACHER_EDIT, + Permission.STUDENT_EDIT, + Permission.STUDENT_LIST, + Permission.TEACHER_LIST, + Permission.TEACHER_CREATE, + Permission.STUDENT_CREATE, + Permission.TEACHER_DELETE, + Permission.STUDENT_DELETE, + ], + }), + ], + }); + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValueOnce(mockAdminUser).mockResolvedValueOnce(mockStudentUser); + accountService.findById.mockResolvedValueOnce(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + + return { mockStudentAccount, mockAdminUser }; + }; + it('should not throw when editing a student', async () => { + const { mockAdminUser, mockStudentAccount } = setup(); + const currentUser = { userId: mockAdminUser.id } as ICurrentUser; + const params = { id: mockStudentAccount.id } as AccountByIdParams; + const body = {} as AccountByIdBodyParams; + await expect(accountUc.updateAccountById(currentUser, params, body)).resolves.not.toThrow(); + }); + }); + + describe('When using a teacher user to edit another teacher', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockTeacherUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.TEACHER, + permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], + }), + ], + }); + const mockOtherTeacherUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.TEACHER, + permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], + }), + ], + }); + const mockOtherTeacherAccount = accountFactory.buildWithId({ + userId: mockOtherTeacherUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValueOnce(mockTeacherUser).mockResolvedValueOnce(mockOtherTeacherUser); + accountService.findById.mockResolvedValueOnce(AccountEntityToDtoMapper.mapToDto(mockOtherTeacherAccount)); + + return { mockOtherTeacherAccount, mockTeacherUser }; + }; + it('should throw ForbiddenOperationError', async () => { + const { mockTeacherUser, mockOtherTeacherAccount } = setup(); + const currentUser = { userId: mockTeacherUser.id } as ICurrentUser; + const params = { id: mockOtherTeacherAccount.id } as AccountByIdParams; + const body = {} as AccountByIdBodyParams; + await expect(accountUc.updateAccountById(currentUser, params, body)).rejects.toThrow(ForbiddenOperationError); + }); + }); + + describe('When using an admin user of other school', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + const mockOtherSchool = schoolFactory.buildWithId(); + + const mockAdminUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.ADMINISTRATOR, + permissions: [ + Permission.TEACHER_EDIT, + Permission.STUDENT_EDIT, + Permission.STUDENT_LIST, + Permission.TEACHER_LIST, + Permission.TEACHER_CREATE, + Permission.STUDENT_CREATE, + Permission.TEACHER_DELETE, + Permission.STUDENT_DELETE, + ], + }), + ], + }); + const mockTeacherUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.TEACHER, + permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], + }), + ], + }); + const mockDifferentSchoolAdminUser = userFactory.buildWithId({ + school: mockOtherSchool, + roles: [...mockAdminUser.roles], + }); + + const mockTeacherAccount = accountFactory.buildWithId({ + userId: mockTeacherUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValueOnce(mockDifferentSchoolAdminUser).mockResolvedValueOnce(mockTeacherUser); + accountService.findById.mockResolvedValueOnce(AccountEntityToDtoMapper.mapToDto(mockTeacherAccount)); + + return { mockDifferentSchoolAdminUser, mockTeacherAccount }; + }; + it('should throw ForbiddenOperationError', async () => { + const { mockDifferentSchoolAdminUser, mockTeacherAccount } = setup(); + const currentUser = { userId: mockDifferentSchoolAdminUser.id } as ICurrentUser; + const params = { id: mockTeacherAccount.id } as AccountByIdParams; + const body = {} as AccountByIdBodyParams; + await expect(accountUc.updateAccountById(currentUser, params, body)).rejects.toThrow(ForbiddenOperationError); + }); + }); + + describe('When using a superhero user', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockSuperheroUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.SUPERHERO, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }), + ], + }); + const mockAdminUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.ADMINISTRATOR, + permissions: [ + Permission.TEACHER_EDIT, + Permission.STUDENT_EDIT, + Permission.STUDENT_LIST, + Permission.TEACHER_LIST, + Permission.TEACHER_CREATE, + Permission.STUDENT_CREATE, + Permission.TEACHER_DELETE, + Permission.STUDENT_DELETE, + ], + }), + ], + }); + const mockAdminAccount = accountFactory.buildWithId({ + userId: mockAdminUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValueOnce(mockSuperheroUser).mockResolvedValueOnce(mockAdminUser); + accountService.findById.mockResolvedValueOnce(AccountEntityToDtoMapper.mapToDto(mockAdminAccount)); + + return { mockAdminAccount, mockSuperheroUser }; + }; + it('should not throw when editing a admin', async () => { + const { mockSuperheroUser, mockAdminAccount } = setup(); + const currentUser = { userId: mockSuperheroUser.id } as ICurrentUser; + const params = { id: mockAdminAccount.id } as AccountByIdParams; + const body = {} as AccountByIdBodyParams; + await expect(accountUc.updateAccountById(currentUser, params, body)).resolves.not.toThrow(); + }); + }); + + describe('When using an user with undefined role', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockUserWithoutRole = userFactory.buildWithId({ + school: mockSchool, + roles: [], + }); + const mockUnknownRoleUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: 'undefinedRole' as RoleName, permissions: ['' as Permission] })], + }); + const mockAccountWithoutRole = accountFactory.buildWithId({ + userId: mockUserWithoutRole.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValueOnce(mockUnknownRoleUser).mockResolvedValueOnce(mockUserWithoutRole); + accountService.findById.mockResolvedValueOnce(AccountEntityToDtoMapper.mapToDto(mockAccountWithoutRole)); + + return { mockAccountWithoutRole, mockUnknownRoleUser }; + }; + it('should fail by default', async () => { + const { mockUnknownRoleUser, mockAccountWithoutRole } = setup(); + const currentUser = { userId: mockUnknownRoleUser.id } as ICurrentUser; + const params = { id: mockAccountWithoutRole.id } as AccountByIdParams; + const body = {} as AccountByIdBodyParams; + await expect(accountUc.updateAccountById(currentUser, params, body)).rejects.toThrow(ForbiddenOperationError); + }); + }); + + describe('When editing an user without role', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockAdminUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.ADMINISTRATOR, + permissions: [ + Permission.TEACHER_EDIT, + Permission.STUDENT_EDIT, + Permission.STUDENT_LIST, + Permission.TEACHER_LIST, + Permission.TEACHER_CREATE, + Permission.STUDENT_CREATE, + Permission.TEACHER_DELETE, + Permission.STUDENT_DELETE, + ], + }), + ], + }); + const mockUnknownRoleUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: 'undefinedRole' as RoleName, permissions: ['' as Permission] })], + }); + const mockUnknownRoleUserAccount = accountFactory.buildWithId({ + userId: mockUnknownRoleUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValueOnce(mockAdminUser).mockResolvedValueOnce(mockUnknownRoleUser); + accountService.findById.mockResolvedValueOnce(AccountEntityToDtoMapper.mapToDto(mockUnknownRoleUserAccount)); + + return { mockAdminUser, mockUnknownRoleUserAccount }; + }; + it('should throw ForbiddenOperationError', async () => { + const { mockAdminUser, mockUnknownRoleUserAccount } = setup(); + const currentUser = { userId: mockAdminUser.id } as ICurrentUser; + const params = { id: mockUnknownRoleUserAccount.id } as AccountByIdParams; + const body = {} as AccountByIdBodyParams; + await expect(accountUc.updateAccountById(currentUser, params, body)).rejects.toThrow(ForbiddenOperationError); + }); + }); }); }); - describe('checkBrutForce', () => { - let updateMock: jest.Mock; - beforeAll(() => { - configService.get.mockReturnValue(LOGIN_BLOCK_TIME); - }); - afterAll(() => { - configService.get.mockRestore(); + describe('deleteAccountById', () => { + describe('When current user is authorized', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockSuperheroUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.SUPERHERO, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }), + ], + }); + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockResolvedValue(mockSuperheroUser); + + accountService.findById.mockResolvedValue(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + + return { mockSuperheroUser, mockStudentAccount }; + }; + it('should delete an account', async () => { + const { mockSuperheroUser, mockStudentAccount } = setup(); + await expect( + accountUc.deleteAccountById( + { userId: mockSuperheroUser.id } as ICurrentUser, + { id: mockStudentAccount.id } as AccountByIdParams + ) + ).resolves.not.toThrow(); + }); }); - beforeEach(() => { - // eslint-disable-next-line jest/unbound-method - updateMock = accountService.updateLastTriedFailedLogin as jest.Mock; - updateMock.mockClear(); + + describe('When the current user is no superhero', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockAdminUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.ADMINISTRATOR, + permissions: [ + Permission.TEACHER_EDIT, + Permission.STUDENT_EDIT, + Permission.STUDENT_LIST, + Permission.TEACHER_LIST, + Permission.TEACHER_CREATE, + Permission.STUDENT_CREATE, + Permission.TEACHER_DELETE, + Permission.STUDENT_DELETE, + ], + }), + ], + }); + + const mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + + const mockStudentAccount = accountFactory.buildWithId({ + userId: mockStudentUser.id, + password: defaultPasswordHash, + }); + + userRepo.findById.mockImplementation((userId: EntityId): Promise => { + if (mockAdminUser.id === userId) { + return Promise.resolve(mockAdminUser); + } + throw new EntityNotFoundError(User.name); + }); + + return { mockAdminUser, mockStudentAccount }; + }; + it('should throw ForbiddenOperationError', async () => { + const { mockAdminUser, mockStudentAccount } = setup(); + await expect( + accountUc.deleteAccountById( + { userId: mockAdminUser.id } as ICurrentUser, + { id: mockStudentAccount.id } as AccountByIdParams + ) + ).rejects.toThrow(ForbiddenOperationError); + }); }); - it('should throw, if time difference < the allowed time', async () => { - await expect( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - accountUc.checkBrutForce(mockAccountWithLastFailedLogin.username, mockAccountWithLastFailedLogin.systemId!) - ).rejects.toThrow(BruteForcePrevention); + + describe('When no account matches the search term', () => { + const setup = () => { + const mockSchool = schoolFactory.buildWithId(); + + const mockSuperheroUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.SUPERHERO, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }), + ], + }); + + userRepo.findById.mockImplementation((userId: EntityId): Promise => { + if (mockSuperheroUser.id === userId) { + return Promise.resolve(mockSuperheroUser); + } + throw new EntityNotFoundError(User.name); + }); + + accountService.findById.mockImplementation((id: EntityId): Promise => { + if (id === 'xxx') { + throw new EntityNotFoundError(Account.name); + } + return Promise.reject(); + }); + + return { mockSuperheroUser }; + }; + it('should throw, if no account matches the search term', async () => { + const { mockSuperheroUser } = setup(); + await expect( + accountUc.deleteAccountById( + { userId: mockSuperheroUser.id } as ICurrentUser, + { id: 'xxx' } as AccountByIdParams + ) + ).rejects.toThrow(EntityNotFoundError); + }); }); - it('should not throw Error, if the time difference > the allowed time', async () => { - await expect( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - accountUc.checkBrutForce(mockAccountWithSystemId.username, mockAccountWithSystemId.systemId!) - ).resolves.not.toThrow(); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(updateMock.mock.calls[0][0]).toEqual(mockAccountWithSystemId.id); - const newDate = new Date().getTime() - 10000; - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect((updateMock.mock.calls[0][1] as Date).getTime()).toBeGreaterThan(newDate); + }); + + describe('checkBrutForce', () => { + describe('When time difference < the allowed time', () => { + const setup = () => { + const mockAccountWithLastFailedLogin = accountFactory.buildWithId({ + userId: undefined, + password: defaultPasswordHash, + systemId: systemFactory.buildWithId().id, + lasttriedFailedLogin: new Date(), + }); + + configService.get.mockReturnValue(LOGIN_BLOCK_TIME); + + accountService.findByUsernameAndSystemId.mockImplementation( + (username: string, systemId: EntityId | ObjectId): Promise => { + if ( + mockAccountWithLastFailedLogin.username === username && + mockAccountWithLastFailedLogin.systemId === systemId + ) { + return Promise.resolve(AccountEntityToDtoMapper.mapToDto(mockAccountWithLastFailedLogin)); + } + throw new EntityNotFoundError(Account.name); + } + ); + + return { mockAccountWithLastFailedLogin }; + }; + + it('should throw BruteForcePrevention', async () => { + const { mockAccountWithLastFailedLogin } = setup(); + await expect( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + accountUc.checkBrutForce(mockAccountWithLastFailedLogin.username, mockAccountWithLastFailedLogin.systemId!) + ).rejects.toThrow(BruteForcePrevention); + }); }); - it('should not throw, if lasttriedFailedLogin is undefined', async () => { - await expect( - accountUc.checkBrutForce( - mockAccountWithNoLastFailedLogin.username, + + describe('When the time difference > the allowed time', () => { + const setup = () => { + const mockAccountWithSystemId = accountFactory.withSystemId(new ObjectId(10)).build(); + + // eslint-disable-next-line jest/unbound-method + const updateMock = accountService.updateLastTriedFailedLogin as jest.Mock; + + configService.get.mockReturnValue(LOGIN_BLOCK_TIME); + + accountService.findByUsernameAndSystemId.mockImplementation( + (username: string, systemId: EntityId | ObjectId): Promise => { + if (mockAccountWithSystemId.username === username && mockAccountWithSystemId.systemId === systemId) { + return Promise.resolve(AccountEntityToDtoMapper.mapToDto(mockAccountWithSystemId)); + } + throw new EntityNotFoundError(Account.name); + } + ); + + return { mockAccountWithSystemId, updateMock }; + }; + + it('should not throw Error, ', async () => { + const { mockAccountWithSystemId, updateMock } = setup(); + + await expect( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - mockAccountWithNoLastFailedLogin.systemId! - ) - ).resolves.not.toThrow(); + accountUc.checkBrutForce(mockAccountWithSystemId.username, mockAccountWithSystemId.systemId!) + ).resolves.not.toThrow(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(updateMock.mock.calls[0][0]).toEqual(mockAccountWithSystemId.id); + const newDate = new Date().getTime() - 10000; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect((updateMock.mock.calls[0][1] as Date).getTime()).toBeGreaterThan(newDate); + }); + }); + + describe('When lasttriedFailedLogin is undefined', () => { + const setup = () => { + const mockAccountWithNoLastFailedLogin = accountFactory.buildWithId({ + userId: undefined, + password: defaultPasswordHash, + systemId: systemFactory.buildWithId().id, + lasttriedFailedLogin: undefined, + }); + + configService.get.mockReturnValue(LOGIN_BLOCK_TIME); + + accountService.findByUsernameAndSystemId.mockImplementation( + (username: string, systemId: EntityId | ObjectId): Promise => { + if ( + mockAccountWithNoLastFailedLogin.username === username && + mockAccountWithNoLastFailedLogin.systemId === systemId + ) { + return Promise.resolve(AccountEntityToDtoMapper.mapToDto(mockAccountWithNoLastFailedLogin)); + } + throw new EntityNotFoundError(Account.name); + } + ); + + return { mockAccountWithNoLastFailedLogin }; + }; + it('should not throw', async () => { + const { mockAccountWithNoLastFailedLogin } = setup(); + await expect( + accountUc.checkBrutForce( + mockAccountWithNoLastFailedLogin.username, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + mockAccountWithNoLastFailedLogin.systemId! + ) + ).resolves.not.toThrow(); + }); }); }); }); From 75fd0ccdbda1e13ef2d9050c85e446ed7a97dc8f Mon Sep 17 00:00:00 2001 From: MajedAlaitwniCap Date: Mon, 25 Sep 2023 16:04:21 +0200 Subject: [PATCH 15/24] use setup for account idm service test --- .../services/account-idm.service.spec.ts | 291 ++++++++++-------- 1 file changed, 170 insertions(+), 121 deletions(-) diff --git a/apps/server/src/modules/account/services/account-idm.service.spec.ts b/apps/server/src/modules/account/services/account-idm.service.spec.ts index 09f37602041..2db1666cdc5 100644 --- a/apps/server/src/modules/account/services/account-idm.service.spec.ts +++ b/apps/server/src/modules/account/services/account-idm.service.spec.ts @@ -70,155 +70,203 @@ describe('AccountIdmService', () => { }); describe('save', () => { - const setup = () => { - idmServiceMock.createAccount.mockResolvedValue(mockIdmAccount.id); - idmServiceMock.updateAccount.mockResolvedValue(mockIdmAccount.id); - idmServiceMock.updateAccountPassword.mockResolvedValue(mockIdmAccount.id); - idmServiceMock.findAccountById.mockResolvedValue(mockIdmAccount); - }; + describe('when save an existing account', () => { + const setup = () => { + idmServiceMock.createAccount.mockResolvedValue(mockIdmAccount.id); + idmServiceMock.updateAccount.mockResolvedValue(mockIdmAccount.id); + idmServiceMock.updateAccountPassword.mockResolvedValue(mockIdmAccount.id); + idmServiceMock.findAccountById.mockResolvedValue(mockIdmAccount); + const updateSpy = jest.spyOn(idmServiceMock, 'updateAccount'); + const createSpy = jest.spyOn(idmServiceMock, 'createAccount'); + + const mockAccountDto = { + id: mockIdmAccountRefId, + username: 'testUserName', + userId: 'userId', + systemId: 'systemId', + }; + return { updateSpy, createSpy, mockAccountDto }; + }; - it('should update an existing account', async () => { - setup(); - const updateSpy = jest.spyOn(idmServiceMock, 'updateAccount'); - const createSpy = jest.spyOn(idmServiceMock, 'createAccount'); + it('should update account information', async () => { + const { updateSpy, createSpy, mockAccountDto } = setup(); - const mockAccountDto = { - id: mockIdmAccountRefId, - username: 'testUserName', - userId: 'userId', - systemId: 'systemId', - }; - const ret = await accountIdmService.save(mockAccountDto); - - expect(updateSpy).toHaveBeenCalled(); - expect(createSpy).not.toHaveBeenCalled(); - - expect(ret).toBeDefined(); - expect(ret).toMatchObject>({ - id: mockIdmAccount.attRefTechnicalId, - idmReferenceId: mockIdmAccount.id, - createdAt: mockIdmAccount.createdDate, - updatedAt: mockIdmAccount.createdDate, - username: mockIdmAccount.username, + const ret = await accountIdmService.save(mockAccountDto); + + expect(updateSpy).toHaveBeenCalled(); + expect(createSpy).not.toHaveBeenCalled(); + + expect(ret).toBeDefined(); + expect(ret).toMatchObject>({ + id: mockIdmAccount.attRefTechnicalId, + idmReferenceId: mockIdmAccount.id, + createdAt: mockIdmAccount.createdDate, + updatedAt: mockIdmAccount.createdDate, + username: mockIdmAccount.username, + }); }); }); - it('should update an existing accounts password', async () => { - setup(); - const updateSpy = jest.spyOn(idmServiceMock, 'updateAccount'); - const updatePasswordSpy = jest.spyOn(idmServiceMock, 'updateAccountPassword'); - - const mockAccountDto: AccountSaveDto = { - id: mockIdmAccountRefId, - username: 'testUserName', - userId: 'userId', - systemId: 'systemId', - password: 'password', + describe('when save an existing account', () => { + const setup = () => { + idmServiceMock.createAccount.mockResolvedValue(mockIdmAccount.id); + idmServiceMock.updateAccount.mockResolvedValue(mockIdmAccount.id); + idmServiceMock.updateAccountPassword.mockResolvedValue(mockIdmAccount.id); + idmServiceMock.findAccountById.mockResolvedValue(mockIdmAccount); + const updateSpy = jest.spyOn(idmServiceMock, 'updateAccount'); + const updatePasswordSpy = jest.spyOn(idmServiceMock, 'updateAccountPassword'); + + const mockAccountDto: AccountSaveDto = { + id: mockIdmAccountRefId, + username: 'testUserName', + userId: 'userId', + systemId: 'systemId', + password: 'password', + }; + return { updateSpy, updatePasswordSpy, mockAccountDto }; }; - const ret = await accountIdmService.save(mockAccountDto); + it('should update account password', async () => { + const { updateSpy, updatePasswordSpy, mockAccountDto } = setup(); - expect(updateSpy).toHaveBeenCalled(); - expect(updatePasswordSpy).toHaveBeenCalled(); - expect(ret).toBeDefined(); + const ret = await accountIdmService.save(mockAccountDto); + + expect(updateSpy).toHaveBeenCalled(); + expect(updatePasswordSpy).toHaveBeenCalled(); + expect(ret).toBeDefined(); + }); }); - it('should create a new account', async () => { - setup(); - const updateSpy = jest.spyOn(idmServiceMock, 'updateAccount'); - const createSpy = jest.spyOn(idmServiceMock, 'createAccount'); + describe('when save not existing account', () => { + const setup = () => { + idmServiceMock.createAccount.mockResolvedValue(mockIdmAccount.id); + idmServiceMock.updateAccount.mockResolvedValue(mockIdmAccount.id); + idmServiceMock.updateAccountPassword.mockResolvedValue(mockIdmAccount.id); + idmServiceMock.findAccountById.mockResolvedValue(mockIdmAccount); + const updateSpy = jest.spyOn(idmServiceMock, 'updateAccount'); + const createSpy = jest.spyOn(idmServiceMock, 'createAccount'); + + const mockAccountDto = { username: 'testUserName', id: undefined, userId: 'userId', systemId: 'systemId' }; - const mockAccountDto = { username: 'testUserName', id: undefined, userId: 'userId', systemId: 'systemId' }; - const ret = await accountIdmService.save(mockAccountDto); + return { updateSpy, createSpy, mockAccountDto }; + }; + it('should create a new account', async () => { + const { updateSpy, createSpy, mockAccountDto } = setup(); - expect(updateSpy).not.toHaveBeenCalled(); - expect(createSpy).toHaveBeenCalled(); + const ret = await accountIdmService.save(mockAccountDto); - expect(ret).toBeDefined(); - expect(ret).toMatchObject>({ - id: mockIdmAccount.attRefTechnicalId, - idmReferenceId: mockIdmAccount.id, - createdAt: mockIdmAccount.createdDate, - updatedAt: mockIdmAccount.createdDate, - username: mockIdmAccount.username, + expect(updateSpy).not.toHaveBeenCalled(); + expect(createSpy).toHaveBeenCalled(); + + expect(ret).toBeDefined(); + expect(ret).toMatchObject>({ + id: mockIdmAccount.attRefTechnicalId, + idmReferenceId: mockIdmAccount.id, + createdAt: mockIdmAccount.createdDate, + updatedAt: mockIdmAccount.createdDate, + username: mockIdmAccount.username, + }); }); }); - it('should create a new account on update error', async () => { - setup(); - accountLookupServiceMock.getExternalId.mockResolvedValue(null); - const mockAccountDto = { - id: mockIdmAccountRefId, - username: 'testUserName', - userId: 'userId', - systemId: 'systemId', - }; - const ret = await accountIdmService.save(mockAccountDto); - - expect(idmServiceMock.createAccount).toHaveBeenCalled(); - expect(ret).toBeDefined(); - expect(ret).toMatchObject>({ - id: mockIdmAccount.attRefTechnicalId, - idmReferenceId: mockIdmAccount.id, - createdAt: mockIdmAccount.createdDate, - updatedAt: mockIdmAccount.createdDate, - username: mockIdmAccount.username, + describe('when save not existing account', () => { + const setup = () => { + idmServiceMock.createAccount.mockResolvedValue(mockIdmAccount.id); + idmServiceMock.updateAccount.mockResolvedValue(mockIdmAccount.id); + idmServiceMock.updateAccountPassword.mockResolvedValue(mockIdmAccount.id); + idmServiceMock.findAccountById.mockResolvedValue(mockIdmAccount); + accountLookupServiceMock.getExternalId.mockResolvedValue(null); + const mockAccountDto = { + id: mockIdmAccountRefId, + username: 'testUserName', + userId: 'userId', + systemId: 'systemId', + }; + + return { mockAccountDto }; + }; + it('should create a new account on update error', async () => { + const { mockAccountDto } = setup(); + + const ret = await accountIdmService.save(mockAccountDto); + + expect(idmServiceMock.createAccount).toHaveBeenCalled(); + expect(ret).toBeDefined(); + expect(ret).toMatchObject>({ + id: mockIdmAccount.attRefTechnicalId, + idmReferenceId: mockIdmAccount.id, + createdAt: mockIdmAccount.createdDate, + updatedAt: mockIdmAccount.createdDate, + username: mockIdmAccount.username, + }); }); }); }); describe('updateUsername', () => { - it('should map result correctly', async () => { - accountLookupServiceMock.getExternalId.mockResolvedValue(mockIdmAccount.id); - const ret = await accountIdmService.updateUsername(mockIdmAccountRefId, 'any'); - - expect(ret).toBeDefined(); - expect(ret).toMatchObject>({ - id: mockIdmAccount.attRefTechnicalId, - idmReferenceId: mockIdmAccount.id, - createdAt: mockIdmAccount.createdDate, - updatedAt: mockIdmAccount.createdDate, - username: mockIdmAccount.username, + describe('when update Username', () => { + const setup = () => { + accountLookupServiceMock.getExternalId.mockResolvedValue(mockIdmAccount.id); + }; + it('should map result correctly', async () => { + setup(); + const ret = await accountIdmService.updateUsername(mockIdmAccountRefId, 'any'); + + expect(ret).toBeDefined(); + expect(ret).toMatchObject>({ + id: mockIdmAccount.attRefTechnicalId, + idmReferenceId: mockIdmAccount.id, + createdAt: mockIdmAccount.createdDate, + updatedAt: mockIdmAccount.createdDate, + username: mockIdmAccount.username, + }); }); }); }); describe('updatePassword', () => { - it('should map result correctly', async () => { - accountLookupServiceMock.getExternalId.mockResolvedValue(mockIdmAccount.id); - const ret = await accountIdmService.updatePassword(mockIdmAccountRefId, 'any'); - - expect(ret).toBeDefined(); - expect(ret).toMatchObject>({ - id: mockIdmAccount.attRefTechnicalId, - idmReferenceId: mockIdmAccount.id, - createdAt: mockIdmAccount.createdDate, - updatedAt: mockIdmAccount.createdDate, - username: mockIdmAccount.username, + describe('when update password', () => { + const setup = () => { + accountLookupServiceMock.getExternalId.mockResolvedValue(mockIdmAccount.id); + }; + it('should map result correctly', async () => { + setup(); + const ret = await accountIdmService.updatePassword(mockIdmAccountRefId, 'any'); + + expect(ret).toBeDefined(); + expect(ret).toMatchObject>({ + id: mockIdmAccount.attRefTechnicalId, + idmReferenceId: mockIdmAccount.id, + createdAt: mockIdmAccount.createdDate, + updatedAt: mockIdmAccount.createdDate, + username: mockIdmAccount.username, + }); }); }); }); describe('validatePassword', () => { - const setup = (acceptPassword: boolean) => { - idmOauthServiceMock.resourceOwnerPasswordGrant.mockResolvedValue( - acceptPassword ? '{ "alg": "HS256", "typ": "JWT" }' : undefined - ); - }; - it('should validate password by checking JWT', async () => { - setup(true); - const ret = await accountIdmService.validatePassword( - { username: 'username' } as unknown as AccountDto, - 'password' - ); - expect(ret).toBe(true); - }); - it('should report wrong password, i. e. non successful JWT creation', async () => { - setup(false); - const ret = await accountIdmService.validatePassword( - { username: 'username' } as unknown as AccountDto, - 'password' - ); - expect(ret).toBe(false); + describe('when validate password', () => { + const setup = (acceptPassword: boolean) => { + idmOauthServiceMock.resourceOwnerPasswordGrant.mockResolvedValue( + acceptPassword ? '{ "alg": "HS256", "typ": "JWT" }' : undefined + ); + }; + it('should validate password by checking JWT', async () => { + setup(true); + const ret = await accountIdmService.validatePassword( + { username: 'username' } as unknown as AccountDto, + 'password' + ); + expect(ret).toBe(true); + }); + it('should report wrong password, i. e. non successful JWT creation', async () => { + setup(false); + const ret = await accountIdmService.validatePassword( + { username: 'username' } as unknown as AccountDto, + 'password' + ); + expect(ret).toBe(false); + }); }); }); @@ -252,11 +300,12 @@ describe('AccountIdmService', () => { describe('deleteByUserId', () => { const setup = () => { idmServiceMock.findAccountByFctIntId.mockResolvedValue(mockIdmAccount); + const deleteSpy = jest.spyOn(idmServiceMock, 'deleteAccountById'); + return { deleteSpy }; }; it('should delete the account with given user id via repo', async () => { - setup(); - const deleteSpy = jest.spyOn(idmServiceMock, 'deleteAccountById'); + const { deleteSpy } = setup(); await accountIdmService.deleteByUserId(mockIdmAccount.attRefFunctionalIntId ?? ''); expect(deleteSpy).toHaveBeenCalledWith(mockIdmAccount.id); From c1bd6caa62bb750add72a2866cc57a9bc17e1ac3 Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Tue, 26 Sep 2023 10:53:11 +0200 Subject: [PATCH 16/24] Use setup method and new test structure for account.validation.service test. --- .../account.validation.service.spec.ts | 586 +++++++++++------- 1 file changed, 368 insertions(+), 218 deletions(-) diff --git a/apps/server/src/modules/account/services/account.validation.service.spec.ts b/apps/server/src/modules/account/services/account.validation.service.spec.ts index dba1e2bf02a..7608ae52dd8 100644 --- a/apps/server/src/modules/account/services/account.validation.service.spec.ts +++ b/apps/server/src/modules/account/services/account.validation.service.spec.ts @@ -1,9 +1,9 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { EntityNotFoundError } from '@shared/common'; -import { Account, EntityId, Permission, Role, RoleName, User } from '@shared/domain'; +import { Permission, Role, RoleName } from '@shared/domain'; import { UserRepo } from '@shared/repo'; import { accountFactory, setupEntities, systemFactory, userFactory } from '@shared/testing'; import { ObjectId } from 'bson'; +import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { AccountRepo } from '../repo/account.repo'; import { AccountValidationService } from './account.validation.service'; @@ -11,26 +11,8 @@ describe('AccountValidationService', () => { let module: TestingModule; let accountValidationService: AccountValidationService; - let mockTeacherUser: User; - let mockTeacherAccount: Account; - - let mockStudentUser: User; - let mockStudentAccount: Account; - - let mockOtherTeacherUser: User; - let mockOtherTeacherAccount: Account; - - let mockAdminUser: User; - - let mockExternalUser: User; - let mockExternalUserAccount: Account; - let mockOtherExternalUser: User; - let mockOtherExternalUserAccount: Account; - - let oprhanAccount: Account; - - let mockUsers: User[]; - let mockAccounts: Account[]; + let userRepo: DeepMocked; + let accountRepo: DeepMocked; afterAll(async () => { await module.close(); @@ -42,237 +24,405 @@ describe('AccountValidationService', () => { AccountValidationService, { provide: AccountRepo, - useValue: { - findById: jest.fn().mockImplementation((accountId: EntityId): Promise => { - const account = mockAccounts.find((tempAccount) => tempAccount.id === accountId); - - if (account) { - return Promise.resolve(account); - } - throw new EntityNotFoundError(Account.name); - }), - searchByUsernameExactMatch: jest - .fn() - .mockImplementation((username: string): Promise<[Account[], number]> => { - const account = mockAccounts.find((tempAccount) => tempAccount.username === username); - - if (account) { - return Promise.resolve([[account], 1]); - } - if (username === 'not@available.username') { - return Promise.resolve([[mockOtherTeacherAccount], 1]); - } - if (username === 'multiple@account.username') { - return Promise.resolve([mockAccounts, mockAccounts.length]); - } - return Promise.resolve([[], 0]); - }), - findByUserId: (userId: EntityId): Promise => { - const account = mockAccounts.find((tempAccount) => tempAccount.userId?.toString() === userId); - if (account) { - return Promise.resolve(account); - } - return Promise.resolve(null); - }, - }, + useValue: createMock(), }, { provide: UserRepo, - useValue: { - findById: jest.fn().mockImplementation((userId: EntityId): Promise => { - const user = mockUsers.find((tempUser) => tempUser.id === userId); - if (user) { - return Promise.resolve(user); - } - throw new EntityNotFoundError(User.name); - }), - findByEmail: jest.fn().mockImplementation((email: string): Promise => { - const user = mockUsers.find((tempUser) => tempUser.email === email); - - if (user) { - return Promise.resolve([user]); - } - if (email === 'multiple@user.email') { - return Promise.resolve(mockUsers); - } - return Promise.resolve([]); - }), - }, + useValue: createMock(), }, ], }).compile(); accountValidationService = module.get(AccountValidationService); + + userRepo = module.get(UserRepo); + accountRepo = module.get(AccountRepo); + await setupEntities(); }); beforeEach(() => { - mockTeacherUser = userFactory.buildWithId({ - roles: [new Role({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] })], - }); - mockStudentUser = userFactory.buildWithId({ - roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], - }); - mockOtherTeacherUser = userFactory.buildWithId({ - roles: [ - new Role({ - name: RoleName.TEACHER, - permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], - }), - ], - }); - mockExternalUser = userFactory.buildWithId({ - roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], - }); - mockOtherExternalUser = userFactory.buildWithId({ - roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], - }); + jest.resetAllMocks(); + }); - mockTeacherAccount = accountFactory.buildWithId({ userId: mockTeacherUser.id }); - mockStudentAccount = accountFactory.buildWithId({ userId: mockStudentUser.id }); - mockOtherTeacherAccount = accountFactory.buildWithId({ - userId: mockOtherTeacherUser.id, - }); - const externalSystemA = systemFactory.buildWithId(); - const externalSystemB = systemFactory.buildWithId(); - mockExternalUserAccount = accountFactory.buildWithId({ - userId: mockExternalUser.id, - username: 'unique.within@system', - systemId: externalSystemA.id, + describe('isUniqueEmail', () => { + describe('When new email is available', () => { + const setup = () => { + userRepo.findByEmail.mockResolvedValueOnce([]); + accountRepo.searchByUsernameExactMatch.mockResolvedValueOnce([[], 0]); + }; + it('should return true', async () => { + setup(); + + const res = await accountValidationService.isUniqueEmail('an@available.email'); + expect(res).toBe(true); + }); }); - mockOtherExternalUserAccount = accountFactory.buildWithId({ - userId: mockOtherExternalUser.id, - username: 'unique.within@system', - systemId: externalSystemB.id, + + describe('When new email is available', () => { + const setup = () => { + const mockStudentUser = userFactory.buildWithId({ + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + + userRepo.findByEmail.mockResolvedValueOnce([mockStudentUser]); + accountRepo.searchByUsernameExactMatch.mockResolvedValueOnce([[], 0]); + + return { mockStudentUser }; + }; + it('should return true and ignore current user', async () => { + const { mockStudentUser } = setup(); + const res = await accountValidationService.isUniqueEmail(mockStudentUser.email, mockStudentUser.id); + expect(res).toBe(true); + }); }); - oprhanAccount = accountFactory.buildWithId({ - username: 'orphan@account', - userId: undefined, - systemId: new ObjectId(), + describe('When new email is available', () => { + const setup = () => { + const mockStudentUser = userFactory.buildWithId({ + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + + const mockStudentAccount = accountFactory.buildWithId({ userId: mockStudentUser.id }); + + userRepo.findByEmail.mockResolvedValueOnce([mockStudentUser]); + accountRepo.searchByUsernameExactMatch.mockResolvedValueOnce([[mockStudentAccount], 1]); + + return { mockStudentUser, mockStudentAccount }; + }; + it('should return true and ignore current users account', async () => { + const { mockStudentUser, mockStudentAccount } = setup(); + const res = await accountValidationService.isUniqueEmail( + mockStudentAccount.username, + mockStudentUser.id, + mockStudentAccount.id + ); + expect(res).toBe(true); + }); }); - mockAccounts = [ - mockTeacherAccount, - mockStudentAccount, - mockOtherTeacherAccount, - mockExternalUserAccount, - mockOtherExternalUserAccount, - oprhanAccount, - ]; - mockAdminUser = userFactory.buildWithId({ - roles: [ - new Role({ - name: RoleName.ADMINISTRATOR, - permissions: [ - Permission.TEACHER_EDIT, - Permission.STUDENT_EDIT, - Permission.STUDENT_LIST, - Permission.TEACHER_LIST, - Permission.TEACHER_CREATE, - Permission.STUDENT_CREATE, - Permission.TEACHER_DELETE, - Permission.STUDENT_DELETE, + describe('When new email already in use by another user', () => { + const setup = () => { + const mockStudentUser = userFactory.buildWithId({ + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const mockAdminUser = userFactory.buildWithId({ + roles: [ + new Role({ + name: RoleName.ADMINISTRATOR, + permissions: [ + Permission.TEACHER_EDIT, + Permission.STUDENT_EDIT, + Permission.STUDENT_LIST, + Permission.TEACHER_LIST, + Permission.TEACHER_CREATE, + Permission.STUDENT_CREATE, + Permission.TEACHER_DELETE, + Permission.STUDENT_DELETE, + ], + }), ], - }), - ], - }); - mockUsers = [ - mockTeacherUser, - mockStudentUser, - mockOtherTeacherUser, - mockAdminUser, - mockExternalUser, - mockOtherExternalUser, - ]; - }); + }); + const mockAdminAccount = accountFactory.buildWithId({ userId: mockAdminUser.id }); + const mockStudentAccount = accountFactory.buildWithId({ userId: mockStudentUser.id }); - describe('isUniqueEmail', () => { - it('should return true if new email is available', async () => { - const res = await accountValidationService.isUniqueEmail('an@available.email'); - expect(res).toBe(true); - }); - it('should return true if new email is available and ignore current user', async () => { - const res = await accountValidationService.isUniqueEmail(mockStudentUser.email, mockStudentUser.id); - expect(res).toBe(true); - }); - it('should return true if new email is available and ignore current users account', async () => { - const res = await accountValidationService.isUniqueEmail( - mockStudentAccount.username, - mockStudentUser.id, - mockStudentAccount.id - ); - expect(res).toBe(true); - }); - it('should return false if new email already in use by another user', async () => { - const res = await accountValidationService.isUniqueEmail( - mockAdminUser.email, - mockStudentUser.id, - mockStudentAccount.id - ); - expect(res).toBe(false); + userRepo.findByEmail.mockResolvedValueOnce([mockAdminUser]); + accountRepo.searchByUsernameExactMatch.mockResolvedValueOnce([[mockAdminAccount], 1]); + + return { mockAdminUser, mockStudentUser, mockStudentAccount }; + }; + it('should return false', async () => { + const { mockAdminUser, mockStudentUser, mockStudentAccount } = setup(); + const res = await accountValidationService.isUniqueEmail( + mockAdminUser.email, + mockStudentUser.id, + mockStudentAccount.id + ); + expect(res).toBe(false); + }); }); - it('should return false if new email is already in use by any user, system id is given', async () => { - const res = await accountValidationService.isUniqueEmail( - mockTeacherAccount.username, - mockStudentUser.id, - mockStudentAccount.id, - mockStudentAccount.systemId?.toString() - ); - expect(res).toBe(false); + + describe('When new email already in use by any user', () => { + const setup = () => { + const mockTeacherUser = userFactory.buildWithId({ + roles: [new Role({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] })], + }); + const mockStudentUser = userFactory.buildWithId({ + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + + const mockTeacherAccount = accountFactory.buildWithId({ userId: mockTeacherUser.id }); + const mockStudentAccount = accountFactory.buildWithId({ userId: mockStudentUser.id }); + + userRepo.findByEmail.mockResolvedValueOnce([mockTeacherUser]); + accountRepo.searchByUsernameExactMatch.mockResolvedValueOnce([[mockTeacherAccount], 1]); + + return { mockTeacherAccount, mockStudentUser, mockStudentAccount }; + }; + it('should return false, system id is given', async () => { + const { mockTeacherAccount, mockStudentUser, mockStudentAccount } = setup(); + const res = await accountValidationService.isUniqueEmail( + mockTeacherAccount.username, + mockStudentUser.id, + mockStudentAccount.id, + mockStudentAccount.systemId?.toString() + ); + expect(res).toBe(false); + }); }); - it('should return false if new email already in use by multiple users', async () => { - const res = await accountValidationService.isUniqueEmail( - 'multiple@user.email', - mockStudentUser.id, - mockStudentAccount.id, - mockStudentAccount.systemId?.toString() - ); - expect(res).toBe(false); + + describe('When new email already in use by multiple users', () => { + const setup = () => { + const mockTeacherUser = userFactory.buildWithId({ + roles: [new Role({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] })], + }); + const mockStudentUser = userFactory.buildWithId({ + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const mockOtherTeacherUser = userFactory.buildWithId({ + roles: [ + new Role({ + name: RoleName.TEACHER, + permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], + }), + ], + }); + const mockStudentAccount = accountFactory.buildWithId({ userId: mockStudentUser.id }); + + const mockUsers = [mockTeacherUser, mockStudentUser, mockOtherTeacherUser]; + + userRepo.findByEmail.mockResolvedValueOnce(mockUsers); + accountRepo.searchByUsernameExactMatch.mockResolvedValueOnce([[], 0]); + + return { mockStudentUser, mockStudentAccount }; + }; + it('should return false', async () => { + const { mockStudentUser, mockStudentAccount } = setup(); + const res = await accountValidationService.isUniqueEmail( + 'multiple@user.email', + mockStudentUser.id, + mockStudentAccount.id, + mockStudentAccount.systemId?.toString() + ); + expect(res).toBe(false); + }); }); - it('should return false if new email already in use by multiple accounts', async () => { - const res = await accountValidationService.isUniqueEmail( - 'multiple@account.username', - mockStudentUser.id, - mockStudentAccount.id, - mockStudentAccount.systemId?.toString() - ); - expect(res).toBe(false); + + describe('When new email already in use by multiple accounts', () => { + const setup = () => { + const mockTeacherUser = userFactory.buildWithId({ + roles: [new Role({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] })], + }); + const mockStudentUser = userFactory.buildWithId({ + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const mockOtherTeacherUser = userFactory.buildWithId({ + roles: [ + new Role({ + name: RoleName.TEACHER, + permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], + }), + ], + }); + + const mockTeacherAccount = accountFactory.buildWithId({ userId: mockTeacherUser.id }); + const mockStudentAccount = accountFactory.buildWithId({ userId: mockStudentUser.id }); + const mockOtherTeacherAccount = accountFactory.buildWithId({ + userId: mockOtherTeacherUser.id, + }); + + const mockAccounts = [mockTeacherAccount, mockStudentAccount, mockOtherTeacherAccount]; + userRepo.findByEmail.mockResolvedValueOnce([]); + accountRepo.searchByUsernameExactMatch.mockResolvedValueOnce([mockAccounts, mockAccounts.length]); + + return { mockStudentUser, mockStudentAccount }; + }; + it('should return false', async () => { + const { mockStudentUser, mockStudentAccount } = setup(); + const res = await accountValidationService.isUniqueEmail( + 'multiple@account.username', + mockStudentUser.id, + mockStudentAccount.id, + mockStudentAccount.systemId?.toString() + ); + expect(res).toBe(false); + }); }); - it('should ignore existing username if other system', async () => { - const res = await accountValidationService.isUniqueEmail( - mockExternalUser.email, - mockExternalUser.id, - mockExternalUserAccount.id, - mockOtherExternalUserAccount.systemId?.toString() - ); - expect(res).toBe(true); + + describe('When its another system', () => { + const setup = () => { + const mockExternalUser = userFactory.buildWithId({ + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + const mockOtherExternalUser = userFactory.buildWithId({ + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + + const externalSystemA = systemFactory.buildWithId(); + const externalSystemB = systemFactory.buildWithId(); + const mockExternalUserAccount = accountFactory.buildWithId({ + userId: mockExternalUser.id, + username: 'unique.within@system', + systemId: externalSystemA.id, + }); + const mockOtherExternalUserAccount = accountFactory.buildWithId({ + userId: mockOtherExternalUser.id, + username: 'unique.within@system', + systemId: externalSystemB.id, + }); + + userRepo.findByEmail.mockResolvedValueOnce([mockExternalUser]); + accountRepo.searchByUsernameExactMatch.mockResolvedValueOnce([[mockExternalUserAccount], 1]); + + return { mockExternalUser, mockExternalUserAccount, mockOtherExternalUserAccount }; + }; + it('should ignore existing username', async () => { + const { mockExternalUser, mockExternalUserAccount, mockOtherExternalUserAccount } = setup(); + const res = await accountValidationService.isUniqueEmail( + mockExternalUser.email, + mockExternalUser.id, + mockExternalUserAccount.id, + mockOtherExternalUserAccount.systemId?.toString() + ); + expect(res).toBe(true); + }); }); }); describe('isUniqueEmailForUser', () => { - it('should return true, if its the email of the given user', async () => { - const res = await accountValidationService.isUniqueEmailForUser(mockStudentUser.email, mockStudentUser.id); - expect(res).toBe(true); + describe('When its the email of the given user', () => { + const setup = () => { + const mockStudentUser = userFactory.buildWithId({ + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + + const mockStudentAccount = accountFactory.buildWithId({ userId: mockStudentUser.id }); + + userRepo.findByEmail.mockResolvedValueOnce([mockStudentUser]); + accountRepo.searchByUsernameExactMatch.mockResolvedValueOnce([[mockStudentAccount], 1]); + accountRepo.findByUserId.mockResolvedValueOnce(mockStudentAccount); + + return { mockStudentUser }; + }; + it('should return true', async () => { + const { mockStudentUser } = setup(); + const res = await accountValidationService.isUniqueEmailForUser(mockStudentUser.email, mockStudentUser.id); + expect(res).toBe(true); + }); }); - it('should return false, if not the given users email', async () => { - const res = await accountValidationService.isUniqueEmailForUser(mockStudentUser.email, mockAdminUser.id); - expect(res).toBe(false); + + describe('When its not the given users email', () => { + const setup = () => { + const mockStudentUser = userFactory.buildWithId({ + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + + const mockStudentAccount = accountFactory.buildWithId({ userId: mockStudentUser.id }); + + const mockAdminUser = userFactory.buildWithId({ + roles: [ + new Role({ + name: RoleName.ADMINISTRATOR, + permissions: [ + Permission.TEACHER_EDIT, + Permission.STUDENT_EDIT, + Permission.STUDENT_LIST, + Permission.TEACHER_LIST, + Permission.TEACHER_CREATE, + Permission.STUDENT_CREATE, + Permission.TEACHER_DELETE, + Permission.STUDENT_DELETE, + ], + }), + ], + }); + const mockAdminAccount = accountFactory.buildWithId({ userId: mockAdminUser.id }); + + userRepo.findByEmail.mockResolvedValueOnce([mockStudentUser]); + accountRepo.searchByUsernameExactMatch.mockResolvedValueOnce([[mockStudentAccount], 1]); + accountRepo.findByUserId.mockResolvedValueOnce(mockAdminAccount); + + return { mockStudentUser, mockAdminUser }; + }; + it('should return false', async () => { + const { mockStudentUser, mockAdminUser } = setup(); + const res = await accountValidationService.isUniqueEmailForUser(mockStudentUser.email, mockAdminUser.id); + expect(res).toBe(false); + }); }); }); describe('isUniqueEmailForAccount', () => { - it('should return true, if its the email of the given user', async () => { - const res = await accountValidationService.isUniqueEmailForAccount(mockStudentUser.email, mockStudentAccount.id); - expect(res).toBe(true); + describe('When its the email of the given user', () => { + const setup = () => { + const mockStudentUser = userFactory.buildWithId({ + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + + const mockStudentAccount = accountFactory.buildWithId({ userId: mockStudentUser.id }); + + userRepo.findByEmail.mockResolvedValueOnce([mockStudentUser]); + accountRepo.searchByUsernameExactMatch.mockResolvedValueOnce([[mockStudentAccount], 1]); + accountRepo.findById.mockResolvedValueOnce(mockStudentAccount); + + return { mockStudentUser, mockStudentAccount }; + }; + it('should return true', async () => { + const { mockStudentUser, mockStudentAccount } = setup(); + const res = await accountValidationService.isUniqueEmailForAccount( + mockStudentUser.email, + mockStudentAccount.id + ); + expect(res).toBe(true); + }); }); - it('should return false, if not the given users email', async () => { - const res = await accountValidationService.isUniqueEmailForAccount(mockStudentUser.email, mockTeacherAccount.id); - expect(res).toBe(false); + describe('When its not the given users email', () => { + const setup = () => { + const mockTeacherUser = userFactory.buildWithId({ + roles: [new Role({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] })], + }); + const mockStudentUser = userFactory.buildWithId({ + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], + }); + + const mockTeacherAccount = accountFactory.buildWithId({ userId: mockTeacherUser.id }); + const mockStudentAccount = accountFactory.buildWithId({ userId: mockStudentUser.id }); + + userRepo.findByEmail.mockResolvedValueOnce([mockStudentUser]); + accountRepo.searchByUsernameExactMatch.mockResolvedValueOnce([[mockStudentAccount], 1]); + accountRepo.findById.mockResolvedValueOnce(mockTeacherAccount); + + return { mockStudentUser, mockTeacherAccount }; + }; + it('should return false', async () => { + const { mockStudentUser, mockTeacherAccount } = setup(); + const res = await accountValidationService.isUniqueEmailForAccount( + mockStudentUser.email, + mockTeacherAccount.id + ); + expect(res).toBe(false); + }); }); - it('should ignore missing user for a given account', async () => { - const res = await accountValidationService.isUniqueEmailForAccount(oprhanAccount.username, oprhanAccount.id); - expect(res).toBe(true); + + describe('When user is missing in account', () => { + const setup = () => { + const oprhanAccount = accountFactory.buildWithId({ + username: 'orphan@account', + userId: undefined, + systemId: new ObjectId(), + }); + + userRepo.findByEmail.mockResolvedValueOnce([]); + accountRepo.searchByUsernameExactMatch.mockResolvedValueOnce([[], 0]); + accountRepo.findById.mockResolvedValueOnce(oprhanAccount); + + return { oprhanAccount }; + }; + it('should ignore missing user for given account', async () => { + const { oprhanAccount } = setup(); + const res = await accountValidationService.isUniqueEmailForAccount(oprhanAccount.username, oprhanAccount.id); + expect(res).toBe(true); + }); }); }); }); From b7b259a86a8bd9b4f2d20fe1cd260329d4da8c41 Mon Sep 17 00:00:00 2001 From: MajedAlaitwniCap Date: Tue, 26 Sep 2023 11:00:52 +0200 Subject: [PATCH 17/24] use setup and new test structure for account service integration test --- .../account.service.integration.spec.ts | 201 +++++++++++------- 1 file changed, 129 insertions(+), 72 deletions(-) diff --git a/apps/server/src/modules/account/services/account.service.integration.spec.ts b/apps/server/src/modules/account/services/account.service.integration.spec.ts index 9ed14236061..e52d069cc32 100644 --- a/apps/server/src/modules/account/services/account.service.integration.spec.ts +++ b/apps/server/src/modules/account/services/account.service.integration.spec.ts @@ -158,95 +158,152 @@ describe('AccountService Integration', () => { ); }; - it('save should create a new account', async () => { - if (!isIdmReachable) return; - const account = await accountService.save(testAccount); - await compareDbAccount(account.id, account); - await compareIdmAccount(account.idmReferenceId ?? '', account); - }); + describe('save', () => { + describe('when account not exists', () => { + it('should create a new account', async () => { + if (!isIdmReachable) return; + const account = await accountService.save(testAccount); + await compareDbAccount(account.id, account); + await compareIdmAccount(account.idmReferenceId ?? '', account); + }); + }); - it('save should update existing account', async () => { - if (!isIdmReachable) return; - const newUsername = 'jane.doe@mail.tld'; - const [dbId, idmId] = await createAccount(); - const originalAccount = await accountService.findById(dbId); - const updatedAccount = await accountService.save({ - ...originalAccount, - username: newUsername, + describe('when account exists', () => { + const setup = async () => { + const newUsername = 'jane.doe@mail.tld'; + const [dbId, idmId] = await createAccount(); + const originalAccount = await accountService.findById(dbId); + return { newUsername, dbId, idmId, originalAccount }; + }; + it('save should update existing account', async () => { + if (!isIdmReachable) return; + const { newUsername, dbId, idmId, originalAccount } = await setup(); + const updatedAccount = await accountService.save({ + ...originalAccount, + username: newUsername, + }); + await compareDbAccount(dbId, updatedAccount); + await compareIdmAccount(idmId, updatedAccount); + }); }); - await compareDbAccount(dbId, updatedAccount); - await compareIdmAccount(idmId, updatedAccount); - }); - it('save should create idm account for existing db account', async () => { - if (!isIdmReachable) return; - const newUsername = 'jane.doe@mail.tld'; - const dbId = await createDbAccount(); - const originalAccount = await accountService.findById(dbId); - const updatedAccount = await accountService.save({ - ...originalAccount, - username: newUsername, + describe('when only db account exists', () => { + const setup = async () => { + const newUsername = 'jane.doe@mail.tld'; + const dbId = await createDbAccount(); + const originalAccount = await accountService.findById(dbId); + return { newUsername, dbId, originalAccount }; + }; + it('should create idm account for existing db account', async () => { + if (!isIdmReachable) return; + const { newUsername, dbId, originalAccount } = await setup(); + + const updatedAccount = await accountService.save({ + ...originalAccount, + username: newUsername, + }); + await compareDbAccount(dbId, updatedAccount); + await compareIdmAccount(updatedAccount.idmReferenceId ?? '', updatedAccount); + }); }); - await compareDbAccount(dbId, updatedAccount); - await compareIdmAccount(updatedAccount.idmReferenceId ?? '', updatedAccount); }); - it('updateUsername should update username', async () => { - if (!isIdmReachable) return; - const newUserName = 'jane.doe@mail.tld'; - const [dbId, idmId] = await createAccount(); - await accountService.updateUsername(dbId, newUserName); + describe('updateUsername', () => { + describe('when upating Username', () => { + const setup = async () => { + const newUsername = 'jane.doe@mail.tld'; + const [dbId, idmId] = await createAccount(); - const foundAccount = await identityManagementService.findAccountById(idmId); - expect(foundAccount).toEqual( - expect.objectContaining>({ - username: newUserName, - }) - ); - const foundDbAccount = await accountRepo.findById(dbId); - expect(foundDbAccount).toEqual( - expect.objectContaining>({ - username: newUserName, - }) - ); + const foundAccount = await identityManagementService.findAccountById(idmId); + const foundDbAccount = await accountRepo.findById(dbId); + + return { newUsername, dbId, idmId, foundAccount, foundDbAccount }; + }; + it('should update username', async () => { + if (!isIdmReachable) return; + const { newUsername, dbId, foundAccount, foundDbAccount } = await setup(); + + await accountService.updateUsername(dbId, newUsername); + + expect(foundAccount).toEqual( + expect.objectContaining>({ + username: newUsername, + }) + ); + expect(foundDbAccount).toEqual( + expect.objectContaining>({ + username: newUsername, + }) + ); + }); + }); }); - it('updatePassword should update password', async () => { - if (!isIdmReachable) return; - const [dbId] = await createAccount(); + describe('updatePassword', () => { + describe('when upating password', () => { + const setup = async () => { + const [dbId] = await createAccount(); + + const foundDbAccountBefore = await accountRepo.findById(dbId); + const previousPasswordHash = foundDbAccountBefore.password; + const foundDbAccountAfter = await accountRepo.findById(dbId); - const foundDbAccountBefore = await accountRepo.findById(dbId); - const previousPasswordHash = foundDbAccountBefore.password; + return { dbId, previousPasswordHash, foundDbAccountAfter }; + }; + it('should update password', async () => { + if (!isIdmReachable) return; + const { dbId, previousPasswordHash, foundDbAccountAfter } = await setup(); - await expect(accountService.updatePassword(dbId, 'newPassword')).resolves.not.toThrow(); + await expect(accountService.updatePassword(dbId, 'newPassword')).resolves.not.toThrow(); - const foundDbAccountAfter = await accountRepo.findById(dbId); - expect(foundDbAccountAfter.password).not.toEqual(previousPasswordHash); + expect(foundDbAccountAfter.password).not.toEqual(previousPasswordHash); + }); + }); }); - it('delete should remove account', async () => { - if (!isIdmReachable) return; - const [dbId, idmId] = await createAccount(); - const foundIdmAccount = await identityManagementService.findAccountById(idmId); - expect(foundIdmAccount).toBeDefined(); - const foundDbAccount = await accountRepo.findById(dbId); - expect(foundDbAccount).toBeDefined(); + describe('delete', () => { + describe('when delete', () => { + const setup = async () => { + const [dbId, idmId] = await createAccount(); + const foundIdmAccount = await identityManagementService.findAccountById(idmId); + const foundDbAccount = await accountRepo.findById(dbId); + + return { dbId, idmId, foundIdmAccount, foundDbAccount }; + }; + it('should remove account', async () => { + if (!isIdmReachable) return; + const { dbId, idmId, foundIdmAccount, foundDbAccount } = await setup(); - await accountService.delete(dbId); - await expect(identityManagementService.findAccountById(idmId)).rejects.toThrow(); - await expect(accountRepo.findById(dbId)).rejects.toThrow(); + expect(foundIdmAccount).toBeDefined(); + expect(foundDbAccount).toBeDefined(); + + await accountService.delete(dbId); + await expect(identityManagementService.findAccountById(idmId)).rejects.toThrow(); + await expect(accountRepo.findById(dbId)).rejects.toThrow(); + }); + }); }); - it('deleteByUserId should remove account', async () => { - if (!isIdmReachable) return; - const [dbId, idmId] = await createAccount(); - const foundAccount = await identityManagementService.findAccountById(idmId); - expect(foundAccount).toBeDefined(); - const foundDbAccount = await accountRepo.findById(dbId); - expect(foundDbAccount).toBeDefined(); + describe('deleteByUserId', () => { + describe('when delete by User Id', () => { + const setup = async () => { + const [dbId, idmId] = await createAccount(); + const foundIdmAccount = await identityManagementService.findAccountById(idmId); + const foundDbAccount = await accountRepo.findById(dbId); - await accountService.deleteByUserId(testAccount.userId ?? ''); - await expect(identityManagementService.findAccountById(idmId)).rejects.toThrow(); - await expect(accountRepo.findById(dbId)).rejects.toThrow(); + return { dbId, idmId, foundIdmAccount, foundDbAccount }; + }; + it('should remove account', async () => { + if (!isIdmReachable) return; + const { dbId, idmId, foundIdmAccount, foundDbAccount } = await setup(); + + expect(foundIdmAccount).toBeDefined(); + expect(foundDbAccount).toBeDefined(); + + await accountService.deleteByUserId(testAccount.userId ?? ''); + await expect(identityManagementService.findAccountById(idmId)).rejects.toThrow(); + await expect(accountRepo.findById(dbId)).rejects.toThrow(); + }); + }); }); }); From 870477f681a7240cc097a6044d4934d1cf859cbc Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Tue, 26 Sep 2023 17:17:41 +0200 Subject: [PATCH 18/24] Use setup method and new test structure in account.service.spec.ts test. --- .../account/services/account.service.spec.ts | 710 +++++++++++------- 1 file changed, 438 insertions(+), 272 deletions(-) diff --git a/apps/server/src/modules/account/services/account.service.spec.ts b/apps/server/src/modules/account/services/account.service.spec.ts index 33dc783d4eb..2246c0de658 100644 --- a/apps/server/src/modules/account/services/account.service.spec.ts +++ b/apps/server/src/modules/account/services/account.service.spec.ts @@ -63,402 +63,568 @@ describe('AccountService', () => { }); describe('findById', () => { - it('should call findById in accountServiceDb', async () => { - await expect(accountService.findById('id')).resolves.not.toThrow(); - expect(accountServiceDb.findById).toHaveBeenCalledTimes(1); + describe('When calling findById in accountService', () => { + it('should call findById in accountServiceDb', async () => { + await expect(accountService.findById('id')).resolves.not.toThrow(); + expect(accountServiceDb.findById).toHaveBeenCalledTimes(1); + }); + }); + + describe('When identity management is primary', () => { + const setup = () => { + configService.get.mockReturnValue(true); + return new AccountService(accountServiceDb, accountServiceIdm, configService, accountValidationService, logger); + }; + + it('should call idm implementation', async () => { + const service = setup(); + await expect(service.findById('accountId')).resolves.not.toThrow(); + expect(accountServiceIdm.findById).toHaveBeenCalledTimes(1); + }); }); }); describe('findByUserId', () => { - it('should call findByUserId in accountServiceDb', async () => { - await expect(accountService.findByUserId('userId')).resolves.not.toThrow(); - expect(accountServiceDb.findByUserId).toHaveBeenCalledTimes(1); + describe('When calling findByUserId in accountService', () => { + it('should call findByUserId in accountServiceDb', async () => { + await expect(accountService.findByUserId('userId')).resolves.not.toThrow(); + expect(accountServiceDb.findByUserId).toHaveBeenCalledTimes(1); + }); + }); + describe('When identity management is primary', () => { + const setup = () => { + configService.get.mockReturnValue(true); + return new AccountService(accountServiceDb, accountServiceIdm, configService, accountValidationService, logger); + }; + + it('should call idm implementation', async () => { + const service = setup(); + await expect(service.findByUserId('userId')).resolves.not.toThrow(); + expect(accountServiceIdm.findByUserId).toHaveBeenCalledTimes(1); + }); }); }); describe('findByUsernameAndSystemId', () => { - it('should call findByUsernameAndSystemId in accountServiceDb', async () => { - await expect(accountService.findByUsernameAndSystemId('username', 'systemId')).resolves.not.toThrow(); - expect(accountServiceDb.findByUsernameAndSystemId).toHaveBeenCalledTimes(1); + describe('When calling findByUsernameAndSystemId in accountService', () => { + it('should call findByUsernameAndSystemId in accountServiceDb', async () => { + await expect(accountService.findByUsernameAndSystemId('username', 'systemId')).resolves.not.toThrow(); + expect(accountServiceDb.findByUsernameAndSystemId).toHaveBeenCalledTimes(1); + }); }); - }); + describe('when identity management is primary', () => { + const setup = () => { + configService.get.mockReturnValue(true); + return new AccountService(accountServiceDb, accountServiceIdm, configService, accountValidationService, logger); + }; - describe('findMultipleByUserId', () => { - it('should call findMultipleByUserId in accountServiceDb', async () => { - await expect(accountService.findMultipleByUserId(['userId1, userId2'])).resolves.not.toThrow(); - expect(accountServiceDb.findMultipleByUserId).toHaveBeenCalledTimes(1); + it('should call idm implementation', async () => { + const service = setup(); + await expect(service.findByUsernameAndSystemId('username', 'systemId')).resolves.not.toThrow(); + expect(accountServiceIdm.findByUsernameAndSystemId).toHaveBeenCalledTimes(1); + }); }); }); - describe('findByUserIdOrFail', () => { - it('should call findByUserIdOrFail in accountServiceDb', async () => { - await expect(accountService.findByUserIdOrFail('userId')).resolves.not.toThrow(); - expect(accountServiceDb.findByUserIdOrFail).toHaveBeenCalledTimes(1); + describe('findMultipleByUserId', () => { + describe('When calling findMultipleByUserId in accountService', () => { + it('should call findMultipleByUserId in accountServiceDb', async () => { + await expect(accountService.findMultipleByUserId(['userId1, userId2'])).resolves.not.toThrow(); + expect(accountServiceDb.findMultipleByUserId).toHaveBeenCalledTimes(1); + }); }); - }); + describe('When identity management is primary', () => { + const setup = () => { + configService.get.mockReturnValue(true); + return new AccountService(accountServiceDb, accountServiceIdm, configService, accountValidationService, logger); + }; - describe('save', () => { - it('should call save in accountServiceDb', async () => { - await expect(accountService.save({} as AccountSaveDto)).resolves.not.toThrow(); - expect(accountServiceDb.save).toHaveBeenCalledTimes(1); + it('should call idm implementation', async () => { + const service = setup(); + await expect(service.findMultipleByUserId(['userId'])).resolves.not.toThrow(); + expect(accountServiceIdm.findMultipleByUserId).toHaveBeenCalledTimes(1); + }); }); - it('should call save in accountServiceIdm if feature is enabled', async () => { - const spy = jest.spyOn(configService, 'get'); - spy.mockReturnValueOnce(true); + }); - await expect(accountService.save({} as AccountSaveDto)).resolves.not.toThrow(); - expect(accountServiceIdm.save).toHaveBeenCalledTimes(1); + describe('findByUserIdOrFail', () => { + describe('When calling findByUserIdOrFail in accountService', () => { + it('should call findByUserIdOrFail in accountServiceDb', async () => { + await expect(accountService.findByUserIdOrFail('userId')).resolves.not.toThrow(); + expect(accountServiceDb.findByUserIdOrFail).toHaveBeenCalledTimes(1); + }); }); - it('should not call save in accountServiceIdm if feature is disabled', async () => { - const spy = jest.spyOn(configService, 'get'); - spy.mockReturnValueOnce(false); + describe('When identity management is primary', () => { + const setup = () => { + configService.get.mockReturnValue(true); + return new AccountService(accountServiceDb, accountServiceIdm, configService, accountValidationService, logger); + }; - await expect(accountService.save({} as AccountSaveDto)).resolves.not.toThrow(); - expect(accountServiceIdm.save).not.toHaveBeenCalled(); + it('should call idm implementation', async () => { + const service = setup(); + await expect(service.findByUserIdOrFail('userId')).resolves.not.toThrow(); + expect(accountServiceIdm.findByUserIdOrFail).toHaveBeenCalledTimes(1); + }); }); }); - describe('saveWithValidation', () => { - it('should not sanitize username for external user', async () => { - const spy = jest.spyOn(accountService, 'save'); - const params: AccountSaveDto = { - username: ' John.Doe@domain.tld ', - systemId: 'ABC123', - }; - await accountService.saveWithValidation(params); - expect(spy).toHaveBeenCalledWith( - expect.objectContaining({ - username: ' John.Doe@domain.tld ', - }) - ); - spy.mockRestore(); - }); - it('should throw if username for a local user is not an email', async () => { - const params: AccountSaveDto = { - username: 'John Doe', - password: 'JohnsPassword', - }; - await expect(accountService.saveWithValidation(params)).rejects.toThrow('Username is not an email'); - }); - it('should not throw if username for an external user is not an email', async () => { - const params: AccountSaveDto = { - username: 'John Doe', - systemId: 'ABC123', - }; - await expect(accountService.saveWithValidation(params)).resolves.not.toThrow(); + describe('save', () => { + describe('When calling save in accountService', () => { + it('should call save in accountServiceDb', async () => { + await expect(accountService.save({} as AccountSaveDto)).resolves.not.toThrow(); + expect(accountServiceDb.save).toHaveBeenCalledTimes(1); + }); }); - it('should not throw if username for an external user is a ldap search string', async () => { - const params: AccountSaveDto = { - username: 'dc=schul-cloud,dc=org/fake.ldap', - systemId: 'ABC123', + describe('When calling save in accountService if feature is enabled', () => { + const setup = () => { + configService.get.mockReturnValueOnce(true); }; - await expect(accountService.saveWithValidation(params)).resolves.not.toThrow(); + it('should call save in accountServiceIdm', async () => { + setup(); + + await expect(accountService.save({} as AccountSaveDto)).resolves.not.toThrow(); + expect(accountServiceIdm.save).toHaveBeenCalledTimes(1); + }); }); - it('should throw if no password is provided for an internal user', async () => { - const params: AccountSaveDto = { - username: 'john.doe@mail.tld', + describe('When calling save in accountService if feature is disabled', () => { + const setup = () => { + configService.get.mockReturnValueOnce(false); }; - await expect(accountService.saveWithValidation(params)).rejects.toThrow('No password provided'); + it('should not call save in accountServiceIdm', async () => { + setup(); + + await expect(accountService.save({} as AccountSaveDto)).resolves.not.toThrow(); + expect(accountServiceIdm.save).not.toHaveBeenCalled(); + }); }); - it('should throw if account already exists', async () => { - const params: AccountSaveDto = { - username: 'john.doe@mail.tld', - password: 'JohnsPassword', - userId: 'userId123', + describe('when identity management is primary', () => { + const setup = () => { + configService.get.mockReturnValue(true); + return new AccountService(accountServiceDb, accountServiceIdm, configService, accountValidationService, logger); }; - accountServiceDb.findByUserId.mockResolvedValueOnce({ id: 'foundAccount123' } as AccountDto); - await expect(accountService.saveWithValidation(params)).rejects.toThrow('Account already exists'); - }); - it('should throw if username already exists', async () => { - const accountIsUniqueEmailSpy = jest.spyOn(accountValidationService, 'isUniqueEmail'); - accountIsUniqueEmailSpy.mockResolvedValueOnce(false); - const params: AccountSaveDto = { - username: 'john.doe@mail.tld', - password: 'JohnsPassword', - }; - await expect(accountService.saveWithValidation(params)).rejects.toThrow('Username already exists'); + it('should call idm implementation', async () => { + setup(); + await expect(accountService.save({ username: 'username' })).resolves.not.toThrow(); + expect(accountServiceIdm.save).toHaveBeenCalledTimes(1); + }); }); }); - describe('updateUsername', () => { - it('should call updateUsername in accountServiceDb', async () => { - await expect(accountService.updateUsername('accountId', 'username')).resolves.not.toThrow(); - expect(accountServiceDb.updateUsername).toHaveBeenCalledTimes(1); + describe('saveWithValidation', () => { + describe('When calling saveWithValidation on accountService', () => { + const setup = () => { + const spy = jest.spyOn(accountService, 'save'); + return spy; + }; + it('should not sanitize username for external user', async () => { + const spy = setup(); + + const params: AccountSaveDto = { + username: ' John.Doe@domain.tld ', + systemId: 'ABC123', + }; + await accountService.saveWithValidation(params); + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + username: ' John.Doe@domain.tld ', + }) + ); + spy.mockRestore(); + }); }); - it('should call updateUsername in accountServiceIdm if feature is enabled', async () => { - const spy = jest.spyOn(configService, 'get'); - spy.mockReturnValueOnce(true); - await expect(accountService.updateUsername('accountId', 'username')).resolves.not.toThrow(); - expect(accountServiceIdm.updateUsername).toHaveBeenCalledTimes(1); + describe('When username for a local user is not an email', () => { + it('should throw', async () => { + const params: AccountSaveDto = { + username: 'John Doe', + password: 'JohnsPassword', + }; + await expect(accountService.saveWithValidation(params)).rejects.toThrow('Username is not an email'); + }); }); - it('should not call updateUsername in accountServiceIdm if feature is disabled', async () => { - const spy = jest.spyOn(configService, 'get'); - spy.mockReturnValueOnce(false); - await expect(accountService.updateUsername('accountId', 'username')).resolves.not.toThrow(); - expect(accountServiceIdm.updateUsername).not.toHaveBeenCalled(); + describe('When username for an external user is not an email', () => { + it('should not throw', async () => { + const params: AccountSaveDto = { + username: 'John Doe', + systemId: 'ABC123', + }; + await expect(accountService.saveWithValidation(params)).resolves.not.toThrow(); + }); }); - }); - describe('updateLastTriedFailedLogin', () => { - it('should call updateLastTriedFailedLogin in accountServiceDb', async () => { - await expect(accountService.updateLastTriedFailedLogin('accountId', {} as Date)).resolves.not.toThrow(); - expect(accountServiceDb.updateLastTriedFailedLogin).toHaveBeenCalledTimes(1); + describe('When username for an external user is a ldap search string', () => { + it('should not throw', async () => { + const params: AccountSaveDto = { + username: 'dc=schul-cloud,dc=org/fake.ldap', + systemId: 'ABC123', + }; + await expect(accountService.saveWithValidation(params)).resolves.not.toThrow(); + }); }); - }); - describe('updatePassword', () => { - it('should call updatePassword in accountServiceDb', async () => { - await expect(accountService.updatePassword('accountId', 'password')).resolves.not.toThrow(); - expect(accountServiceDb.updatePassword).toHaveBeenCalledTimes(1); + describe('When no password is provided for an internal user', () => { + it('should throw', async () => { + const params: AccountSaveDto = { + username: 'john.doe@mail.tld', + }; + await expect(accountService.saveWithValidation(params)).rejects.toThrow('No password provided'); + }); }); - it('should call updatePassword in accountServiceIdm if feature is enabled', async () => { - const spy = jest.spyOn(configService, 'get'); - spy.mockReturnValueOnce(true); - await expect(accountService.updatePassword('accountId', 'password')).resolves.not.toThrow(); - expect(accountServiceIdm.updatePassword).toHaveBeenCalledTimes(1); + describe('When account already exists', () => { + it('should throw', async () => { + const params: AccountSaveDto = { + username: 'john.doe@mail.tld', + password: 'JohnsPassword', + userId: 'userId123', + }; + accountServiceDb.findByUserId.mockResolvedValueOnce({ id: 'foundAccount123' } as AccountDto); + await expect(accountService.saveWithValidation(params)).rejects.toThrow('Account already exists'); + }); }); - it('should not call updatePassword in accountServiceIdm if feature is disabled', async () => { - const spy = jest.spyOn(configService, 'get'); - spy.mockReturnValueOnce(false); - await expect(accountService.updatePassword('accountId', 'password')).resolves.not.toThrow(); - expect(accountServiceIdm.updatePassword).not.toHaveBeenCalled(); + describe('When username already exists', () => { + const setup = () => { + accountValidationService.isUniqueEmail.mockResolvedValueOnce(false); + }; + it('should throw', async () => { + setup(); + const params: AccountSaveDto = { + username: 'john.doe@mail.tld', + password: 'JohnsPassword', + }; + await expect(accountService.saveWithValidation(params)).rejects.toThrow('Username already exists'); + }); }); - }); + describe('When identity management is primary', () => { + const setup = () => { + configService.get.mockReturnValue(true); + return new AccountService(accountServiceDb, accountServiceIdm, configService, accountValidationService, logger); + }; - describe('validatePassword', () => { - const setup = () => { - configService.get.mockReturnValue(true); - return new AccountService(accountServiceDb, accountServiceIdm, configService, accountValidationService, logger); - }; - it('should call validatePassword in accountServiceDb', async () => { - await expect(accountService.validatePassword({} as AccountDto, 'password')).resolves.not.toThrow(); - expect(accountServiceIdm.validatePassword).toHaveBeenCalledTimes(0); - expect(accountServiceDb.validatePassword).toHaveBeenCalledTimes(1); - }); - it('should call validatePassword in accountServiceIdm if feature is enabled', async () => { - const service = setup(); - await expect(service.validatePassword({} as AccountDto, 'password')).resolves.not.toThrow(); - expect(accountServiceDb.validatePassword).toHaveBeenCalledTimes(0); - expect(accountServiceIdm.validatePassword).toHaveBeenCalledTimes(1); + it('should call idm implementation', async () => { + setup(); + await expect( + accountService.saveWithValidation({ username: 'username@mail.tld', password: 'password' }) + ).resolves.not.toThrow(); + expect(accountServiceIdm.save).toHaveBeenCalledTimes(1); + }); }); }); - describe('delete', () => { - it('should call delete in accountServiceDb', async () => { - await expect(accountService.delete('accountId')).resolves.not.toThrow(); - expect(accountServiceDb.delete).toHaveBeenCalledTimes(1); + describe('updateUsername', () => { + describe('When calling updateUsername in accountService', () => { + it('should call updateUsername in accountServiceDb', async () => { + await expect(accountService.updateUsername('accountId', 'username')).resolves.not.toThrow(); + expect(accountServiceDb.updateUsername).toHaveBeenCalledTimes(1); + }); }); - it('should call delete in accountServiceIdm if feature is enabled', async () => { - const spy = jest.spyOn(configService, 'get'); - spy.mockReturnValueOnce(true); - await expect(accountService.delete('accountId')).resolves.not.toThrow(); - expect(accountServiceIdm.delete).toHaveBeenCalledTimes(1); - }); - it('should not call delete in accountServiceIdm if feature is disabled', async () => { - const spy = jest.spyOn(configService, 'get'); - spy.mockReturnValueOnce(false); + describe('When calling updateUsername in accountService if idm feature is enabled', () => { + const setup = () => { + configService.get.mockReturnValueOnce(true); + }; + it('should call updateUsername in accountServiceIdm', async () => { + setup(); - await expect(accountService.delete('accountId')).resolves.not.toThrow(); - expect(accountServiceIdm.delete).not.toHaveBeenCalled(); + await expect(accountService.updateUsername('accountId', 'username')).resolves.not.toThrow(); + expect(accountServiceIdm.updateUsername).toHaveBeenCalledTimes(1); + }); }); - }); - describe('deleteByUserId', () => { - it('should call deleteByUserId in accountServiceDb', async () => { - await expect(accountService.deleteByUserId('userId')).resolves.not.toThrow(); - expect(accountServiceDb.deleteByUserId).toHaveBeenCalledTimes(1); - }); - it('should call deleteByUserId in accountServiceIdm if feature is enabled', async () => { - const spy = jest.spyOn(configService, 'get'); - spy.mockReturnValueOnce(true); + describe('When calling updateUsername in accountService if idm feature is disabled', () => { + const setup = () => { + configService.get.mockReturnValueOnce(false); + }; + it('should not call updateUsername in accountServiceIdm', async () => { + setup(); - await expect(accountService.deleteByUserId('userId')).resolves.not.toThrow(); - expect(accountServiceIdm.deleteByUserId).toHaveBeenCalledTimes(1); + await expect(accountService.updateUsername('accountId', 'username')).resolves.not.toThrow(); + expect(accountServiceIdm.updateUsername).not.toHaveBeenCalled(); + }); }); - it('should not call deleteByUserId in accountServiceIdm if feature is disabled', async () => { - const spy = jest.spyOn(configService, 'get'); - spy.mockReturnValueOnce(false); + describe('When identity management is primary', () => { + const setup = () => { + configService.get.mockReturnValue(true); + return new AccountService(accountServiceDb, accountServiceIdm, configService, accountValidationService, logger); + }; - await expect(accountService.deleteByUserId('userId')).resolves.not.toThrow(); - expect(accountServiceIdm.deleteByUserId).not.toHaveBeenCalled(); + it('should call idm implementation', async () => { + setup(); + await expect(accountService.updateUsername('accountId', 'username')).resolves.not.toThrow(); + expect(accountServiceIdm.updateUsername).toHaveBeenCalledTimes(1); + }); }); }); - describe('findMany', () => { - it('should call findMany in accountServiceDb', async () => { - await expect(accountService.findMany()).resolves.not.toThrow(); - expect(accountServiceDb.findMany).toHaveBeenCalledTimes(1); + describe('updateLastTriedFailedLogin', () => { + describe('When calling updateLastTriedFailedLogin in accountService', () => { + it('should call updateLastTriedFailedLogin in accountServiceDb', async () => { + await expect(accountService.updateLastTriedFailedLogin('accountId', {} as Date)).resolves.not.toThrow(); + expect(accountServiceDb.updateLastTriedFailedLogin).toHaveBeenCalledTimes(1); + }); }); - }); + describe('when identity management is primary', () => { + const setup = () => { + configService.get.mockReturnValue(true); + return new AccountService(accountServiceDb, accountServiceIdm, configService, accountValidationService, logger); + }; - describe('searchByUsernamePartialMatch', () => { - it('should call searchByUsernamePartialMatch in accountServiceDb', async () => { - await expect(accountService.searchByUsernamePartialMatch('username', 1, 1)).resolves.not.toThrow(); - expect(accountServiceDb.searchByUsernamePartialMatch).toHaveBeenCalledTimes(1); + it('should call idm implementation', async () => { + setup(); + await expect(accountService.updateLastTriedFailedLogin('accountId', new Date())).resolves.not.toThrow(); + expect(accountServiceIdm.updateLastTriedFailedLogin).toHaveBeenCalledTimes(1); + }); }); }); - describe('searchByUsernameExactMatch', () => { - it('should call searchByUsernameExactMatch in accountServiceDb', async () => { - await expect(accountService.searchByUsernameExactMatch('username')).resolves.not.toThrow(); - expect(accountServiceDb.searchByUsernameExactMatch).toHaveBeenCalledTimes(1); + describe('updatePassword', () => { + describe('When calling updatePassword in accountService', () => { + it('should call updatePassword in accountServiceDb', async () => { + await expect(accountService.updatePassword('accountId', 'password')).resolves.not.toThrow(); + expect(accountServiceDb.updatePassword).toHaveBeenCalledTimes(1); + }); }); - }); - describe('executeIdmMethod', () => { - it('should throw an error object', async () => { - const spy = jest.spyOn(configService, 'get'); - spy.mockReturnValueOnce(true); - const spyLogger = jest.spyOn(logger, 'error'); - const testError = new Error('error'); + describe('When calling updatePassword in accountService if feature is enabled', () => { + const setup = () => { + configService.get.mockReturnValueOnce(true); + }; + it('should call updatePassword in accountServiceIdm', async () => { + setup(); - const deleteByUserIdMock = jest.spyOn(accountServiceIdm, 'deleteByUserId'); - deleteByUserIdMock.mockImplementationOnce(() => { - throw testError; + await expect(accountService.updatePassword('accountId', 'password')).resolves.not.toThrow(); + expect(accountServiceIdm.updatePassword).toHaveBeenCalledTimes(1); }); - - await expect(accountService.deleteByUserId('userId')).resolves.not.toThrow(); - expect(spyLogger).toHaveBeenCalledWith(testError, expect.anything()); }); - it('should throw an non error object', async () => { - const spy = jest.spyOn(configService, 'get'); - spy.mockReturnValueOnce(true); - const spyLogger = jest.spyOn(logger, 'error'); + describe('When calling updatePassword in accountService if feature is disabled', () => { + const setup = () => { + configService.get.mockReturnValueOnce(false); + }; + it('should not call updatePassword in accountServiceIdm', async () => { + setup(); - const deleteByUserIdMock = jest.spyOn(accountServiceIdm, 'deleteByUserId'); - deleteByUserIdMock.mockImplementationOnce(() => { - // eslint-disable-next-line @typescript-eslint/no-throw-literal - throw 'a non error object'; + await expect(accountService.updatePassword('accountId', 'password')).resolves.not.toThrow(); + expect(accountServiceIdm.updatePassword).not.toHaveBeenCalled(); }); + }); + describe('When identity management is primary', () => { + const setup = () => { + configService.get.mockReturnValue(true); + return new AccountService(accountServiceDb, accountServiceIdm, configService, accountValidationService, logger); + }; - await expect(accountService.deleteByUserId('userId')).resolves.not.toThrow(); - expect(spyLogger).toHaveBeenCalledWith('a non error object'); + it('should call idm implementation', async () => { + setup(); + await expect(accountService.updatePassword('accountId', 'password')).resolves.not.toThrow(); + expect(accountServiceIdm.updatePassword).toHaveBeenCalledTimes(1); + }); }); }); - describe('when identity management is primary', () => { - const setup = () => { - configService.get.mockReturnValue(true); - return new AccountService(accountServiceDb, accountServiceIdm, configService, accountValidationService, logger); - }; - - describe('findById', () => { - it('should call idm implementation', async () => { - const service = setup(); - await expect(service.findById('accountId')).resolves.not.toThrow(); - expect(accountServiceIdm.findById).toHaveBeenCalledTimes(1); + describe('validatePassword', () => { + describe('When calling validatePassword in accountService', () => { + it('should call validatePassword in accountServiceDb', async () => { + await expect(accountService.validatePassword({} as AccountDto, 'password')).resolves.not.toThrow(); + expect(accountServiceIdm.validatePassword).toHaveBeenCalledTimes(0); + expect(accountServiceDb.validatePassword).toHaveBeenCalledTimes(1); }); }); - describe('findMultipleByUserId', () => { - it('should call idm implementation', async () => { + describe('When calling validatePassword in accountService if feature is enabled', () => { + const setup = () => { + configService.get.mockReturnValue(true); + return new AccountService(accountServiceDb, accountServiceIdm, configService, accountValidationService, logger); + }; + it('should call validatePassword in accountServiceIdm', async () => { const service = setup(); - await expect(service.findMultipleByUserId(['userId'])).resolves.not.toThrow(); - expect(accountServiceIdm.findMultipleByUserId).toHaveBeenCalledTimes(1); + await expect(service.validatePassword({} as AccountDto, 'password')).resolves.not.toThrow(); + expect(accountServiceDb.validatePassword).toHaveBeenCalledTimes(0); + expect(accountServiceIdm.validatePassword).toHaveBeenCalledTimes(1); }); }); + }); - describe('findByUserId', () => { - it('should call idm implementation', async () => { - const service = setup(); - await expect(service.findByUserId('userId')).resolves.not.toThrow(); - expect(accountServiceIdm.findByUserId).toHaveBeenCalledTimes(1); + describe('delete', () => { + describe('When calling delete in accountService', () => { + it('should call delete in accountServiceDb', async () => { + await expect(accountService.delete('accountId')).resolves.not.toThrow(); + expect(accountServiceDb.delete).toHaveBeenCalledTimes(1); }); }); - describe('findByUserIdOrFail', () => { - it('should call idm implementation', async () => { - const service = setup(); - await expect(service.findByUserIdOrFail('userId')).resolves.not.toThrow(); - expect(accountServiceIdm.findByUserIdOrFail).toHaveBeenCalledTimes(1); + describe('When calling delete in accountService if feature is enabled', () => { + const setup = () => { + configService.get.mockReturnValueOnce(true); + }; + it('should call delete in accountServiceIdm', async () => { + setup(); + + await expect(accountService.delete('accountId')).resolves.not.toThrow(); + expect(accountServiceIdm.delete).toHaveBeenCalledTimes(1); }); }); - describe('findByUsernameAndSystemId', () => { - it('should call idm implementation', async () => { - const service = setup(); - await expect(service.findByUsernameAndSystemId('username', 'systemId')).resolves.not.toThrow(); - expect(accountServiceIdm.findByUsernameAndSystemId).toHaveBeenCalledTimes(1); + describe('When calling delete in accountService if feature is disabled', () => { + const setup = () => { + configService.get.mockReturnValueOnce(false); + }; + it('should not call delete in accountServiceIdm', async () => { + setup(); + + await expect(accountService.delete('accountId')).resolves.not.toThrow(); + expect(accountServiceIdm.delete).not.toHaveBeenCalled(); }); }); + describe('When identity management is primary', () => { + const setup = () => { + configService.get.mockReturnValue(true); + return new AccountService(accountServiceDb, accountServiceIdm, configService, accountValidationService, logger); + }; - describe('searchByUsernamePartialMatch', () => { it('should call idm implementation', async () => { - const service = setup(); - await expect(service.searchByUsernamePartialMatch('username', 0, 1)).resolves.not.toThrow(); - expect(accountServiceIdm.searchByUsernamePartialMatch).toHaveBeenCalledTimes(1); + setup(); + await expect(accountService.delete('accountId')).resolves.not.toThrow(); + expect(accountServiceIdm.delete).toHaveBeenCalledTimes(1); }); }); + }); - describe('searchByUsernameExactMatch', () => { - it('should call idm implementation', async () => { - const service = setup(); - await expect(service.searchByUsernameExactMatch('username')).resolves.not.toThrow(); - expect(accountServiceIdm.searchByUsernameExactMatch).toHaveBeenCalledTimes(1); + describe('deleteByUserId', () => { + describe('When calling deleteByUserId in accountService', () => { + it('should call deleteByUserId in accountServiceDb', async () => { + await expect(accountService.deleteByUserId('userId')).resolves.not.toThrow(); + expect(accountServiceDb.deleteByUserId).toHaveBeenCalledTimes(1); }); }); - describe('save', () => { - it('should call idm implementation', async () => { + describe('When calling deleteByUserId in accountService if feature is enabled', () => { + const setup = () => { + configService.get.mockReturnValueOnce(true); + }; + it('should call deleteByUserId in accountServiceIdm', async () => { setup(); - await expect(accountService.save({ username: 'username' })).resolves.not.toThrow(); - expect(accountServiceIdm.save).toHaveBeenCalledTimes(1); + + await expect(accountService.deleteByUserId('userId')).resolves.not.toThrow(); + expect(accountServiceIdm.deleteByUserId).toHaveBeenCalledTimes(1); }); }); - describe('saveWithValidation', () => { - it('should call idm implementation', async () => { + describe('When calling deleteByUserId in accountService if feature is disabled', () => { + const setup = () => { + configService.get.mockReturnValueOnce(false); + }; + it('should not call deleteByUserId in accountServiceIdm', async () => { setup(); - await expect( - accountService.saveWithValidation({ username: 'username@mail.tld', password: 'password' }) - ).resolves.not.toThrow(); - expect(accountServiceIdm.save).toHaveBeenCalledTimes(1); + + await expect(accountService.deleteByUserId('userId')).resolves.not.toThrow(); + expect(accountServiceIdm.deleteByUserId).not.toHaveBeenCalled(); }); }); + describe('When identity management is primary', () => { + const setup = () => { + configService.get.mockReturnValue(true); + return new AccountService(accountServiceDb, accountServiceIdm, configService, accountValidationService, logger); + }; - describe('updateUsername', () => { it('should call idm implementation', async () => { setup(); - await expect(accountService.updateUsername('accountId', 'username')).resolves.not.toThrow(); - expect(accountServiceIdm.updateUsername).toHaveBeenCalledTimes(1); + await expect(accountService.deleteByUserId('userId')).resolves.not.toThrow(); + expect(accountServiceIdm.deleteByUserId).toHaveBeenCalledTimes(1); }); }); + }); - describe('updateLastTriedFailedLogin', () => { - it('should call idm implementation', async () => { - setup(); - await expect(accountService.updateLastTriedFailedLogin('accountId', new Date())).resolves.not.toThrow(); - expect(accountServiceIdm.updateLastTriedFailedLogin).toHaveBeenCalledTimes(1); + describe('findMany', () => { + describe('When calling findMany in accountService', () => { + it('should call findMany in accountServiceDb', async () => { + await expect(accountService.findMany()).resolves.not.toThrow(); + expect(accountServiceDb.findMany).toHaveBeenCalledTimes(1); }); }); + }); - describe('updatePassword', () => { - it('should call idm implementation', async () => { - setup(); - await expect(accountService.updatePassword('accountId', 'password')).resolves.not.toThrow(); - expect(accountServiceIdm.updatePassword).toHaveBeenCalledTimes(1); + describe('searchByUsernamePartialMatch', () => { + describe('When calling searchByUsernamePartialMatch in accountService', () => { + it('should call searchByUsernamePartialMatch in accountServiceDb', async () => { + await expect(accountService.searchByUsernamePartialMatch('username', 1, 1)).resolves.not.toThrow(); + expect(accountServiceDb.searchByUsernamePartialMatch).toHaveBeenCalledTimes(1); }); }); + describe('when identity management is primary', () => { + const setup = () => { + configService.get.mockReturnValue(true); + return new AccountService(accountServiceDb, accountServiceIdm, configService, accountValidationService, logger); + }; - describe('delete', () => { it('should call idm implementation', async () => { - setup(); - await expect(accountService.delete('accountId')).resolves.not.toThrow(); - expect(accountServiceIdm.delete).toHaveBeenCalledTimes(1); + const service = setup(); + await expect(service.searchByUsernamePartialMatch('username', 0, 1)).resolves.not.toThrow(); + expect(accountServiceIdm.searchByUsernamePartialMatch).toHaveBeenCalledTimes(1); }); }); + }); + + describe('searchByUsernameExactMatch', () => { + describe('When calling searchByUsernameExactMatch in accountService', () => { + it('should call searchByUsernameExactMatch in accountServiceDb', async () => { + await expect(accountService.searchByUsernameExactMatch('username')).resolves.not.toThrow(); + expect(accountServiceDb.searchByUsernameExactMatch).toHaveBeenCalledTimes(1); + }); + }); + describe('when identity management is primary', () => { + const setup = () => { + configService.get.mockReturnValue(true); + return new AccountService(accountServiceDb, accountServiceIdm, configService, accountValidationService, logger); + }; - describe('deleteByUserId', () => { it('should call idm implementation', async () => { - setup(); + const service = setup(); + await expect(service.searchByUsernameExactMatch('username')).resolves.not.toThrow(); + expect(accountServiceIdm.searchByUsernameExactMatch).toHaveBeenCalledTimes(1); + }); + }); + }); + + describe('executeIdmMethod', () => { + describe('When idm feature is enabled', () => { + const setup = () => { + configService.get.mockReturnValueOnce(true); + const testError = new Error('error'); + accountServiceIdm.deleteByUserId.mockImplementationOnce(() => { + throw testError; + }); + + const spyLogger = jest.spyOn(logger, 'error'); + + return { testError, spyLogger }; + }; + it('should call executeIdmMethod and throw an error object', async () => { + const { testError, spyLogger } = setup(); + await expect(accountService.deleteByUserId('userId')).resolves.not.toThrow(); - expect(accountServiceIdm.deleteByUserId).toHaveBeenCalledTimes(1); + expect(spyLogger).toHaveBeenCalledWith(testError, expect.anything()); + }); + }); + + describe('When idm feature is enabled', () => { + const setup = () => { + configService.get.mockReturnValueOnce(true); + const spyLogger = jest.spyOn(logger, 'error'); + const deleteByUserIdMock = jest.spyOn(accountServiceIdm, 'deleteByUserId'); + deleteByUserIdMock.mockImplementationOnce(() => { + // eslint-disable-next-line @typescript-eslint/no-throw-literal + throw 'a non error object'; + }); + return { spyLogger }; + }; + it('should call executeIdmMethod and throw an error object', async () => { + const { spyLogger } = setup(); + + await expect(accountService.deleteByUserId('userId')).resolves.not.toThrow(); + expect(spyLogger).toHaveBeenCalledWith('a non error object'); }); }); }); From e2e2eb35de6d72f48b0a02543638eef6f4e90261 Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Wed, 27 Sep 2023 09:40:53 +0200 Subject: [PATCH 19/24] Fix order in account.service.integration.spec.ts --- .../services/account.service.integration.spec.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/server/src/modules/account/services/account.service.integration.spec.ts b/apps/server/src/modules/account/services/account.service.integration.spec.ts index 9a0169cd536..226fc5bfac2 100644 --- a/apps/server/src/modules/account/services/account.service.integration.spec.ts +++ b/apps/server/src/modules/account/services/account.service.integration.spec.ts @@ -210,21 +210,20 @@ describe('AccountService Integration', () => { }); describe('updateUsername', () => { - describe('when upating Username', () => { + describe('when updating Username', () => { const setup = async () => { const newUsername = 'jane.doe@mail.tld'; const [dbId, idmId] = await createAccount(); - const foundAccount = await identityManagementService.findAccountById(idmId); - const foundDbAccount = await accountRepo.findById(dbId); - - return { newUsername, dbId, idmId, foundAccount, foundDbAccount }; + return { newUsername, dbId, idmId }; }; it('should update username', async () => { if (!isIdmReachable) return; - const { newUsername, dbId, foundAccount, foundDbAccount } = await setup(); + const { newUsername, dbId, idmId } = await setup(); await accountService.updateUsername(dbId, newUsername); + const foundAccount = await identityManagementService.findAccountById(idmId); + const foundDbAccount = await accountRepo.findById(dbId); expect(foundAccount).toEqual( expect.objectContaining>({ @@ -241,7 +240,7 @@ describe('AccountService Integration', () => { }); describe('updatePassword', () => { - describe('when upating password', () => { + describe('when updating password', () => { const setup = async () => { const [dbId] = await createAccount(); From 0f2bbe01dc6c81b9473686a1f7f9e4b3c0a26638 Mon Sep 17 00:00:00 2001 From: Maximilian Kreuzkam Date: Wed, 27 Sep 2023 10:05:04 +0200 Subject: [PATCH 20/24] Remove unnecessary tests and resolved todos. --- .../controller/api-test/account.api.spec.ts | 42 ------------------- .../controller/dto/password-pattern.ts | 2 - .../src/modules/account/review-comments.md | 1 - 3 files changed, 45 deletions(-) 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 63f7a8f7e26..3250da07c4d 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 @@ -398,28 +398,6 @@ describe('Account Controller (API)', () => { await loggedInClient.get(`/${studentAccount.id}`).expect(200); }); }); - describe('When searching with a superhero user', () => { - const setup = async () => { - const school = schoolFactory.buildWithId(); - - const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); - const superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); - const superheroAccount = mapUserToAccount(superheroUser); - - em.persist([school, superheroRoles, superheroUser, superheroAccount]); - await em.flush(); - - const loggedInClient = await testApiClient.login(superheroAccount); - - return { loggedInClient }; - }; - - it('should reject if id has invalid format', async () => { - const { loggedInClient } = await setup(); - // TODO soll nicht 404 sondern 400 - await loggedInClient.get(`/qwerty`).send().expect(404); - }); - }); describe('When searching with a not authorized user', () => { const setup = async () => { @@ -607,26 +585,6 @@ describe('Account Controller (API)', () => { }); }); - describe('When using a superhero user', () => { - const setup = async () => { - const school = schoolFactory.buildWithId(); - const superheroRoles = roleFactory.build({ name: RoleName.SUPERHERO, permissions: [] }); - const superheroUser = userFactory.buildWithId({ roles: [superheroRoles] }); - const superheroAccount = mapUserToAccount(superheroUser); - - em.persist([school, superheroRoles, superheroUser, superheroAccount]); - await em.flush(); - - const loggedInClient = await testApiClient.login(superheroAccount); - - return { loggedInClient }; - }; - it('should reject invalid account id format', async () => { - const { loggedInClient } = await setup(); - // TODO soll nicht 404 sondern 400 - await loggedInClient.delete('/qwerty').expect(404); - }); - }); describe('When using a not authorized (admin) user', () => { const setup = async () => { const school = schoolFactory.buildWithId(); diff --git a/apps/server/src/modules/account/controller/dto/password-pattern.ts b/apps/server/src/modules/account/controller/dto/password-pattern.ts index bc41132265c..d849b8db235 100644 --- a/apps/server/src/modules/account/controller/dto/password-pattern.ts +++ b/apps/server/src/modules/account/controller/dto/password-pattern.ts @@ -1,3 +1 @@ -// TODO: check if this has happened -// TODO: Compare with client export const passwordPattern = /^(?=.*[A-Z])(?=.*[0-9])(?=.*[a-z])(?=.*[-_!<>§$%&/()=?\\;:,.#+*~'])\S.{6,253}\S$/; diff --git a/apps/server/src/modules/account/review-comments.md b/apps/server/src/modules/account/review-comments.md index 215e6522d4b..fc636019cdd 100644 --- a/apps/server/src/modules/account/review-comments.md +++ b/apps/server/src/modules/account/review-comments.md @@ -4,7 +4,6 @@ - write an md file or flow diagram describing how things work - in what layer do the services belong? -- look at ALL spec files, adjust to test structure - naming of DO vs Entity (DO is the leading, "Account", entity is just the datalayer representation "AccountEntity") - new decisions for loggables From 64a875ff6fb0ad8b5d2e5f4841ed273a156f064c Mon Sep 17 00:00:00 2001 From: MajedAlaitwniCap Date: Wed, 27 Sep 2023 11:16:39 +0200 Subject: [PATCH 21/24] resolve security hotspot issue --- apps/server/src/shared/testing/factory/account.factory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/shared/testing/factory/account.factory.ts b/apps/server/src/shared/testing/factory/account.factory.ts index 0ae84459449..5c4bd173b49 100644 --- a/apps/server/src/shared/testing/factory/account.factory.ts +++ b/apps/server/src/shared/testing/factory/account.factory.ts @@ -30,7 +30,7 @@ class AccountFactory extends BaseFactory { credentialHash: 'credentialHash', expiresAt: new Date(), lasttriedFailedLogin: new Date(), - password: 'password', + password: '$2a$10$/DsztV5o6P5piW2eWJsxw.4nHovmJGBA.QNwiTmuZ/uvUc40b.Uhu', systemId: new ObjectId(), token: 'token', }).afterBuild((acc) => { From d727557a5118348b18c8dd9691d9a828795c2d74 Mon Sep 17 00:00:00 2001 From: MajedAlaitwniCap Date: Wed, 27 Sep 2023 11:16:39 +0200 Subject: [PATCH 22/24] resolve security hotspot issue --- apps/server/src/shared/testing/factory/account.factory.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/server/src/shared/testing/factory/account.factory.ts b/apps/server/src/shared/testing/factory/account.factory.ts index 5c4bd173b49..a3568dbf80a 100644 --- a/apps/server/src/shared/testing/factory/account.factory.ts +++ b/apps/server/src/shared/testing/factory/account.factory.ts @@ -5,6 +5,8 @@ import { ObjectId } from 'bson'; import { DeepPartial } from 'fishery'; import { BaseFactory } from './base.factory'; +export const defaultTestPassword = 'DummyPasswd!1'; +export const defaultTestPasswordHash = '$2a$10$/DsztV5o6P5piW2eWJsxw.4nHovmJGBA.QNwiTmuZ/uvUc40b.Uhu'; class AccountFactory extends BaseFactory { withSystemId(id: EntityId | ObjectId): this { const params: DeepPartial = { systemId: id }; @@ -30,7 +32,7 @@ class AccountFactory extends BaseFactory { credentialHash: 'credentialHash', expiresAt: new Date(), lasttriedFailedLogin: new Date(), - password: '$2a$10$/DsztV5o6P5piW2eWJsxw.4nHovmJGBA.QNwiTmuZ/uvUc40b.Uhu', + password: defaultTestPassword, systemId: new ObjectId(), token: 'token', }).afterBuild((acc) => { @@ -51,8 +53,6 @@ class AccountFactory extends BaseFactory { } } -export const defaultTestPassword = 'DummyPasswd!1'; -export const defaultTestPasswordHash = '$2a$10$/DsztV5o6P5piW2eWJsxw.4nHovmJGBA.QNwiTmuZ/uvUc40b.Uhu'; // !!! important username should not be contain a space !!! export const accountFactory = AccountFactory.define(Account, ({ sequence }) => { return { From b4a2e50a3d5ef3e8bdb4978aeba3fed8b0a2fa21 Mon Sep 17 00:00:00 2001 From: MajedAlaitwniCap Date: Thu, 5 Oct 2023 18:35:40 +0200 Subject: [PATCH 23/24] Edit describe massages --- .../controller/api-test/account.api.spec.ts | 2 +- .../account-entity-to-dto.mapper.spec.ts | 10 +++---- .../repo/account.repo.integration.spec.ts | 2 +- .../services/account-db.service.spec.ts | 16 +++++----- .../account-idm.service.integration.spec.ts | 2 +- .../services/account-idm.service.spec.ts | 28 +++++++++-------- .../account.service.integration.spec.ts | 4 +-- .../account.validation.service.spec.ts | 4 +-- .../src/modules/account/uc/account.uc.spec.ts | 30 +++++++++---------- 9 files changed, 50 insertions(+), 48 deletions(-) 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 3250da07c4d..9e7dd1a7d18 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 @@ -283,7 +283,7 @@ describe('Account Controller (API)', () => { return { query, loggedInClient }; }; - it('should search for user name', async () => { + it('should search for username', async () => { const { query, loggedInClient } = await setup(); await loggedInClient.get().query(query).send().expect(200); diff --git a/apps/server/src/modules/account/mapper/account-entity-to-dto.mapper.spec.ts b/apps/server/src/modules/account/mapper/account-entity-to-dto.mapper.spec.ts index 6539a5418b9..4de5040113f 100644 --- a/apps/server/src/modules/account/mapper/account-entity-to-dto.mapper.spec.ts +++ b/apps/server/src/modules/account/mapper/account-entity-to-dto.mapper.spec.ts @@ -16,19 +16,19 @@ describe('AccountEntityToDtoMapper', () => { describe('mapToDto', () => { describe('When mapping AccountEntity to AccountDto', () => { const setup = () => { - const fullEntity = accountFactory.withAllProperties().buildWithId({}, '000000000000000000000001'); + const accountEntity = accountFactory.withAllProperties().buildWithId({}, '000000000000000000000001'); const missingSystemUserIdEntity: Account = accountFactory.withoutSystemAndUserId().build(); - return { fullEntity, missingSystemUserIdEntity }; + return { accountEntity, missingSystemUserIdEntity }; }; it('should map all fields', () => { - const { fullEntity } = setup(); + const { accountEntity } = setup(); - const ret = AccountEntityToDtoMapper.mapToDto(fullEntity); + const ret = AccountEntityToDtoMapper.mapToDto(accountEntity); - expect({ ...ret, _id: fullEntity._id }).toMatchObject(fullEntity); + expect({ ...ret, _id: accountEntity._id }).toMatchObject(accountEntity); }); it('should ignore missing ids', () => { diff --git a/apps/server/src/modules/account/repo/account.repo.integration.spec.ts b/apps/server/src/modules/account/repo/account.repo.integration.spec.ts index e3733cc4a39..1b1193cf8a4 100644 --- a/apps/server/src/modules/account/repo/account.repo.integration.spec.ts +++ b/apps/server/src/modules/account/repo/account.repo.integration.spec.ts @@ -122,7 +122,7 @@ describe('account repo', () => { }); describe('When id does not exist', () => { - it('should throw', async () => { + it('should throw not found error', async () => { await expect(repo.findByUserIdOrFail('123456789012')).rejects.toThrow(NotFoundError); }); }); 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 23f8c478a5a..8cc6a33cbb6 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 @@ -194,7 +194,7 @@ describe('AccountDbService', () => { }); describe('findMultipleByUserId', () => { - describe('when search existing multiple Ids', () => { + describe('when searching for multiple existing ids', () => { const setup = () => { const mockTeacherUser = userFactory.buildWithId(); const mockStudentUser = userFactory.buildWithId(); @@ -267,7 +267,7 @@ describe('AccountDbService', () => { }); }); - describe('when user not exists', () => { + describe('when user does not exist', () => { const setup = () => { const mockTeacherUser = userFactory.buildWithId(); const mockTeacherAccount = accountFactory.buildWithId({ @@ -304,7 +304,7 @@ describe('AccountDbService', () => { return { mockTeacherAccountDto, mockTeacherAccount }; }; - it('should update', async () => { + it('should update account', async () => { const { mockTeacherAccountDto, mockTeacherAccount } = setup(); const ret = await accountService.save(mockTeacherAccountDto); @@ -362,7 +362,7 @@ describe('AccountDbService', () => { return { mockStudentUser, mockTeacherAccountDto, mockTeacherAccount }; }; - it('should update', async () => { + it('should update account', async () => { const { mockStudentUser, mockTeacherAccountDto, mockTeacherAccount } = setup(); const ret = await accountService.save(mockTeacherAccountDto); @@ -406,7 +406,7 @@ describe('AccountDbService', () => { }); }); - describe('when account not exists', () => { + describe('when account does not exists', () => { const setup = () => { const mockUserWithoutAccount = userFactory.buildWithId(); @@ -671,7 +671,7 @@ describe('AccountDbService', () => { }); describe('delete', () => { - describe('when deleting existing account', () => { + describe('when delete an existing account', () => { const setup = () => { const mockTeacherAccount = accountFactory.buildWithId(); @@ -697,7 +697,7 @@ describe('AccountDbService', () => { return { mockTeacherAccount }; }; - it('should throw', async () => { + it('should throw account not found', async () => { const { mockTeacherAccount } = setup(); await expect(accountService.delete(mockTeacherAccount.id)).rejects.toThrow(); }); @@ -802,7 +802,7 @@ describe('AccountDbService', () => { return {}; }; - it('should call repo', async () => { + it('should call repo each time', async () => { setup(); const foundAccounts = await accountService.findMany(); expect(accountRepo.findMany).toHaveBeenCalledWith(0, 100); diff --git a/apps/server/src/modules/account/services/account-idm.service.integration.spec.ts b/apps/server/src/modules/account/services/account-idm.service.integration.spec.ts index 41dd3214311..f50e8fcc07e 100644 --- a/apps/server/src/modules/account/services/account-idm.service.integration.spec.ts +++ b/apps/server/src/modules/account/services/account-idm.service.integration.spec.ts @@ -95,7 +95,7 @@ describe('AccountIdmService Integration', () => { }); describe('save', () => { - describe('when account not exists', () => { + describe('when account does not exists', () => { it('should create a new account', async () => { if (!isIdmReachable) return; const createdAccount = await accountIdmService.save(testAccount); diff --git a/apps/server/src/modules/account/services/account-idm.service.spec.ts b/apps/server/src/modules/account/services/account-idm.service.spec.ts index 6b379aa0b39..1669b4ca4c4 100644 --- a/apps/server/src/modules/account/services/account-idm.service.spec.ts +++ b/apps/server/src/modules/account/services/account-idm.service.spec.ts @@ -296,7 +296,7 @@ describe('AccountIdmService', () => { accountLookupServiceMock.getExternalId.mockResolvedValue(null); }; - it('should throw error', async () => { + it('should throw account not found error', async () => { setup(); await expect(accountIdmService.delete(mockIdmAccountRefId)).rejects.toThrow(); }); @@ -304,17 +304,19 @@ describe('AccountIdmService', () => { }); describe('deleteByUserId', () => { - const setup = () => { - idmServiceMock.findAccountByDbcUserId.mockResolvedValue(mockIdmAccount); - const deleteSpy = jest.spyOn(idmServiceMock, 'deleteAccountById'); - return { deleteSpy }; - }; + describe('when deleting an account by user id', () => { + const setup = () => { + idmServiceMock.findAccountByDbcUserId.mockResolvedValue(mockIdmAccount); + const deleteSpy = jest.spyOn(idmServiceMock, 'deleteAccountById'); + return { deleteSpy }; + }; - it('should delete the account with given user id via repo', async () => { - const { deleteSpy } = setup(); + it('should delete the account with given user id via repo', async () => { + const { deleteSpy } = setup(); - await accountIdmService.deleteByUserId(mockIdmAccount.attDbcUserId ?? ''); - expect(deleteSpy).toHaveBeenCalledWith(mockIdmAccount.id); + await accountIdmService.deleteByUserId(mockIdmAccount.attDbcUserId ?? ''); + expect(deleteSpy).toHaveBeenCalledWith(mockIdmAccount.id); + }); }); }); @@ -336,7 +338,7 @@ describe('AccountIdmService', () => { idmServiceMock.findAccountById.mockRejectedValue(new Error()); }; - it('should throw', async () => { + it('should throw account not found', async () => { setup(); await expect(accountIdmService.findById('notExistingId')).rejects.toThrow(); }); @@ -408,7 +410,7 @@ describe('AccountIdmService', () => { idmServiceMock.findAccountByDbcUserId.mockResolvedValue(undefined as unknown as IdmAccount); }; - it('should throw', async () => { + it('should throw account not found', async () => { setup(); await expect(accountIdmService.findByUserIdOrFail('notExistingId')).rejects.toThrow(EntityNotFoundError); }); @@ -514,7 +516,7 @@ describe('AccountIdmService', () => { }); }); - it('findMany should throw', async () => { + it('findMany should throw not implemented Exception', async () => { await expect(accountIdmService.findMany(0, 0)).rejects.toThrow(NotImplementedException); }); }); diff --git a/apps/server/src/modules/account/services/account.service.integration.spec.ts b/apps/server/src/modules/account/services/account.service.integration.spec.ts index 226fc5bfac2..5d5caa24263 100644 --- a/apps/server/src/modules/account/services/account.service.integration.spec.ts +++ b/apps/server/src/modules/account/services/account.service.integration.spec.ts @@ -262,7 +262,7 @@ describe('AccountService Integration', () => { }); describe('delete', () => { - describe('when delete', () => { + describe('when delete an account', () => { const setup = async () => { const [dbId, idmId] = await createAccount(); const foundIdmAccount = await identityManagementService.findAccountById(idmId); @@ -285,7 +285,7 @@ describe('AccountService Integration', () => { }); describe('deleteByUserId', () => { - describe('when delete by User Id', () => { + describe('when delete an account by User Id', () => { const setup = async () => { const [dbId, idmId] = await createAccount(); const foundIdmAccount = await identityManagementService.findAccountById(idmId); diff --git a/apps/server/src/modules/account/services/account.validation.service.spec.ts b/apps/server/src/modules/account/services/account.validation.service.spec.ts index 7608ae52dd8..c152f01a59b 100644 --- a/apps/server/src/modules/account/services/account.validation.service.spec.ts +++ b/apps/server/src/modules/account/services/account.validation.service.spec.ts @@ -142,7 +142,7 @@ describe('AccountValidationService', () => { }); }); - describe('When new email already in use by any user', () => { + describe('When new email already in use by any user and system id is given', () => { const setup = () => { const mockTeacherUser = userFactory.buildWithId({ roles: [new Role({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] })], @@ -159,7 +159,7 @@ describe('AccountValidationService', () => { return { mockTeacherAccount, mockStudentUser, mockStudentAccount }; }; - it('should return false, system id is given', async () => { + it('should return false', async () => { const { mockTeacherAccount, mockStudentUser, mockStudentAccount } = setup(); const res = await accountValidationService.isUniqueEmail( mockTeacherAccount.username, 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 84ca3f88296..e210a5c9ab5 100644 --- a/apps/server/src/modules/account/uc/account.uc.spec.ts +++ b/apps/server/src/modules/account/uc/account.uc.spec.ts @@ -104,7 +104,7 @@ describe('AccountUc', () => { }); }); - describe('When account does not exist', () => { + describe('When account does not exists', () => { const setup = () => { const mockSchool = schoolFactory.buildWithId(); @@ -125,7 +125,7 @@ describe('AccountUc', () => { return { mockUserWithoutAccount }; }; - it('should throw', async () => { + it('should throw entity not found error', async () => { const { mockUserWithoutAccount } = setup(); await expect( accountUc.updateMyAccount(mockUserWithoutAccount.id, { @@ -679,7 +679,7 @@ describe('AccountUc', () => { }); }); - describe('When account does not exist', () => { + describe('When account does not exists', () => { const setup = () => { const mockSchool = schoolFactory.buildWithId(); const mockUserWithoutAccount = userFactory.buildWithId({ @@ -853,7 +853,7 @@ describe('AccountUc', () => { }); }); - describe('When the admin manipulated the users password', () => { + describe('When the admin manipulate the users password', () => { const setup = () => { const mockSchool = schoolFactory.buildWithId(); @@ -886,7 +886,7 @@ describe('AccountUc', () => { }); }); - describe('When this is the users first login', () => { + describe('when a user logs in for the first time', () => { const setup = () => { const mockSchool = schoolFactory.buildWithId(); @@ -920,7 +920,7 @@ describe('AccountUc', () => { }); }); - describe('When this is the users first login (if undefined)', () => { + describe('when a user logs in for the first time (if undefined)', () => { const setup = () => { const mockSchool = schoolFactory.buildWithId(); @@ -1250,7 +1250,7 @@ describe('AccountUc', () => { return { mockSuperheroUser }; }; - it('should throw', async () => { + it('should throw Invalid search type', async () => { const { mockSuperheroUser } = setup(); await expect( accountUc.searchAccounts( @@ -1261,7 +1261,7 @@ describe('AccountUc', () => { }); }); - describe('When user is no superhero', () => { + describe('When user is not superhero', () => { const setup = () => { const mockSchool = schoolFactory.buildWithId(); @@ -2119,7 +2119,7 @@ describe('AccountUc', () => { }); describe('updateAccountById', () => { - describe('When executing user does not exist', () => { + describe('when updating a user that does not exist', () => { const setup = () => { const mockSchool = schoolFactory.buildWithId(); @@ -2567,7 +2567,7 @@ describe('AccountUc', () => { return { mockAdminUser, mockTeacherAccount }; }; - it('should not throw when editing a teacher', async () => { + it('should not throw error when editing a teacher', async () => { const { mockAdminUser, mockTeacherAccount } = setup(); const currentUser = { userId: mockAdminUser.id } as ICurrentUser; const params = { id: mockTeacherAccount.id } as AccountByIdParams; @@ -2603,7 +2603,7 @@ describe('AccountUc', () => { return { mockStudentAccount, mockTeacherUser }; }; - it('should not throw when editing a student', async () => { + it('should not throw error when editing a student', async () => { const { mockTeacherUser, mockStudentAccount } = setup(); const currentUser = { userId: mockTeacherUser.id } as ICurrentUser; const params = { id: mockStudentAccount.id } as AccountByIdParams; @@ -2647,7 +2647,7 @@ describe('AccountUc', () => { return { mockStudentAccount, mockAdminUser }; }; - it('should not throw when editing a student', async () => { + it('should not throw error when editing a student', async () => { const { mockAdminUser, mockStudentAccount } = setup(); const currentUser = { userId: mockAdminUser.id } as ICurrentUser; const params = { id: mockStudentAccount.id } as AccountByIdParams; @@ -2794,7 +2794,7 @@ describe('AccountUc', () => { return { mockAdminAccount, mockSuperheroUser }; }; - it('should not throw when editing a admin', async () => { + it('should not throw error when editing a admin', async () => { const { mockSuperheroUser, mockAdminAccount } = setup(); const currentUser = { userId: mockSuperheroUser.id } as ICurrentUser; const params = { id: mockAdminAccount.id } as AccountByIdParams; @@ -2922,7 +2922,7 @@ describe('AccountUc', () => { }); }); - describe('When the current user is no superhero', () => { + describe('When the current user is not superhero', () => { const setup = () => { const mockSchool = schoolFactory.buildWithId(); @@ -3114,7 +3114,7 @@ describe('AccountUc', () => { return { mockAccountWithNoLastFailedLogin }; }; - it('should not throw', async () => { + it('should not throw error', async () => { const { mockAccountWithNoLastFailedLogin } = setup(); await expect( accountUc.checkBrutForce( From e62de7553d47f4824cffe6c266caea606b6c4c9e Mon Sep 17 00:00:00 2001 From: MajedAlaitwniCap Date: Fri, 6 Oct 2023 10:07:07 +0200 Subject: [PATCH 24/24] Edit describe massages part 2 --- .../modules/account/services/account.service.spec.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/server/src/modules/account/services/account.service.spec.ts b/apps/server/src/modules/account/services/account.service.spec.ts index 2246c0de658..834bc5b0f89 100644 --- a/apps/server/src/modules/account/services/account.service.spec.ts +++ b/apps/server/src/modules/account/services/account.service.spec.ts @@ -234,7 +234,7 @@ describe('AccountService', () => { }); describe('When username for a local user is not an email', () => { - it('should throw', async () => { + it('should throw username is not an email error', async () => { const params: AccountSaveDto = { username: 'John Doe', password: 'JohnsPassword', @@ -244,7 +244,7 @@ describe('AccountService', () => { }); describe('When username for an external user is not an email', () => { - it('should not throw', async () => { + it('should not throw an error', async () => { const params: AccountSaveDto = { username: 'John Doe', systemId: 'ABC123', @@ -254,7 +254,7 @@ describe('AccountService', () => { }); describe('When username for an external user is a ldap search string', () => { - it('should not throw', async () => { + it('should not throw an error', async () => { const params: AccountSaveDto = { username: 'dc=schul-cloud,dc=org/fake.ldap', systemId: 'ABC123', @@ -264,7 +264,7 @@ describe('AccountService', () => { }); describe('When no password is provided for an internal user', () => { - it('should throw', async () => { + it('should throw no password provided error', async () => { const params: AccountSaveDto = { username: 'john.doe@mail.tld', }; @@ -273,7 +273,7 @@ describe('AccountService', () => { }); describe('When account already exists', () => { - it('should throw', async () => { + it('should throw account already exists', async () => { const params: AccountSaveDto = { username: 'john.doe@mail.tld', password: 'JohnsPassword', @@ -288,7 +288,7 @@ describe('AccountService', () => { const setup = () => { accountValidationService.isUniqueEmail.mockResolvedValueOnce(false); }; - it('should throw', async () => { + it('should throw username already exists', async () => { setup(); const params: AccountSaveDto = { username: 'john.doe@mail.tld',