From 9652b635b2580c9241dfac1b2e7f8f375960e618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= <103562092+MarvinOehlerkingCap@users.noreply.github.com> Date: Fri, 3 Nov 2023 13:17:41 +0100 Subject: [PATCH 1/3] N21-1128 Add automated test for Brandenburg Central LDAP Login (#4510) --- .../controllers/api-test/login.api.spec.ts | 143 ++++++++++++++---- 1 file changed, 113 insertions(+), 30 deletions(-) diff --git a/apps/server/src/modules/authentication/controllers/api-test/login.api.spec.ts b/apps/server/src/modules/authentication/controllers/api-test/login.api.spec.ts index 253d692055d..7da3c21eab9 100644 --- a/apps/server/src/modules/authentication/controllers/api-test/login.api.spec.ts +++ b/apps/server/src/modules/authentication/controllers/api-test/login.api.spec.ts @@ -1,16 +1,17 @@ import { EntityManager } from '@mikro-orm/core'; +import { SSOErrorCode } from '@modules/oauth/loggable'; +import { OauthTokenResponse } from '@modules/oauth/service/dto'; +import { ServerTestModule } from '@modules/server/server.module'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Account, RoleName, SchoolEntity, SystemEntity, User } from '@shared/domain'; import { accountFactory, roleFactory, schoolFactory, systemFactory, userFactory } from '@shared/testing'; -import { SSOErrorCode } from '@modules/oauth/loggable'; -import { OauthTokenResponse } from '@modules/oauth/service/dto'; -import { ServerTestModule } from '@modules/server/server.module'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import crypto, { KeyPairKeyObjectResult } from 'crypto'; import jwt from 'jsonwebtoken'; import request, { Response } from 'supertest'; +import { ICurrentUser } from '../../interface'; import { LdapAuthorizationBodyParams, LocalAuthorizationBodyParams, OauthLoginResponse } from '../dto'; const ldapAccountUserName = 'ldapAccountUserName'; @@ -145,41 +146,38 @@ describe('Login Controller (api)', () => { }); describe('loginLdap', () => { - let account: Account; - let user: User; - let school: SchoolEntity; - let system: SystemEntity; - - beforeAll(async () => { - const schoolExternalId = 'mockSchoolExternalId'; - system = systemFactory.withLdapConfig().buildWithId({}); - school = schoolFactory.buildWithId({ systems: [system], externalId: schoolExternalId }); - const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); + describe('when user login succeeds', () => { + const setup = async () => { + const schoolExternalId = 'mockSchoolExternalId'; + const system: SystemEntity = systemFactory.withLdapConfig().buildWithId({}); + const school: SchoolEntity = schoolFactory.buildWithId({ systems: [system], externalId: schoolExternalId }); + const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); - user = userFactory.buildWithId({ school, roles: [studentRoles], ldapDn: mockUserLdapDN }); + const user: User = userFactory.buildWithId({ school, roles: [studentRoles], ldapDn: mockUserLdapDN }); - account = accountFactory.buildWithId({ - userId: user.id, - username: `${schoolExternalId}/${ldapAccountUserName}`.toLowerCase(), - systemId: system.id, - }); + const account: Account = accountFactory.buildWithId({ + userId: user.id, + username: `${schoolExternalId}/${ldapAccountUserName}`.toLowerCase(), + systemId: system.id, + }); - em.persist(system); - em.persist(school); - em.persist(studentRoles); - em.persist(user); - em.persist(account); - await em.flush(); - }); + await em.persistAndFlush([system, school, studentRoles, user, account]); - describe('when user login succeeds', () => { - it('should return jwt', async () => { const params: LdapAuthorizationBodyParams = { username: ldapAccountUserName, password: defaultPassword, schoolId: school.id, systemId: system.id, }; + + return { + params, + }; + }; + + it('should return jwt', async () => { + const { params } = await setup(); + const response = await request(app.getHttpServer()).post(`${basePath}/ldap`).send(params); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access @@ -199,14 +197,99 @@ describe('Login Controller (api)', () => { }); describe('when user login fails', () => { - it('should return error response', async () => { + const setup = async () => { + const schoolExternalId = 'mockSchoolExternalId'; + const system: SystemEntity = systemFactory.withLdapConfig().buildWithId({}); + const school: SchoolEntity = schoolFactory.buildWithId({ systems: [system], externalId: schoolExternalId }); + const studentRoles = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); + + const user: User = userFactory.buildWithId({ school, roles: [studentRoles], ldapDn: mockUserLdapDN }); + + const account: Account = accountFactory.buildWithId({ + userId: user.id, + username: `${schoolExternalId}/${ldapAccountUserName}`.toLowerCase(), + systemId: system.id, + }); + + await em.persistAndFlush([system, school, studentRoles, user, account]); + const params: LdapAuthorizationBodyParams = { username: 'nonExistentUser', password: 'wrongPassword', schoolId: school.id, systemId: system.id, }; - await request(app.getHttpServer()).post(`${basePath}/ldap`).send(params).expect(401); + + return { + params, + }; + }; + + it('should return error response', async () => { + const { params } = await setup(); + + const response = await request(app.getHttpServer()).post(`${basePath}/ldap`).send(params); + + expect(response.status).toEqual(HttpStatus.UNAUTHORIZED); + }); + }); + + describe('when logging in as a user of the Central LDAP of Brandenburg', () => { + const setup = async () => { + const officialSchoolNumber = '01234'; + const system: SystemEntity = systemFactory.withLdapConfig().buildWithId({}); + const school: SchoolEntity = schoolFactory.buildWithId({ + systems: [system], + externalId: officialSchoolNumber, + officialSchoolNumber, + }); + const studentRole = roleFactory.build({ name: RoleName.STUDENT, permissions: [] }); + + const user: User = userFactory.buildWithId({ school, roles: [studentRole], ldapDn: mockUserLdapDN }); + + const account: Account = accountFactory.buildWithId({ + userId: user.id, + username: `${officialSchoolNumber}/${ldapAccountUserName}`.toLowerCase(), + systemId: system.id, + }); + + await em.persistAndFlush([system, school, studentRole, user, account]); + + const params: LdapAuthorizationBodyParams = { + username: ldapAccountUserName, + password: defaultPassword, + schoolId: school.id, + systemId: system.id, + }; + + return { + params, + user, + account, + school, + system, + studentRole, + }; + }; + + it('should return a jwt', async () => { + const { params, user, account, school, system, studentRole } = await setup(); + + const response = await request(app.getHttpServer()).post(`${basePath}/ldap`).send(params); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(response.body.accessToken).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-argument + const decodedToken = jwt.decode(response.body.accessToken); + expect(decodedToken).toMatchObject({ + userId: user.id, + systemId: system.id, + roles: [studentRole.id], + schoolId: school.id, + accountId: account.id, + isExternalUser: false, + }); + expect(decodedToken).not.toHaveProperty('externalIdToken'); }); }); }); From 8da2a40248dfc4a425c12ca7a3e76c3be78c5758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= <103562092+MarvinOehlerkingCap@users.noreply.github.com> Date: Fri, 3 Nov 2023 13:58:57 +0100 Subject: [PATCH 2/3] N21-1310 Refactor user login migration api spec (#4520) --- apps/server/src/modules/provisioning/index.ts | 2 +- .../modules/provisioning/strategy/index.ts | 2 +- .../provisioning/strategy/sanis/index.ts | 3 + .../api-test/user-login-migration.api.spec.ts | 234 ++++++++++++------ .../response/user-login-migration.response.ts | 4 + .../user-login-migration.controller.ts | 5 +- .../mapper/user-login-migration.mapper.ts | 3 +- .../src/shared/testing/factory/index.ts | 1 + .../shared/testing/user-role-permissions.ts | 1 + 9 files changed, 170 insertions(+), 85 deletions(-) create mode 100644 apps/server/src/modules/provisioning/strategy/sanis/index.ts diff --git a/apps/server/src/modules/provisioning/index.ts b/apps/server/src/modules/provisioning/index.ts index b9814818220..0e0bc64d04a 100644 --- a/apps/server/src/modules/provisioning/index.ts +++ b/apps/server/src/modules/provisioning/index.ts @@ -1,4 +1,4 @@ export * from './provisioning.module'; export * from './dto/provisioning.dto'; export * from './service/provisioning.service'; -export * from './strategy/index'; +export * from './strategy'; diff --git a/apps/server/src/modules/provisioning/strategy/index.ts b/apps/server/src/modules/provisioning/strategy/index.ts index 369357cc351..e0eeeae1776 100644 --- a/apps/server/src/modules/provisioning/strategy/index.ts +++ b/apps/server/src/modules/provisioning/strategy/index.ts @@ -2,4 +2,4 @@ export * from './base.strategy'; export * from './iserv/iserv.strategy'; export * from './oidc/oidc.strategy'; export * from './oidc-mock/oidc-mock.strategy'; -export * from './sanis/sanis.strategy'; +export * from './sanis'; diff --git a/apps/server/src/modules/provisioning/strategy/sanis/index.ts b/apps/server/src/modules/provisioning/strategy/sanis/index.ts new file mode 100644 index 00000000000..4f98cbd73e9 --- /dev/null +++ b/apps/server/src/modules/provisioning/strategy/sanis/index.ts @@ -0,0 +1,3 @@ +export * from './response'; +export * from './sanis.strategy'; +export * from './sanis-response.mapper'; diff --git a/apps/server/src/modules/user-login-migration/controller/api-test/user-login-migration.api.spec.ts b/apps/server/src/modules/user-login-migration/controller/api-test/user-login-migration.api.spec.ts index b2de2ac9fc0..154148c1fa0 100644 --- a/apps/server/src/modules/user-login-migration/controller/api-test/user-login-migration.api.spec.ts +++ b/apps/server/src/modules/user-login-migration/controller/api-test/user-login-migration.api.spec.ts @@ -1,12 +1,16 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { OauthTokenResponse } from '@modules/oauth/service/dto'; +import { SanisResponse, SanisRole } from '@modules/provisioning'; +import { ServerTestModule } from '@modules/server'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { Permission, SchoolEntity, SystemEntity, User } from '@shared/domain'; +import { SchoolEntity, SystemEntity, User } from '@shared/domain'; import { UserLoginMigrationEntity } from '@shared/domain/entity/user-login-migration.entity'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { cleanupCollections, + JwtTestFactory, schoolFactory, systemFactory, TestApiClient, @@ -14,14 +18,11 @@ import { userFactory, userLoginMigrationFactory, } from '@shared/testing'; -import { JwtTestFactory } from '@shared/testing/factory/jwt.test.factory'; -import { OauthTokenResponse } from '@modules/oauth/service/dto'; -import { ServerTestModule } from '@modules/server'; +import { ErrorResponse } from '@src/core/error/dto'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { UUID } from 'bson'; import { Response } from 'supertest'; -import { SanisResponse, SanisRole } from '@modules/provisioning/strategy/sanis/response'; import { UserLoginMigrationResponse } from '../dto'; import { Oauth2MigrationParams } from '../dto/oauth2-migration.params'; @@ -37,6 +38,7 @@ jest.mock('jwks-rsa', () => () => { getSigningKeys: jest.fn(), }; }); + describe('UserLoginMigrationController (API)', () => { let app: INestApplication; let em: EntityManager; @@ -80,8 +82,8 @@ describe('UserLoginMigrationController (API)', () => { sourceSystem, startedAt: date, mandatorySince: date, - closedAt: undefined, - finishedAt: undefined, + closedAt: date, + finishedAt: date, }); const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }); @@ -107,6 +109,7 @@ describe('UserLoginMigrationController (API)', () => { expect(response.body).toEqual({ data: [ { + id: userLoginMigration.id, sourceSystemId: sourceSystem.id, targetSystemId: targetSystem.id, startedAt: userLoginMigration.startedAt.toISOString(), @@ -130,7 +133,7 @@ describe('UserLoginMigrationController (API)', () => { }); describe('[GET] /user-login-migrations/schools/:schoolId', () => { - describe('when a user login migration is found', () => { + describe('when a user login migration exists', () => { const setup = async () => { const date: Date = new Date(2023, 5, 4); const sourceSystem: SystemEntity = systemFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); @@ -147,9 +150,7 @@ describe('UserLoginMigrationController (API)', () => { closedAt: undefined, finishedAt: undefined, }); - const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }, [ - Permission.USER_LOGIN_MIGRATION_ADMIN, - ]); + const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }); await em.persistAndFlush([sourceSystem, targetSystem, school, adminAccount, adminUser, userLoginMigration]); @@ -171,6 +172,7 @@ describe('UserLoginMigrationController (API)', () => { expect(response.status).toEqual(HttpStatus.OK); expect(response.body).toEqual({ + id: userLoginMigration.id, sourceSystemId: sourceSystem.id, targetSystemId: targetSystem.id, startedAt: userLoginMigration.startedAt.toISOString(), @@ -181,12 +183,10 @@ describe('UserLoginMigrationController (API)', () => { }); }); - describe('when no user login migration is found', () => { + describe('when no user login migration exists', () => { const setup = async () => { const school: SchoolEntity = schoolFactory.buildWithId(); - const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }, [ - Permission.USER_LOGIN_MIGRATION_ADMIN, - ]); + const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }); await em.persistAndFlush([school, adminAccount, adminUser]); @@ -198,13 +198,26 @@ describe('UserLoginMigrationController (API)', () => { }; }; - it('should return the users migration', async () => { + it('should have the status "not found"', async () => { const { loggedInClient, school } = await setup(); const response: Response = await loggedInClient.get(`schools/${school.id}`); expect(response.status).toEqual(HttpStatus.NOT_FOUND); }); + + it('should return an error response', async () => { + const { loggedInClient, school } = await setup(); + + const response: Response = await loggedInClient.get(`schools/${school.id}`); + + expect(response.body).toEqual({ + message: 'Not Found', + type: 'NOT_FOUND', + code: 404, + title: 'Not Found', + }); + }); }); describe('when unauthorized', () => { @@ -217,7 +230,7 @@ describe('UserLoginMigrationController (API)', () => { }); describe('[POST] /start', () => { - describe('when current User start the migration successfully', () => { + describe('when current user start the migration successfully', () => { const setup = async () => { const sourceSystem: SystemEntity = systemFactory.withLdapConfig().buildWithId({ alias: 'SourceSystem' }); const targetSystem: SystemEntity = systemFactory.withOauthConfig().buildWithId({ alias: 'SANIS' }); @@ -226,9 +239,7 @@ describe('UserLoginMigrationController (API)', () => { officialSchoolNumber: '12345', }); - const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }, [ - Permission.USER_LOGIN_MIGRATION_ADMIN, - ]); + const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }); await em.persistAndFlush([sourceSystem, targetSystem, school, adminAccount, adminUser]); @@ -236,6 +247,8 @@ describe('UserLoginMigrationController (API)', () => { return { loggedInClient, + sourceSystem, + targetSystem, }; }; @@ -246,6 +259,28 @@ describe('UserLoginMigrationController (API)', () => { expect(response.status).toEqual(HttpStatus.CREATED); }); + + it('should return the user login migration', async () => { + const { loggedInClient, sourceSystem, targetSystem } = await setup(); + + const response: Response = await loggedInClient.post(`/start`); + + expect(response.body).toEqual({ + id: expect.any(String), + sourceSystemId: sourceSystem.id, + startedAt: expect.any(String), + targetSystemId: targetSystem.id, + }); + }); + + it('should should change the database correctly', async () => { + const { loggedInClient } = await setup(); + + const response: Response = await loggedInClient.post(`/start`); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment + await em.findOneOrFail(UserLoginMigrationEntity, { id: response.body.id }); + }); }); describe('when current User start the migration and is not authorized', () => { @@ -296,9 +331,7 @@ describe('UserLoginMigrationController (API)', () => { mandatorySince: date, }); - const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }, [ - Permission.USER_LOGIN_MIGRATION_ADMIN, - ]); + const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }); await em.persistAndFlush([sourceSystem, targetSystem, school, adminAccount, adminUser, userLoginMigration]); @@ -339,9 +372,7 @@ describe('UserLoginMigrationController (API)', () => { finishedAt: date, }); - const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }, [ - Permission.USER_LOGIN_MIGRATION_ADMIN, - ]); + const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }); await em.persistAndFlush([sourceSystem, targetSystem, school, adminAccount, adminUser, userLoginMigration]); @@ -370,9 +401,7 @@ describe('UserLoginMigrationController (API)', () => { systems: [sourceSystem], }); - const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }, [ - Permission.USER_LOGIN_MIGRATION_ADMIN, - ]); + const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }); await em.persistAndFlush([sourceSystem, targetSystem, school, adminAccount, adminUser]); @@ -466,10 +495,10 @@ describe('UserLoginMigrationController (API)', () => { startedAt: new Date('2022-12-17T03:24:00'), }); - const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin( - { school, externalId: 'externalUserId' }, - [Permission.USER_LOGIN_MIGRATION_ADMIN] - ); + const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ + school, + externalId: 'externalUserId', + }); await em.persistAndFlush([sourceSystem, targetSystem, school, adminAccount, adminUser, userLoginMigration]); em.clear(); @@ -610,9 +639,7 @@ describe('UserLoginMigrationController (API)', () => { }); school.userLoginMigration = userLoginMigration; - const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }, [ - Permission.USER_LOGIN_MIGRATION_ADMIN, - ]); + const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }); await em.persistAndFlush([sourceSystem, targetSystem, school, adminAccount, adminUser, userLoginMigration]); em.clear(); @@ -642,16 +669,29 @@ describe('UserLoginMigrationController (API)', () => { expect(response.body).not.toHaveProperty('closedAt'); expect(response.body).not.toHaveProperty('finishedAt'); }); + + it('should should change the database correctly', async () => { + const { loggedInClient } = await setup(); + + const response: Response = await loggedInClient.put(`/restart`); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment + const entity = await em.findOneOrFail(UserLoginMigrationEntity, { id: response.body.id }); + + expect(entity.startedAt).toBeDefined(); + expect(entity.closedAt).toBeUndefined(); + expect(entity.finishedAt).toBeUndefined(); + }); }); describe('when invalid User restart the migration', () => { const setup = async () => { - const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin(); + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); - await em.persistAndFlush([adminAccount, adminUser]); + await em.persistAndFlush([teacherAccount, teacherUser]); em.clear(); - const loggedInClient = await testApiClient.login(adminAccount); + const loggedInClient = await testApiClient.login(teacherAccount); return { loggedInClient, @@ -692,9 +732,7 @@ describe('UserLoginMigrationController (API)', () => { }); school.userLoginMigration = userLoginMigration; - const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }, [ - Permission.USER_LOGIN_MIGRATION_ADMIN, - ]); + const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }); await em.persistAndFlush([sourceSystem, targetSystem, school, adminAccount, adminUser, userLoginMigration]); em.clear(); @@ -734,9 +772,7 @@ describe('UserLoginMigrationController (API)', () => { }); school.userLoginMigration = userLoginMigration; - const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }, [ - Permission.USER_LOGIN_MIGRATION_ADMIN, - ]); + const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }); await em.persistAndFlush([sourceSystem, targetSystem, school, adminAccount, adminUser, userLoginMigration]); em.clear(); @@ -778,9 +814,7 @@ describe('UserLoginMigrationController (API)', () => { }); school.userLoginMigration = userLoginMigration; - const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }, [ - Permission.USER_LOGIN_MIGRATION_ADMIN, - ]); + const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }); await em.persistAndFlush([sourceSystem, targetSystem, school, adminAccount, adminUser, userLoginMigration]); em.clear(); @@ -801,6 +835,17 @@ describe('UserLoginMigrationController (API)', () => { const responseBody = response.body as UserLoginMigrationResponse; expect(responseBody.mandatorySince).toBeDefined(); }); + + it('should should change the database correctly', async () => { + const { loggedInClient } = await setup(); + + const response: Response = await loggedInClient.put('/mandatory', { mandatory: true }); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment + const entity = await em.findOneOrFail(UserLoginMigrationEntity, { id: response.body.id }); + + expect(entity.mandatorySince).toBeDefined(); + }); }); describe('when migration is set from mandatory to optional', () => { @@ -818,13 +863,10 @@ describe('UserLoginMigrationController (API)', () => { sourceSystem, startedAt: new Date(2023, 1, 4), mandatorySince: new Date(2023, 1, 4), - closedAt: new Date(2023, 1, 5), }); school.userLoginMigration = userLoginMigration; - const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }, [ - Permission.USER_LOGIN_MIGRATION_ADMIN, - ]); + const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }); await em.persistAndFlush([sourceSystem, targetSystem, school, adminAccount, adminUser, userLoginMigration]); em.clear(); @@ -845,6 +887,17 @@ describe('UserLoginMigrationController (API)', () => { const responseBody = response.body as UserLoginMigrationResponse; expect(responseBody.mandatorySince).toBeUndefined(); }); + + it('should should change the database correctly', async () => { + const { loggedInClient } = await setup(); + + const response: Response = await loggedInClient.put('/mandatory', { mandatory: false }); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment + const entity = await em.findOneOrFail(UserLoginMigrationEntity, { id: response.body.id }); + + expect(entity.mandatorySince).toBeUndefined(); + }); }); describe('when migration is not started', () => { @@ -856,9 +909,7 @@ describe('UserLoginMigrationController (API)', () => { officialSchoolNumber: '12345', }); - const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }, [ - Permission.USER_LOGIN_MIGRATION_ADMIN, - ]); + const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }); await em.persistAndFlush([sourceSystem, targetSystem, school, adminAccount, adminUser]); em.clear(); @@ -897,9 +948,7 @@ describe('UserLoginMigrationController (API)', () => { }); school.userLoginMigration = userLoginMigration; - const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }, [ - Permission.USER_LOGIN_MIGRATION_ADMIN, - ]); + const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }); await em.persistAndFlush([sourceSystem, targetSystem, school, adminAccount, adminUser, userLoginMigration]); em.clear(); @@ -947,12 +996,12 @@ describe('UserLoginMigrationController (API)', () => { }); school.userLoginMigration = userLoginMigration; - const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }, []); + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }); - await em.persistAndFlush([sourceSystem, targetSystem, school, adminAccount, adminUser, userLoginMigration]); + await em.persistAndFlush([sourceSystem, targetSystem, school, teacherAccount, teacherUser, userLoginMigration]); em.clear(); - const loggedInClient = await testApiClient.login(adminAccount); + const loggedInClient = await testApiClient.login(teacherAccount); return { loggedInClient, @@ -990,9 +1039,7 @@ describe('UserLoginMigrationController (API)', () => { lastLoginSystemChange: new Date(2023, 1, 5), }); - const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }, [ - Permission.USER_LOGIN_MIGRATION_ADMIN, - ]); + const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }); await em.persistAndFlush([ sourceSystem, @@ -1018,7 +1065,7 @@ describe('UserLoginMigrationController (API)', () => { const response: Response = await loggedInClient.post('/close'); - expect(response.status).toEqual(HttpStatus.CREATED); + expect(response.status).toEqual(HttpStatus.OK); }); it('should return the closed user login migration', async () => { @@ -1027,6 +1074,7 @@ describe('UserLoginMigrationController (API)', () => { const response: Response = await loggedInClient.post('/close'); expect(response.body).toEqual({ + id: expect.any(String), targetSystemId: userLoginMigration.targetSystem.id, sourceSystemId: userLoginMigration.sourceSystem?.id, startedAt: userLoginMigration.startedAt.toISOString(), @@ -1034,6 +1082,18 @@ describe('UserLoginMigrationController (API)', () => { finishedAt: expect.any(String), }); }); + + it('should should change the database correctly', async () => { + const { loggedInClient } = await setup(); + + const response: Response = await loggedInClient.post('/close'); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment + const entity = await em.findOneOrFail(UserLoginMigrationEntity, { id: response.body.id }); + + expect(entity.closedAt).toBeDefined(); + expect(entity.finishedAt).toBeDefined(); + }); }); describe('when migration is not started', () => { @@ -1045,9 +1105,7 @@ describe('UserLoginMigrationController (API)', () => { officialSchoolNumber: '12345', }); - const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }, [ - Permission.USER_LOGIN_MIGRATION_ADMIN, - ]); + const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }); await em.persistAndFlush([sourceSystem, targetSystem, school, adminAccount, adminUser]); em.clear(); @@ -1066,6 +1124,19 @@ describe('UserLoginMigrationController (API)', () => { expect(response.status).toEqual(HttpStatus.NOT_FOUND); }); + + it('should return an error response', async () => { + const { loggedInClient } = await setup(); + + const response: Response = await loggedInClient.post('/close'); + + expect(response.body).toEqual({ + message: 'Not Found', + type: 'USER_LOGIN_MIGRATION_NOT_FOUND', + code: 404, + title: 'User Login Migration Not Found', + }); + }); }); describe('when the migration is already closed', () => { @@ -1085,9 +1156,7 @@ describe('UserLoginMigrationController (API)', () => { closedAt: new Date(2023, 1, 5), }); - const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }, [ - Permission.USER_LOGIN_MIGRATION_ADMIN, - ]); + const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }); await em.persistAndFlush([sourceSystem, targetSystem, school, adminAccount, adminUser, userLoginMigration]); em.clear(); @@ -1100,12 +1169,21 @@ describe('UserLoginMigrationController (API)', () => { }; }; + it('should return status ok', async () => { + const { loggedInClient } = await setup(); + + const response: Response = await loggedInClient.post('/close'); + + expect(response.status).toEqual(HttpStatus.OK); + }); + it('should return the same user login migration', async () => { const { loggedInClient, userLoginMigration } = await setup(); const response: Response = await loggedInClient.post('/close'); expect(response.body).toEqual({ + id: userLoginMigration.id, targetSystemId: userLoginMigration.targetSystem.id, sourceSystemId: userLoginMigration.sourceSystem?.id, startedAt: userLoginMigration.startedAt.toISOString(), @@ -1132,9 +1210,7 @@ describe('UserLoginMigrationController (API)', () => { finishedAt: new Date(2023, 1, 6), }); - const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }, [ - Permission.USER_LOGIN_MIGRATION_ADMIN, - ]); + const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }); await em.persistAndFlush([sourceSystem, targetSystem, school, adminAccount, adminUser, userLoginMigration]); em.clear(); @@ -1181,12 +1257,12 @@ describe('UserLoginMigrationController (API)', () => { closedAt: new Date(2023, 1, 5), }); - const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }, []); + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }); - await em.persistAndFlush([sourceSystem, targetSystem, school, adminAccount, adminUser, userLoginMigration]); + await em.persistAndFlush([sourceSystem, targetSystem, school, teacherAccount, teacherUser, userLoginMigration]); em.clear(); - const loggedInClient = await testApiClient.login(adminAccount); + const loggedInClient = await testApiClient.login(teacherAccount); return { loggedInClient, @@ -1220,9 +1296,7 @@ describe('UserLoginMigrationController (API)', () => { const user: User = userFactory.buildWithId(); - const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }, [ - Permission.USER_LOGIN_MIGRATION_ADMIN, - ]); + const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ school }); await em.persistAndFlush([ sourceSystem, diff --git a/apps/server/src/modules/user-login-migration/controller/dto/response/user-login-migration.response.ts b/apps/server/src/modules/user-login-migration/controller/dto/response/user-login-migration.response.ts index 51c02793d09..efa9fd83720 100644 --- a/apps/server/src/modules/user-login-migration/controller/dto/response/user-login-migration.response.ts +++ b/apps/server/src/modules/user-login-migration/controller/dto/response/user-login-migration.response.ts @@ -1,6 +1,9 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class UserLoginMigrationResponse { + @ApiProperty() + id: string; + @ApiPropertyOptional({ description: 'Id of the system which is the origin of the migration', }) @@ -32,6 +35,7 @@ export class UserLoginMigrationResponse { finishedAt?: Date; constructor(props: UserLoginMigrationResponse) { + this.id = props.id; this.sourceSystemId = props.sourceSystemId; this.targetSystemId = props.targetSystemId; this.mandatorySince = props.mandatorySince; diff --git a/apps/server/src/modules/user-login-migration/controller/user-login-migration.controller.ts b/apps/server/src/modules/user-login-migration/controller/user-login-migration.controller.ts index 3e788a54725..fc1c9c9f9cf 100644 --- a/apps/server/src/modules/user-login-migration/controller/user-login-migration.controller.ts +++ b/apps/server/src/modules/user-login-migration/controller/user-login-migration.controller.ts @@ -1,4 +1,5 @@ -import { Body, Controller, Get, Param, Post, Put, Query } from '@nestjs/common'; +import { Authenticate, CurrentUser, ICurrentUser, JWT } from '@modules/authentication'; +import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; import { ApiForbiddenResponse, ApiInternalServerErrorResponse, @@ -11,7 +12,6 @@ import { ApiUnprocessableEntityResponse, } from '@nestjs/swagger'; import { Page, UserLoginMigrationDO } from '@shared/domain'; -import { Authenticate, CurrentUser, ICurrentUser, JWT } from '@modules/authentication'; import { SchoolNumberMissingLoggableException, UserLoginMigrationAlreadyClosedLoggableException, @@ -181,6 +181,7 @@ export class UserLoginMigrationController { } @Post('close') + @HttpCode(HttpStatus.OK) @ApiUnprocessableEntityResponse({ description: 'User login migration is already closed and cannot be modified. Restart is possible.', type: UserLoginMigrationAlreadyClosedLoggableException, diff --git a/apps/server/src/modules/user-login-migration/mapper/user-login-migration.mapper.ts b/apps/server/src/modules/user-login-migration/mapper/user-login-migration.mapper.ts index cf11c639133..272e2309392 100644 --- a/apps/server/src/modules/user-login-migration/mapper/user-login-migration.mapper.ts +++ b/apps/server/src/modules/user-login-migration/mapper/user-login-migration.mapper.ts @@ -1,6 +1,6 @@ import { UserLoginMigrationDO } from '@shared/domain'; import { UserLoginMigrationResponse, UserLoginMigrationSearchParams } from '../controller/dto'; -import { UserLoginMigrationQuery } from '../uc/dto/user-login-migration-query'; +import { UserLoginMigrationQuery } from '../uc'; export class UserLoginMigrationMapper { static mapSearchParamsToQuery(searchParams: UserLoginMigrationSearchParams): UserLoginMigrationQuery { @@ -12,6 +12,7 @@ export class UserLoginMigrationMapper { static mapUserLoginMigrationDoToResponse(domainObject: UserLoginMigrationDO): UserLoginMigrationResponse { const response: UserLoginMigrationResponse = new UserLoginMigrationResponse({ + id: domainObject.id as string, sourceSystemId: domainObject.sourceSystemId, targetSystemId: domainObject.targetSystemId, startedAt: domainObject.startedAt, diff --git a/apps/server/src/shared/testing/factory/index.ts b/apps/server/src/shared/testing/factory/index.ts index d981b4ca29c..7d5ec2ab753 100644 --- a/apps/server/src/shared/testing/factory/index.ts +++ b/apps/server/src/shared/testing/factory/index.ts @@ -36,3 +36,4 @@ export * from './user-login-migration.factory'; export * from './user.do.factory'; export * from './user.factory'; export * from './legacy-file-entity-mock.factory'; +export * from './jwt.test.factory'; diff --git a/apps/server/src/shared/testing/user-role-permissions.ts b/apps/server/src/shared/testing/user-role-permissions.ts index 6c38287a37e..a3c82aecc7c 100644 --- a/apps/server/src/shared/testing/user-role-permissions.ts +++ b/apps/server/src/shared/testing/user-role-permissions.ts @@ -140,4 +140,5 @@ export const adminPermissions = [ Permission.IMPORT_USER_VIEW, Permission.SCHOOL_TOOL_ADMIN, Permission.GROUP_FULL_ADMIN, + Permission.USER_LOGIN_MIGRATION_ADMIN, ] as Permission[]; From 0d2718d1b02ca279848311548b643160e38feafe Mon Sep 17 00:00:00 2001 From: agnisa-cap Date: Fri, 3 Nov 2023 14:35:38 +0100 Subject: [PATCH 3/3] N21-1329 adds gzip as encoding for provisioning data request (#4513) --- .../strategy/sanis/sanis.strategy.spec.ts | 16 +++++++++++++++- .../strategy/sanis/sanis.strategy.ts | 5 ++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.spec.ts b/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.spec.ts index f0ea97f89fd..0ef8173c2d2 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.spec.ts @@ -188,7 +188,21 @@ describe('SanisStrategy', () => { provisioningUrl, expect.objectContaining({ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - headers: expect.objectContaining({ Authorization: 'Bearer sanisAccessToken' }), + headers: expect.objectContaining({ Authorization: 'Bearer sanisAccessToken', 'Accept-Encoding': 'gzip' }), + }) + ); + }); + + it('should accept gzip compressed data', async () => { + const { input, provisioningUrl } = setup(); + + await strategy.getData(input); + + expect(httpService.get).toHaveBeenCalledWith( + provisioningUrl, + expect.objectContaining({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + headers: expect.objectContaining({ 'Accept-Encoding': 'gzip' }), }) ); }); diff --git a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts b/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts index a09ae204c69..ad48ae06d93 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts @@ -39,7 +39,10 @@ export class SanisProvisioningStrategy extends OidcProvisioningStrategy { } const axiosConfig: AxiosRequestConfig = { - headers: { Authorization: `Bearer ${input.accessToken}` }, + headers: { + Authorization: `Bearer ${input.accessToken}`, + 'Accept-Encoding': 'gzip', + }, }; const axiosResponse: AxiosResponse = await firstValueFrom(