From 3d949c85beb1df7b3a305dff9405fab0cdc16e09 Mon Sep 17 00:00:00 2001 From: MajedAlaitwniCap Date: Thu, 25 Jul 2024 13:38:34 +0200 Subject: [PATCH] add test factory for ICurrentUser --- .../modules/account/api/account.uc.spec.ts | 128 ++++++++++-------- .../mapper/current-user.mapper.spec.ts | 90 ++++-------- .../services/authentication.service.spec.ts | 21 ++- .../strategy/jwt.strategy.spec.ts | 47 ++++--- .../strategy/oauth2.strategy.spec.ts | 8 +- .../strategy/ws-jwt.strategy.spec.ts | 48 +++---- .../testing/icurrentuser.factory.ts | 71 ++++++++++ .../modules/authentication/testing/index.ts | 2 + .../testing/jwtpayload.factory.ts | 86 ++++++++++++ .../controller/oauth-sso.controller.spec.ts | 4 +- 10 files changed, 316 insertions(+), 189 deletions(-) create mode 100644 apps/server/src/modules/authentication/testing/icurrentuser.factory.ts create mode 100644 apps/server/src/modules/authentication/testing/index.ts create mode 100644 apps/server/src/modules/authentication/testing/jwtpayload.factory.ts diff --git a/apps/server/src/modules/account/api/account.uc.spec.ts b/apps/server/src/modules/account/api/account.uc.spec.ts index d49b30e786b..21898b4edbb 100644 --- a/apps/server/src/modules/account/api/account.uc.spec.ts +++ b/apps/server/src/modules/account/api/account.uc.spec.ts @@ -1,5 +1,4 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ICurrentUser } from '@modules/authentication'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { EntityNotFoundError } from '@shared/common'; @@ -11,14 +10,15 @@ import { Role, User } from '@shared/domain/entity'; import { Permission, RoleName } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { schoolEntityFactory, setupEntities, userFactory } from '@shared/testing'; +import { iCurrentUserFactory } from '@src/modules/authentication/testing'; import { Account, AccountSave } from '../domain'; import { AccountEntity } from '../domain/entity/account.entity'; import { AccountService } from '../domain/services'; import { AccountEntityToDoMapper } from '../repo/micro-orm/mapper'; +import { accountFactory } from '../testing'; import { AccountUc } from './account.uc'; import { AccountSearchDto, AccountSearchType, UpdateAccountDto } from './dto'; import { ResolvedAccountDto, ResolvedSearchListAccountDto } from './dto/resolved-account.dto'; -import { accountFactory } from '../testing'; describe('AccountUc', () => { let module: TestingModule; @@ -291,7 +291,7 @@ describe('AccountUc', () => { it('should return one account', async () => { const { mockSuperheroUser, mockStudentUser, mockStudentAccount } = setup(); const accounts = await accountUc.searchAccounts( - { userId: mockSuperheroUser.id } as ICurrentUser, + iCurrentUserFactory.buildWithId({ userId: mockSuperheroUser.id }), { type: AccountSearchType.USER_ID, value: mockStudentUser.id } as AccountSearchDto ); const expected = new ResolvedSearchListAccountDto( @@ -349,7 +349,7 @@ describe('AccountUc', () => { it('should return empty list', async () => { const { mockSuperheroUser, mockUserWithoutAccount } = setup(); const accounts = await accountUc.searchAccounts( - { userId: mockSuperheroUser.id } as ICurrentUser, + iCurrentUserFactory.buildWithId({ userId: mockSuperheroUser.id }), { type: AccountSearchType.USER_ID, value: mockUserWithoutAccount.id } as AccountSearchDto ); const expected = new ResolvedSearchListAccountDto([], 0, 0, 0); @@ -392,7 +392,7 @@ describe('AccountUc', () => { it('should return one or more accounts, ', async () => { const { mockSuperheroUser } = setup(); const accounts = await accountUc.searchAccounts( - { userId: mockSuperheroUser.id } as ICurrentUser, + iCurrentUserFactory.buildWithId({ userId: mockSuperheroUser.id }), { type: AccountSearchType.USERNAME, value: '' } as AccountSearchDto ); expect(accounts.skip).toEqual(0); @@ -458,24 +458,24 @@ describe('AccountUc', () => { it('should throw UnauthorizedException', async () => { const { mockTeacherUser, mockAdminUser, mockStudentUser, mockOtherStudentUser } = setup(); await expect( - accountUc.searchAccounts( - { userId: mockTeacherUser.id } as ICurrentUser, - { type: AccountSearchType.USER_ID, value: mockAdminUser.id } as AccountSearchDto - ) + accountUc.searchAccounts(iCurrentUserFactory.buildWithId({ userId: mockTeacherUser.id }), { + type: AccountSearchType.USER_ID, + value: mockAdminUser.id, + } as AccountSearchDto) ).rejects.toThrow(UnauthorizedException); await expect( - accountUc.searchAccounts( - { userId: mockStudentUser.id } as ICurrentUser, - { type: AccountSearchType.USER_ID, value: mockOtherStudentUser.id } as AccountSearchDto - ) + accountUc.searchAccounts(iCurrentUserFactory.buildWithId({ userId: mockStudentUser.id }), { + type: AccountSearchType.USER_ID, + value: mockOtherStudentUser.id, + } as AccountSearchDto) ).rejects.toThrow(UnauthorizedException); await expect( - accountUc.searchAccounts( - { userId: mockStudentUser.id } as ICurrentUser, - { type: AccountSearchType.USER_ID, value: mockTeacherUser.id } as AccountSearchDto - ) + accountUc.searchAccounts(iCurrentUserFactory.buildWithId({ userId: mockStudentUser.id }), { + type: AccountSearchType.USER_ID, + value: mockTeacherUser.id, + } as AccountSearchDto) ).rejects.toThrow(UnauthorizedException); }); }); @@ -501,10 +501,9 @@ describe('AccountUc', () => { it('should throw Invalid search type', async () => { const { mockSuperheroUser } = setup(); await expect( - accountUc.searchAccounts( - { userId: mockSuperheroUser.id } as ICurrentUser, - { type: '' as AccountSearchType } as AccountSearchDto - ) + accountUc.searchAccounts(iCurrentUserFactory.buildWithId({ userId: mockSuperheroUser.id }), { + type: '' as AccountSearchType, + } as AccountSearchDto) ).rejects.toThrow('Invalid search type.'); }); }); @@ -539,10 +538,10 @@ describe('AccountUc', () => { it('should throw UnauthorizedException', async () => { const { mockTeacherUser, mockStudentUser } = setup(); await expect( - accountUc.searchAccounts( - { userId: mockTeacherUser.id } as ICurrentUser, - { type: AccountSearchType.USERNAME, value: mockStudentUser.id } as AccountSearchDto - ) + accountUc.searchAccounts(iCurrentUserFactory.buildWithId({ userId: mockTeacherUser.id }), { + type: AccountSearchType.USERNAME, + value: mockStudentUser.id, + } as AccountSearchDto) ).rejects.toThrow(UnauthorizedException); }); }); @@ -589,7 +588,7 @@ describe('AccountUc', () => { }; 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 currentUser = iCurrentUserFactory.buildWithId({ userId: mockAdminUser.id }); const params = { type: AccountSearchType.USER_ID, value: mockTeacherUser.id } as AccountSearchDto; await expect(accountUc.searchAccounts(currentUser, params)).resolves.not.toThrow(); }); @@ -632,7 +631,7 @@ describe('AccountUc', () => { }; 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 currentUser = iCurrentUserFactory.buildWithId({ userId: mockAdminUser.id }); const params = { type: AccountSearchType.USER_ID, value: mockStudentUser.id } as AccountSearchDto; await expect(accountUc.searchAccounts(currentUser, params)).resolves.not.toThrow(); }); @@ -671,7 +670,7 @@ describe('AccountUc', () => { 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 currentUser = iCurrentUserFactory.buildWithId({ userId: mockAdminUser.id }); const params = { type: AccountSearchType.USER_ID, value: mockAdminUser.id } as AccountSearchDto; await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); }); @@ -730,7 +729,7 @@ describe('AccountUc', () => { }; 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; + const currentUser = iCurrentUserFactory.buildWithId({ userId: mockDifferentSchoolAdminUser.id }); let params = { type: AccountSearchType.USER_ID, value: mockTeacherUser.id } as AccountSearchDto; await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); @@ -773,7 +772,7 @@ describe('AccountUc', () => { }; 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 currentUser = iCurrentUserFactory.buildWithId({ userId: mockTeacherUser.id }); const params = { type: AccountSearchType.USER_ID, value: mockOtherTeacherUser.id, @@ -810,7 +809,7 @@ describe('AccountUc', () => { }; 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 currentUser = iCurrentUserFactory.buildWithId({ userId: mockTeacherUser.id }); const params = { type: AccountSearchType.USER_ID, value: mockStudentUser.id } as AccountSearchDto; await expect(accountUc.searchAccounts(currentUser, params)).resolves.not.toThrow(); }); @@ -857,7 +856,7 @@ describe('AccountUc', () => { }; 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 currentUser = iCurrentUserFactory.buildWithId({ userId: mockTeacherUser.id }); const params = { type: AccountSearchType.USER_ID, value: mockAdminUser.id } as AccountSearchDto; await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); }); @@ -898,7 +897,7 @@ describe('AccountUc', () => { }; 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; + const currentUser = iCurrentUserFactory.buildWithId({ userId: mockDifferentSchoolTeacherUser.id }); let params = { type: AccountSearchType.USER_ID, value: mockTeacherUser.id } as AccountSearchDto; await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); @@ -941,7 +940,7 @@ describe('AccountUc', () => { 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 currentUser = iCurrentUserFactory.buildWithId({ userId: mockTeacherNoUserPermissionUser.id }); const params = { type: AccountSearchType.USER_ID, value: mockStudentSchoolPermissionUser.id, @@ -978,7 +977,7 @@ describe('AccountUc', () => { 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 currentUser = iCurrentUserFactory.buildWithId({ userId: mockTeacherNoUserNoSchoolPermissionUser.id }); const params = { type: AccountSearchType.USER_ID, value: mockStudentUser.id } as AccountSearchDto; await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(UnauthorizedException); }); @@ -1013,7 +1012,7 @@ describe('AccountUc', () => { 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 currentUser = iCurrentUserFactory.buildWithId({ userId: mockStudentSchoolPermissionUser.id }); const params = { type: AccountSearchType.USER_ID, value: mockOtherStudentSchoolPermissionUser.id, @@ -1073,7 +1072,7 @@ describe('AccountUc', () => { }; 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; + const currentUser = iCurrentUserFactory.buildWithId({ userId: mockStudentUser.id }); let params = { type: AccountSearchType.USER_ID, value: mockAdminUser.id } as AccountSearchDto; await expect(accountUc.searchAccounts(currentUser, params)).rejects.toThrow(); @@ -1195,7 +1194,7 @@ describe('AccountUc', () => { mockDifferentSchoolStudentAccount, } = setup(); - const currentUser = { userId: mockSuperheroUser.id } as ICurrentUser; + const currentUser = iCurrentUserFactory.buildWithId({ userId: mockSuperheroUser.id }); let params = { type: AccountSearchType.USERNAME, @@ -1262,7 +1261,7 @@ describe('AccountUc', () => { it('should return an account', async () => { const { mockSuperheroUser, mockStudentUser, mockStudentAccount } = setup(); const account = await accountUc.findAccountById( - { userId: mockSuperheroUser.id } as ICurrentUser, + iCurrentUserFactory.buildWithId({ userId: mockSuperheroUser.id }), mockStudentAccount.id ); expect(account).toStrictEqual( @@ -1308,7 +1307,10 @@ describe('AccountUc', () => { it('should throw UnauthorizedException', async () => { const { mockTeacherUser, mockStudentAccount } = setup(); await expect( - accountUc.findAccountById({ userId: mockTeacherUser.id } as ICurrentUser, mockStudentAccount.id) + accountUc.findAccountById( + iCurrentUserFactory.buildWithId({ userId: mockTeacherUser.id }), + mockStudentAccount.id + ) ).rejects.toThrow(UnauthorizedException); }); }); @@ -1337,7 +1339,7 @@ describe('AccountUc', () => { it('should throw EntityNotFoundError', async () => { const { mockSuperheroUser } = setup(); await expect( - accountUc.findAccountById({ userId: mockSuperheroUser.id } as ICurrentUser, 'xxx') + accountUc.findAccountById(iCurrentUserFactory.buildWithId({ userId: mockSuperheroUser.id }), 'xxx') ).rejects.toThrow(EntityNotFoundError); }); }); @@ -1366,7 +1368,7 @@ describe('AccountUc', () => { it('should throw EntityNotFoundError', async () => { const { mockSuperheroUser } = setup(); await expect( - accountUc.findAccountById({ userId: mockSuperheroUser.id } as ICurrentUser, 'xxx') + accountUc.findAccountById(iCurrentUserFactory.buildWithId({ userId: mockSuperheroUser.id }), 'xxx') ).rejects.toThrow(EntityNotFoundError); }); }); @@ -1419,7 +1421,7 @@ describe('AccountUc', () => { }; it('should throw EntityNotFoundError', async () => { const { mockStudentAccount } = setup(); - const currentUser = { userId: '000000000000000' } as ICurrentUser; + const currentUser = iCurrentUserFactory.buildWithId({ userId: '00000000000' }); const body = {} as UpdateAccountDto; await expect(accountUc.updateAccountById(currentUser, mockStudentAccount.id, body)).rejects.toThrow( EntityNotFoundError @@ -1463,7 +1465,7 @@ describe('AccountUc', () => { }; it('should throw EntityNotFoundError', async () => { const { mockAdminUser } = setup(); - const currentUser = { userId: mockAdminUser.id } as ICurrentUser; + const currentUser = iCurrentUserFactory.buildWithId({ userId: mockAdminUser.id }); const body = {} as UpdateAccountDto; await expect(accountUc.updateAccountById(currentUser, '000000000000000', body)).rejects.toThrow( EntityNotFoundError @@ -1499,9 +1501,13 @@ describe('AccountUc', () => { it('should throw EntityNotFoundError', async () => { const { mockSuperheroUser, mockAccountWithoutUser } = setup(); await expect( - accountUc.updateAccountById({ userId: mockSuperheroUser.id } as ICurrentUser, mockAccountWithoutUser.id, { - username: 'user-fail@to.update', - } as UpdateAccountDto) + accountUc.updateAccountById( + iCurrentUserFactory.buildWithId({ userId: mockSuperheroUser.id }), + mockAccountWithoutUser.id, + { + username: 'user-fail@to.update', + } as UpdateAccountDto + ) ).rejects.toThrow(EntityNotFoundError); }); }); @@ -1554,7 +1560,7 @@ describe('AccountUc', () => { }; it('should not throw error when editing a teacher', async () => { const { mockAdminUser, mockTeacherAccount } = setup(); - const currentUser = { userId: mockAdminUser.id } as ICurrentUser; + const currentUser = iCurrentUserFactory.buildWithId({ userId: mockAdminUser.id }); const body = {} as UpdateAccountDto; await expect(accountUc.updateAccountById(currentUser, mockTeacherAccount.id, body)).resolves.not.toThrow(); }); @@ -1593,7 +1599,7 @@ describe('AccountUc', () => { }; it('should not throw error when editing a student', async () => { const { mockTeacherUser, mockStudentAccount } = setup(); - const currentUser = { userId: mockTeacherUser.id } as ICurrentUser; + const currentUser = iCurrentUserFactory.buildWithId({ userId: mockTeacherUser.id }); const body = {} as UpdateAccountDto; await expect(accountUc.updateAccountById(currentUser, mockStudentAccount.id, body)).resolves.not.toThrow(); }); @@ -1640,7 +1646,7 @@ describe('AccountUc', () => { }; it('should not throw error when editing a student', async () => { const { mockAdminUser, mockStudentAccount } = setup(); - const currentUser = { userId: mockAdminUser.id } as ICurrentUser; + const currentUser = iCurrentUserFactory.buildWithId({ userId: mockAdminUser.id }); const body = {} as UpdateAccountDto; await expect(accountUc.updateAccountById(currentUser, mockStudentAccount.id, body)).resolves.not.toThrow(); }); @@ -1682,7 +1688,7 @@ describe('AccountUc', () => { }; it('should throw UnauthorizedException', async () => { const { mockTeacherUser, mockOtherTeacherAccount } = setup(); - const currentUser = { userId: mockTeacherUser.id } as ICurrentUser; + const currentUser = iCurrentUserFactory.buildWithId({ userId: mockTeacherUser.id }); const body = {} as UpdateAccountDto; await expect(accountUc.updateAccountById(currentUser, mockOtherTeacherAccount.id, body)).rejects.toThrow( UnauthorizedException @@ -1741,7 +1747,7 @@ describe('AccountUc', () => { }; it('should throw UnauthorizedException', async () => { const { mockDifferentSchoolAdminUser, mockTeacherAccount } = setup(); - const currentUser = { userId: mockDifferentSchoolAdminUser.id } as ICurrentUser; + const currentUser = iCurrentUserFactory.buildWithId({ userId: mockDifferentSchoolAdminUser.id }); const body = {} as UpdateAccountDto; await expect(accountUc.updateAccountById(currentUser, mockTeacherAccount.id, body)).rejects.toThrow( UnauthorizedException @@ -1796,7 +1802,7 @@ describe('AccountUc', () => { }; it('should not throw error when editing a admin', async () => { const { mockSuperheroUser, mockAdminAccount } = setup(); - const currentUser = { userId: mockSuperheroUser.id } as ICurrentUser; + const currentUser = iCurrentUserFactory.buildWithId({ userId: mockSuperheroUser.id }); const body = {} as UpdateAccountDto; await expect(accountUc.updateAccountById(currentUser, mockAdminAccount.id, body)).resolves.not.toThrow(); }); @@ -1828,7 +1834,7 @@ describe('AccountUc', () => { }; it('should fail by default', async () => { const { mockUnknownRoleUser, mockAccountWithoutRole } = setup(); - const currentUser = { userId: mockUnknownRoleUser.id } as ICurrentUser; + const currentUser = iCurrentUserFactory.buildWithId({ userId: mockUnknownRoleUser.id }); const body = {} as UpdateAccountDto; await expect(accountUc.updateAccountById(currentUser, mockAccountWithoutRole.id, body)).rejects.toThrow( UnauthorizedException @@ -1876,7 +1882,7 @@ describe('AccountUc', () => { }; it('should throw UnauthorizedException', async () => { const { mockAdminUser, mockUnknownRoleUserAccount } = setup(); - const currentUser = { userId: mockAdminUser.id } as ICurrentUser; + const currentUser = iCurrentUserFactory.buildWithId({ userId: mockAdminUser.id }); const body = {} as UpdateAccountDto; await expect(accountUc.updateAccountById(currentUser, mockUnknownRoleUserAccount.id, body)).rejects.toThrow( UnauthorizedException @@ -1918,7 +1924,10 @@ describe('AccountUc', () => { it('should delete an account', async () => { const { mockSuperheroUser, mockStudentAccount } = setup(); await expect( - accountUc.deleteAccountById({ userId: mockSuperheroUser.id } as ICurrentUser, mockStudentAccount.id) + accountUc.deleteAccountById( + iCurrentUserFactory.buildWithId({ userId: mockSuperheroUser.id }), + mockStudentAccount.id + ) ).resolves.not.toThrow(); }); }); @@ -1971,7 +1980,10 @@ describe('AccountUc', () => { it('should throw UnauthorizedException', async () => { const { mockAdminUser, mockStudentAccount } = setup(); await expect( - accountUc.deleteAccountById({ userId: mockAdminUser.id } as ICurrentUser, mockStudentAccount.id) + accountUc.deleteAccountById( + iCurrentUserFactory.buildWithId({ userId: mockAdminUser.id }), + mockStudentAccount.id + ) ).rejects.toThrow(UnauthorizedException); }); }); @@ -2009,7 +2021,7 @@ describe('AccountUc', () => { it('should throw, if no account matches the search term', async () => { const { mockSuperheroUser } = setup(); await expect( - accountUc.deleteAccountById({ userId: mockSuperheroUser.id } as ICurrentUser, 'xxx') + accountUc.deleteAccountById(iCurrentUserFactory.buildWithId({ userId: mockSuperheroUser.id }), 'xxx') ).rejects.toThrow(EntityNotFoundError); }); }); diff --git a/apps/server/src/modules/authentication/mapper/current-user.mapper.spec.ts b/apps/server/src/modules/authentication/mapper/current-user.mapper.spec.ts index c852b725fd3..f3a9945e8b8 100644 --- a/apps/server/src/modules/authentication/mapper/current-user.mapper.spec.ts +++ b/apps/server/src/modules/authentication/mapper/current-user.mapper.spec.ts @@ -3,7 +3,8 @@ import { UserDO } from '@shared/domain/domainobject/user.do'; import { Permission, RoleName } from '@shared/domain/interface'; import { roleFactory, schoolEntityFactory, setupEntities, userDoFactory, userFactory } from '@shared/testing'; import { ICurrentUser, OauthCurrentUser } from '../interface'; -import { CreateJwtPayload, JwtPayload } from '../interface/jwt-payload'; +import { CreateJwtPayload } from '../interface/jwt-payload'; +import { iCurrentUserFactory, jwtPayloadFactory } from '../testing'; import { CurrentUserMapper } from './current-user.mapper'; describe('CurrentUserMapper', () => { @@ -221,95 +222,69 @@ describe('CurrentUserMapper', () => { describe('jwtToICurrentUser', () => { describe('when JWT is provided with all claims', () => { const setup = () => { - const jwtPayload: JwtPayload = { - accountId: 'dummyAccountId', - systemId: 'dummySystemId', - roles: ['mockRoleId'], - schoolId: 'dummySchoolId', - userId: 'dummyUserId', - support: true, - isExternalUser: true, - sub: 'dummyAccountId', - jti: 'random string', - aud: 'some audience', - iss: 'feathers', - iat: Math.floor(new Date().getTime() / 1000), - exp: Math.floor(new Date().getTime() / 1000) + 3600, - }; + const mockJwtPayload = jwtPayloadFactory.build(); return { - jwtPayload, + mockJwtPayload, }; }; it('should return current user', () => { - const { jwtPayload } = setup(); + const { mockJwtPayload } = setup(); - const currentUser = CurrentUserMapper.jwtToICurrentUser(jwtPayload); + const currentUser = CurrentUserMapper.jwtToICurrentUser(mockJwtPayload); expect(currentUser).toMatchObject({ - accountId: jwtPayload.accountId, - systemId: jwtPayload.systemId, - roles: [jwtPayload.roles[0]], - schoolId: jwtPayload.schoolId, - userId: jwtPayload.userId, - impersonated: jwtPayload.support, + accountId: mockJwtPayload.accountId, + systemId: mockJwtPayload.systemId, + roles: [mockJwtPayload.roles[0]], + schoolId: mockJwtPayload.schoolId, + userId: mockJwtPayload.userId, + impersonated: mockJwtPayload.support, }); }); it('should return current user with default for isExternalUser', () => { - const { jwtPayload } = setup(); + const { mockJwtPayload } = setup(); - const currentUser = CurrentUserMapper.jwtToICurrentUser(jwtPayload); + const currentUser = CurrentUserMapper.jwtToICurrentUser(mockJwtPayload); expect(currentUser).toMatchObject({ - isExternalUser: jwtPayload.isExternalUser, + isExternalUser: mockJwtPayload.isExternalUser, }); }); }); describe('when JWT is provided without optional claims', () => { const setup = () => { - const jwtPayload: JwtPayload = { - accountId: 'dummyAccountId', - roles: ['mockRoleId'], - schoolId: 'dummySchoolId', - userId: 'dummyUserId', - isExternalUser: false, - sub: 'dummyAccountId', - jti: 'random string', - aud: 'some audience', - iss: 'feathers', - iat: Math.floor(new Date().getTime() / 1000), - exp: Math.floor(new Date().getTime() / 1000) + 3600, - }; + const mockJwtPayload = jwtPayloadFactory.build(); return { - jwtPayload, + mockJwtPayload, }; }; it('should return current user', () => { - const { jwtPayload } = setup(); + const { mockJwtPayload } = setup(); - const currentUser = CurrentUserMapper.jwtToICurrentUser(jwtPayload); + const currentUser = CurrentUserMapper.jwtToICurrentUser(mockJwtPayload); expect(currentUser).toMatchObject({ - accountId: jwtPayload.accountId, - roles: [jwtPayload.roles[0]], - schoolId: jwtPayload.schoolId, - userId: jwtPayload.userId, - isExternalUser: false, + accountId: mockJwtPayload.accountId, + roles: [mockJwtPayload.roles[0]], + schoolId: mockJwtPayload.schoolId, + userId: mockJwtPayload.userId, + isExternalUser: true, }); }); it('should return current user with default for isExternalUser', () => { - const { jwtPayload } = setup(); + const { mockJwtPayload } = setup(); - const currentUser = CurrentUserMapper.jwtToICurrentUser(jwtPayload); + const currentUser = CurrentUserMapper.jwtToICurrentUser(mockJwtPayload); expect(currentUser).toMatchObject({ - isExternalUser: false, + isExternalUser: true, }); }); }); @@ -317,15 +292,7 @@ describe('CurrentUserMapper', () => { describe('mapCurrentUserToCreateJwtPayload', () => { it('should map current user to create jwt payload', () => { - const currentUser: ICurrentUser = { - accountId: 'dummyAccountId', - systemId: 'dummySystemId', - roles: ['mockRoleId'], - schoolId: 'dummySchoolId', - userId: 'dummyUserId', - impersonated: true, - isExternalUser: false, - }; + const currentUser = iCurrentUserFactory.build(); const createJwtPayload: CreateJwtPayload = CurrentUserMapper.mapCurrentUserToCreateJwtPayload(currentUser); @@ -335,7 +302,6 @@ describe('CurrentUserMapper', () => { roles: currentUser.roles, schoolId: currentUser.schoolId, userId: currentUser.userId, - support: currentUser.impersonated, isExternalUser: false, }); }); diff --git a/apps/server/src/modules/authentication/services/authentication.service.spec.ts b/apps/server/src/modules/authentication/services/authentication.service.spec.ts index 32d6850f243..6e50e4a7d4b 100644 --- a/apps/server/src/modules/authentication/services/authentication.service.spec.ts +++ b/apps/server/src/modules/authentication/services/authentication.service.spec.ts @@ -1,6 +1,5 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { AccountService, Account } from '@modules/account'; -import { ICurrentUser } from '@modules/authentication'; +import { Account, AccountService } from '@modules/account'; import { UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; @@ -8,8 +7,9 @@ import { Test, TestingModule } from '@nestjs/testing'; import jwt from 'jsonwebtoken'; import { BruteForceError } from '../errors/brute-force.error'; import { JwtValidationAdapter } from '../helper/jwt-validation.adapter'; -import { AuthenticationService } from './authentication.service'; import { UserAccountDeactivatedLoggableException } from '../loggable/user-account-deactivated-exception'; +import { iCurrentUserFactory } from '../testing'; +import { AuthenticationService } from './authentication.service'; jest.mock('jsonwebtoken'); @@ -114,18 +114,13 @@ describe('AuthenticationService', () => { describe('generateJwt', () => { describe('when generating new jwt', () => { it('should pass the correct parameters', async () => { - const mockCurrentUser: ICurrentUser = { - accountId: 'mockAccountId', - roles: ['student'], - schoolId: 'mockSchoolId', - userId: 'mockUserId', - isExternalUser: false, - }; - await authenticationService.generateJwt(mockCurrentUser); + const mockCurrentStudentUser = iCurrentUserFactory.buildStudentICurrentUser(); + + await authenticationService.generateJwt(mockCurrentStudentUser); expect(jwtService.sign).toBeCalledWith( - mockCurrentUser, + mockCurrentStudentUser, expect.objectContaining({ - subject: mockCurrentUser.accountId, + subject: mockCurrentStudentUser.accountId, }) ); }); diff --git a/apps/server/src/modules/authentication/strategy/jwt.strategy.spec.ts b/apps/server/src/modules/authentication/strategy/jwt.strategy.spec.ts index 7e581164daf..8a25c259c85 100644 --- a/apps/server/src/modules/authentication/strategy/jwt.strategy.spec.ts +++ b/apps/server/src/modules/authentication/strategy/jwt.strategy.spec.ts @@ -1,4 +1,3 @@ -import { ObjectId } from '@mikro-orm/mongodb'; import { JwtModule } from '@nestjs/jwt'; import { PassportModule } from '@nestjs/passport'; import { Test, TestingModule } from '@nestjs/testing'; @@ -7,24 +6,15 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { UnauthorizedException } from '@nestjs/common'; import { setupEntities } from '@shared/testing'; import { jwtConstants } from '../constants'; -import { JwtPayload } from '../interface/jwt-payload'; import { JwtValidationAdapter } from '../helper/jwt-validation.adapter'; +import { jwtPayloadFactory } from '../testing'; import { JwtStrategy } from './jwt.strategy'; describe('jwt strategy', () => { let validationAdapter: DeepMocked; let strategy: JwtStrategy; let module: TestingModule; - const jwtPayload: JwtPayload = { - accountId: new ObjectId().toHexString(), - userId: new ObjectId().toHexString(), - schoolId: new ObjectId().toHexString(), - roles: [new ObjectId().toHexString()], - jti: 'someRandomString', - systemId: new ObjectId().toHexString(), - support: true, - } as JwtPayload; beforeAll(async () => { await setupEntities(); @@ -55,39 +45,48 @@ describe('jwt strategy', () => { describe('when authenticate a user with jwt', () => { const setup = () => { + const mockJwtPayload = jwtPayloadFactory.build(); + validationAdapter.isWhitelisted.mockResolvedValueOnce(); validationAdapter.isWhitelisted.mockClear(); + return { + mockJwtPayload, + }; }; it('should check jwt for being whitelisted', async () => { - setup(); - await strategy.validate(jwtPayload); - expect(validationAdapter.isWhitelisted).toHaveBeenCalledWith(jwtPayload.accountId, jwtPayload.jti); + const { mockJwtPayload } = setup(); + await strategy.validate(mockJwtPayload); + expect(validationAdapter.isWhitelisted).toHaveBeenCalledWith(mockJwtPayload.accountId, mockJwtPayload.jti); }); it('should return user', async () => { - setup(); - const user = await strategy.validate(jwtPayload); + const { mockJwtPayload } = setup(); + const user = await strategy.validate(mockJwtPayload); expect(user).toMatchObject({ - userId: jwtPayload.userId, - roles: [jwtPayload.roles[0]], - schoolId: jwtPayload.schoolId, - accountId: jwtPayload.accountId, - systemId: jwtPayload.systemId, - impersonated: jwtPayload.support, + userId: mockJwtPayload.userId, + roles: [mockJwtPayload.roles[0]], + schoolId: mockJwtPayload.schoolId, + accountId: mockJwtPayload.accountId, + systemId: mockJwtPayload.systemId, + impersonated: mockJwtPayload.support, }); }); }); describe('when jwt is not whitelisted', () => { const setup = () => { + const mockJwtPayload = jwtPayloadFactory.build(); validationAdapter.isWhitelisted.mockRejectedValueOnce(null); validationAdapter.isWhitelisted.mockClear(); + return { + mockJwtPayload, + }; }; it('should throw an UnauthorizedException', async () => { - setup(); - await expect(() => strategy.validate(jwtPayload)).rejects.toThrow(UnauthorizedException); + const { mockJwtPayload } = setup(); + await expect(() => strategy.validate(mockJwtPayload)).rejects.toThrow(UnauthorizedException); }); }); }); diff --git a/apps/server/src/modules/authentication/strategy/oauth2.strategy.spec.ts b/apps/server/src/modules/authentication/strategy/oauth2.strategy.spec.ts index efd16d2773c..17817eebf7c 100644 --- a/apps/server/src/modules/authentication/strategy/oauth2.strategy.spec.ts +++ b/apps/server/src/modules/authentication/strategy/oauth2.strategy.spec.ts @@ -6,6 +6,7 @@ import { UserDO } from '@shared/domain/domainobject/user.do'; import { RoleName } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { userDoFactory } from '@shared/testing'; +import { accountDoFactory } from '@src/modules/account/testing'; import { ICurrentUser, OauthCurrentUser } from '../interface'; @@ -56,12 +57,7 @@ describe('Oauth2Strategy', () => { const setup = () => { const systemId: EntityId = 'systemId'; const user: UserDO = userDoFactory.withRoles([{ id: 'roleId', name: RoleName.USER }]).buildWithId(); - const account: Account = new Account({ - id: 'accountId', - createdAt: new Date(), - updatedAt: new Date(), - username: 'username', - }); + const account = accountDoFactory.build(); const idToken = 'idToken'; oauthService.authenticateUser.mockResolvedValue( diff --git a/apps/server/src/modules/authentication/strategy/ws-jwt.strategy.spec.ts b/apps/server/src/modules/authentication/strategy/ws-jwt.strategy.spec.ts index 2361f2b48b9..5d3e12674ad 100644 --- a/apps/server/src/modules/authentication/strategy/ws-jwt.strategy.spec.ts +++ b/apps/server/src/modules/authentication/strategy/ws-jwt.strategy.spec.ts @@ -1,4 +1,3 @@ -import { ObjectId } from '@mikro-orm/mongodb'; import { JwtModule } from '@nestjs/jwt'; import { PassportModule } from '@nestjs/passport'; import { Test, TestingModule } from '@nestjs/testing'; @@ -6,23 +5,14 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { WsException } from '@nestjs/websockets'; import { setupEntities } from '@shared/testing'; import { jwtConstants } from '../constants'; -import { JwtPayload } from '../interface/jwt-payload'; import { JwtValidationAdapter } from '../helper/jwt-validation.adapter'; import { WsJwtStrategy } from './ws-jwt.strategy'; +import { jwtPayloadFactory } from '../testing'; describe('jwt strategy', () => { let validationAdapter: DeepMocked; let strategy: WsJwtStrategy; let module: TestingModule; - const jwtPayload: JwtPayload = { - accountId: new ObjectId().toHexString(), - userId: new ObjectId().toHexString(), - schoolId: new ObjectId().toHexString(), - roles: [new ObjectId().toHexString()], - jti: 'someRandomString', - systemId: new ObjectId().toHexString(), - support: true, - } as JwtPayload; beforeAll(async () => { await setupEntities(); @@ -53,39 +43,49 @@ describe('jwt strategy', () => { describe('when authenticate a user with jwt', () => { const setup = () => { + const mockJwtPayload = jwtPayloadFactory.build(); + validationAdapter.isWhitelisted.mockResolvedValueOnce(); validationAdapter.isWhitelisted.mockClear(); + return { + mockJwtPayload, + }; }; it('should check jwt for being whitelisted', async () => { - setup(); - await strategy.validate(jwtPayload); - expect(validationAdapter.isWhitelisted).toHaveBeenCalledWith(jwtPayload.accountId, jwtPayload.jti); + const { mockJwtPayload } = setup(); + await strategy.validate(mockJwtPayload); + expect(validationAdapter.isWhitelisted).toHaveBeenCalledWith(mockJwtPayload.accountId, mockJwtPayload.jti); }); it('should return user', async () => { - setup(); - const user = await strategy.validate(jwtPayload); + const { mockJwtPayload } = setup(); + const user = await strategy.validate(mockJwtPayload); expect(user).toMatchObject({ - userId: jwtPayload.userId, - roles: [jwtPayload.roles[0]], - schoolId: jwtPayload.schoolId, - accountId: jwtPayload.accountId, - systemId: jwtPayload.systemId, - impersonated: jwtPayload.support, + userId: mockJwtPayload.userId, + roles: [mockJwtPayload.roles[0]], + schoolId: mockJwtPayload.schoolId, + accountId: mockJwtPayload.accountId, + systemId: mockJwtPayload.systemId, + impersonated: mockJwtPayload.support, }); }); }); describe('when jwt is not whitelisted', () => { const setup = () => { + const mockJwtPayload = jwtPayloadFactory.build(); + validationAdapter.isWhitelisted.mockRejectedValueOnce(null); validationAdapter.isWhitelisted.mockClear(); + return { + mockJwtPayload, + }; }; it('should throw an UnauthorizedException', async () => { - setup(); - await expect(() => strategy.validate(jwtPayload)).rejects.toThrow(WsException); + const { mockJwtPayload } = setup(); + await expect(() => strategy.validate(mockJwtPayload)).rejects.toThrow(WsException); }); }); }); diff --git a/apps/server/src/modules/authentication/testing/icurrentuser.factory.ts b/apps/server/src/modules/authentication/testing/icurrentuser.factory.ts new file mode 100644 index 00000000000..c4fc5263ea7 --- /dev/null +++ b/apps/server/src/modules/authentication/testing/icurrentuser.factory.ts @@ -0,0 +1,71 @@ +import { BaseFactory } from '@shared/testing'; +import { ICurrentUser } from '../interface'; + +class CurrentUser implements ICurrentUser { + userId: string; + + roles: string[]; + + schoolId: string; + + accountId: string; + + systemId: string; + + isExternalUser: boolean; + + constructor(data: ICurrentUser) { + this.userId = data.userId; + this.roles = data.roles; + this.schoolId = data.schoolId; + this.accountId = data.accountId; + this.systemId = data.systemId || ''; + this.isExternalUser = data.isExternalUser; + } +} + +export class ICurrentUserFactory extends BaseFactory { + public buildAdminICurrentUser(): ICurrentUser { + return { + userId: 'mockUserId', + roles: ['admin'], + schoolId: 'mockSchoolId', + accountId: 'mockAccountId', + systemId: 'mockSystemId', + isExternalUser: false, + }; + } + + public buildStudentICurrentUser(): ICurrentUser { + return { + userId: 'mockUserId', + roles: ['student'], + schoolId: 'mockSchoolId', + accountId: 'mockAccountId', + systemId: 'mockSystemId', + isExternalUser: false, + }; + } + + public buildTeacherICurrentUser(): ICurrentUser { + return { + userId: 'mockUserId', + roles: ['teacher'], + schoolId: 'mockSchoolId', + accountId: 'mockAccountId', + systemId: 'mockSystemId', + isExternalUser: false, + }; + } +} + +export const iCurrentUserFactory = ICurrentUserFactory.define(CurrentUser, ({ sequence }) => { + return { + userId: `mockUserId ${sequence}`, + roles: [], + schoolId: `mockSchoolId ${sequence}`, + accountId: `mockAccountId ${sequence}`, + systemId: `mockSystemId ${sequence}`, + isExternalUser: false, + }; +}); diff --git a/apps/server/src/modules/authentication/testing/index.ts b/apps/server/src/modules/authentication/testing/index.ts new file mode 100644 index 00000000000..c2fda86d139 --- /dev/null +++ b/apps/server/src/modules/authentication/testing/index.ts @@ -0,0 +1,2 @@ +export * from './icurrentuser.factory'; +export * from './jwtpayload.factory'; diff --git a/apps/server/src/modules/authentication/testing/jwtpayload.factory.ts b/apps/server/src/modules/authentication/testing/jwtpayload.factory.ts new file mode 100644 index 00000000000..bb74cdfc082 --- /dev/null +++ b/apps/server/src/modules/authentication/testing/jwtpayload.factory.ts @@ -0,0 +1,86 @@ +import { BaseFactory } from '@shared/testing'; +import { JwtPayload } from '../interface/jwt-payload'; + +class JWTPayload implements JwtPayload { + accountId: string; + + userId: string; + + schoolId: string; + + roles: string[]; + + systemId?: string; + + support?: boolean; + + isExternalUser: boolean; + + aud: string; + + exp: number; + + iat: number; + + iss: string; + + jti: string; + + sub: string; + + constructor(data: JwtPayload) { + this.accountId = data.accountId; + this.userId = data.userId; + this.schoolId = data.schoolId; + this.roles = data.roles; + this.systemId = data.systemId || ''; + this.support = data.support || false; + this.isExternalUser = data.isExternalUser; + this.aud = data.aud; + this.exp = data.exp; + this.iat = data.iat; + this.iss = data.iss; + this.jti = data.jti; + this.sub = data.sub; + } +} + +export class JwtPayloadFactory extends BaseFactory { + public static buildJWTPayloadCurrentUser(): JwtPayload { + const jwtPayload: JwtPayload = { + accountId: 'dummyAccountId', + systemId: 'dummySystemId', + roles: ['mockRoleId'], + schoolId: 'dummySchoolId', + userId: 'dummyUserId', + support: true, + isExternalUser: true, + sub: 'dummyAccountId', + jti: 'random string', + aud: 'some audience', + iss: 'feathers', + iat: Math.floor(new Date().getTime() / 1000), + exp: Math.floor(new Date().getTime() / 1000) + 3600, + }; + + return jwtPayload; + } +} + +export const jwtPayloadFactory = JwtPayloadFactory.define(JWTPayload, ({ sequence }) => { + return { + accountId: `dummyAccountId ${sequence}`, + userId: `dummyUserId ${sequence}`, + schoolId: `dummySchoolId ${sequence}`, + roles: ['mockRoleId'], + systemId: `dummySystemId ${sequence}`, + support: true, + isExternalUser: true, + sub: `dummyAccountId ${sequence}`, + jti: `random string ${sequence}`, + aud: 'some audience', + iss: 'feathers', + iat: Math.floor(new Date().getTime() / 1000), + exp: Math.floor(new Date().getTime() / 1000) + 3600, + }; +}); diff --git a/apps/server/src/modules/oauth/controller/oauth-sso.controller.spec.ts b/apps/server/src/modules/oauth/controller/oauth-sso.controller.spec.ts index 056735c63da..14ef5ea318e 100644 --- a/apps/server/src/modules/oauth/controller/oauth-sso.controller.spec.ts +++ b/apps/server/src/modules/oauth/controller/oauth-sso.controller.spec.ts @@ -1,10 +1,10 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons'; -import { ICurrentUser } from '@modules/authentication'; import { HydraOauthUc } from '@modules/oauth/uc/hydra-oauth.uc'; import { UnauthorizedException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { LegacyLogger } from '@src/core/logger'; +import { iCurrentUserFactory } from '@src/modules/authentication/testing'; import { Request } from 'express'; import { StatelessAuthorizationParams } from './dto/stateless-authorization.params'; import { OauthSSOController } from './oauth-sso.controller'; @@ -85,7 +85,7 @@ describe('OAuthController', () => { }); describe('requestAuthToken', () => { - const currentUser: ICurrentUser = { userId: 'userId' } as ICurrentUser; + const currentUser = iCurrentUserFactory.buildWithId({ userId: 'userId' }); const oauthClientId = 'clientId'; it('should call the hydraOauthUc', async () => {