diff --git a/features/API-admin-user/Create_new_user.feature b/features/API-admin-user/Create_new_user.feature index 39392156ee..164105724b 100644 --- a/features/API-admin-user/Create_new_user.feature +++ b/features/API-admin-user/Create_new_user.feature @@ -1,42 +1,40 @@ @api-admin-user Feature: Create new user -Background: - Given a logged-in user on the Swagger UI page - Given the user has access to 'email', 'first name' and 'last name' of the user to create - -Scenario: Successfully create new 'disaster-manager' user - Given the user is using the `api/user` endpoint - Given the user has filled in 'email', 'first name' and 'last name' - Given the user also uses the 'email' as 'username' - Given the 'middleName' property is removed (as it's optional) - Given the user has generated a random password using https://passwordsgenerator.net/ and filled it in - Given the user has trimmed the country-list to only the relevant countries (on production: always just 1 country) - Given the user leaves the role on 'disaster-manager' and the status on 'active' - When the user presses 'Execute' - Then a status 201 is returned and an object with 'email' and 'token' properties - -Scenario: Successfully create new 'guest' user - Given the user is using the `api/user` endpoint - Given everything is filled in as in previous scenario, except role = 'guest' - When the user presses 'Execute' - Then a status 201 is returned and an object with 'email' and 'token' properties - --------------------------------- -NOTE: Below scenario is not in the exact right location at the moment, but closely related to above scenarios. - -Scenario: Successfully create new user in Mailchimp 1-by-1 - Given the user is logged in to mailchimp 'IbfSystem' using credentials from Bitwarden - Given the user has navigated via 'Audience' and 'All contacts' and 'Add contacts' to 'Add subscriber' - Given the user has filled in 'email', 'first name' and 'last name' - Given the user has added the right country-tag (e.g. 'Zambia' for 'Zambia') - Given the user has checked the permission checkbox - When clicking 'subscribe' - Then the user appears in the audience with the right tag and the status 'Subscribed' - -Scenario: Successfully create new user in Mailchimp through import - Given the user is logged in to mailchimp 'IbfSystem' using credentials from Bitwarden - When the user has navigated via 'Audience' and 'All contacts' and 'Add contacts' to 'Import contacts' - Then this can be used to achieve the same result as above in bulk + Background: + Given a logged-in user on the Swagger UI page + Given the user has access to 'email', 'first name' and 'last name' of the user to create + + Scenario: Successfully create new 'disaster-manager' user + Given the user is using the `api/user` endpoint + Given the user has filled in 'email', 'first name' and 'last name' + Given the 'middleName' property is removed (as it's optional) + Given the user has generated a random password using https://passwordsgenerator.net/ and filled it in + Given the user has trimmed the country-list to only the relevant countries (on production: always just 1 country) + When the user presses 'Execute' + Then a status 201 is returned and an object with 'email' and 'token' properties + + Scenario: Successfully create new 'guest' user + Given the user is using the `api/user` endpoint + Given everything is filled in as in previous scenario, except role = 'guest' + When the user presses 'Execute' + Then a status 201 is returned and an object with 'email' and 'token' properties + + -------------------------------- + NOTE: Below scenario is not in the exact right location at the moment, but closely related to above scenarios. + + Scenario: Successfully create new user in Mailchimp 1-by-1 + Given the user is logged in to mailchimp 'IbfSystem' using credentials from Bitwarden + Given the user has navigated via 'Audience' and 'All contacts' and 'Add contacts' to 'Add subscriber' + Given the user has filled in 'email', 'first name' and 'last name' + Given the user has added the right country-tag (e.g. 'Zambia' for 'Zambia') + Given the user has checked the permission checkbox + When clicking 'subscribe' + Then the user appears in the audience with the right tag and the status 'Subscribed' + + Scenario: Successfully create new user in Mailchimp through import + Given the user is logged in to mailchimp 'IbfSystem' using credentials from Bitwarden + When the user has navigated via 'Audience' and 'All contacts' and 'Add contacts' to 'Import contacts' + Then this can be used to achieve the same result as above in bulk diff --git a/features/IBF-portal-user/dashboard-page/Use_header_section.feature b/features/IBF-portal-user/dashboard-page/Use_header_section.feature index e85c7c39c2..dfb2446401 100644 --- a/features/IBF-portal-user/dashboard-page/Use_header_section.feature +++ b/features/IBF-portal-user/dashboard-page/Use_header_section.feature @@ -1,38 +1,38 @@ @ibf-portal-user Feature: View and use header section -Background: - Given a logged-in user on the dashboard page - Given logged in for a specific "country" - -Scenario: View header of dashboard page - When the user enters the dashboard page - Then the user sees the Header section at the top of the page - And it shows 'IBF PORTAL' followed by the "country" name, followed by the selected "disaster-type" name - And it contains a Logout button - And it shows "Logged in as" with the user's username - And the username is underlined and clickable - And it contains the logos of the "country" - -Scenario: View header in Triggered mode - When the user is viewing the Header section - Then 'Log-out' button displays in purple color - -Scenario: View header in Non-triggered mode - When the user is viewing the Header section - Then 'Log-out' button displays in navy-blue color - -Scenario: Logout - When the user clicks the "Log Out" button in the header - Then the user get logged out from IBF-portal - And returns to the "login" page - -Scenario: Open the "Change Pasword" form - When the user clicks on the username - Then a popup opens with "Change Password" as title - And the user sees two fields: "New Password" and "Confirm Password" - And the "Change Password" button is disabled - And further scenarios on how to use the popup are in 'Change_password.feature' + Background: + Given a logged-in user on the dashboard page + Given logged in for a specific "country" + + Scenario: View header of dashboard page + When the user enters the dashboard page + Then the user sees the Header section at the top of the page + And it shows 'IBF PORTAL' followed by the "country" name, followed by the selected "disaster-type" name + And it contains a Logout button + And it shows "Logged in as" with the user's email address + And the email address is underlined and clickable + And it contains the logos of the "country" + + Scenario: View header in Triggered mode + When the user is viewing the Header section + Then 'Log-out' button displays in purple color + + Scenario: View header in Non-triggered mode + When the user is viewing the Header section + Then 'Log-out' button displays in navy-blue color + + Scenario: Logout + When the user clicks the "Log Out" button in the header + Then the user get logged out from IBF-portal + And returns to the "login" page + + Scenario: Open the "Change Password" form + When the user clicks on the email address + Then a popup opens with "Change Password" as title + And the user sees two fields: "New Password" and "Confirm Password" + And the "Change Password" button is disabled + And further scenarios on how to use the popup are in 'Change_password.feature' diff --git a/interfaces/IBF-dashboard/src/app/auth/auth.service.ts b/interfaces/IBF-dashboard/src/app/auth/auth.service.ts index fa6581e939..66519fd17d 100644 --- a/interfaces/IBF-dashboard/src/app/auth/auth.service.ts +++ b/interfaces/IBF-dashboard/src/app/auth/auth.service.ts @@ -79,12 +79,10 @@ export class AuthService implements OnDestroy { const user: User = { token: rawToken, email: decodedToken.email, - username: decodedToken.username, firstName: decodedToken.firstName, middleName: decodedToken.middleName, lastName: decodedToken.lastName, userRole: decodedToken.userRole, - userStatus: decodedToken.userStatus, countries: decodedToken.countries, disasterTypes: decodedToken.disasterTypes, }; @@ -93,13 +91,13 @@ export class AuthService implements OnDestroy { return user; } - public login(email, password) { + public login(email: string, password: string) { return this.apiService .login(email, password) .subscribe(this.onLoginResponse, this.onLoginError); } - private onLoginResponse = (response) => { + private onLoginResponse = (response: { user: User }) => { if (!response.user?.token) { return; } @@ -129,7 +127,7 @@ export class AuthService implements OnDestroy { message: `Authentication Failed: ${message}`, duration: 5000, }); - toast.present(); + void toast.present(); console.error('AuthService error: ', error); }; @@ -161,7 +159,7 @@ export class AuthService implements OnDestroy { message: `Password changed successfully`, duration: 5000, }); - toast.present(); + void toast.present(); }; private onChangePasswordError = async (error) => { @@ -171,7 +169,7 @@ export class AuthService implements OnDestroy { message: `Authentication Failed: ${message}`, duration: 5000, }); - toast.present(); + void toast.present(); console.error('AuthService error: ', error); }; } diff --git a/interfaces/IBF-dashboard/src/app/models/user/user-status.enum.ts b/interfaces/IBF-dashboard/src/app/models/user/user-status.enum.ts deleted file mode 100644 index 6c70134c9b..0000000000 --- a/interfaces/IBF-dashboard/src/app/models/user/user-status.enum.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum UserStatus { - Active = 'active', - Inactive = 'inactive', -} diff --git a/interfaces/IBF-dashboard/src/app/models/user/user.model.ts b/interfaces/IBF-dashboard/src/app/models/user/user.model.ts index 483249edba..fa797a1aaf 100644 --- a/interfaces/IBF-dashboard/src/app/models/user/user.model.ts +++ b/interfaces/IBF-dashboard/src/app/models/user/user.model.ts @@ -1,15 +1,12 @@ import { UserRole } from 'src/app/models/user/user-role.enum'; -import { UserStatus } from 'src/app/models/user/user-status.enum'; export class User { token: string; email: string; - username: string; firstName: string; middleName?: string; lastName: string; userRole: UserRole; countries: string[]; disasterTypes: string[]; - userStatus: UserStatus; } diff --git a/interfaces/IBF-dashboard/src/app/services/api.service.ts b/interfaces/IBF-dashboard/src/app/services/api.service.ts index 6b973aacd8..a81ecc354b 100644 --- a/interfaces/IBF-dashboard/src/app/services/api.service.ts +++ b/interfaces/IBF-dashboard/src/app/services/api.service.ts @@ -122,7 +122,7 @@ export class ApiService { // API-endpoints: ///////////////////////////////////////////////////////////////////////////// - login(email: string, password: string): Observable { + login(email: string, password: string): Observable<{ user: User }> { this.log('ApiService : login()'); return this.post( diff --git a/interfaces/IBF-dashboard/src/app/services/jwt.service.ts b/interfaces/IBF-dashboard/src/app/services/jwt.service.ts index a8830494cc..1228cd5e26 100644 --- a/interfaces/IBF-dashboard/src/app/services/jwt.service.ts +++ b/interfaces/IBF-dashboard/src/app/services/jwt.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@angular/core'; import { JwtHelperService } from '@auth0/angular-jwt'; +import { User } from 'src/app/models/user/user.model'; @Injectable({ providedIn: 'root', @@ -20,8 +21,7 @@ export class JwtService { window.localStorage.removeItem(this.tokenKey); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public decodeToken(rawToken: string): any { + public decodeToken(rawToken: string): User { return this.jwtHelper.decodeToken(rawToken); } diff --git a/services/API-service/migration/1738064090018-RemoveUserProperties.ts b/services/API-service/migration/1738064090018-RemoveUserProperties.ts new file mode 100644 index 0000000000..ff79bb57b8 --- /dev/null +++ b/services/API-service/migration/1738064090018-RemoveUserProperties.ts @@ -0,0 +1,29 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RemoveUserProperties1738064090018 implements MigrationInterface { + name = 'RemoveUserProperties1738064090018'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "IBF-app"."user" DROP CONSTRAINT "UQ_09cdc2f534910e14de7705815a8"`, + ); + await queryRunner.query( + `ALTER TABLE "IBF-app"."user" DROP COLUMN "username"`, + ); + await queryRunner.query( + `ALTER TABLE "IBF-app"."user" DROP COLUMN "userStatus"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "IBF-app"."user" ADD "userStatus" character varying NOT NULL DEFAULT 'active'`, + ); + await queryRunner.query( + `ALTER TABLE "IBF-app"."user" ADD "username" character varying NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "IBF-app"."user" ADD CONSTRAINT "UQ_09cdc2f534910e14de7705815a8" UNIQUE ("username")`, + ); + } +} diff --git a/services/API-service/src/api/user/dto/create-user.dto.ts b/services/API-service/src/api/user/dto/create-user.dto.ts index 248a03efcd..4c3d398345 100644 --- a/services/API-service/src/api/user/dto/create-user.dto.ts +++ b/services/API-service/src/api/user/dto/create-user.dto.ts @@ -4,7 +4,6 @@ import { ArrayNotEmpty, IsArray, IsEmail, - IsEnum, IsIn, IsNotEmpty, IsOptional, @@ -15,9 +14,10 @@ import { import countries from '../../../scripts/json/countries.json'; import disasterTypes from '../../../scripts/json/disasters.json'; import { UserRole } from '../user-role.enum'; -import { UserStatus } from '../user-status.enum'; -const userRoleArray = Object.values(UserRole).map((item) => String(item)); +export const userRoleArray = Object.values(UserRole).map((item) => + String(item), +); export class CreateUserDto { @ApiProperty({ example: 'dunant@redcross.nl' }) @@ -25,11 +25,6 @@ export class CreateUserDto { @IsNotEmpty() public email: string; - @ApiProperty({ example: 'dunant' }) - @IsString() - @IsNotEmpty() - public username: string; - @ApiProperty({ example: 'Henry' }) @IsString() @IsNotEmpty() @@ -69,14 +64,6 @@ export class CreateUserDto { @ArrayNotEmpty() public disasterTypes: string[]; - @ApiProperty({ - example: UserStatus.Active, - default: UserStatus.Inactive, - }) - @IsEnum(UserStatus) - @IsNotEmpty() - public status: UserStatus; - @ApiProperty({ example: 'password' }) @IsNotEmpty() @MinLength(4) diff --git a/services/API-service/src/api/user/dto/update-user.dto.ts b/services/API-service/src/api/user/dto/update-user.dto.ts new file mode 100644 index 0000000000..95741084f3 --- /dev/null +++ b/services/API-service/src/api/user/dto/update-user.dto.ts @@ -0,0 +1,36 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import { IsIn, IsOptional, IsString } from 'class-validator'; + +import { UserRole } from '../user-role.enum'; +import { userRoleArray } from './create-user.dto'; + +export class UpdateUserDto { + @ApiProperty({ example: 'Henry' }) + @IsOptional() + @IsString() + public firstName?: string; + + @ApiProperty({ example: 'Middle name' }) + @IsOptional() + @IsString() + public middleName?: string; + + @ApiProperty({ example: 'Dunant' }) + @IsString() + @IsOptional() + public lastName?: string; + + @ApiProperty({ + enum: userRoleArray, + example: userRoleArray.join(' | '), + }) + @IsIn(userRoleArray) + @IsOptional() + public role?: UserRole; + + @ApiProperty({ example: '+31600000000' }) + @IsString() + @IsOptional() + public whatsappNumber?: string; +} diff --git a/services/API-service/src/api/user/user-status.enum.ts b/services/API-service/src/api/user/user-status.enum.ts deleted file mode 100644 index 6c70134c9b..0000000000 --- a/services/API-service/src/api/user/user-status.enum.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum UserStatus { - Active = 'active', - Inactive = 'inactive', -} diff --git a/services/API-service/src/api/user/user.controller.ts b/services/API-service/src/api/user/user.controller.ts index afebd1343b..b27a20c475 100644 --- a/services/API-service/src/api/user/user.controller.ts +++ b/services/API-service/src/api/user/user.controller.ts @@ -2,7 +2,9 @@ import { Body, Controller, HttpStatus, + Patch, Post, + Query, UseGuards, UsePipes, } from '@nestjs/common'; @@ -10,6 +12,7 @@ import { HttpException } from '@nestjs/common/exceptions/http.exception'; import { ApiBearerAuth, ApiOperation, + ApiQuery, ApiResponse, ApiTags, } from '@nestjs/swagger'; @@ -18,6 +21,7 @@ import { Roles } from '../../roles.decorator'; import { RolesGuard } from '../../roles.guard'; import { ValidationPipe } from '../../shared/pipes/validation.pipe'; import { CreateUserDto, LoginUserDto, UpdatePasswordDto } from './dto'; +import { UpdateUserDto } from './dto/update-user.dto'; import { UserRole } from './user-role.enum'; import { UserDecorator } from './user.decorator'; import { UserResponseObject } from './user.model'; @@ -64,20 +68,11 @@ export class UserController { public async login( @Body() loginUserDto: LoginUserDto, ): Promise { - const _user = await this.userService.findOne(loginUserDto); - if (!_user) { + const user = await this.userService.findOne(loginUserDto); + if (!user) { throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED); } - - const token = await this.userService.generateJWT(_user); - const { email, userRole } = _user; - const user = { - email, - token, - userRole, - }; - - return { user }; + return await this.userService.buildUserRO(user); } @ApiBearerAuth() @@ -88,6 +83,19 @@ export class UserController { @UserDecorator('userId') loggedInUserId: string, @Body() userData: UpdatePasswordDto, ) { - return this.userService.update(loggedInUserId, userData); + return this.userService.updatePassword(loggedInUserId, userData); + } + + @Roles(UserRole.Admin) + @ApiBearerAuth() + @UseGuards(RolesGuard) + @ApiOperation({ summary: 'Update user properties' }) + @ApiQuery({ name: 'email', required: true, type: 'string' }) + @Patch() + public async updateUser( + @Body() updateUserData: UpdateUserDto, + @Query('email') email: string, + ): Promise { + return this.userService.updateUser(email, updateUserData); } } diff --git a/services/API-service/src/api/user/user.entity.ts b/services/API-service/src/api/user/user.entity.ts index e7de74318f..80a04851d3 100644 --- a/services/API-service/src/api/user/user.entity.ts +++ b/services/API-service/src/api/user/user.entity.ts @@ -16,7 +16,6 @@ import { DisasterEntity } from '../disaster/disaster.entity'; import { EapActionStatusEntity } from '../eap-actions/eap-action-status.entity'; import { EventPlaceCodeEntity } from '../event/event-place-code.entity'; import { UserRole } from './user-role.enum'; -import { UserStatus } from './user-status.enum'; @Entity('user') export class UserEntity { @@ -30,9 +29,6 @@ export class UserEntity { @Column({ nullable: true }) public whatsappNumber: string; - @Column({ unique: true }) - public username: string; - @Column() public firstName: string; @@ -79,9 +75,6 @@ export class UserEntity { }) public disasterTypes: DisasterEntity[]; - @Column({ default: UserStatus.Active }) - public userStatus: UserStatus; - @Column({ select: false }) public password: string; diff --git a/services/API-service/src/api/user/user.model.ts b/services/API-service/src/api/user/user.model.ts index e0822aed5d..64651b14c0 100644 --- a/services/API-service/src/api/user/user.model.ts +++ b/services/API-service/src/api/user/user.model.ts @@ -1,17 +1,14 @@ import { ApiProperty } from '@nestjs/swagger'; import { UserRole } from './user-role.enum'; -import { UserStatus } from './user-status.enum'; export class User { public userId: string; public email: string; - public username: string; public firstName: string; public middleName?: string; public lastName: string; public userRole: UserRole; - public userStatus: UserStatus; public countries: string[]; public disasterTypes: string[]; public exp: number; @@ -22,16 +19,26 @@ export class UserData { @ApiProperty({ example: 'dunant@redcross.nl' }) public email: string; + @ApiProperty({ example: 'Henry' }) + public firstName: string; + + @ApiProperty({ default: null }) + public middleName?: string; + + @ApiProperty({ example: 'Dunant' }) + public lastName: string; + + @ApiProperty({ example: UserRole.DisasterManager }) + public userRole: UserRole; + + @ApiProperty({ example: '+31600000000' }) + public whatsappNumber: string; + @ApiProperty({ example: 'this-is-an-example-token-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI1YmI2ZDFmZi1jMzMyLTRiZGEtOGIyMi0zNTA1ZjE0MTA5MDYiLCJlbWFpbCI6ImR1bmFudEByZWRjcm9zcy5ubCIsInVzZXJuYW1lIjoiZHVuYW50IiwiZmlyc3ROYW1lIjoiSGVucnkiLCJtaWRkbGVOYW1lIjpudWxsLCJsYXN0TmFtZSI6IkR1bmFudCIsInVzZXJSb2xlIjoiYWRtaW4iLCJ1c2VyU3RhdHVzIjoiYWN0aXZlIiwiY291bnRyaWVzIjpbIlVHQSIsIlpNQiIsIktFTiIsIkVUSCIsIkVHWSIsIlBITCJdLCJleHAiOjE2MjgyNDE4NDEuODg5LCJpYXQiOjE2MjMwNTc4NDF9.HNRmmrlKjASGjPIdSVDqpYKbWyXrrpr53iqof9tx2PU', }) - public token: string; - - @ApiProperty({ - example: UserRole.DisasterManager, - }) - public userRole: UserRole; + public token?: string; } export class UserResponseObject { diff --git a/services/API-service/src/api/user/user.service.spec.ts b/services/API-service/src/api/user/user.service.spec.ts index c72159bbb5..44102ac2c9 100644 --- a/services/API-service/src/api/user/user.service.spec.ts +++ b/services/API-service/src/api/user/user.service.spec.ts @@ -6,7 +6,6 @@ import { DisasterType } from '../disaster/disaster-type.enum'; import { DisasterEntity } from '../disaster/disaster.entity'; import { LookupService } from '../notification/lookup/lookup.service'; import { UserRole } from './user-role.enum'; -import { UserStatus } from './user-status.enum'; import { UserEntity } from './user.entity'; import { UserService } from './user.service'; @@ -30,14 +29,12 @@ const user: UserEntity = { userId: '1', email: 'test@example.org', whatsappNumber: '+3100000000', - username: 'test@example.org', firstName: 'Test', middleName: 'User', lastName: 'Example', userRole: UserRole.DisasterManager, countries: [], - disasterTypes: disasters, - userStatus: UserStatus.Active, + disasterTypes: disasters, // NOTE: if this is passed as empty array, a mock for disasterRepository.find() is needed password: '', created: new Date(), hashPassword: function (): void { @@ -54,12 +51,30 @@ describe('UserService', () => { userService = new UserService(new LookupService()); }); - describe('generateJWT', () => { - it('should generate a JWT token of type string and starting with the characters "eyJ"', async () => { - const generated = await userService.generateJWT(user); + describe('buildUserRO', () => { + it('should generate an object including a JWT token starting with the characters "eyJ"', async () => { + // Arrange + const includeToken = true; + + // Act + const userRO = await userService.buildUserRO(user, includeToken); + + // Assert const expectedFirstCharacters = 'eyJ'; - expect(typeof generated).toBe('string'); - expect(generated.indexOf(expectedFirstCharacters)).toBe(0); + expect(userRO.user.token).toBeDefined(); + expect(userRO.user.token?.indexOf(expectedFirstCharacters)).toBe(0); + }); + + it('should generate an object without a JWT token when instructed as such', async () => { + // Arrange + const includeToken = false; + + // Act + const userRO = await userService.buildUserRO(user, includeToken); + + // Assert + expect(userRO.user).toBeDefined(); + expect(userRO.user.token).not.toBeDefined(); }); }); }); diff --git a/services/API-service/src/api/user/user.service.ts b/services/API-service/src/api/user/user.service.ts index d7fd8153e1..893e43b02f 100644 --- a/services/API-service/src/api/user/user.service.ts +++ b/services/API-service/src/api/user/user.service.ts @@ -11,9 +11,10 @@ import { CountryEntity } from '../country/country.entity'; import { DisasterEntity } from '../disaster/disaster.entity'; import { LookupService } from '../notification/lookup/lookup.service'; import { CreateUserDto, LoginUserDto, UpdatePasswordDto } from './dto'; +import { UpdateUserDto } from './dto/update-user.dto'; import { UserRole } from './user-role.enum'; import { UserEntity } from './user.entity'; -import { UserResponseObject } from './user.model'; +import { UserData, UserResponseObject } from './user.model'; @Injectable() export class UserService { @@ -50,13 +51,12 @@ export class UserService { public async create(dto: CreateUserDto): Promise { const email = dto.email.toLowerCase(); - const username = dto.username.toLowerCase(); const user = await this.userRepository.findOne({ - where: [{ email: email }, { username: username }], + where: { email }, }); if (user) { - const errors = { errors: 'Email and username must be unique.' }; + const errors = { errors: 'Email must be unique.' }; throw new HttpException( { message: 'Input data validation failed', errors }, HttpStatus.BAD_REQUEST, @@ -72,13 +72,11 @@ export class UserService { // create new user const newUser = new UserEntity(); newUser.email = email; - newUser.username = dto.username; newUser.password = dto.password; newUser.firstName = dto.firstName; newUser.middleName = dto.middleName; newUser.lastName = dto.lastName; newUser.userRole = dto.role; - newUser.userStatus = dto.status; newUser.whatsappNumber = dto.whatsappNumber; newUser.countries = await this.countryRepository.find({ where: { countryCodeISO3: In(dto.countryCodesISO3) }, @@ -115,7 +113,7 @@ export class UserService { return this.buildUserRO(user); } - public async update( + public async updatePassword( loggedInUserId: string, dto: UpdatePasswordDto, ): Promise { @@ -155,7 +153,36 @@ export class UserService { return this.buildUserRO(updateUser); } - public async generateJWT(user: UserEntity): Promise { + public async updateUser( + email: string, + updateUserData: UpdateUserDto, + ): Promise { + const user = await this.userRepository.findOne({ + where: { email }, + }); + if (!user) { + const errors = { User: 'Not found' }; + throw new HttpException({ errors }, HttpStatus.NOT_FOUND); + } + + // If nothing to update, raise a 400 Bad Request. + if (Object.keys(updateUserData).length === 0) { + throw new HttpException( + 'Update user error: no attributes supplied to update', + HttpStatus.BAD_REQUEST, + ); + } + + // Overwrite any non-nested attributes of the user (so not countries/disaster-types) + for (const attribute in updateUserData) { + user[attribute] = updateUserData[attribute]; + } + + const savedUser = await this.userRepository.save(user); + return this.buildUserRO(savedUser, false); + } + + private async generateJWT(user: UserEntity): Promise { const today = new Date(); const exp = new Date(today); exp.setDate(today.getDate() + 60); @@ -164,12 +191,10 @@ export class UserService { { userId: user.userId, email: user.email, - username: user.username, firstName: user.firstName, middleName: user.middleName, lastName: user.lastName, userRole: user.userRole, - userStatus: user.userStatus, countries: user.countries.map( (countryEntity): string => countryEntity.countryCodeISO3, ), @@ -186,13 +211,23 @@ export class UserService { return result; } - private async buildUserRO(user: UserEntity): Promise { - const userRO = { + public async buildUserRO( + user: UserEntity, + includeToken = true, + ): Promise { + const userRO: UserData = { email: user.email, - token: await this.generateJWT(user), + firstName: user.firstName, + middleName: user.middleName, + lastName: user.lastName, userRole: user.userRole, + whatsappNumber: user.whatsappNumber, }; + if (includeToken) { + userRO.token = await this.generateJWT(user); + } + return { user: userRO }; } } diff --git a/services/API-service/src/main.ts b/services/API-service/src/main.ts index 5e6bbc7b91..1cb3e28dd8 100644 --- a/services/API-service/src/main.ts +++ b/services/API-service/src/main.ts @@ -20,7 +20,7 @@ async function bootstrap(): Promise { const apiDocumentationFavicon = 'https://www.510.global/wp-content/uploads/2017/09/cropped-510-FLAVICON-01-32x32.png'; const apiDocumentationDescription = - 'This page serves as the documentation of IBF API endpoints, and can also be used for executing API-calls.
To get access:
  • If you have an account:
    • use the `/api/user/login` endpoint below
    • click `Try it out`, fill in your username and password, and click `Execute`
    • copy the resulting `token`-attribute and paste it in the `Authorize` button on the top right of this page.
  • If you do not have an account, contact the IBF Development Team.
  • You can verify your access by using the `check API` endpoints below:
    • `/api` works (also without authenticaition) as long as the API itself works
    • `/api/authentication` only works if you have successfully authorized
'; + 'This page serves as the documentation of IBF API endpoints, and can also be used for executing API-calls.
To get access:
  • If you have an account:
    • use the `/api/user/login` endpoint below
    • click `Try it out`, fill in your email and password, and click `Execute`
    • copy the resulting `token`-attribute and paste it in the `Authorize` button on the top right of this page.
  • If you do not have an account, contact the IBF Development Team.
  • You can verify your access by using the `check API` endpoints below:
    • `/api` works (also without authenticaition) as long as the API itself works
    • `/api/authentication` only works if you have successfully authorized
'; app.setGlobalPrefix('api'); diff --git a/services/API-service/src/scripts/json/users.json b/services/API-service/src/scripts/json/users.json index e1b8de1b0b..19d5b485f2 100644 --- a/services/API-service/src/scripts/json/users.json +++ b/services/API-service/src/scripts/json/users.json @@ -1,122 +1,98 @@ [ { "email": "dunant@redcross.nl", - "username": "dunant", "firstName": "Henry", "lastName": "Dunant", "userRole": "admin", - "password": "password", - "userStatus": "active" + "password": "password" }, { "email": "uganda@redcross.nl", - "username": "uganda", "firstName": "Uganda", "lastName": "Manager", "userRole": "disaster-manager", "password": "password", - "userStatus": "active", "countries": ["UGA"] }, { "email": "zambia@redcross.nl", - "username": "zambia", "firstName": "Zambia", "lastName": "Manager", "userRole": "disaster-manager", "password": "password", - "userStatus": "active", "countries": ["ZMB"] }, { "email": "zimbabwe@redcross.nl", - "username": "zimbabwe", "firstName": "Zimbabwe", "lastName": "Manager", "userRole": "disaster-manager", "password": "password", - "userStatus": "active", "countries": ["ZWE"] }, { "email": "kenya@redcross.nl", - "username": "kenya", "firstName": "Kenya", "lastName": "Manager", "userRole": "disaster-manager", "password": "password", - "userStatus": "active", "countries": ["KEN"] }, { "email": "ethiopia@redcross.nl", - "username": "ethiopia", "firstName": "Ethiopia", "lastName": "Manager", "userRole": "disaster-manager", "password": "password", - "userStatus": "active", "countries": ["ETH"] }, { "email": "philippines@redcross.nl", - "username": "philippines", "firstName": "Philippines", "lastName": "Manager", "userRole": "disaster-manager", "password": "password", - "userStatus": "active", "countries": ["PHL"] }, { "email": "malawi@redcross.nl", - "username": "malawi", "firstName": "Malawi", "lastName": "Manager", "userRole": "disaster-manager", "password": "password", - "userStatus": "active", "countries": ["MWI"] }, { "email": "malawi-flash-floods@redcross.nl", - "username": "malawi-flash-floods", "firstName": "Malawi", "middleName": "Flash Floods", "lastName": "Manager", "userRole": "disaster-manager", "password": "password", - "userStatus": "active", "countries": ["MWI"], "disasterTypes": ["flash-floods"] }, { "email": "southsudan@redcross.nl", - "username": "southsudan", "firstName": "South Sudan", "lastName": "Manager", "userRole": "disaster-manager", "password": "password", - "userStatus": "active", "countries": ["SSD"] }, { "email": "lesotho@redcross.nl", - "username": "lesotho", "firstName": "Lesotho", "lastName": "Manager", "userRole": "disaster-manager", "password": "password", - "userStatus": "active", "countries": ["LSO"] }, { "email": "demo-user@redcross.nl", - "username": "demo-user@redcross.nl", "firstName": "Demo", "lastName": "User", "userRole": "disaster-manager", - "password": "ibf-portal", - "userStatus": "active" + "password": "ibf-portal" } ] diff --git a/services/API-service/src/scripts/seed-helper.ts b/services/API-service/src/scripts/seed-helper.ts index c6aaf0c8f5..c180e65627 100644 --- a/services/API-service/src/scripts/seed-helper.ts +++ b/services/API-service/src/scripts/seed-helper.ts @@ -48,7 +48,7 @@ export class SeedHelper { ) { let q; if (entity.tableName === 'user') { - q = `DELETE FROM \"${repository.metadata.schema}\".\"${entity.tableName}\" WHERE username <> 'dunant'`; + q = `DELETE FROM \"${repository.metadata.schema}\".\"${entity.tableName}\" WHERE email <> 'dunant@redcross.nl'`; } else { q = `TRUNCATE TABLE \"${repository.metadata.schema}\".\"${entity.tableName}\" CASCADE;`; } diff --git a/services/API-service/src/scripts/seed-init.ts b/services/API-service/src/scripts/seed-init.ts index 918e8ce8b0..6aa0418035 100644 --- a/services/API-service/src/scripts/seed-init.ts +++ b/services/API-service/src/scripts/seed-init.ts @@ -18,7 +18,6 @@ import { IndicatorMetadataEntity } from '../api/metadata/indicator-metadata.enti import { LayerMetadataEntity } from '../api/metadata/layer-metadata.entity'; import { NotificationInfoEntity } from '../api/notification/notifcation-info.entity'; import { UserRole } from '../api/user/user-role.enum'; -import { UserStatus } from '../api/user/user-status.enum'; import { UserEntity } from '../api/user/user.entity'; import areasOfFocus from './json/areas-of-focus.json'; import countries from './json/countries.json'; @@ -132,6 +131,7 @@ export class SeedInit implements InterfaceScript { console.log('Seed Users...'); const userRepository = this.dataSource.getRepository(UserEntity); + const dunantEmail = 'dunant@redcross.nl'; let selectedUsers; if (process.env.PRODUCTION_DATA_SERVER === 'yes') { selectedUsers = users.filter((user): boolean => { @@ -140,10 +140,10 @@ export class SeedInit implements InterfaceScript { selectedUsers[0].password = process.env.ADMIN_PASSWORD; } else { const dunantUser = await userRepository.findOne({ - where: { username: 'dunant' }, + where: { email: dunantEmail }, }); if (dunantUser) { - selectedUsers = users.filter((user) => user.username !== 'dunant'); + selectedUsers = users.filter((user) => user.email !== dunantEmail); dunantUser.countries = await countryRepository.find(); await userRepository.save(dunantUser); } else { @@ -155,7 +155,6 @@ export class SeedInit implements InterfaceScript { selectedUsers.map(async (user): Promise => { const userEntity = new UserEntity(); userEntity.email = user.email; - userEntity.username = user.username; userEntity.firstName = user.firstName; userEntity.lastName = user.lastName; userEntity.userRole = user.userRole as UserRole; @@ -177,7 +176,6 @@ export class SeedInit implements InterfaceScript { }; }), }); - userEntity.userStatus = user.userStatus as UserStatus; userEntity.password = user.password; return userEntity; }), diff --git a/services/API-service/src/scripts/seed-prod.ts b/services/API-service/src/scripts/seed-prod.ts index f4a598d380..e909a05458 100644 --- a/services/API-service/src/scripts/seed-prod.ts +++ b/services/API-service/src/scripts/seed-prod.ts @@ -3,7 +3,6 @@ import { Injectable } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { UserRole } from '../api/user/user-role.enum'; -import { UserStatus } from '../api/user/user-status.enum'; import { UserEntity } from '../api/user/user.entity'; import users from './json/users.json'; import { InterfaceScript } from './scripts.module'; @@ -25,12 +24,10 @@ export class SeedProd implements InterfaceScript { const adminUser = new UserEntity(); adminUser.email = user.email; - adminUser.username = user.username; adminUser.firstName = user.firstName; adminUser.lastName = user.lastName; adminUser.userRole = user.userRole as UserRole; adminUser.password = user.password; - adminUser.userStatus = user.userStatus as UserStatus; await userRepository.save(adminUser); } else { diff --git a/tests/e2e/Pages/LoginPage.ts b/tests/e2e/Pages/LoginPage.ts index 6f89cedd4d..de79a2e41a 100644 --- a/tests/e2e/Pages/LoginPage.ts +++ b/tests/e2e/Pages/LoginPage.ts @@ -9,7 +9,7 @@ const welcomeMessageEnglishTranslation = class LoginPage extends DashboardPage { readonly page: Page; - readonly usernameInput: Locator; + readonly emailInput: Locator; readonly passwordInput: Locator; readonly loginButton: Locator; readonly welcomeMessage: Locator; @@ -17,18 +17,18 @@ class LoginPage extends DashboardPage { constructor(page: Page) { super(page); this.page = page; - this.usernameInput = this.page.getByLabel('E-mail'); + this.emailInput = this.page.getByLabel('E-mail'); this.passwordInput = this.page.locator('input[type="password"]'); this.loginButton = this.page.getByRole('button', { name: 'Log in' }); this.welcomeMessage = this.page.getByTestId('login-welcome-message'); } - async login(username?: string, password?: string) { - if (!username || !password) { - throw new Error('Username and password are required'); + async login(email?: string, password?: string) { + if (!email || !password) { + throw new Error('Email and password are required'); } - await this.usernameInput.fill(username); + await this.emailInput.fill(email); await this.passwordInput.fill(password); await this.loginButton.click(); } diff --git a/tests/e2e/README.md b/tests/e2e/README.md index f3ee209268..1566e41333 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -97,24 +97,24 @@ import BasePage from './BasePage'; class LoginPage extends BasePage { readonly page: Page; - readonly usernameInput: Locator; + readonly emailInput: Locator; readonly passwordInput: Locator; readonly loginButton: Locator; constructor(page: Page) { super(page); this.page = page; - this.usernameInput = this.page.getByLabel('E-mail'); + this.emailInput = this.page.getByLabel('E-mail'); this.passwordInput = this.page.locator('input[type="password"]'); this.loginButton = this.page.getByRole('button', { name: 'Log in' }); } - async login(username?: string, password?: string) { - if (!username || !password) { - throw new Error('Username and password are required'); + async login(email?: string, password?: string) { + if (!email || !password) { + throw new Error('Email and password are required'); } - await this.usernameInput.fill(username); + await this.emailInput.fill(email); await this.passwordInput.fill(password); await this.loginButton.click(); } diff --git a/tests/integration/fixtures/users.const.ts b/tests/integration/fixtures/users.const.ts index 97a227e85d..13e1775a8a 100644 --- a/tests/integration/fixtures/users.const.ts +++ b/tests/integration/fixtures/users.const.ts @@ -1,9 +1,7 @@ import { UserRole } from '../helpers/API-service/enum/user-role.enum'; -import { UserStatus } from '../helpers/API-service/enum/user-status.enum'; export const userData = { email: 'dunant@redcross.nl', - username: 'dunant', firstName: 'Henry', middleName: 'string', lastName: 'Dunant', @@ -17,7 +15,6 @@ export const userData = { 'typhoon', 'flash-floods', ], - status: UserStatus.Active, password: 'password', whatsappNumber: '+31612345678', }; diff --git a/tests/integration/helpers/API-service/dto/create-user.dto.ts b/tests/integration/helpers/API-service/dto/create-user.dto.ts index abeffa3325..893e2bf5e8 100644 --- a/tests/integration/helpers/API-service/dto/create-user.dto.ts +++ b/tests/integration/helpers/API-service/dto/create-user.dto.ts @@ -1,16 +1,13 @@ import { UserRole } from '../enum/user-role.enum'; -import { UserStatus } from '../enum/user-status.enum'; export interface CreateUserDto { email: string; - username: string; firstName: string; middleName?: string; lastName: string; role: UserRole; countryCodesISO3: string[]; disasterTypes: string[]; - status: UserStatus; password: string; whatsappNumber: string; } diff --git a/tests/integration/helpers/API-service/dto/update-user.dto.ts b/tests/integration/helpers/API-service/dto/update-user.dto.ts new file mode 100644 index 0000000000..28814bdcf7 --- /dev/null +++ b/tests/integration/helpers/API-service/dto/update-user.dto.ts @@ -0,0 +1,9 @@ +import { UserRole } from '../enum/user-role.enum'; + +export interface UpdateUserDto { + firstName?: string; + middleName?: string; + lastName?: string; + role?: UserRole; + whatsappNumber?: string; +} diff --git a/tests/integration/helpers/API-service/enum/user-status.enum.ts b/tests/integration/helpers/API-service/enum/user-status.enum.ts deleted file mode 100644 index 6c70134c9b..0000000000 --- a/tests/integration/helpers/API-service/enum/user-status.enum.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum UserStatus { - Active = 'active', - Inactive = 'inactive', -} diff --git a/tests/integration/helpers/API-service/json/users.json b/tests/integration/helpers/API-service/json/users.json index 42b33be1dd..5205dda1ca 100644 --- a/tests/integration/helpers/API-service/json/users.json +++ b/tests/integration/helpers/API-service/json/users.json @@ -1,112 +1,90 @@ [ { "email": "dunant@redcross.nl", - "username": "dunant", "firstName": "Henry", "lastName": "Dunant", "userRole": "admin", - "password": "password", - "userStatus": "active" + "password": "password" }, { "email": "uganda@redcross.nl", - "username": "uganda", "firstName": "Uganda", "lastName": "Manager", "userRole": "disaster-manager", "password": "password", - "userStatus": "active", "countries": ["UGA"] }, { "email": "zambia@redcross.nl", - "username": "zambia", "firstName": "Zambia", "lastName": "Manager", "userRole": "disaster-manager", "password": "password", - "userStatus": "active", "countries": ["ZMB"] }, { "email": "zimbabwe@redcross.nl", - "username": "zimbabwe", "firstName": "Zimbabwe", "lastName": "Manager", "userRole": "disaster-manager", "password": "password", - "userStatus": "active", "countries": ["ZWE"] }, { "email": "kenya@redcross.nl", - "username": "kenya", "firstName": "Kenya", "lastName": "Manager", "userRole": "disaster-manager", "password": "password", - "userStatus": "active", "countries": ["KEN"] }, { "email": "ethiopia@redcross.nl", - "username": "ethiopia", "firstName": "Ethiopia", "lastName": "Manager", "userRole": "disaster-manager", "password": "password", - "userStatus": "active", "countries": ["ETH"] }, { "email": "philippines@redcross.nl", - "username": "philippines", "firstName": "Philippines", "lastName": "Manager", "userRole": "disaster-manager", "password": "password", - "userStatus": "active", "countries": ["PHL"] }, { "email": "malawi@redcross.nl", - "username": "malawi", "firstName": "Malawi", "lastName": "Manager", "userRole": "disaster-manager", "password": "password", - "userStatus": "active", "countries": ["MWI"] }, { "email": "malawi-flash-floods@redcross.nl", - "username": "malawi-flash-floods", "firstName": "Malawi", "middleName": "Flash Floods", "lastName": "Manager", "userRole": "disaster-manager", "password": "password", - "userStatus": "active", "countries": ["MWI"], "disasterTypes": ["flash-floods"] }, { "email": "southsudan@redcross.nl", - "username": "southsudan", "firstName": "South Sudan", "lastName": "Manager", "userRole": "disaster-manager", "password": "password", - "userStatus": "active", "countries": ["SSD"] }, { "email": "demo-user@redcross.nl", - "username": "demo-user@redcross.nl", "firstName": "Demo", "lastName": "User", "userRole": "disaster-manager", - "password": "ibf-portal", - "userStatus": "active" + "password": "ibf-portal" } ] diff --git a/tests/integration/helpers/utility.helper.ts b/tests/integration/helpers/utility.helper.ts index 1dea4d5738..c57df160b8 100644 --- a/tests/integration/helpers/utility.helper.ts +++ b/tests/integration/helpers/utility.helper.ts @@ -2,6 +2,7 @@ import * as request from 'supertest'; import TestAgent from 'supertest/lib/agent'; import { CreateUserDto } from './API-service/dto/create-user.dto'; +import { UpdateUserDto } from './API-service/dto/update-user.dto'; import { UploadTyphoonTrackDto } from './API-service/dto/upload-typhoon-track.dto'; import { DisasterType } from './API-service/enum/disaster-type.enum'; import { @@ -181,6 +182,18 @@ export function changePassword( .send({ email, password }); } +export function updateUser( + email: string, + updateUserData: UpdateUserDto, + accessToken: string, +): Promise { + return getServer() + .patch('/user') + .set('Authorization', `Bearer ${accessToken}`) + .query({ email }) + .send(updateUserData); +} + // Start splitting this up into multiple helper files export function getTyphoonTrack( countryCodeISO3: string, diff --git a/tests/integration/tests/users/change-password.test.ts b/tests/integration/tests/users/change-password.test.ts deleted file mode 100644 index 576d10063d..0000000000 --- a/tests/integration/tests/users/change-password.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { userData } from '../../fixtures/users.const'; -import { - changePassword, - getAccessToken, - loginUser, - resetDB, -} from '../../helpers/utility.helper'; - -describe('change password of user ..', () => { - let accessToken: string; - - beforeAll(async () => { - accessToken = await getAccessToken(); - await resetDB(accessToken); - }); - - it('successfully and log-in with it', async () => { - // Arrange - const newPassword = 'new-password'; - - // Act - const changePasswordResult = await changePassword( - userData.email, - newPassword, - accessToken, - ); - const loginResult = await loginUser(userData.email, newPassword); - - // Assert - expect(changePasswordResult.status).toBe(201); - expect(loginResult.status).toBe(201); - - // Clean up: change password back as subsequent tests will fail otherwise - // REFACTOR: This is a code smell. We should not have to clean up after a test, but this is a pretty specific case. - const changePasswordBackResult = await changePassword( - userData.email, - userData.password, - accessToken, - ); - expect(changePasswordBackResult.status).toBe(201); - }); - - it('fail for unrecognized user', async () => { - // Arrange - const email = 'unexisting-user@redcross.nl'; - const newPassword = 'new-password'; - - // Act - const changePasswordResult = await changePassword( - email, - newPassword, - accessToken, - ); - - // Assert - expect(changePasswordResult.status).toBe(404); - }); -}); diff --git a/tests/integration/tests/users/create-user.test.ts b/tests/integration/tests/users/create-user.test.ts deleted file mode 100644 index b7cd57c084..0000000000 --- a/tests/integration/tests/users/create-user.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { userData } from '../../fixtures/users.const'; -import { UserRole } from '../../helpers/API-service/enum/user-role.enum'; -import { - createUser, - getAccessToken, - loginUser, - resetDB, -} from '../../helpers/utility.helper'; - -describe('create user', () => { - let accessToken: string; - - beforeAll(async () => { - accessToken = await getAccessToken(); - await resetDB(accessToken); - }); - - it('successfully, and log-in with it', async () => { - // Arrange - const newUserData = structuredClone(userData); - newUserData.email = 'new-user@redcross.nl'; - newUserData.username = 'new-user'; - - // Act - const createResult = await createUser(newUserData, accessToken); - const loginResult = await loginUser( - createResult.body.user.email, - newUserData.password, - ); - - // Assert - expect(createResult.status).toBe(201); - expect(createResult.body.user.userRole).toBe(UserRole.DisasterManager); - - expect(loginResult.status).toBe(201); - }); - - it('fails when email or username exists already', async () => { - // Arrange - const existingUserData = structuredClone(userData); - - // Act - const createResult = await createUser(existingUserData, accessToken); - - // Assert - expect(createResult.status).toBe(400); - }); -}); diff --git a/tests/integration/tests/users/manage-users.test.ts b/tests/integration/tests/users/manage-users.test.ts new file mode 100644 index 0000000000..38e202af09 --- /dev/null +++ b/tests/integration/tests/users/manage-users.test.ts @@ -0,0 +1,149 @@ +import { userData } from '../../fixtures/users.const'; +import { UserRole } from '../../helpers/API-service/enum/user-role.enum'; +import { + changePassword, + createUser, + getAccessToken, + loginUser, + resetDB, + updateUser, +} from '../../helpers/utility.helper'; + +describe('manage users', () => { + let accessToken: string; + + beforeAll(async () => { + accessToken = await getAccessToken(); + await resetDB(accessToken); + }); + + describe('create user', () => { + it('should create user successfully and log-in with it', async () => { + // Arrange + const newUserData = structuredClone(userData); + newUserData.email = 'new-user@redcross.nl'; + + // Act + const createResult = await createUser(newUserData, accessToken); + const loginResult = await loginUser( + createResult.body.user.email, + newUserData.password, + ); + + // Assert + expect(createResult.status).toBe(201); + expect(createResult.body.user.userRole).toBe(UserRole.DisasterManager); + + expect(loginResult.status).toBe(201); + }); + + it('should fail when email already exists', async () => { + // Arrange + const existingUserData = structuredClone(userData); + + // Act + const createResult = await createUser(existingUserData, accessToken); + + // Assert + expect(createResult.status).toBe(400); + }); + }); + + describe('update user properties', () => { + it('should successfully update properties', async () => { + // Arrange + const email = userData.email; + const newFirstName = 'new-first-name'; + const newUserRole = UserRole.Admin; // Don't actually change the role, to not mess up other tests, but at least test that it is possible + const updatedData = { firstName: newFirstName, role: newUserRole }; + + // Act + const updateUserResult = await updateUser( + email, + updatedData, + accessToken, + ); + + // Assert + expect(updateUserResult.status).toBe(200); + expect(updateUserResult.body.user.firstName).toBe(newFirstName); + }); + + it('should throw NOT_FOUND on unknown email', async () => { + // Arrange + const email = 'unkown-email@redcross.nl'; + const updatedData = { firstName: 'new-first-name' }; + + // Act + const updateUserResult = await updateUser( + email, + updatedData, + accessToken, + ); + + // Assert + expect(updateUserResult.status).toBe(404); + }); + + it('should throw BAD_REQUEST on no passed arguments', async () => { + // Arrange + const email = userData.email; + const updatedData = {}; + + // Act + const updateUserResult = await updateUser( + email, + updatedData, + accessToken, + ); + + // Assert + expect(updateUserResult.status).toBe(400); + }); + }); + + describe('change password', () => { + it('should fail for unrecognized user', async () => { + // Arrange + const email = 'unexisting-user@redcross.nl'; + const newPassword = 'new-password'; + + // Act + const changePasswordResult = await changePassword( + email, + newPassword, + accessToken, + ); + + // Assert + expect(changePasswordResult.status).toBe(404); + }); + + // Make sure this test is last in its beforeAll block, as it changes the password, which would make subsequent tests fail + it('should successfully change password and log-in with it', async () => { + // Arrange + const newPassword = 'new-password'; + + // Act + const changePasswordResult = await changePassword( + userData.email, + newPassword, + accessToken, + ); + const loginResult = await loginUser(userData.email, newPassword); + + // Assert + expect(changePasswordResult.status).toBe(201); + expect(loginResult.status).toBe(201); + + // Clean up: change password back as subsequent tests will fail otherwise + // REFACTOR: This is a code smell. We should not have to clean up after a test, but this is a pretty specific case. + const changePasswordBackResult = await changePassword( + userData.email, + userData.password, + accessToken, + ); + expect(changePasswordBackResult.status).toBe(201); + }); + }); +});