diff --git a/core/pom.xml b/core/pom.xml index 1e2d6b3b24ed8ac4a1a590a0b9eda2e6f1cb4454..e78c7f03283df5d54c8caf961cf54834fa69743f 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -23,6 +23,11 @@ <artifactId>postgresql</artifactId> <version>42.5.4</version> </dependency> + <dependency> + <groupId>com.h2database</groupId> + <artifactId>h2</artifactId> + <scope>test</scope> + </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> @@ -41,12 +46,6 @@ <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> - <dependency> - <groupId>org.testng</groupId> - <artifactId>testng</artifactId> - <version>RELEASE</version> - <scope>test</scope> - </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> @@ -64,23 +63,6 @@ <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> - <plugin> - <groupId>org.springdoc</groupId> - <artifactId>springdoc-openapi-maven-plugin</artifactId> - <executions> - <execution> - <id>integration-test</id> - <goals> - <goal>generate</goal> - </goals> - </execution> - </executions> - <configuration> - <apiDocsUrl>http://localhost:8080/openapi.yaml</apiDocsUrl> - <outputFileName>openapi.yaml</outputFileName> - <outputDir>..</outputDir> - </configuration> - </plugin> </plugins> </build> </project> \ No newline at end of file diff --git a/core/src/main/java/cz/muni/fi/pa165/core/CoreApplication.java b/core/src/main/java/cz/muni/fi/pa165/core/CoreApplication.java index 1e1b34ad31e1affcd5bf9fbc97db09598e5ec7f7..287eb56c6d4d40ac0b567ca864f43fd32ea77847 100644 --- a/core/src/main/java/cz/muni/fi/pa165/core/CoreApplication.java +++ b/core/src/main/java/cz/muni/fi/pa165/core/CoreApplication.java @@ -2,6 +2,8 @@ package cz.muni.fi.pa165.core; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; @SpringBootApplication public class CoreApplication { diff --git a/core/src/main/java/cz/muni/fi/pa165/core/DataInitializer.java b/core/src/main/java/cz/muni/fi/pa165/core/DataInitializer.java index 160775cf406e00d5e3961f13abe469d37a946674..fe715833b08ae436d0c6f5c695b8ddb0914a91d9 100644 --- a/core/src/main/java/cz/muni/fi/pa165/core/DataInitializer.java +++ b/core/src/main/java/cz/muni/fi/pa165/core/DataInitializer.java @@ -23,9 +23,8 @@ import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; - -@RequiredArgsConstructor @Component +@RequiredArgsConstructor public class DataInitializer implements ApplicationRunner { private final UserService userService; private final DeviceService deviceService; diff --git a/core/src/main/java/cz/muni/fi/pa165/core/common/DomainFacade.java b/core/src/main/java/cz/muni/fi/pa165/core/common/DomainFacade.java index 06830e0770daf4cbc0db6ebf20a64d7ef9b0c48b..223739051b446989f73445815f87b519bbae3c0b 100644 --- a/core/src/main/java/cz/muni/fi/pa165/core/common/DomainFacade.java +++ b/core/src/main/java/cz/muni/fi/pa165/core/common/DomainFacade.java @@ -91,11 +91,7 @@ public abstract class DomainFacade<E extends DomainObject, * @return the DTO representation of the created entity */ public T create(C createDto) { - try { - return mapper.toDto(service.create(mapper.fromCreateDto(createDto))); - } catch (DataIntegrityViolationException e) { - throw new ResponseStatusException(HttpStatus.CONFLICT, "Entity with the same name already exists", e); - } + return mapper.toDto(service.create(mapper.fromCreateDto(createDto))); } /** diff --git a/core/src/main/java/cz/muni/fi/pa165/core/common/DomainService.java b/core/src/main/java/cz/muni/fi/pa165/core/common/DomainService.java index 1f3240a11a6c55677bb392bbfbd8efb36aca7571..30ebda5b9a4e25220d951d45211f1271fdb63496 100644 --- a/core/src/main/java/cz/muni/fi/pa165/core/common/DomainService.java +++ b/core/src/main/java/cz/muni/fi/pa165/core/common/DomainService.java @@ -85,6 +85,7 @@ public abstract class DomainService<T extends DomainObject> { .orElseThrow(() -> new EntityNotFoundException("Entity with '" + id + "' not found.")); } + /** * Deletes all entities by setting their deletion datetime. */ @@ -141,11 +142,12 @@ public abstract class DomainService<T extends DomainObject> { return entity; } + /** * Deletes all entities from the database without soft-delete functionality. */ @Transactional - public void deleteAllHardDelete() { + public void hardDeleteAll() { getRepository().deleteAll(); } } diff --git a/core/src/main/java/cz/muni/fi/pa165/core/company/CompanyRepository.java b/core/src/main/java/cz/muni/fi/pa165/core/company/CompanyRepository.java index 99baaa36e21fff186ecb7d05f4f569f149d5f6e8..dca89a1d00b885615edda650ce8b265fc96af1b8 100644 --- a/core/src/main/java/cz/muni/fi/pa165/core/company/CompanyRepository.java +++ b/core/src/main/java/cz/muni/fi/pa165/core/company/CompanyRepository.java @@ -3,5 +3,10 @@ package cz.muni.fi.pa165.core.company; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository -public interface CompanyRepository extends JpaRepository<Company, String> {} +public interface CompanyRepository extends JpaRepository<Company, String> { + + Optional<Company> findByName(String name); +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/company/CompanyService.java b/core/src/main/java/cz/muni/fi/pa165/core/company/CompanyService.java index 6c5215ccd986f73f8eb378d216fe944a6408e4dd..32f8a085d9a8ba479df4902af1574d5b00bf714d 100644 --- a/core/src/main/java/cz/muni/fi/pa165/core/company/CompanyService.java +++ b/core/src/main/java/cz/muni/fi/pa165/core/company/CompanyService.java @@ -1,9 +1,11 @@ package cz.muni.fi.pa165.core.company; import cz.muni.fi.pa165.core.common.DomainService; +import cz.muni.fi.pa165.core.user.User; import lombok.Getter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service public class CompanyService extends DomainService<Company> { @@ -14,5 +16,14 @@ public class CompanyService extends DomainService<Company> { public CompanyService(CompanyRepository companyRepository) { this.repository = companyRepository; } + + @Transactional(readOnly = true) + public Company findByName(String name) { + return getRepository() + .findByName(name) + .filter(entity -> entity.deletedDateTime == null) + .orElse(null); + } } + diff --git a/core/src/main/java/cz/muni/fi/pa165/core/helpers/exceptions/EntityConflictException.java b/core/src/main/java/cz/muni/fi/pa165/core/helpers/exceptions/EntityConflictException.java new file mode 100644 index 0000000000000000000000000000000000000000..3177af77277dad0751063786fbad3ad83dd9afde --- /dev/null +++ b/core/src/main/java/cz/muni/fi/pa165/core/helpers/exceptions/EntityConflictException.java @@ -0,0 +1,7 @@ +package cz.muni.fi.pa165.core.helpers.exceptions; + +public class EntityConflictException extends RuntimeException { + public EntityConflictException(String message) { + super(message); + } +} diff --git a/core/src/main/java/cz/muni/fi/pa165/core/user/User.java b/core/src/main/java/cz/muni/fi/pa165/core/user/User.java index ac0a4bcb96f6c8a18e38e1119031d202c88da333..f6988365d2572de01e8589affb93bfb75563c70e 100644 --- a/core/src/main/java/cz/muni/fi/pa165/core/user/User.java +++ b/core/src/main/java/cz/muni/fi/pa165/core/user/User.java @@ -35,6 +35,6 @@ public class User extends DomainObject { private String lastName; - @OneToMany(mappedBy = "user") + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL) private List<Role> rolesList; } diff --git a/core/src/main/java/cz/muni/fi/pa165/core/user/UserController.java b/core/src/main/java/cz/muni/fi/pa165/core/user/UserController.java index 1837b499d617e1bf8c6aec8868cb69f303f631dc..f16c69cba3cb1ac3c9a1c82e304948b4a343879d 100644 --- a/core/src/main/java/cz/muni/fi/pa165/core/user/UserController.java +++ b/core/src/main/java/cz/muni/fi/pa165/core/user/UserController.java @@ -1,7 +1,14 @@ package cz.muni.fi.pa165.core.user; -import cz.muni.fi.pa165.model.dto.user.*; +import cz.muni.fi.pa165.core.helpers.exceptions.EntityConflictException; import cz.muni.fi.pa165.model.dto.common.Result; +import cz.muni.fi.pa165.model.dto.user.ChangePasswordDto; +import cz.muni.fi.pa165.model.dto.user.LoginInfoDto; +import cz.muni.fi.pa165.model.dto.user.UserCreateDto; +import cz.muni.fi.pa165.model.dto.user.UserDto; +import cz.muni.fi.pa165.model.dto.user.UserStatisticsCreateDto; +import cz.muni.fi.pa165.model.dto.user.UserUpdateDto; +import jakarta.persistence.EntityNotFoundException; import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import io.swagger.v3.oas.annotations.Operation; @@ -22,9 +29,9 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.server.ResponseStatusException; -import java.util.List; + +import static java.util.Collections.singletonMap; @RestController @RequestMapping("/api/user") @@ -56,9 +63,14 @@ public class UserController { @ApiResponse(responseCode = "404", description = "User not found", content = @Content) }) @GetMapping("/{id}") - public UserDto findById( + public ResponseEntity<?> findById( @Parameter(description = "ID of user to be searched") @PathVariable String id) { - return userFacade.findById(id); + try { + return ResponseEntity.ok(userFacade.findById(id)); + } catch (EntityNotFoundException ex) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage()); + } + } @Operation( @@ -82,16 +94,14 @@ public class UserController { content = @Content) }) @PostMapping - public ResponseEntity<UserDto> create( + public ResponseEntity<?> create( @Parameter(description = "User to be created") @RequestBody UserCreateDto userCreateDto) { try { - ResponseEntity<UserDto> response = ResponseEntity.status(HttpStatus.CREATED).body(userFacade.create(userCreateDto)); - return response; - } catch (ResponseStatusException ex) { - if (ex.getStatusCode() == HttpStatus.CONFLICT) { - return ResponseEntity.status(HttpStatus.CONFLICT).build(); - } - throw new IllegalArgumentException("Possible error"); + return ResponseEntity.status(HttpStatus.CREATED).body(userFacade.create(userCreateDto)); + } catch (EntityConflictException ex) { + return ResponseEntity.status(HttpStatus.CONFLICT) + .body(ex.getMessage()); + } } @@ -122,12 +132,21 @@ public class UserController { @ApiResponse(responseCode = "200", description = "User updated successfully"), @ApiResponse(responseCode = "400", description = "Invalid input"), @ApiResponse(responseCode = "404", description = "User not found"), + @ApiResponse(responseCode = "409", description = "User (with username) already exists"), @ApiResponse(responseCode = "500", description = "Internal server error") }) - public UserDto updateById( + public ResponseEntity<?> updateById( @Parameter(description = "ID of the user to be updated") @PathVariable String id, @RequestBody @Valid UserUpdateDto userUpdateDto) { - return userFacade.updateById(userUpdateDto, id); + try { + return ResponseEntity.status(HttpStatus.OK).body(userFacade.updateById(userUpdateDto, id)); + } catch (EntityConflictException ex) { + return ResponseEntity.status(HttpStatus.CONFLICT) + .body(ex.getMessage()); + } catch (EntityNotFoundException ex) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(ex.getMessage()); + } } @DeleteMapping("/{id}") @@ -137,10 +156,15 @@ public class UserController { @ApiResponse(responseCode = "404", description = "User not found"), @ApiResponse(responseCode = "500", description = "Internal server error") }) - public UserDto deleteById( + public ResponseEntity<?> deleteById( @Parameter(description = "ID of the user to be deleted") @PathVariable String id) { + try { + return ResponseEntity.status(HttpStatus.OK).body(userFacade.deleteById(id)); + } catch (EntityNotFoundException ex) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(ex.getMessage()); - return userFacade.deleteById(id); + } } @PostMapping("/statistics") @@ -155,26 +179,6 @@ public class UserController { createStatDto.getStartTime(), createStatDto.getEndTime()); } - @Operation( - summary = "Get all users", - description = "Returns all users", - tags = {"user"}) - @ApiResponses( - value = { - @ApiResponse( - responseCode = "200", - description = "Users found", - content = { - @Content( - mediaType = "application/json", - schema = @Schema(implementation = Result.class)) - }) - }) - @GetMapping("/all") - public List<UserDto> findAll() { - return userFacade.findAll(); - } - @Operation(summary = "Log in a user", tags = {"user"}) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successful login"), @@ -195,9 +199,9 @@ public class UserController { return userFacade.logout(); } - @PutMapping("/password/{userId}") + @PutMapping("/password") @Operation(summary = "Change user password", - description = "Change the password for the specified user.", + description = "Change the password for the logged user.", tags = {"user"} ) @ApiResponses(value = { @@ -208,14 +212,10 @@ public class UserController { @ApiResponse(responseCode = "500", description = "Internal server error.", content = @Content) }) public ResponseEntity<?> changePassword( - @Parameter(description = "The ID of the user whose password will be changed.", required = true) - @PathVariable String userId, @Parameter(description = "The old and new password information.", required = true) @Valid @RequestBody ChangePasswordDto request) { - return userFacade.changePassword(request, userId); // userId from JWT token. + final String userId = "00000"; + return userFacade.changePassword(request, userId); // userId from JWT token or session cookie. } // TODO: get user with roles - -} - - +} \ No newline at end of file diff --git a/core/src/main/java/cz/muni/fi/pa165/core/user/UserFacade.java b/core/src/main/java/cz/muni/fi/pa165/core/user/UserFacade.java index a31277ef80b5d682a8b8b840312cac87e434cf75..7295bcc7e73e9cd49121ac755bc604e4ff79e192 100644 --- a/core/src/main/java/cz/muni/fi/pa165/core/user/UserFacade.java +++ b/core/src/main/java/cz/muni/fi/pa165/core/user/UserFacade.java @@ -1,11 +1,18 @@ package cz.muni.fi.pa165.core.user; import cz.muni.fi.pa165.core.common.DomainFacade; -import cz.muni.fi.pa165.model.dto.user.*; +import cz.muni.fi.pa165.core.helpers.exceptions.EntityConflictException; +import cz.muni.fi.pa165.model.dto.user.ChangePasswordDto; +import cz.muni.fi.pa165.model.dto.user.LoginInfoDto; +import cz.muni.fi.pa165.model.dto.user.UserCreateDto; +import cz.muni.fi.pa165.model.dto.user.UserDto; +import cz.muni.fi.pa165.model.dto.user.UserUpdateDto; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; +import java.util.Objects; + import static cz.muni.fi.pa165.core.user.utils.PasswordUtil.isPasswordValid; @@ -15,6 +22,7 @@ public class UserFacade extends DomainFacade<User, UserDto, UserCreateDto, UserU private final UserService userService; // For the "UserService" specific methods private final UserMapper userMapper; // For the "UserMapper" specific methods + @Autowired public UserFacade(UserService userService, UserMapper userMapper) { super(userService, userMapper); @@ -22,23 +30,59 @@ public class UserFacade extends DomainFacade<User, UserDto, UserCreateDto, UserU this.userMapper = userMapper; } + /** + * Returns the user with the given username. + * + * @param username the username of the user to retrieve. + * @return the user with the given username. + */ + public UserDto findByUsername(String username) { + return mapper.toDto(userService.findByUsername(username)); + } + + @Override + public UserDto create(UserCreateDto createDto) { + if (findByUsername(createDto.getUsername()) != null) { + throw new EntityConflictException("User with given username already exists"); + } + return super.create(createDto); + } + + @Override + public UserDto updateById(UserUpdateDto updateDto, String id) { + if (findByUsername(updateDto.getUsername()) != null) { + throw new EntityConflictException("User with given username already exists"); + } + return super.updateById(updateDto, id); + } + + // TODO: make logout method without ResponseEntity public ResponseEntity<?> logout() { // validate JWT token... return ResponseEntity.ok("Logged out successfully."); } + // TODO: make login method without ResponseEntity public ResponseEntity<?> login(LoginInfoDto request) { String username = request.getUsername(); String password = request.getPassword(); if (username.isBlank()) { - ResponseEntity.badRequest().body("Username is empty"); + // TODO: potential validation + return ResponseEntity.status(401).body("Username is empty."); } if (!isPasswordValid(password)) { - ResponseEntity.badRequest().body("password is not valid"); + return ResponseEntity.status(401).body("Password is not valid."); + } + UserDto userDto = findByUsername(username); + + if (Objects.isNull(userDto) || !userService.loginCheck(userDto.getId(), password)) { + return ResponseEntity.status(401).body("Invalid username or password."); } - return ResponseEntity.ok("ok!"); + + return ResponseEntity.ok("Login successful."); } + // TODO: make changePassword method without ResponseEntity public ResponseEntity<?> changePassword(ChangePasswordDto request, String userId){ String oldPassword = request.getOldPassword(); String newPassword = request.getNewPassword(); @@ -58,7 +102,8 @@ public class UserFacade extends DomainFacade<User, UserDto, UserCreateDto, UserU // TODO: Check if old password matches user's current password // TODO: Update user's password in the database - return ResponseEntity.ok("Password changed successfully."); } + + } diff --git a/core/src/main/java/cz/muni/fi/pa165/core/user/UserRepository.java b/core/src/main/java/cz/muni/fi/pa165/core/user/UserRepository.java index 2bd0ba41ef3b77fb3b8e6029851117a827a80d3a..ed8d6a55112a7fcdaad1030b4a62aa0fad6507c9 100644 --- a/core/src/main/java/cz/muni/fi/pa165/core/user/UserRepository.java +++ b/core/src/main/java/cz/muni/fi/pa165/core/user/UserRepository.java @@ -13,9 +13,9 @@ import java.util.Optional; @Repository public interface UserRepository extends JpaRepository<User, String> { - @Modifying - @Query("UPDATE User u set u.username= :#{#user.username}, u.password= :#{#user.password}, u.lastName= :#{#user.lastName}, u.firstName= :#{#user.firstName}, u.email= :#{#user.email} where u.id = :#{#id}") - int update(@Param("user") User user, @Param("id") String id); + // @Modifying + // @Query("UPDATE User u set u.username= :#{#user.username}, u.password= :#{#user.password}, u.lastName= :#{#user.lastName}, u.firstName= :#{#user.firstName}, u.email= :#{#user.email} where u.id = :#{#id}") + // int update(@Param("user") User user, @Param("id") String id); @Query("Select SUM(m.consumptionKWH) " + @@ -29,4 +29,7 @@ public interface UserRepository extends JpaRepository<User, String> { @Param("houseId") String houseId, @Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime); + + Optional<User> findByUsername(String username); + Optional<User> findByIdAndPassword(String id, String password); } diff --git a/core/src/main/java/cz/muni/fi/pa165/core/user/UserService.java b/core/src/main/java/cz/muni/fi/pa165/core/user/UserService.java index eb39e986567c7c51e761a12a14e11a4f73445585..a6c55162253a8df37f4833b43cf4bae2ead8472c 100644 --- a/core/src/main/java/cz/muni/fi/pa165/core/user/UserService.java +++ b/core/src/main/java/cz/muni/fi/pa165/core/user/UserService.java @@ -13,28 +13,37 @@ import java.util.List; @Service public class UserService extends DomainService<User> { - @Getter private final UserRepository repository; - - @Autowired - public UserService(UserRepository repository) { - this.repository = repository; - } - - @Transactional(readOnly = true) - public User update(User user, String id) { - int result = repository.update(user, id); - if (result != 1) { - throw new EntityNotFoundException("User '" + id + "' not found."); - } - return findById(id); - } - - @Transactional(readOnly = true) - public Double getConsumption(String userId, - String houseId, - LocalDateTime startTime, - LocalDateTime endTime) { - return repository.getStatisticsData(userId, houseId, startTime, endTime); - } - + @Getter + private final UserRepository repository; + + @Autowired + public UserService(UserRepository repository) { + this.repository = repository; + } + + + @Transactional(readOnly = true) + public User findByUsername(String username) { + return getRepository() + .findByUsername(username) + .filter(entity -> entity.deletedDateTime == null) + .orElse(null); + } + + @Transactional(readOnly = true) + public Double getConsumption(String userId, + String houseId, + LocalDateTime startTime, + LocalDateTime endTime) { + return repository.getStatisticsData(userId, houseId, startTime, endTime); + } + + // TODO: use salt + @Transactional(readOnly = true) + public boolean loginCheck(String userId, String password) { + return getRepository() + .findByIdAndPassword(userId, password) + .filter(entity -> entity.deletedDateTime == null) + .isPresent(); + } } diff --git a/core/src/main/resources/application.properties b/core/src/main/resources/application.properties index 070e8b6001edc3e74ec054d705c2ef0bf72f9340..83d4e4f6de291cb252a0ed6ef9e2873752fd1522 100644 --- a/core/src/main/resources/application.properties +++ b/core/src/main/resources/application.properties @@ -1,4 +1,4 @@ -##spring.h2.console.enabled=true +#spring.h2.console.enabled=true spring.datasource.url=jdbc:postgresql://localhost:5432/postgres spring.datasource.username=postgres spring.datasource.password=password diff --git a/core/src/test/java/cz/muni/fi/pa165/core/CoreApplicationTest.java b/core/src/test/java/cz/muni/fi/pa165/core/CoreApplicationTest.java index 4a5948aaf1349e50bdfa1d2eb23ba440f5c190ba..559893fc7dac30318606f7766ca48d1002913491 100644 --- a/core/src/test/java/cz/muni/fi/pa165/core/CoreApplicationTest.java +++ b/core/src/test/java/cz/muni/fi/pa165/core/CoreApplicationTest.java @@ -2,10 +2,11 @@ package cz.muni.fi.pa165.core; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; -@SpringBootTest +@SpringBootTest(classes = CoreApplication.class) +@TestPropertySource(locations = "classpath:application-test.properties") class CoreApplicationTest { - - /*@Test - void contextLoads() {}*/ + @Test + void contextLoads() {} } diff --git a/core/src/test/java/cz/muni/fi/pa165/core/Persistence.java b/core/src/test/java/cz/muni/fi/pa165/core/Persistence.java new file mode 100644 index 0000000000000000000000000000000000000000..bb6b8646e0e748e06bdd2fbe9098753dedafd854 --- /dev/null +++ b/core/src/test/java/cz/muni/fi/pa165/core/Persistence.java @@ -0,0 +1,12 @@ +package cz.muni.fi.pa165.core; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; + +public class Persistence { + private static final EntityManagerFactory factory = jakarta.persistence.Persistence.createEntityManagerFactory("jpa.persistence.core"); + + static EntityManager getEntityManager() { + return factory.createEntityManager(); + } +} diff --git a/core/src/test/java/cz/muni/fi/pa165/core/company/CompanyControllerTest.java b/core/src/test/java/cz/muni/fi/pa165/core/company/CompanyControllerTest.java index 87535c321b2d234f1217eaf4e9e1d4b5c2bcd6e7..0fdc20e53682bbfbb972abe22c049b9afc874b6c 100644 --- a/core/src/test/java/cz/muni/fi/pa165/core/company/CompanyControllerTest.java +++ b/core/src/test/java/cz/muni/fi/pa165/core/company/CompanyControllerTest.java @@ -1,49 +1,74 @@ package cz.muni.fi.pa165.core.company; import com.fasterxml.jackson.databind.ObjectMapper; -import cz.muni.fi.pa165.core.user.UserController; import cz.muni.fi.pa165.model.dto.company.CompanyCreateDto; import cz.muni.fi.pa165.model.dto.company.CompanyDto; -import cz.muni.fi.pa165.model.dto.user.UserCreateDto; -import cz.muni.fi.pa165.model.dto.user.UserDto; -import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.servlet.MockMvc; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** * Integration tests for the {@link CompanyController} class. + * @author xskacel */ @SpringBootTest @AutoConfigureMockMvc +@TestPropertySource(locations = "classpath:application-test.properties") public class CompanyControllerTest { @Autowired private MockMvc mockMvc; - @Autowired private ObjectMapper objectMapper; + @Autowired + private CompanyFacade companyFacade; + + @Autowired + private CompanyService companyService; + private final static String URL = "/api/company"; private final static String CONTENT_TYPE = "application/json"; + @BeforeEach + void setUp() { + CompanyCreateDto companyCreateDto1 = new CompanyCreateDto(); + companyCreateDto1.setName("company uno"); + companyFacade.create(companyCreateDto1); + + CompanyCreateDto companyCreateDto2 = new CompanyCreateDto(); + companyCreateDto2.setName("company duo"); + companyFacade.create(companyCreateDto2); + } + + @AfterEach + void cleanUp() { + companyService.hardDeleteAll(); + } + /** * Tests the {@link CompanyController#create(CompanyCreateDto)} method with valid input. * Expects the response status code to be 201 (Created). */ @Test - @DisplayName("Create company with valid input") void createNonExistingCompanyTest() throws Exception { // Prepare CompanyCreateDto createDto = new CompanyCreateDto(); createDto.setName("superDuperCompany"); + // Testing if created user is in Repository (should not be) + assertNull(companyService.findByName(createDto.getName())); + // Execute String response = mockMvc.perform(post(URL) .contentType(CONTENT_TYPE) diff --git a/core/src/test/java/cz/muni/fi/pa165/core/user/UserControllerTest.java b/core/src/test/java/cz/muni/fi/pa165/core/user/UserControllerTest.java index 751cab2a39bff9274c4239364df1ddb9da039aab..65b58be7ca2b1e537623baf4562d4478c6ad1ccb 100644 --- a/core/src/test/java/cz/muni/fi/pa165/core/user/UserControllerTest.java +++ b/core/src/test/java/cz/muni/fi/pa165/core/user/UserControllerTest.java @@ -1,42 +1,84 @@ package cz.muni.fi.pa165.core.user; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; + +import cz.muni.fi.pa165.model.dto.common.Result; +import cz.muni.fi.pa165.model.dto.user.ChangePasswordDto; +import cz.muni.fi.pa165.model.dto.user.LoginInfoDto; import cz.muni.fi.pa165.model.dto.user.UserCreateDto; import cz.muni.fi.pa165.model.dto.user.UserDto; -import org.junit.jupiter.api.DisplayName; +import cz.muni.fi.pa165.model.dto.user.UserUpdateDto; +import jakarta.persistence.EntityNotFoundException; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.servlet.MockMvc; +import java.util.List; + import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** * Integration tests for the {@link UserController} class. + * + * @author xskacel */ @SpringBootTest @AutoConfigureMockMvc +@TestPropertySource(locations = "classpath:application-test.properties") public class UserControllerTest { - @Autowired private MockMvc mockMvc; - @Autowired private ObjectMapper objectMapper; - + @Autowired + private UserFacade userFacade; + @Autowired + private UserService userService; private final static String URL = "/api/user"; - private final static String CONTENT_TYPE = "application/json"; + private final static String CONTENT_TYPE = "application/json"; + + @BeforeEach + void setUp() { + UserCreateDto createDto1 = new UserCreateDto(); + createDto1.setUsername("skaci"); + createDto1.setPassword("testPassword"); + createDto1.setFirstName("Marek"); + createDto1.setLastName("Bogo"); + createDto1.setEmail("Bozo@test.com"); + userFacade.create(createDto1); + + UserCreateDto createDto2 = new UserCreateDto(); + createDto2.setUsername("paci"); + createDto2.setPassword("asdasd"); + createDto2.setFirstName("eeerrs"); + createDto2.setLastName("lslsls"); + createDto2.setEmail("aeeradfa@cdscsd.cz"); + userFacade.create(createDto2); + } + + @AfterEach + void cleanUp() { + userService.hardDeleteAll(); + } + /** * Tests the {@link UserController#create(UserCreateDto)} method with valid input. - * Expects the response status code to be 201 (Created). + * Expects the response status code to be <201> (Created). */ @Test - @DisplayName("Create user with valid input") - void createNonExistingUserTest() throws Exception { + void shouldCreateValidUser() throws Exception { // Prepare UserCreateDto createDto = new UserCreateDto(); createDto.setUsername("testUser"); @@ -45,6 +87,9 @@ public class UserControllerTest { createDto.setLastName("Doe"); createDto.setEmail("johndoe@test.com"); + // Testing if created user is in Repository (should not be) + assertNull(userService.findByUsername(createDto.getUsername())); + // Execute String response = mockMvc.perform(post(URL) .contentType(CONTENT_TYPE) @@ -61,5 +106,353 @@ public class UserControllerTest { assertThat(userDto.getEmail()).isEqualTo(createDto.getEmail()); assertThat(userDto.getFirstName()).isEqualTo(createDto.getFirstName()); assertThat(userDto.getLastName()).isEqualTo(createDto.getLastName()); + assertDoesNotThrow(() -> userService.findById(userDto.getId())); + } + + /** + * Tests attempting to create a user with a username that already exists. + * Expects the response status code to be <409> (Conflict). + */ + @Test + void shouldNotCreateUserThatAlreadyExists() throws Exception { + // Prepare + UserCreateDto createDto = new UserCreateDto(); + createDto.setUsername("skaci"); + createDto.setPassword("testPassword"); + createDto.setFirstName("John"); + createDto.setLastName("Doe"); + createDto.setEmail("johndoe@test.com"); + assertNotNull(userService.findByUsername(createDto.getUsername())); + mockMvc.perform(post(URL) + .contentType(CONTENT_TYPE) + .content(objectMapper.writeValueAsString(createDto))) + .andExpect(status().isConflict()) + .andExpect(content().string("User with given username already exists")); + + assertNotNull(userService.findByUsername(createDto.getUsername())); + } + + /** + * Tests the deletion of an existing user. + * Expects the response status code to be <200>. + */ + @Test + void shouldDeleteExistingUser() throws Exception { + List<UserDto> userDtoList = userFacade.findAll(); + assertTrue(userDtoList.size() > 0); + UserDto userToDelete = userDtoList.get(0); + assertDoesNotThrow(() -> userService.findById(userToDelete.getId())); + + // Execute + String response = mockMvc.perform(delete( + URL + "/{id}", userToDelete.getId()) + .contentType(CONTENT_TYPE)) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + // Verify + UserDto userDto = objectMapper.readValue(response, UserDto.class); + assertThat(userDto.getId()).isEqualTo(userToDelete.getId()); + assertThat(userDto.getUsername()).isEqualTo(userToDelete.getUsername()); + assertThat(userDto.getEmail()).isEqualTo(userToDelete.getEmail()); + assertThat(userDto.getFirstName()).isEqualTo(userToDelete.getFirstName()); + assertThat(userDto.getLastName()).isEqualTo(userToDelete.getLastName()); + assertThrows(EntityNotFoundException.class, () -> userService.findById(userToDelete.getId())); + } + + /** + * Tests the deletion of an existing user. + * Expects the response status code to be <200>. + */ + @Test + void shouldNotDeleteUserThatDoesNotExists() throws Exception { + // nonsense ID + final String id = "0000xxxx1111"; + assertThrows(EntityNotFoundException.class, () -> userService.findById(id)); + + // Execute + mockMvc.perform(delete( + URL + "/{id}", id) + .contentType(CONTENT_TYPE)) + .andExpect(status().isNotFound()) + .andExpect(content().string("Entity with '" + id + "' not found.")); + } + + /** + * Tests the {@link UserController#updateById(String, UserUpdateDto)} method with valid input. + * Expects the response status code to be <200>. + */ + @Test + void shouldUpdateValidUser() throws Exception { + // Prepare + List<UserDto> userDtoList = userFacade.findAll(); + assertTrue(userDtoList.size() > 0); + UserDto userToUpdate = userDtoList.get(0); + + UserUpdateDto userUpdateDto = new UserUpdateDto(); + userUpdateDto.setUsername("newSkaci"); + userUpdateDto.setPassword("password"); + userUpdateDto.setFirstName("Goku"); + userUpdateDto.setLastName("KidBuu"); + userUpdateDto.setEmail("goku@test.com"); + + // Testing if updated user is in Repository (should not be) + assertNull(userService.findByUsername(userUpdateDto.getUsername())); + assertNotNull(userService.findByUsername(userToUpdate.getUsername())); + + // Execute + String response = mockMvc.perform(put(URL + "/{id}", userToUpdate.getId()) + .contentType(CONTENT_TYPE) + .content(objectMapper.writeValueAsString(userUpdateDto))) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + // Verify + UserDto userDto = objectMapper.readValue(response, UserDto.class); + assertThat(userDto.getId()).isEqualTo(userToUpdate.getId()); + assertThat(userDto.getUsername()).isEqualTo(userUpdateDto.getUsername()); + assertThat(userDto.getEmail()).isEqualTo(userUpdateDto.getEmail()); + assertThat(userDto.getFirstName()).isEqualTo(userUpdateDto.getFirstName()); + assertThat(userDto.getLastName()).isEqualTo(userUpdateDto.getLastName()); + + assertDoesNotThrow(() -> userService.findById(userDto.getId())); + + assertNotNull(userService.findByUsername(userUpdateDto.getUsername())); + assertNull(userService.findByUsername(userToUpdate.getUsername())); + assertEquals(userService.findAll().size(), userDtoList.size()); + } + + /** + * Tests the {@link UserController#updateById(String, UserUpdateDto)} method with valid input + * But with username that already exists in the database. + * Expects the response status code to be <409>. + */ + @Test + void shouldNotUpdateUserUsernameThatAlreadyExists() throws Exception { + List<UserDto> userDtoList = userFacade.findAll(); + assertTrue(userDtoList.size() > 0); + UserDto userToUpdate = userDtoList.get(0); + + UserUpdateDto userUpdateDto = new UserUpdateDto(); + userUpdateDto.setUsername("paci"); + userUpdateDto.setPassword("password"); + userUpdateDto.setFirstName("Goku"); + userUpdateDto.setLastName("KidBuu"); + userUpdateDto.setEmail("goku@test.com"); + + // Testing if updated user is in Repository (in this case it should) + assertNotNull(userService.findByUsername(userUpdateDto.getUsername())); + assertNotNull(userService.findByUsername(userToUpdate.getUsername())); + + // Execute + mockMvc.perform(put(URL + "/{id}", userToUpdate.getId()) + .contentType(CONTENT_TYPE) + .content(objectMapper.writeValueAsString(userUpdateDto))) + .andExpect(status().isConflict()) + .andExpect(content().string("User with given username already exists")); + + // Verify (nothing changed) + assertDoesNotThrow(() -> userService.findById(userToUpdate.getId())); + UserDto userDto = userFacade.findById(userToUpdate.getId()); + assertThat(userDto.getId()).isEqualTo(userToUpdate.getId()); + assertThat(userDto.getUsername()).isEqualTo(userToUpdate.getUsername()); + assertThat(userDto.getEmail()).isEqualTo(userToUpdate.getEmail()); + assertThat(userDto.getFirstName()).isEqualTo(userToUpdate.getFirstName()); + assertThat(userDto.getLastName()).isEqualTo(userToUpdate.getLastName()); + + assertNotNull(userService.findByUsername(userUpdateDto.getUsername())); + assertNotNull(userService.findByUsername(userToUpdate.getUsername())); + assertEquals(userService.findAll().size(), userDtoList.size()); + } + + /** + * Tests the {@link UserController#updateById(String, UserUpdateDto)} with ID + * that does not exist in the db. + * Expects the response status code to be <409>. + */ + @Test + void shouldNotUpdateUserThatDoesNotExist() throws Exception { + // nonsense ID + final String id = "0000xxxx1111"; + assertThrows(EntityNotFoundException.class, () -> userService.findById(id)); + + UserUpdateDto userUpdateDto = new UserUpdateDto(); + userUpdateDto.setUsername("ooo"); + userUpdateDto.setPassword("Password"); + userUpdateDto.setFirstName("Goku"); + userUpdateDto.setLastName("KidBuu"); + userUpdateDto.setEmail("goku@test.com"); + + // Execute + mockMvc.perform(put(URL + "/{id}", id) + .contentType(CONTENT_TYPE) + .content(objectMapper.writeValueAsString(userUpdateDto))) + .andExpect(status().isNotFound()) + .andExpect(content().string("Entity with '" + id + "' not found.")); + + } + + @Test + void shouldLogout() throws Exception { + // Execute + mockMvc.perform(post(URL + "/logout") + .contentType(CONTENT_TYPE)) + .andExpect(status().isOk()) + .andExpect(content().string("Logged out successfully.")); + } + + @Test + void shouldLoginSuccessfully() throws Exception { + final LoginInfoDto loginInfoDto = new LoginInfoDto(); + loginInfoDto.setUsername("skaci"); + loginInfoDto.setPassword("testPassword"); + + + // Execute + mockMvc.perform(post(URL + "/login") + .contentType(CONTENT_TYPE) + .content(objectMapper.writeValueAsString(loginInfoDto))) + .andExpect(status().isOk()) + .andExpect(content().string("Login successful.")); + } + + + @Test + void shouldNotLoginSuccessfullyBecauseOfBadPassword() throws Exception { + final LoginInfoDto loginInfoDto = new LoginInfoDto(); + loginInfoDto.setUsername("skaci"); + loginInfoDto.setPassword("wrongPassword"); + + // Execute + mockMvc.perform(post(URL + "/login") + .contentType(CONTENT_TYPE) + .content(objectMapper.writeValueAsString(loginInfoDto))) + .andExpect(status().isUnauthorized()) + .andExpect(content().string("Invalid username or password.")); + } + + @Test + void shouldNotLoginSuccessfullyBecauseOfBadUsername() throws Exception { + final LoginInfoDto loginInfoDto = new LoginInfoDto(); + loginInfoDto.setUsername("blabla"); + loginInfoDto.setPassword("testPassword"); + + // Execute + mockMvc.perform(post(URL + "/login") + .contentType(CONTENT_TYPE) + .content(objectMapper.writeValueAsString(loginInfoDto))) + .andExpect(status().isUnauthorized()) + .andExpect(content().string("Invalid username or password.")); + } + + @Test + void shouldChangePassword() throws Exception { + final ChangePasswordDto changePasswordDto = new ChangePasswordDto(); + changePasswordDto.setOldPassword("SomeIrrelevantDataAtm"); + changePasswordDto.setNewPassword("blablaBla"); + changePasswordDto.setNewPasswordConfirmation("blablaBla"); + + // Execute + mockMvc.perform(put(URL + "/password") + .contentType(CONTENT_TYPE) + .content(objectMapper.writeValueAsString(changePasswordDto))) + .andExpect(status().isOk()) + .andExpect(content().string("Password changed successfully.")); + } + + @Test + void shouldNotChangePasswordSincePasswordsDoNotEqual() throws Exception { + final ChangePasswordDto changePasswordDto = new ChangePasswordDto(); + changePasswordDto.setOldPassword("SomeIrrelevantDataAtm"); + changePasswordDto.setNewPassword("onePassword"); + changePasswordDto.setNewPasswordConfirmation("secondPassword"); + + // Execute + mockMvc.perform(put(URL + "/password") + .contentType(CONTENT_TYPE) + .content(objectMapper.writeValueAsString(changePasswordDto))) + .andExpect(status().isBadRequest()) + .andExpect(content().string("New password and confirmation password do not match.")); + } + + @Test + void shouldFindValidUserById() throws Exception { + List<UserDto> userDtoList = userFacade.findAll(); + assertTrue(userDtoList.size() > 0); + UserDto userToFind = userDtoList.get(0); + assertDoesNotThrow(() -> userService.findById(userToFind.getId())); + + // Execute + String response = mockMvc.perform(get(URL + "/{id}", userToFind.getId()) + .contentType(CONTENT_TYPE)) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + // Verify + UserDto userDto = objectMapper.readValue(response, UserDto.class); + assertThat(userDto.getId()).isEqualTo(userToFind.getId()); + assertThat(userDto.getUsername()).isEqualTo(userToFind.getUsername()); + assertThat(userDto.getEmail()).isEqualTo(userToFind.getEmail()); + assertThat(userDto.getFirstName()).isEqualTo(userToFind.getFirstName()); + assertThat(userDto.getLastName()).isEqualTo(userToFind.getLastName()); + } + + @Test + void shouldNotFindUserByNonExistingId() throws Exception { + // nonsense ID + final String id = "0000xxxx1111"; + assertThrows(EntityNotFoundException.class, () -> userService.findById(id)); + + // Execute + mockMvc.perform(get(URL + "/{id}", id) + .contentType(CONTENT_TYPE)) + .andExpect(status().isNotFound()) + .andExpect(content().string("Entity with '" + id + "' not found.")); + } + + @Test + void shouldFindAllUsers() throws Exception { + // Execute + String response = mockMvc.perform(get(URL + "?page=0") + .contentType(CONTENT_TYPE)) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + Result<UserDto> userDtoResult = objectMapper.readValue(response, new TypeReference<>() { + }); + assertThat(userDtoResult.getPage()).isEqualTo(0); + assertThat(userDtoResult.getPageSize()).isEqualTo(10); + assertThat(userDtoResult.getTotal()).isEqualTo(2); + + final UserDto firstUserFromResponse = userDtoResult.getItems().get(0); + UserDto userDto1 = new UserDto(); + userDto1.setUsername("skaci"); + userDto1.setFirstName("Marek"); + userDto1.setLastName("Bogo"); + userDto1.setEmail("Bozo@test.com"); + assertThat(firstUserFromResponse.getUsername()).isEqualTo(userDto1.getUsername()); + assertThat(firstUserFromResponse.getFirstName()).isEqualTo(userDto1.getFirstName()); + assertThat(firstUserFromResponse.getLastName()).isEqualTo(userDto1.getLastName()); + assertThat(firstUserFromResponse.getEmail()).isEqualTo(userDto1.getEmail()); + + final UserDto secondUserFromResponse = userDtoResult.getItems().get(1); + UserDto userDto2 = new UserDto(); + userDto2.setUsername("paci"); + userDto2.setFirstName("eeerrs"); + userDto2.setLastName("lslsls"); + userDto2.setEmail("aeeradfa@cdscsd.cz"); + + assertThat(secondUserFromResponse.getUsername()).isEqualTo(userDto2.getUsername()); + assertThat(secondUserFromResponse.getFirstName()).isEqualTo(userDto2.getFirstName()); + assertThat(secondUserFromResponse.getLastName()).isEqualTo(userDto2.getLastName()); + assertThat(secondUserFromResponse.getEmail()).isEqualTo(userDto2.getEmail()); } } diff --git a/core/src/test/resources/META-INF/persistence.xml b/core/src/test/resources/META-INF/persistence.xml new file mode 100644 index 0000000000000000000000000000000000000000..08a6c01acca876f435dd4903e940da167de6764a --- /dev/null +++ b/core/src/test/resources/META-INF/persistence.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence https://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"> + <persistence-unit name="jpa.persistence.core" transaction-type="RESOURCE_LOCAL"> + <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider> + <class>cz.muni.fi.pa165.core.user.User</class> + <properties> + <property name="hibernate.hbm2ddl.auto" value="update" /> <!-- create / create-drop / update --> + <property name="hibernate.show_sql" value="true" /> <!-- Show SQL in console --> + <property name="hibernate.format_sql" value="true" /> <!-- Show SQL formatted --> + </properties> + </persistence-unit> +</persistence> diff --git a/core/src/test/resources/application-test.properties b/core/src/test/resources/application-test.properties new file mode 100644 index 0000000000000000000000000000000000000000..d7f9587c9f3398fcf71fe9ab4619dd07779a71b2 --- /dev/null +++ b/core/src/test/resources/application-test.properties @@ -0,0 +1,7 @@ +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_ON_EXIT=FALSE +spring.datasource.username=sa +spring.datasource.password= + +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.hibernate.show-sql=true diff --git a/core/src/test/resources/logback.xml b/core/src/test/resources/logback.xml new file mode 100644 index 0000000000000000000000000000000000000000..e59e95552e8d5a89bab45a9aff80b060c4067b32 --- /dev/null +++ b/core/src/test/resources/logback.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<configuration> + + <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> + <encoder> + <pattern>%d %5p %40.40c:%4L - %m%n</pattern> + </encoder> + </appender> + + <root level="info"> + <appender-ref ref="console"/> + </root> + + <logger name="org.hibernate.SQL" level="DEBUG"/> + +</configuration> \ No newline at end of file