Skip to content

Commit

Permalink
School users managment (#68)
Browse files Browse the repository at this point in the history
  • Loading branch information
mmarchois authored Apr 1, 2021
1 parent bbfcff0 commit 6777dd8
Show file tree
Hide file tree
Showing 60 changed files with 655 additions and 420 deletions.
28 changes: 28 additions & 0 deletions api/migrations/1617268818418-SchoolUsers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {MigrationInterface, QueryRunner} from "typeorm";

export class SchoolUsers1617268818418 implements MigrationInterface {
name = 'SchoolUsers1617268818418'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "school" DROP CONSTRAINT "FK_bbfa0c35398b642f1e3903e9789"`);
await queryRunner.query(`ALTER TABLE "school" RENAME COLUMN "directorId" TO "email"`);
await queryRunner.query(`CREATE TABLE "school_user" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "schoolId" uuid NOT NULL, "userId" uuid NOT NULL, CONSTRAINT "PK_b75c78082d7ea9dff30f9aba409" PRIMARY KEY ("id"))`);
await queryRunner.query(`ALTER TABLE "lead" ALTER COLUMN "email" DROP NOT NULL`);
await queryRunner.query(`ALTER TABLE "school" DROP COLUMN "email"`);
await queryRunner.query(`ALTER TABLE "school" ADD "email" character varying`);
await queryRunner.query(`ALTER TABLE "school_user" ADD CONSTRAINT "FK_414383e1bc95a2c691ddfd7a49f" FOREIGN KEY ("schoolId") REFERENCES "school"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "school_user" ADD CONSTRAINT "FK_85a63063b7de70a37efc967c156" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "school_user" DROP CONSTRAINT "FK_85a63063b7de70a37efc967c156"`);
await queryRunner.query(`ALTER TABLE "school_user" DROP CONSTRAINT "FK_414383e1bc95a2c691ddfd7a49f"`);
await queryRunner.query(`ALTER TABLE "school" DROP COLUMN "email"`);
await queryRunner.query(`ALTER TABLE "school" ADD "email" uuid`);
await queryRunner.query(`ALTER TABLE "lead" ALTER COLUMN "email" SET NOT NULL`);
await queryRunner.query(`DROP TABLE "school_user"`);
await queryRunner.query(`ALTER TABLE "school" RENAME COLUMN "email" TO "directorId"`);
await queryRunner.query(`ALTER TABLE "school" ADD CONSTRAINT "FK_bbfa0c35398b642f1e3903e9789" FOREIGN KEY ("directorId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
}

}

This file was deleted.

1 change: 1 addition & 0 deletions api/src/Application/School/Command/CreateSchoolCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export class CreateSchoolCommand implements ICommand {
public readonly city: string,
public readonly status: Status,
public readonly type: Type,
public readonly email?: string,
public readonly phoneNumber?: string,
public readonly numberOfStudents?: number,
public readonly numberOfClasses?: number,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe('CreateSchoolCommandHandler', () => {
'Paris',
Status.PRIVATE,
Type.ELEMENTARY,
'mathieu@fairness.coop',
'010101010101',
200,
10,
Expand Down Expand Up @@ -55,6 +56,7 @@ describe('CreateSchoolCommandHandler', () => {
'Paris',
Status.PRIVATE,
Type.ELEMENTARY,
'mathieu@fairness.coop',
'010101010101',
200,
10,
Expand All @@ -81,6 +83,7 @@ describe('CreateSchoolCommandHandler', () => {
'Paris',
Status.PRIVATE,
Type.ELEMENTARY,
'mathieu@fairness.coop',
'010101010101',
200,
10,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export class CreateSchoolCommandHandler {
city,
name,
zipCode,
email,
numberOfClasses,
numberOfStudents,
status,
Expand All @@ -43,6 +44,7 @@ export class CreateSchoolCommandHandler {
city,
status,
type,
email,
phoneNumber,
numberOfStudents,
numberOfClasses,
Expand Down
1 change: 1 addition & 0 deletions api/src/Application/School/Command/UpdateSchoolCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export class UpdateSchoolCommand implements ICommand {
public readonly city: string,
public readonly status: Status,
public readonly type: Type,
public readonly email?: string,
public readonly phoneNumber?: string,
public readonly numberOfStudents?: number,
public readonly numberOfClasses?: number,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ describe('UpdateSchoolCommandHandler', () => {
'Paris',
Status.PRIVATE,
Type.ELEMENTARY,
'mathieu@fairness.coop',
'010101010101',
200,
10,
Expand Down Expand Up @@ -106,6 +107,7 @@ describe('UpdateSchoolCommandHandler', () => {
'Paris',
Status.PRIVATE,
Type.ELEMENTARY,
'mathieu@fairness.coop',
'010101010101',
200,
10,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export class UpdateSchoolCommandHandler {
address,
city,
name,
email,
zipCode,
numberOfClasses,
numberOfStudents,
Expand Down Expand Up @@ -51,6 +52,7 @@ export class UpdateSchoolCommandHandler {
city,
status,
type,
email,
phoneNumber,
numberOfStudents,
numberOfClasses,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ICommand } from 'src/Application/ICommand';

export class AssignDirectorToSchoolCommand implements ICommand {
export class AssignUserToSchoolCommand implements ICommand {
constructor(
public readonly userId: string,
public readonly schoolId: string
Expand Down
Original file line number Diff line number Diff line change
@@ -1,54 +1,72 @@
import { mock, instance, when, verify, anything } from 'ts-mockito';
import { mock, instance, when, verify, anything, deepEqual } from 'ts-mockito';
import { School } from 'src/Domain/School/School.entity';
import { AssignDirectorToSchoolCommandHandler } from 'src/Application/School/Command/AssignDirectorToSchoolCommandHandler';
import { AssignDirectorToSchoolCommand } from 'src/Application/School/Command/AssignDirectorToSchoolCommand';
import { AssignUserToSchoolCommandHandler } from 'src/Application/School/Command/User/AssignUserToSchoolCommandHandler';
import { AssignUserToSchoolCommand } from 'src/Application/School/Command/User/AssignUserToSchoolCommand';
import { SchoolRepository } from 'src/Infrastructure/School/Repository/SchoolRepository';
import { UserRepository } from 'src/Infrastructure/User/Repository/UserRepository';
import { User, UserRole } from 'src/Domain/User/User.entity';
import { SchoolNotFoundException } from 'src/Domain/School/Exception/SchoolNotFoundException';
import { UserNotFoundException } from 'src/Domain/User/Exception/UserNotFoundException';
import { UserShouldBeDirectorException } from 'src/Domain/User/Exception/UserShouldBeDirectorException';
import { SchoolUserRepository } from 'src/Infrastructure/School/Repository/SchoolUserRepository';
import { SchoolUser } from 'src/Domain/School/SchoolUser.entity';
import { IsUserAlreadyAssignedToSchool } from 'src/Domain/User/Specification/IsUserAlreadyAssignedToSchool';
import { UserAlreadyAssignedToSchoolException } from 'src/Domain/User/Exception/UserAlreadyAssignedToSchoolException';

describe('AssignDirectorToSchoolCommandHandler', () => {
describe('AssignUserToSchoolCommandHandler', () => {
let schoolRepository: SchoolRepository;
let schoolUserRepository: SchoolUserRepository;
let userRepository: UserRepository;
let isUserAlreadyAssignedToSchool: IsUserAlreadyAssignedToSchool;
let school: School;
let user: User;
let handler: AssignDirectorToSchoolCommandHandler;
let handler: AssignUserToSchoolCommandHandler;

const command = new AssignDirectorToSchoolCommand(
const createdSchoolUser = mock(SchoolUser);

const command = new AssignUserToSchoolCommand(
'df8910f9-ac0a-412b-b9a8-dbf299340abc',
'fcf9a99f-0c7b-45ca-b68a-bfd79d73a49f',
);

beforeEach(() => {
schoolRepository = mock(SchoolRepository);
schoolUserRepository = mock(SchoolUserRepository);
userRepository = mock(UserRepository);
isUserAlreadyAssignedToSchool = mock(IsUserAlreadyAssignedToSchool);
school = mock(School);
user = mock(User);

handler = new AssignDirectorToSchoolCommandHandler(
handler = new AssignUserToSchoolCommandHandler(
instance(schoolRepository),
instance(schoolUserRepository),
instance(userRepository),
instance(isUserAlreadyAssignedToSchool),
);
});

it('testDirectorSuccessfullyAssigned', async () => {
when(createdSchoolUser.getId()).thenReturn('0b1d9435-4258-42f1-882d-4f314f8fb57d');
when(user.getRole()).thenReturn(UserRole.DIRECTOR);
when(schoolRepository.findOneById('fcf9a99f-0c7b-45ca-b68a-bfd79d73a49f'))
.thenResolve(instance(school));
when(userRepository.findOneById('df8910f9-ac0a-412b-b9a8-dbf299340abc'))
.thenResolve(instance(user));
when(school.getId()).thenReturn('fcf9a99f-0c7b-45ca-b68a-bfd79d73a49f');
when(isUserAlreadyAssignedToSchool.isSatisfiedBy(instance(school), instance(user)))
.thenResolve(false);
when(schoolUserRepository.save(
deepEqual(new SchoolUser(instance(school), instance(user)))
)).thenResolve(instance(createdSchoolUser));

expect(await handler.execute(command)).toBe(
'fcf9a99f-0c7b-45ca-b68a-bfd79d73a49f'
'0b1d9435-4258-42f1-882d-4f314f8fb57d'
);

verify(schoolRepository.findOneById('fcf9a99f-0c7b-45ca-b68a-bfd79d73a49f')).once();
verify(userRepository.findOneById('df8910f9-ac0a-412b-b9a8-dbf299340abc')).once();
verify(schoolRepository.save(instance(school))).once();
verify(school.updateDirector(instance(user))).once();
verify(isUserAlreadyAssignedToSchool.isSatisfiedBy(instance(school), instance(user))).once();
verify(schoolUserRepository.save(
deepEqual(new SchoolUser(instance(school), instance(user)))
)).once();
});

it('testSchoolNotFound', async () => {
Expand All @@ -61,8 +79,8 @@ describe('AssignDirectorToSchoolCommandHandler', () => {
expect(e.message).toBe('schools.errors.not_found');
verify(schoolRepository.findOneById('fcf9a99f-0c7b-45ca-b68a-bfd79d73a49f')).once();
verify(userRepository.findOneById(anything())).never();
verify(schoolRepository.save(anything())).never();
verify(school.updateDirector(anything())).never();
verify(schoolUserRepository.save(anything())).never();
verify(isUserAlreadyAssignedToSchool.isSatisfiedBy(anything(), anything())).never();
}
});

Expand All @@ -79,27 +97,28 @@ describe('AssignDirectorToSchoolCommandHandler', () => {
expect(e.message).toBe('users.errors.not_found');
verify(schoolRepository.findOneById('fcf9a99f-0c7b-45ca-b68a-bfd79d73a49f')).once();
verify(userRepository.findOneById('df8910f9-ac0a-412b-b9a8-dbf299340abc')).once();
verify(schoolRepository.save(anything())).never();
verify(school.updateDirector(anything())).never();
verify(schoolUserRepository.save(anything())).never();
verify(isUserAlreadyAssignedToSchool.isSatisfiedBy(anything(), anything())).never();
}
});

it('testUserNotDirector', async () => {
when(user.getRole()).thenReturn(UserRole.PHOTOGRAPHER);
it('testUserAlreadyAssigned', async () => {
when(schoolRepository.findOneById('fcf9a99f-0c7b-45ca-b68a-bfd79d73a49f'))
.thenResolve(instance(school));
when(userRepository.findOneById('df8910f9-ac0a-412b-b9a8-dbf299340abc'))
.thenResolve(instance(user));
when(isUserAlreadyAssignedToSchool.isSatisfiedBy(instance(school), instance(user)))
.thenResolve(true);

try {
expect(await handler.execute(command)).toBeUndefined();
} catch (e) {
expect(e).toBeInstanceOf(UserShouldBeDirectorException);
expect(e.message).toBe('users.errors.should_be_director');
expect(e).toBeInstanceOf(UserAlreadyAssignedToSchoolException);
expect(e.message).toBe('users.errors.already_assigned_to_school');
verify(schoolRepository.findOneById('fcf9a99f-0c7b-45ca-b68a-bfd79d73a49f')).once();
verify(userRepository.findOneById('df8910f9-ac0a-412b-b9a8-dbf299340abc')).once();
verify(schoolRepository.save(anything())).never();
verify(school.updateDirector(anything())).never();
verify(schoolUserRepository.save(anything())).never();
verify(isUserAlreadyAssignedToSchool.isSatisfiedBy(instance(school), instance(user))).once();
}
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Inject } from '@nestjs/common';
import { CommandHandler } from '@nestjs/cqrs';
import { SchoolNotFoundException } from 'src/Domain/School/Exception/SchoolNotFoundException';
import { ISchoolRepository } from 'src/Domain/School/Repository/ISchoolRepository';
import { ISchoolUserRepository } from 'src/Domain/School/Repository/ISchoolUserRepository';
import { SchoolUser } from 'src/Domain/School/SchoolUser.entity';
import { UserAlreadyAssignedToSchoolException } from 'src/Domain/User/Exception/UserAlreadyAssignedToSchoolException';
import { UserNotFoundException } from 'src/Domain/User/Exception/UserNotFoundException';
import { IUserRepository } from 'src/Domain/User/Repository/IUserRepository';
import { IsUserAlreadyAssignedToSchool } from 'src/Domain/User/Specification/IsUserAlreadyAssignedToSchool';
import { AssignUserToSchoolCommand } from './AssignUserToSchoolCommand';

@CommandHandler(AssignUserToSchoolCommand)
export class AssignUserToSchoolCommandHandler {
constructor(
@Inject('ISchoolRepository')
private readonly schoolRepository: ISchoolRepository,
@Inject('ISchoolUserRepository')
private readonly schoolUserRepository: ISchoolUserRepository,
@Inject('IUserRepository')
private readonly userRepository: IUserRepository,
private readonly isUserAlreadyAssignedToSchool: IsUserAlreadyAssignedToSchool,
) {}

public async execute(command: AssignUserToSchoolCommand): Promise<string> {
const { schoolId, userId } = command;

const school = await this.schoolRepository.findOneById(schoolId);
if (!school) {
throw new SchoolNotFoundException();
}

const user = await this.userRepository.findOneById(userId);
if (!user) {
throw new UserNotFoundException();
}

if (true === (await this.isUserAlreadyAssignedToSchool.isSatisfiedBy(school, user))) {
throw new UserAlreadyAssignedToSchoolException();
}

const schoolUser = await this.schoolUserRepository.save(
new SchoolUser(school, user)
);

return schoolUser.getId();
}
}
Loading

0 comments on commit 6777dd8

Please sign in to comment.