diff --git a/apps/server/src/modules/account/controller/account.controller.spec.ts b/apps/server/src/modules/account/controller/account.controller.spec.ts new file mode 100644 index 00000000000..ef15672edc5 --- /dev/null +++ b/apps/server/src/modules/account/controller/account.controller.spec.ts @@ -0,0 +1,24 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AccountController } from './account.controller'; +import { AccountUc } from '../uc/account.uc'; + +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(); + }); +}); diff --git a/apps/server/src/modules/account/controller/account.controller.ts b/apps/server/src/modules/account/controller/account.controller.ts index 2f45de2403e..2256cb9fc90 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 '@modules/authentication'; -import { Authenticate, CurrentUser } from '@modules/authentication/decorator/auth.decorator'; +import { ICurrentUser } from '@src/modules/authentication'; import { AccountUc } from '../uc/account.uc'; import { AccountByIdBodyParams, @@ -33,8 +33,6 @@ 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 44d47a3c3d2..fefdb006bf8 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,642 +1,315 @@ -import { EntityManager } from '@mikro-orm/mongodb'; -import { INestApplication } from '@nestjs/common'; +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 { - accountFactory, - roleFactory, - schoolFactory, - userFactory, - TestApiClient, - cleanupCollections, -} from '@shared/testing'; +import { ICurrentUser } from '@src/modules/authentication'; +import { accountFactory, mapUserToCurrentUser, roleFactory, schoolFactory, userFactory } from '@shared/testing'; import { AccountByIdBodyParams, AccountSearchQueryParams, AccountSearchType, PatchMyAccountParams, PatchMyPasswordParams, -} from '@modules/account/controller/dto'; -import { ServerTestModule } from '@modules/server/server.module'; +} from '@src/modules/account/controller/dto'; +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 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'; - const mapUserToAccount = (user: User): Account => - accountFactory.buildWithId({ - userId: user.id, - username: user.email, - password: defaultPasswordHash, + 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: [] }); + + 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(); + }; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ServerTestModule], - }).compile(); + }) + .overrideGuard(JwtAuthGuard) + .useValue({ + canActivate(context: ExecutionContext) { + const req: Request = context.switchToHttp().getRequest(); + req.user = currentUser; + return true; + }, + }) + .compile(); app = moduleFixture.createNestApplication(); await app.init(); em = app.get(EntityManager); - testApiClient = new TestApiClient(app, basePath); }); beforeEach(async () => { - await cleanupCollections(em); + await setup(); }); afterAll(async () => { - await cleanupCollections(em); + // await cleanupCollections(em); await app.close(); }); describe('[PATCH] me/password', () => { - 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, studentRoles, studentUser, studentAccount]); - await em.flush(); - - const loggedInClient = await testApiClient.login(studentAccount); - - const passwordPatchParams: PatchMyPasswordParams = { - password: 'Valid12$', - confirmPassword: 'Valid12$', - }; - - return { passwordPatchParams, loggedInClient, studentAccount }; + it(`should update the current user's (temporary) password`, async () => { + currentUser = mapUserToCurrentUser(studentUser, studentAccount); + const params: PatchMyPasswordParams = { + password: 'Valid12$', + confirmPassword: 'Valid12$', }; + await request(app.getHttpServer()) // + .patch(`${basePath}/me/password`) + .send(params) + .expect(200); - 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); - }); + const updatedAccount = await em.findOneOrFail(Account, studentAccount.id); + expect(updatedAccount.password).not.toEqual(defaultPasswordHash); }); - - 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, studentRoles, studentUser, studentAccount]); - await em.flush(); - - const loggedInClient = await testApiClient.login(studentAccount); - - const passwordPatchParams: PatchMyPasswordParams = { - password: 'weak', - confirmPassword: 'weak', - }; - - return { passwordPatchParams, loggedInClient }; + it('should reject if new password is weak', async () => { + currentUser = mapUserToCurrentUser(studentUser, studentAccount); + const params: PatchMyPasswordParams = { + password: 'weak', + confirmPassword: 'weak', }; - - it('should reject the password change', async () => { - const { passwordPatchParams, loggedInClient } = await setup(); - - await loggedInClient.patch('/me/password', passwordPatchParams).expect(400); - }); + await request(app.getHttpServer()) // + .patch(`${basePath}/me/password`) + .send(params) + .expect(400); }); }); describe('[PATCH] me', () => { - 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, studentRoles, studentUser, 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 }; + it(`should update a users account`, async () => { + const newEmailValue = 'new@mail.com'; + currentUser = mapUserToCurrentUser(studentUser, studentAccount); + const params: PatchMyAccountParams = { + passwordOld: defaultPassword, + email: 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); - }); + await request(app.getHttpServer()) // + .patch(`${basePath}/me`) + .send(params) + .expect(200); + const updatedAccount = await em.findOneOrFail(Account, studentAccount.id); + expect(updatedAccount.username).toEqual(newEmailValue); }); - 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, studentRoles, studentUser, 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 }; + it('should reject if new email is not valid', async () => { + currentUser = mapUserToCurrentUser(studentUser, studentAccount); + const params: PatchMyAccountParams = { + passwordOld: defaultPassword, + email: 'invalid', }; - - it('should reject patch request', async () => { - const { patchMyAccountParams, loggedInClient } = await setup(); - - await loggedInClient.patch('/me', patchMyAccountParams).expect(400); - }); + await request(app.getHttpServer()) // + .patch(`${basePath}/me`) + .send(params) + .expect(400); }); }); describe('[GET]', () => { - 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 }; + 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, }; - it('should successfully search for user id', async () => { - const { query, loggedInClient } = await setup(); - - await loggedInClient.get().query(query).send().expect(200); - }); + await request(app.getHttpServer()) // + .get(`${basePath}`) + .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. - 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 }; + 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, }; - it('should search for user id', async () => { - const { query, loggedInClient } = await setup(); - - await loggedInClient.get().query(query).send().expect(200); - }); + await request(app.getHttpServer()) // + .get(`${basePath}`) + .query(query) + .send() + .expect(200); }); - - 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 }; + it('should search for user name', async () => { + currentUser = mapUserToCurrentUser(superheroUser, superheroAccount); + const query: AccountSearchQueryParams = { + type: AccountSearchType.USERNAME, + value: '', + skip: 5, + limit: 5, }; - it('should search for username', async () => { - const { query, loggedInClient } = await setup(); - - await loggedInClient.get().query(query).send().expect(200); - }); + await request(app.getHttpServer()) // + .get(`${basePath}`) + .query(query) + .send() + .expect(200); }); - - 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 }; + it('should reject if type is unknown', async () => { + currentUser = mapUserToCurrentUser(superheroUser, superheroAccount); + const query: AccountSearchQueryParams = { + type: '' as AccountSearchType, + value: '', + skip: 5, + limit: 5, }; - - it('should reject if type is unknown', async () => { - const { query, loggedInClient } = await setup(); - - await loggedInClient.get().query(query).send().expect(400); - }); + await request(app.getHttpServer()) // + .get(`${basePath}`) + .query(query) + .send() + .expect(400); }); - 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 }; + it('should reject if user is not authorized', async () => { + currentUser = mapUserToCurrentUser(adminUser, adminAccount); + const query: AccountSearchQueryParams = { + type: AccountSearchType.USERNAME, + value: '', + skip: 5, + limit: 5, }; - - it('should reject search for user', async () => { - const { query, loggedInClient } = await setup(); - - await loggedInClient.get().query(query).send().expect(403); - }); + await request(app.getHttpServer()) // + .get(`${basePath}`) + .query(query) + .send() + .expect(403); }); }); describe('[GET] :id', () => { - 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); - - 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('When searching with a not authorized user', () => { - 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); - - 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('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 not existing account id', async () => { - const { loggedInClient } = await setup(); - await loggedInClient.get(`/000000000000000000000000`).expect(404); - }); + it('should reject not existing account id', async () => { + currentUser = mapUserToCurrentUser(superheroUser, superheroAccount); + await request(app.getHttpServer()) // + .get(`${basePath}/000000000000000000000000`) + .expect(404); }); }); describe('[PATCH] :id', () => { - describe('When using 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 body: AccountByIdBodyParams = { - password: defaultPassword, - username: studentAccount.username, - activated: true, - }; - - return { body, loggedInClient, studentAccount }; + it('should update account', async () => { + currentUser = mapUserToCurrentUser(superheroUser, superheroAccount); + const body: AccountByIdBodyParams = { + password: defaultPassword, + username: studentAccount.username, + activated: true, }; - - it('should update account', async () => { - const { body, loggedInClient, studentAccount } = await setup(); - - await loggedInClient.patch(`/${studentAccount.id}`, body).expect(200); - }); + await request(app.getHttpServer()) // + .patch(`${basePath}/${studentAccount.id}`) + .send(body) + .expect(200); }); - - describe('When the user is not authorized', () => { - 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, studentRoles, studentUser, studentAccount]); - await em.flush(); - - const loggedInClient = await testApiClient.login(studentAccount); - - const body: AccountByIdBodyParams = { - password: defaultPassword, - username: studentAccount.username, - activated: true, - }; - - return { body, loggedInClient, studentAccount }; + it('should reject if user is not authorized', async () => { + currentUser = mapUserToCurrentUser(studentUser, studentAccount); + const body: AccountByIdBodyParams = { + password: defaultPassword, + username: studentAccount.username, + activated: true, }; - it('should reject update request', async () => { - const { body, loggedInClient, studentAccount } = await setup(); - - await loggedInClient.patch(`/${studentAccount.id}`, body).expect(403); - }); + await request(app.getHttpServer()) // + .patch(`${basePath}/${studentAccount.id}`) + .send(body) + .expect(403); }); - - describe('When updating 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 body: AccountByIdBodyParams = { - password: defaultPassword, - username: studentAccount.username, - activated: true, - }; - - 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, }; - it('should reject not existing account id', async () => { - const { body, loggedInClient } = await setup(); - await loggedInClient.patch('/000000000000000000000000', body).expect(404); - }); + await request(app.getHttpServer()) // + .patch(`${basePath}/000000000000000000000000`) + .send(body) + .expect(404); }); }); describe('[DELETE] :id', () => { - describe('When using 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); - - 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('When using a not authorized (admin) user', () => { - 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); - - 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('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 not existing account id', async () => { - const { loggedInClient } = await setup(); - await loggedInClient.delete('/000000000000000000000000').expect(404); - }); + it('should reject not existing account id', async () => { + currentUser = mapUserToCurrentUser(superheroUser, studentAccount); + await request(app.getHttpServer()) // + .delete(`${basePath}/000000000000000000000000`) + .expect(404); }); }); }); 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 d849b8db235..6ca2fd9fab2 100644 --- a/apps/server/src/modules/account/controller/dto/password-pattern.ts +++ b/apps/server/src/modules/account/controller/dto/password-pattern.ts @@ -1 +1,2 @@ +// 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 4de5040113f..8e9522434eb 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 { accountFactory } from '@shared/testing'; +import { ObjectId } from 'bson'; import { AccountEntityToDtoMapper } from './account-entity-to-dto.mapper'; describe('AccountEntityToDtoMapper', () => { @@ -14,80 +14,101 @@ describe('AccountEntityToDtoMapper', () => { }); describe('mapToDto', () => { - describe('When mapping AccountEntity to AccountDto', () => { - const setup = () => { - const accountEntity = accountFactory.withAllProperties().buildWithId({}, '000000000000000000000001'); - - const missingSystemUserIdEntity: Account = accountFactory.withoutSystemAndUserId().build(); - - return { accountEntity, missingSystemUserIdEntity }; + it('should map all fields', () => { + 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); + + 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); + }); - it('should map all fields', () => { - const { accountEntity } = setup(); - - const ret = AccountEntityToDtoMapper.mapToDto(accountEntity); - - expect({ ...ret, _id: accountEntity._id }).toMatchObject(accountEntity); - }); - - it('should ignore missing ids', () => { - const { missingSystemUserIdEntity } = setup(); - - const ret = AccountEntityToDtoMapper.mapToDto(missingSystemUserIdEntity); + it('should ignore missing ids', () => { + const testEntity: Account = { + _id: new ObjectId(), + id: 'id', + username: 'username', + createdAt: new Date(), + updatedAt: new Date(), + }; + const ret = AccountEntityToDtoMapper.mapToDto(testEntity); - expect(ret.userId).toBeUndefined(); - expect(ret.systemId).toBeUndefined(); - }); + expect(ret.userId).toBeUndefined(); + expect(ret.systemId).toBeUndefined(); }); }); describe('mapSearchResult', () => { - 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 }; + 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(), + }; + const testAmount = 10; - it('should map exact same amount of entities', () => { - const { testEntities, testAmount } = setup(); - - const [accounts, total] = AccountEntityToDtoMapper.mapSearchResult([testEntities, testAmount]); + const [accounts, total] = AccountEntityToDtoMapper.mapSearchResult([[testEntity1, testEntity2], testAmount]); - expect(total).toBe(testAmount); - expect(accounts).toHaveLength(2); - expect(accounts).toContainEqual(expect.objectContaining({ id: '000000000000000000000001' })); - expect(accounts).toContainEqual(expect.objectContaining({ id: '000000000000000000000002' })); - }); + expect(total).toBe(testAmount); + expect(accounts).toHaveLength(2); + expect(accounts).toContainEqual(expect.objectContaining({ id: '1' })); + expect(accounts).toContainEqual(expect.objectContaining({ id: '2' })); }); }); describe('mapAccountsToDto', () => { - 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; + 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(), + }; + const ret = AccountEntityToDtoMapper.mapAccountsToDto([testEntity1, testEntity2]); - 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' })); - }); + expect(ret).toHaveLength(2); + expect(ret).toContainEqual(expect.objectContaining({ id: '1' })); + expect(ret).toContainEqual(expect.objectContaining({ id: '2' })); }); }); }); 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 d8af59e6716..417497b3218 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,8 +19,6 @@ 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-idm-to-dto.mapper.db.spec.ts b/apps/server/src/modules/account/mapper/account-idm-to-dto.mapper.db.spec.ts index ee7d1644c94..2430afe6081 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('mapToDto', () => { - describe('when mapping from entity to dto', () => { - const setup = () => { + describe('when mapping from entity to dto', () => { + describe('mapToDto', () => { + it('should map all fields', () => { const testIdmEntity: IdmAccount = { id: 'id', username: 'username', @@ -38,12 +38,6 @@ describe('AccountIdmToDtoMapperDb', () => { attDbcUserId: 'attDbcUserId', attDbcSystemId: 'attDbcSystemId', }; - return testIdmEntity; - }; - - it('should map all fields', () => { - const testIdmEntity = setup(); - const ret = mapper.mapToDto(testIdmEntity); expect(ret).toEqual( @@ -58,42 +52,30 @@ describe('AccountIdmToDtoMapperDb', () => { }) ); }); - }); - - describe('when date is undefined', () => { - const setup = () => { - const testIdmEntity: IdmAccount = { - id: 'id', - }; - return testIdmEntity; - }; - it('should use actual date', () => { - const testIdmEntity = setup(); + describe('when date is undefined', () => { + it('should use actual date', () => { + const testIdmEntity: IdmAccount = { + id: 'id', + }; + const ret = mapper.mapToDto(testIdmEntity); - const ret = mapper.mapToDto(testIdmEntity); - - const now = new Date(); - expect(ret.createdAt).toEqual(now); - expect(ret.updatedAt).toEqual(now); + const now = new Date(); + expect(ret.createdAt).toEqual(now); + expect(ret.updatedAt).toEqual(now); + }); }); - }); - describe('when a fields value is missing', () => { - const setup = () => { - const testIdmEntity: IdmAccount = { - id: 'id', - }; - return testIdmEntity; - }; - - it('should fill with empty string', () => { - const testIdmEntity = setup(); - - const ret = mapper.mapToDto(testIdmEntity); + describe('when a fields value is missing', () => { + it('should fill with empty string', () => { + const testIdmEntity: IdmAccount = { + id: 'id', + }; + 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 554e2d3025a..0d60a2cc57f 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,52 +30,39 @@ describe('AccountIdmToDtoMapperIdm', () => { await module.close(); }); - describe('mapToDto', () => { - describe('when mapping from entity to dto', () => { - const setup = () => { - const testIdmEntity: IdmAccount = { - id: 'id', - username: 'username', - email: 'email', - firstName: 'firstName', - lastName: 'lastName', - createdDate: new Date(), - attDbcAccountId: 'attDbcAccountId', - attDbcUserId: 'attDbcUserId', - attDbcSystemId: 'attDbcSystemId', - }; - return testIdmEntity; + describe('when mapping from entity to dto', () => { + it('should map all fields', () => { + const testIdmEntity: IdmAccount = { + id: 'id', + username: 'username', + email: 'email', + firstName: 'firstName', + lastName: 'lastName', + createdDate: new Date(), + attDbcAccountId: 'attDbcAccountId', + attDbcUserId: 'attDbcUserId', + attDbcSystemId: 'attDbcSystemId', }; - - 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.attDbcUserId, - systemId: testIdmEntity.attDbcSystemId, - createdAt: testIdmEntity.createdDate, - updatedAt: testIdmEntity.createdDate, - username: testIdmEntity.username, - }) - ); - }); + const ret = mapper.mapToDto(testIdmEntity); + + expect(ret).toEqual( + expect.objectContaining>({ + id: testIdmEntity.id, + idmReferenceId: undefined, + userId: testIdmEntity.attDbcUserId, + systemId: testIdmEntity.attDbcSystemId, + createdAt: testIdmEntity.createdDate, + updatedAt: testIdmEntity.createdDate, + username: testIdmEntity.username, + }) + ); }); + describe('when date is undefined', () => { - const setup = () => { + it('should use actual date', () => { const testIdmEntity: IdmAccount = { id: 'id', }; - return testIdmEntity; - }; - - it('should use actual date', () => { - const testIdmEntity = setup(); - const ret = mapper.mapToDto(testIdmEntity); expect(ret.createdAt).toEqual(now); @@ -84,16 +71,10 @@ describe('AccountIdmToDtoMapperIdm', () => { }); describe('when a fields value is missing', () => { - const setup = () => { + it('should fill with empty string', () => { const testIdmEntity: IdmAccount = { id: 'id', }; - return testIdmEntity; - }; - - it('should fill with empty string', () => { - const testIdmEntity = setup(); - const ret = mapper.mapToDto(testIdmEntity); expect(ret.username).toBe(''); 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 64858623c67..c4bd892faa0 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,57 +1,62 @@ import { Account } from '@shared/domain'; import { AccountDto } from '@modules/account/services/dto/account.dto'; -import { accountDtoFactory, accountFactory } from '@shared/testing'; +import { ObjectId } from '@mikro-orm/mongodb'; import { AccountResponseMapper } from '.'; describe('AccountResponseMapper', () => { describe('mapToResponseFromEntity', () => { - describe('When mapping AccountEntity to AccountResponse', () => { - const setup = () => { - const testEntityAllFields: Account = accountFactory.withAllProperties().buildWithId(); - - const testEntityMissingUserId: Account = accountFactory.withoutSystemAndUserId().build(); - - return { testEntityAllFields, testEntityMissingUserId }; + 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); - 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(); + 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); + expect(ret.updatedAt).toBe(testEntity.updatedAt); + }); - const ret = AccountResponseMapper.mapToResponseFromEntity(testEntityMissingUserId); + 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(), + }; + const ret = AccountResponseMapper.mapToResponseFromEntity(testEntity); - expect(ret.userId).toBeUndefined(); - }); + expect(ret.userId).toBeUndefined(); }); }); describe('mapToResponse', () => { - describe('When mapping AccountDto to AccountResponse', () => { - const setup = () => { - const testDto: AccountDto = accountDtoFactory.buildWithId(); - return testDto; + 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(), }; + const ret = AccountResponseMapper.mapToResponse(testDto); - 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); - }); + 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); + expect(ret.updatedAt).toBe(testDto.updatedAt); }); }); }); 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 84e20519bfe..dac10f98255 100644 --- a/apps/server/src/modules/account/mapper/account-response.mapper.ts +++ b/apps/server/src/modules/account/mapper/account-response.mapper.ts @@ -3,7 +3,6 @@ import { AccountDto } from '@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.integration.spec.ts b/apps/server/src/modules/account/repo/account.repo.integration.spec.ts index 1b1193cf8a4..bf4a44119fe 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,6 +10,7 @@ describe('account repo', () => { let module: TestingModule; let em: EntityManager; let repo: AccountRepo; + let mockAccounts: Account[]; beforeAll(async () => { module = await Test.createTestingModule({ @@ -24,6 +25,16 @@ 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); }); @@ -33,340 +44,183 @@ describe('account repo', () => { }); describe('findByUserId', () => { - 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); - }); + 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('findByUsernameAndSystemId', () => { - 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 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 not given', () => { - it('should return null', async () => { - const account = await repo.findByUsernameAndSystemId('', new ObjectId(undefined)); - expect(account).toBeNull(); - }); + it('should return null', async () => { + const account = await repo.findByUsernameAndSystemId('', new ObjectId(undefined)); + expect(account).toBeNull(); }); }); describe('findMultipleByUserId', () => { - 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 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 not existing user ids are given', () => { - it('should return empty list', async () => { - const accounts = await repo.findMultipleByUserId(['123456789012', '098765432101']); - expect(accounts).toHaveLength(0); - }); + 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('findByUserIdOrFail', () => { - 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 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 id does not exist', () => { - it('should throw not found error', async () => { - await expect(repo.findByUserIdOrFail('123456789012')).rejects.toThrow(NotFoundError); - }); + 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('getObjectReference', () => { - 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); - }); + 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('saveWithoutFlush', () => { - 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); - }); + it('should add an account to the persist stack', () => { + const account = accountFactory.build(); + + repo.saveWithoutFlush(account); + expect(em.getUnitOfWork().getPersistStack().size).toBe(1); }); }); describe('flush', () => { - describe('When repo is flushed', () => { - const setup = () => { - const account = accountFactory.build(); - em.persist(account); - return account; - }; - - it('should save account', async () => { - const account = setup(); + it('should flush after save', async () => { + const account = accountFactory.build(); + em.persist(account); - 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('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 })); - }); - }); - }); + 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(); - 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 })); - }); - }); + const [result] = await repo.searchByUsernameExactMatch('USER@EXAMPLE.COM'); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(expect.objectContaining({ username: originalUsername })); - 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 })); - }); + const [result2] = await repo.searchByUsernamePartialMatch('user'); + expect(result2).toHaveLength(1); + expect(result2[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(); - 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); - }); + 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 })); }); - }); + it('should not find by wildcard', async () => { + const originalUsername = 'USER@EXAMPLE.COM'; + const account = accountFactory.build({ username: originalUsername }); + await em.persistAndFlush([account]); + em.clear(); - describe('deleteById', () => { - describe('When an id is given', () => { - const setup = async () => { - const account = accountFactory.buildWithId(); - await em.persistAndFlush([account]); + let [accounts] = await repo.searchByUsernameExactMatch('USER@EXAMPLECCOM'); + expect(accounts).toHaveLength(0); - return account; - }; + [accounts] = await repo.searchByUsernameExactMatch('.*'); + expect(accounts).toHaveLength(0); + }); + }); - it('should delete an account by id', async () => { - const account = await setup(); + describe('deleteId', () => { + it('should delete an account by id', async () => { + const account = accountFactory.buildWithId(); + await em.persistAndFlush([account]); - await expect(repo.deleteById(account.id)).resolves.not.toThrow(); + await expect(repo.deleteById(account.id)).resolves.not.toThrow(); - await expect(repo.findById(account.id)).rejects.toThrow(NotFoundError); - }); + await expect(repo.findById(account.id)).rejects.toThrow(NotFoundError); }); }); describe('deleteByUserId', () => { - 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 }; - }; + 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]); - it('should delete an account by user id', async () => { - const { user, account } = await setup(); + await expect(repo.deleteByUserId(user.id)).resolves.not.toThrow(); - await expect(repo.deleteByUserId(user.id)).resolves.not.toThrow(); - - await expect(repo.findById(account.id)).rejects.toThrow(NotFoundError); - }); + await expect(repo.findById(account.id)).rejects.toThrow(NotFoundError); }); }); describe('findMany', () => { - 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); - }); + 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); }); }); }); diff --git a/apps/server/src/modules/account/repo/account.repo.ts b/apps/server/src/modules/account/repo/account.repo.ts index e848973c5c6..fb68f0a759b 100644 --- a/apps/server/src/modules/account/repo/account.repo.ts +++ b/apps/server/src/modules/account/repo/account.repo.ts @@ -15,9 +15,7 @@ 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) }); } @@ -49,8 +47,6 @@ 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); } @@ -84,7 +80,6 @@ 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 deleted file mode 100644 index fc636019cdd..00000000000 --- a/apps/server/src/modules/account/review-comments.md +++ /dev/null @@ -1,12 +0,0 @@ -# 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? - -- 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.spec.ts b/apps/server/src/modules/account/services/account-db.service.spec.ts index 7fa4c4e44a8..64075bcb40c 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 } from '@shared/domain'; +import { Account, EntityId, Permission, Role, RoleName, SchoolEntity, User } from '@shared/domain'; import { IdentityManagementService } from '@shared/infra/identity-management/identity-management.service'; -import { accountFactory, setupEntities, userFactory } from '@shared/testing'; +import { accountFactory, schoolFactory, setupEntities, userFactory } from '@shared/testing'; import { AccountEntityToDtoMapper } from '@modules/account/mapper'; import { AccountDto } from '@modules/account/services/dto'; import { IServerConfig } from '@modules/server'; @@ -19,11 +19,23 @@ import { AbstractAccountService } from './account.service.abstract'; describe('AccountDbService', () => { let module: TestingModule; let accountService: AbstractAccountService; - let accountRepo: DeepMocked; + let mockAccounts: Account[]; + let accountRepo: AccountRepo; let accountLookupServiceMock: DeepMocked; const defaultPassword = 'DummyPasswd!1'; + let mockSchool: SchoolEntity; + + let mockTeacherUser: User; + let mockStudentUser: User; + let mockUserWithoutAccount: User; + + let mockTeacherAccount: Account; + let mockStudentAccount: Account; + + let mockAccountWithSystemId: Account; + afterAll(async () => { await module.close(); }); @@ -35,7 +47,69 @@ describe('AccountDbService', () => { AccountLookupService, { provide: AccountRepo, - useValue: createMock(), + 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)), + }, }, { provide: LegacyLogger, @@ -51,7 +125,14 @@ describe('AccountDbService', () => { }, { provide: AccountLookupService, - useValue: createMock(), + useValue: createMock({ + getInternalId: (id: EntityId | ObjectId): Promise => { + if (ObjectId.isValid(id)) { + return Promise.resolve(new ObjectId(id)); + } + return Promise.resolve(null); + }, + }), }, ], }).compile(); @@ -62,9 +143,28 @@ 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(() => { @@ -73,615 +173,294 @@ describe('AccountDbService', () => { }); describe('findById', () => { - 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 - ); - }); + it( + 'should return accountDto', + async () => { + const resultAccount = await accountService.findById(mockTeacherAccount.id); + expect(resultAccount).toEqual(AccountEntityToDtoMapper.mapToDto(mockTeacherAccount)); + }, + 10 * 60 * 1000 + ); }); describe('findByUserId', () => { - 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 accountDto', async () => { + const resultAccount = await accountService.findByUserId(mockTeacherUser.id); + expect(resultAccount).toEqual(AccountEntityToDtoMapper.mapToDto(mockTeacherAccount)); }); - - describe('when user id not exists', () => { - it('should return null', async () => { - const resultAccount = await accountService.findByUserId('nonExistentId'); - expect(resultAccount).toBeNull(); - }); + it('should return null', async () => { + const resultAccount = await accountService.findByUserId('nonExistentId'); + expect(resultAccount).toBeNull(); }); }); describe('findByUsernameAndSystemId', () => { - 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 accountDto', async () => { + const resultAccount = await accountService.findByUsernameAndSystemId( + mockAccountWithSystemId.username, + mockAccountWithSystemId.systemId ?? '' + ); + expect(resultAccount).not.toBe(undefined); }); - - 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 username does not exist', async () => { + const resultAccount = await accountService.findByUsernameAndSystemId( + 'nonExistentUsername', + mockAccountWithSystemId.systemId ?? '' + ); + 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(); - }); + it('should return null if system id does not exist', async () => { + const resultAccount = await accountService.findByUsernameAndSystemId( + mockAccountWithSystemId.username, + 'nonExistentSystemId' ?? '' + ); + expect(resultAccount).toBeNull(); }); }); describe('findMultipleByUserId', () => { - describe('when searching for multiple existing 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 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 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); - }); + it('should return empty array on mismatch', async () => { + const resultAccount = await accountService.findMultipleByUserId(['nonExistentId1']); + expect(resultAccount).toHaveLength(0); }); }); describe('findByUserIdOrFail', () => { - 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 return accountDto', async () => { + const resultAccount = await accountService.findByUserIdOrFail(mockTeacherUser.id); + expect(resultAccount).toEqual(AccountEntityToDtoMapper.mapToDto(mockTeacherAccount)); }); - - describe('when user does not exist', () => { - 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); - }); + it('should throw EntityNotFoundError', async () => { + await expect(accountService.findByUserIdOrFail('nonExistentId')).rejects.toThrow(EntityNotFoundError); }); }); describe('save', () => { - 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 account', 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', 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'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 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 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 account', 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 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 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 does 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 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 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 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 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 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 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 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 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, + 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({ password: undefined, - } as AccountDto; - - 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, - }) - ); - }); + 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, + }) + ); }); }); describe('updateUsername', () => { - 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, - }); + 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('updateLastTriedFailedLogin', () => { - 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, - }); + 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('validatePassword', () => { - 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 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 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 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 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); - }); + it('should report missing account password', async () => { + const ret = await accountService.validatePassword({ password: undefined } as AccountDto, 'incorrectPwd'); + expect(ret).toBe(false); }); }); describe('updatePassword', () => { - 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(); + it('should update password', async () => { + const newPassword = 'newPassword'; + const ret = await accountService.updatePassword(mockTeacherAccount.id, newPassword); - 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'); - } - }); + 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 delete an existing account', () => { - const setup = () => { - const mockTeacherAccount = accountFactory.buildWithId(); - - accountRepo.findById.mockResolvedValue(mockTeacherAccount); - accountLookupServiceMock.getInternalId.mockResolvedValue(mockTeacherAccount._id); - - return { mockTeacherAccount }; - }; + describe('when deleting existing account', () => { it('should delete account via repo', async () => { - const { mockTeacherAccount } = setup(); await accountService.delete(mockTeacherAccount.id); expect(accountRepo.deleteById).toHaveBeenCalledWith(new ObjectId(mockTeacherAccount.id)); }); @@ -689,125 +468,55 @@ 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 account not found', async () => { - const { mockTeacherAccount } = setup(); + it('should throw', async () => { + setup(); await expect(accountService.delete(mockTeacherAccount.id)).rejects.toThrow(); }); }); }); describe('deleteByUserId', () => { - 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); - }); + 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('searchByUsernamePartialMatch', () => { - 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); + 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); - expect(accounts[0]).toEqual(AccountEntityToDtoMapper.mapToDto(mockTeacherAccount)); - }); + expect(accounts[0]).toEqual(AccountEntityToDtoMapper.mapToDto(mockTeacherAccount)); }); }); - describe('searchByUsernameExactMatch', () => { - 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)); - }); + 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('findMany', () => { - 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 each time', async () => { - setup(); - const foundAccounts = await accountService.findMany(); - expect(accountRepo.findMany).toHaveBeenCalledWith(0, 100); - expect(foundAccounts).toBeDefined(); - }); + 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(); }); }); }); 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 2ea02eeb3c4..1209ed86744 100644 --- a/apps/server/src/modules/account/services/account-db.service.ts +++ b/apps/server/src/modules/account/services/account-db.service.ts @@ -1,15 +1,13 @@ 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 bcrypt from 'bcryptjs'; -import { AccountEntityToDtoMapper } from '../mapper'; import { AccountRepo } from '../repo/account.repo'; -import { AccountLookupService } from './account-lookup.service'; -import { AbstractAccountService } from './account.service.abstract'; +import { AccountEntityToDtoMapper } from '../mapper'; import { AccountDto, AccountSaveDto } from './dto'; - -// HINT: do more empty lines :) +import { AbstractAccountService } from './account.service.abstract'; +import { AccountLookupService } from './account-lookup.service'; @Injectable() export class AccountServiceDb extends AbstractAccountService { @@ -34,7 +32,10 @@ export class AccountServiceDb extends AbstractAccountService { } async findByUserIdOrFail(userId: EntityId): Promise { - const accountEntity = await this.accountRepo.findByUserIdOrFail(userId); + const accountEntity = await this.accountRepo.findByUserId(userId); + if (!accountEntity) { + throw new EntityNotFoundError('Account'); + } return AccountEntityToDtoMapper.mapToDto(accountEntity); } @@ -45,8 +46,6 @@ 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); @@ -76,7 +75,7 @@ export class AccountServiceDb extends AbstractAccountService { credentialHash: accountDto.credentialHash, }); - await this.accountRepo.save(account); // HINT: this can be done once in the end + await this.accountRepo.save(account); } return AccountEntityToDtoMapper.mapToDto(account); } @@ -129,7 +128,7 @@ export class AccountServiceDb extends AbstractAccountService { if (!account.password) { return Promise.resolve(false); } - return bcrypt.compare(comparePassword, account.password); // hint: first get result, then return seperately + return bcrypt.compare(comparePassword, account.password); } private async getInternalId(id: EntityId | ObjectId): Promise { 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 2249a485f98..4761bbd80ca 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 @@ -94,125 +94,79 @@ describe('AccountIdmService Integration', () => { } }); - describe('save', () => { - describe('when account does 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, - attDbcAccountId: createdAccount.id, - attDbcUserId: createdAccount.userId, - attDbcSystemId: createdAccount.systemId, - }) - ); - }); - }); + 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, + attDbcAccountId: testDbcAccountId, + attDbcUserId: createdAccount.userId, + attDbcSystemId: createdAccount.systemId, + }) + ); }); - 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 { idmId, newUserName } = await setup(); - - await accountIdmService.save({ - id: testDbcAccountId, - username: newUserName, - }); - - const foundAccount = await identityManagementService.findAccountById(idmId); - - expect(foundAccount).toEqual( - expect.objectContaining({ - id: idmId, - username: newUserName, - }) - ); - }); + it('save should update existing account', async () => { + if (!isIdmReachable) return; + const newUsername = 'jane.doe@mail.tld'; + const idmId = await createAccount(); + + await accountIdmService.save({ + id: testDbcAccountId, + username: newUsername, }); + const foundAccount = await identityManagementService.findAccountById(idmId); + + expect(foundAccount).toEqual( + expect.objectContaining({ + id: idmId, + username: newUsername, + }) + ); }); - describe('updateUsername', () => { - describe('when updating username', () => { - const setup = async () => { - const newUserName = 'jane.doe@mail.tld'; - const idmId = await createAccount(); - - return { newUserName, idmId }; - }; - it('should update only username', async () => { - if (!isIdmReachable) return; - const { newUserName, idmId } = await setup(); - - await accountIdmService.updateUsername(testDbcAccountId, newUserName); - const foundAccount = await identityManagementService.findAccountById(idmId); - - expect(foundAccount).toEqual( - expect.objectContaining>({ - username: newUserName, - }) - ); - }); - }); + it('updateUsername should update username', async () => { + if (!isIdmReachable) return; + const newUserName = 'jane.doe@mail.tld'; + const idmId = await createAccount(); + await accountIdmService.updateUsername(testDbcAccountId, newUserName); + + const foundAccount = await identityManagementService.findAccountById(idmId); + + expect(foundAccount).toEqual( + expect.objectContaining>({ + username: newUserName, + }) + ); }); - describe('updatePassword', () => { - describe('when updating with permitted password', () => { - const setup = async () => { - await createAccount(); - }; - it('should update password', async () => { - if (!isIdmReachable) return; - await setup(); - await expect(accountIdmService.updatePassword(testDbcAccountId, 'newPassword')).resolves.not.toThrow(); - }); - }); + it('updatePassword should update password', async () => { + if (!isIdmReachable) return; + await createAccount(); + await expect(accountIdmService.updatePassword(testDbcAccountId, 'newPassword')).resolves.not.toThrow(); }); - describe('delete', () => { - describe('when delete account', () => { - 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(testDbcAccountId); - await expect(identityManagementService.findAccountById(idmId)).rejects.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(testDbcAccountId); + await expect(identityManagementService.findAccountById(idmId)).rejects.toThrow(); }); - describe('deleteByUserId', () => { - describe('when deleting by UserId', () => { - 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 ?? ''); - 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(); }); }); 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 1669b4ca4c4..4b997d1b3fe 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 @@ -76,203 +76,155 @@ describe('AccountIdmService', () => { }); describe('save', () => { - 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 }; + const setup = () => { + idmServiceMock.createAccount.mockResolvedValue(mockIdmAccount.id); + idmServiceMock.updateAccount.mockResolvedValue(mockIdmAccount.id); + idmServiceMock.updateAccountPassword.mockResolvedValue(mockIdmAccount.id); + idmServiceMock.findAccountById.mockResolvedValue(mockIdmAccount); + }; + + it('should update an existing account', async () => { + setup(); + const updateSpy = jest.spyOn(idmServiceMock, 'updateAccount'); + const createSpy = jest.spyOn(idmServiceMock, 'createAccount'); + + const mockAccountDto = { + id: mockIdmAccountRefId, + username: 'testUserName', + userId: 'userId', + systemId: 'systemId', }; - - it('should update account information', async () => { - const { updateSpy, createSpy, mockAccountDto } = setup(); - - const ret = await accountIdmService.save(mockAccountDto); - - expect(updateSpy).toHaveBeenCalled(); - expect(createSpy).not.toHaveBeenCalled(); - - expect(ret).toBeDefined(); - expect(ret).toMatchObject>({ - id: mockIdmAccount.attDbcAccountId, - 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.attDbcAccountId, + idmReferenceId: mockIdmAccount.id, + createdAt: mockIdmAccount.createdDate, + updatedAt: mockIdmAccount.createdDate, + username: mockIdmAccount.username, }); }); - 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 }; + 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', }; - it('should update account password', async () => { - const { updateSpy, updatePasswordSpy, mockAccountDto } = setup(); + const ret = await accountIdmService.save(mockAccountDto); - const ret = await accountIdmService.save(mockAccountDto); - - expect(updateSpy).toHaveBeenCalled(); - expect(updatePasswordSpy).toHaveBeenCalled(); - expect(ret).toBeDefined(); - }); + expect(updateSpy).toHaveBeenCalled(); + expect(updatePasswordSpy).toHaveBeenCalled(); + expect(ret).toBeDefined(); }); - 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' }; - - return { updateSpy, createSpy, mockAccountDto }; - }; - it('should create a new account', async () => { - const { updateSpy, createSpy, mockAccountDto } = setup(); + it('should create a new account', async () => { + setup(); + const updateSpy = jest.spyOn(idmServiceMock, 'updateAccount'); + const createSpy = jest.spyOn(idmServiceMock, 'createAccount'); - const ret = await accountIdmService.save(mockAccountDto); + const mockAccountDto = { username: 'testUserName', id: undefined, userId: 'userId', systemId: 'systemId' }; + const ret = await accountIdmService.save(mockAccountDto); - expect(updateSpy).not.toHaveBeenCalled(); - expect(createSpy).toHaveBeenCalled(); + expect(updateSpy).not.toHaveBeenCalled(); + expect(createSpy).toHaveBeenCalled(); - expect(ret).toBeDefined(); - expect(ret).toMatchObject>({ - id: mockIdmAccount.attDbcAccountId, - idmReferenceId: mockIdmAccount.id, - createdAt: mockIdmAccount.createdDate, - updatedAt: mockIdmAccount.createdDate, - username: mockIdmAccount.username, - }); + expect(ret).toBeDefined(); + expect(ret).toMatchObject>({ + id: mockIdmAccount.attDbcAccountId, + 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 () => { + setup(); + accountLookupServiceMock.getExternalId.mockResolvedValue(null); + const mockAccountDto = { + id: mockIdmAccountRefId, + username: 'testUserName', + userId: 'userId', + systemId: 'systemId', }; - 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.attDbcAccountId, - idmReferenceId: mockIdmAccount.id, - createdAt: mockIdmAccount.createdDate, - updatedAt: mockIdmAccount.createdDate, - username: mockIdmAccount.username, - }); + + const ret = await accountIdmService.save(mockAccountDto); + + expect(idmServiceMock.createAccount).toHaveBeenCalled(); + expect(ret).toBeDefined(); + expect(ret).toMatchObject>({ + id: mockIdmAccount.attDbcAccountId, + idmReferenceId: mockIdmAccount.id, + createdAt: mockIdmAccount.createdDate, + updatedAt: mockIdmAccount.createdDate, + username: mockIdmAccount.username, }); }); }); describe('updateUsername', () => { - 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.attDbcAccountId, - idmReferenceId: mockIdmAccount.id, - createdAt: mockIdmAccount.createdDate, - updatedAt: mockIdmAccount.createdDate, - username: mockIdmAccount.username, - }); + 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.attDbcAccountId, + idmReferenceId: mockIdmAccount.id, + createdAt: mockIdmAccount.createdDate, + updatedAt: mockIdmAccount.createdDate, + username: mockIdmAccount.username, }); }); }); describe('updatePassword', () => { - 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.attDbcAccountId, - idmReferenceId: mockIdmAccount.id, - createdAt: mockIdmAccount.createdDate, - updatedAt: mockIdmAccount.createdDate, - username: mockIdmAccount.username, - }); + 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.attDbcAccountId, + idmReferenceId: mockIdmAccount.id, + createdAt: mockIdmAccount.createdDate, + updatedAt: mockIdmAccount.createdDate, + username: mockIdmAccount.username, }); }); }); describe('validatePassword', () => { - 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); - }); + 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); }); }); @@ -296,7 +248,7 @@ describe('AccountIdmService', () => { accountLookupServiceMock.getExternalId.mockResolvedValue(null); }; - it('should throw account not found error', async () => { + it('should throw error', async () => { setup(); await expect(accountIdmService.delete(mockIdmAccountRefId)).rejects.toThrow(); }); @@ -304,19 +256,16 @@ describe('AccountIdmService', () => { }); describe('deleteByUserId', () => { - describe('when deleting an account by user id', () => { - const setup = () => { - idmServiceMock.findAccountByDbcUserId.mockResolvedValue(mockIdmAccount); - const deleteSpy = jest.spyOn(idmServiceMock, 'deleteAccountById'); - return { deleteSpy }; - }; + const setup = () => { + idmServiceMock.findAccountByDbcUserId.mockResolvedValue(mockIdmAccount); + }; - 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 () => { + setup(); + const deleteSpy = jest.spyOn(idmServiceMock, 'deleteAccountById'); - await accountIdmService.deleteByUserId(mockIdmAccount.attDbcUserId ?? ''); - expect(deleteSpy).toHaveBeenCalledWith(mockIdmAccount.id); - }); + await accountIdmService.deleteByUserId(mockIdmAccount.attDbcUserId ?? ''); + expect(deleteSpy).toHaveBeenCalledWith(mockIdmAccount.id); }); }); @@ -338,7 +287,7 @@ describe('AccountIdmService', () => { idmServiceMock.findAccountById.mockRejectedValue(new Error()); }; - it('should throw account not found', async () => { + it('should throw', async () => { setup(); await expect(accountIdmService.findById('notExistingId')).rejects.toThrow(); }); @@ -410,7 +359,7 @@ describe('AccountIdmService', () => { idmServiceMock.findAccountByDbcUserId.mockResolvedValue(undefined as unknown as IdmAccount); }; - it('should throw account not found', async () => { + it('should throw', async () => { setup(); await expect(accountIdmService.findByUserIdOrFail('notExistingId')).rejects.toThrow(EntityNotFoundError); }); @@ -516,7 +465,7 @@ describe('AccountIdmService', () => { }); }); - it('findMany should throw not implemented Exception', async () => { + it('findMany should throw', async () => { await expect(accountIdmService.findMany(0, 0)).rejects.toThrow(NotImplementedException); }); }); 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 039db80eddf..68bcfb42bae 100644 --- a/apps/server/src/modules/account/services/account-idm.service.ts +++ b/apps/server/src/modules/account/services/account-idm.service.ts @@ -1,13 +1,13 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { Injectable, NotImplementedException } from '@nestjs/common'; import { EntityNotFoundError } from '@shared/common'; -import { IdentityManagementOauthService, IdentityManagementService } from '@shared/infra/identity-management'; import { Counted, EntityId, IdmAccount, IdmAccountUpdate } from '@shared/domain'; +import { IdentityManagementService, IdentityManagementOauthService } from '@shared/infra/identity-management'; import { LegacyLogger } from '@src/core/logger'; 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 { @@ -27,7 +27,6 @@ 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) { @@ -35,7 +34,6 @@ export class AccountServiceIdm extends AbstractAccountService { // eslint-disable-next-line no-await-in-loop results.push(await this.identityManager.findAccountByDbcUserId(userId)); } catch { - // TODO: dont simply forget errors. maybe use a filter instead? // ignore entry } } @@ -48,7 +46,6 @@ export class AccountServiceIdm extends AbstractAccountService { const result = await this.identityManager.findAccountByDbcUserId(userId); return this.accountIdmToDtoMapper.mapToDto(result); } catch { - // TODO: dont simply forget errors return null; } } @@ -96,10 +93,8 @@ export class AccountServiceIdm extends AbstractAccountService { attDbcUserId: accountDto.userId, attDbcSystemId: 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 { 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 d25dbc0ac4a..b2e198f6a86 100644 --- a/apps/server/src/modules/account/services/account.service.abstract.ts +++ b/apps/server/src/modules/account/services/account.service.abstract.ts @@ -2,8 +2,6 @@ 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; @@ -13,7 +11,6 @@ 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.integration.spec.ts b/apps/server/src/modules/account/services/account.service.integration.spec.ts index 5d5caa24263..d001925000b 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 @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createMock } from '@golevelup/ts-jest'; import KeycloakAdminClient from '@keycloak/keycloak-admin-client-cjs/keycloak-admin-client-cjs-index'; import { EntityManager } from '@mikro-orm/mongodb'; @@ -159,151 +158,95 @@ describe('AccountService Integration', () => { ); }; - 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); - }); - }); - - 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); - }); - }); - - 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(); + 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); + }); - const updatedAccount = await accountService.save({ - ...originalAccount, - username: newUsername, - }); - await compareDbAccount(dbId, updatedAccount); - await compareIdmAccount(updatedAccount.idmReferenceId ?? '', updatedAccount); - }); + 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, }); + await compareDbAccount(dbId, updatedAccount); + await compareIdmAccount(idmId, updatedAccount); }); - describe('updateUsername', () => { - describe('when updating Username', () => { - const setup = async () => { - const newUsername = 'jane.doe@mail.tld'; - const [dbId, idmId] = await createAccount(); - - return { newUsername, dbId, idmId }; - }; - it('should update username', async () => { - if (!isIdmReachable) return; - 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>({ - username: newUsername, - }) - ); - expect(foundDbAccount).toEqual( - expect.objectContaining>({ - username: newUsername, - }) - ); - }); + 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, }); + await compareDbAccount(dbId, updatedAccount); + await compareIdmAccount(updatedAccount.idmReferenceId ?? '', updatedAccount); }); - describe('updatePassword', () => { - describe('when updating password', () => { - const setup = async () => { - const [dbId] = await createAccount(); - - const foundDbAccountBefore = await accountRepo.findById(dbId); - const previousPasswordHash = foundDbAccountBefore.password; - const foundDbAccountAfter = await accountRepo.findById(dbId); - - return { dbId, previousPasswordHash, foundDbAccountAfter }; - }; - it('should update password', async () => { - if (!isIdmReachable) return; - const { dbId, previousPasswordHash, foundDbAccountAfter } = await setup(); + 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); - await expect(accountService.updatePassword(dbId, 'newPassword')).resolves.not.toThrow(); - - expect(foundDbAccountAfter.password).not.toEqual(previousPasswordHash); - }); - }); + 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, + }) + ); }); - describe('delete', () => { - describe('when delete an account', () => { - const setup = async () => { - const [dbId, idmId] = await createAccount(); - const foundIdmAccount = await identityManagementService.findAccountById(idmId); - const foundDbAccount = await accountRepo.findById(dbId); + it('updatePassword should update password', async () => { + if (!isIdmReachable) return; + const [dbId] = await createAccount(); - return { dbId, idmId, foundIdmAccount, foundDbAccount }; - }; - it('should remove account', async () => { - if (!isIdmReachable) return; - const { dbId, idmId, foundIdmAccount, foundDbAccount } = await setup(); + const foundDbAccountBefore = await accountRepo.findById(dbId); + const previousPasswordHash = foundDbAccountBefore.password; - expect(foundIdmAccount).toBeDefined(); - expect(foundDbAccount).toBeDefined(); + await expect(accountService.updatePassword(dbId, 'newPassword')).resolves.not.toThrow(); - await accountService.delete(dbId); - await expect(identityManagementService.findAccountById(idmId)).rejects.toThrow(); - await expect(accountRepo.findById(dbId)).rejects.toThrow(); - }); - }); + const foundDbAccountAfter = await accountRepo.findById(dbId); + expect(foundDbAccountAfter.password).not.toEqual(previousPasswordHash); }); - describe('deleteByUserId', () => { - describe('when delete an account by User Id', () => { - const setup = async () => { - const [dbId, idmId] = await createAccount(); - const foundIdmAccount = await identityManagementService.findAccountById(idmId); - const foundDbAccount = await accountRepo.findById(dbId); + 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(); - 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(); + 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(); - await accountService.deleteByUserId(testAccount.userId ?? ''); - await expect(identityManagementService.findAccountById(idmId)).rejects.toThrow(); - await expect(accountRepo.findById(dbId)).rejects.toThrow(); - }); - }); + await accountService.deleteByUserId(testAccount.userId ?? ''); + await expect(identityManagementService.findAccountById(idmId)).rejects.toThrow(); + await expect(accountRepo.findById(dbId)).rejects.toThrow(); }); }); 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 67851d0a6b0..4cb95d96a36 100644 --- a/apps/server/src/modules/account/services/account.service.spec.ts +++ b/apps/server/src/modules/account/services/account.service.spec.ts @@ -63,568 +63,402 @@ describe('AccountService', () => { }); describe('findById', () => { - 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); - }); + it('should call findById in accountServiceDb', async () => { + await expect(accountService.findById('id')).resolves.not.toThrow(); + expect(accountServiceDb.findById).toHaveBeenCalledTimes(1); }); }); describe('findByUserId', () => { - 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); - }); + it('should call findByUserId in accountServiceDb', async () => { + await expect(accountService.findByUserId('userId')).resolves.not.toThrow(); + expect(accountServiceDb.findByUserId).toHaveBeenCalledTimes(1); }); }); describe('findByUsernameAndSystemId', () => { - 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); - }; - - it('should call idm implementation', async () => { - const service = setup(); - await expect(service.findByUsernameAndSystemId('username', 'systemId')).resolves.not.toThrow(); - expect(accountServiceIdm.findByUsernameAndSystemId).toHaveBeenCalledTimes(1); - }); + it('should call findByUsernameAndSystemId in accountServiceDb', async () => { + await expect(accountService.findByUsernameAndSystemId('username', 'systemId')).resolves.not.toThrow(); + expect(accountServiceDb.findByUsernameAndSystemId).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); - }); + 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); - }; + }); - it('should call idm implementation', async () => { - const service = setup(); - await expect(service.findMultipleByUserId(['userId'])).resolves.not.toThrow(); - expect(accountServiceIdm.findMultipleByUserId).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('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); - }); + describe('save', () => { + it('should call save in accountServiceDb', async () => { + await expect(accountService.save({} as AccountSaveDto)).resolves.not.toThrow(); + expect(accountServiceDb.save).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 save in accountServiceIdm if feature is enabled', async () => { + const spy = jest.spyOn(configService, 'get'); + spy.mockReturnValueOnce(true); - it('should call idm implementation', async () => { - const service = setup(); - await expect(service.findByUserIdOrFail('userId')).resolves.not.toThrow(); - expect(accountServiceIdm.findByUserIdOrFail).toHaveBeenCalledTimes(1); - }); + await expect(accountService.save({} as AccountSaveDto)).resolves.not.toThrow(); + expect(accountServiceIdm.save).toHaveBeenCalledTimes(1); + }); + it('should not call save in accountServiceIdm if feature is disabled', async () => { + const spy = jest.spyOn(configService, 'get'); + spy.mockReturnValueOnce(false); + + await expect(accountService.save({} as AccountSaveDto)).resolves.not.toThrow(); + expect(accountServiceIdm.save).not.toHaveBeenCalled(); }); }); - 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); - }); + 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'); }); - describe('When calling save in accountService if feature is enabled', () => { - const setup = () => { - configService.get.mockReturnValueOnce(true); + it('should not throw if username for an external user is not an email', async () => { + const params: AccountSaveDto = { + username: 'John Doe', + systemId: 'ABC123', }; - it('should call save in accountServiceIdm', async () => { - setup(); - - await expect(accountService.save({} as AccountSaveDto)).resolves.not.toThrow(); - expect(accountServiceIdm.save).toHaveBeenCalledTimes(1); - }); + await expect(accountService.saveWithValidation(params)).resolves.not.toThrow(); }); - describe('When calling save in accountService if feature is disabled', () => { - const setup = () => { - configService.get.mockReturnValueOnce(false); + 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', }; - it('should not call save in accountServiceIdm', async () => { - setup(); - - await expect(accountService.save({} as AccountSaveDto)).resolves.not.toThrow(); - expect(accountServiceIdm.save).not.toHaveBeenCalled(); - }); + await expect(accountService.saveWithValidation(params)).resolves.not.toThrow(); }); - describe('when identity management is primary', () => { - const setup = () => { - configService.get.mockReturnValue(true); - return new AccountService(accountServiceDb, accountServiceIdm, configService, accountValidationService, logger); + it('should throw if no password is provided for an internal user', async () => { + const params: AccountSaveDto = { + username: 'john.doe@mail.tld', }; - it('should call idm implementation', async () => { - setup(); - await expect(accountService.save({ username: 'username' })).resolves.not.toThrow(); - expect(accountServiceIdm.save).toHaveBeenCalledTimes(1); - }); + await expect(accountService.saveWithValidation(params)).rejects.toThrow('No password provided'); }); - }); - - describe('saveWithValidation', () => { - describe('When calling saveWithValidation on accountService', () => { - const setup = () => { - const spy = jest.spyOn(accountService, 'save'); - return spy; + it('should throw if account already exists', 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 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', }; - it('should not sanitize username for external user', async () => { - const spy = setup(); + await expect(accountService.saveWithValidation(params)).rejects.toThrow('Username already exists'); + }); + }); - 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(); - }); + describe('updateUsername', () => { + it('should call updateUsername in accountServiceDb', async () => { + await expect(accountService.updateUsername('accountId', 'username')).resolves.not.toThrow(); + expect(accountServiceDb.updateUsername).toHaveBeenCalledTimes(1); }); + it('should call updateUsername in accountServiceIdm if feature is enabled', async () => { + const spy = jest.spyOn(configService, 'get'); + spy.mockReturnValueOnce(true); - describe('When username for a local user is not an email', () => { - it('should throw username is not an email error', async () => { - const params: AccountSaveDto = { - username: 'John Doe', - password: 'JohnsPassword', - }; - await expect(accountService.saveWithValidation(params)).rejects.toThrow('Username is not an email'); - }); + await expect(accountService.updateUsername('accountId', 'username')).resolves.not.toThrow(); + expect(accountServiceIdm.updateUsername).toHaveBeenCalledTimes(1); }); + it('should not call updateUsername in accountServiceIdm if feature is disabled', async () => { + const spy = jest.spyOn(configService, 'get'); + spy.mockReturnValueOnce(false); - describe('When username for an external user is not an email', () => { - it('should not throw an error', async () => { - const params: AccountSaveDto = { - username: 'John Doe', - systemId: 'ABC123', - }; - await expect(accountService.saveWithValidation(params)).resolves.not.toThrow(); - }); + await expect(accountService.updateUsername('accountId', 'username')).resolves.not.toThrow(); + expect(accountServiceIdm.updateUsername).not.toHaveBeenCalled(); }); + }); - describe('When username for an external user is a ldap search string', () => { - it('should not throw an error', async () => { - const params: AccountSaveDto = { - username: 'dc=schul-cloud,dc=org/fake.ldap', - 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 no password is provided for an internal user', () => { - it('should throw no password provided error', async () => { - const params: AccountSaveDto = { - username: 'john.doe@mail.tld', - }; - await expect(accountService.saveWithValidation(params)).rejects.toThrow('No password provided'); - }); + describe('updatePassword', () => { + it('should call updatePassword in accountServiceDb', async () => { + await expect(accountService.updatePassword('accountId', 'password')).resolves.not.toThrow(); + expect(accountServiceDb.updatePassword).toHaveBeenCalledTimes(1); }); + it('should call updatePassword in accountServiceIdm if feature is enabled', async () => { + const spy = jest.spyOn(configService, 'get'); + spy.mockReturnValueOnce(true); - describe('When account already exists', () => { - it('should throw account already exists', 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'); - }); + await expect(accountService.updatePassword('accountId', 'password')).resolves.not.toThrow(); + expect(accountServiceIdm.updatePassword).toHaveBeenCalledTimes(1); }); + it('should not call updatePassword in accountServiceIdm if feature is disabled', async () => { + const spy = jest.spyOn(configService, 'get'); + spy.mockReturnValueOnce(false); - describe('When username already exists', () => { - const setup = () => { - accountValidationService.isUniqueEmail.mockResolvedValueOnce(false); - }; - it('should throw username already exists', async () => { - setup(); - const params: AccountSaveDto = { - username: 'john.doe@mail.tld', - password: 'JohnsPassword', - }; - await expect(accountService.saveWithValidation(params)).rejects.toThrow('Username already exists'); - }); + 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); - }; + }); - 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('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); }); }); - 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); - }); + describe('delete', () => { + it('should call delete in accountServiceDb', async () => { + await expect(accountService.delete('accountId')).resolves.not.toThrow(); + expect(accountServiceDb.delete).toHaveBeenCalledTimes(1); }); + it('should call delete 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 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).toHaveBeenCalledTimes(1); + }); + it('should not call delete 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).toHaveBeenCalledTimes(1); - }); + await expect(accountService.delete('accountId')).resolves.not.toThrow(); + expect(accountServiceIdm.delete).not.toHaveBeenCalled(); }); + }); - 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(); + 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); - await expect(accountService.updateUsername('accountId', 'username')).resolves.not.toThrow(); - expect(accountServiceIdm.updateUsername).not.toHaveBeenCalled(); - }); + await expect(accountService.deleteByUserId('userId')).resolves.not.toThrow(); + expect(accountServiceIdm.deleteByUserId).toHaveBeenCalledTimes(1); }); - describe('When identity management is primary', () => { - const setup = () => { - configService.get.mockReturnValue(true); - return new AccountService(accountServiceDb, accountServiceIdm, configService, accountValidationService, logger); - }; + it('should not call deleteByUserId in accountServiceIdm if feature is disabled', async () => { + const spy = jest.spyOn(configService, 'get'); + spy.mockReturnValueOnce(false); - 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).not.toHaveBeenCalled(); }); }); - 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('findMany', () => { + it('should call findMany in accountServiceDb', async () => { + await expect(accountService.findMany()).resolves.not.toThrow(); + expect(accountServiceDb.findMany).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 () => { - setup(); - await expect(accountService.updateLastTriedFailedLogin('accountId', new Date())).resolves.not.toThrow(); - expect(accountServiceIdm.updateLastTriedFailedLogin).toHaveBeenCalledTimes(1); - }); + describe('searchByUsernamePartialMatch', () => { + it('should call searchByUsernamePartialMatch in accountServiceDb', async () => { + await expect(accountService.searchByUsernamePartialMatch('username', 1, 1)).resolves.not.toThrow(); + expect(accountServiceDb.searchByUsernamePartialMatch).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('searchByUsernameExactMatch', () => { + it('should call searchByUsernameExactMatch in accountServiceDb', async () => { + await expect(accountService.searchByUsernameExactMatch('username')).resolves.not.toThrow(); + expect(accountServiceDb.searchByUsernameExactMatch).toHaveBeenCalledTimes(1); }); + }); - describe('When calling updatePassword in accountService if feature is enabled', () => { - const setup = () => { - configService.get.mockReturnValueOnce(true); - }; - it('should call updatePassword in accountServiceIdm', async () => { - setup(); + 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'); - await expect(accountService.updatePassword('accountId', 'password')).resolves.not.toThrow(); - expect(accountServiceIdm.updatePassword).toHaveBeenCalledTimes(1); + const deleteByUserIdMock = jest.spyOn(accountServiceIdm, 'deleteByUserId'); + deleteByUserIdMock.mockImplementationOnce(() => { + throw testError; }); + + await expect(accountService.deleteByUserId('userId')).resolves.not.toThrow(); + expect(spyLogger).toHaveBeenCalledWith(testError, expect.anything()); }); - 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(); + it('should throw an non error object', async () => { + const spy = jest.spyOn(configService, 'get'); + spy.mockReturnValueOnce(true); + const spyLogger = jest.spyOn(logger, 'error'); - await expect(accountService.updatePassword('accountId', 'password')).resolves.not.toThrow(); - expect(accountServiceIdm.updatePassword).not.toHaveBeenCalled(); + const deleteByUserIdMock = jest.spyOn(accountServiceIdm, 'deleteByUserId'); + deleteByUserIdMock.mockImplementationOnce(() => { + // eslint-disable-next-line @typescript-eslint/no-throw-literal + throw 'a non error object'; }); - }); - 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 () => { - setup(); - 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('a non error object'); }); }); - 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('when identity management is primary', () => { + const setup = () => { + configService.get.mockReturnValue(true); + return new AccountService(accountServiceDb, accountServiceIdm, configService, accountValidationService, logger); + }; - 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 () => { + describe('findById', () => { + it('should call idm implementation', async () => { const service = setup(); - await expect(service.validatePassword({} as AccountDto, 'password')).resolves.not.toThrow(); - expect(accountServiceDb.validatePassword).toHaveBeenCalledTimes(0); - expect(accountServiceIdm.validatePassword).toHaveBeenCalledTimes(1); + await expect(service.findById('accountId')).resolves.not.toThrow(); + expect(accountServiceIdm.findById).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('findMultipleByUserId', () => { + it('should call idm implementation', async () => { + const service = setup(); + await expect(service.findMultipleByUserId(['userId'])).resolves.not.toThrow(); + expect(accountServiceIdm.findMultipleByUserId).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('findByUserId', () => { + it('should call idm implementation', async () => { + const service = setup(); + await expect(service.findByUserId('userId')).resolves.not.toThrow(); + expect(accountServiceIdm.findByUserId).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('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 identity management is primary', () => { - const setup = () => { - configService.get.mockReturnValue(true); - return new AccountService(accountServiceDb, accountServiceIdm, configService, accountValidationService, logger); - }; + describe('findByUsernameAndSystemId', () => { 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.findByUsernameAndSystemId('username', 'systemId')).resolves.not.toThrow(); + expect(accountServiceIdm.findByUsernameAndSystemId).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('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); }); }); - 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.deleteByUserId('userId')).resolves.not.toThrow(); - expect(accountServiceIdm.deleteByUserId).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('When calling deleteByUserId in accountService if feature is disabled', () => { - const setup = () => { - configService.get.mockReturnValueOnce(false); - }; - it('should not call deleteByUserId in accountServiceIdm', async () => { + describe('save', () => { + it('should call idm implementation', async () => { setup(); - - await expect(accountService.deleteByUserId('userId')).resolves.not.toThrow(); - expect(accountServiceIdm.deleteByUserId).not.toHaveBeenCalled(); + await expect(accountService.save({ username: 'username' })).resolves.not.toThrow(); + expect(accountServiceIdm.save).toHaveBeenCalledTimes(1); }); }); - describe('When identity management is primary', () => { - const setup = () => { - configService.get.mockReturnValue(true); - return new AccountService(accountServiceDb, accountServiceIdm, configService, accountValidationService, logger); - }; + describe('saveWithValidation', () => { it('should call idm implementation', async () => { setup(); - await expect(accountService.deleteByUserId('userId')).resolves.not.toThrow(); - expect(accountServiceIdm.deleteByUserId).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('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); + await expect( + accountService.saveWithValidation({ username: 'username@mail.tld', password: 'password' }) + ).resolves.not.toThrow(); + expect(accountServiceIdm.save).toHaveBeenCalledTimes(1); }); }); - 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 () => { - const service = setup(); - await expect(service.searchByUsernamePartialMatch('username', 0, 1)).resolves.not.toThrow(); - expect(accountServiceIdm.searchByUsernamePartialMatch).toHaveBeenCalledTimes(1); + setup(); + await expect(accountService.updateUsername('accountId', 'username')).resolves.not.toThrow(); + expect(accountServiceIdm.updateUsername).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('updateLastTriedFailedLogin', () => { + it('should call idm implementation', async () => { + setup(); + await expect(accountService.updateLastTriedFailedLogin('accountId', new Date())).resolves.not.toThrow(); + expect(accountServiceIdm.updateLastTriedFailedLogin).toHaveBeenCalledTimes(1); }); }); - describe('when identity management is primary', () => { - const setup = () => { - configService.get.mockReturnValue(true); - return new AccountService(accountServiceDb, accountServiceIdm, configService, accountValidationService, logger); - }; + describe('updatePassword', () => { it('should call idm implementation', async () => { - const service = setup(); - await expect(service.searchByUsernameExactMatch('username')).resolves.not.toThrow(); - expect(accountServiceIdm.searchByUsernameExactMatch).toHaveBeenCalledTimes(1); + setup(); + await expect(accountService.updatePassword('accountId', 'password')).resolves.not.toThrow(); + expect(accountServiceIdm.updatePassword).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(spyLogger).toHaveBeenCalledWith(testError, expect.anything()); + describe('delete', () => { + it('should call idm implementation', async () => { + setup(); + await expect(accountService.delete('accountId')).resolves.not.toThrow(); + expect(accountServiceIdm.delete).toHaveBeenCalledTimes(1); }); }); - 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(); - + describe('deleteByUserId', () => { + it('should call idm implementation', async () => { + setup(); await expect(accountService.deleteByUserId('userId')).resolves.not.toThrow(); - expect(spyLogger).toHaveBeenCalledWith('a non error object'); + expect(accountServiceIdm.deleteByUserId).toHaveBeenCalledTimes(1); }); }); }); diff --git a/apps/server/src/modules/account/services/account.service.ts b/apps/server/src/modules/account/services/account.service.ts index 3c8a5ff2058..6c2070550ab 100644 --- a/apps/server/src/modules/account/services/account.service.ts +++ b/apps/server/src/modules/account/services/account.service.ts @@ -4,8 +4,7 @@ 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'; // TODO: use path alias -// TODO: account needs to define its own config, which is made available for the server +import { LegacyLogger } from '../../../core/logger'; import { IServerConfig } from '../../server/server.config'; import { AccountServiceDb } from './account-db.service'; import { AccountServiceIdm } from './account-idm.service'; @@ -13,11 +12,6 @@ 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; @@ -84,7 +78,6 @@ 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) { @@ -115,7 +108,6 @@ 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.spec.ts b/apps/server/src/modules/account/services/account.validation.service.spec.ts index c152f01a59b..dba1e2bf02a 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 { Permission, Role, RoleName } from '@shared/domain'; +import { EntityNotFoundError } from '@shared/common'; +import { Account, EntityId, Permission, Role, RoleName, User } 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,8 +11,26 @@ describe('AccountValidationService', () => { let module: TestingModule; let accountValidationService: AccountValidationService; - let userRepo: DeepMocked; - let accountRepo: DeepMocked; + 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[]; afterAll(async () => { await module.close(); @@ -24,405 +42,237 @@ describe('AccountValidationService', () => { AccountValidationService, { provide: AccountRepo, - useValue: createMock(), + 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); + }, + }, }, { provide: UserRepo, - useValue: createMock(), + 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([]); + }), + }, }, ], }).compile(); accountValidationService = module.get(AccountValidationService); - - userRepo = module.get(UserRepo); - accountRepo = module.get(AccountRepo); - await setupEntities(); }); beforeEach(() => { - jest.resetAllMocks(); - }); - - 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); - }); + mockTeacherUser = userFactory.buildWithId({ + roles: [new Role({ name: RoleName.TEACHER, permissions: [Permission.STUDENT_EDIT] })], }); - - 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); - }); + mockStudentUser = userFactory.buildWithId({ + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], }); - - 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); - }); + mockOtherTeacherUser = userFactory.buildWithId({ + roles: [ + new Role({ + name: RoleName.TEACHER, + permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], + }), + ], }); - - 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, - ], - }), - ], - }); - const mockAdminAccount = accountFactory.buildWithId({ userId: mockAdminUser.id }); - const mockStudentAccount = accountFactory.buildWithId({ userId: mockStudentUser.id }); - - 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); - }); + mockExternalUser = userFactory.buildWithId({ + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], }); - - 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] })], - }); - 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', async () => { - const { mockTeacherAccount, mockStudentUser, mockStudentAccount } = setup(); - const res = await accountValidationService.isUniqueEmail( - mockTeacherAccount.username, - mockStudentUser.id, - mockStudentAccount.id, - mockStudentAccount.systemId?.toString() - ); - expect(res).toBe(false); - }); + mockOtherExternalUser = userFactory.buildWithId({ + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], }); - 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]); + 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, + }); + mockOtherExternalUserAccount = accountFactory.buildWithId({ + userId: mockOtherExternalUser.id, + username: 'unique.within@system', + systemId: externalSystemB.id, + }); - 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); - }); + oprhanAccount = accountFactory.buildWithId({ + username: 'orphan@account', + userId: undefined, + systemId: new ObjectId(), }); - 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], - }), + 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, ], - }); - - 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); - }); + }), + ], }); + mockUsers = [ + mockTeacherUser, + mockStudentUser, + mockOtherTeacherUser, + mockAdminUser, + mockExternalUser, + mockOtherExternalUser, + ]; + }); - 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('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); + }); + 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); + }); + 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); + }); + 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); + }); + 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('isUniqueEmailForUser', () => { - 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 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 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); - }); + 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('isUniqueEmailForAccount', () => { - 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 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 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 return false, if not the given users email', async () => { + const res = await accountValidationService.isUniqueEmailForAccount(mockStudentUser.email, mockTeacherAccount.id); + expect(res).toBe(false); }); - - 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); - }); + it('should ignore missing user for a given account', async () => { + const res = await accountValidationService.isUniqueEmailForAccount(oprhanAccount.username, oprhanAccount.id); + expect(res).toBe(true); }); }); }); 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 2cabc9eabb3..fc47569ed71 100644 --- a/apps/server/src/modules/account/services/account.validation.service.ts +++ b/apps/server/src/modules/account/services/account.validation.service.ts @@ -5,11 +5,9 @@ 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 @@ -29,12 +27,12 @@ export class AccountValidationService { } async isUniqueEmailForUser(email: string, userId: EntityId): Promise { - const account = await this.accountRepo.findByUserId(userId); // TODO: findOrFail? + const account = await this.accountRepo.findByUserId(userId); return this.isUniqueEmail(email, userId, account?.id, account?.systemId?.toString()); } async isUniqueEmailForAccount(email: string, accountId: EntityId): Promise { - const account = await this.accountRepo.findById(accountId); // TODO: findOrFail? + const account = await this.accountRepo.findById(accountId); 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 c3765576e50..760be1f2453 100644 --- a/apps/server/src/modules/account/services/dto/account.dto.ts +++ b/apps/server/src/modules/account/services/dto/account.dto.ts @@ -1,7 +1,6 @@ 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.spec.ts b/apps/server/src/modules/account/uc/account.uc.spec.ts index 526720c43f7..aa4cdf56a82 100644 --- a/apps/server/src/modules/account/uc/account.uc.spec.ts +++ b/apps/server/src/modules/account/uc/account.uc.spec.ts @@ -4,11 +4,13 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AuthorizationError, EntityNotFoundError, ForbiddenOperationError, ValidationError } from '@shared/common'; import { Account, + Counted, EntityId, Permission, PermissionService, Role, RoleName, + SchoolEntity, SchoolRolePermission, SchoolRoles, User, @@ -35,19 +37,62 @@ import { AccountUc } from './account.uc'; describe('AccountUc', () => { let module: TestingModule; let accountUc: AccountUc; - let userRepo: DeepMocked; - let accountService: DeepMocked; - let accountValidationService: DeepMocked; + let userRepo: UserRepo; + let accountService: AccountService; + let accountValidationService: AccountValidationService; let configService: DeepMocked; + let mockSchool: SchoolEntity; + let mockOtherSchool: SchoolEntity; + let mockSchoolWithStudentVisibility: SchoolEntity; + + 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(); }); @@ -57,7 +102,103 @@ describe('AccountUc', () => { AccountUc, { provide: AccountService, - useValue: createMock(), + 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), + }, }, { provide: ConfigService, @@ -65,12 +206,42 @@ describe('AccountUc', () => { }, { provide: UserRepo, - useValue: createMock(), + 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(); + }), + }, }, PermissionService, { provide: AccountValidationService, - useValue: createMock(), + useValue: { + isUniqueEmail: jest.fn().mockResolvedValue(true), + }, }, ], }).compile(); @@ -78,3052 +249,983 @@ 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(() => { - 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 - ); - }); + 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], + }), + ], }); - - describe('When account does not exists', () => { - 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], - }), + 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, ], - }); - - accountService.findByUserIdOrFail.mockImplementation((): Promise => { - throw new EntityNotFoundError(Account.name); - }); - - return { mockUserWithoutAccount }; - }; - - it('should throw entity not found error', 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(); - - 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, - }); - - accountService.findByUserIdOrFail.mockResolvedValueOnce( - AccountEntityToDtoMapper.mapToDto(mockExternalUserAccount) - ); - - return { mockExternalUserAccount }; - }; - it('should throw ForbiddenOperationError', async () => { - const { mockExternalUserAccount } = setup(); - - await expect( - accountUc.updateMyAccount(mockExternalUserAccount.userId?.toString() ?? '', { - passwordOld: defaultPassword, - }) - ).rejects.toThrow(ForbiddenOperationError); - }); + mockTeacherUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.TEACHER, + permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], + }), + ], }); - - 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); - }); + mockOtherTeacherUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.TEACHER, + permissions: [Permission.STUDENT_EDIT, Permission.STUDENT_LIST, Permission.TEACHER_LIST], + }), + ], }); - - describe('When changing own name is not allowed', () => { - 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 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); - }); + mockTeacherNoUserPermissionUser = userFactory.buildWithId({ + school: mockSchoolWithStudentVisibility, + roles: [ + new Role({ + name: RoleName.TEACHER, + permissions: [], + }), + ], }); - - 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(); - }); + mockTeacherNoUserNoSchoolPermissionUser = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.TEACHER, + permissions: [], + }), + ], }); - 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() })); - }); + mockStudentSchoolPermissionUser = userFactory.buildWithId({ + school: mockSchoolWithStudentVisibility, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], }); - - 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() })); - }); + mockOtherStudentSchoolPermissionUser = userFactory.buildWithId({ + school: mockSchoolWithStudentVisibility, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], }); - 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'); - 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() })); - }); + mockStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], }); - - 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); - }); + mockOtherStudentUser = userFactory.buildWithId({ + school: mockSchool, + roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], }); - - 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(); - }); + mockDifferentSchoolAdminUser = userFactory.buildWithId({ + school: mockOtherSchool, + roles: [...mockAdminUser.roles], }); - - 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, - }); - - 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(); - }); + mockDifferentSchoolTeacherUser = userFactory.buildWithId({ + school: mockOtherSchool, + roles: [...mockTeacherUser.roles], }); - - 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, - ], - }), - ], - }); - - const mockAdminAccount = accountFactory.buildWithId({ - userId: mockAdminUser.id, - password: defaultPasswordHash, - }); - - userRepo.findById.mockResolvedValue(mockAdminUser); - accountService.findByUserIdOrFail.mockResolvedValue(AccountEntityToDtoMapper.mapToDto(mockAdminAccount)); - accountService.validatePassword.mockResolvedValue(true); - - 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(); - }); + mockDifferentSchoolStudentUser = userFactory.buildWithId({ + school: mockOtherSchool, + roles: [...mockStudentUser.roles], }); - - 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, - }); - - userRepo.findById.mockResolvedValue(mockSuperheroUser); - accountService.findByUserIdOrFail.mockResolvedValue(AccountEntityToDtoMapper.mapToDto(mockSuperheroAccount)); - accountService.validatePassword.mockResolvedValue(true); - - 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(); - }); + mockUserWithoutAccount = userFactory.buildWithId({ + school: mockSchool, + roles: [ + new Role({ + name: RoleName.ADMINISTRATOR, + permissions: [Permission.TEACHER_EDIT, Permission.STUDENT_EDIT], + }), + ], }); - - describe('When user can not be updated', () => { - 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, - }); - - userRepo.findById.mockResolvedValue(mockTeacherUser); - userRepo.save.mockRejectedValueOnce(undefined); - accountService.findByUserIdOrFail.mockResolvedValue(AccountEntityToDtoMapper.mapToDto(mockTeacherAccount)); - accountService.validatePassword.mockResolvedValue(true); - - return { mockTeacherUser, mockTeacherAccount }; - }; - it('should throw EntityNotFoundError', async () => { - const { mockTeacherUser } = setup(); - await expect( - accountUc.updateMyAccount(mockTeacherUser.id, { - passwordOld: defaultPassword, - firstName: 'failToUpdate', - }) - ).rejects.toThrow(EntityNotFoundError); - }); + mockUserWithoutRole = userFactory.buildWithId({ + school: mockSchool, + roles: [], }); - - 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, - password: defaultPasswordHash, - }); - - 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); - }); + 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: [] })], }); - 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'); + 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, + }); - accountValidationService.isUniqueEmail.mockResolvedValue(true); + 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 { mockStudentUser, spyAccountServiceSave }; - }; - it('should not update password', async () => { - const { mockStudentUser, spyAccountServiceSave } = setup(); - await accountUc.updateMyAccount(mockStudentUser.id, { + 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, - passwordNew: undefined, - email: 'newemail@to.update', - }); - expect(spyAccountServiceSave).toHaveBeenCalledWith( - expect.objectContaining({ - password: undefined, - }) - ); - }); + }) + ).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, + }) + ); }); }); 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 passwords do not match', async () => { + await expect( + accountUc.replaceMyTemporaryPassword( + mockStudentAccount.userId?.toString() ?? '', + defaultPassword, + 'FooPasswd!1' + ) + ).rejects.toThrow(ForbiddenOperationError); }); - describe('When account does not exists', () => { - 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 account does not exist', async () => { + await expect( + accountUc.replaceMyTemporaryPassword(mockUserWithoutAccount.id, defaultPassword, defaultPassword) + ).rejects.toThrow(EntityNotFoundError); }); - - 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); - }); + 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); + }); + }); - 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) - ); + 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(); - await expect( - accountUc.replaceMyTemporaryPassword( - mockExternalUserAccount.userId?.toString() ?? '', - defaultPassword, - defaultPassword - ) - ).rejects.toThrow(ForbiddenOperationError); + describe('hasPermissionsToAccessAccount', () => { + beforeEach(() => { + configService.get.mockReturnValue(false); }); - }); - describe('When not the users password is temporary', () => { - const setup = () => { - const mockSchool = schoolFactory.buildWithId(); + 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(); + }); + 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(); + }); + it('admin can not access any account of a foreign school via user id', async () => { + const currentUser = { userId: mockDifferentSchoolAdminUser.id } as ICurrentUser; - const mockStudentUser = userFactory.buildWithId({ - school: mockSchool, - roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], - forcePasswordChange: false, - preferences: { firstLogin: true }, - }); + let params = { type: AccountSearchType.USER_ID, value: mockTeacherUser.id } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); - const mockStudentAccount = accountFactory.buildWithId({ - userId: mockStudentUser.id, - password: defaultPasswordHash, - }); + params = { type: AccountSearchType.USER_ID, value: mockStudentUser.id } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); + }); + 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(); + }); + 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(); + }); + 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(); + }); + it('teacher can not access any account of a foreign school via user id', async () => { + const currentUser = { userId: mockDifferentSchoolTeacherUser.id } as ICurrentUser; - userRepo.findById.mockResolvedValueOnce(mockStudentUser); - accountService.findByUserIdOrFail.mockResolvedValueOnce(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); + let params = { type: AccountSearchType.USER_ID, value: mockTeacherUser.id } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); - return { mockStudentAccount }; - }; - it('should throw ForbiddenOperationError', async () => { - const { mockStudentAccount } = setup(); - await expect( - accountUc.replaceMyTemporaryPassword( - mockStudentAccount.userId?.toString() ?? '', - defaultPassword, - defaultPassword - ) - ).rejects.toThrow(ForbiddenOperationError); + params = { type: AccountSearchType.USER_ID, value: mockStudentUser.id } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); + }); + 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(); + }); + 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 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 }, - }); + 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); + }); + it('student can not access any other account via user id', async () => { + const currentUser = { userId: mockStudentUser.id } as ICurrentUser; - const mockStudentAccount = accountFactory.buildWithId({ - userId: mockStudentUser.id, - password: defaultPasswordHash, - }); + let params = { type: AccountSearchType.USER_ID, value: mockAdminUser.id } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); - userRepo.findById.mockResolvedValueOnce(mockStudentUser); - accountService.findByUserIdOrFail.mockResolvedValueOnce(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); - accountService.validatePassword.mockResolvedValueOnce(true); + params = { type: AccountSearchType.USER_ID, value: mockTeacherUser.id } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); - return { mockStudentAccount }; - }; - it('should throw ForbiddenOperationError', async () => { - const { mockStudentAccount } = setup(); - await expect( - accountUc.replaceMyTemporaryPassword( - mockStudentAccount.userId?.toString() ?? '', - defaultPassword, - defaultPassword - ) - ).rejects.toThrow(ForbiddenOperationError); + params = { type: AccountSearchType.USER_ID, value: mockStudentUser.id } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); }); - }); + it('superhero can access any account via username', async () => { + const currentUser = { userId: mockSuperheroUser.id } as ICurrentUser; - describe('When old password is undefined', () => { - const setup = () => { - const mockSchool = schoolFactory.buildWithId(); + let params = { type: AccountSearchType.USERNAME, value: mockAdminAccount.username } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).resolves.not.toThrow(); - const mockStudentUser = userFactory.buildWithId({ - school: mockSchool, - roles: [new Role({ name: RoleName.STUDENT, permissions: [] })], - forcePasswordChange: false, - preferences: { firstLogin: false }, - }); + params = { type: AccountSearchType.USERNAME, value: mockTeacherAccount.username } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).resolves.not.toThrow(); - const mockStudentAccount = accountFactory.buildWithId({ - userId: mockStudentUser.id, - password: undefined, - }); + params = { type: AccountSearchType.USERNAME, value: mockStudentAccount.username } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).resolves.not.toThrow(); - userRepo.findById.mockResolvedValueOnce(mockStudentUser); - accountService.findByUserIdOrFail.mockResolvedValueOnce(AccountEntityToDtoMapper.mapToDto(mockStudentAccount)); - accountService.validatePassword.mockResolvedValueOnce(true); + params = { + type: AccountSearchType.USERNAME, + value: mockDifferentSchoolAdminAccount.username, + } as AccountSearchQueryParams; + await expect(accountUc.searchAccounts(currentUser, params)).resolves.not.toThrow(); - return { mockStudentAccount }; - }; - it('should throw Error', async () => { - const { mockStudentAccount } = setup(); - await expect( - accountUc.replaceMyTemporaryPassword( - mockStudentAccount.userId?.toString() ?? '', - defaultPassword, - defaultPassword - ) - ).rejects.toThrow(Error); + 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('When the admin manipulate 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({ + 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, 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 a user logs in for the first time', () => { - 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(); - }); + activated: mockStudentAccount.activated, + }) + ); }); - - describe('when a user logs in for the first time (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(); - }); + 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); }); - - 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); - }); + 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); }); - - 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); - }); + 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); }); }); - 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('saveAccount', () => { + afterEach(() => { + jest.clearAllMocks(); }); - 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 call account service', async () => { + const spy = jest.spyOn(accountService, 'saveWithValidation'); + const params: AccountSaveDto = { + username: 'john.doe@domain.tld', + password: defaultPassword, }; - 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); - }); + await accountUc.saveAccount(params); + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + username: 'john.doe@domain.tld', + }) + ); }); - 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('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); }); - - 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); - }); + 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); }); - - 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 Invalid search type', async () => { - const { mockSuperheroUser } = setup(); - await expect( - accountUc.searchAccounts( - { userId: mockSuperheroUser.id } as ICurrentUser, - { type: '' as AccountSearchType } as AccountSearchQueryParams - ) - ).rejects.toThrow('Invalid search type.'); - }); + 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(); }); - - describe('When user is not 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); - }); + 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()); }); - 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(); - }); - }); + 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(); }); - }); - - 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, - }) - ); - }); + 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); }); - - 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); - }); + 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('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); - }); + 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 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); - }); + 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('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('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('updateAccountById', () => { - describe('when updating a user that 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; + it('teacher can edit student', async () => { + 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)).rejects.toThrow(EntityNotFoundError); + await expect(accountUc.updateAccountById(currentUser, params, body)).resolves.not.toThrow(); }); - }); - - 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(); + it('admin can edit student', async () => { const currentUser = { userId: mockAdminUser.id } as ICurrentUser; - const params = { id: '000000000000000' } as AccountByIdParams; + const params = { id: mockStudentAccount.id } as AccountByIdParams; const body = {} as AccountByIdBodyParams; - await expect(accountUc.updateAccountById(currentUser, params, body)).rejects.toThrow(EntityNotFoundError); + await expect(accountUc.updateAccountById(currentUser, params, body)).resolves.not.toThrow(); }); - }); - - 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('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)); - }); - 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("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)); - }); - - return { mockStudentAccount, mockSuperheroUser }; - }; - it('should update target account activation state', async () => { - const { mockStudentAccount, mockSuperheroUser } = setup(); + it('superhero can edit admin', 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(); + 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 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('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 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(); + it('user without role cannot be edited', 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('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); - }); - }); - - 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); - }); - }); - - 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 error 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 error 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 error 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 error 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); - }); + const params = { id: mockUnknownRoleUserAccount.id } as AccountByIdParams; + const body = {} as AccountByIdBodyParams; + await expect(accountUc.updateAccountById(currentUser, params, body)).rejects.toThrow(ForbiddenOperationError); }); }); }); 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(); - }); + 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('When the current user is not 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 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 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 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('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); - }); + let updateMock: jest.Mock; + beforeAll(() => { + configService.get.mockReturnValue(LOGIN_BLOCK_TIME); }); - - 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 - 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); - }); + afterAll(() => { + configService.get.mockRestore(); }); - - 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 error', 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(); - }); + beforeEach(() => { + // eslint-disable-next-line jest/unbound-method + updateMock = accountService.updateLastTriedFailedLogin as jest.Mock; + updateMock.mockClear(); + }); + 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); + }); + 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); + }); + it('should not throw, if lasttriedFailedLogin is undefined', async () => { + await expect( + accountUc.checkBrutForce( + mockAccountWithNoLastFailedLogin.username, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + mockAccountWithNoLastFailedLogin.systemId! + ) + ).resolves.not.toThrow(); }); }); }); diff --git a/apps/server/src/modules/account/uc/account.uc.ts b/apps/server/src/modules/account/uc/account.uc.ts index f9e21b28c63..ac4ac053dae 100644 --- a/apps/server/src/modules/account/uc/account.uc.ts +++ b/apps/server/src/modules/account/uc/account.uc.ts @@ -8,7 +8,6 @@ import { } from '@shared/common/error'; import { Account, EntityId, Permission, PermissionService, Role, RoleName, SchoolEntity, User } from '@shared/domain'; import { UserRepo } from '@shared/repo'; -// TODO: module internals should be imported with relative paths import { AccountService } from '@modules/account/services/account.service'; import { AccountDto } from '@modules/account/services/dto/account.dto'; @@ -43,14 +42,6 @@ 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. * @@ -64,9 +55,7 @@ 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.'); } @@ -83,10 +72,8 @@ 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); } @@ -106,7 +93,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); // TODO: mapping should be done in controller + return AccountResponseMapper.mapToResponse(account); } async saveAccount(dto: AccountSaveDto): Promise { @@ -175,8 +162,6 @@ 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); } @@ -315,7 +300,6 @@ 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/testing/factory/account.factory.ts b/apps/server/src/shared/testing/factory/account.factory.ts index a3568dbf80a..b0c0b8434c1 100644 --- a/apps/server/src/shared/testing/factory/account.factory.ts +++ b/apps/server/src/shared/testing/factory/account.factory.ts @@ -5,8 +5,6 @@ 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 }; @@ -23,36 +21,10 @@ 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: defaultTestPassword, - 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'; +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 {