Skip to content

Commit

Permalink
#790 call backend when creating users
Browse files Browse the repository at this point in the history
+ check if email not exists
  • Loading branch information
janikEndtner committed Feb 2, 2024
1 parent 6927b65 commit b31995c
Show file tree
Hide file tree
Showing 12 changed files with 165 additions and 20 deletions.
4 changes: 4 additions & 0 deletions backend/src/main/java/ch/puzzle/okr/dto/NewUserDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package ch.puzzle.okr.dto;

public record NewUserDto(String firstname, String lastname, String email) {
}
36 changes: 30 additions & 6 deletions frontend/src/app/services/user.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getTestBed, TestBed } from '@angular/core/testing';
import { fakeAsync, getTestBed, TestBed, tick } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UserService } from './user.service';
import { users } from '../shared/testData';
import { testUser, users } from '../shared/testData';

describe('UserService', () => {
let service: UserService;
Expand All @@ -21,11 +21,11 @@ describe('UserService', () => {
httpMock.verify();
});

test('should be created', () => {
it('should be created', () => {
expect(service).toBeTruthy();
});

test('getUsers should only reload users when they are not loaded yet', (done) => {
it('getUsers should only reload users when they are not loaded yet', (done) => {
const spy = jest.spyOn(service, 'reloadUsers');
service.getUsers().subscribe(() => {
expect(spy).toBeCalledTimes(1);
Expand All @@ -38,11 +38,11 @@ describe('UserService', () => {
});
});

test('get current user should throw error, when not loaded', () => {
it('get current user should throw error, when not loaded', () => {
expect(() => service.getCurrentUser()).toThrowError('user should not be undefined here');
});

test('init current user should load user', (done) => {
it('init current user should load user', (done) => {
expect(() => service.getCurrentUser()).toThrowError('user should not be undefined here');
service.getOrInitCurrentUser().subscribe(() => {
expect(service.getCurrentUser()).toBe(users[0]);
Expand All @@ -51,4 +51,28 @@ describe('UserService', () => {
const req = httpMock.expectOne('api/v1/users/current');
req.flush(users[0]);
});

it('setIsOkrChampion should call put operation, reloadUsers and reloadCurrentUser', fakeAsync(() => {
service.setIsOkrChampion(testUser, true).subscribe();
const req = httpMock.expectOne(`api/v1/users/${testUser.id}/isokrchampion/true`);
req.flush(users[0]);

tick();

const req2 = httpMock.expectOne(`api/v1/users`);
const req3 = httpMock.expectOne(`api/v1/users/current`);
req2.flush({});
req3.flush({});
}));

it('createUsers should call createAll and reloadUsers', fakeAsync(() => {
service.createUsers(users).subscribe();
const req = httpMock.expectOne(`api/v1/users/createall`);
req.flush(users);

tick();

const req2 = httpMock.expectOne(`api/v1/users`);
req2.flush({});
}));
});
12 changes: 11 additions & 1 deletion frontend/src/app/services/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, of, tap } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { User } from '../shared/types/model/User';
import { NewUser } from '../shared/types/model/NewUser';

@Injectable({
providedIn: 'root',
Expand Down Expand Up @@ -50,6 +51,15 @@ export class UserService {
}

setIsOkrChampion(user: User, isOkrChampion: boolean) {
return this.httpClient.put(`${this.API_URL}/${user.id}/isokrchampion/${isOkrChampion}`, {});
return this.httpClient.put(`${this.API_URL}/${user.id}/isokrchampion/${isOkrChampion}`, {}).pipe(
tap(() => {
this.reloadUsers();
this.reloadCurrentUser().subscribe();
}),
);
}

createUsers(userList: NewUser[]) {
return this.httpClient.post<User>(`${this.API_URL}/createall`, userList).pipe(tap(() => this.reloadUsers()));
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,36 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';

import { InviteUserDialogComponent } from './invite-user-dialog.component';
import { MatDialogModule } from '@angular/material/dialog';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { NewUserComponent } from '../new-user/new-user.component';
import { PuzzleIconComponent } from '../../shared/custom/puzzle-icon/puzzle-icon.component';
import { PuzzleIconButtonComponent } from '../../shared/custom/puzzle-icon-button/puzzle-icon-button.component';
import { UserService } from '../../services/user.service';
import { testUser } from '../../shared/testData';
import { DialogRef } from '@angular/cdk/dialog';
import { of } from 'rxjs';

describe('InviteUserDialogComponent', () => {
let component: InviteUserDialogComponent;
let fixture: ComponentFixture<InviteUserDialogComponent>;

const userServiceMock = {
createUsers: jest.fn(),
};

const dialogRefMock = {
close: jest.fn(),
};

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [InviteUserDialogComponent, NewUserComponent, PuzzleIconComponent, PuzzleIconButtonComponent],
imports: [MatDialogModule, FormsModule, ReactiveFormsModule],
providers: [
{ provide: UserService, useValue: userServiceMock },
{ provide: DialogRef, useValue: dialogRefMock },
],
}).compileComponents();

fixture = TestBed.createComponent(InviteUserDialogComponent);
Expand All @@ -41,4 +57,14 @@ describe('InviteUserDialogComponent', () => {

expect(component.users).toStrictEqual([user1, user3]);
});

it('inviteUsers should call createUsers and close dialog', fakeAsync(() => {
userServiceMock.createUsers.mockReturnValue(of([testUser]));
component.inviteUsers();
tick();

expect(userServiceMock.createUsers).toBeCalledTimes(1);
expect(userServiceMock.createUsers).toBeCalledWith(component.users);
expect(dialogRefMock.close).toBeCalledTimes(1);
}));
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Component, ViewChild } from '@angular/core';
import { NewUser } from '../../shared/types/model/NewUser';
import { NgForm } from '@angular/forms';
import { UserService } from '../../services/user.service';
import { DialogRef } from '@angular/cdk/dialog';

@Component({
selector: 'app-invite-user-dialog',
Expand All @@ -14,9 +16,14 @@ export class InviteUserDialogComponent {

users: NewUser[] = [{ ...this.emptyUser }];

constructor() {}
constructor(
private readonly userService: UserService,
private readonly dialogRef: DialogRef,
) {}

inviteUsers() {}
inviteUsers() {
this.userService.createUsers(this.users).subscribe(() => this.dialogRef.close());
}

addUser() {
this.users.push({ ...this.emptyUser });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,7 @@ export class MemberDetailComponent implements OnInit, OnDestroy {
isOkrChampionChange(okrChampion: boolean, user: User) {
this.userService.setIsOkrChampion(user, okrChampion).subscribe(() => {
this.loadUser(user.id);
this.userService.reloadUsers();
this.teamService.reloadTeams();
this.userService.reloadCurrentUser().subscribe();
});
}
}
13 changes: 10 additions & 3 deletions frontend/src/app/team-management/new-user/new-user.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
@if (showError(firstName)) {
<mat-error>
@if (firstName.invalid) {
<span>Ungültige Angabe</span>
<span>Angabe benötigt</span>
}
</mat-error>
}
Expand All @@ -41,7 +41,7 @@
@if (showError(lastName)) {
<mat-error>
@if (lastName.invalid) {
<span>Ungültige Angabe</span>
<span>Angabe benötigt</span>
}
</mat-error>
}
Expand All @@ -60,12 +60,19 @@
class="value-field"
#email="ngModel"
email
appUniqueEmail
[(ngModel)]="user.email"
/>
@if (showError(email)) {
<mat-error>
@if (email.invalid) {
<span>Ungültige Angabe</span>
@if (email.errors?.["required"]) {
<span>Angabe benötigt</span>
} @else if (email.errors?.["notUniqueMail"]) {
<span>E-Mail existiert bereits</span>
} @else if (email.errors?.["email"]) {
<span>E-Mail ungültig</span>
}
}
</mat-error>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ import { ControlContainer, NgForm, NgModel } from '@angular/forms';
changeDetection: ChangeDetectionStrategy.Default,
})
export class NewUserComponent implements AfterViewInit {
randNr = Math.round(Math.random() * 10000);

@Input({ required: true })
index!: number;

Expand All @@ -35,8 +33,8 @@ export class NewUserComponent implements AfterViewInit {
this.firstInput.nativeElement.focus();
}

showError(firstName: NgModel) {
return firstName.invalid && (firstName.dirty || firstName.touched);
showError(control: NgModel) {
return control.invalid && (control.dirty || control.touched);
}

remove() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { UniqueEmailValidatorDirective } from './unique-mail.directive';
import { users } from '../../shared/testData';
import { TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
import { AbstractControl } from '@angular/forms';

describe('UniqueMailDirective', () => {
const userServiceMock = {
getUsers: jest.fn(),
} as any;

beforeEach(() => {
userServiceMock.getUsers.mockReturnValue(of(users));
});

it('should create an instance', () => {
TestBed.runInInjectionContext(() => {
const directive = new UniqueEmailValidatorDirective(userServiceMock);
expect(directive).toBeTruthy();
});
});

it('should return validationError if user exists, otherwise null', () => {
TestBed.runInInjectionContext(() => {
const directive = new UniqueEmailValidatorDirective(userServiceMock);

let control = { value: users[0].email } as AbstractControl;
expect(directive.validate(control)).toStrictEqual({ notUniqueMail: { value: users[0].email } });

control = { value: '[email protected]' } as AbstractControl;
expect(directive.validate(control)).toStrictEqual(null);

expect(directive).toBeTruthy();
});
});
});
32 changes: 32 additions & 0 deletions frontend/src/app/team-management/new-user/unique-mail.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Directive } from '@angular/core';
import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator } from '@angular/forms';
import { UserService } from '../../services/user.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Directive({
selector: '[appUniqueEmail]',
providers: [
{
provide: NG_VALIDATORS,
useExisting: UniqueEmailValidatorDirective,
multi: true,
},
],
})
export class UniqueEmailValidatorDirective implements Validator {
private existingUserMails: string[] = [];

constructor(private readonly userService: UserService) {
this.userService
.getUsers()
.pipe(takeUntilDestroyed())
.subscribe((users) => {
this.existingUserMails = users.map((u) => u.email);
});
}

validate(control: AbstractControl): ValidationErrors | null {
const existingUser = this.existingUserMails.includes(control.value);
return existingUser ? { notUniqueMail: { value: control.value } } : null;
}
}
2 changes: 2 additions & 0 deletions frontend/src/app/team-management/team-management.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { EditOkrChampionComponent } from './edit-okr-champion/edit-okr-champion.
import { MatTooltipModule } from '@angular/material/tooltip';
import { NewUserComponent } from './new-user/new-user.component';
import { InviteUserDialogComponent } from './invite-user-dialog/invite-user-dialog.component';
import { UniqueEmailValidatorDirective } from './new-user/unique-mail.directive';

@NgModule({
declarations: [
Expand All @@ -64,6 +65,7 @@ import { InviteUserDialogComponent } from './invite-user-dialog/invite-user-dial
EditOkrChampionComponent,
NewUserComponent,
InviteUserDialogComponent,
UniqueEmailValidatorDirective,
],
imports: [
CommonModule,
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/assets/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@
"PUT": "Das Check-in wurde erfolgreich aktualisiert."
},
"USERS": {
"PUT": "Der User wurde erfolgreich aktualisiert."
"PUT": "Der Member wurde erfolgreich aktualisiert.",
"POST": "Die Members wurden erfolgreich eingeladen"
}
},
"DIALOG_ERRORS": {
Expand Down

0 comments on commit b31995c

Please sign in to comment.