Skip to content

Commit

Permalink
Delete user (#69)
Browse files Browse the repository at this point in the history
  • Loading branch information
mmarchois committed Apr 3, 2021
1 parent 6777dd8 commit a34961e
Show file tree
Hide file tree
Showing 26 changed files with 293 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,19 @@ import { User } from 'src/Domain/User/User.entity';
import { GetSchoolUsersQuery } from './GetSchoolUsersQuery';
import { GetSchoolUsersQueryHandler } from './GetSchoolUsersQueryHandler';
import { SchoolUserRepository } from 'src/Infrastructure/School/Repository/SchoolUserRepository';
import { UserSchoolView } from '../../View/UserSchoolView';
import { UserSummaryView } from 'src/Application/User/View/UserSummaryView';

describe('GetSchoolsQueryHandler', () => {
it('testGetSchoolUsers', async () => {
const schoolUserRepository = mock(SchoolUserRepository);

const user1 = mock(User);
when(user1.getId()).thenReturn('4de2ffc4-e835-44c8-95b7-17c171c09873');
when(user1.getFirstName()).thenReturn('Mathieu');
when(user1.getLastName()).thenReturn('MARCHOIS');
when(user1.getEmail()).thenReturn('mathieu@fairness.coop');

const schoolUser1 = mock(SchoolUser);
when(schoolUser1.getId()).thenReturn(
'4de2ffc4-e835-44c8-95b7-17c171c09873'
);
when(schoolUser1.getUser()).thenReturn(instance(user1));

when(
Expand All @@ -32,7 +30,7 @@ describe('GetSchoolsQueryHandler', () => {
);

const expectedResult = [
new UserSchoolView(
new UserSummaryView(
'4de2ffc4-e835-44c8-95b7-17c171c09873',
'Mathieu',
'MARCHOIS',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { QueryHandler } from '@nestjs/cqrs';
import { Inject } from '@nestjs/common';
import { GetSchoolUsersQuery } from './GetSchoolUsersQuery';
import { ISchoolUserRepository } from 'src/Domain/School/Repository/ISchoolUserRepository';
import { UserSchoolView } from '../../View/UserSchoolView';
import { UserSummaryView } from 'src/Application/User/View/UserSummaryView';

@QueryHandler(GetSchoolUsersQuery)
export class GetSchoolUsersQueryHandler {
Expand All @@ -11,17 +11,19 @@ export class GetSchoolUsersQueryHandler {
private readonly schoolUserRepository: ISchoolUserRepository
) {}

public async execute(query: GetSchoolUsersQuery): Promise<UserSchoolView[]> {
const userViews: UserSchoolView[] = [];
public async execute(query: GetSchoolUsersQuery): Promise<UserSummaryView[]> {
const userViews: UserSummaryView[] = [];
const schoolUsers = await this.schoolUserRepository.findUsersBySchool(query.schoolId);

for (const schoolUser of schoolUsers) {
const user = schoolUser.getUser();

userViews.push(
new UserSchoolView(
schoolUser.getId(),
schoolUser.getUser().getFirstName(),
schoolUser.getUser().getLastName(),
schoolUser.getUser().getEmail()
new UserSummaryView(
user.getId(),
user.getFirstName(),
user.getLastName(),
user.getEmail()
)
);
}
Expand Down
8 changes: 0 additions & 8 deletions api/src/Application/School/View/UserSchoolView.ts

This file was deleted.

8 changes: 8 additions & 0 deletions api/src/Application/User/Command/RemoveUserCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ICommand } from 'src/Application/ICommand';

export class RemoveUserCommand implements ICommand {
constructor(
public readonly id: string,
public readonly currentUserId: string
) {}
}
75 changes: 75 additions & 0 deletions api/src/Application/User/Command/RemoveUserCommandHandler.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { mock, instance, when, verify, anything } from 'ts-mockito';
import { UserRepository } from 'src/Infrastructure/User/Repository/UserRepository';
import { User } from 'src/Domain/User/User.entity';
import { RemoveUserCommandHandler } from './RemoveUserCommandHandler';
import { RemoveUserCommand } from './RemoveUserCommand';
import { UserNotFoundException } from 'src/Domain/User/Exception/UserNotFoundException';
import { CantRemoveYourselfException } from 'src/Domain/User/Exception/CantRemoveYourselfException';

describe('RemoveUserCommandHandler', () => {
let userRepository: UserRepository;
let removedUser: User;
let handler: RemoveUserCommandHandler;

const command = new RemoveUserCommand(
'17efcbee-bd2f-410e-9e99-51684b592bad',
'c47f70f1-101c-4d9b-84e1-f88bed74f957'
);

beforeEach(() => {
userRepository = mock(UserRepository);
removedUser = mock(User);

handler = new RemoveUserCommandHandler(
instance(userRepository)
);
});

it('testUserRemovedSuccessfully', async () => {
when(userRepository.findOneById('17efcbee-bd2f-410e-9e99-51684b592bad'))
.thenResolve(instance(removedUser));
when(removedUser.getId()).thenReturn(
'17efcbee-bd2f-410e-9e99-51684b592bad'
);
when(
userRepository.save(instance(removedUser))
).thenResolve(instance(removedUser));

await handler.execute(command);

verify(
userRepository.remove(instance(removedUser))
).once();
verify(userRepository.findOneById('17efcbee-bd2f-410e-9e99-51684b592bad')).once();
});

it('testUserNotFound', async () => {
when(userRepository.findOneById('17efcbee-bd2f-410e-9e99-51684b592bad'))
.thenResolve(null);

try {
expect(await handler.execute(command)).toBeUndefined();
} catch (e) {
expect(e).toBeInstanceOf(UserNotFoundException);
expect(e.message).toBe('users.errors.not_found');
verify(userRepository.findOneById('17efcbee-bd2f-410e-9e99-51684b592bad')).once();
verify(userRepository.remove(anything())).never();
}
});

it('testCantRemoveYourself', async () => {
const command2 = new RemoveUserCommand(
'17efcbee-bd2f-410e-9e99-51684b592bad',
'17efcbee-bd2f-410e-9e99-51684b592bad'
);

try {
expect(await handler.execute(command2)).toBeUndefined();
} catch (e) {
expect(e).toBeInstanceOf(CantRemoveYourselfException);
expect(e.message).toBe('users.errors.cant_remove_yourself');
verify(userRepository.findOneById(anything())).never();
verify(userRepository.remove(anything())).never();
}
});
});
28 changes: 28 additions & 0 deletions api/src/Application/User/Command/RemoveUserCommandHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Inject } from '@nestjs/common';
import { CommandHandler } from '@nestjs/cqrs';
import { CantRemoveYourselfException } from 'src/Domain/User/Exception/CantRemoveYourselfException';
import { UserNotFoundException } from 'src/Domain/User/Exception/UserNotFoundException';
import { IUserRepository } from 'src/Domain/User/Repository/IUserRepository';
import { RemoveUserCommand } from './RemoveUserCommand';

@CommandHandler(RemoveUserCommand)
export class RemoveUserCommandHandler {
constructor(
@Inject('IUserRepository')
private readonly userRepository: IUserRepository,
) {}

public async execute({ id, currentUserId }: RemoveUserCommand): Promise<void> {
if (id === currentUserId) {
throw new CantRemoveYourselfException();
}

const user = await this.userRepository.findOneById(id);

if (!user) {
throw new UserNotFoundException();
}

await this.userRepository.remove(user);
}
}
5 changes: 5 additions & 0 deletions api/src/Domain/User/Exception/CantRemoveYourselfException.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class CantRemoveYourselfException extends Error {
constructor() {
super('users.errors.cant_remove_yourself');
}
}
1 change: 1 addition & 0 deletions api/src/Domain/User/Repository/IUserRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { User, UserRole } from '../User.entity';

export interface IUserRepository {
save(user: User): Promise<User>;
remove(user: User): void;
findOneByApiToken(apiToken: string): Promise<User | undefined>;
findOneByEmail(email: string): Promise<User | undefined>;
findOneById(id: string): Promise<User | undefined>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { IdDTO } from 'src/Infrastructure/Common/DTO/IdDTO';
import { Roles } from 'src/Infrastructure/User/Decorator/Roles';
import { RolesGuard } from 'src/Infrastructure/User/Security/RolesGuard';
import { UserRole } from 'src/Domain/User/User.entity';
import { UserSchoolView } from 'src/Application/School/View/UserSchoolView';
import { GetSchoolUsersQuery } from 'src/Application/School/Query/User/GetSchoolUsersQuery';
import { UserSummaryView } from 'src/Application/User/View/UserSummaryView';

@Controller('schools')
@ApiTags('School')
Expand All @@ -22,7 +22,7 @@ export class GetSchoolUsersAction {
@Get(':id/users')
@Roles(UserRole.PHOTOGRAPHER, UserRole.DIRECTOR)
@ApiOperation({summary: 'Get users for a specific school'})
public async index(@Param() dto: IdDTO): Promise<UserSchoolView[]> {
public async index(@Param() dto: IdDTO): Promise<UserSummaryView[]> {
try {
return await this.queryBus.execute(new GetSchoolUsersQuery(dto.id));
} catch (e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,14 @@ export class SchoolUserRepository implements ISchoolUserRepository {
.createQueryBuilder('schoolUser')
.select([
'schoolUser.id',
'user.id',
'user.firstName',
'user.lastName',
'user.email'
])
.innerJoin('schoolUser.user', 'user')
.innerJoin('schoolUser.school', 'school', 'school.id = :schoolId', { schoolId })
.orderBy('user.lastName', 'ASC')
.getMany();
}
}
43 changes: 43 additions & 0 deletions api/src/Infrastructure/User/Action/RemoveUserAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {
Controller,
Inject,
BadRequestException,
UseGuards,
Param,
Delete
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { ICommandBus } from 'src/Application/ICommandBus';
import { RemoveUserCommand } from 'src/Application/User/Command/RemoveUserCommand';
import { UserRole } from 'src/Domain/User/User.entity';
import { IdDTO } from 'src/Infrastructure/Common/DTO/IdDTO';
import { Roles } from 'src/Infrastructure/User/Decorator/Roles';
import { RolesGuard } from 'src/Infrastructure/User/Security/RolesGuard';
import { LoggedUser } from '../Decorator/LoggedUser';
import { UserAuthView } from '../Security/UserAuthView';

@Controller('users')
@ApiTags('User')
@ApiBearerAuth()
@UseGuards(AuthGuard('bearer'), RolesGuard)
export class RemoveUserAction {
constructor(
@Inject('ICommandBus')
private readonly commandBus: ICommandBus
) {}

@Delete(':id')
@Roles(UserRole.PHOTOGRAPHER)
@ApiOperation({ summary: 'Remove user' })
public async index(
@Param() { id }: IdDTO,
@LoggedUser() user: UserAuthView
) {
try {
await this.commandBus.execute(new RemoveUserCommand(id, user.id));
} catch (e) {
throw new BadRequestException(e.message);
}
}
}
4 changes: 4 additions & 0 deletions api/src/Infrastructure/User/Repository/UserRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export class UserRepository implements IUserRepository {
return this.repository.save(user);
}

public remove(user: User): void {
this.repository.delete(user.getId());
}

public findOneByApiToken(apiToken: string): Promise<User | undefined> {
return this.repository
.createQueryBuilder('user')
Expand Down
4 changes: 4 additions & 0 deletions api/src/Infrastructure/User/user.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { CreateUserCommandHandler } from 'src/Application/User/Command/CreateUse
import { CreateUserAction } from './Action/CreateUserAction';
import { GetUsersByRoleQueryHandler } from 'src/Application/User/Query/GetUsersByRoleQueryHandler';
import { GetPhotographersAction } from './Action/GetPhotographersAction';
import { RemoveUserCommandHandler } from 'src/Application/User/Command/RemoveUserCommandHandler';
import { RemoveUserAction } from './Action/RemoveUserAction';

@Module({
imports: [
Expand All @@ -29,6 +31,7 @@ import { GetPhotographersAction } from './Action/GetPhotographersAction';
UserLoginAction,
UpdateMeAction,
GetMeAction,
RemoveUserAction,
CreateUserAction,
GetPhotographersAction
],
Expand All @@ -43,6 +46,7 @@ import { GetPhotographersAction } from './Action/GetPhotographersAction';
RolesGuard,
CreateUserCommandHandler,
GetUsersByRoleQueryHandler,
RemoveUserCommandHandler,
]
})
export class UserModule {}
9 changes: 8 additions & 1 deletion client/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"pagination": "Affichage {start}-{from} sur {totalItems}",
"common": {
"actions": "Actions",
"not_defined": "Non-défini",
"form": {
"save": "Enregistrer",
"edit": "Modifier",
Expand All @@ -30,6 +31,9 @@
"title": "Mon compte",
"logout": "Se déconnecter"
},
"calendar": {
"breadcrumb": "Calendrier"
},
"leads": {
"breadcrumb": "Prospects",
"statutes": {
Expand Down Expand Up @@ -58,7 +62,7 @@
"address": "Adresse",
"reference": "Référence",
"number_of_students": "Nombre d'élèves",
"informations": "Fiche prospect \"{name}\"",
"informations": "Fiche prospect",
"type": "Type d'établissement",
"status": "Statut"
},
Expand Down Expand Up @@ -151,6 +155,9 @@
"title": "Edition de l'établissement"
},
"users": {
"delete": {
"confirm": "Êtes-vous sûr de vouloir supprimer cet utilisateur ?"
},
"add": {
"title": "Créer un compte utilisateur",
"notice": "L'utilisateur que vous allez créer içi aura uniquement à cet établissement."
Expand Down
13 changes: 13 additions & 0 deletions client/src/components/Nav.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import SchoolIcon from './icons/SchoolIcon.svelte';
import LeadIcon from './icons/LeadIcon.svelte';
import UsersIcon from './icons/UsersIcon.svelte';
import CalendarIcon from './icons/CalendarIcon.svelte';
import ChevronDownIcon from './icons/ChevronDownIcon.svelte';
import { settings, currentPath } from 'store';
import { ROLE_PHOTOGRAPHER } from 'constants/roles';
Expand All @@ -14,6 +15,7 @@
const schoolsPath = '/admin/schools';
const productsPath = '/admin/products';
const calendarPath = '/admin/calendar';
const usersPath = '/admin/users';
const leadsPath = '/admin/leads';
const activeClass =
Expand Down Expand Up @@ -45,6 +47,17 @@
<span class="ml-4">{$_('dashboard.title')}</span>
</a>
</li>
{#if $session.user.scope === ROLE_PHOTOGRAPHER}
<li class="relative px-6 py-3">
{#if $currentPath.includes(calendarPath)}
<span class="{activeClass}" aria-hidden="true"></span>
{/if}
<a class={$currentPath.includes(calendarPath) ? activeLinkClass : linkClass} href={calendarPath}>
<CalendarIcon className={'w-5 h-5'} />
<span class="ml-4">{$_('calendar.breadcrumb')}</span>
</a>
</li>
{/if}
<li class="relative px-6 py-3">
{#if $currentPath.includes(schoolsPath)}
<span class={activeClass} aria-hidden="true"></span>
Expand Down
Loading

0 comments on commit a34961e

Please sign in to comment.